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