Skip to content

Latest commit

 

History

History
200 lines (174 loc) · 12.4 KB

ARCHITECTURE.md

File metadata and controls

200 lines (174 loc) · 12.4 KB

MobileGLDebugger — Architecture

A RenderDoc-style debugger for MobileGL retrace files (apitrace v6 .trace, or .tgz fixtures: a gzipped tar whose trace.trace member may itself be container-compressed, e.g. brotli). Windows-first (MSVC/Clang, CMake + Ninja, Git Bash), C++20.

Components

app: mgdb_ui.exe            ImGui (docking) + SDL3 + OpenGL3. Talks ONLY to mgdb_retrace.dll
                            through the C ABI in include/mgdb/mgdb_api.h.
dll: mgdb_retrace.dll       Trace parsing + replay engine + GL state tracker + backends.
                            Statically links MobileGL_s for the MobileGL backends.
internal object libs:
  mgdb_trace                apitrace v6 parser + model + container decompression.
  mgdb_dispatch             Generated GL call invoker (marshals trace::Call -> real GL),
                            EGL call routing, call classification.
  mgdb_pal                  Platform abstraction on SDL3 (window + GL context on every
                            desktop platform; windowing/context creation is 100% SDL3 —
                            the only OS-native bit is an opengl32.dll GetProcAddress
                            fallback for GL 1.1 entry points SDL cannot see).
  mgdb_backend              IBackend implementations: NativeGL, MobileGL Espryt/Magma.

Reference (READ-ONLY, never modify): MobileGL repo, default path C:/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL (cache var MOBILEGL_ROOT). Useful references inside it:

  • Trace format spec: 3rdparty/apitrace/docs/FORMAT.markdown
  • Format constants: 3rdparty/apitrace/lib/trace/trace_format.hpp
  • Parsers/containers: 3rdparty/apitrace/lib/trace/trace_parser.cpp, trace_file_{snappy,brotli,zlib,zstd}.cpp
  • GL specs for codegen: 3rdparty/apitrace/specs/*.py
  • Backend table/state: MobileGL/MG_Backend/BackendObject.h, MobileGL/MG_Backend/BackendObjects.h, MobileGL/MG_Backend/Init.cpp, MobileGL/MG_State/GLState/Core.h
  • Espryt = BackendType::DirectGLES, Magma = BackendType::DirectVulkan (MOBILEGL_BACKEND_TYPE env).

Fixed contracts (do NOT edit these files)

  • include/mgdb/mgdb_api.h — C ABI between UI and DLL.
  • src/trace/include/mgdb/trace/Model.h, .../Parser.h — trace model + parser.
  • src/trace/src/containers/ContainerReader.h — container decompression interface.
  • src/dispatch/include/mgdb/dispatch/Invoker.h, .../TraceArgs.h — invoker + EGL arg helpers.
  • src/pal/include/mgdb/pal/Pal.h — platform abstraction.
  • src/backend/include/mgdb/backend/Backend.h — backend interface + registry.
  • src/replay/include/mgdb/replay/ReplayController.h, .../StateTracker.h — engine internals.
  • src/ui/src/UiContext.h, src/ui/src/panels/Panels.h — UI panel seam.
  • Top-level CMakeLists.txt, cmake/Deps.cmake, per-module CMakeLists.txt (except where an assignment explicitly grants ownership).

Generated-code contract (dispatch)

src/dispatch/generated/GlProcs.h (produced by src/dispatch/tools/gen_dispatch.py) must provide:

namespace mgdb::dispatch {
struct GlProcs { /* one typed PFN member per GLES2/3 entry point */ };
void LoadGlProcs(GlProcs& out, GetProcFn getProc, void* user);
// Executes a gl* call via resolved pointers. Returns false when the function is unknown
// to the generator (err set). Never throws.
bool InvokeGl(const GlProcs& procs, const mgdb::trace::Call& call, std::string& err);
extern const char* const kDrawCallNames[];        // nullptr-terminated
extern const char* const kFrameBoundaryNames[];   // nullptr-terminated
}

Marshaling rules: SInt/UInt/Enum/Bitmask -> integer casts; Opaque/UInt pointer args (glVertexAttribPointer, glMapBuffer*, glDrawElements indices) -> reinterpret of the numeric value (VBO-offset semantics, apitrace-style); Blob -> data() pointer or nullptr when empty; Array -> for glShaderSource an array of strings; missing trailing args -> defaults. glGet*/glIs*/glMapBufferRange are called through (harmless; keeps driver semantics real).

