CVE-2026-91738
Overview
Background
- Occlusion query
- a GL query object that counts whether/how many fragments pass the depth/stencil test, implemented on Metal via
setVisibilityResultModewriting a per-query result into a visibility buffer. - Visibility result offset
- the byte offset into the visibility result buffer where a given query’s sample count is stored, passed to Metal for each active query within a render pass encoder.
- `mMaxVisibilityQueryOffset`
- the device-dependent maximum visibility offset Metal accepts inside one render pass (
262136on Apple GPU family 7, otherwise65528). - Render pass encoder
- a
MTLRenderCommandEncoder(mRenderEncoder) whose visibility result buffer and offsets are fixed for the life of the encoding, so exceeding the offset limit requires ending and restarting encoding.
Root Cause Analysis
When a long-lived occlusion query spanned enough render passes, ContextMtl kept allocating fresh visibility result offsets from mtl::OcclusionQueryPool and feeding them to mRenderEncoder via startOcclusionQueryInRenderPass without ever checking them against the Metal device’s maximum supported visibility offset. The invariant that the offset handed to setVisibilityResultMode stays within mMaxVisibilityQueryOffset was never enforced, so a sufficiently large allocated offset became invalid input to the Metal driver.
The fix introduces mtl::OcclusionQueryPool::canAllocateQueryOffset, and both when a query begins and when the querying process resumes in the dirty-render-pass path it now calls endEncoding(true) before crossing the limit, forcing a new render pass encoder whose offsets restart from a valid range. The obsolete restartActiveOcclusionQueryInRenderPass helper is removed because restart is now driven from setupDraw gated on getVisibilityResultMode() == MTLVisibilityResultModeDisabled plus the new offset check.
canAllocateQueryOffset bound check that splits the render pass whenever the next offset would exceed mMaxVisibilityQueryOffset.Attack Path
- Begin a long occlusion query
Web content (WebGL/WebGL2) starts an occlusion/
ANY_SAMPLES_PASSEDquery that must remain active across many draw calls and render-pass transitions. - Force many render-pass allocations
By repeatedly triggering render-pass breaks while the query stays active, the attacker drives
mtl::OcclusionQueryPoolto allocate an ever-growing sequence of visibility result offsets. - Cross the device offset limit
Without the check, an allocated offset eventually exceeds
mMaxVisibilityQueryOffsetand is passed tosetVisibilityResultModeon the render encoder, violating a Metal input constraint. - Trigger the improper-input condition
The out-of-range offset reaches the Metal driver as invalid input, corrupting occlusion-query bookkeeping or causing a driver-level fault.
(inferred) - Escalate from the malformed GPU submission
Any memory-safety consequence in the GPU process or Metal driver beyond the rejected/invalid submission is not demonstrated by the diff.
(inferred)
Impact Assessment
65528/262136 bytes. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6. Any escalation to memory corruption, information disclosure, or code execution in the GPU process is beyond what the patch demonstrates and is (inferred).Files Changed
util/autogen/angle_features_autogen.hsrc/libANGLE/renderer/metal/DisplayMtl.mmutil/autogen/angle_features_autogen.cppsrc/tests/angle_end2end_tests_expectations.txtsrc/libANGLE/renderer/metal/mtl_occlusion_query_pool.hsrc/libANGLE/renderer/metal/ContextMtl.mmsrc/libANGLE/renderer/metal/mtl_command_buffer.mmsrc/libANGLE/renderer/metal/DisplayMtl.hsrc/libANGLE/renderer/metal/mtl_occlusion_query_pool.mminclude/platform/autogen/FeaturesMtl_autogen.hsrc/libANGLE/renderer/metal/mtl_render_utils.mmsrc/libANGLE/renderer/metal/ContextMtl.hsrc/tests/gl_tests/OcclusionQueriesTest.cppsrc/libANGLE/renderer/metal/mtl_command_buffer.hinclude/platform/mtl_features.json
Audit Directions
- Unvalidated device-limit offsetsAudit every
setVisibilityResultMode, buffer-offset, and index/instance base passed to Metal encoders to confirm each is bounded against itsMTLDevice/DisplayMtlcapability before submission. - State spanning encoder boundariesReview query/transform-feedback/state that survives across render-pass restarts in
ContextMtl, since accumulated allocations (like visibility offsets) can silently outgrow single-pass limits. - Removed restart helpersLook for other in-place “restart”/“resume” paths that reallocate GPU resources without re-checking capacity limits, the same pattern that made
restartActiveOcclusionQueryInRenderPassunsafe.
Patch
From 2c5c60cd270d1596fa8abe06bd277983852f4b2b Mon Sep 17 00:00:00 2001 From: Le Hoang Quyen <[email protected]> Date: Wed, 22 Jul 2026 19:13:16 +0800 Subject: [PATCH] Metal: Split render pass on occlusion query offset limit When an occlusion query's visibility offset exceeds the maximum limit supported by the Metal device in a single render pass, end the active render pass encoder and restart the querying process in a new render pass encoder. Bug: chromium:536450979 Change-Id: If625475852239174105bcca6759b722de3423f13 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8134016 Reviewed-by: Geoff Lang <[email protected]> Commit-Queue: Quyen Le <[email protected]> --- diff --git a/include/platform/mtl_features.json b/include/platform/mtl_features.json index 78f0cbc..acdbfe0 100644 --- a/include/platform/mtl_features.json +++ b/include/platform/mtl_features.json @@ -418,6 +418,14 @@ "description": [ "Clear allocated textures to non-zero values to help detect uninitialized reads." ] + }, + { + "name": "limit_max_visibility_query_offset", + "category": "Features", + "description": [ + "Limit maximum visibility query offset to 65528 bytes." + ], + "issue": "http://anglebug.com/537661068" } ] } diff --git a/src/libANGLE/renderer/metal/ContextMtl.h b/src/libANGLE/renderer/metal/ContextMtl.h index bd10f2f..4ea4a3c 100644 --- a/src/libANGLE/renderer/metal/ContextMtl.h +++ b/src/libANGLE/renderer/metal/ContextMtl.h @@ -304,10 +304,6 @@ // Disable the occlusion query in the current render pass. // The render pass must already started. void disableActiveOcclusionQueryInRenderPass(); - // Re-enable the occlusion query in the current render pass. - // The render pass must already started. - // NOTE: the old query's result will be retained and combined with the new result. - angle::Result restartActiveOcclusionQueryInRenderPass(); // Invoke by TransformFeedbackMtl void onTransformFeedbackActive(const gl::Context *context, TransformFeedbackMtl *xfb); diff --git a/src/libANGLE/renderer/metal/ContextMtl.mm b/src/libANGLE/renderer/metal/ContextMtl.mm index c57e665..ad2cd40 100644 --- a/src/libANGLE/renderer/metal/ContextMtl.mm +++ b/src/libANGLE/renderer/metal/ContextMtl.mm @@ -2221,6 +2221,11 @@ ASSERT(mOcclusionQuery == nullptr); mOcclusionQuery = query; + if (mRenderEncoder.valid() && !mOcclusionQueryPool.canAllocateQueryOffset(this)) + { + endEncoding(true); + } + if (mRenderEncoder.valid()) { // if render pass has started, start the query in the encoder @@ -2270,16 +2275,6 @@ mOcclusionQuery->getAllocatedVisibilityOffsets().back()); } -angle::Result ContextMtl::restartActiveOcclusionQueryInRenderPass() -{ - if (!mOcclusionQuery || mOcclusionQuery->getAllocatedVisibilityOffsets().empty()) - { - return angle::Result::Continue; - } - - return startOcclusionQueryInRenderPass(mOcclusionQuery, false); -} - angle::Result ContextMtl::startOcclusionQueryInRenderPass(QueryMtl *query, bool clearOldValue) { ASSERT(mRenderEncoder.valid()); @@ -2504,10 +2499,18 @@ ANGLE_TRY(handleDirtyRenderPass(context)); } - if (mOcclusionQuery && mOcclusionQueryPool.getNumRenderPassAllocatedQueries() == 0) + if (mOcclusionQuery && + mRenderEncoder.getVisibilityResultMode() == MTLVisibilityResultModeDisabled) { - // The occlusion query is still active, and a new render pass has started. - // We need to continue the querying process in the new render encoder. + // The occlusion query is still active, and a new render pass has started or we have paused + // the querying process. We need to continue the querying process. + if (!mOcclusionQueryPool.canAllocateQueryOffset(this)) + { + // The render pass visibility query offset limit has been reached. End the current + // encoding; the caller will retry with a new render encoder. + endEncoding(true); + return angle::Result::Continue; + } ANGLE_TRY(startOcclusionQueryInRenderPass(mOcclusionQuery, false)); } diff --git a/src/libANGLE/renderer/metal/DisplayMtl.h b/src/libANGLE/renderer/metal/DisplayMtl.h index 6901918..f5da632 100644 --- a/src/libANGLE/renderer/metal/DisplayMtl.h +++ b/src/libANGLE/renderer/metal/DisplayMtl.h @@ -155,6 +155,7 @@ mtl::StateCache &getStateCache() { return mStateCache; } mtl::LibraryCache &getLibraryCache() { return mLibraryCache; } uint32_t getMaxColorTargetBits() { return mMaxColorTargetBits; } + uint32_t getMaxVisibilityQueryOffset() const { return mMaxVisibilityQueryOffset; } bool hasFragmentMemoryBarriers() const { return mHasFragmentMemoryBarriers; } id<MTLLibrary> getDefaultShadersLib(); @@ -219,6 +220,7 @@ mutable gl::Limitations mNativeLimitations; mutable ShPixelLocalStorageOptions mNativePLSOptions; mutable uint32_t mMaxColorTargetBits = 0; + mutable uint32_t mMaxVisibilityQueryOffset = 0; mutable bool mHasFragmentMemoryBarriers; angle::FeaturesMtl mFeatures; diff --git a/src/libANGLE/renderer/metal/DisplayMtl.mm b/src/libANGLE/renderer/metal/DisplayMtl.mm index 662e1ab..71f0c61 100644 --- a/src/libANGLE/renderer/metal/DisplayMtl.mm +++ b/src/libANGLE/renderer/metal/DisplayMtl.mm @@ -798,6 +798,15 @@ mMaxColorTargetBits = mNativeCaps.maxColorAttachments * 32; } + if (!mFeatures.limitMaxVisibilityQueryOffset.enabled && supportsAppleGPUFamily(7)) + { + mMaxVisibilityQueryOffset = 262136; + } + else + { + mMaxVisibilityQueryOffset = 65528; + } + // MSAA mNativeCaps.maxSamples = mFormatTable.getMaxSamples(); mNativeCaps.maxSampleMaskWords = 1; @@ -1204,6 +1213,11 @@ } ANGLE_FEATURE_CONDITION((&mFeatures), allowGenMultipleMipsPerPass, true); + + // TODO(anglebug.com/537661068): using visibility query offset > 65528 causes bugs in the Apple + // Silicon driver. + ANGLE_FEATURE_CONDITION((&mFeatures), limitMaxVisibilityQueryOffset, true); + ANGLE_FEATURE_CONDITION((&mFeatures), forceBufferGPUStorage, false); ANGLE_FEATURE_CONDITION((&mFeatures), hasExplicitMemBarrier, (isOSX || isCatalyst) && !isARM); ANGLE_FEATURE_CONDITION((&mFeatures), hasDepthAutoResolve, supportsEitherGPUFamily(3, 2)); diff --git a/src/libANGLE/renderer/metal/mtl_command_buffer.h b/src/libANGLE/renderer/metal/mtl_command_buffer.h index 8c9b9f2..2221ba0 100644 --- a/src/libANGLE/renderer/metal/mtl_command_buffer.h +++ b/src/libANGLE/renderer/metal/mtl_command_buffer.h @@ -562,6 +562,7 @@ uint32_t baseInstance); RenderCommandEncoder &setVisibilityResultMode(MTLVisibilityResultMode mode, size_t offset); + MTLVisibilityResultMode getVisibilityResultMode() const; RenderCommandEncoder &useResource(const BufferRef &resource, MTLResourceUsage usage, diff --git a/src/libANGLE/renderer/metal/mtl_command_buffer.mm b/src/libANGLE/renderer/metal/mtl_command_buffer.mm index feb19f5..1dd725d 100644 --- a/src/libANGLE/renderer/metal/mtl_command_buffer.mm +++ b/src/libANGLE/renderer/metal/mtl_command_buffer.mm @@ -2238,6 +2238,11 @@ return *this; } +MTLVisibilityResultMode RenderCommandEncoder::getVisibilityResultMode() const +{ + return mStateCache.visibilityResultMode; +} + RenderCommandEncoder &RenderCommandEncoder::useResource(const BufferRef &resource, MTLResourceUsage usage, MTLRenderStages stages) diff --git a/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.h b/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.h index 77db2e8..33325e9 100644 --- a/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.h +++ b/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.h @@ -39,6 +39,7 @@ // the render pass or the query that already has an allocated offset. // Note: a query might have more than one allocated offset. They will be combined in the final // step. + bool canAllocateQueryOffset(ContextMtl *contextMtl) const; angle::Result allocateQueryOffset(ContextMtl *contextMtl, QueryMtl *query, bool clearOldValue); // Deallocate all offsets used for a query. void deallocateQueryOffset(ContextMtl *contextMtl, QueryMtl *query); diff --git a/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.mm b/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.mm index 40163a1..8b5555d 100644 --- a/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.mm +++ b/src/libANGLE/renderer/metal/mtl_occlusion_query_pool.mm @@ -36,10 +36,20 @@ mAllocatedQueries.clear(); } +bool OcclusionQueryPool::canAllocateQueryOffset(ContextMtl *contextMtl) const +{ + uint32_t currentOffset = + static_cast<uint32_t>(mAllocatedQueries.size()) * kOcclusionQueryResultSize; + return currentOffset + kOcclusionQueryResultSize <= + contextMtl->getDisplay()->getMaxVisibilityQueryOffset(); +} + angle::Result OcclusionQueryPool::allocateQueryOffset(ContextMtl *contextMtl, QueryMtl *query, bool clearOldValue) { + CHECK(canAllocateQueryOffset(contextMtl)); + // Only query that already has allocated offset or first query of the render pass is allowed to // keep old value. Other queries must be reset to zero before counting the samples visibility in // draw calls. diff --git a/src/libANGLE/renderer/metal/mtl_render_utils.mm b/src/libANGLE/renderer/metal/mtl_render_utils.mm index 6cc0b00..0710ffe 100644 --- a/src/libANGLE/renderer/metal/mtl_render_utils.mm +++ b/src/libANGLE/renderer/metal/mtl_render_utils.mm @@ -224,14 +224,12 @@ angle::FormatID::R32G32B32A32_UINT, }; -// Class to automatically disable occlusion query upon entering block and re-able it upon -// exiting block. +// Class to automatically disable occlusion query upon entering block. The query will be resumed in +// the next ContextMtl's draw call. struct ScopedDisableOcclusionQuery { - ScopedDisableOcclusionQuery(ContextMtl *contextMtl, - RenderCommandEncoder *encoder, - angle::Result *resultOut) - : mContextMtl(contextMtl), mEncoder(encoder), mResultOut(resultOut) + ScopedDisableOcclusionQuery(ContextMtl *contextMtl, RenderCommandEncoder *encoder) + : mContextMtl(contextMtl), mEncoder(encoder) { #ifndef NDEBUG if (contextMtl->hasActiveOcclusionQuery()) @@ -244,13 +242,13 @@ } ~ScopedDisableOcclusionQuery() { - *mResultOut = mContextMtl->restartActiveOcclusionQueryInRenderPass(); #ifndef NDEBUG if (mContextMtl->hasActiveOcclusionQuery()) { mEncoder->popDebugGroup(); } #else + ANGLE_UNUSED_VARIABLE(mContextMtl); ANGLE_UNUSED_VARIABLE(mEncoder); #endif } @@ -258,8 +256,6 @@ private: ContextMtl *mContextMtl; RenderCommandEncoder *mEncoder; - - angle::Result *mResultOut; }; void GetBlitTexCoords(const NormalizedCoords &normalizedCoords, @@ -1163,10 +1159,9 @@ ContextMtl *contextMtl = GetImpl(context); ANGLE_TRY(setupClearWithDraw(context, cmdEncoder, overridedParams)); - angle::Result result; { // Need to disable occlusion query, otherwise clearing will affect the occlusion counting - ScopedDisableOcclusionQuery disableOcclusionQuery(contextMtl, cmdEncoder, &result); + ScopedDisableOcclusionQuery disableOcclusionQuery(contextMtl, cmdEncoder); // Draw the screen aligned triangle cmdEncoder->draw(MTLPrimitiveTypeTriangle, 0, 3); } @@ -1174,7 +1169,7 @@ // Invalidate current context's state contextMtl->invalidateState(context); - return result; + return angle::Result::Continue; } // ColorBlitUtils implementation @@ -1309,10 +1304,9 @@ ContextMtl *contextMtl = GetImpl(context); ANGLE_TRY(setupColorBlitWithDraw(context, cmdEncoder, params)); - angle::Result result; { // Need to disable occlusion query, otherwise blitting will affect the occlusion counting - ScopedDisableOcclusionQuery disableOcclusionQuery(contextMtl, cmdEncoder, &result); + ScopedDisableOcclusionQuery disableOcclusionQuery(contextMtl, cmdEncoder); // Draw the screen aligned quad cmdEncoder->draw(MTLPrimitiveTypeTriangleStrip, 0, 4); } @@ -1320,7 +1314,7 @@ // Invalidate current context's state contextMtl->invalidateState(context); - return result; + return angle::Result::Continue; } angle::Result DepthStencilBlitUtils::ensureShadersInitialized( @@ -1517,10 +1511,9 @@ ANGLE_TRY(setupDepthStencilBlitWithDraw(context, cmdEncoder, params)); - angle::Result result; { // Need to disable occlusion query, otherwise blitting will affect the occlusion counting - ScopedDisableOcclusionQuery disableOcclusionQuery(contextMtl, cmdEncoder, &result); + ScopedDisableOcclusionQuery disableOcclusionQuery(contextMtl, cmdEncoder); // Draw the screen aligned quad cmdEncoder->draw(MTLPrimitiveTypeTriangleStrip, 0, 4); } @@ -1528,7 +1521,7 @@ // Invalidate current context's state contextMtl->invalidateState(context); - return result; + return angle::Result::Continue; } angle::Result DepthStencilBlitUtils::blitStencilViaCopyBuffer( @@ -2493,10 +2486,9 @@ options.textureOffset[1] = params.textureArea.y; cmdEncoder->setFragmentData(options, 0); - angle::Result result; { // Need to disable occlusion query, otherwise blitting will affect the occlusion counting - ScopedDisableOcclusionQuery disableOcclusionQuery(contextMtl, cmdEncoder, &result); + ScopedDisableOcclusionQuery disableOcclusionQuery(contextMtl, cmdEncoder); // Draw the screen aligned quad cmdEncoder->draw(MTLPrimitiveTypeTriangleStrip, 0, 4); } @@ -2504,7 +2496,7 @@ // Invalidate current context's state contextMtl->invalidateState(context); - return result; + return angle::Result::Continue; } angle::Result CopyPixelsUtils::packPixelsCS(ContextMtl *contextMtl, diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt index 8c536a2..c3095c5 100644 --- a/src/tests/angle_end2end_tests_expectations.txt +++ b/src/tests/angle_end2end_tests_expectations.txt @@ -2850,6 +2850,9 @@ 529867799 VULKAN : ReadPixelsPBOTest.PackLargeRowLength/* = SKIP 529867799 INTEL OPENGL : ReadPixelsPBOTest.PackLargeRowLength/* = SKIP +// Using many queries will fail on Pixel 4 GLES +537692502 PIXEL4ORXL GLES : OcclusionQueriesTest.ExcessiveNumberOfQueries/* = SKIP + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // Slow tests, should appear last in this file // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! diff --git a/src/tests/gl_tests/OcclusionQueriesTest.cpp b/src/tests/gl_tests/OcclusionQueriesTest.cpp index f45e80e..429e9da 100644 --- a/src/tests/gl_tests/OcclusionQueriesTest.cpp +++ b/src/tests/gl_tests/OcclusionQueriesTest.cpp @@ -1317,6 +1317,130 @@ EXPECT_GL_TRUE(result); } +// Test that a very large number of scissored clears while an occlusion query is active still +// yields a correct query result. On backends that emulate scissored clears with draws, each clear +// pauses and resumes the query in the same render pass; the render pass must be split before the +// backend's per-pass visibility offset limit is reached. +TEST_P(OcclusionQueriesTest, ManyScissoredClearsInsideQuery) +{ + ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3 && + !IsGLExtensionEnabled("GL_EXT_occlusion_query_boolean")); + + // Metal allows at most 32768 visibility result slots per render pass. + constexpr int kClearCount = 35000; + + glDepthMask(GL_TRUE); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + EXPECT_GL_NO_ERROR(); + + GLQueryEXT query; + glBeginQueryEXT(GL_ANY_SAMPLES_PASSED_EXT, query); + + // this quad should not be occluded + drawQuad(mProgram, essl1_shaders::PositionAttrib(), 0.8f); + + glEnable(GL_SCISSOR_TEST); + glScissor(0, 0, 1, 1); + for (int i = 0; i < kClearCount; ++i) ... (file diff truncated)
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 8c536a2..c3095c5 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -2850,6 +2850,9 @@
529867799 VULKAN : ReadPixelsPBOTest.PackLargeRowLength/* = SKIP
529867799 INTEL OPENGL : ReadPixelsPBOTest.PackLargeRowLength/* = SKIP
+// Using many queries will fail on Pixel 4 GLES
+537692502 PIXEL4ORXL GLES : OcclusionQueriesTest.ExcessiveNumberOfQueries/* = SKIP
+
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Slow tests, should appear last in this file
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
diff --git a/src/tests/gl_tests/OcclusionQueriesTest.cpp b/src/tests/gl_tests/OcclusionQueriesTest.cpp
index f45e80e..429e9da 100644
--- a/src/tests/gl_tests/OcclusionQueriesTest.cpp
+++ b/src/tests/gl_tests/OcclusionQueriesTest.cpp
@@ -1317,6 +1317,130 @@
EXPECT_GL_TRUE(result);
}
+// Test that a very large number of scissored clears while an occlusion query is active still
+// yields a correct query result. On backends that emulate scissored clears with draws, each clear
+// pauses and resumes the query in the same render pass; the render pass must be split before the
+// backend's per-pass visibility offset limit is reached.
+TEST_P(OcclusionQueriesTest, ManyScissoredClearsInsideQuery)
+{
+ ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3 &&
+ !IsGLExtensionEnabled("GL_EXT_occlusion_query_boolean"));
+
+ // Metal allows at most 32768 visibility result slots per render pass.
+ constexpr int kClearCount = 35000;
+
+ glDepthMask(GL_TRUE);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+ EXPECT_GL_NO_ERROR();
+
+ GLQueryEXT query;
+ glBeginQueryEXT(GL_ANY_SAMPLES_PASSED_EXT, query);
+
+ // this quad should not be occluded
+ drawQuad(mProgram, essl1_shaders::PositionAttrib(), 0.8f);
+
+ glEnable(GL_SCISSOR_TEST);
+ glScissor(0, 0, 1, 1);
+ for (int i = 0; i < kClearCount; ++i)
+ {
+ glClear(GL_COLOR_BUFFER_BIT);
+ }
+ glDisable(GL_SCISSOR_TEST);
+
+ // this quad should not be occluded
+ drawQuad(mProgram, essl1_shaders::PositionAttrib(), 0.8f);
+
+ glEndQueryEXT(GL_ANY_SAMPLES_PASSED_EXT);
+ EXPECT_GL_NO_ERROR();
+
+ GLuint result = GL_FALSE;
+ glGetQueryObjectuivEXT(query, GL_QUERY_RESULT_EXT,
+ &result); // will block waiting for result
+ EXPECT_GL_NO_ERROR();
+
+ EXPECT_GL_TRUE(result);
+}
+
+// Test that a very large number of distinct queries and draw calls in a single render pass
+// still works and yields correct query results when exceeding the backend's per-pass
+// visibility offset limit.
+TEST_P(OcclusionQueriesTest, ExcessiveNumberOfQueries)
+{
+ ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3 &&
+ !IsGLExtensionEnabled("GL_EXT_occlusion_query_boolean"));
+
+ constexpr int kQueryCount = 35000;
+
+ glDisable(GL_DEPTH_TEST);
+ glClear(GL_COLOR_BUFFER_BIT);
+ EXPECT_GL_NO_ERROR();
+
+ constexpr char kVS[] =
+ "attribute vec4 a_position;\n"
+ "attribute vec4 a_color;\n"
+ "varying vec4 v_color;\n"
+ "void main() {\n"
+ " gl_Position = a_position;\n"
+ " gl_PointSize = 4.0;\n"
+ " v_color = a_color;\n"
+ "}\n";
+
+ constexpr char kFS[] =
+ "precision mediump float;\n"
+ "varying vec4 v_color;\n"
+ "void main() {\n"
+ " gl_FragColor = v_color;\n"
+ "}\n";
+
+ ANGLE_GL_PROGRAM(program, kVS, kFS);
+ glUseProgram(program);
+
+ GLint posLoc = glGetAttribLocation(program, "a_position");
+ GLint colorLoc = glGetAttribLocation(program, "a_color");
+ ASSERT_NE(-1, posLoc);
+ ASSERT_NE(-1, colorLoc);
+
+ glDisableVertexAttribArray(posLoc);
+ glDisableVertexAttribArray(colorLoc);
+
+ std::vector<GLQueryEXT> queries(kQueryCount);
+
+ for (int i = 0; i < kQueryCount; ++i)
+ {
+ glBeginQueryEXT(GL_ANY_SAMPLES_PASSED_EXT, queries[i]);
+ if (i % 2 != 0)
+ {
+ int idx = i / 2;
+ GLfloat x = (static_cast<GLfloat>(idx % 30) / 30.0f) * 1.8f - 0.9f;
+ GLfloat y = (static_cast<GLfloat>((idx / 30) % 30) / 30.0f) * 1.8f - 0.9f;
+ glVertexAttrib3f(posLoc, x, y, 0.0f);
+
+ GLfloat r = static_cast<GLfloat>(i % 256) / 255.0f;
+ GLfloat g = static_cast<GLfloat>((i * 7) % 256) / 255.0f;
+ GLfloat b = static_cast<GLfloat>((i * 13) % 256) / 255.0f;
+ glVertexAttrib4f(colorLoc, r, g, b, 1.0f);
+ glDrawArrays(GL_POINTS, 0, 1);
+ }
+ glEndQueryEXT(GL_ANY_SAMPLES_PASSED_EXT);
+ }
+ EXPECT_GL_NO_ERROR();
+
+ for (int i = 0; i < kQueryCount; ++i)
+ {
+ GLuint result = GL_FALSE;
+ glGetQueryObjectuivEXT(queries[i], GL_QUERY_RESULT_EXT, &result);
+ EXPECT_GL_NO_ERROR();
+ if (i % 2 != 0)
+ {
+ EXPECT_GL_TRUE(result) << " at query index " << i;
+ }
+ else
+ {
+ EXPECT_GL_FALSE(result) << " at query index " << i;
+ }
+ }
+}
+
ANGLE_INSTANTIATE_TEST_ES2_AND_ES3(OcclusionQueriesTest);
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OcclusionQueriesTestES3);