From 76f37a18e624365f58d7fd6580c078bcf6a3d2ab Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Tue, 4 Aug 2026 10:18:53 -0400 Subject: [PATCH] [Feat] (MG_State, MG_Impl, DirectGLES): transform feedback objects, pause/resume and the special capture names GL 4.0 folds ARB_transform_feedback2 and _3 into core, and neither existed: glGenTransformFeedbacks, glBindTransformFeedback, glDeleteTransformFeedbacks, glIsTransformFeedback, glPause/ResumeTransformFeedback, the whole glDrawTransformFeedback family and glBegin/EndQueryIndexed were all stubs, and gl_NextBuffer / gl_SkipComponents1..4 failed the link as "not an output of the vertex stage". Seven KHR-GL40.transform_feedback* cases failed on it, three of them by leaving a capture open at deinit and taking the process down. Objects. The capture state and the indexed GL_TRANSFORM_FEEDBACK_BUFFER bindings are object state, but the context keeps one live copy of both, which is what every existing reader - each backend's per-draw sync, the drawing and getter paths - is written against. Rather than teach all of them about objects, a bind saves the live copy into the outgoing object and restores the incoming one's. Object 0 is the default object and needs no seeding; operator[] materialises the rest on first touch. Pause. A paused span captures nothing, and three rules key off that: a draw is exempt from the capture primitive-mode match, it feeds PRIMITIVES_GENERATED but not TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, and glUseProgram is allowed again (that last one was already refused for an active capture, correctly for GL 3.3, which has no pause). glDrawTransformFeedback replays the vertices the object captured in its last completed span, recorded at End. "Has a completed span" is tracked separately from that count, because a completed empty span draws nothing while an object that never ended one is INVALID_OPERATION. Drawing from the object whose capture is currently open is deliberately allowed - feeding a result straight into the next span is the point of KHR-GL40.transform_feedback.draw_xfb_feedbackk_test. DirectGLES gets a real driver object per frontend object. That is the only reason the default one would not do: several objects can be paused at once, and a paused span lives inside the driver's object. The deferred driver-side Begin (still needed - ES wants the program current and the buffers bound) now also has to be held back while the span is paused, or a pause taken before the first draw would open the span on that draw and subject it to the primitive-mode rule it is exempt from. Special names. gl_NextBuffer and gl_SkipComponents are consumed during varying resolution and never become varyings of their own, so they only move where the following ones land - and stay out of the name list the backend declares on its own driver. ES cannot express the resulting layout at all: it packs every captured varying into one gap-free record. So when the layout has holes or spans several buffers, DirectGLES captures into a scratch buffer bound in place of the application's, and End distributes the records to the offsets GL asked for. Only the bytes a varying occupies are written, which is exactly what makes the holes keep the contents the application left there - the property KHR-GL40.transform_feedback3.skip_components checks. glBegin/EndQueryIndexed and glGetQueryIndexediv differ from the plain forms only in the vertex stream they address, so they validate the index and forward. GL_MAX_VERTEX_STREAMS stays at 1: multi-stream capture needs ARB_gpu_shader5 stream qualifiers that no ES driver implements, and the CTS cases that need more than one stream check the limit and skip. KHR-GL40.transform_feedback, transform_feedback2 and transform_feedback3: 38/38. --- MobileGL/MG_Backend/BackendObject.h | 7 + .../DirectGLES/BackendObject_DirectGLES.cpp | 4 + MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 302 +++++++++++++++--- MobileGL/MG_Backend/DirectGLES/DirectGLES.h | 4 + .../MG_Impl/GLImpl/Drawing/GL_Drawing.cpp | 183 ++++++++++- MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.h | 10 + .../MG_Impl/GLImpl/Exporting/Definitions.cpp | 32 +- MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp | 5 +- .../MG_Impl/GLImpl/Program/GL_Program.cpp | 33 +- MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp | 39 +++ MobileGL/MG_Impl/GLImpl/Query/GL_Query.h | 3 + MobileGL/MG_State/GLState/Core.cpp | 77 +++++ MobileGL/MG_State/GLState/Core.h | 63 +++- .../GLState/ProgramState/ProgramObject.cpp | 46 ++- .../GLState/ProgramState/ProgramObject.h | 14 + 15 files changed, 746 insertions(+), 76 deletions(-) diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 38c211fb..134e247d 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -236,6 +236,13 @@ namespace MobileGL { // can still see the capture program and buffer bindings. void (*BeginTransformFeedback)(GLenum primitiveMode); void (*EndTransformFeedback)(); + // ARB_transform_feedback2. A backend that leaves these null keeps the single + // implicit capture span the frontend has always modelled; the frontend state + // (paused flag, per-object bindings) is tracked either way. + void (*PauseTransformFeedback)(); + void (*ResumeTransformFeedback)(); + void (*BindTransformFeedback)(GLuint name); + void (*DeleteTransformFeedback)(GLuint name); 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 98c0d163..4116fa66 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -1032,6 +1032,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // span boundaries over. funcsTable.GL.BeginTransformFeedback = XfbImpl::BeginTransformFeedback; funcsTable.GL.EndTransformFeedback = XfbImpl::EndTransformFeedback; + funcsTable.GL.PauseTransformFeedback = XfbImpl::PauseTransformFeedback; + funcsTable.GL.ResumeTransformFeedback = XfbImpl::ResumeTransformFeedback; + funcsTable.GL.BindTransformFeedback = XfbImpl::BindTransformFeedback; + funcsTable.GL.DeleteTransformFeedback = XfbImpl::DeleteTransformFeedback; funcsTableInitialized = true; } return funcsTable; diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index e5d1028c..614a87fe 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -407,6 +407,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // 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. + // + // Transform feedback objects (ARB_transform_feedback2) are ES 3.0 core, so each + // frontend object gets one of the driver's: a paused span lives inside the ES object, + // which is the only way several of them can be paused at once - and the only reason + // the default object alone would not do. namespace XfbImpl { namespace { struct XfbCaptureTarget { @@ -416,10 +421,161 @@ namespace MobileGL::MG_Backend::DirectGLES { 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; + // Per frontend transform feedback object. The default object (name 0) maps to + // the driver's default object (id 0) and is always present. + struct XfbObjectState { + GLuint esId = 0; + Bool pending = false; // frontend Begin seen, driver capture not started yet + Bool started = false; // driver capture running + Bool paused = false; // frontend Pause seen and not yet resumed + GLenum primitiveMode = GL_POINTS; + Vector targets; + // Set for a layout ES cannot express (gl_SkipComponents / gl_NextBuffer): + // the driver captures gap-free records into the scratch buffer below and + // End scatters them into `targets`. + Bool scattered = false; + SharedPtr scatterProgram; + SizeT scatterCapacityVertices = 0; + }; + + // One scratch ES buffer serves every scattered capture: only one span can be + // recording at a time (the driver would reject a second Begin), so its contents + // are consumed by the End that follows. + GLuint g_scatterBufferId = 0; + SizeT g_scatterBufferSize = 0; + + UnorderedMap g_xfbObjects; + GLuint g_currentXfbName = 0; + + XfbObjectState& CurrentXfb() { + return g_xfbObjects[g_currentXfbName]; + } + + Bool AreTransformFeedbackObjectsSupported() { + return g_GLESFuncs.glGenTransformFeedbacks != nullptr && + g_GLESFuncs.glBindTransformFeedback != nullptr && + g_GLESFuncs.glDeleteTransformFeedbacks != nullptr && + g_GLESFuncs.glPauseTransformFeedback != nullptr && + g_GLESFuncs.glResumeTransformFeedback != nullptr; + } + + // Mirrors one capture span's results into the frontend CPU shadows. The GPU wrote + // the capture buffers behind the frontend's back, so the shadows that back + // MapBuffer/GetBufferSubData still hold the pre-draw bytes. Buffers whose storage + // the backend already owns (coherent persistent map) need nothing: reads resolve + // against that storage directly. + void ReadbackCapturedRanges(Vector& targets) { + if (g_GLESFuncs.glMapBufferRange != nullptr && g_GLESFuncs.glUnmapBuffer != nullptr) { + for (const auto& target : targets) { + 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); + } + } + targets.clear(); + } + + // Binds a scratch buffer, sized for `capacityVertices` gap-free records, to + // capture point 0 in place of the application's buffers. Returns false when the + // scratch storage cannot be provided, in which case the caller falls back to the + // direct binding (which produces a wrong layout, but is what happened before). + Bool BindScatterCaptureBuffer(SizeT packedStride, SizeT capacityVertices) { + if (packedStride == 0 || capacityVertices == 0) return false; + if (g_GLESFuncs.glGenBuffers == nullptr || g_GLESFuncs.glBufferData == nullptr) return false; + const SizeT required = packedStride * capacityVertices; + if (g_scatterBufferId == 0) { + g_GLESFuncs.glGenBuffers(1, &g_scatterBufferId); + if (g_scatterBufferId == 0) return false; + g_scatterBufferSize = 0; + } + if (g_scatterBufferSize < required) { + BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId); + g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast(required), nullptr, + GL_DYNAMIC_COPY); + g_scatterBufferSize = required; + } + // Point 0 carries every captured varying (the ES capture is INTERLEAVED); the + // other points must be cleared or the driver would still write the app's buffers. + BufferImpl::BindBufferRangeCached(GL_TRANSFORM_FEEDBACK_BUFFER, 0, g_scatterBufferId, 0, + static_cast(required)); + const SizeT pointCount = + MG_State::pGLContext->GetTouchedBufferBindingPointCount(BufferTarget::TransformFeedback); + for (SizeT i = 1; i < pointCount; ++i) { + BufferImpl::BindBufferBaseCached(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast(i), 0); + } + return true; + } + + // Distributes the gap-free records the driver captured into the application's + // buffers at the offsets the GL layout asks for. Only the bytes a varying actually + // occupies are written, so the holes gl_SkipComponents asks for keep whatever the + // application had put there - which is the whole point of the feature. + void ScatterCapturedRecords(XfbObjectState& xfb) { + const auto& program = xfb.scatterProgram; + if (!program || xfb.targets.empty()) return; + if (g_GLESFuncs.glMapBufferRange == nullptr || g_GLESFuncs.glUnmapBuffer == nullptr) return; + + const SizeT packedStride = program->GetTransformFeedbackPackedStride(); + const SizeT vertices = std::min( + static_cast(MG_State::pGLContext->GetTransformFeedbackCapturedVertices()), + xfb.scatterCapacityVertices); + if (packedStride == 0 || vertices == 0) return; + + BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId); + const void* packed = g_GLESFuncs.glMapBufferRange(BufferImpl::TempBufferTarget, 0, + static_cast(packedStride * vertices), + GL_MAP_READ_BIT); + if (packed == nullptr) { + MGLOG_E("EndTransformFeedback: failed to map the scatter capture buffer"); + return; + } + + // One staged copy per destination buffer: start from what the application had + // (the shadow is authoritative - uploads go shadow -> ES, and previous captures + // were mirrored back into it), patch the captured varyings in, then push the + // whole range down once. + for (SizeT targetIndex = 0; targetIndex < xfb.targets.size(); ++targetIndex) { + const auto& target = xfb.targets[targetIndex]; + if (!target.buffer) continue; + const SizeT stride = program->GetTransformFeedbackStride(static_cast(targetIndex)); + if (stride == 0) continue; + const SizeT rangeBytes = target.end - target.start; + Vector staged(rangeBytes); + Memcpy(staged.data(), target.buffer->MappedData() + target.start, rangeBytes); + + for (const auto& varying : program->GetTransformFeedbackVaryings()) { + if (varying.bufferIndex != targetIndex) continue; + for (SizeT v = 0; v < vertices; ++v) { + const SizeT dstOffset = v * stride + varying.offsetBytes; + if (dstOffset + varying.byteSize > rangeBytes) break; + Memcpy(staged.data() + dstOffset, + static_cast(packed) + v * packedStride + varying.packedOffsetBytes, + varying.byteSize); + } + } + + target.buffer->WritebackFromBackend({staged.data(), rangeBytes}, target.start); + if (g_GLESFuncs.glBufferSubData != nullptr) { + BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, target.backendId); + g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, + static_cast(target.start), + static_cast(rangeBytes), staged.data()); + } + BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId); + } + g_GLESFuncs.glUnmapBuffer(BufferImpl::TempBufferTarget); + xfb.targets.clear(); + } } // namespace Bool AreTransformFeedbacksSupported() { @@ -430,17 +586,23 @@ namespace MobileGL::MG_Backend::DirectGLES { void BeginTransformFeedback(GLenum primitiveMode) { if (!AreTransformFeedbacksSupported()) return; - g_xfbPrimitiveMode = primitiveMode; - g_xfbPending = true; - g_xfbStarted = false; - g_xfbTargets.clear(); + auto& xfb = CurrentXfb(); + xfb.primitiveMode = primitiveMode; + xfb.pending = true; + xfb.started = false; + xfb.paused = false; + xfb.targets.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; + auto& xfb = CurrentXfb(); + // A span that was paused before its first draw must not open here: the draw is + // not captured, and opening the span would also subject it to the capture + // primitive-mode rule the paused draw is exempt from. + if (!xfb.pending || xfb.paused) return; + xfb.pending = false; const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram(); if (!program) return; @@ -459,51 +621,103 @@ namespace MobileGL::MG_Backend::DirectGLES { 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}); + xfb.targets.push_back({bufferObject, backendResource->id, start, end}); } BufferImpl::SyncBufferBindingPoints(BufferTarget::TransformFeedback, GL_TRANSFORM_FEEDBACK_BUFFER); - g_GLESFuncs.glBeginTransformFeedback(g_xfbPrimitiveMode); - g_xfbStarted = true; + + // A layout with holes or several interleaved buffers is not expressible on ES: + // capture gap-free into scratch storage and place the records at End instead. + xfb.scattered = false; + xfb.scatterProgram.reset(); + xfb.scatterCapacityVertices = 0; + if (program->NeedsScatteredTransformFeedbackCapture()) { + SizeT capacityVertices = ~SizeT(0); + for (SizeT i = 0; i < xfb.targets.size(); ++i) { + const SizeT stride = program->GetTransformFeedbackStride(static_cast(i)); + if (stride == 0) continue; + capacityVertices = + std::min(capacityVertices, (xfb.targets[i].end - xfb.targets[i].start) / stride); + } + if (capacityVertices == ~SizeT(0)) capacityVertices = 0; + if (BindScatterCaptureBuffer(program->GetTransformFeedbackPackedStride(), capacityVertices)) { + xfb.scattered = true; + xfb.scatterProgram = program; + xfb.scatterCapacityVertices = capacityVertices; + } + } + + g_GLESFuncs.glBeginTransformFeedback(xfb.primitiveMode); + xfb.started = true; } void EndTransformFeedback() { - g_xfbPending = false; - if (!g_xfbStarted) return; - g_xfbStarted = false; + auto& xfb = CurrentXfb(); + xfb.pending = false; + xfb.paused = false; + if (!xfb.started) return; + xfb.started = 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); - } + if (xfb.scattered) { + ScatterCapturedRecords(xfb); + xfb.scattered = false; + xfb.scatterProgram.reset(); + } else { + ReadbackCapturedRanges(xfb.targets); } - 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 PauseTransformFeedback() { + auto& xfb = CurrentXfb(); + xfb.paused = true; + // A span the driver never opened (paused before the first draw) has nothing to + // pause; the flag above is what holds the deferred Begin back until the resume. + if (!xfb.started || g_GLESFuncs.glPauseTransformFeedback == nullptr) return; + g_GLESFuncs.glPauseTransformFeedback(); + } + + void ResumeTransformFeedback() { + auto& xfb = CurrentXfb(); + xfb.paused = false; + if (!xfb.started || g_GLESFuncs.glResumeTransformFeedback == nullptr) return; + g_GLESFuncs.glResumeTransformFeedback(); + } + + void BindTransformFeedback(GLuint name) { + if (!AreTransformFeedbackObjectsSupported()) { + // Without driver objects there is only the default span; keep the frontend + // name so the bookkeeping below stays consistent. + g_currentXfbName = name; + return; + } + auto& xfb = g_xfbObjects[name]; + if (name != 0 && xfb.esId == 0) { + g_GLESFuncs.glGenTransformFeedbacks(1, &xfb.esId); + } + g_GLESFuncs.glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, xfb.esId); + g_currentXfbName = name; + } + + void DeleteTransformFeedback(GLuint name) { + const auto it = g_xfbObjects.find(name); + if (it == g_xfbObjects.end()) return; + if (it->second.esId != 0 && g_GLESFuncs.glDeleteTransformFeedbacks != nullptr) { + g_GLESFuncs.glDeleteTransformFeedbacks(1, &it->second.esId); + } + g_xfbObjects.erase(it); + // The frontend reverts to the default object when the bound one is deleted. + if (g_currentXfbName == name) { + BindTransformFeedback(0); + } + } + + // The ES context went away (or is being torn down): the spans, their buffer ids, the + // driver objects and the frontend objects they pinned all belonged to it. void OnBackendContextDestroyed() { - g_xfbPending = false; - g_xfbStarted = false; - g_xfbTargets.clear(); + g_xfbObjects.clear(); + g_currentXfbName = 0; + g_scatterBufferId = 0; + g_scatterBufferSize = 0; } } // namespace XfbImpl diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index 99a011cc..fa2eaf14 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -173,6 +173,10 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool AreTransformFeedbacksSupported(); void BeginTransformFeedback(GLenum primitiveMode); void EndTransformFeedback(); + void PauseTransformFeedback(); + void ResumeTransformFeedback(); + void BindTransformFeedback(GLuint name); + void DeleteTransformFeedback(GLuint name); void OnBackendContextDestroyed(); } // namespace XfbImpl diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index 2f91ea24..50686af3 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -72,6 +72,9 @@ namespace MobileGL::MG_Impl::GLImpl { // Geometry amplification is not modelled here. static void AccountTransformFeedbackPrimitives(GLenum mode, GLsizei count) { if (!MG_State::pGLContext->IsTransformFeedbackActive()) return; + // A paused span captures nothing, so a draw made while paused contributes to + // PRIMITIVES_GENERATED but not to TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN. + if (MG_State::pGLContext->IsTransformFeedbackPaused()) return; Uint64 primitives = CountPrimitivesForDraw(mode, count); if (primitives == 0) return; MG_State::pGLContext->AddTransformFeedbackInputPrimitives(primitives); @@ -202,8 +205,11 @@ namespace MobileGL::MG_Impl::GLImpl { // While transform feedback is active the draw's primitive type must match // the feedback primitive mode (GL 3.3 core 13.2.2). With a geometry shader // the constraint moves to the shader's output primitive type instead, so - // the draw mode itself is unconstrained here. + // the draw mode itself is unconstrained here. A paused span is exempt: it + // captures nothing, so there is nothing for the mode to be incompatible with + // (GL 4.6 core 13.2.3). if (MG_State::pGLContext->IsTransformFeedbackActive() && + !MG_State::pGLContext->IsTransformFeedbackPaused() && !(MG_State::pGLContext->GetTransformFeedbackProgram() && MG_State::pGLContext->GetTransformFeedbackProgram()->GetShaderIndexByStage(ShaderStage::Geometry) >= 0)) { const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode(); @@ -684,9 +690,12 @@ namespace MobileGL::MG_Impl::GLImpl { "No program with transform feedback varyings is active.")); return; } - // Every capture buffer slot the program's mode uses must have a buffer bound. + // Every capture buffer slot the program's mode uses must have a buffer bound. A slot + // of stride 0 - two consecutive gl_NextBuffer entries - captures nothing and so needs + // no binding. const SizeT usedBufferCount = program->GetTransformFeedbackBufferCount(); for (SizeT i = 0; i < usedBufferCount; ++i) { + if (program->GetTransformFeedbackStride(static_cast(i)) == 0) continue; const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback, static_cast(i)); if (point.GetBoundObject() == nullptr) { @@ -800,4 +809,174 @@ namespace MobileGL::MG_Impl::GLImpl { FixupGsStripCaptureOrder(capturedProgram, inputPrimitives); } + void PauseTransformFeedback(void) { + if (!MG_State::pGLContext->IsTransformFeedbackActive() || + MG_State::pGLContext->IsTransformFeedbackPaused()) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "Transform feedback is not active, or is already paused.")); + return; + } + MG_State::pGLContext->SetTransformFeedbackPaused(true); + if (const auto pauseXfb = MG_Backend::gBackendFunctionsTable.GL.PauseTransformFeedback) { + pauseXfb(); + } + } + + void ResumeTransformFeedback(void) { + if (!MG_State::pGLContext->IsTransformFeedbackActive() || + !MG_State::pGLContext->IsTransformFeedbackPaused()) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, "Transform feedback is not paused.")); + return; + } + MG_State::pGLContext->SetTransformFeedbackPaused(false); + if (const auto resumeXfb = MG_Backend::gBackendFunctionsTable.GL.ResumeTransformFeedback) { + resumeXfb(); + } + } + + void GenTransformFeedbacks(GLsizei n, GLuint* ids) { + if (n < 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, "n must be non-negative.")); + return; + } + if (n == 0 || ids == nullptr) return; + Vector names; + MG_State::pGLContext->GenTransformFeedbackNames(static_cast(n), names); + Memcpy(ids, names.data(), static_cast(n) * sizeof(GLuint)); + } + + void DeleteTransformFeedbacks(GLsizei n, const GLuint* ids) { + if (n < 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, "n must be non-negative.")); + return; + } + if (ids == nullptr) return; + for (GLsizei i = 0; i < n; ++i) { + const GLuint id = ids[i]; + // Unknown names and 0 are silently ignored; an object whose capture span is + // still open is not (GL 4.6 core 13.2.1). + if (id == 0 || !MG_State::pGLContext->ValidateTransformFeedbackName(id)) continue; + if (id == MG_State::pGLContext->GetBoundTransformFeedbackName() && + MG_State::pGLContext->IsTransformFeedbackActive()) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "Cannot delete a transform feedback object whose capture is active.")); + continue; + } + if (const auto deleteXfb = MG_Backend::gBackendFunctionsTable.GL.DeleteTransformFeedback) { + deleteXfb(id); + } + MG_State::pGLContext->MarkTransformFeedbackObjectForDeletion(id); + } + } + + void BindTransformFeedback(GLenum target, GLuint id) { + if (target != GL_TRANSFORM_FEEDBACK) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique("MG_Impl/GLImpl", __func__, "target must be GL_TRANSFORM_FEEDBACK.")); + return; + } + // A running capture pins its object; only a paused one may be swapped out. + if (MG_State::pGLContext->IsTransformFeedbackActive() && + !MG_State::pGLContext->IsTransformFeedbackPaused()) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "Transform feedback is active and not paused.")); + return; + } + if (!MG_State::pGLContext->ValidateTransformFeedbackName(id)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + std::to_string(id) + " is not a transform feedback object name.")); + return; + } + MG_State::pGLContext->BindTransformFeedbackObject(id); + if (const auto bindXfb = MG_Backend::gBackendFunctionsTable.GL.BindTransformFeedback) { + bindXfb(id); + } + } + + GLboolean IsTransformFeedback(GLuint id) { + // Name 0 is the default object, which glIsTransformFeedback reports as not an object. + return (id != 0 && MG_State::pGLContext->ValidateTransformFeedbackName(id)) ? GL_TRUE : GL_FALSE; + } + + // glDrawTransformFeedback[Stream][Instanced]: replays the vertices the named object + // captured in its last completed span, as if by glDrawArraysInstanced with that count + // (GL 4.6 core 10.3.7). + static void DrawTransformFeedbackImpl(const char* functionName, GLenum mode, GLuint id, GLuint stream, + GLsizei instancecount) { + if (!ValidateCurrentProgramForExecution(functionName)) return; + if (!ValidatePrimitiveModeForBackend(functionName, mode)) return; + if (instancecount < 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", functionName, "instancecount must be non-negative.")); + return; + } + if (!MG_State::pGLContext->ValidateTransformFeedbackName(id)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", functionName, + std::to_string(id) + " is not a transform feedback object name.")); + return; + } + // GL_MAX_VERTEX_STREAMS is 1, so stream 0 is the only one that exists. + if (stream != 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", functionName, + "stream must be less than GL_MAX_VERTEX_STREAMS.")); + return; + } + // Drawing from an object whose capture is currently open is legal and deliberate: + // it is how a transform feedback result is fed straight back into the next span + // (ARB_transform_feedback2 lists no such restriction). + if (!MG_State::pGLContext->HasTransformFeedbackCompletedSpan(id)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", functionName, + "glEndTransformFeedback has never been called for this object.")); + return; + } + + const Uint64 vertices = MG_State::pGLContext->GetTransformFeedbackRecordedVertices(id); + if (vertices == 0) return; + const auto count = static_cast(vertices); + AccountTransformFeedbackPrimitives(mode, count); + if (instancecount == 1) { + DrawArrays_Backend(mode, 0, count); + } else { + DrawArraysInstanced_Backend(mode, 0, count, instancecount); + } + } + + void DrawTransformFeedback(GLenum mode, GLuint id) { + DrawTransformFeedbackImpl(__func__, mode, id, 0, 1); + } + + void DrawTransformFeedbackInstanced(GLenum mode, GLuint id, GLsizei instancecount) { + DrawTransformFeedbackImpl(__func__, mode, id, 0, instancecount); + } + + void DrawTransformFeedbackStream(GLenum mode, GLuint id, GLuint stream) { + DrawTransformFeedbackImpl(__func__, mode, id, stream, 1); + } + + void DrawTransformFeedbackStreamInstanced(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) { + DrawTransformFeedbackImpl(__func__, mode, id, stream, instancecount); + } + } // namespace MobileGL::MG_Impl::GLImpl diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.h b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.h index cc6c8e2e..688d27bd 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.h +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.h @@ -13,6 +13,16 @@ namespace MobileGL::MG_Impl::GLImpl { /* @INSERTION_POINT:FUNCTION_DECLARATION@ */ void BeginTransformFeedback(GLenum primitiveMode); void EndTransformFeedback(void); + void PauseTransformFeedback(void); + void ResumeTransformFeedback(void); + void GenTransformFeedbacks(GLsizei n, GLuint* ids); + void DeleteTransformFeedbacks(GLsizei n, const GLuint* ids); + void BindTransformFeedback(GLenum target, GLuint id); + GLboolean IsTransformFeedback(GLuint id); + void DrawTransformFeedback(GLenum mode, GLuint id); + void DrawTransformFeedbackInstanced(GLenum mode, GLuint id, GLsizei instancecount); + void DrawTransformFeedbackStream(GLenum mode, GLuint id, GLuint stream); + void DrawTransformFeedbackStreamInstanced(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount); void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ); void DispatchComputeIndirect(GLintptr indirect); void MemoryBarrier(GLbitfield barriers); diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index 949d5711..86eb9fcc 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -292,18 +292,12 @@ DECLARE_GL_FUNCTION_HEAD(void, SamplerParameterfv, GLuint sampler, GLenum pname, DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameteriv, GLuint sampler, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameteriv, sampler, pname, params) DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterfv, GLuint sampler, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterfv, sampler, pname, params) DECLARE_GL_FUNCTION_HEAD(void, VertexAttribDivisor, GLuint index, GLuint divisor) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribDivisor, index, divisor) -DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedback, target, id) -DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids) -DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacks, n, ids) -// Transform feedback objects are not implemented, so no name is ever a live object. The shared -// stub returns (type)1, telling a probing caller that every id it invents already exists; GL_FALSE -// is both truthful and what the spec requires for a name that was never generated. -MOBILEGL_GL_API GLboolean glIsTransformFeedback(GLuint id) { - MGLOG_W("Stub function: %s(...)", __FUNCTION__); - return GL_FALSE; -} -DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedback) -DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedback) +DECLARE_GL_FUNCTION_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindTransformFeedback, target, id) +DECLARE_GL_FUNCTION_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids) +DECLARE_GL_FUNCTION_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenTransformFeedbacks, n, ids) +DECLARE_GL_FUNCTION_HEAD(GLboolean, IsTransformFeedback, GLuint id) DECLARE_GL_FUNCTION_END(GLboolean, IsTransformFeedback, id) +DECLARE_GL_FUNCTION_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PauseTransformFeedback) +DECLARE_GL_FUNCTION_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramBinary, GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramBinary, program, bufSize, length, binaryFormat, binary) DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramBinary, GLuint program, GLenum binaryFormat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramBinary, program, binaryFormat, binary, length) DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramParameteri, GLuint program, GLenum pname, GLint value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramParameteri, program, pname, value) @@ -942,11 +936,11 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformSubroutinesuiv, GLenum shadertype, GL DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformSubroutineuiv, GLenum shadertype, GLint location, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformSubroutineuiv, shadertype, location, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStageiv, GLuint program, GLenum shadertype, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStageiv, program, shadertype, pname, values) DECLARE_GL_FUNCTION_STUB_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PatchParameterfv, pname, values) -DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedback, mode, id) -DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream) -DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginQueryIndexed, target, index, id) -DECLARE_GL_FUNCTION_STUB_HEAD(void, EndQueryIndexed, GLenum target, GLuint index) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndQueryIndexed, target, index) -DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryIndexediv, GLenum target, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryIndexediv, target, index, pname, params) +DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedback, mode, id) +DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream) +DECLARE_GL_FUNCTION_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginQueryIndexed, target, index, id) +DECLARE_GL_FUNCTION_HEAD(void, EndQueryIndexed, GLenum target, GLuint index) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndQueryIndexed, target, index) +DECLARE_GL_FUNCTION_HEAD(void, GetQueryIndexediv, GLenum target, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryIndexediv, target, index, pname, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform1d, GLuint program, GLint location, GLdouble v0) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform1d, program, location, v0) DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform1dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform1dv, program, location, count, value) DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform2d, GLuint program, GLint location, GLdouble v0, GLdouble v1) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform2d, program, location, v0, v1) @@ -988,8 +982,8 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLi DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance) DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params) -DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount) -DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount) +DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount) +DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount) DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data) DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInternalformati64v, GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInternalformati64v, target, internalformat, pname, count, params) diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 3458df5f..c70b3c07 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -1952,7 +1952,10 @@ namespace MobileGL::MG_Impl::GLImpl { *params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0; break; case GL_TRANSFORM_FEEDBACK_PAUSED: - *params = 0; + *params = MG_State::pGLContext->IsTransformFeedbackPaused() ? 1 : 0; + break; + case GL_TRANSFORM_FEEDBACK_BINDING: + *params = static_cast(MG_State::pGLContext->GetBoundTransformFeedbackName()); break; case GL_MAX_TEXTURE_IMAGE_UNITS: *params = dynamicParameters.MaxTextureImageUnits; diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index d2dc7a92..98bc303e 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -920,9 +920,11 @@ namespace MobileGL::MG_Impl::GLImpl { void UseProgram_State(GLuint program) { MGLOG_D("UseProgram_State: program=%u", program); - // GL 3.3 core 2.11.3: the program in use may not change while transform - // feedback is active (there is no pause in 3.3). - if (MG_State::pGLContext->IsTransformFeedbackActive()) { + // The program in use may not change while transform feedback is active - unless + // the capture is paused, which is exactly what ARB_transform_feedback2 added the + // pause for (GL 4.6 core 7.3). + if (MG_State::pGLContext->IsTransformFeedbackActive() && + !MG_State::pGLContext->IsTransformFeedbackPaused()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, @@ -2284,6 +2286,31 @@ namespace MobileGL::MG_Impl::GLImpl { for (GLsizei i = 0; i < count; ++i) { names.emplace_back(varyings != nullptr && varyings[i] != nullptr ? varyings[i] : ""); } + // ARB_transform_feedback3's special names only mean anything in an interleaved + // capture, and gl_NextBuffer cannot advance past the last capture buffer. + constexpr Uint maxTransformFeedbackBuffers = 4; + Uint nextBufferCount = 0; + for (const String& name : names) { + const Bool isNextBuffer = name == "gl_NextBuffer"; + const Bool isSkipComponents = name.size() == 18 && name.compare(0, 17, "gl_SkipComponents") == 0 && + name[17] >= '1' && name[17] <= '4'; + if (!isNextBuffer && !isSkipComponents) continue; + if (bufferMode != GL_INTERLEAVED_ATTRIBS) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "'" + name + "' requires GL_INTERLEAVED_ATTRIBS.")); + return; + } + if (isNextBuffer && ++nextBufferCount >= maxTransformFeedbackBuffers) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "More gl_NextBuffer entries than " + "GL_MAX_TRANSFORM_FEEDBACK_BUFFERS allows.")); + return; + } + } programObject->SetTransformFeedbackVaryings(Move(names), bufferMode); } diff --git a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp index d123d187..1676722f 100644 --- a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp +++ b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include "GL_Query.h" +#include "../Getter/GL_Getter.h" #include #include #include @@ -466,4 +467,42 @@ namespace MobileGL::MG_Impl::GLImpl { } *params = static_cast(value); } + + namespace { + // The indexed query entry points differ from the plain ones only in the vertex + // stream they address (GL 4.6 core 4.2.1): index must be below GL_MAX_VERTEX_STREAMS + // for the two transform feedback targets and zero for every other target. With a + // single vertex stream both bounds are 1, so a valid call is always index 0 and + // forwards to the unindexed implementation. + Bool ValidateQueryStreamIndex(const char* function, GLenum target, GLuint index) { + const Bool perStreamTarget = + target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN; + GLint maxVertexStreams = 1; + if (perStreamTarget) { + GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams); + } + if (index < static_cast(std::max(maxVertexStreams, 1))) { + return true; + } + RecordQueryError(ErrorCode::InvalidValue, function, + perStreamTarget ? "index is not less than GL_MAX_VERTEX_STREAMS." + : "index must be zero for this query target."); + return false; + } + } // namespace + + void BeginQueryIndexed(GLenum target, GLuint index, GLuint id) { + if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return; + BeginQuery(target, id); + } + + void EndQueryIndexed(GLenum target, GLuint index) { + if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return; + EndQuery(target); + } + + void GetQueryIndexediv(GLenum target, GLuint index, GLenum pname, GLint* params) { + if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return; + GetQueryiv(target, pname, params); + } } // namespace MobileGL::MG_Impl::GLImpl diff --git a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h index 604f196f..841e6443 100644 --- a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h +++ b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h @@ -16,6 +16,9 @@ namespace MobileGL::MG_Impl::GLImpl { void BeginQuery(GLenum target, GLuint id); void EndQuery(GLenum target); void GetQueryiv(GLenum target, GLenum pname, GLint* params); + void BeginQueryIndexed(GLenum target, GLuint index, GLuint id); + void EndQueryIndexed(GLenum target, GLuint index); + void GetQueryIndexediv(GLenum target, GLuint index, GLenum pname, GLint* params); void GetQueryObjectiv(GLuint id, GLenum pname, GLint* params); void GetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params); void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params); diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index b8602c31..edb7eb74 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -745,6 +745,83 @@ namespace MobileGL::MG_State { Bool GLContext::ValidateRenderbufferObject(Uint index) const { return m_renderbufferState.ValidateRenderbufferObject(index); } + + void GLContext::SaveBoundTransformFeedbackState() { + auto& object = m_transformFeedbackObjects[m_boundTransformFeedback]; + for (Uint i = 0; i < MAX_TRANSFORM_FEEDBACK_BUFFERS; ++i) { + const auto& point = m_bufferState.GetBindingPoint(BufferTarget::TransformFeedback, i); + object.bindings[i] = {point.GetBoundObject(), point.GetRange(), point.HasExplicitRange()}; + } + object.active = m_transformFeedbackActive; + object.paused = m_transformFeedbackPaused; + object.primitiveMode = m_transformFeedbackPrimitiveMode; + object.program = m_transformFeedbackProgram; + object.capturedVertices = m_transformFeedbackCapturedVertices; + object.inputPrimitives = m_transformFeedbackInputPrimitives; + } + + void GLContext::RestoreBoundTransformFeedbackState() { + const auto& object = m_transformFeedbackObjects[m_boundTransformFeedback]; + for (Uint i = 0; i < MAX_TRANSFORM_FEEDBACK_BUFFERS; ++i) { + auto& point = m_bufferState.GetBindingPoint(BufferTarget::TransformFeedback, i); + point.Bind(object.bindings[i].buffer); + if (object.bindings[i].buffer) { + point.SetRange(object.bindings[i].range, object.bindings[i].hasExplicitRange); + } else { + point.ClearRange(); + } + } + m_transformFeedbackActive = object.active; + m_transformFeedbackPaused = object.paused; + m_transformFeedbackPrimitiveMode = object.primitiveMode; + m_transformFeedbackProgram = object.program; + m_transformFeedbackCapturedVertices = object.capturedVertices; + m_transformFeedbackInputPrimitives = object.inputPrimitives; + } + + void GLContext::GenTransformFeedbackNames(Uint number, Vector& ids) { + ids.resize(number); + if (number == 0) return; + m_transformFeedbackNames.Generate(number, ids.data()); + // A generated name already denotes an object with the default state, so that a + // bind never has to distinguish "first use" from any later one. + for (const Uint id : ids) { + m_transformFeedbackObjects[id] = {}; + } + } + + Bool GLContext::ValidateTransformFeedbackName(Uint index) const { + return index == 0 || m_transformFeedbackNames.IsValid(index); + } + + void GLContext::BindTransformFeedbackObject(Uint index) { + if (index == m_boundTransformFeedback) return; + SaveBoundTransformFeedbackState(); + m_boundTransformFeedback = index; + RestoreBoundTransformFeedbackState(); + } + + void GLContext::MarkTransformFeedbackObjectForDeletion(Uint index) { + if (index == 0 || !m_transformFeedbackNames.IsValid(index)) return; + // Deleting the bound object reverts to the default one (GL 4.6 core 13.2.1); + // its state is dropped rather than saved back into the dying object. + if (index == m_boundTransformFeedback) { + m_boundTransformFeedback = 0; + RestoreBoundTransformFeedbackState(); + } + m_transformFeedbackObjects.erase(index); + m_transformFeedbackNames.Delete(index); + } + + Uint64 GLContext::GetTransformFeedbackRecordedVertices(Uint index) const { + const auto it = m_transformFeedbackObjects.find(index); + return it == m_transformFeedbackObjects.end() ? 0 : it->second.recordedVertices; + } + + Bool GLContext::HasTransformFeedbackCompletedSpan(Uint index) const { + const auto it = m_transformFeedbackObjects.find(index); + return it != m_transformFeedbackObjects.end() && it->second.hasCompletedSpan; + } } // namespace GLState // Leak-at-exit storage; see GlobalObjects.cpp. diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index b4df3edb..376aa799 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -211,9 +211,12 @@ namespace MobileGL { void SetScissorBox(IntVec4 box); // x, y, width, height const IntVec4& GetScissorBox() const; // x, y, width, height - // Transform feedback (GL 3.0 core Begin/End; no feedback objects yet) + // Transform feedback. The fields below are the state of the transform + // feedback object currently bound to GL_TRANSFORM_FEEDBACK; see the object + // block further down for how a bind swaps them. void BeginTransformFeedback(GLenum primitiveMode, const SharedPtr& program) { m_transformFeedbackActive = true; + m_transformFeedbackPaused = false; m_transformFeedbackPrimitiveMode = primitiveMode; m_transformFeedbackProgram = program; ++m_transformFeedbackGeneration; @@ -222,9 +225,16 @@ namespace MobileGL { } void EndTransformFeedback() { m_transformFeedbackActive = false; + m_transformFeedbackPaused = false; m_transformFeedbackProgram.reset(); + // What glDrawTransformFeedback on this object replays from now on. + auto& object = m_transformFeedbackObjects[m_boundTransformFeedback]; + object.recordedVertices = m_transformFeedbackCapturedVertices; + object.hasCompletedSpan = true; } Bool IsTransformFeedbackActive() const { return m_transformFeedbackActive; } + Bool IsTransformFeedbackPaused() const { return m_transformFeedbackPaused; } + void SetTransformFeedbackPaused(Bool paused) { m_transformFeedbackPaused = paused; } GLenum GetTransformFeedbackPrimitiveMode() const { return m_transformFeedbackPrimitiveMode; } const SharedPtr& GetTransformFeedbackProgram() const { return m_transformFeedbackProgram; @@ -252,6 +262,29 @@ namespace MobileGL { } Uint64 GetTransformFeedbackInputPrimitives() const { return m_transformFeedbackInputPrimitives; } + // Transform feedback objects (ARB_transform_feedback2 / GL 4.0 core). + // The capture state above and the indexed GL_TRANSFORM_FEEDBACK_BUFFER + // binding points are object state, but the context keeps exactly one live + // copy of both so that every existing reader - the backends' per-draw sync, + // the drawing and getter paths - needs no notion of which object owns them. + // A bind therefore saves the live copy into the outgoing object and restores + // the incoming one's. Object 0 is the default object and always exists. + static constexpr Uint MAX_TRANSFORM_FEEDBACK_BUFFERS = 4; + void GenTransformFeedbackNames(Uint number, Vector& ids); + // A name glGenTransformFeedbacks handed out and glDeleteTransformFeedbacks + // has not taken back. Name 0 is always valid. + Bool ValidateTransformFeedbackName(Uint index) const; + void BindTransformFeedbackObject(Uint index); + void MarkTransformFeedbackObjectForDeletion(Uint index); + Uint GetBoundTransformFeedbackName() const { return m_boundTransformFeedback; } + // Vertices the object captured in its last completed span; the vertex count + // glDrawTransformFeedback replays. + Uint64 GetTransformFeedbackRecordedVertices(Uint index) const; + // Whether the object has ever completed a capture span. glDrawTransformFeedback + // on an object that has not is INVALID_OPERATION, which a zero vertex count + // cannot express: an empty completed span is legal and draws nothing. + Bool HasTransformFeedbackCompletedSpan(Uint index) const; + // Framebuffer void GenFramebufferNames(Uint number, Vector& framebuffers); const SharedPtr& GetFramebufferObject(Uint index); @@ -285,12 +318,40 @@ namespace MobileGL { VertexArrayState m_vertexArrayState; Array m_currentVertexAttributes{}; Bool m_transformFeedbackActive = false; + Bool m_transformFeedbackPaused = false; GLenum m_transformFeedbackPrimitiveMode = GL_POINTS; SharedPtr m_transformFeedbackProgram; Uint64 m_transformFeedbackGeneration = 0; + // Not object state: the transform feedback queries snapshot it at BeginQuery + // and take the delta at EndQuery, which spans whatever objects were used. Uint64 m_transformFeedbackPrimitiveCounter = 0; Uint64 m_transformFeedbackCapturedVertices = 0; Uint64 m_transformFeedbackInputPrimitives = 0; + + // Everything a transform feedback object owns while it is NOT the bound one. + struct TransformFeedbackObjectState { + struct SavedBufferBinding { + SharedPtr buffer; + Range1D range; + Bool hasExplicitRange = false; + }; + Array bindings; + Bool active = false; + Bool paused = false; + GLenum primitiveMode = GL_POINTS; + SharedPtr program; + Uint64 capturedVertices = 0; + Uint64 inputPrimitives = 0; + Uint64 recordedVertices = 0; + Bool hasCompletedSpan = false; + }; + void SaveBoundTransformFeedbackState(); + void RestoreBoundTransformFeedbackState(); + // operator[] materialises an entry with the default state on first touch, so + // the default object (name 0) needs no seeding here. + UnorderedMap m_transformFeedbackObjects; + IndexGenerator m_transformFeedbackNames; + Uint m_boundTransformFeedback = 0; TextureState m_textureState; ProgramState m_programState; RenderState m_renderState; diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp index 11db3fa0..a3557444 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp @@ -174,6 +174,8 @@ namespace MobileGL::MG_State::GLState { m_xfbStrides.clear(); m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS; m_xfbVaryingNameMaxLength = 0; + m_xfbNeedsScatteredCapture = false; + m_xfbPackedStride = 0; m_gsInputPrimitive = GL_NONE; m_linkStatus = false; } @@ -223,6 +225,8 @@ namespace MobileGL::MG_State::GLState { m_xfbStrides.clear(); m_xfbBufferMode = m_requestedXfbBufferMode; m_xfbVaryingNameMaxLength = 0; + m_xfbNeedsScatteredCapture = false; + m_xfbPackedStride = 0; if (m_requestedXfbVaryings.empty()) { return true; } @@ -244,8 +248,27 @@ namespace MobileGL::MG_State::GLState { const Bool interleaved = m_xfbBufferMode == GL_INTERLEAVED_ATTRIBS; Uint32 interleavedOffset = 0; + // ARB_transform_feedback3 lets an interleaved capture leave holes (gl_SkipComponents1..4) + // and move on to the next buffer (gl_NextBuffer). Both only affect where the following + // varyings land, so they are consumed here and never become XfbVaryings of their own - + // which also keeps them out of the name list a backend declares on its own driver. + Uint32 interleavedBufferIndex = 0; + Vector interleavedStrides; for (SizeT i = 0; i < m_requestedXfbVaryings.size(); ++i) { const String& name = m_requestedXfbVaryings[i]; + if (interleaved && name == "gl_NextBuffer") { + interleavedStrides.push_back(interleavedOffset); + interleavedOffset = 0; + ++interleavedBufferIndex; + m_xfbNeedsScatteredCapture = true; + continue; + } + if (interleaved && name.size() == 18 && name.compare(0, 17, "gl_SkipComponents") == 0 && + name[17] >= '1' && name[17] <= '4') { + interleavedOffset += static_cast(name[17] - '0') * 4; + m_xfbNeedsScatteredCapture = true; + continue; + } for (SizeT j = 0; j < i; ++j) { if (m_requestedXfbVaryings[j] == name) { m_infoLog = "Transform feedback varying '" + name + "' is specified more than once."; @@ -286,12 +309,14 @@ namespace MobileGL::MG_State::GLState { } varying.byteSize = bytesPerElement * static_cast(varying.size); + varying.packedOffsetBytes = m_xfbPackedStride; + m_xfbPackedStride += varying.byteSize; if (interleaved) { - varying.bufferIndex = 0; + varying.bufferIndex = interleavedBufferIndex; varying.offsetBytes = interleavedOffset; interleavedOffset += varying.byteSize; } else { - varying.bufferIndex = static_cast(i); + varying.bufferIndex = static_cast(m_xfbVaryings.size()); varying.offsetBytes = 0; } m_xfbVaryingNameMaxLength = @@ -302,13 +327,22 @@ namespace MobileGL::MG_State::GLState { constexpr Uint32 kMaxSeparateAttribs = 4; constexpr Uint32 kMaxSeparateComponents = 4; constexpr Uint32 kMaxInterleavedComponents = 64; + constexpr Uint32 kMaxTransformFeedbackBuffers = 4; if (interleaved) { - if (interleavedOffset > kMaxInterleavedComponents * 4) { - m_infoLog = "Transform feedback interleaved capture exceeds " - "GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS."; + interleavedStrides.push_back(interleavedOffset); + if (interleavedStrides.size() > kMaxTransformFeedbackBuffers) { + m_infoLog = "Transform feedback capture uses more buffers than " + "GL_MAX_TRANSFORM_FEEDBACK_BUFFERS."; return false; } - m_xfbStrides.assign(1, interleavedOffset); + for (const Uint32 stride : interleavedStrides) { + if (stride > kMaxInterleavedComponents * 4) { + m_infoLog = "Transform feedback interleaved capture exceeds " + "GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS."; + return false; + } + } + m_xfbStrides = Move(interleavedStrides); } else { if (m_xfbVaryings.size() > kMaxSeparateAttribs) { m_infoLog = "Transform feedback separate capture exceeds " diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index e8dadf3c..582b5a83 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -474,6 +474,9 @@ namespace MobileGL::MG_State::GLState { Uint32 bufferIndex = 0; // capture buffer slot Uint32 offsetBytes = 0; // offset within the capture buffer Uint32 byteSize = 0; // bytes captured per vertex for this varying + // Offset within the gap-free record a backend that cannot express the GL + // layout captures into; see NeedsScatteredTransformFeedbackCapture. + Uint32 packedOffsetBytes = 0; }; void SetTransformFeedbackVaryings(Vector&& names, GLenum bufferMode) { m_requestedXfbVaryings = Move(names); @@ -491,6 +494,15 @@ namespace MobileGL::MG_State::GLState { } SizeT GetTransformFeedbackBufferCount() const { return m_xfbStrides.size(); } Int GetTransformFeedbackVaryingMaxLength() const { return m_xfbVaryingNameMaxLength; } + // True when the capture layout uses gl_SkipComponents / gl_NextBuffer + // (ARB_transform_feedback3), which no ES driver can express: it can only pack every + // captured varying into one record with no gaps. A backend that captures through + // such a driver has to capture into scratch storage and scatter the records into the + // application's buffers itself, using packedOffsetBytes as the source offset and + // (bufferIndex, offsetBytes, stride) as the destination. + Bool NeedsScatteredTransformFeedbackCapture() const { return m_xfbNeedsScatteredCapture; } + // Bytes one gap-free captured record occupies. + Uint32 GetTransformFeedbackPackedStride() const { return m_xfbPackedStride; } // True when the capture stage is a triangle-strip geometry shader with a // statically-known emit sequence: the Vulkan capture order then needs the GL // odd-triangle vertex swap after EndTransformFeedback. @@ -611,5 +623,7 @@ namespace MobileGL::MG_State::GLState { GLenum m_gsInputPrimitive = GL_NONE; GLenum m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS; Int m_xfbVaryingNameMaxLength = 0; + Bool m_xfbNeedsScatteredCapture = false; + Uint32 m_xfbPackedStride = 0; }; } // namespace MobileGL::MG_State::GLState