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.
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_TYPEenv).
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-moduleCMakeLists.txt(except where an assignment explicitly grants ownership).
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.
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: parseEGL_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 realegl*(its EGLImpl handles Win32 itself; PAL window HWND is passed asEGLNativeWindowType).eglCreateWindowSurface-> the backend's PAL window (AndroidANativeWindow*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 toglFenceSync/glClientWaitSync/glDeleteSyncwhen resolvable, else succeed-as-noop.eglCreateImageKHR/eglDestroyImageKHR/eglBindTexImage/eglSwapBuffersWithDamage*-> log + report unhandled (adapter returns false; controller logs a warning and continues).
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.
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.
- 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_CreateWindowerrors). - 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_retracelinks 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(+gtestviaGTest::gtest_main).
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.
- 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 supportsGL_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/glGetActiveUniformBlockivare guarded inMobileGLBackend::GetProcAddressbecause 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.dllis MobileGL's own drop-in shim (a MobileGL build byproduct). Any by-name load ofopengl32.dllfrom abin/-resident exe binds the shim instead of the system GL;mgdb::palpre-empts this by pinning the real system opengl32.dll at DLL load (PinSystemOpenGl32insrc/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.
{ "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" } }