From 1740a8a41a6b506f6acdb6869fac115fee2a346f Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 12:59:22 -0400 Subject: [PATCH] [Feat, Test] (GLImpl, GLState): implement glBeginConditionalRender and discard the commands GL 4.6 10.9 names --- .../MG_Impl/GLImpl/Drawing/GL_Drawing.cpp | 37 ++++++ .../MG_Impl/GLImpl/Exporting/Definitions.cpp | 4 +- .../GLImpl/Framebuffer/GL_Framebuffer.cpp | 8 ++ MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp | 69 ++++++++++ MobileGL/MG_Impl/GLImpl/Query/GL_Query.h | 5 + MobileGL/MG_State/GLState/Core.h | 32 +++++ MobileGL/MG_Test/Query/QueryTest.cpp | 125 ++++++++++++++++++ 7 files changed, 278 insertions(+), 2 deletions(-) diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index 408e08fb..ceaf42fb 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -326,10 +326,23 @@ namespace MobileGL::MG_Impl::GLImpl { } } + // GL 4.6 core 10.9: inside a conditional block whose predicate did not pass, the drawing + // commands, Clear, ClearBuffer* and the compute dispatches are DISCARDED. The gate sits on the + // wrappers that ISSUE the backend call rather than at the top of each entry point, so that + // everything a real driver would still do inside the block - argument validation and the + // errors it raises - happens exactly as it does outside one, and only the command itself is + // dropped. It is deliberately not on the frontend's transform-feedback accounting either: + // that mirrors what the capture stage would have written, and a conditional block around a + // capturing draw has no test coverage in either direction. + static Bool ConditionalRenderDiscardsCommand() { + return MG_State::pGLContext->ConditionalRenderDiscardsCommands(); + } + void Clear_Backend(GLbitfield mask) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.Clear(mask); } @@ -337,6 +350,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices); } @@ -345,6 +359,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount); } @@ -353,6 +368,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount, basevertex); } @@ -361,6 +377,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count); } @@ -368,6 +385,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount); } @@ -376,6 +394,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex); } @@ -384,6 +403,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride); } @@ -391,6 +411,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride); } @@ -399,6 +420,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount, maxdrawcount, stride); } @@ -408,6 +430,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount, stride); } @@ -417,6 +440,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices, basevertex); } @@ -426,6 +450,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices); } @@ -435,6 +460,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance( mode, count, type, indices, instancecount, basevertex, baseinstance); } @@ -444,6 +470,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex); } @@ -453,6 +480,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices, instancecount, baseinstance); } @@ -462,6 +490,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount); } @@ -469,6 +498,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect); } void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, @@ -476,6 +506,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount, baseinstance); } @@ -484,6 +515,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount); } @@ -491,6 +523,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect); } @@ -519,6 +552,9 @@ namespace MobileGL::MG_Impl::GLImpl { return; } } + // GL 4.3 added both dispatches to the conditional-render set (GL 4.6 core 10.9), which is + // exactly what KHR-GL43.compute_shader.conditional-dispatching checks. + if (ConditionalRenderDiscardsCommand()) return; dispatchCompute(numGroupsX, numGroupsY, numGroupsZ); } @@ -570,6 +606,7 @@ namespace MobileGL::MG_Impl::GLImpl { return; } if (!ValidateCurrentProgramForCompute(__func__)) return; + if (ConditionalRenderDiscardsCommand()) return; dispatchComputeIndirect(indirect); } diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index 0df3254c..c23a9f3e 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -725,8 +725,8 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, LoadName, GLuint name) DECLARE_GL_FUNCTION_S DECLARE_GL_FUNCTION_STUB_HEAD(void, PushName, GLuint name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushName, name) DECLARE_GL_FUNCTION_STUB_HEAD(void, PopName) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopName) DECLARE_GL_FUNCTION_HEAD(void, ClampColor, GLenum target, GLenum clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClampColor, target, clamp) -DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginConditionalRender, id, mode) -DECLARE_GL_FUNCTION_STUB_HEAD(void, EndConditionalRender, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndConditionalRender) +DECLARE_GL_FUNCTION_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginConditionalRender, id, mode) +DECLARE_GL_FUNCTION_HEAD(void, EndConditionalRender) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndConditionalRender) DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI1i, GLuint index, GLint x) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI1i, index, x) DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI2i, GLuint index, GLint x, GLint y) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI2i, index, x, y) DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI3i, GLuint index, GLint x, GLint y, GLint z) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI3i, index, x, y, z) diff --git a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp index 60292eea..8bd69f80 100644 --- a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp +++ b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp @@ -2613,18 +2613,26 @@ namespace MobileGL::MG_Impl::GLImpl { } void ClearBufferfi_Backend(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { + // GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands. + if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return; MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil); } void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) { + // GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands. + if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return; MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value); } void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* value) { + // GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands. + if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return; MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value); } void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* value) { + // GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands. + if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return; MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value); } diff --git a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp index c8b95895..dcc470f5 100644 --- a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp +++ b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp @@ -565,6 +565,75 @@ namespace MobileGL::MG_Impl::GLImpl { queryObject->ended = true; } + void BeginConditionalRender(GLuint id, GLenum mode) { + // GL 4.6 core 10.9's eight modes. The _INVERTED half flips the sense of the predicate; + // the BY_REGION half only narrows WHERE an implementation is permitted to discard, so + // treating it as its whole-framebuffer sibling is what an implementation without region + // granularity does. The _NO_WAIT half is a permission to render rather than stall, not an + // obligation - see the resolve below. + Bool inverted = false; + switch (mode) { + case GL_QUERY_WAIT: + case GL_QUERY_NO_WAIT: + case GL_QUERY_BY_REGION_WAIT: + case GL_QUERY_BY_REGION_NO_WAIT: + inverted = false; + break; + case GL_QUERY_WAIT_INVERTED: + case GL_QUERY_NO_WAIT_INVERTED: + case GL_QUERY_BY_REGION_WAIT_INVERTED: + case GL_QUERY_BY_REGION_NO_WAIT_INVERTED: + inverted = true; + break; + default: + RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "mode is not a conditional render mode."); + return; + } + + if (MG_State::pGLContext->IsConditionalRenderActive()) { + RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is already active."); + return; + } + + { + const std::lock_guard lock(g_queryObjectsMutex); + const auto* queryObject = FindQueryObjectLocked(id); + // A generated NAME is not yet a query object; it becomes one at its first use with a + // target (the same rule glIsQuery answers by). + if (!queryObject || (!queryObject->created && queryObject->target == 0)) { + RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "id is not the name of a query object."); + return; + } + if (queryObject->active) { + RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "The query object is still active."); + return; + } + if (queryObject->target != GL_SAMPLES_PASSED && queryObject->target != GL_ANY_SAMPLES_PASSED && + queryObject->target != GL_ANY_SAMPLES_PASSED_CONSERVATIVE) { + RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, + "Conditional rendering requires an occlusion query object."); + return; + } + } + + // Resolved ONCE, here, and by WAITING even for the _NO_WAIT modes: the spec lets those + // render instead of stalling, so always waiting is conforming and is the only choice that + // gives the whole block one deterministic verdict. Reading it per command instead would + // let a result that lands mid-block change the answer half way through. + Uint64 samplesPassed = 0; + if (!GetQueryObjectValue(id, GL_QUERY_RESULT, __FUNCTION__, samplesPassed)) return; + const Bool passed = samplesPassed != 0; + MG_State::pGLContext->BeginConditionalRender(id, mode, inverted ? passed : !passed); + } + + void EndConditionalRender() { + if (!MG_State::pGLContext->IsConditionalRenderActive()) { + RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is not active."); + return; + } + MG_State::pGLContext->EndConditionalRender(); + } + void GetQueryiv(GLenum target, GLenum pname, GLint* params) { if (!params) { return; diff --git a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h index 5921e7d2..51998693 100644 --- a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h +++ b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h @@ -29,6 +29,11 @@ namespace MobileGL::MG_Impl::GLImpl { void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset); void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset); void QueryCounter(GLuint id, GLenum target); + // Conditional rendering (GL 4.6 core 10.9). Implemented here rather than beside the drawing + // entry points because the predicate is a QUERY OBJECT's result, and the object registry - + // with the lock that guards it - lives in this file. + void BeginConditionalRender(GLuint id, GLenum mode); + void EndConditionalRender(); // Destroys every still-registered query object exactly as DeleteQueries would. // GL requires queries to die with their context; called only from full library // teardown (DestroyImpl), where no context survives on any thread, so the diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index b72c899d..70c2a5e9 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -368,6 +368,31 @@ namespace MobileGL { return m_transformFeedbackGeometryCaptureDraws; } + // Conditional rendering (GL 4.6 core 10.9). `discard` is the verdict already + // resolved from the query object at glBeginConditionalRender - the predicate is + // read ONCE there, not per command, because GL specifies the block against the + // result available at Begin and re-reading it would let a query that is still + // being written change the answer mid-block. + void BeginConditionalRender(GLuint queryId, GLenum mode, Bool discard) { + m_conditionalRenderActive = true; + m_conditionalRenderQuery = queryId; + m_conditionalRenderMode = mode; + m_conditionalRenderDiscards = discard; + } + void EndConditionalRender() { + m_conditionalRenderActive = false; + m_conditionalRenderQuery = 0; + m_conditionalRenderMode = GL_NONE; + m_conditionalRenderDiscards = false; + } + Bool IsConditionalRenderActive() const { return m_conditionalRenderActive; } + GLuint GetConditionalRenderQuery() const { return m_conditionalRenderQuery; } + // Whether the commands GL 4.6 core 10.9 makes conditional are being discarded + // right now. False whenever no block is open, so a caller needs no second test. + Bool ConditionalRenderDiscardsCommands() const { + return m_conditionalRenderActive && m_conditionalRenderDiscards; + } + // 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 @@ -466,6 +491,13 @@ namespace MobileGL { Uint64 m_transformFeedbackAccountedCaptureDraws = 0; Uint64 m_transformFeedbackGeometryCaptureDraws = 0; + // Conditional rendering. Context state, not object state: GL 4.6 core 10.9 allows + // exactly one block open at a time and no object owns it. + Bool m_conditionalRenderActive = false; + Bool m_conditionalRenderDiscards = false; + GLuint m_conditionalRenderQuery = 0; + GLenum m_conditionalRenderMode = GL_NONE; + // Everything a transform feedback object owns while it is NOT the bound one. struct TransformFeedbackObjectState { struct SavedBufferBinding { diff --git a/MobileGL/MG_Test/Query/QueryTest.cpp b/MobileGL/MG_Test/Query/QueryTest.cpp index 34430c8e..ed7d93e9 100644 --- a/MobileGL/MG_Test/Query/QueryTest.cpp +++ b/MobileGL/MG_Test/Query/QueryTest.cpp @@ -140,6 +140,29 @@ namespace { void StubEndXfbPrimitivesQuery(MG_Backend::BackendQueryHandle) { ++g_stubXfbEndCount; } + // Stub backend occlusion queries. The host has no ES context, and BeginQuery refuses the + // occlusion targets outright when the backend advertises no hook - so a conditional-render + // test cannot get a legal predicate object without these. g_stubResultNs is the sample count + // the "driver" reports, which is the whole input to the predicate. + MG_Backend::BackendQueryHandle StubBeginOcclusionQuery() { + return reinterpret_cast(static_cast(0x54)); + } + + void StubEndOcclusionQuery(MG_Backend::BackendQueryHandle) {} + + void InstallStubBackendOcclusionQueries() { + auto& backendGL = MG_Backend::gBackendFunctionsTable.GL; + backendGL.BeginOcclusionQuery = StubBeginOcclusionQuery; + backendGL.EndOcclusionQuery = StubEndOcclusionQuery; + backendGL.IsQueryResultAvailable = StubIsQueryResultAvailable; + backendGL.GetQueryResult64 = StubGetQueryResult64; + backendGL.DeleteBackendQuery = StubDeleteBackendQuery; + g_stubDeleteCount = 0; + g_stubResultAvailable = true; + g_stubResultObtainable = true; + g_stubResultNs = 0; + } + void InstallStubBackendXfbQueries() { auto& backendGL = MG_Backend::gBackendFunctionsTable.GL; backendGL.BeginXfbPrimitivesQuery = StubBeginXfbPrimitivesQuery; @@ -677,6 +700,108 @@ TEST_F(QueryTest, PrimitivesGeneratedKeepsTheBackendResultUnderTheCpuPreference) // unified truthy rule (set, non-empty, not "0", case-insensitive not "false"). // Running the binary under MOBILEGL_DISABLE_TIMERQUERY=1 therefore exercises // the real end-to-end path rather than the struct field alone. +// KHR-GL43.compute_shader.conditional-dispatching and the conditional_render family. +// glBeginConditionalRender/glEndConditionalRender were bare stubs: every command inside a +// conditional block executed whatever the query said, so the block that should have been +// discarded ran and doubled the atomic counter the case reads back. +TEST_F(QueryTest, ConditionalRenderResolvesItsPredicateFromTheOcclusionQuery) { + ScopedBackendFunctionsOverride backendGuard; + InstallStubBackendOcclusionQueries(); + + GLuint ids[2] = {0, 0}; + MG_Impl::GLImpl::GenQueries(2, ids); + ASSERT_NE(ids[0], 0u); + ASSERT_NE(ids[1], 0u); + + // One span that saw samples and one that saw none, which is exactly the pair the + // conformance case builds out of a passing and a failing depth test. + g_stubResultNs = 1; + MG_Impl::GLImpl::BeginQuery(GL_ANY_SAMPLES_PASSED, ids[0]); + MG_Impl::GLImpl::EndQuery(GL_ANY_SAMPLES_PASSED); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + GLuint passedResult = 0xFFFFFFFFu; + MG_Impl::GLImpl::GetQueryObjectuiv(ids[0], GL_QUERY_RESULT, &passedResult); + ASSERT_EQ(passedResult, 1u); + + g_stubResultNs = 0; + MG_Impl::GLImpl::BeginQuery(GL_ANY_SAMPLES_PASSED, ids[1]); + MG_Impl::GLImpl::EndQuery(GL_ANY_SAMPLES_PASSED); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // A block on the query that passed executes. + MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_TRUE(MG_State::pGLContext->IsConditionalRenderActive()); + EXPECT_FALSE(MG_State::pGLContext->ConditionalRenderDiscardsCommands()); + MG_Impl::GLImpl::EndConditionalRender(); + EXPECT_FALSE(MG_State::pGLContext->IsConditionalRenderActive()); + EXPECT_FALSE(MG_State::pGLContext->ConditionalRenderDiscardsCommands()); + + // A block on the query that did not passes nothing through. + MG_Impl::GLImpl::BeginConditionalRender(ids[1], GL_QUERY_WAIT); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_TRUE(MG_State::pGLContext->ConditionalRenderDiscardsCommands()); + MG_Impl::GLImpl::EndConditionalRender(); + + // ...and the _INVERTED modes swap both verdicts. + MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT_INVERTED); + EXPECT_TRUE(MG_State::pGLContext->ConditionalRenderDiscardsCommands()); + MG_Impl::GLImpl::EndConditionalRender(); + MG_Impl::GLImpl::BeginConditionalRender(ids[1], GL_QUERY_BY_REGION_NO_WAIT_INVERTED); + EXPECT_FALSE(MG_State::pGLContext->ConditionalRenderDiscardsCommands()); + MG_Impl::GLImpl::EndConditionalRender(); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteQueries(2, ids); +} + +TEST_F(QueryTest, ConditionalRenderRejectsTheErrorsTheSpecNames) { + ScopedBackendFunctionsOverride backendGuard; + InstallStubBackendOcclusionQueries(); + + GLuint ids[2] = {0, 0}; + MG_Impl::GLImpl::GenQueries(2, ids); + g_stubResultNs = 1; + MG_Impl::GLImpl::BeginQuery(GL_ANY_SAMPLES_PASSED, ids[0]); + MG_Impl::GLImpl::EndQuery(GL_ANY_SAMPLES_PASSED); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // GL 4.6 core 10.9, one rule at a time. + MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_TIME_ELAPSED); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_ENUM); + EXPECT_FALSE(MG_State::pGLContext->IsConditionalRenderActive()); + + // A generated NAME is not yet a query object. + MG_Impl::GLImpl::BeginConditionalRender(ids[1], GL_QUERY_WAIT); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); + MG_Impl::GLImpl::BeginConditionalRender(0, GL_QUERY_WAIT); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); + + // A query that is not an occlusion query cannot drive one. + GLuint timerId = 0; + MG_Impl::GLImpl::GenQueries(1, &timerId); + MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, timerId); + MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::BeginConditionalRender(timerId, GL_QUERY_WAIT); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // End without a block, and a nested Begin. + MG_Impl::GLImpl::EndConditionalRender(); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + // The rejected nested Begin must not have disturbed the open block. + EXPECT_EQ(MG_State::pGLContext->GetConditionalRenderQuery(), ids[0]); + MG_Impl::GLImpl::EndConditionalRender(); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteQueries(2, ids); + MG_Impl::GLImpl::DeleteQueries(1, &timerId); +} + TEST_F(QueryTest, DisableTimerQueryFeatureMatchesEnvironment) { const char* raw = std::getenv("MOBILEGL_DISABLE_TIMERQUERY"); Bool expected = false;