From e9382f532974aad8953f149dcb43e428bc7a88ca Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Sat, 1 Aug 2026 11:57:18 -0400 Subject: [PATCH] [Fix] (DirectGLES): exact transform feedback primitive queries Two leftovers from the capture passthrough, both only observable with a geometry shader in the pipeline: - GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED fell back to the frontend's CPU accounting, which counts the primitives the draw call assembles and so cannot see a geometry stage's amplification. Both are core ES query targets (GL_PRIMITIVES_GENERATED from 3.2 on, gated accordingly so an older driver doesn't get a stray GL_INVALID_ENUM), so they now go straight to the driver's own counters. Generalized the occlusion-query handle's isOcclusion flag into the glBeginQuery target it already had to remember for glEndQuery, which is what tells the result read to use the core 32-bit getter. - FixupGsStripCaptureOrder rewrites captured strip triangles from Vulkan's (i, i+1, i+2) order into GL's (i+1, i, i+2). A driver-side capture already emits GL order, so the rewrite corrupted it - KHR-GL33.transform_feedback .geometry read back the odd triangle rotated one vertex. Skipped when the backend owns the capture span. KHR-GL3{0,1,2,3}.transform_feedback is now 21/21 on Espryt; DirectVulkan (lavapipe) re-verified at 21/21 for the shared frontend change. --- .../DirectGLES/BackendObject_DirectGLES.cpp | 4 ++ MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 68 +++++++++++++------ MobileGL/MG_Backend/DirectGLES/DirectGLES.h | 5 ++ .../MG_Impl/GLImpl/Drawing/GL_Drawing.cpp | 6 ++ 4 files changed, 63 insertions(+), 20 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 9ab66a42..f77f001c 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -942,6 +942,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // when the timer-query group above is disabled. funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery; funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery; + // Real driver primitive counters: the frontend's CPU accounting cannot see a + // geometry shader's amplification. + funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery; + funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery; funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable; funcsTable.GL.GetQueryResult64 = GetQueryResult64; funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery; diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 01fd5a18..a8f30a69 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -4473,11 +4473,13 @@ namespace MobileGL::MG_Backend::DirectGLES { struct GLESQueryObject { GLuint queryId = 0; Uint contextGeneration = 0; - // GL_ANY_SAMPLES_PASSED result is 0/1 and only ever reachable through the - // core (non-extension) glGetQueryObjectuiv getter - GL_EXT_disjoint_timer_query's - // 64-bit glGetQueryObjectui64vEXT is timer-specific and may be entirely absent - // on drivers that otherwise fully support core ES3 occlusion queries. - Bool isOcclusion = false; + // Non-zero for the core (non-timer) query targets - occlusion and transform + // feedback primitives - and then holds the glBeginQuery target, which glEndQuery + // needs back. Their results are counts reachable only through the core + // glGetQueryObjectuiv getter: GL_EXT_disjoint_timer_query's 64-bit + // glGetQueryObjectui64vEXT is timer-specific and may be entirely absent on + // drivers that otherwise fully support these core ES queries. + GLenum coreTarget = 0; }; } @@ -4670,15 +4672,16 @@ namespace MobileGL::MG_Backend::DirectGLES { return new GLESQueryObject{queryId, g_syncContextGeneration}; } - // GL_SAMPLES_PASSED/GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) occlusion queries. Unlike the - // timer queries above, these are core ES 3.0 (no GL_EXT_disjoint_timer_query needed). - Bool AreOcclusionQueriesSupported() { + // The core (non-timer) query targets - occlusion and transform feedback primitives. + // Unlike the timer queries above these are core ES (no GL_EXT_disjoint_timer_query + // needed), so they share one begin/end pair keyed on the glBeginQuery target. + static Bool AreCoreQueriesSupported() { return g_GLESFuncs.glGenQueries && g_GLESFuncs.glDeleteQueries && g_GLESFuncs.glBeginQuery && g_GLESFuncs.glEndQuery && g_GLESFuncs.glGetQueryObjectuiv; } - BackendQueryHandle BeginOcclusionQuery() { - if (!IsBackendContextCurrentOnThisThread() || !AreOcclusionQueriesSupported()) { + static BackendQueryHandle BeginCoreQuery(GLenum target) { + if (!IsBackendContextCurrentOnThisThread() || !AreCoreQueriesSupported()) { return nullptr; } GLuint queryId = 0; @@ -4686,22 +4689,47 @@ namespace MobileGL::MG_Backend::DirectGLES { if (queryId == 0) { return nullptr; } + g_GLESFuncs.glBeginQuery(target, queryId); + return new GLESQueryObject{queryId, g_syncContextGeneration, target}; + } + + static void EndCoreQuery(BackendQueryHandle handle) { + const auto* query = static_cast(handle); + if (query == nullptr || query->coreTarget == 0 || + query->contextGeneration != g_syncContextGeneration || !IsBackendContextCurrentOnThisThread() || + !g_GLESFuncs.glEndQuery) { + return; + } + g_GLESFuncs.glEndQuery(query->coreTarget); + } + + Bool AreOcclusionQueriesSupported() { return AreCoreQueriesSupported(); } + + BackendQueryHandle BeginOcclusionQuery() { // ES only implements the boolean ANY_SAMPLES_PASSED variant, not an exact // GL_SAMPLES_PASSED count; the frontend already coerces ANY_SAMPLES_PASSED* // targets to boolean, and desktop GL_SAMPLES_PASSED reads a 0/1 approximation. - g_GLESFuncs.glBeginQuery(GL_ANY_SAMPLES_PASSED, queryId); - return new GLESQueryObject{queryId, g_syncContextGeneration, /*isOcclusion=*/true}; + return BeginCoreQuery(GL_ANY_SAMPLES_PASSED); } - void EndOcclusionQuery(BackendQueryHandle handle) { - const auto* query = static_cast(handle); - if (query == nullptr || query->contextGeneration != g_syncContextGeneration || - !IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glEndQuery) { - return; + void EndOcclusionQuery(BackendQueryHandle handle) { EndCoreQuery(handle); } + + // GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED. The frontend + // otherwise counts primitives on the CPU from the draw calls, which cannot see a + // geometry shader's amplification; the real driver's counters are exact. Returning + // null keeps that CPU accounting as the fallback. + BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated) { + // GL_PRIMITIVES_GENERATED is only a legal query target from ES 3.2 on (it comes + // with geometry shaders); issuing it earlier just leaves a stray GL_INVALID_ENUM + // that some later unrelated glGetError would report as its own failure. + if (generated && g_GLESCapabilities.GLESVersion.Major * 10 + g_GLESCapabilities.GLESVersion.Minor < 32) { + return nullptr; } - g_GLESFuncs.glEndQuery(GL_ANY_SAMPLES_PASSED); + return BeginCoreQuery(generated ? GL_PRIMITIVES_GENERATED : GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN); } + void EndXfbPrimitivesQuery(BackendQueryHandle handle) { EndCoreQuery(handle); } + Bool IsQueryResultAvailable(BackendQueryHandle handle) { const auto* query = static_cast(handle); // Null/stale handles report available so the frontend proceeds to @@ -4729,7 +4757,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // the handle. if (query == nullptr || query->contextGeneration != g_syncContextGeneration || !g_GLESFuncs.glGetQueryObjectuiv || - (!query->isOcclusion && !g_GLESFuncs.glGetQueryObjectui64vEXT)) { + (query->coreTarget == 0 && !g_GLESFuncs.glGetQueryObjectui64vEXT)) { return true; } // A thread that does not own the ES context cannot issue GL calls, @@ -4767,7 +4795,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // context switch) the result may be garbage, which is tolerable for // an F3 GPU% readout, and consuming the latched flag here could hide // the event from another observer. - if (query->isOcclusion) { + if (query->coreTarget != 0) { GLuint result32 = 0; g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT, &result32); *outNanoseconds = static_cast(result32); diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index 223a5f56..99a011cc 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -135,6 +135,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // flow through GetQueryResult64/DeleteBackendQuery like the timer queries above. BackendQueryHandle BeginOcclusionQuery(); void EndOcclusionQuery(BackendQueryHandle query); + // GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED, also core ES + // (GL_PRIMITIVES_GENERATED from ES 3.2 on). Null when the target is unavailable, in + // which case the frontend falls back to counting primitives from the draw calls. + BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated); + void EndXfbPrimitivesQuery(BackendQueryHandle query); Bool IsQueryResultAvailable(BackendQueryHandle query); // Returns true when a final value landed in *outNanoseconds (a zero for // null or stale-generation handles IS final: the frontend may cache it diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index e90ee908..31d23cb6 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -590,6 +590,12 @@ namespace MobileGL::MG_Impl::GLImpl { // vertex records of every odd triangle within each emitted strip. static void FixupGsStripCaptureOrder(const SharedPtr& program, Uint64 inputPrimitives) { + // Only Vulkan-order captures need this. A backend that runs the capture on its + // own GL/ES driver (it owns the span, hence the EndTransformFeedback entry) has + // already produced GL's vertex order, and reordering it again would corrupt it. + if (MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback != nullptr) { + return; + } if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) { return; }