Draw-call names (minimum): glDrawArrays glDrawElements glDrawRangeElements glDrawArraysInstanced glDrawElementsInstanced glDrawElementsBaseVertex glDrawArraysInstancedBaseInstance glDrawElementsInstancedBaseVertex glDrawElementsInstancedBaseInstance glDrawElementsInstancedBaseVertexBaseInstance glDrawArraysIndirect glDrawElementsIndirect glMultiDrawArrays glMultiDrawElements glDispatchCompute glDispatchComputeIndirect glClear glClearBufferiv glClearBufferuiv glClearBufferfv glClearBufferfi glBlitFramebuffer. Frame-boundary names: eglSwapBuffers eglSwapBuffersWithDamageKHR eglSwapBuffersWithDamageEXT.

EGL adaptation (platform abstraction for cross-platform fixtures)

Fixtures may be captured on Android (EGL + native Android handles). During replay, ALL egl* calls route through mgdb::dispatch::IEglAdapter — never to a real EGL. The adapter maps the trace's platform calls onto the host platform:

  • Handles (EGLDisplay/Config/Context/Surface) are synthetic uint32 ids allocated by the adapter; trace handle values are only keys into an adapter-side map.
  • eglGetDisplay/Initialize/ChooseConfig/GetConfigAttrib/QueryString/BindAPI/GetError -> succeed trivially (return values are ignored by replay).
  • eglCreateContext: parse EGL_CONTEXT_CLIENT_VERSION / EGL_CONTEXT_(MAJOR|MINOR)_VERSION_KHR; NativeGL backend goes through the PAL (pal::CreateGLContext, SDL3 underneath): GLES requested -> try an ES-profile context (SDL_GL_CONTEXT_PROFILE_ES), else fall back to desktop compat GL with ES-alias fixes; MobileGL backend forwards to MobileGL's real egl* (its EGLImpl handles Win32 itself; PAL window HWND is passed as EGLNativeWindowType).
  • eglCreateWindowSurface -> the backend's PAL window (Android ANativeWindow* in trace is discarded).
  • eglCreatePbufferSurface -> hidden PAL window of requested size.
  • eglMakeCurrent/eglSwapBuffers/eglSwapInterval -> pal context ops; SwapBuffers also = frame boundary.
  • eglDestroyContext/Surface -> release synthetic handle.
  • eglCreateSyncKHR/ClientWaitSyncKHR/DestroySyncKHR -> map to glFenceSync/glClientWaitSync/glDeleteSync when resolvable, else succeed-as-noop.
  • eglCreateImageKHR/eglDestroyImageKHR/eglBindTexImage/eglSwapBuffersWithDamage* -> log + report unhandled (adapter returns false; controller logs a warning and continues).

Backend switching semantics

mgdb_set_backend / seeking backwards both tear down the backend (destroy GL context + window), re-create it, and re-execute calls [0, targetEid] — RenderDoc-style replay-to-EID, so state is always correct after a switch. MobileGL Espryt<->Magma switching: set MOBILEGL_BACKEND_TYPE before first MobileGL init; afterwards re-init MobileGL backend internals (MG_Config::ActiveBackendType, MG_Backend::pActiveBackendObject, gBackendFunctionsTable) best-effort; correctness is guaranteed by the full replay that follows.

Backend-call log (API Inspector column 2)

For MobileGL backends the backend wraps every non-null slot of MG_Backend::gBackendFunctionsTable.GL with a logging thunk that appends {eid, slotName} to a log; IBackend::BeginCall(eid) sets the current eid, DrainBackendCalls(eid) collects entries. NativeGL always returns an empty list.

State JSON schema (mgdb_state_json)

{
  "eid": 123,
  "backend": { "kind": "MobileGLEspryt", "isMobileGL": true, "renderer": "Espryt", "version": "..." },
  "current": { "program": 5, "vao": 1, "drawFbo": 0, "readFbo": 0, "activeTextureUnit": 0 },
  "pipeline": { "blend": true, "blendFuncSrcRGB": "GL_SRC_ALPHA", "...": "render-state fields as strings/bools/numbers" },
  "textures": [ { "name": 1, "target": "GL_TEXTURE_2D", "width": 854, "height": 480,
                  "internalFormat": "GL_RGBA8", "levels": 1 } ],
  "textureBindings": [ { "unit": 0, "target": "GL_TEXTURE_2D", "texture": 3, "sampler": 0 } ],
  "images": [ { "unit": 0, "texture": 4, "level": 0, "access": "GL_READ_WRITE" } ],
  "framebuffers": [ { "name": 0, "attachments": [ { "attachment": "GL_COLOR_ATTACHMENT0",
      "type": "texture", "texture": 2, "level": 0, "width": 854, "height": 480 } ] } ],
  "programs": [ { "name": 5, "shaders": [ { "name": 6, "type": "GL_VERTEX_SHADER",
      "source": "..." } ], "linked": true } ],
  "buffers": [ { "name": 2, "size": 4096, "boundTargets": ["GL_ARRAY_BUFFER"] } ],
  "vertexArrays": [ { "name": 1, "attribs": [ { "index": 0, "enabled": true, "size": 3,
      "type": "GL_FLOAT", "buffer": 2 } ] } ],
  "mobilegl": { "...": "internal MobileGL state, only for MobileGL backends" }
}

