From df7d1edeca003fdcb0ffeff8110646ff7e421041 Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Sat, 1 Aug 2026 11:51:19 -0400 Subject: [PATCH] [Feat] (DirectGLES): implement transform feedback capture Transform feedback was frontend-only on DirectGLES: glBeginTransformFeedback just flipped MobileGL's own capture state and the real ES driver was never told to capture anything, so every capture buffer read back as whatever it held before the draw (zeros for a fresh glBufferData(NULL)). DirectVulkan drives its capture from its own draw recording, so the shared GLFunctionsTable had no entries for the span at all. Capture now runs on the real driver: - The backend program declares the capture set with glTransformFeedbackVaryings before it links. SPIRV-Cross keeps user output names verbatim in the transpiled ESSL, so the frontend's requested names carry over unchanged. - New GLFunctionsTable Begin/EndTransformFeedback entries hand the span boundaries to the backend (null for DirectVulkan, which is unaffected). - The driver-side begin is deferred to the first draw of the span: ES needs the capturing program current and the capture buffers bound, and both only become true once PrepareForDraw has run. A span that never draws never touches the driver, which is what the GL semantics amount to anyway. - The end mirrors the captured ranges back into the frontend buffer shadows - the GPU wrote them behind the frontend's back, so MapBuffer/GetBufferSubData would otherwise still return the pre-draw bytes. Takes KHR-GL32.transform_feedback from 13/21 to 19/21; the two remaining failures are the geometry-amplified primitive queries, which still go through the frontend's CPU accounting. --- MobileGL/MG_Backend/BackendObject.h | 7 ++ .../DirectGLES/BackendObject_DirectGLES.cpp | 5 + MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 114 ++++++++++++++++++ MobileGL/MG_Backend/DirectGLES/DirectGLES.h | 12 ++ MobileGL/MG_Backend/DirectGLES/Managers.cpp | 20 +++ .../MG_Impl/GLImpl/Drawing/GL_Drawing.cpp | 8 ++ 6 files changed, 166 insertions(+) diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 3082f013..88190dd0 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -229,6 +229,13 @@ namespace MobileGL { // (optional; null = frontend falls back to CPU accounting). BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated); void (*EndXfbPrimitivesQuery)(BackendQueryHandle query); + // Transform feedback capture spans, for backends whose own GL/ES driver + // performs the capture (DirectGLES). Both optional; null means the backend + // drives capture from its draw recording instead (DirectVulkan). End is + // called while the frontend capture state is still active, so the backend + // can still see the capture program and buffer bindings. + void (*BeginTransformFeedback)(GLenum primitiveMode); + void (*EndTransformFeedback)(); Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported }; struct GlobalBackendFunctionsTable { diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 11f368cd..9ab66a42 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -945,6 +945,11 @@ namespace MobileGL::MG_Backend::DirectGLES { funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable; funcsTable.GL.GetQueryResult64 = GetQueryResult64; funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery; + // Transform feedback is captured by the real ES driver rather than + // reconstructed from the draw recording, so the frontend has to hand the + // span boundaries over. + funcsTable.GL.BeginTransformFeedback = XfbImpl::BeginTransformFeedback; + funcsTable.GL.EndTransformFeedback = XfbImpl::EndTransformFeedback; funcsTableInitialized = true; } return funcsTable; diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 9e889fb1..01fd5a18 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -321,6 +321,115 @@ namespace MobileGL::MG_Backend::DirectGLES { } } // namespace BufferImpl + // Transform feedback is captured by the real ES driver: the backend program + // declares the capture set at link time (see BackendProgramObjectImpl::SyncToBackend) + // and the span below wraps the driver's own glBeginTransformFeedback/glEndTransformFeedback. + // + // The driver-side Begin is deferred from the frontend's glBeginTransformFeedback to + // the first draw of the span: ES requires the capturing program to be current and + // the capture buffers bound when Begin is issued, and both of those only become true + // once PrepareForDraw has run. A span that never draws therefore never touches the + // driver at all, which is also what the GL semantics amount to. + namespace XfbImpl { + namespace { + struct XfbCaptureTarget { + SharedPtr buffer; + Uint backendId = 0; + SizeT start = 0; + SizeT end = 0; + }; + + Bool g_xfbPending = false; // frontend Begin seen, driver capture not started yet + Bool g_xfbStarted = false; // driver capture running + GLenum g_xfbPrimitiveMode = GL_POINTS; + Vector g_xfbTargets; + } // namespace + + Bool AreTransformFeedbacksSupported() { + return g_GLESFuncs.glBeginTransformFeedback != nullptr && + g_GLESFuncs.glEndTransformFeedback != nullptr && + g_GLESFuncs.glTransformFeedbackVaryings != nullptr; + } + + void BeginTransformFeedback(GLenum primitiveMode) { + if (!AreTransformFeedbacksSupported()) return; + g_xfbPrimitiveMode = primitiveMode; + g_xfbPending = true; + g_xfbStarted = false; + g_xfbTargets.clear(); + } + + // Tail of PrepareForDraw: the program is bound and every buffer the draw needs + // is up to date, so the capture buffers can be bound and the span opened. + void StartPendingTransformFeedback() { + if (!g_xfbPending) return; + g_xfbPending = false; + const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram(); + if (!program) return; + + // Snapshot what the driver is about to capture into. GL forbids rebinding the + // capture buffers while the span is open, so this stays valid until End, and + // recording it here keeps End independent of the frontend capture state. + const SizeT bufferCount = program->GetTransformFeedbackBufferCount(); + for (SizeT i = 0; i < bufferCount; ++i) { + auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback, + static_cast(i)); + const auto& bufferObject = point.GetBoundObject(); + if (!bufferObject) continue; + auto* backendResource = BufferImpl::EnsureBufferResource(bufferObject); + if (!backendResource || backendResource->id == 0) continue; + const Range1D range = point.GetRange(); + const SizeT start = std::min(range.start, bufferObject->GetSize()); + const SizeT end = std::min(range.end, bufferObject->GetSize()); + if (end <= start) continue; + g_xfbTargets.push_back({bufferObject, backendResource->id, start, end}); + } + + BufferImpl::SyncBufferBindingPoints(BufferTarget::TransformFeedback, GL_TRANSFORM_FEEDBACK_BUFFER); + g_GLESFuncs.glBeginTransformFeedback(g_xfbPrimitiveMode); + g_xfbStarted = true; + } + + void EndTransformFeedback() { + g_xfbPending = false; + if (!g_xfbStarted) return; + g_xfbStarted = false; + g_GLESFuncs.glEndTransformFeedback(); + + // The GPU wrote the capture buffers behind the frontend's back, so the CPU + // shadows that back MapBuffer/GetBufferSubData still hold the pre-draw bytes. + // Mirror the captured ranges into them. Buffers whose storage the backend + // already owns (coherent persistent map) need nothing: reads resolve against + // that storage directly. + if (g_GLESFuncs.glMapBufferRange != nullptr && g_GLESFuncs.glUnmapBuffer != nullptr) { + for (const auto& target : g_xfbTargets) { + if (!target.buffer || target.buffer->IsBackendPersistentMapped()) continue; + const SizeT size = target.end - target.start; + BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, target.backendId); + void* mapped = g_GLESFuncs.glMapBufferRange(BufferImpl::TempBufferTarget, + static_cast(target.start), + static_cast(size), GL_MAP_READ_BIT); + if (mapped == nullptr) { + MGLOG_E("EndTransformFeedback: failed to map backend buffer %u for capture readback", + target.backendId); + continue; + } + target.buffer->WritebackFromBackend({mapped, size}, target.start); + g_GLESFuncs.glUnmapBuffer(BufferImpl::TempBufferTarget); + } + } + g_xfbTargets.clear(); + } + + // The ES context went away (or is being torn down): the span, its buffer ids and + // the frontend objects it pinned all belonged to it. + void OnBackendContextDestroyed() { + g_xfbPending = false; + g_xfbStarted = false; + g_xfbTargets.clear(); + } + } // namespace XfbImpl + namespace VertexArrayImpl { void SyncCurrentVAO() { #ifdef TRACY_ENABLE @@ -1081,6 +1190,10 @@ namespace MobileGL::MG_Backend::DirectGLES { BindCurrentTextures(); BindCurrentProgramWithResources(); + + // Last: opening the capture span needs the program current and the capture + // buffers bound, and ES rejects most binding changes once it is open. + XfbImpl::StartPendingTransformFeedback(); } // Rebinds every frontend texture unit's textures (and sampler objects) on the @@ -4737,6 +4850,7 @@ namespace MobileGL::MG_Backend::DirectGLES { void DestroyEGLContext() { BufferImpl::OnBackendContextDestroyed(); + XfbImpl::OnBackendContextDestroyed(); ScratchFBOImpl::OnBackendContextDestroyed(); FramebufferImpl::InvalidateFramebufferBindingCache(); PixelStoreImpl::InvalidatePackStateCache(); diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index 4b47e0b5..223a5f56 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -159,6 +159,18 @@ namespace MobileGL::MG_Backend::DirectGLES { void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities); void DestroyEGLContext(); + // Transform feedback capture spans, performed by the real ES driver. The + // capture set is declared on the backend program at link time; the driver-side + // begin is deferred to the first draw of the span (ES needs the capturing + // program current and the capture buffers bound), and the end also mirrors the + // captured bytes back into the frontend buffer shadows. + namespace XfbImpl { + Bool AreTransformFeedbacksSupported(); + void BeginTransformFeedback(GLenum primitiveMode); + void EndTransformFeedback(); + void OnBackendContextDestroyed(); + } // namespace XfbImpl + extern MG_External::EGLFunctionsTable g_EGLFuncs; extern MG_External::GLESFunctionsTable g_GLESFuncs; extern MG_External::GLESCapabilities g_GLESCapabilities; diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index ccf979fc..126ee945 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -3386,6 +3386,26 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("Processed shader source length: %zu", source.length()); } + // Transform feedback capture runs on the real driver (see XfbImpl in + // DirectGLES.cpp), so the capture set has to be declared on the backend + // program before it links. SPIRV-Cross keeps user output names verbatim in + // the transpiled ESSL (`out vec4 result_0;` stays `result_0`), so the + // frontend's requested names carry over unchanged. + if (stateProgramObject->GetTransformFeedbackVaryingCount() > 0 && + g_GLESFuncs.glTransformFeedbackVaryings != nullptr) { + const auto& xfbVaryings = stateProgramObject->GetTransformFeedbackVaryings(); + Vector xfbNames; + xfbNames.reserve(xfbVaryings.size()); + for (const auto& xfbVarying : xfbVaryings) { + xfbNames.push_back(xfbVarying.name.c_str()); + } + MGLOG_D("Declaring %zu transform feedback varyings on program %u", xfbNames.size(), + m_backendProgramId); + g_GLESFuncs.glTransformFeedbackVaryings(m_backendProgramId, static_cast(xfbNames.size()), + xfbNames.data(), + stateProgramObject->GetTransformFeedbackBufferMode()); + } + // Link program MGLOG_D("Linking program %u", m_backendProgramId); g_GLESFuncs.glLinkProgram(m_backendProgramId); diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index 3c57b71f..e90ee908 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -578,6 +578,9 @@ namespace MobileGL::MG_Impl::GLImpl { } } MG_State::pGLContext->BeginTransformFeedback(primitiveMode, program); + if (const auto beginXfb = MG_Backend::gBackendFunctionsTable.GL.BeginTransformFeedback) { + beginXfb(primitiveMode); + } } // Vulkan transform feedback captures triangle strips in plain (i, i+1, i+2) @@ -649,6 +652,11 @@ namespace MobileGL::MG_Impl::GLImpl { } const auto capturedProgram = MG_State::pGLContext->GetTransformFeedbackProgram(); const Uint64 inputPrimitives = MG_State::pGLContext->GetTransformFeedbackInputPrimitives(); + // Closed while the capture state is still active: a backend that captures + // through its own driver reads the capture program and buffer bindings here. + if (const auto endXfb = MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback) { + endXfb(); + } MG_State::pGLContext->EndTransformFeedback(); // Captured results must be visible to MapBuffer/GetBufferSubData after // End; the capture targets are host-coherent GPU memory, so completing