diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 616cdd06..884a59b2 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -236,6 +236,14 @@ namespace MobileGL { // (optional; null = frontend falls back to CPU accounting). BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated); void (*EndXfbPrimitivesQuery)(BackendQueryHandle query); + // Whether GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN should be answered from the + // frontend's own accounting wherever that accounting is exact - a capture with no + // geometry stage - instead of from the query above. Set by DirectGLES, whose result + // is whatever the ES driver's PRIMITIVES_WRITTEN counter says: Adreno reports twice + // the written count for a vertex-only capture that follows a large render pass, + // where the desktop-exact answer is the one the frontend already computed. Defaults + // to false, so a backend that never sets it keeps using its GPU result. + Bool PrefersCpuXfbPrimitiveAccounting = false; // 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 diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index b35b82cc..2a212c4e 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -1166,6 +1166,12 @@ namespace MobileGL::MG_Backend::DirectGLES { // geometry shader's amplification. funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery; funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery; + // ...but where it CAN see the whole capture - no geometry stage - the frontend's + // own count is the desktop-exact one and the ES driver's is only as good as the + // vendor made it (Adreno doubles PRIMITIVES_WRITTEN for a vertex-only capture that + // follows a large render pass). The query above stays installed: it is still what + // answers an amplifying span, and PRIMITIVES_GENERATED always. + funcsTable.GL.PrefersCpuXfbPrimitiveAccounting = true; funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable; funcsTable.GL.GetQueryResult64 = GetQueryResult64; funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery; diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index 080445a0..121af025 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -108,6 +108,12 @@ namespace MobileGL::MG_Impl::GLImpl { const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram(); if (program != nullptr) { + // A geometry stage writes what it emits, not what the draw assembled, and the + // amplification factor lives in the shader. Record that this span contained such + // a draw so the transform feedback queries keep their backend result for it. + if (program->GetShaderIndexByStage(ShaderStage::Geometry) >= 0) { + MG_State::pGLContext->AddTransformFeedbackGeometryCaptureDraw(); + } // Capacity in captured vertices = the tightest bound buffer. Uint64 capacityVertices = ~0ull; for (SizeT i = 0; i < program->GetTransformFeedbackBufferCount(); ++i) { @@ -127,6 +133,11 @@ namespace MobileGL::MG_Impl::GLImpl { } MG_State::pGLContext->AddTransformFeedbackPrimitives(primitives); MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive); + // Only draws that get this far are in the written counter at all. The instanced and + // indirect entry points never call this function, so a span that contains one is NOT + // fully accounted, and the queries must be able to tell: they compare this counter's + // delta against zero before standing in for the backend's own result. + MG_State::pGLContext->AddTransformFeedbackAccountedCaptureDraw(); } // Every primitive mode a draw command accepts (GL 4.6 core table 10.1, plus diff --git a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp index 5a7d84f1..c8b95895 100644 --- a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp +++ b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp @@ -31,8 +31,15 @@ namespace MobileGL::MG_Impl::GLImpl { Bool ended = false; Bool resultCached = false; Uint64 cachedResult = 0; - // Transform feedback primitive counter at BeginQuery time. + // The transform feedback primitive counter matching this query's target, at + // BeginQuery time. Uint64 counterSnapshot = 0; + // Capture-draw counters at BeginQuery time: how many capture draws the CPU + // accounting had reproduced exactly, and how many of those it could not (a + // geometry stage amplifies). Their deltas decide whether the CPU result may + // stand in for the backend's. + Uint64 accountedCaptureDrawSnapshot = 0; + Uint64 geometryCaptureDrawSnapshot = 0; }; // Query calls may arrive from any thread (launchers migrate the context @@ -122,6 +129,46 @@ namespace MobileGL::MG_Impl::GLImpl { g_activeTimeElapsedQueryId = 0; } + // The CPU accounting counter a transform feedback query target reads: what the capture + // buffers took for GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, and everything the capture + // stage assembled - a paused span included - for GL_PRIMITIVES_GENERATED. One counter + // for both targets would report the clamped written count as the generated one. + Uint64 TransformFeedbackCounterForTarget(GLenum target) { + return target == GL_PRIMITIVES_GENERATED + ? MG_State::pGLContext->GetTransformFeedbackGeneratedCounter() + : MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter(); + } + + // The span's CPU accounting delta. Saturating: a snapshot left above its counter (a + // context switch between Begin and End, a counter that never moved) would otherwise + // wrap to 2^64-1, which GetQueryObjectuiv hands the app as 4294967295. + Uint64 TransformFeedbackCpuResult(const QueryObject* queryObject) { + const Uint64 counter = TransformFeedbackCounterForTarget(queryObject->target); + return counter > queryObject->counterSnapshot ? counter - queryObject->counterSnapshot : 0; + } + + // Whether this ended span's result should come from the CPU accounting rather than from + // the backend query it also ran. Three conditions, all necessary: + // * the backend asked for it (DirectGLES, whose ES driver counter is the unreliable + // one; DirectVulkan never sets the bit and so is untouched by any of this); + // * the target is PRIMITIVES_WRITTEN. GL_PRIMITIVES_GENERATED counts primitives + // whether or not a capture is active, and the accounting only ever sees capture + // draws, so the backend's counter is the more complete answer there; + // * the span was fully accounted: at least one capture draw reached the accounting + // (the instanced, indirect and multi-draw entry points do not call it at all, so a + // span made of those is invisible to it) and none of them amplified through a + // geometry stage, which the CPU cannot model. + Bool PrefersCpuTransformFeedbackResult(const QueryObject* queryObject) { + if (!MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting) return false; + if (queryObject->target != GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN) return false; + if (MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws() != + queryObject->geometryCaptureDrawSnapshot) { + return false; + } + return MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws() != + queryObject->accountedCaptureDrawSnapshot; + } + // Shared GetQueryObject* implementation. Returns false when an error // was recorded and no value should be written back. `outValueProduced`, when given, // additionally distinguishes "succeeded with a value" from "succeeded but the result is not @@ -407,7 +454,11 @@ namespace MobileGL::MG_Impl::GLImpl { const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery; queryObject->backendHandle = beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr; - queryObject->counterSnapshot = MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter(); + queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target); + queryObject->accountedCaptureDrawSnapshot = + MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws(); + queryObject->geometryCaptureDrawSnapshot = + MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws(); } else if (isOcclusionQuery) { queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery(); } else { @@ -448,12 +499,21 @@ namespace MobileGL::MG_Impl::GLImpl { if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) { endXfbPrimitivesQuery(queryObject->backendHandle); } - // Result comes from the GPU query at read time. - } else { - queryObject->cachedResult = - MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter() - queryObject->counterSnapshot; + } + // A backend query that is not going to be read is released here, not left to be + // collected later: the span is over, the driver object has nothing left to say. + // Ending it first is what makes that legal. + if (!queryObject->backendHandle || PrefersCpuTransformFeedbackResult(queryObject)) { + if (queryObject->backendHandle) { + if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) { + deleteBackendQuery(queryObject->backendHandle); + } + queryObject->backendHandle = nullptr; + } + queryObject->cachedResult = TransformFeedbackCpuResult(queryObject); queryObject->resultCached = true; } + // Otherwise the result comes from the GPU query at read time. queryObject->active = false; queryObject->ended = true; activeQueryId = 0; diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index 4dfb1078..b72c899d 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -328,6 +328,7 @@ namespace MobileGL { // transform feedback counter cannot see them - nothing was being captured. void AddTransformFeedbackPausedPrimitives(Uint64 primitives) { m_transformFeedbackPausedPrimitiveCounter += primitives; + m_transformFeedbackGeneratedPrimitiveCounter += primitives; } Uint64 GetTransformFeedbackPausedPrimitiveCounter() const { return m_transformFeedbackPausedPrimitiveCounter; @@ -342,8 +343,30 @@ namespace MobileGL { // (pre-clamp; drives the GS strip capture-order fixup at EndTF). void AddTransformFeedbackInputPrimitives(Uint64 primitives) { m_transformFeedbackInputPrimitives += primitives; + m_transformFeedbackGeneratedPrimitiveCounter += primitives; } Uint64 GetTransformFeedbackInputPrimitives() const { return m_transformFeedbackInputPrimitives; } + // What a GL_PRIMITIVES_GENERATED query counts over its span: every primitive the + // capture stage assembled, including the ones a paused span discarded (those are + // generated but never written). Kept as its own running total rather than derived + // from the input counter above, which BeginTransformFeedback resets per span while + // a query may cover several of them. + Uint64 GetTransformFeedbackGeneratedCounter() const { + return m_transformFeedbackGeneratedPrimitiveCounter; + } + // Capture draws whose written-primitive count the CPU accounting reproduced + // exactly, and the subset it could not: a program with a geometry stage amplifies + // by whatever the shader emits, which only the driver's own counter knows. The + // transform feedback queries diff both over their span to decide whether the CPU + // delta may stand in for the backend's GPU result (GL_Query.cpp). + void AddTransformFeedbackAccountedCaptureDraw() { ++m_transformFeedbackAccountedCaptureDraws; } + Uint64 GetTransformFeedbackAccountedCaptureDraws() const { + return m_transformFeedbackAccountedCaptureDraws; + } + void AddTransformFeedbackGeometryCaptureDraw() { ++m_transformFeedbackGeometryCaptureDraws; } + Uint64 GetTransformFeedbackGeometryCaptureDraws() const { + return m_transformFeedbackGeometryCaptureDraws; + } // Transform feedback objects (ARB_transform_feedback2 / GL 4.0 core). // The capture state above and the indexed GL_TRANSFORM_FEEDBACK_BUFFER @@ -439,6 +462,9 @@ namespace MobileGL { Uint64 m_transformFeedbackPausedPrimitiveCounter = 0; Uint64 m_transformFeedbackCapturedVertices = 0; Uint64 m_transformFeedbackInputPrimitives = 0; + Uint64 m_transformFeedbackGeneratedPrimitiveCounter = 0; + Uint64 m_transformFeedbackAccountedCaptureDraws = 0; + Uint64 m_transformFeedbackGeometryCaptureDraws = 0; // Everything a transform feedback object owns while it is NOT the bound one. struct TransformFeedbackObjectState { diff --git a/MobileGL/MG_Test/Query/QueryTest.cpp b/MobileGL/MG_Test/Query/QueryTest.cpp index 78c3c7d9..34430c8e 100644 --- a/MobileGL/MG_Test/Query/QueryTest.cpp +++ b/MobileGL/MG_Test/Query/QueryTest.cpp @@ -19,6 +19,7 @@ #include #include #include +#include using namespace MobileGL; @@ -119,6 +120,56 @@ namespace { g_stubResultObtainable = true; g_stubResultNs = 0; } + + // Stub backend transform feedback primitive queries. g_stubXfbQuerySupported = false + // models a backend with no GPU counter at all (null handle), which is what leaves the + // frontend's CPU accounting as the only source; g_stubResultNs is what the "driver" + // would answer when its query IS read, deliberately set to a value the CPU accounting + // never produces so the two sources are told apart. + Int g_stubXfbBeginCount = 0; + Int g_stubXfbEndCount = 0; + Bool g_stubXfbQuerySupported = true; + + MG_Backend::BackendQueryHandle StubBeginXfbPrimitivesQuery(Bool) { + if (!g_stubXfbQuerySupported) { + return nullptr; + } + ++g_stubXfbBeginCount; + return reinterpret_cast(static_cast(0x53)); + } + + void StubEndXfbPrimitivesQuery(MG_Backend::BackendQueryHandle) { ++g_stubXfbEndCount; } + + void InstallStubBackendXfbQueries() { + auto& backendGL = MG_Backend::gBackendFunctionsTable.GL; + backendGL.BeginXfbPrimitivesQuery = StubBeginXfbPrimitivesQuery; + backendGL.EndXfbPrimitivesQuery = StubEndXfbPrimitivesQuery; + backendGL.IsQueryResultAvailable = StubIsQueryResultAvailable; + backendGL.GetQueryResult64 = StubGetQueryResult64; + backendGL.DeleteBackendQuery = StubDeleteBackendQuery; + // Off by default: the tests that exercise the DirectGLES preference turn it on. + backendGL.PrefersCpuXfbPrimitiveAccounting = false; + g_stubXfbBeginCount = 0; + g_stubXfbEndCount = 0; + g_stubXfbQuerySupported = true; + g_stubDeleteCount = 0; + g_stubResultAvailable = true; + g_stubResultObtainable = true; + g_stubResultNs = 0; + } + + // What AccountTransformFeedbackPrimitives (GL_Drawing.cpp) records for one captured + // draw, without needing a draw: `assembled` primitives came out of the vertex stage + // and `written` of them fitted in the capture buffers (they differ once the buffers + // overflow, which is the whole point of PRIMITIVES_WRITTEN). + void SimulateAccountedCaptureDraw(Uint64 assembled, Uint64 written, Bool throughGeometryStage = false) { + MG_State::pGLContext->AddTransformFeedbackInputPrimitives(assembled); + if (throughGeometryStage) { + MG_State::pGLContext->AddTransformFeedbackGeometryCaptureDraw(); + } + MG_State::pGLContext->AddTransformFeedbackPrimitives(written); + MG_State::pGLContext->AddTransformFeedbackAccountedCaptureDraw(); + } } // namespace class QueryTest : public ::testing::Test { @@ -448,6 +499,178 @@ TEST_F(QueryTest, BackendResultsPropagateThroughFrontend) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +// The two transform feedback targets count different things and must therefore read +// different counters: PRIMITIVES_WRITTEN what the capture buffers took, PRIMITIVES_GENERATED +// every primitive the capture stage assembled - including the ones a paused span threw away, +// which are generated but never written. Answering both from the written counter (as the +// fallback used to) reports the clamped number as the generated one. +TEST_F(QueryTest, TransformFeedbackQueryTargetsReadTheirOwnCounter) { + const ScopedBackendFunctionsOverride backendGuard; + InstallStubBackendXfbQueries(); + g_stubXfbQuerySupported = false; // no GPU counter: the CPU accounting is the only source + + GLuint ids[2] = {0, 0}; + MG_Impl::GLImpl::GenQueries(2, ids); + ASSERT_NE(ids[0], 0u); + ASSERT_NE(ids[1], 0u); + + MG_Impl::GLImpl::BeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, ids[0]); + MG_Impl::GLImpl::BeginQuery(GL_PRIMITIVES_GENERATED, ids[1]); + // Four points assembled into a buffer with room for three. + SimulateAccountedCaptureDraw(/*assembled=*/4, /*written=*/3); + // ...and two more points assembled while the span was paused: generated, never written. + MG_State::pGLContext->AddTransformFeedbackPausedPrimitives(2); + MG_Impl::GLImpl::EndQuery(GL_PRIMITIVES_GENERATED); + MG_Impl::GLImpl::EndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN); + + GLuint written = 0; + GLuint generated = 0; + MG_Impl::GLImpl::GetQueryObjectuiv(ids[0], GL_QUERY_RESULT, &written); + MG_Impl::GLImpl::GetQueryObjectuiv(ids[1], GL_QUERY_RESULT, &generated); + EXPECT_EQ(written, 3u); + EXPECT_EQ(generated, 6u); + + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::DeleteQueries(2, ids); +} + +// A query span that captured nothing at all reads zero from the CPU accounting rather than +// the unsigned wrap-around a bare End-minus-Begin subtraction produces the moment the +// snapshot is not below the counter (GetQueryObjectuiv would hand the app 4294967295). +TEST_F(QueryTest, AnEmptyTransformFeedbackSpanReadsZero) { + const ScopedBackendFunctionsOverride backendGuard; + InstallStubBackendXfbQueries(); + g_stubXfbQuerySupported = false; + + GLuint id = 0; + MG_Impl::GLImpl::GenQueries(1, &id); + ASSERT_NE(id, 0u); + + MG_Impl::GLImpl::BeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, id); + MG_Impl::GLImpl::EndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN); + + GLuint result = 123u; + MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result); + EXPECT_EQ(result, 0u); + + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::DeleteQueries(1, &id); +} + +// The DirectGLES preference: for a capture the frontend counted exactly - every draw +// accounted, none of them amplified by a geometry stage - the CPU number is the +// desktop-exact one and the ES driver's PRIMITIVES_WRITTEN counter is not consulted, even +// though the backend query ran. The backend query object is released at EndQuery instead of +// being left to a result read that will never come. +TEST_F(QueryTest, VertexOnlyCaptureSpansPreferTheCpuPrimitiveAccounting) { + const ScopedBackendFunctionsOverride backendGuard; + InstallStubBackendXfbQueries(); + MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting = true; + g_stubResultNs = 6; // what the driver's counter would have said - twice the truth + + GLuint id = 0; + MG_Impl::GLImpl::GenQueries(1, &id); + ASSERT_NE(id, 0u); + + MG_Impl::GLImpl::BeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, id); + SimulateAccountedCaptureDraw(/*assembled=*/4, /*written=*/3); + MG_Impl::GLImpl::EndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN); + EXPECT_EQ(g_stubXfbBeginCount, 1); + EXPECT_EQ(g_stubXfbEndCount, 1); + EXPECT_EQ(g_stubDeleteCount, 1); // ended, then released - not leaked + + GLint available = -1; + MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available); + EXPECT_EQ(available, 1); + + GLuint result = 0; + MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result); + EXPECT_EQ(result, 3u); + EXPECT_EQ(g_stubDeleteCount, 1); // the read had no handle left to release + + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::DeleteQueries(1, &id); + EXPECT_EQ(g_stubDeleteCount, 1); +} + +// The regression gate for that preference: a capture fed by a geometry stage writes whatever +// the shader emits, which the CPU accounting cannot model, so the backend's counter stays the +// answer and its handle survives EndQuery to be read later. +TEST_F(QueryTest, AGeometryStageCaptureKeepsTheBackendPrimitiveResult) { + const ScopedBackendFunctionsOverride backendGuard; + InstallStubBackendXfbQueries(); + MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting = true; + g_stubResultNs = 9; // the amplified count only the driver knows + + GLuint id = 0; + MG_Impl::GLImpl::GenQueries(1, &id); + ASSERT_NE(id, 0u); + + MG_Impl::GLImpl::BeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, id); + SimulateAccountedCaptureDraw(/*assembled=*/1, /*written=*/1, /*throughGeometryStage=*/true); + MG_Impl::GLImpl::EndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN); + EXPECT_EQ(g_stubDeleteCount, 0); // still to be read + + GLuint result = 0; + MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result); + EXPECT_EQ(result, 9u); + EXPECT_EQ(g_stubDeleteCount, 1); + + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::DeleteQueries(1, &id); +} + +// The other half of that gate: the instanced, indirect and multi-draw entry points never +// reach the CPU accounting, so a span made of those moves no counter at all. Its delta would +// be zero, which is not "nothing was written" - it is "nothing was counted" - and the +// backend's result has to stand. +TEST_F(QueryTest, ACaptureSpanTheAccountingNeverSawKeepsTheBackendResult) { + const ScopedBackendFunctionsOverride backendGuard; + InstallStubBackendXfbQueries(); + MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting = true; + g_stubResultNs = 12; + + GLuint id = 0; + MG_Impl::GLImpl::GenQueries(1, &id); + ASSERT_NE(id, 0u); + + MG_Impl::GLImpl::BeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, id); + MG_Impl::GLImpl::EndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN); + + GLuint result = 0; + MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result); + EXPECT_EQ(result, 12u); + + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::DeleteQueries(1, &id); +} + +// GL_PRIMITIVES_GENERATED counts primitives whether or not a capture is active, while the +// CPU accounting only ever sees capture draws - so the preference above deliberately does +// not extend to that target, whatever the backend asked for. +TEST_F(QueryTest, PrimitivesGeneratedKeepsTheBackendResultUnderTheCpuPreference) { + const ScopedBackendFunctionsOverride backendGuard; + InstallStubBackendXfbQueries(); + MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting = true; + g_stubResultNs = 7; + + GLuint id = 0; + MG_Impl::GLImpl::GenQueries(1, &id); + ASSERT_NE(id, 0u); + + MG_Impl::GLImpl::BeginQuery(GL_PRIMITIVES_GENERATED, id); + SimulateAccountedCaptureDraw(/*assembled=*/4, /*written=*/3); + MG_Impl::GLImpl::EndQuery(GL_PRIMITIVES_GENERATED); + EXPECT_EQ(g_stubDeleteCount, 0); + + GLuint result = 0; + MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result); + EXPECT_EQ(result, 7u); + + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::DeleteQueries(1, &id); +} + // Environment-agnostic property test for the env -> ConfigLoader -> Features // chain: whatever MOBILEGL_DISABLE_TIMERQUERY is set to in the environment of // this test process, MG_ConfigLoader::Init must have parsed it with the