Conventions

  • Namespace mgdb::<module>; no exceptions across the C ABI; errors via out-params / mgdb_last_error.
  • Engine thread affinity: every GL-/window-touching C-API call (open/close/seek/ set_backend/state_json/read_*/get_backend_info) is marshaled onto one dedicated engine thread inside the DLL (src/replay/src/EngineThread.h). Win32 windows can only be destroyed on their creating thread and WGL contexts are single-thread-current; the UI calls the API from several threads, so without marshaling, replay windows leak and every later backend re-init fails (SDL_RegisterApp / SDL_CreateWindow errors).
  • All file I/O UTF-8. Never write outside the repo. The MobileGL repo is read-only reference.
  • Every module is an OBJECT library; mgdb_retrace links them all (guarantees self-registration static initializers are kept).
  • Build: cmake -S . -B build-<name> -G Ninja -DCMAKE_BUILD_TYPE=Debug && cmake --build build-<name> --target <t>. Use your own build dir (parallel agents must not share one).
  • Dep targets from cmake/Deps.cmake: mgdb::zlib mgdb::snappy mgdb::brotli mgdb::zstd SDL3::SDL3 imgui nlohmann_json::nlohmann_json mgdb_khr_headers (+ gtest via GTest::gtest_main).

UI GL rule (hard requirement)

mgdb_ui.exe must NEVER call raw gl* symbols. mgdb_retrace.dll statically links MobileGL (a full GLES implementation) and re-exports its gl*, so a linked glViewport() in the UI would bind to MobileGL — with no MobileGL context on the UI thread, that crashes. UI-side GL must go through imgl3w (imgui_impl_opengl3_loader.h, which the imgui OpenGL3 backend already initializes) or be resolved manually via SDL_GL_GetProcAddress. See src/ui/src/App.cpp and src/ui/src/panels/TextureViewerPanel.cpp.

Known issues (v1)

  • Engine operations (open/seek/backend-switch) run on a UI worker thread with a progress overlay (mgdb_set_progress_callback); there is no job cancellation yet — quitting mid-replay waits for the in-flight job. Small forward seeks (<= 10k calls) still run synchronously on the UI thread for click responsiveness.
  • Texture readback (src/replay/src/Readback.cpp) only supports GL_TEXTURE_2D; cube maps / 3D / array textures report an error.
  • Espryt<->Magma switching is a full MobileGL teardown + re-init + replay-from-0 — correct, but not incremental (see "Backend switching semantics").
  • No GL object-name remapping: replayed glCreate*/glGen* return values use MobileGL's freshly allocated names while subsequent calls use the recorded names. This matches on full traces (identical allocation history) but diverges on trimmed fixtures, whose pre-trim allocations are lost; rendering on such fixtures is best-effort (MobileGL records errors for unknown names; glUniformBlockBinding / glGetActiveUniformBlockiv are guarded in MobileGLBackend::GetProcAddress because MobileGL dereferences its null "no such program" reference there). A recorded<->actual name map belongs in the dispatch layer.
  • The dispatch generator does not know the GLES 3.2 DSA entry points (glCreateBuffers, glNamedBufferStorage, glMapNamedBufferRange, glCreateFramebuffers, glNamedFramebufferTexture, ...); replay logs a warning and continues, so fixture replays that rely on DSA calls lose the corresponding state.
  • bin/opengl32.dll is MobileGL's own drop-in shim (a MobileGL build byproduct). Any by-name load of opengl32.dll from a bin/-resident exe binds the shim instead of the system GL; mgdb::pal pre-empts this by pinning the real system opengl32.dll at DLL load (PinSystemOpenGl32 in src/pal/src/Pal_SDL.cpp). Removing the shim from the deploy output (cmake) would make the pin unnecessary.
  • MobileGL's GL state is effectively shared across its EGL contexts (no per-context state swap on eglMakeCurrent); the adapter's initial-viewport fixup (ApplyInitialViewportIfMissing) therefore only applies while the viewport is zero-sized.