Compare commits

...
Author SHA1 Message Date
swung0x48 4b41f01b68 [Merge] (DirectGLES, GLState, ShaderTranspiler): land GL43 wave5 with its two new passes inside the L2 boundary 2026-08-21 00:43:39 -04:00
swung0x48 e81e938bb8 [Docs] (GLImpl): name the right copy_image conformance case in the 1D-array bounds note 2026-08-21 00:28:22 -04:00
swung0x48 5fa849674e [Fix] (DirectVulkan): gate the GL_DOUBLE vertex narrowing on the same fp64 flag the shader demotion uses 2026-08-21 00:28:03 -04:00
swung0x48 aed10f65a6 [Fix, Test] (GLImpl, MG_IntegrationTest): enforce the tessellation draw-mode rules and waive the XFB mode match for it 2026-08-21 00:23:15 -04:00
swung0x48 a687fc4577 [Fix] (ProgramState): stop the uniform-location grow path minting locations past GL_MAX_UNIFORM_LOCATIONS 2026-08-21 00:19:28 -04:00
swung0x48 ef66aea73b [Fix, Test] (GLImpl, DirectGLES, DirectVulkan): address a 1D array's copy-image layers on Z, not Y 2026-08-21 00:15:01 -04:00
swung0x48 c129cdec2d [Fix, Test] (ShaderTranspiler, ProgramState): reject an out-of-range atomic-counter offset at compile 2026-08-21 00:10:53 -04:00
swung0x48 7a7340ebe2 [Fix, Test] (GLImpl, ProgramState): answer the classic uniform queries for atomic counters at GL level 2026-08-21 00:07:07 -04:00
swung0x48 325ba07776 [Fix, Test] (ShaderTranspiler): route every sub-array of an array-of-arrays uniform to its own UBO offset 2026-08-21 00:02:30 -04:00
swung0x48 7aa91e8024 [Fix, Test] (DirectGLES, DirectVulkan): narrow GL_DOUBLE vertex arrays to float32 instead of dropping them 2026-08-20 23:54:49 -04:00
swung0x48 1b5a39473e [Fix, Test] (ShaderTranspiler, DirectGLES): flatten the atomic-counter block's declared offsets for ESSL 2026-08-20 23:39:26 -04:00
swung0x48 0e5f591cfa [Docs] (ShaderTranspiler): name the conformance cases fp64 block demotion costs 2026-08-20 23:26:18 -04:00
swung0x48 79336c5ccc [Fix, Test] (DirectGLES): re-issue the indexed binding of a storage buffer whose store was regrown 2026-08-20 23:16:08 -04:00
swung0x48 be45dbcf54 [Test] (MG_IntegrationTest): pin a non-constant index into an array of storage blocks 2026-08-20 23:15:48 -04:00
swung0x48 f7dfa01c18 [Fix, Test] (ShaderTranspiler, DirectGLES): make every array-of-storage-blocks index a constant for ESSL 2026-08-20 23:08:38 -04:00
swung0x48 50efa4410a [Test] (MG_IntegrationTest): dispatch an imageAtomicAdd against the two 1D image targets 2026-08-20 22:55:51 -04:00
swung0x48 bea3086b41 [Diagnostic, Test] (DirectGLES): name the image-uniform split as a cause when the backend link fails 2026-08-20 22:53:25 -04:00
swung0x48 8ae93c837d [Fix, Test] (DirectGLES): order the split image pair's store before its load with memoryBarrierImage 2026-08-20 22:49:05 -04:00
swung0x48 4154f2e941 [Fix, Test] (ShaderTranspiler, DirectGLES): widen a non-arrayed 1D storage image's atomic coordinate 2026-08-20 22:46:32 -04:00
swung0x48 cd07d42a47 [Fix, Test] (RenderState, DirectGLES, MG_IntegrationTest): tell a deliberately empty scissor box apart from one that was never written 2026-08-20 22:27:14 -04:00
swung0x48 ae0373eb48 [Merge] (DirectGLES, ShaderTranspiler): land GL43 wave4 with the interface-block rename inside the L2 boundary 2026-08-20 21:13:55 -04:00
swung0x48 7480bf4490 [Perf] (CTS-Harness): add a --cpu-mask switch and pin glcts to the big cluster by default 2026-08-20 21:03:54 -04:00
swung0x48 54b206d90c [Test, Bench] (ShaderTranspiler): pin the parse-verdict memo and measure the deferred parse 2026-08-20 18:53:57 -04:00
swung0x48 5daf7bf093 [Perf] (ShaderTranspiler, ProgramState): memoize the glslang parse verdict so a repeated compile skips the parse 2026-08-20 18:53:57 -04:00
swung0x48 a8228ca287 [Merge] (ShaderTranspiler, GLState, DirectGLES): land dev GL43 wave2/wave3 under the translation cache 2026-08-20 18:03:06 -04:00
swung0x48 8b827bd2ce [Fix, Test] (TextureFormatProcessor, DirectGLES, MG_IntegrationTest): give every unrenderable signed-normalized colour attachment an exact float substitute 2026-08-20 17:17:31 -04:00
swung0x48 6aa161fee7 [Fix, Test] (DirectGLES, ShaderTranspiler, MG_IntegrationTest): spell an interface block declared in both directions once per producing stage 2026-08-20 16:56:58 -04:00
swung0x48 48a70fea81 [Fix, Test] (DirectGLES, PixelStoreProcessor, MG_IntegrationTest): read a packed level's stored words instead of trusting the shadow 2026-08-20 16:15:52 -04:00
swung0x48 6ea4f32635 [Fix, Test] (TextureFormatProcessor): store the desktop-only low-bit formats without a driver requantization 2026-08-20 16:04:38 -04:00
swung0x48 dc1fffb041 [Fix, Test] (GLImpl): bound glCopyImageSubData's region against both images 2026-08-20 16:02:08 -04:00
swung0x48 d24d5b5ccd [Fix, Test] (BackendLoader, DirectGLES, DirectVulkan, GLImpl): answer the layer and viewport-index provoking-vertex conventions from the backend 2026-08-20 15:40:25 -04:00
swung0x48 51883cf1a3 [Fix, Test] (GLState): deliver the GL_MIN_MAP_BUFFER_ALIGNMENT that glGetIntegerv advertises 2026-08-20 15:35:02 -04:00
swung0x48 6dfadeb7d2 [Fix, Test] (BackendLoader): drain and gate every capability probe whose pname is not ES core 2026-08-20 15:30:11 -04:00
swung0x48 4fc3531d0d [Fix, Test] (BackendLoader, DirectVulkan, ShaderTranspiler): report GL_MAX_CLIP_DISTANCES from the backend's real clip-distance capability 2026-08-20 15:25:29 -04:00
swung0x48 9bde0e500f [Merge] (CTS): land the GL43 wave-3 fixes and the DirectVulkan texture-shape repairs 2026-08-20 14:17:20 -04:00
swung0x48 3477d87b50 [Fix] (DirectVulkan): back a 1D array with its layers in arrayLayers, not in the image height 2026-08-20 14:14:21 -04:00
swung0x48 c2a081fa75 [Fix] (GLState, DirectVulkan): bust the texture-sync skip when a re-spec moved only the shape 2026-08-20 14:02:05 -04:00
swung0x48 685fd750c9 [Test] (MG_Test): expect buffer-texture level queries to answer, not to error 2026-08-20 13:50:45 -04:00
swung0x48 02c9b8a32d [Fix, Test] (GLImpl, DirectVulkan, MG_IntegrationTest): record glVertexAttribLFormat's state and drop the array at draw 2026-08-20 13:44:44 -04:00
swung0x48 26f02567d7 [Fix, Test] (GLState, GLImpl): reserve an inactive uniform's explicit location and pin the link to GL_MAX_UNIFORM_LOCATIONS 2026-08-20 13:39:38 -04:00
swung0x48 a3dbe234d7 [Fix, Test] (GLImpl, MG_IntegrationTest): answer glGetTexLevelParameter for buffer textures instead of erroring 2026-08-20 13:27:50 -04:00
swung0x48 de8e7a4606 [Fix, Test] (ShaderTranspiler): parse layout literals in every GLSL base and key array-of-arrays uniforms per element 2026-08-20 13:24:04 -04:00
swung0x48 31a5da6190 [Test] (BackendLoader, DirectGLES): cover the per-stage storage block limits and the mg_IndirectParams injection gate 2026-08-20 13:16:29 -04:00
swung0x48 8899f065f4 [Fix] (DirectGLES): gate the mg_IndirectParams vertex-stage injection on the driver having a vertex storage block 2026-08-20 13:16:29 -04:00
swung0x48 a991f63899 [Fix] (GLImpl): answer the per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS queries from the backend instead of a fixed 16 2026-08-20 13:16:29 -04:00
swung0x48 3ff9cfe5c2 [Fix] (BackendLoader, DirectGLES, DirectVulkan): derive the per-stage shader storage block limits from the backend 2026-08-20 13:16:29 -04:00
swung0x48 6359fba455 [Test] (MG_Test): compile the compute-limit probe against the captured env, not the null-env fallback 2026-08-20 13:09:38 -04:00
swung0x48 872876961d [Fix] (GLState): pin the storage-binding ceiling's min/max to Int so no platform can widen either argument 2026-08-20 13:08:22 -04:00
swung0x48 e2923a239f [Fix, Test] (ShaderTranspiler): size a non-final unsized storage-block member so the members after it stop aliasing it 2026-08-20 13:05:50 -04:00
swung0x48 1740a8a41a [Feat, Test] (GLImpl, GLState): implement glBeginConditionalRender and discard the commands GL 4.6 10.9 names 2026-08-20 12:59:22 -04:00
swung0x48 6b1d89f279 [Fix, Test] (DirectGLES, MG_IntegrationTest): re-sync image-unit bindings when a draw's image texture was re-specified 2026-08-20 12:52:13 -04:00
swung0x48 01fbe0b4b0 [Fix, Test] (GLState, ShaderTranspiler): reject a storage-block binding at or past GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS 2026-08-20 12:45:56 -04:00
swung0x48 cb155c5b94 [Fix, Test] (GLImpl, ShaderTranspiler): reconcile the compute work-group limits glGetIntegeri_v and glslang advertise 2026-08-20 12:41:01 -04:00
swung0x48 04a06438c5 [Fix] (GLState): count an image-uniform array once however reflection spelled it 2026-08-20 12:16:57 -04:00
swung0x48 db00774224 [Fix] (DirectGLES): report the image formats GLSL ES cannot spell instead of losing the program silently 2026-08-20 12:16:56 -04:00
swung0x48 f378c1a064 [Fix, Test] (DirectGLES): read 1D-array and cube-map-array levels back layer by layer in glGetTexImage 2026-08-20 12:16:55 -04:00
swung0x48 421ccd08c6 [Fix, Test] (DirectGLES): make both halves of a split read+write image coherent 2026-08-20 12:04:43 -04:00
swung0x48 039af520bf [Fix, Test] (GLState): fail the link when a stage exceeds GL_MAX_*_IMAGE_UNIFORMS 2026-08-20 12:02:22 -04:00
Swung0x48 cdba7bed2e [Test, Bench] (ShaderTranspiler): pin L1 backend-agnosticism and measure the whole-front-end hit 2026-08-20 12:00:01 -04:00
Swung0x48 1eeeb44d94 [Perf] (ProgramState): serve a whole linked program from translation cache L1, skipping the link entirely 2026-08-20 12:00:01 -04:00
swung0x48 fa2e15c27e [Fix, Test] (GLImpl): answer GL_IMAGE_FORMAT_COMPATIBILITY_TYPE from glGetTexParameterfv 2026-08-20 11:56:24 -04:00
Swung0x48 14744f117c [Refactor] (ProgramInterface): build the program-resource model from the reflection snapshot, retiring GetReflection 2026-08-20 11:47:57 -04:00
Swung0x48 8329ab4264 [Refactor] (ProgramState): answer the GL query surface from an owned reflection snapshot, not the live TProgram 2026-08-20 11:43:42 -04:00
swung0x48 ee98c453ed [Fix, Test] (GLImpl): enforce GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS on the bind and indexed-query paths 2026-08-20 11:39:28 -04:00
swung0x48 f88322ce84 [Feat, Test] (DirectGLES, ShaderTranspiler): bind atomic counter buffers end-to-end on the ES backend 2026-08-20 11:36:52 -04:00
Swung0x48 93f1106ba4 [Fix] (ShaderTranspiler): key translation cache L1 on the front-end environment only, not backend identity 2026-08-20 11:30:31 -04:00
swung0x48 31b5b563d6 [Fix, Test] (GLState): fail the link when two atomic counters share a binding and an offset 2026-08-20 11:27:13 -04:00
swung0x48 a9fb7ef0af [Fix, Test] (GLImpl, GLState): answer GL_ACTIVE_ATOMIC_COUNTER_BUFFERS and implement glGetActiveAtomicCounterBufferiv 2026-08-20 11:23:33 -04:00
swung0x48 6159166d38 [Fix, Test] (GLImpl, ShaderTranspiler): reconcile the atomic-counter limits glGetIntegerv and glslang advertise 2026-08-20 11:20:21 -04:00
swung0x48 c6d1b29407 [Merge] (CTS): land the GL43 copy_image and clear_tex_image fixes 2026-08-20 11:16:59 -04:00
Swung0x48 5fecfa42f6 [Bench] (ShaderTranspiler): bracket the translation-cache win with a CTS-sized and a heavy stage 2026-08-20 11:11:37 -04:00
Swung0x48 d48e5d0053 [Fix] (ShaderTranspiler): leak the translation caches so no worker inserts into a destroyed one at exit 2026-08-20 11:09:01 -04:00
swung0x48 0995dfea35 [Test] (MG_IntegrationTest): force the iterationRP repairs on when the pinned ICD is lavapipe 2026-08-20 11:06:48 -04:00
Swung0x48 7a0182b58f [Bench] (ShaderTranspiler): measure the translation cache on a repeated-compile loop 2026-08-20 10:59:46 -04:00
Swung0x48 0f523db14d [Test] (ShaderTranspiler): cover both translation-cache key inventories, eviction and the concurrent path 2026-08-20 10:59:46 -04:00
Swung0x48 442cec1a15 [Perf] (DirectGLES): memoize the SPIR-V to ESSL transpile per stage (translation cache L2) 2026-08-20 10:51:05 -04:00
Swung0x48 246a438138 [Perf] (ShaderTranspiler): memoize a linked program's sanitized SPIR-V (translation cache L1) 2026-08-20 10:51:05 -04:00
swung0x48 042c61fb75 [Fix, Test] (TextureUtil, GLImpl): accept GL_STENCIL_INDEX as a stencil-only texture internal format 2026-08-20 10:49:57 -04:00
swung0x48 a8bebe1a3c [Fix, Test] (GLImpl, GLState): refuse a compressed texture in glClearTexImage/glClearTexSubImage 2026-08-20 10:44:52 -04:00
swung0x48 85cd6913b3 [Fix] (DirectGLES): sync a texture whose mip chain only defines the upper levels 2026-08-20 10:38:52 -04:00
swung0x48 afebf38e90 [Fix, Test] (GLImpl): require only the requested level to exist in glGetTexImage 2026-08-20 10:38:29 -04:00
swung0x48 898c39f1de [Fix] (DirectGLES, DirectVulkan): decline a null copy-image endpoint and settle a renderbuffer on its attachment layout 2026-08-20 10:23:03 -04:00
swung0x48 b1774e80be [Fix, Test] (GLImpl): make copy-image completeness mipmap-aware per GL 4.6 core 8.17 2026-08-20 10:20:34 -04:00
swung0x48 a9b4c47fea [Fix, Test] (GLImpl): record the specific compressed internalformat in TexImage3D and TexStorage3D 2026-08-20 10:17:55 -04:00
swung0x48 1c0be3e715 [Fix, Test] (DirectGLES, TextureUtil): return RGB9_E5 glGetTexImage from the stored words 2026-08-20 10:14:48 -04:00
swung0x48 27ec3d3438 [Merge] (CTS): land the Adreno CTS wave-1 conformance fixes 2026-08-20 10:11:43 -04:00
swung0x48 52718ecf84 [Fix, Test] (GLImpl, DirectGLES, DirectVulkan): accept GL_RENDERBUFFER endpoints in glCopyImageSubData 2026-08-20 10:11:38 -04:00
swung0x48 baeb2fa1bc [Perf] (ShaderTranspiler, Benchmark): add a per-stage stopwatch for the DirectGLES program-build chain 2026-08-20 10:08:02 -04:00
RISC-1145 54a88ef1e2 Merge pull request #15 from MobileGL-Dev/asio-include-fix-bug
fixed Asio include bug and added the ignored dir item .gradle
2026-08-20 21:48:47 +08:00
swung0x48 0b3101bf6b [Perf, Test] (ShaderTranspiler, DirectGLES): answer both pass-gate probes from one SPIR-V parse 2026-08-20 07:53:07 -04:00
swung0x48 a4fda520ed [Fix, Test] (ShaderTranspiler, DirectGLES): clamp multisample fetches to the backend's real sample count 2026-08-20 06:15:39 -04:00
swung0x48 f0fd6407ae [Fix] (ShaderTranspiler): keep the demoted viewport-index variable after its private pointer type 2026-08-20 05:37:53 -04:00
swung0x48 bde14cae29 [Fix, Test] (DirectGLES, MG_Test): request GL_OES_viewport_array in the emitted ESSL, or lower the builtin away 2026-08-20 05:29:18 -04:00
swung0x48 f2f6430e34 [Feat] (Loader): detect GL_OES_viewport_array in the GLES capability scan 2026-08-20 05:29:12 -04:00
swung0x48 15e36ad1e9 [Feat, Test] (ShaderTranspiler, MG_Test): demote gl_ViewportIndex to a plain global for ESSL targets 2026-08-20 05:29:08 -04:00
swung0x48 7940a09491 [Fix, Test] (GLImpl, MG_Test): validate glBlitFramebuffer's mask bits, filter enum and LINEAR depth rule 2026-08-20 05:14:11 -04:00
swung0x48 1e8d4661e6 [Fix, Test] (GLImpl, MG_Test): raise a draw's mode INVALID_ENUM before the no-current-program guard 2026-08-20 05:10:38 -04:00
swung0x48 916702629e [Fix, Test] (GLImpl, MG_Test): validate glFenceSync's condition/flags and glWaitSync's flags/timeout 2026-08-20 05:08:11 -04:00
swung0x48 9b37c77ae2 [Test] (MG_IntegrationTest): pin an overflowing vertex-only transform feedback capture's written and generated counts 2026-08-20 04:59:00 -04:00
swung0x48 085eb5835b [Fix, Test] (GLImpl, MG_State, DirectGLES, MG_Test): separate the transform feedback query counters and prefer the exact CPU count on DirectGLES 2026-08-20 04:58:54 -04:00
swung0x48 56377d2025 [Fix, Test] (DirectGLES, MG_IntegrationTest): drop the image-binding layer for targets that have none 2026-08-20 04:36:01 -04:00
swung0x48 7d2c16a90e [Fix] (DirectGLES): bound every driver error drain so a lost context cannot spin forever 2026-08-20 04:32:14 -04:00
swung0x48 261cfd1591 [Fix] (DirectGLES): retry a failed blit's colour and depth/stencil aspects independently 2026-08-20 04:31:22 -04:00
swung0x48 bbc7b9ca84 [Fix] (DirectGLES): report refused renderbuffer storage and collect dead backend twins on object churn 2026-08-20 04:21:10 -04:00
swung0x48 c1b3b16cab [Fix] (DirectGLES): drain the ES error queue in ErrorLopper's non-debug arm too 2026-08-20 04:21:09 -04:00
swung0x48 f17cb23ea3 [Fix, Test] (GLImpl, DirectGLES): deallocate zero-sized multisample images instead of defining them 2026-08-20 04:15:46 -04:00
swung0x48 f297af7d2b [Fix] (GLImpl, DirectGLES, DirectVulkan): floor every advertised sample cap and clamp the realised count in the backends 2026-08-20 04:10:45 -04:00
swung0x48 d9abf1c2c1 [Fix] (DirectGLES): probe the real multisample texture sample counts instead of hardcoding one 2026-08-20 04:07:38 -04:00
swung0x48 392736fb6b [Fix, Test] (ShaderTranspiler): rewrite float-equals-zero exactly instead of within a 1e-4 epsilon 2026-08-20 03:59:29 -04:00
swung0x48 0944925679 [Fix] (CTS-Harness): pin device glcts surface to 256^2 rgba8888d24s8, sync qpa, classify no-log reboots as hangs 2026-08-20 03:48:53 -04:00
142 changed files with 21750 additions and 1203 deletions
+7
View File
@@ -270,6 +270,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
@@ -279,8 +280,10 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
@@ -291,12 +294,15 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
@@ -386,6 +392,7 @@ set(SOURCE_FILES
MobileGL/MG_State/GLState/TextureState/TextureState.cpp MobileGL/MG_State/GLState/TextureState/TextureState.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
+9
View File
@@ -203,6 +203,15 @@ namespace MobileGL::MG_Config {
// immediately stay serial by their own construction). Off by default; never // immediately stay serial by their own construction). Off by default; never
// advertise it. // advertise it.
QuirkOverride AsyncOptimisticShaderStatus = QuirkOverride::Auto; QuirkOverride AsyncOptimisticShaderStatus = QuirkOverride::Auto;
// MOBILEGL_SHADER_CACHE: the three-level, in-memory shader translation memo
// (MG_Util/ShaderTranspiler/TranslationCache.h). The levels follow the GL
// entry points - L1c memoizes one glCompileShader's PARSE VERDICT, L1 a
// linked program's whole front end, L2 DirectGLES's emitted ESSL. Auto is
// ON; ForceOff turns ALL THREE off and makes every translation run from
// scratch. The escape hatch exists because a wrong cache hit is a silently
// miscompiled shader: if a device ever renders differently with the cache
// on, one run with this falsy says so.
QuirkOverride ShaderTranslationCache = QuirkOverride::Auto;
}; };
extern FeaturesTable Features; extern FeaturesTable Features;
} // namespace MobileGL::MG_Config } // namespace MobileGL::MG_Config
+1
View File
@@ -194,6 +194,7 @@ namespace MobileGL::MG_ConfigLoader {
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64); features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
features.AsyncOptimisticShaderStatus = features.AsyncOptimisticShaderStatus =
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS"); QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
} }
inline void InitBackendType() { inline void InitBackendType() {
+10
View File
@@ -18,6 +18,8 @@
#include <MG_Impl/GLImpl/Query/GL_Query.h> #include <MG_Impl/GLImpl/Query/GL_Query.h>
#include <MG_Util/Async/ShaderCompilePool.h> #include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h> #include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
#include <atomic> #include <atomic>
#include <mutex> #include <mutex>
@@ -72,6 +74,14 @@ namespace MobileGL {
// built-in symbol tables the prewarm latch stands for, so leaving it set would // built-in symbol tables the prewarm latch stands for, so leaving it set would
// make the next Initialize() skip a prewarm it genuinely needs. // make the next Initialize() skip a prewarm it genuinely needs.
MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch(); MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch();
// The two-level translation memo. Nothing in it references a glslang object -
// both levels hold plain bytes - so this is RSS hygiene rather than a lifetime
// requirement, and it is safe either side of FinalizeProcess. Stats first: an
// fordebug build gets one line per level saying how the run went.
MG_Util::ShaderTranspiler::LogShaderTranslationCacheStats();
MG_Util::ShaderTranspiler::ClearShaderTranslationCaches();
MG_State::GLState::LogProgramTranslationCacheStats();
MG_State::GLState::ClearProgramTranslationCache();
MG_Backend::gBackendFunctionsTable = {}; MG_Backend::gBackendFunctionsTable = {};
g_isInitialized = false; g_isInitialized = false;
if (logLifecycle) { if (logLifecycle) {
+64 -2
View File
@@ -14,6 +14,7 @@ namespace MobileGL {
namespace MG_State::GLState { namespace MG_State::GLState {
class FramebufferObject; class FramebufferObject;
class ITextureObject; class ITextureObject;
class RenderbufferObject;
} }
enum class BackendType { enum class BackendType {
@@ -24,6 +25,19 @@ namespace MobileGL {
}; };
namespace MG_Backend { namespace MG_Backend {
// One endpoint of a glCopyImageSubData. GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER
// alongside the ten whole-image texture targets, and a renderbuffer name lives in a
// namespace of its own - so an endpoint is a sum type, not an ITextureObject. At most
// one of the two pointers is set; neither is set when the name named nothing, which is
// the INVALID_VALUE the frontend validator reports.
struct CopyImageEndpoint {
SharedPtr<MG_State::GLState::ITextureObject> Texture;
SharedPtr<MG_State::GLState::RenderbufferObject> Renderbuffer;
Bool IsRenderbuffer() const { return Renderbuffer != nullptr; }
Bool Exists() const { return Texture != nullptr || Renderbuffer != nullptr; }
};
enum class FormatCapability : Uint64 { enum class FormatCapability : Uint64 {
Creatable = 1ull << 0, Creatable = 1ull << 0,
@@ -160,9 +174,9 @@ namespace MobileGL {
GLsizei height, GLint border); GLsizei height, GLint border);
void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height); GLsizei width, GLsizei height);
void (*CopyImageSubData)(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, void (*CopyImageSubData)(const CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void (*GenerateMipmap)(GLenum target); void (*GenerateMipmap)(GLenum target);
@@ -236,6 +250,14 @@ namespace MobileGL {
// (optional; null = frontend falls back to CPU accounting). // (optional; null = frontend falls back to CPU accounting).
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated); BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query); 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 // Transform feedback capture spans, for backends whose own GL/ES driver
// performs the capture (DirectGLES). Both optional; null means the backend // performs the capture (DirectGLES). Both optional; null means the backend
// drives capture from its draw recording instead (DirectVulkan). End is // drives capture from its draw recording instead (DirectVulkan). End is
@@ -318,6 +340,22 @@ namespace MobileGL {
Int MaxVertexAttribs = 16; Int MaxVertexAttribs = 16;
Int MaxComputeShaderStorageBlocks = 8; Int MaxComputeShaderStorageBlocks = 8;
Int MaxCombinedShaderStorageBlocks = 32; Int MaxCombinedShaderStorageBlocks = 32;
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Zero is a legal answer for the four
// non-compute, non-fragment stages and these defaults are the spec minimums, not
// placeholders: GL 4.6 table 23.64 and ES 3.2 table 21.44 both set the minimum for
// vertex, tessellation control, tessellation evaluation and geometry at 0, and only
// fragment (8 in GL, 4 in ES) and compute are guaranteed to have any. Every real ARM
// GLES driver takes that allowance - a Mali-G925 reports 0 for all four - so a
// backend that cannot honour a graphics-stage storage block MUST report 0 here
// rather than a hopeful number. Advertising a non-zero count the driver will refuse
// does not make the block work; it only moves the failure from an honest
// "unsupported" at query time to a backend link error the frontend never surfaces,
// after which every draw with that program silently renders nothing.
Int MaxVertexShaderStorageBlocks = 0;
Int MaxTessControlShaderStorageBlocks = 0;
Int MaxTessEvaluationShaderStorageBlocks = 0;
Int MaxGeometryShaderStorageBlocks = 0;
Int MaxFragmentShaderStorageBlocks = 8;
Int MaxComputeUniformBlocks = 12; Int MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128; Int MaxComputeWorkGroupInvocations = 128;
Int MaxShaderStorageBufferBindings = 8; Int MaxShaderStorageBufferBindings = 8;
@@ -334,8 +372,32 @@ namespace MobileGL {
Int MaxComputeImageUniforms = 8; Int MaxComputeImageUniforms = 8;
Int MaxDrawBuffers = 8; Int MaxDrawBuffers = 8;
Int MaxColorAttachments = 8; Int MaxColorAttachments = 8;
// GL_MAX_CLIP_DISTANCES. Zero is a legal answer here, not a placeholder, and a
// backend that cannot host a clip distance MUST report it: advertising eight the
// backend will refuse does not make gl_ClipDistance work, it only moves the failure
// from an honest "unsupported" at query time to a backend shader-compile error the
// frontend never surfaces, after which every draw with that program silently renders
// nothing. DirectGLES fills it from GL_EXT_clip_cull_distance, DirectVulkan from the
// shaderClipDistance device feature. The DEFAULT stays at the GL 4.3 core minimum
// because it describes the no-backend case (standalone shader compiles, unit tests),
// where there is no device to be honest about and BuildTBuiltInResource still has to
// hand glslang a workable gl_MaxClipDistances.
Int MaxClipDistances = 8; Int MaxClipDistances = 8;
Int MaxViewports = 16; Int MaxViewports = 16;
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX: which vertex of a
// primitive supplies gl_Layer and gl_ViewportIndex. GL 4.6 table 23.65 makes
// GL_UNDEFINED_VERTEX a legal answer for both, and it is the honest default - naming
// a convention is a statement about behaviour, so a backend that does not pin one
// must not claim it does. DirectGLES fills the layer one from the ES 3.2 query and
// the viewport one from GL_OES_viewport_array, and leaves UNDEFINED where the
// capability is absent: without the viewport array extension only viewport 0 is ever
// rasterized, so no convention selects anything. DirectVulkan keeps UNDEFINED for
// both - which vertex provokes is decided per pipeline by
// VulkanRenderer::SelectProvokingVertexMode out of VK_EXT_provoking_vertex,
// provokingVertexModePerPipeline and the topology, so no single convention is true
// of the backend.
GLenum LayerProvokingVertex = GL_UNDEFINED_VERTEX;
GLenum ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
Int MaxViewportWidth = 16384; Int MaxViewportWidth = 16384;
Int MaxViewportHeight = 16384; Int MaxViewportHeight = 16384;
Float ViewportBoundsRangeMin = 0.0f; Float ViewportBoundsRangeMin = 0.0f;
@@ -8,6 +8,7 @@
#include "BackendObject_DirectGLES.h" #include "BackendObject_DirectGLES.h"
#include "MG_Backend/BackendObject.h" #include "MG_Backend/BackendObject.h"
#include "MG_Backend/BackendObjects.h"
#include <MG_Backend/DirectGLES/DirectGLES.h> #include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h> #include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/Utils.h> #include <MG_Backend/DirectGLES/Utils.h>
@@ -212,7 +213,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) { if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
reasons.push_back("no colour-renderable three-channel format on OpenGL ES"); reasons.push_back("no colour-renderable three-channel format on OpenGL ES");
} }
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) { // A format is either 8- or 16-bit signed normalized, so at most one of the two ever
// survives GetApplicablePixelFormatNormalizeOptions and the reason is not duplicated.
if ((options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) ||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
reasons.push_back("EXT_render_snorm not supported"); reasons.push_back("EXT_render_snorm not supported");
} }
@@ -406,9 +410,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
return complete; return complete;
} }
// `samples` only reaches the multisample targets; every other target ignores it. The
// descending sample walk (ProbeTextureSampleCounts) reuses this whole routine rather than
// repeating the gen/bind/completeness/delete dance.
Bool ProbeTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target, GLenum internalFormat, Bool ProbeTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target, GLenum internalFormat,
GLenum imageFormat, GLenum imageType, TextureInternalFormat logicalFormat, GLenum imageFormat, GLenum imageType, TextureInternalFormat logicalFormat,
Bool* outRenderable) { Bool* outRenderable, Int samples = 1) {
if (!IsGLESProbeTextureTarget(target) || !gl.glGenTextures || !gl.glBindTexture || !gl.glDeleteTextures) { if (!IsGLESProbeTextureTarget(target) || !gl.glGenTextures || !gl.glBindTexture || !gl.glDeleteTextures) {
return false; return false;
} }
@@ -428,10 +435,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool isMultisample = IsGLESProbeMultisampleTarget(target); const Bool isMultisample = IsGLESProbeMultisampleTarget(target);
if (isMultisample) { if (isMultisample) {
const auto probeSamples = static_cast<GLsizei>(std::max(samples, 1));
if (target == TextureTarget::Texture2DMultisample && gl.glTexStorage2DMultisample) { if (target == TextureTarget::Texture2DMultisample && gl.glTexStorage2DMultisample) {
gl.glTexStorage2DMultisample(glTarget, 1, internalFormat, 1, 1, GL_TRUE); gl.glTexStorage2DMultisample(glTarget, probeSamples, internalFormat, 1, 1, GL_TRUE);
} else if (target == TextureTarget::Texture2DMultisampleArray && gl.glTexStorage3DMultisample) { } else if (target == TextureTarget::Texture2DMultisampleArray && gl.glTexStorage3DMultisample) {
gl.glTexStorage3DMultisample(glTarget, 1, internalFormat, 1, 1, 1, GL_TRUE); gl.glTexStorage3DMultisample(glTarget, probeSamples, internalFormat, 1, 1, 1, GL_TRUE);
} else { } else {
gl.glBindTexture(glTarget, static_cast<GLuint>(previousBinding)); gl.glBindTexture(glTarget, static_cast<GLuint>(previousBinding));
gl.glDeleteTextures(1, &texture); gl.glDeleteTextures(1, &texture);
@@ -527,6 +535,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
return sampleCounts; return sampleCounts;
} }
// The multisample TEXTURE twin of ProbeRenderbufferSampleCounts. It used to be a
// hardcoded {1}, which made glGetInternalformativ(GL_SAMPLES) claim a one-sample maximum
// for every format on the multisample targets even where glTexImage2DMultisample happily
// accepts four - GL 4.6 core 8.8 makes that query the definition of the maximum, so the
// two answers cannot both be right. Completeness is required at every count, exactly as
// the renderbuffer walk requires it; the caller only reaches here once the one-sample
// probe has already succeeded, so 1 terminates the list without being re-probed.
Vector<Int> ProbeTextureSampleCounts(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
GLenum internalFormat, GLenum imageFormat, GLenum imageType,
TextureInternalFormat logicalFormat, Int maxSamples) {
Vector<Int> sampleCounts;
for (Int samples = std::max(maxSamples, 1); samples > 1; samples >>= 1) {
Bool renderable = false;
const Bool created = ProbeTexture(gl, target, internalFormat, imageFormat, imageType, logicalFormat,
&renderable, samples);
if (created && renderable) {
sampleCounts.push_back(samples);
}
}
sampleCounts.push_back(1);
return sampleCounts;
}
void PopulateFormatCapabilitiesImpl(const MG_External::GLESFunctionsTable& gl, void PopulateFormatCapabilitiesImpl(const MG_External::GLESFunctionsTable& gl,
const MG_External::GLESCapabilities& capabilities, const MG_External::GLESCapabilities& capabilities,
FormatCapabilityCache& cache) { FormatCapabilityCache& cache) {
@@ -627,7 +658,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
AddFullFormatCaps(cache, targetIndex, formatIndex, AddFullFormatCaps(cache, targetIndex, formatIndex,
BuildTextureCapsFromProbe(logicalFormat, target, nativeRenderable)); BuildTextureCapsFromProbe(logicalFormat, target, nativeRenderable));
if (IsGLESProbeMultisampleTarget(target)) { if (IsGLESProbeMultisampleTarget(target)) {
cache.SampleCounts[targetIndex][formatIndex] = {1}; const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat);
cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts(
gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
nativeInfo.ImageType, logicalFormat, maxSamples);
} }
} }
shouldProbeFallback = !nativeCreated || !nativeRenderable; shouldProbeFallback = !nativeCreated || !nativeRenderable;
@@ -645,7 +680,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo); LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo);
} }
if (IsGLESProbeMultisampleTarget(target)) { if (IsGLESProbeMultisampleTarget(target)) {
cache.SampleCounts[targetIndex][formatIndex] = {1}; const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat);
cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts(
gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
fallbackInfo.ImageType, logicalFormat, maxSamples);
} }
} }
} }
@@ -747,6 +786,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
PopulateFormatCapabilitiesImpl(gl, capabilities, cache); PopulateFormatCapabilitiesImpl(gl, capabilities, cache);
} }
Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat,
Int samples) {
if (samples <= 1) {
return samples;
}
Int maxSamples = 0;
const SizeT formatIndex = static_cast<SizeT>(logicalFormat);
if (pActiveBackendObject && targetIndex < kFormatCapabilityTargetCount &&
formatIndex < kFormatCapabilityFormatCount) {
// Descending, so the head is the largest count this device actually allocated.
const Vector<Int>& probedCounts =
pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
if (!probedCounts.empty()) {
maxSamples = probedCounts.front();
}
}
if (maxSamples <= 0) {
maxSamples = GetGLESFormatMaxSamples(g_GLESCapabilities, logicalFormat, imageFormat);
}
return std::min(samples, std::max(maxSamples, 1));
}
BackendObject_DirectGLES::~BackendObject_DirectGLES() { BackendObject_DirectGLES::~BackendObject_DirectGLES() {
DestroyEGLContext(); DestroyEGLContext();
} }
@@ -1107,6 +1169,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// geometry shader's amplification. // geometry shader's amplification.
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery; funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery; 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.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64; funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery; funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
@@ -1186,9 +1254,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS)); static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks; m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks;
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_GLESCapabilities.MaxCombinedShaderStorageBlocks; m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_GLESCapabilities.MaxCombinedShaderStorageBlocks;
// Per-stage storage-block counts, forwarded from the host driver rather than invented.
// A stage the driver cannot serve reports 0, which is a legal answer everywhere these
// limits appear (GL 4.6 table 23.64, ES 3.2 table 21.44 - the minimum is 0 for every
// graphics stage except fragment) and is the only answer that lets an application take
// its own fallback instead of building a program the driver will refuse to link. The
// stage limit cannot exceed the combined limit or the number of binding points there
// are to bind buffers to, so clamp to both.
const auto clampStageStorageBlocks = [this](Int stageLimit) {
return std::min({std::max(stageLimit, 0), std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0),
std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)});
};
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
m_dynamicParameters.MaxVertexShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxVertexShaderStorageBlocks);
m_dynamicParameters.MaxTessControlShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxTessControlShaderStorageBlocks);
m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxTessEvaluationShaderStorageBlocks);
m_dynamicParameters.MaxGeometryShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxGeometryShaderStorageBlocks);
m_dynamicParameters.MaxFragmentShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxFragmentShaderStorageBlocks);
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks; m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations; m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings; // (MaxShaderStorageBufferBindings is assigned above, before the per-stage clamp reads it.)
// This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and // This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and
// on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather // on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
// than a driver answer (m_GLESCapabilities.MaxTextureBufferSizeIsDriverReported says // than a driver answer (m_GLESCapabilities.MaxTextureBufferSizeIsDriverReported says
@@ -1249,6 +1339,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments; m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances; m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports; m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports;
// Whatever the driver said about which vertex supplies gl_Layer, and GL_UNDEFINED_VERTEX
// for gl_ViewportIndex on every driver without GL_OES_viewport_array - which is both test
// devices. That is not a shortfall being hidden: without the extension only viewport 0 is
// ever rasterized, so no vertex "selects" a viewport index and naming a convention would
// describe behaviour this backend does not implement.
m_dynamicParameters.LayerProvokingVertex = m_GLESCapabilities.LayerProvokingVertex;
m_dynamicParameters.ViewportIndexProvokingVertex = m_GLESCapabilities.ViewportIndexProvokingVertex;
m_dynamicParameters.MaxViewportWidth = m_GLESCapabilities.MaxViewportWidth; m_dynamicParameters.MaxViewportWidth = m_GLESCapabilities.MaxViewportWidth;
m_dynamicParameters.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight; m_dynamicParameters.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight;
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin; m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
@@ -18,6 +18,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
const MG_External::GLESCapabilities& capabilities, const MG_External::GLESCapabilities& capabilities,
FormatCapabilityCache& cache); FormatCapabilityCache& cache);
// Clamps a requested sample count down to what the ES driver can really deliver for this
// format on this format-capability target: the probed per-format list when there is one, the
// driver's per-class GL_MAX_*_SAMPLES otherwise. The frontend deliberately validates against
// the count MobileGL advertises instead (GL_Getter's GetAdvertisedMaxSamples), which on a
// driver reporting GL_MAX_INTEGER_SAMPLES 1 is higher than the driver accepts, so every ES
// allocation call has to come through here. The shadow state keeps the requested count, so
// GL_TEXTURE_SAMPLES and framebuffer completeness still answer what the application asked for.
Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat,
Int samples);
class BackendObject_DirectGLES : public BackendObject { class BackendObject_DirectGLES : public BackendObject {
public: public:
~BackendObject_DirectGLES() override; ~BackendObject_DirectGLES() override;
+680 -109
View File
@@ -300,10 +300,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
Clear(); Clear();
} }
#else #else
void ErrorLopper::Loop(const std::function<void(GLenum)>& func) {} // Error HYGIENE is not a debugging feature: every site that brackets a risky ES call with
void ErrorLopper::Clear() {} // Clear()/Loop() relied on these to empty the driver's queue, and compiling them to
ErrorLopper::ErrorLopper() = default; // nothing left whatever the driver raised sitting there for an unrelated later
ErrorLopper::~ErrorLopper() = default; // `glGetError() == GL_NO_ERROR` probe to read as its own failure. The callback stays
// unused because MGLOG_D is compiled out at this level, but the queue still gets drained.
// Bounded like DrainESErrors: a driver that never returns GL_NO_ERROR (a lost context is
// the usual way) must not spin here.
constexpr Int kMaxDrainedESErrors = 32;
void ErrorLopper::Loop(const std::function<void(GLenum)>& func) {
static_cast<void>(func);
for (Int i = 0; i < kMaxDrainedESErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
}
}
void ErrorLopper::Clear() {
for (Int i = 0; i < kMaxDrainedESErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
}
}
ErrorLopper::ErrorLopper() {
Clear();
}
ErrorLopper::~ErrorLopper() {
Clear();
}
#endif #endif
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
@@ -373,6 +395,50 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
} }
void SyncAtomicCounterBuffers(const Vector<Int>& glBindings, Int esslBindingTop) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::AtomicCounter);
for (const Int glBinding : glBindings) {
if (glBinding < 0 || static_cast<SizeT>(glBinding) >= pointCount) continue;
const Int esslBinding = esslBindingTop - glBinding;
// Already diagnosed once when the block was transpiled; nothing was bound to it
// there either, so there is nothing to unbind here.
if (esslBinding < 0) continue;
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::AtomicCounter,
static_cast<Uint>(glBinding));
auto& obj = point.GetBoundObject();
if (!obj) {
BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding), 0);
continue;
}
auto* backendResource = EnsureBufferResource(obj);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer found for atomic counter binding point %d.", glBinding);
continue;
}
const auto& range = point.GetRange();
if (range.start == 0 && range.end >= obj->GetSize()) {
BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding),
backendResource->id);
} else {
const auto start = std::min(range.start, obj->GetSize());
const auto end = std::min(range.end, obj->GetSize());
BindBufferRangeCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding),
backendResource->id, static_cast<GLintptr>(start),
static_cast<GLsizeiptr>(end - start));
}
// The whole point of a counter is that the shader INCREMENTS it, and every
// conformance case reads the result back with glMapBufferRange or
// glGetBufferSubData - which serve the frontend's CPU shadow until the buffer is
// flagged (BufferObject::SyncGpuWrites), exactly as for a storage buffer.
obj->MarkGpuWritten();
}
}
void SyncBoundBuffer(BufferTarget target, GLenum glTarget) { void SyncBoundBuffer(BufferTarget target, GLenum glTarget) {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -1319,6 +1385,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
// is stored as an ES 2D array (MapToBackendTextureTarget), and so is layerable; asking // is stored as an ES 2D array (MapToBackendTextureTarget), and so is layerable; asking
// the state target instead answered "no" for it and pinned every 1D-array image binding // the state target instead answered "no" for it and pinned every 1D-array image binding
// to layer 0, whatever the application passed. // to layer 0, whatever the application passed.
//
// `layer` travels with the answer, because GL 4.6 core 8.26 (and ES 3.2 8.22, word for
// word) makes them one rule: "If the texture identified by texture does not have
// multiple layers or faces, the entire texture level is bound, regardless of the values
// of layered and layer." REGARDLESS means ignored - not clamped, and not an error - so
// the driver must not be handed a layer index the texture has no room for. Adreno takes
// such a request literally and leaves the image unit reading zero, which is what failed
// KHR-GL42.bind_image_texture.single_layer's layer:1 rows on GL_TEXTURE_2D and on the
// GL_TEXTURE_1D that is stored as one. Normalizing here and not in the frontend shadow
// is deliberate: GL_IMAGE_BINDING_LAYER must keep echoing what the application passed.
static Bool SupportsLayeredImageBinding(TextureTarget target) { static Bool SupportsLayeredImageBinding(TextureTarget target) {
const TextureTarget backendTarget = TextureImpl::MapToBackendTextureTarget(target); const TextureTarget backendTarget = TextureImpl::MapToBackendTextureTarget(target);
return backendTarget == TextureTarget::Texture3D || backendTarget == TextureTarget::TextureCubeMap || return backendTarget == TextureTarget::Texture3D || backendTarget == TextureTarget::TextureCubeMap ||
@@ -1362,22 +1438,33 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
} }
// Highest image unit that has ever been given a texture, plus one. Maintained by the
// single funnel below, so it is a sound "no draw in this context can be reading an image"
// test: nothing reaches an image unit without going through SyncImageTextureBinding.
// Almost every program (every Minecraft draw) leaves it at zero, which is what keeps the
// draw-path staleness check below at one integer test.
static Uint g_imageUnitHighWaterMark = 0;
void SyncImageTextureBinding(Uint unit) { void SyncImageTextureBinding(Uint unit) {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit)); auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit));
TrackWritableImageBufferUnit(unit, IsWritableImageBufferTexture(imageBinding)); TrackWritableImageBufferUnit(unit, IsWritableImageBufferTexture(imageBinding));
if (imageBinding.Texture && unit + 1 > g_imageUnitHighWaterMark) {
g_imageUnitHighWaterMark = unit + 1;
}
if (!imageBinding.Texture) { if (!imageBinding.Texture) {
g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
return; return;
} }
auto& backendTexture = SyncTextureObjectToBackend(imageBinding.Texture, true); auto& backendTexture = SyncTextureObjectToBackend(imageBinding.Texture, true);
const GLboolean layered = const Bool layerable = SupportsLayeredImageBinding(imageBinding.Texture->GetTarget());
SupportsLayeredImageBinding(imageBinding.Texture->GetTarget()) ? imageBinding.Layered : GL_FALSE; const GLboolean layered = layerable ? imageBinding.Layered : GL_FALSE;
const GLint layer = layerable ? imageBinding.Layer : 0;
g_GLESFuncs.glBindImageTexture(unit, backendTexture->GetBackendTextureId(), imageBinding.Level, g_GLESFuncs.glBindImageTexture(unit, backendTexture->GetBackendTextureId(), imageBinding.Level,
layered, imageBinding.Layer, imageBinding.Access, imageBinding.Format); layered, layer, imageBinding.Access, imageBinding.Format);
} }
// A buffer texture bound to a WRITABLE image unit is a buffer the shader is about to // A buffer texture bound to a WRITABLE image unit is a buffer the shader is about to
@@ -1420,6 +1507,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
SyncImageTextureBinding(unit); SyncImageTextureBinding(unit);
} }
} }
// What the draw path last swept the image units against. A draw never swept them at all:
// an image unit was established once, eagerly, by glBindImageTexture and never revisited.
// That is stale the moment the texture behind it is re-specified with a new size or
// format, because ES 3.1 only allows IMMUTABLE storage on an image unit
// (SyncTextureObjectToBackend's imageBindableStorageRequired), immutable storage cannot be
// redefined, and so the re-spec MINTS A NEW ES TEXTURE NAME - leaving the unit pointing at
// the deleted one and imageSize() reporting the old dimensions
// (KHR-GL43.shader_image_size.advanced-changeSize).
static Uint64 g_imageSweepContextId = 0;
static Uint64 g_imageSweepSamplingGeneration = 0;
static Uint g_imageSweepBackendContextGeneration = 0;
static Bool g_imageSweepValid = false;
// The sweep is a glBindImageTexture per unit, so it must not run per draw: the gate is the
// frontend's sampling-resolution generation, which TextureObjectBase::BumpShapeVersion
// moves on exactly the shape and format changes that can force the re-mint. Deliberately
// NOT the backend-side re-mint counter (g_attachmentBackendIdGeneration's sibling would be
// the obvious choice): a texture that is bound ONLY to an image unit is re-minted inside
// this very sweep, so a backend-side trigger would be bumped after the gate had already
// declined to run it.
void SyncImageTextureBindingsForDraw(const DrawTextureSyncKeys& keys) {
if (g_imageUnitHighWaterMark == 0) return;
if (g_imageSweepValid && g_imageSweepContextId == keys.contextId &&
g_imageSweepSamplingGeneration == keys.samplingGeneration &&
g_imageSweepBackendContextGeneration == g_backendContextGeneration) {
return;
}
SyncImageTextureBindings();
g_imageSweepContextId = keys.contextId;
g_imageSweepSamplingGeneration = keys.samplingGeneration;
g_imageSweepBackendContextGeneration = g_backendContextGeneration;
g_imageSweepValid = true;
}
} // namespace TextureImpl } // namespace TextureImpl
namespace FramebufferImpl { namespace FramebufferImpl {
@@ -2013,19 +2134,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (tailSpanDirty) { // Scissor box. Resolved and shadowed like the viewport above, and if (tailSpanDirty) { // Scissor box. Resolved and shadowed like the viewport above, and
// for the same reason: what has to reach the driver is NOT simply the parameter // for the same reason: what has to reach the driver is NOT simply the parameter
// field. (0,0,0,0) is where RenderStateParameters::ScissorBox starts and the only // field. RenderStateParameters::ScissorBoxes starts all-zero, which means "the
// thing that ever writes it is glScissor, so that value means "the application has // application has never called glScissor" - it is not a GL scissor box. GL's
// never called glScissor" - it is not a GL scissor box. GL's initial box is the // initial box is the whole window, which the frontend has no way to spell before a
// whole window, which the frontend has no way to spell before a surface exists. // surface exists. The pre-resync code got away with pushing the field verbatim only
// The pre-resync code got away with pushing the field verbatim only by accident: // by accident: the shadow held the same default, the field never compared unequal,
// the shadow held the same default, the field never compared unequal, and the ES // and the ES context kept its own correct default. Under the forced full push that
// context kept its own correct default. Under the forced full push that accident // accident is gone, glScissor(0,0,0,0) shrinks the scissor to an EMPTY rectangle,
// is gone, glScissor(0,0,0,0) shrinks the scissor to an EMPTY rectangle, and // and everything drawn with GL_SCISSOR_TEST enabled before the app's first
// everything drawn with GL_SCISSOR_TEST enabled before the app's first glScissor // glScissor is clipped away - Minecraft 26.2 keeps only its unscissored sky and
// is clipped away - Minecraft 26.2 keeps only its unscissored sky and hand and // hand and loses the terrain and the whole GUI.
// loses the terrain and the whole GUI. //
// The condition is the WRITTEN FLAG, not the extent. An empty rectangle is a
// perfectly legal thing to ask for - glScissor(0,0,0,0) means "the scissor test
// rejects every fragment" - so testing `width <= 0 || height <= 0` substituted the
// whole surface for a deliberately empty box and inverted the request into "accept
// every fragment", no matter how many times the application had already called
// glScissor. KHR-GL43.viewport_array.scissor_zero_dimension is exactly that: all 16
// boxes zero-sized with the test enabled, requiring the draw to be clipped away
// entirely. Reading the flag preserves the Minecraft protection bit-for-bit - before
// the first glScissor the bit is clear and the surface size is still substituted -
// while an explicit empty box now reaches the driver verbatim. Negative extents
// cannot arrive here at all: all three entry points reject them with
// GL_INVALID_VALUE before storing (GL_RenderState.cpp's ValidateNonNegativeExtent).
IntVec4 backendScissorBox = parameters.ScissorBoxes[0]; IntVec4 backendScissorBox = parameters.ScissorBoxes[0];
if (backendScissorBox.z() <= 0 || backendScissorBox.w() <= 0) { if ((parameters.ScissorBoxWrittenMask & 1u) == 0) {
Int surfaceWidth = 0; Int surfaceWidth = 0;
Int surfaceHeight = 0; Int surfaceHeight = 0;
if (QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) { if (QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) {
@@ -2363,6 +2496,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
syncBit & DrawSyncBit::IndirectBuffer); syncBit & DrawSyncBit::IndirectBuffer);
VertexArrayImpl::SyncCurrentVAO(currentVAO, vaoTwin); VertexArrayImpl::SyncCurrentVAO(currentVAO, vaoTwin);
TextureImpl::SyncNeccessaryTextures(textureKeys); TextureImpl::SyncNeccessaryTextures(textureKeys);
// A draw reads and writes through its image units too, so the unit bindings have to be
// as current as the sampled ones. Gated (see the sweep): a program with no image binding
// pays one integer test, and one with images re-issues them only when a texture shape
// moved under them.
TextureImpl::SyncImageTextureBindingsForDraw(textureKeys);
// A draw writes through its image units too - the conformance case that found this // A draw writes through its image units too - the conformance case that found this
// stores into a buffer texture from the FRAGMENT stage, not from a dispatch. // stores into a buffer texture from the FRAGMENT stage, not from a dispatch.
TextureImpl::MarkWritableImageBufferTexturesGpuWritten(); TextureImpl::MarkWritableImageBufferTexturesGpuWritten();
@@ -2882,6 +3020,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
} }
// Atomic counter buffers. Bound here rather than beside the storage-buffer sync
// in SyncNeccessaryBuffers because the reserved slot the transpiled ESSL reads
// them at is PROGRAM state: it is `top - GL binding` for the counter blocks THIS
// program declares, and no other program's blocks live there. Both the draw and
// the dispatch path reach this, which is what a compute-shader counter needs.
if (!backendProgram.GetAtomicCounterBindings().empty()) {
BufferImpl::SyncAtomicCounterBuffers(backendProgram.GetAtomicCounterBindings(),
backendProgram.GetAtomicCounterEsslBindingTop());
}
{ {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedNC("BindSamplerUnit", TRACY_ZONECOLOR_BACKEND); ZoneScopedNC("BindSamplerUnit", TRACY_ZONECOLOR_BACKEND);
@@ -3845,11 +3993,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect"); sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect");
} }
static void DrainBlitErrors() { // Empties the ES driver's error queue, BOUNDED. A driver that never answers GL_NO_ERROR - a
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) { // lost context is the usual way, and GL_CONTEXT_LOST is allowed to keep coming back - would
// otherwise spin an unbounded drain forever inside whichever GL entry point happened to be
// cleaning up, which is how a GPU reset reads as an unkillable process whose log simply
// stops. A healthy context cannot queue anywhere near the cap, so reaching it IS the
// diagnostic. Every drain in this backend goes through here so the bound cannot drift apart
// between them.
static constexpr Int kMaxDrainedGLErrors = 32;
static void DrainDriverErrors(const char* site) {
Int drained = 0;
while (drained < kMaxDrainedGLErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR) {
++drained;
}
if (drained == kMaxDrainedGLErrors) {
MGLOG_E_ONCE("%s: the ES driver still reported errors after %d drains - the context is most likely lost",
site, kMaxDrainedGLErrors);
} }
} }
static void DrainBlitErrors() { DrainDriverErrors("BlitFramebuffer"); }
// Sized internal format of the currently bound READ framebuffer's read colour // Sized internal format of the currently bound READ framebuffer's read colour
// attachment, 0 when it cannot be determined. // attachment, 0 when it cannot be determined.
static GLenum QueryReadColorAttachmentInternalFormat() { static GLenum QueryReadColorAttachmentInternalFormat() {
@@ -4624,16 +4789,46 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
return; return;
} }
if (readSamples <= 0 || drawSamples > 0 || (mask & GL_COLOR_BUFFER_BIT) == 0) { // The combined call raised an error, so by GL 4.6 2.3.1 it wrote nothing at all: BOTH
return; // aspect groups still owe their copy, and each has to be retried on its own. Re-issuing
} // the depth/stencil half only as a rider on a SUCCESSFUL colour resolve dropped it
if (ResolveThenBlit(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, filter) && // silently whenever the colour half could not be emulated - and on a framebuffer whose
(mask & ~static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT)) != 0) { // only attachment is depth it never can, because the colour emulation has no attachment
DrainBlitErrors(); // to take a format from (KHR-GL33.framebuffer_blit's depth config test blits
g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, // COLOR|DEPTH|STENCIL across depth-only framebuffers and kept reading the clear value).
mask & ~static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT), filter); const GLbitfield colourBit = mask & static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT);
const GLbitfield dsBits = mask & static_cast<GLbitfield>(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
// The colour group's one emulation is the multisample resolve that also converts format,
// which is the shape this names. It used to double as an early-out for the whole
// function, which is what cost a depth-only mask its single-aspect retry.
const Bool multisampleResolve = readSamples > 0 && drawSamples <= 0;
if (colourBit != 0) {
DrainBlitErrors(); DrainBlitErrors();
g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, colourBit, filter);
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
const Bool emulated =
multisampleResolve &&
ResolveThenBlit(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, filter);
if (!emulated) {
MGLOG_E_ONCE("BlitFramebuffer: the colour aspect was dropped - the driver rejected it on its "
"own and no emulation applies");
}
}
} }
if (dsBits != 0) {
DrainBlitErrors();
g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, dsBits, filter);
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
// Nothing to fall back on yet: ResolveThenBlit is colour-only and the replicate
// pass runs in the opposite direction, so a driver that declines a multisample
// depth/stencil resolve leaves the destination holding its clear value. The log
// is the whole diagnostic - the frontend performs no validation of its own, so
// this never reaches the application as a GL error.
MGLOG_E_ONCE("BlitFramebuffer: the depth/stencil aspect was dropped - the driver rejected it on "
"its own and no emulation applies");
}
}
DrainBlitErrors();
} }
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
@@ -4990,9 +5185,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false; return false;
} }
static void ClearGLErrors() { static void ClearGLErrors() { DrainDriverErrors("DirectGLES"); }
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {}
}
// Binds a guaranteed-complete 1x1 scratch framebuffer at both targets for the // Binds a guaranteed-complete 1x1 scratch framebuffer at both targets for the
// scope (GenerateMipmap must respecify texture storage while no incomplete // scope (GenerateMipmap must respecify texture storage while no incomplete
@@ -5605,15 +5798,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glDispatchComputeIndirect(indirect); g_GLESFuncs.glDispatchComputeIndirect(indirect);
} }
// An atomic counter is a shader storage block by the time it reaches the ES driver (glslang
// lowers every atomic_uint onto one), so an application that asks only for the counter
// barrier is asking about memory the driver knows as storage-buffer memory. Ordering one
// does not oblige a driver to order the other, so the counter bit implies the storage bit
// here - which is what the lowering costs and the only place it can be paid.
static GLbitfield LowerAtomicCounterBarrierBits(GLbitfield barriers) {
if ((barriers & GL_ATOMIC_COUNTER_BARRIER_BIT) != 0) {
barriers |= GL_SHADER_STORAGE_BARRIER_BIT;
}
return barriers;
}
void MemoryBarrier(GLbitfield barriers) { void MemoryBarrier(GLbitfield barriers) {
g_GLESFuncs.glMemoryBarrier(barriers); g_GLESFuncs.glMemoryBarrier(LowerAtomicCounterBarrierBits(barriers));
if (g_GLESCapabilities.IsAngleRenderer) { if (g_GLESCapabilities.IsAngleRenderer) {
g_GLESFuncs.glFlush(); g_GLESFuncs.glFlush();
} }
} }
void MemoryBarrierByRegion(GLbitfield barriers) { void MemoryBarrierByRegion(GLbitfield barriers) {
g_GLESFuncs.glMemoryBarrierByRegion(barriers); g_GLESFuncs.glMemoryBarrierByRegion(LowerAtomicCounterBarrierBits(barriers));
} }
// One endpoint of a glCopyImageSubData, expressed the way the ES driver stores it. // One endpoint of a glCopyImageSubData, expressed the way the ES driver stores it.
@@ -5630,47 +5835,60 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The 1D-array case is not just a rename: GL addresses its layers with y/height while the // The 1D-array case is not just a rename: GL addresses its layers with y/height while the
// ES 2D array that backs it addresses them with z/depth, so the two axes swap with the // ES 2D array that backs it addresses them with z/depth, so the two axes swap with the
// target. // target.
//
// GL_RENDERBUFFER is the exception that must NOT be translated: ES 3.2 core (and
// GL_EXT_copy_image) take it as a srcTarget/dstTarget verbatim, while
// ConvertGLEnumToTextureTarget answers Unknown for it and the translation below would hand
// the driver GL_UNKNOWN_MGL.
struct GLESCopyImageEndpoint { struct GLESCopyImageEndpoint {
GLenum target = GL_TEXTURE_2D; GLenum target = GL_TEXTURE_2D;
// Exactly one of the two is set. The backend object is kept rather than its id, because
// the id is only stable until the OTHER endpoint syncs (a sync can re-mint a texture),
// so it is read at the point of use.
SharedPtr<TextureImpl::BackendTextureObject> texture;
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> renderbuffer;
GLint x = 0; GLint x = 0;
GLint y = 0; GLint y = 0;
GLint z = 0; GLint z = 0;
Bool IsRenderbuffer() const { return renderbuffer != nullptr; }
GLuint Name() const {
if (renderbuffer) return renderbuffer->GetBackendRenderbufferId();
return texture ? texture->GetBackendTextureId() : 0u;
}
}; };
static GLESCopyImageEndpoint MakeGLESCopyImageEndpoint(GLenum appTarget, GLint x, GLint y, GLint z) { // The renderbuffer twin of TextureImpl::SyncTextureObjectToBackend: the same
const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget); // find-or-create-then-sync the framebuffer attachment walk does (see SyncAttachmentObject),
GLESCopyImageEndpoint endpoint{}; // reachable from a path that has a renderbuffer but no framebuffer.
endpoint.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget); static SharedPtr<RenderbufferImpl::BackendRenderbufferObject> SyncRenderbufferObjectToBackend(
if (stateTarget == TextureTarget::Texture1DArray) { const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbufferObject) {
endpoint.x = x; if (!renderbufferObject) return nullptr;
endpoint.y = 0; SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject;
endpoint.z = y; if (auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) {
return endpoint; backendRenderbufferObject = *slot;
} else {
auto& newSlot = RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreate(renderbufferObject);
if (!newSlot) {
newSlot = MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
}
backendRenderbufferObject = newSlot;
} }
endpoint.x = x; backendRenderbufferObject->SyncToBackend(renderbufferObject);
endpoint.y = y; return backendRenderbufferObject;
endpoint.z = z;
return endpoint;
} }
// The region extent swaps the same two axes for a 1D array, and does so for whichever side static Bool MakeGLESCopyImageEndpoint(const CopyImageEndpoint& endpoint, GLenum appTarget, GLint x, GLint y,
// of the copy is one - GL forbids a copy whose two endpoints disagree about how many layers GLint z, GLESCopyImageEndpoint& out) {
// move, so at most one of the two can be a 1D array only in the degenerate single-layer if (endpoint.IsRenderbuffer()) {
// case, where the swap is the identity anyway. out.renderbuffer = SyncRenderbufferObjectToBackend(endpoint.Renderbuffer);
static void ApplyGLESCopyImageExtent(GLenum appSrcTarget, GLenum appDstTarget, GLsizei& height, GLsizei& depth) { if (!out.renderbuffer) return false;
const TextureTarget srcStateTarget = MG_Util::ConvertGLEnumToTextureTarget(appSrcTarget); out.target = GL_RENDERBUFFER;
const TextureTarget dstStateTarget = MG_Util::ConvertGLEnumToTextureTarget(appDstTarget); out.x = x;
if (srcStateTarget != TextureTarget::Texture1DArray && dstStateTarget != TextureTarget::Texture1DArray) { out.y = y;
return; out.z = z;
return true;
} }
std::swap(height, depth);
}
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
// BY VALUE, not by reference. SyncTextureObjectToBackend hands back a reference to a // BY VALUE, not by reference. SyncTextureObjectToBackend hands back a reference to a
// slot inside the backend texture registry, and the second call mutates that very map: // slot inside the backend texture registry, and the second call mutates that very map:
// GetOrCreate indexes it (an insert relocates entries - by rehashing, and also by // GetOrCreate indexes it (an insert relocates entries - by rehashing, and also by
@@ -5680,71 +5898,194 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Either way a reference taken by the first call is stale by the time the second returns, // Either way a reference taken by the first call is stale by the time the second returns,
// and it is read four more times below. Copying the SharedPtr costs two refcount bumps on // and it is read four more times below. Copying the SharedPtr costs two refcount bumps on
// a path that is already doing a texture copy. // a path that is already doing a texture copy.
const SharedPtr<TextureImpl::BackendTextureObject> srcBackendTexture = // An endpoint that named nothing is the frontend validator's INVALID_VALUE and never
TextureImpl::SyncTextureObjectToBackend(srcTexture); // reaches here - but the assertion that says so is compiled out of a release build, and
const SharedPtr<TextureImpl::BackendTextureObject> dstBackendTexture = // SyncTextureObjectToBackend would register a null state object.
TextureImpl::SyncTextureObjectToBackend(dstTexture); if (!endpoint.Texture) return false;
// The DirectVulkan half of this entry point died exactly here, on a texture whose sync out.texture = TextureImpl::SyncTextureObjectToBackend(endpoint.Texture);
// produced nothing - and it died in a release build, where the MOBILEGL_ASSERT that was if (!out.texture) return false;
// supposed to catch it expands to nothing. The four GetBackendTextureId() calls below const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget);
// are the same dereference. The frontend validator is what keeps this unreachable and out.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget);
// what reports the error the application is owed; declining is only how a future gap up // No axis remap for GL_TEXTURE_1D_ARRAY. The frontend STORES a 1D array with its layers
// there stops being a crash. See the level guard in VulkanRenderer::CopyImageSubData. // on y (GetBackendUploadSize moves them across to the ES 2D array's z), but this entry
if (!srcBackendTexture || !dstBackendTexture) { // point does not ADDRESS it that way: GL 4.6 core 18.3.2 treats every array texture as a
MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__); // stack of slices on z and gives a 1D array a height of 1 - exactly the shape the ES 2D
// array has - so GL's (x, 0, layer) and the ES image's (x, 0, layer) already agree.
// Remapping y into z here fetched the wrong slice for every call that spelled the layer
// the way GL defines it.
out.x = x;
out.y = y;
out.z = z;
return true;
}
static TextureInternalFormat GetCopyImageEndpointFormat(const CopyImageEndpoint& endpoint) {
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetInternalFormat();
return endpoint.Texture ? endpoint.Texture->GetFormat() : TextureInternalFormat::Unknown;
}
// Whether this endpoint's CPU shadow can be addressed texel-exactly by the mirror below: one
// upload target (so not a cube map, whose six chains the z axis selects between) and layers on
// the z axis (GL_TEXTURE_1D_ARRAY carries them on y).
static Bool CanMirrorCopyImageShadow(const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
if (!texture) return false;
if (texture->GetTarget() == TextureTarget::Texture1DArray) return false;
return texture->GetUploadTargets().size() == 1;
}
// glCopyImageSubData is defined as a raw texel-block move, so for a destination whose CPU
// shadow has to stay authoritative - a packed format with redundant encodings, where a GPU
// readback can only answer with RE-ENCODED words (see the verbatim branch in GetTexImage) -
// the same move is replayed on the shadow. Nothing is marked dirty: the driver copy already
// put these texels on the GPU, and flagging the level would only schedule a redundant upload
// back over them.
//
// Declined, leaving the shadow exactly as it was, for every shape whose bytes this cannot
// address exactly - a renderbuffer (no shadow at all), a cube or 1D-array endpoint, a level
// whose shadow is missing or not a plain texel grid, a region outside either level, or a
// self-copy within one level, where the row copies could overlap.
static void MirrorCopyImageIntoDestinationShadow(const CopyImageEndpoint& srcEndpoint, GLint srcLevel, GLint srcX,
GLint srcY, GLint srcZ, const CopyImageEndpoint& dstEndpoint,
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei width, GLsizei height, GLsizei depth) {
if (!CanMirrorCopyImageShadow(srcEndpoint.Texture) || !CanMirrorCopyImageShadow(dstEndpoint.Texture)) return;
if (srcEndpoint.Texture == dstEndpoint.Texture && srcLevel == dstLevel) return;
if (width <= 0 || height <= 0 || depth <= 0) return;
if (srcLevel < 0 || dstLevel < 0 || srcX < 0 || srcY < 0 || srcZ < 0 || dstX < 0 || dstY < 0 || dstZ < 0) {
return;
}
auto* srcMipmap = MG_State::GLState::AsMipmapTexture(srcEndpoint.Texture.get());
auto* dstMipmap = MG_State::GLState::AsMipmapTexture(dstEndpoint.Texture.get());
if (!srcMipmap || !dstMipmap) return;
const auto srcUploadTarget = srcEndpoint.Texture->GetUploadTargets()[0];
const auto dstUploadTarget = dstEndpoint.Texture->GetUploadTargets()[0];
const IntVec3 srcSize = srcMipmap->GetMipmapTexelSize(srcUploadTarget, static_cast<Uint>(srcLevel));
const IntVec3 dstSize = dstMipmap->GetMipmapTexelSize(dstUploadTarget, static_cast<Uint>(dstLevel));
const SizeT srcSlices = static_cast<SizeT>(std::max(srcSize.z(), 1));
const SizeT dstSlices = static_cast<SizeT>(std::max(dstSize.z(), 1));
if (srcSize.x() <= 0 || srcSize.y() <= 0 || dstSize.x() <= 0 || dstSize.y() <= 0) return;
const SizeT srcTexels = static_cast<SizeT>(srcSize.x()) * static_cast<SizeT>(srcSize.y()) * srcSlices;
const SizeT dstTexels = static_cast<SizeT>(dstSize.x()) * static_cast<SizeT>(dstSize.y()) * dstSlices;
const SizeT srcBytes = srcMipmap->GetMipmapByteSize(srcUploadTarget, static_cast<Uint>(srcLevel));
const SizeT dstBytes = dstMipmap->GetMipmapByteSize(dstUploadTarget, static_cast<Uint>(dstLevel));
// A shadow that is not exactly texels x texelSize bytes is one this cannot index (a
// compressed blob, or a level whose allocation disagrees with its recorded extent).
const SizeT texelBytes = srcTexels == 0 ? 0 : srcBytes / srcTexels;
if (texelBytes == 0 || srcBytes != srcTexels * texelBytes || dstTexels == 0 ||
dstBytes != dstTexels * texelBytes) {
return;
}
if (static_cast<SizeT>(srcX) + width > static_cast<SizeT>(srcSize.x()) ||
static_cast<SizeT>(srcY) + height > static_cast<SizeT>(srcSize.y()) ||
static_cast<SizeT>(srcZ) + depth > srcSlices ||
static_cast<SizeT>(dstX) + width > static_cast<SizeT>(dstSize.x()) ||
static_cast<SizeT>(dstY) + height > static_cast<SizeT>(dstSize.y()) ||
static_cast<SizeT>(dstZ) + depth > dstSlices) {
return; return;
} }
const GLESCopyImageEndpoint src = MakeGLESCopyImageEndpoint(srcTarget, srcX, srcY, srcZ); const auto* srcBase = static_cast<const Uint8*>(
const GLESCopyImageEndpoint dst = MakeGLESCopyImageEndpoint(dstTarget, dstX, dstY, dstZ); srcMipmap->MapMipmapData(srcUploadTarget, static_cast<Uint>(srcLevel)));
GLsizei copyHeight = srcHeight; auto* dstBase = static_cast<Uint8*>(dstMipmap->MapMipmapData(dstUploadTarget, static_cast<Uint>(dstLevel)));
GLsizei copyDepth = srcDepth; if (!srcBase || !dstBase) return;
ApplyGLESCopyImageExtent(srcTarget, dstTarget, copyHeight, copyDepth);
const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcTexture->GetFormat()); const SizeT rowBytes = static_cast<SizeT>(width) * texelBytes;
const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstTexture->GetFormat()); for (GLsizei slice = 0; slice < depth; ++slice) {
const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcTexture->GetFormat()); for (GLsizei row = 0; row < height; ++row) {
const Bool dstStencil = MG_Util::IsStencilFormatInternalFormat(dstTexture->GetFormat()); const SizeT srcOffset = ((static_cast<SizeT>(srcZ + slice) * static_cast<SizeT>(srcSize.y()) +
if (srcIsDepth || dstIsDepth || srcStencil || dstStencil) { static_cast<SizeT>(srcY + row)) *
static_cast<SizeT>(srcSize.x()) +
static_cast<SizeT>(srcX)) *
texelBytes;
const SizeT dstOffset = ((static_cast<SizeT>(dstZ + slice) * static_cast<SizeT>(dstSize.y()) +
static_cast<SizeT>(dstY + row)) *
static_cast<SizeT>(dstSize.x()) +
static_cast<SizeT>(dstX)) *
texelBytes;
Memcpy(dstBase + dstOffset, srcBase + srcOffset, rowBytes);
}
}
MGLOG_D("CopyImageSubData: mirrored %dx%dx%d texels into the destination's CPU shadow", width, height,
depth);
}
void CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const CopyImageEndpoint& dstEndpoint,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
GLESCopyImageEndpoint src{};
GLESCopyImageEndpoint dst{};
// The DirectVulkan half of this entry point died exactly here, on a texture whose sync
// produced nothing - and it died in a release build, where the MOBILEGL_ASSERT that was
// supposed to catch it expands to nothing. The four Name() calls below are the same
// dereference. The frontend validator is what keeps this unreachable and what reports
// the error the application is owed; declining is only how a future gap up there stops
// being a crash. See the level guard in VulkanRenderer::CopyImageSubData.
if (!MakeGLESCopyImageEndpoint(srcEndpoint, srcTarget, srcX, srcY, srcZ, src) ||
!MakeGLESCopyImageEndpoint(dstEndpoint, dstTarget, dstX, dstY, dstZ, dst)) {
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
return;
}
// Verbatim: GL already spells a 1D array's extent the way the ES 2D array it maps onto
// wants it (height 1, layers on depth) - see MakeGLESCopyImageEndpoint.
const GLsizei copyHeight = srcHeight;
const GLsizei copyDepth = srcDepth;
const TextureInternalFormat srcFormat = GetCopyImageEndpointFormat(srcEndpoint);
const TextureInternalFormat dstFormat = GetCopyImageEndpointFormat(dstEndpoint);
// Both emulation fallbacks below are written against TEXTURE ids and texture targets, so
// an endpoint that is a renderbuffer takes the native ES copy - which accepts
// GL_RENDERBUFFER on both sides - and reports rather than mis-dispatches if the driver
// turns it down.
const Bool anyRenderbuffer = src.IsRenderbuffer() || dst.IsRenderbuffer();
const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcFormat);
const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstFormat);
const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcFormat);
const Bool dstStencil = MG_Util::IsStencilFormatInternalFormat(dstFormat);
if (!anyRenderbuffer && (srcIsDepth || dstIsDepth || srcStencil || dstStencil)) {
MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil, MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil,
"DirectGLES CopyImageSubData only supports depth-only image copies."); "DirectGLES CopyImageSubData only supports depth-only image copies.");
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D, MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
"DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D."); "DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D.");
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1, MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
"DirectGLES depth CopyImageSubData only supports single-layer copies."); "DirectGLES depth CopyImageSubData only supports single-layer copies.");
BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight, BlitDepthTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight,
dstBackendTexture->GetBackendTextureId(), dstLevel, dst.x, dst.y, srcWidth, copyHeight); dst.Name(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
return; return;
} }
if (srcTexture->GetFormat() == TextureInternalFormat::R32F || if (!anyRenderbuffer &&
dstTexture->GetFormat() == TextureInternalFormat::R32F) { (srcFormat == TextureInternalFormat::R32F || dstFormat == TextureInternalFormat::R32F)) {
// The single glGetError below decides the fallback dispatch, and // The single glGetError below decides the fallback dispatch, and
// ErrorLopper::Clear is compiled out at the default log level - drain // ErrorLopper::Clear is compiled out at the default log level - drain
// with the always-live helper so a stale flag cannot misroute a // with the always-live helper so a stale flag cannot misroute a
// succeeded native copy into the 2D-only fallback. // succeeded native copy into the 2D-only fallback.
ClearGLErrors(); ClearGLErrors();
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z, g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z, dst.Name(), dst.target, dstLevel, dst.x, dst.y, dst.z,
srcWidth, copyHeight, copyDepth); srcWidth, copyHeight, copyDepth);
const GLenum copyImageError = g_GLESFuncs.glGetError(); const GLenum copyImageError = g_GLESFuncs.glGetError();
if (copyImageError == GL_NO_ERROR) { if (copyImageError == GL_NO_ERROR) {
return; return;
} }
MOBILEGL_ASSERT(IsColorOnlyFormat(srcTexture->GetFormat()) && IsColorOnlyFormat(dstTexture->GetFormat()), MOBILEGL_ASSERT(IsColorOnlyFormat(srcFormat) && IsColorOnlyFormat(dstFormat),
"DirectGLES CopyImageSubData only supports color-only or depth-only copies."); "DirectGLES CopyImageSubData only supports color-only or depth-only copies.");
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D, MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
"DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D."); "DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D.");
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1, MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
"DirectGLES color CopyImageSubData only supports single-layer copies."); "DirectGLES color CopyImageSubData only supports single-layer copies.");
CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight, CopyR32FTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y); dst.Name(), dst.target, dstLevel, dst.x, dst.y);
return; return;
} }
ClearGLErrors(); ClearGLErrors();
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z, g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z, dst.Name(), dst.target, dstLevel, dst.x, dst.y, dst.z,
srcWidth, copyHeight, copyDepth); srcWidth, copyHeight, copyDepth);
// Every error condition glCopyImageSubData has was already ruled out by the frontend // Every error condition glCopyImageSubData has was already ruled out by the frontend
// validator, so a driver error here is an internal invariant violation, not something // validator, so a driver error here is an internal invariant violation, not something
@@ -5760,6 +6101,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_Util::ConvertGLEnumToString(dst.target).c_str(), MG_Util::ConvertGLEnumToString(dst.target).c_str(),
MG_Util::ConvertGLEnumToString(dstTarget).c_str()); MG_Util::ConvertGLEnumToString(dstTarget).c_str());
MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request."); MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request.");
return;
}
// The copy landed on the GPU. For a destination whose readback cannot be bit-exact the
// CPU shadow is what glGetTexImage answers from, so it has to follow the same move -
// otherwise it hands back whatever the level held before this copy.
if (MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(dstFormat)) {
MirrorCopyImageIntoDestinationShadow(srcEndpoint, srcLevel, srcX, srcY, srcZ, dstEndpoint, dstLevel,
dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth);
} }
} }
@@ -7050,10 +7399,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
data = std::move(expanded); data = std::move(expanded);
} }
static void DrainESErrors() { static void DrainESErrors() { DrainDriverErrors("ReadPixels"); }
for (Int i = 0; i < 32 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
}
}
static GLenum QueryReadAttachmentComponentType() { static GLenum QueryReadAttachmentComponentType() {
GLint framebufferId = 0; GLint framebufferId = 0;
@@ -7339,6 +7685,149 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true; return true;
} }
// ---- Bit-exact readback of a 32-bit packed colour level ---------------------------------------
//
// glGetTexImage of a packed format read with its OWN client type owes the application the words
// the image HOLDS, and neither of the two routes above can promise that once anything other than
// a glTexImage has written the level:
//
// * the colour-attachment route reads GL_RGBA/GL_FLOAT and re-encodes, which canonicalizes an
// RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000, same value, different bits) and
// collapses an R11F_G11F_B10F NaN to the canonical payload 1
// (MG_Util::EncodeFloatToUnsignedSmallFloat) - and a copy-image from RGB9_E5 lands exactly
// such a NaN in the 10-bit blue field every time, because the source's shared-exponent
// field is all ones;
// * the CPU shadow only ever holds what was UPLOADED, so for a level glCopyImageSubData wrote
// it answers with the PRE-COPY contents. MirrorCopyImageIntoDestinationShadow patches that
// up for the shapes it can address texel-exactly and declines for the rest - a renderbuffer
// source (which has no shadow to mirror from at all), a cube or 1D-array endpoint, a
// self-copy - and the decline is silent, so the stale words are served as truth.
//
// glCopyImageSubData is a raw texel-block move and EXT_copy_image puts every 32-bit colour
// format in one compatibility class, so copying the level into a scratch GL_R32UI image and
// reading THAT back as unsigned integers hands over the stored words themselves, whoever wrote
// them. This is what lets the shadow stop being the authority for these formats: it is tried
// first, and every step reports rather than guesses, so a driver that turns any of it down
// simply leaves the old shadow/attachment fallbacks to run.
static GLuint g_packedWordScratchTextureId = 0;
static GLsizei g_packedWordScratchWidth = 0;
static GLsizei g_packedWordScratchHeight = 0;
// Grow-only, so a readback sweep over a mip chain allocates once. Zero when the driver refused
// the storage, which is a decline and not an error.
static GLuint EnsurePackedWordScratchTexture(GLsizei width, GLsizei height) {
if (g_packedWordScratchTextureId != 0 && g_packedWordScratchWidth >= width &&
g_packedWordScratchHeight >= height) {
return g_packedWordScratchTextureId;
}
const GLsizei newWidth = std::max(width, g_packedWordScratchWidth);
const GLsizei newHeight = std::max(height, g_packedWordScratchHeight);
if (g_packedWordScratchTextureId != 0) {
// A scratch FBO may still name the old id, and the driver is free to hand the same
// number back for the replacement - which would false-skip the re-attach.
ScratchFBOImpl::NoteTextureIdDeleted(g_packedWordScratchTextureId);
g_GLESFuncs.glDeleteTextures(1, &g_packedWordScratchTextureId);
g_packedWordScratchTextureId = 0;
g_packedWordScratchWidth = 0;
g_packedWordScratchHeight = 0;
}
GLuint texture = 0;
g_GLESFuncs.glGenTextures(1, &texture);
if (texture == 0) return 0;
ClearGLErrors();
TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit);
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, texture);
// Immutable single-level storage: glCopyImageSubData wants a complete image, and
// glTexStorage clamps TEXTURE_MAX_LEVEL, which is what makes a one-level texture complete
// under the default mipmapping filter.
g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, newWidth, newHeight);
const GLenum storageError = g_GLESFuncs.glGetError();
// Re-bind whatever the binding cache says lives on the temp unit, so the cache stays
// truthful without a driver query (same discipline as CopyR32FTexture2D).
auto* cachedBound = TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit]
[static_cast<SizeT>(TextureTarget::Texture2D)];
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, cachedBound ? cachedBound->GetBackendTextureId() : 0);
if (storageError != GL_NO_ERROR) {
g_GLESFuncs.glDeleteTextures(1, &texture);
MGLOG_D("GetTexImage: no %dx%d GL_R32UI scratch image (%s); the verbatim word readback is unavailable",
newWidth, newHeight, MG_Util::ConvertGLEnumToString(storageError).c_str());
return 0;
}
g_packedWordScratchTextureId = texture;
g_packedWordScratchWidth = newWidth;
g_packedWordScratchHeight = newHeight;
return texture;
}
static void ReleasePackedWordScratchTexture() {
// The ES context (and the name with it) is gone; deleting here would target a recycled
// name in the successor context.
g_packedWordScratchTextureId = 0;
g_packedWordScratchWidth = 0;
g_packedWordScratchHeight = 0;
}
// One slice of `backendTarget`'s level, as width*height stored 32-bit words in `outWords`.
static Bool ReadPackedLevelWordsViaScratch(GLuint texture, GLenum backendTarget, GLint level, GLint slice,
GLsizei width, GLsizei height, Uint32* outWords) {
if (texture == 0 || outWords == nullptr || width <= 0 || height <= 0 || level < 0 || slice < 0) return false;
if (!g_GLESFuncs.glCopyImageSubData) return false;
// Horizontal bands, so neither the scratch image nor the staging buffer scales with the
// level. The scratch is grow-only on purpose - a sweep down a mip chain must not
// reallocate per level - which without a band cap would leave a 4096x4096 readback's
// 64 MiB image parked for the rest of the process. The cap is 1 MiB of GL_R32UI, with
// 4 MiB of staging behind it because the read lands four words per texel.
constexpr SizeT kMaxScratchTexels = SizeT{1} << 18;
const GLsizei bandRows = std::max<GLsizei>(
1, static_cast<GLsizei>(std::min<SizeT>(kMaxScratchTexels / static_cast<SizeT>(width),
static_cast<SizeT>(height))));
const GLuint scratch = EnsurePackedWordScratchTexture(width, bandRows);
if (scratch == 0) return false;
ScopedFramebufferBinding readBinding(/*saveRead=*/true, /*saveDraw=*/false);
auto& scratchFB = ScratchFBOImpl::BlitReadFramebuffer();
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, ScratchFBOImpl::EnsureId(scratchFB));
ScratchFBOImpl::EnsureColorAttachment2D(scratchFB, GL_READ_FRAMEBUFFER, scratch, GL_TEXTURE_2D, 0);
ScratchFBOImpl::EnsureReadBuffer(scratchFB, GL_COLOR_ATTACHMENT0);
if (g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
MGLOG_D("GetTexImage: the GL_R32UI scratch attachment is incomplete; falling back");
return false;
}
// GL_RGBA_INTEGER/GL_UNSIGNED_INT is the one combination ES guarantees for an integer
// colour buffer, so the read lands four words per texel and the red one is compacted out
// here. The PACK scope is the tight default rather than the application's, so a row comes
// back packed at exactly `width * 4` words. One glGetError covers the whole loop: it
// accumulates, and a failure anywhere means the caller falls back rather than trusting a
// partial result.
const SizeT wordsPerRow = static_cast<SizeT>(width) * 4;
Vector<Uint32> staging(static_cast<SizeT>(bandRows) * wordsPerRow);
ScopedPixelPackBuffer packBuffer(0);
ScopedPackState packState(PixelStoreImpl::PackState{4, 0, 0, 0});
ClearGLErrors();
for (GLsizei y = 0; y < height; y += bandRows) {
const GLsizei rows = std::min(bandRows, height - y);
g_GLESFuncs.glCopyImageSubData(texture, backendTarget, level, 0, y, slice, scratch, GL_TEXTURE_2D, 0, 0,
0, 0, width, rows, 1);
g_GLESFuncs.glReadPixels(0, 0, width, rows, GL_RGBA_INTEGER, GL_UNSIGNED_INT, staging.data());
for (GLsizei row = 0; row < rows; ++row) {
const Uint32* srcRow = staging.data() + static_cast<SizeT>(row) * wordsPerRow;
Uint32* dstRow = outWords + static_cast<SizeT>(y + row) * static_cast<SizeT>(width);
for (GLsizei x = 0; x < width; ++x) dstRow[x] = srcRow[static_cast<SizeT>(x) * 4];
}
}
const GLenum error = g_GLESFuncs.glGetError();
if (error != GL_NO_ERROR) {
MGLOG_D("GetTexImage: the GL_R32UI word readback of %s was refused (%s); falling back",
MG_Util::ConvertGLEnumToString(backendTarget).c_str(),
MG_Util::ConvertGLEnumToString(error).c_str());
return false;
}
return true;
}
static Bool IsLegacyNativeReadPixelsFormat(GLenum format) { static Bool IsLegacyNativeReadPixelsFormat(GLenum format) {
return format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || format == GL_RED_INTEGER || return format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || format == GL_RED_INTEGER ||
format == GL_DEPTH_COMPONENT || format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL; format == GL_DEPTH_COMPONENT || format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL;
@@ -7612,9 +8101,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
tempFB, GL_READ_FRAMEBUFFER, backendTexId, tempFB, GL_READ_FRAMEBUFFER, backendTexId,
backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, level, backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, level,
/*withStencil=*/format == GL_DEPTH_STENCIL); /*withStencil=*/format == GL_DEPTH_STENCIL);
} else if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY) { } else if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY ||
// ES cannot attach 3D/array textures through glFramebufferTexture2D; read layer 0. Reads backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY) {
// of deeper slices are served from the CPU shadow instead (see the shadow-first branch). // ES cannot attach 3D/array textures through glFramebufferTexture2D; layer 0 here, and
// the deeper slices one at a time in the per-layer loop below. A CUBE MAP ARRAY is in
// this list for the same reason its layer-faces are addressed as array layers:
// glFramebufferTexture2D has no target token for it, so the 2D attach it used to take
// left the scratch FBO incomplete and every read fell through to the (stale) CPU
// shadow - which is exactly the all-zero result the conformance suite saw.
ScratchFBOImpl::EnsureColorAttachmentLayer(tempFB, GL_READ_FRAMEBUFFER, backendTexId, level, 0); ScratchFBOImpl::EnsureColorAttachmentLayer(tempFB, GL_READ_FRAMEBUFFER, backendTexId, level, 0);
} else { } else {
ScratchFBOImpl::EnsureColorAttachment2D( ScratchFBOImpl::EnsureColorAttachment2D(
@@ -7667,6 +8161,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto size = textureMipmapObject->GetMipmapTexelSize(MG_Util::ConvertGLEnumToTextureUploadTarget(target), level); auto size = textureMipmapObject->GetMipmapTexelSize(MG_Util::ConvertGLEnumToTextureUploadTarget(target), level);
// GL_TEXTURE_1D_ARRAY keeps its LAYERS in the state-side height (that is what
// glTexImage2D(GL_TEXTURE_1D_ARRAY, w, layers) means), while the ES texture behind it is a
// 2D array of height 1 with the layers in depth - GetBackendUploadSize performs exactly
// that swap on the way in. Everything below addresses the ES image, so the same swap has
// to happen here: without it the readback asked layer 0 for a `layers`-row rectangle it
// does not have, and every layer but the first came back undefined (all zeroes on Adreno,
// KHR-GL4x.shader_image_load_store.basic-allTargets-*).
const Bool oneDimensionalArray = textureObject->GetTarget() == TextureTarget::Texture1DArray;
if (oneDimensionalArray) {
size = TextureImpl::GetBackendUploadSize(TextureTarget::Texture1DArray, size);
}
MGLOG_D("GetTexImage: mip level %d size = %dx%d", level, size.x(), size.y()); MGLOG_D("GetTexImage: mip level %d size = %dx%d", level, size.x(), size.y());
// Prefer the client-format conversion for every convertible combination: the "native" ES pairs // Prefer the client-format conversion for every convertible combination: the "native" ES pairs
@@ -7680,12 +8186,75 @@ namespace MobileGL::MG_Backend::DirectGLES {
TextureImpl::BackendTextureFormatAddsAlpha(textureObject->GetFormat(), textureObject->GetTarget()); TextureImpl::BackendTextureFormatAddsAlpha(textureObject->GetFormat(), textureObject->GetTarget());
// GL_PACK_IMAGE_HEIGHT/GL_PACK_SKIP_IMAGES only apply to 3D/array image // GL_PACK_IMAGE_HEIGHT/GL_PACK_SKIP_IMAGES only apply to 3D/array image
// readbacks (cube-map arrays address as arrays); 2D targets must ignore // readbacks (cube-map arrays address as arrays); 2D targets must ignore
// them (GL 3.3 section 6.1.4). // them (GL 3.3 section 6.1.4). A 1D ARRAY is one of those 2D targets: GL hands it back
const Bool applyPackImageParams = backendAttachTarget == GL_TEXTURE_3D || // as a single two-dimensional image whose ROWS are the layers, so the layer stride is
backendAttachTarget == GL_TEXTURE_2D_ARRAY || // one packed row and the image parameters do not enter into it - even though the ES
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY; // texture underneath is an array and is read one layer at a time.
const Bool applyPackImageParams = !oneDimensionalArray &&
(backendAttachTarget == GL_TEXTURE_3D ||
backendAttachTarget == GL_TEXTURE_2D_ARRAY ||
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY);
const GLsizei sliceCount = std::max(size.z(), 1); const GLsizei sliceCount = std::max(size.z(), 1);
const Bool multiSlice = size.z() > 1; const Bool multiSlice = size.z() > 1;
// glGetTexImage answers with the STORED texels, and for a packed format whose encoding
// is not unique the GPU route below cannot: it reads GL_RGBA/GL_FLOAT and re-encodes,
// which canonicalizes an RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000 - the same
// value 8064, different words), and the conformance suite compares the words
// ("CopyImageSubData modified contents of source image"). The scratch FBO does NOT
// decide this for us: Adreno reports an RGB9_E5 colour attachment complete, so the
// shadow branch further down was unreachable. Every other format still prefers the
// GPU, so a rendered-into texture is unaffected.
const Bool rawPackedWordRead = MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
textureObject->GetFormat(), MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
// ...and the GPU CAN answer with the stored words after all, for any 32-bit packed
// format and whoever wrote the level, by going through a scratch GL_R32UI image (see
// ReadPackedLevelWordsViaScratch). Preferred over both routes below because it is the
// only one that is right for a level glCopyImageSubData wrote: the shadow may never
// have seen that write, and re-encoding the attachment cannot reproduce an RGB9_E5
// shared exponent or an R11F_G11F_B10F NaN payload. A multisample image is excluded
// because copy-image requires matching sample counts.
if (rawPackedWordRead && textureObject->GetSamples() == 0) {
// Copy-image addresses a cube map as ONE image with the face on z, where
// glGetTexImage names the face in its target.
const auto readUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
const GLint copyBaseSlice =
(readUploadTarget >= TextureUploadTarget::CubeMapPositiveX &&
readUploadTarget <= TextureUploadTarget::CubeMapNegativeZ)
? static_cast<GLint>(readUploadTarget) -
static_cast<GLint>(TextureUploadTarget::CubeMapPositiveX)
: 0;
const GLenum copyTarget =
TextureImpl::ConvertTextureTargetToBackendGLEnum(textureObject->GetTarget());
const SizeT sliceWords = static_cast<SizeT>(size.x()) * static_cast<SizeT>(size.y());
Vector<Uint32> words(sliceWords * static_cast<SizeT>(sliceCount));
Bool allSlicesRead = true;
for (GLsizei slice = 0; slice < sliceCount && allSlicesRead; ++slice) {
allSlicesRead = ReadPackedLevelWordsViaScratch(backendTexId, copyTarget, level,
copyBaseSlice + slice, size.x(), size.y(),
words.data() + sliceWords * static_cast<SizeT>(slice));
}
if (allSlicesRead &&
ReadbackImpl::StorePackedWordsToClient(reinterpret_cast<const Uint8*>(words.data()), size.x(),
size.y(), sliceCount, type, pixels,
applyPackImageParams)) {
MGLOG_D("GetTexImage: finished %d slice(s) via the bit-exact GL_R32UI word readback", sliceCount);
return;
}
}
// The last resort for the one format the attachment route can never answer for: the
// shadow is only right while nothing but a glTexImage has written the level, which is
// why CopyImageSubData mirrors itself into it where it can.
const Bool verbatimPackedShadowRead =
MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(textureObject->GetFormat()) &&
rawPackedWordRead;
if (verbatimPackedShadowRead &&
GetTexImageViaShadowConversion(textureMipmapObject,
MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(),
size.y(), sliceCount, format, type, pixels, applyPackImageParams)) {
MGLOG_D("GetTexImage: finished via shadow conversion (verbatim packed words)");
return;
}
// A multi-slice read used to go to the CPU shadow outright, on the grounds that the // A multi-slice read used to go to the CPU shadow outright, on the grounds that the
// scratch FBO can only expose one layer at a time. But the shadow only holds what was // scratch FBO can only expose one layer at a time. But the shadow only holds what was
// uploaded, so every slice that was rendered to came back stale - which is exactly what // uploaded, so every slice that was rendered to came back stale - which is exactly what
@@ -7693,7 +8262,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Attach the layers one at a time instead and read each off the GPU, keeping the shadow // Attach the layers one at a time instead and read each off the GPU, keeping the shadow
// for the formats the FBO cannot represent at all. // for the formats the FBO cannot represent at all.
if (multiSlice && tempFBOComplete && if (multiSlice && tempFBOComplete &&
(backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY)) { (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY ||
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY)) {
// Each slice is packed as its own 2D image, so the per-slice call must not apply // Each slice is packed as its own 2D image, so the per-slice call must not apply
// GL_PACK_SKIP_IMAGES / GL_PACK_IMAGE_HEIGHT itself - this walks the destination // GL_PACK_SKIP_IMAGES / GL_PACK_IMAGE_HEIGHT itself - this walks the destination
// over them, using the same layout StoreWideRowsToClient computes. // over them, using the same layout StoreWideRowsToClient computes.
@@ -8871,6 +9441,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
XfbImpl::OnBackendContextDestroyed(); XfbImpl::OnBackendContextDestroyed();
MultiDrawImpl::OnBackendContextDestroyed(); MultiDrawImpl::OnBackendContextDestroyed();
ScratchFBOImpl::OnBackendContextDestroyed(); ScratchFBOImpl::OnBackendContextDestroyed();
ReleasePackedWordScratchTexture();
FramebufferImpl::InvalidateFramebufferBindingCache(); FramebufferImpl::InvalidateFramebufferBindingCache();
VertexArrayImpl::InvalidateVAOBindingCache(); VertexArrayImpl::InvalidateVAOBindingCache();
PixelStoreImpl::InvalidatePackStateCache(); PixelStoreImpl::InvalidatePackStateCache();
+2 -2
View File
@@ -76,9 +76,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLsizei height, GLint border); GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height); GLsizei height);
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, void CopyImageSubData(const CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target); void GenerateMipmap(GLenum target);
File diff suppressed because it is too large Load Diff
+105
View File
@@ -21,6 +21,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType); String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType); String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType);
// Whether a vertex shader may declare a storage block at all, given what the host driver
// reports for GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS. Pure, and separated from the capability
// global purely so the decision can be tested without one.
//
// The indirect half of the gl_BaseInstance lowering in PromoteDrawParameterGlobalsToUniforms
// is the only thing that needs this, and it needs exactly one block. A driver reporting 0 is
// conformant - the minimum is 0 in GL 4.6 table 23.64 and ES 3.2 table 21.44 - and ARM's
// GLES driver does report 0, so this is a live path, not a defensive one.
Bool VertexStageStorageBlockUsable(Int maxVertexShaderStorageBlocks);
// True once the process has entered exit(): past that point the EGL library and // True once the process has entered exit(): past that point the EGL library and
// the driver may already be unloaded, so a backend twin's destructor must not // the driver may already be unloaded, so a backend twin's destructor must not
// call into g_GLESFuncs (the observed crash is a jump through an unmapped driver // call into g_GLESFuncs (the observed crash is a jump through an unmapped driver
@@ -129,7 +139,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Twin creation is the moment a driver-owned id starts needing a guarded // Twin creation is the moment a driver-owned id starts needing a guarded
// destructor; cold path, so the once-guard costs nothing per draw. // destructor; cold path, so the once-guard costs nothing per draw.
EnsureProcessTeardownSentinel(); EnsureProcessTeardownSentinel();
// Sweep BEFORE the entry reference below exists: the map is open-addressed and an
// erase relocates the rest of the probe cluster, so collecting once that reference
// is taken would invalidate it. The sweep is therefore owed from an earlier call
// rather than triggered by this one.
if (m_creationTick >= kCreationGCInterval) {
m_creationTick = 0;
CollectGarbage();
}
const SizeT entryCountBeforeInsert = m_entries.size();
auto& entry = m_entries[stateObj.get()]; auto& entry = m_entries[stateObj.get()];
if (m_entries.size() != entryCountBeforeInsert) {
// A key the registry has never held. Nothing tells the backend that a texture or
// renderbuffer was DELETED - the twin, and the driver storage it owns, lives
// until a collection - and CollectGarbageIfNeeded is ticked only from the
// per-draw sync paths, which a CTS-shaped workload runs about ten times per
// case. 1024 of those ticks then span ~100 cases, so ~100 cases' worth of dead
// (and, for this suite, gigabyte-sized) objects stay allocated at once. Object
// CHURN rather than draw count is what makes the sweep urgent, so a twin the
// registry has never seen ticks it too - and it does so on the path that is
// about to allocate, which is exactly when the memory is needed.
++m_creationTick;
}
if (entry.stateRef.expired()) { if (entry.stateRef.expired()) {
// The previous owner of this address is gone and the allocator handed it // The previous owner of this address is gone and the allocator handed it
// to a new object: its twin describes ids the new state object never made. // to a new object: its twin describes ids the new state object never made.
@@ -203,8 +234,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
private: private:
static constexpr Uint32 kGCInterval = 1024; static constexpr Uint32 kGCInterval = 1024;
// Creations are far rarer than draws, so this counts in a much smaller unit than
// kGCInterval does.
static constexpr Uint32 kCreationGCInterval = 64;
BackendMap m_entries; BackendMap m_entries;
Uint32 m_gcTick = 0; Uint32 m_gcTick = 0;
Uint32 m_creationTick = 0;
Bool m_isCollecting = false; Bool m_isCollecting = false;
}; };
@@ -361,6 +396,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id); void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size); void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
void InvalidateIndexedBufferBindingCache(); void InvalidateIndexedBufferBindingCache();
// Re-issues the GL_ATOMIC_COUNTER_BUFFER binding points a program's shaders declare as
// GL_SHADER_STORAGE_BUFFER bindings at the reserved slots the transpiled ESSL was built
// against (BackendProgramObjectImpl::GetAtomicCounterBindings /
// GetAtomicCounterEsslBindingTop). ES has no counter-buffer target at all, so without
// this the shader reads a storage block nobody ever bound a buffer to and the buffer the
// application bound never reaches the driver.
void SyncAtomicCounterBuffers(const Vector<Int>& glBindings, Int esslBindingTop);
// Buffer-storage pool maintenance. TrimBufferPool evicts over-budget entries // Buffer-storage pool maintenance. TrimBufferPool evicts over-budget entries
// (called once per frame from Present); ClearBufferPool drops all pooled ids // (called once per frame from Present); ClearBufferPool drops all pooled ids
// without glDeleteBuffers (called when the ES context is going away). // without glDeleteBuffers (called when the ES context is going away).
@@ -467,10 +509,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
PendingAttribValueMask& GetPendingAttribValueMaskMemo() { return m_pendingAttribValueMask; } PendingAttribValueMask& GetPendingAttribValueMaskMemo() { return m_pendingAttribValueMask; }
private: private:
// Narrows one enabled GL_DOUBLE array into a tightly packed float32 stream held in
// this VAO's own scratch buffer and declares the attribute against it. ES has no
// 64-bit vertex format, but the source bytes are ordinary IEEE-754 doubles and every
// fp64 value in every shader is already narrowed to 32 bits (DemoteFloat64Pass), so
// narrowing the ARRAY is the coherent completion of that decision rather than
// dropping it. Returns false when the stream cannot be built, in which case the
// caller must DISABLE the array - leaving a 64-bit array enabled with no pointer is
// what the Adreno driver turns into a SIGSEGV at the next draw.
Bool SyncFloat64AttributeAsFloat32(Uint attribIndex, const MG_State::GLState::VertexAttribute& attrib,
Uint32 fetchBaseInstance);
// What the converted float32 stream in m_convertedAttributeBufferIds[i] was built
// from. A hit skips the CPU conversion and the re-upload; the buffer's change serial
// is part of the key, so a glBufferSubData into the source invalidates it.
struct ConvertedFloat64Stream {
Bool valid = false;
Uint64 sourceLifetimeId = 0;
Uint64 sourceChangeSerial = 0;
SizeT sourceOffset = 0;
SizeT sourceStride = 0;
SizeT componentCount = 0;
SizeT elementCount = 0;
};
ResolvedDrawBuffers m_resolvedDrawBuffers; ResolvedDrawBuffers m_resolvedDrawBuffers;
PendingAttribValueMask m_pendingAttribValueMask; PendingAttribValueMask m_pendingAttribValueMask;
Uint m_backendVAOId = 0; Uint m_backendVAOId = 0;
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds; Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
// Scratch stores for the buffer-backed GL_DOUBLE narrowing. Deliberately separate
// from m_clientAttributeBufferIds: that one holds the per-draw upload of a
// CLIENT-MEMORY array, and an attribute index can carry both shapes over its life.
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_convertedAttributeBufferIds;
Array<ConvertedFloat64Stream, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
m_convertedAttributeStreams;
// True while at least one attribute of this VAO is fed by a converted stream. Such a
// stream is derived from buffer CONTENT, which no VAO version covers, so the config
// version early-out in SyncToBackend must not be trusted while it is set.
Bool m_hasConvertedFloat64Attribute = false;
Bool m_isInitialized = false; Bool m_isInitialized = false;
Uint16 m_syncedIndexBufferVersion = 0; Uint16 m_syncedIndexBufferVersion = 0;
// Identity of the buffer the version above was stamped against. Raw and never // Identity of the buffer the version above was stamped against. Raw and never
@@ -1051,6 +1127,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
namespace PrgramImpl { namespace PrgramImpl {
// Defined further down, next to CollectImageFormatBakeInputs; only referenced here.
struct ImageFormatBakeInputs;
class BackendProgramObjectImpl { class BackendProgramObjectImpl {
public: public:
// Per-link cache of a sampler-style uniform's backend location: built once in // Per-link cache of a sampler-style uniform's backend location: built once in
@@ -1136,6 +1215,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
// qualifier, so the overrides are baked into the source). A mismatch means the // qualifier, so the overrides are baked into the source). A mismatch means the
// program is stale exactly like the clamp masks above. // program is stale exactly like the clamp masks above.
Uint64 GetShaderStorageBlockBindingSignature() const { return m_shaderStorageBlockBindingSignature; } Uint64 GetShaderStorageBlockBindingSignature() const { return m_shaderStorageBlockBindingSignature; }
// GL atomic-counter binding points the transpiled stages declare (sorted, unique),
// and the top of the reserved shader-storage range their counter blocks were
// transpiled against - the slot for GL binding N is `top - N`. Empty for every
// program that uses no atomic counter, which is what keeps the per-draw cost of the
// counter sync at one empty-vector test.
const Vector<Int>& GetAtomicCounterBindings() const { return m_atomicCounterGlBindings; }
Int GetAtomicCounterEsslBindingTop() const { return m_atomicCounterEsslBindingTop; }
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; } Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; } const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
@@ -1184,6 +1270,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
private: private:
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject); void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
// One stage's SPIR-V through the DirectGLES pass chain and SPIRV-Cross, producing
// the raw emitted ESSL and the interface blocks this stage's XFB flattening
// rewrote. This is the segment the L2 shader-translation memo keys on, so every
// input it reads must appear in EsslTranslationKeyInputs - see the definition's
// header comment in Managers.cpp and MG_Util/ShaderTranspiler/TranslationCache.h.
// False means SPIRV-Cross refused the module; `outError` then carries its message.
Bool TranspileSpirvToEssl(const Vector<unsigned int>& spirvCode, GLenum glShaderType,
const std::set<String>& xfbCaptureBlockNames,
const ImageFormatBakeInputs& imageFormatBake,
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
const std::map<String, String>& inputBlockRenames,
const std::map<String, String>& outputBlockRenames,
Int atomicCounterEsslBindingTop, Bool enableSpirvValidation,
String& outSource,
std::set<String>& outFlattenedXfbBlockNames,
Vector<Int>& outAtomicCounterGlBindings, String& outError) const;
Uint m_backendProgramId = 0; Uint m_backendProgramId = 0;
// GL name of the frontend program this was last synced from; diagnostics only, so // GL name of the frontend program this was last synced from; diagnostics only, so
// an unusable backend program can be traced back to the glCreateProgram id the app // an unusable backend program can be traced back to the glCreateProgram id the app
@@ -1202,6 +1305,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint m_fragColorBroadcastCount = 1; Uint m_fragColorBroadcastCount = 1;
// 0 is the signature of an empty override set, i.e. what almost every program has. // 0 is the signature of an empty override set, i.e. what almost every program has.
Uint64 m_shaderStorageBlockBindingSignature = 0; Uint64 m_shaderStorageBlockBindingSignature = 0;
Vector<Int> m_atomicCounterGlBindings;
Int m_atomicCounterEsslBindingTop = -1;
Bool m_isInitialized = false; Bool m_isInitialized = false;
Bool m_backendProgramUsable = false; Bool m_backendProgramUsable = false;
+107 -5
View File
@@ -171,6 +171,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) { if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget; options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
} }
// 8-bit signed-normalized storage is core ES, so only the rendering half is in
// question here; the 16-bit bit above additionally needs EXT_texture_norm16 for the
// encoding to exist at all.
if (!capabilities.SupportsRenderSnorm) {
options |= PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget;
}
return options; return options;
} }
@@ -569,6 +575,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
return glslCode; return glslCode;
} }
String RequestViewportArrayExtension(String glslCode, Bool needed) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// gl_ViewportIndex is desktop GL 4.1 core and is in ESSL only under
// GL_OES_viewport_array. SPIRV-Cross prints the identifier as-is and requests no
// extension for it - three lines away from the BuiltInLayer case, which DOES ask for
// one on ES - so an untouched decompile reaches the driver naming a builtin its core
// language has never heard of. The stage then fails to compile, the program is marked
// unusable and every draw made with it renders nothing while raising no GL error.
//
// Same `needed` contract as RequestExtendedImageFormats, and the same hard rule:
// `#extension` on a name the driver does not advertise is itself a compile error
// (ARM's compiler is strict about it), so this must never be emitted speculatively.
// A driver without the extension does not come through here at all - its module took
// the LowerViewportIndexPass fallback and the emitted source no longer names the
// builtin.
static constexpr const char* kDirective = "#extension GL_OES_viewport_array : require\n";
static constexpr const char* kExtName = "GL_OES_viewport_array";
if (!needed || glslCode.find(kExtName) != String::npos) {
return glslCode;
}
// Right after the #version line, for the reason spelled out above: it is the only
// position that must stay first, and ForceSupporterOutput's scan for the LAST
// #extension directive still finds whichever one that ends up being.
const SizeT versionPos = glslCode.find("#version");
if (versionPos == String::npos) {
return kDirective + glslCode;
}
const SizeT lineEnd = glslCode.find('\n', versionPos);
if (lineEnd == String::npos) {
return glslCode + "\n" + kDirective;
}
glslCode.insert(lineEnd + 1, kDirective);
return glslCode;
}
String BakeImageFormatQualifiers(String glslCode, String BakeImageFormatQualifiers(String glslCode,
const UnorderedMap<String, String>& esslFormatByUniformName) { const UnorderedMap<String, String>& esslFormatByUniformName) {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
@@ -785,9 +828,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
// A rebuilt declaration. Keeps SPIRV-Cross's own word order (`uniform readonly // A rebuilt declaration. Keeps SPIRV-Cross's own word order (`uniform readonly
// highp image2D`) so the image-rebinding regex in Managers.cpp still matches what // highp image2D`) so the image-rebinding regex in Managers.cpp still matches what
// comes out of here, whichever order the two passes end up running in. // comes out of here, whichever order the two passes end up running in.
//
// `forceCoherent` is for the SPLIT pair only. GLSL guarantees that a write through
// one image variable is visible to a read through a DIFFERENT one only when both are
// declared coherent, and the split turns a same-variable read-after-write - which
// desktop GLSL orders by construction, so the source almost never says `coherent` -
// into exactly that cross-variable shape. Without it the driver may serve the load
// from a cache that never saw the store through the writeonly half.
String BuildImageDeclaration(const ImageUniformDecl& decl, const char* memoryQualifier, String BuildImageDeclaration(const ImageUniformDecl& decl, const char* memoryQualifier,
const String& variableName) { const String& variableName, Bool forceCoherent = false) {
String out = "layout(" + decl.layout + ") uniform "; String out = "layout(" + decl.layout + ") uniform ";
if (forceCoherent && !ContainsIdentifier(decl.qualifiers, "coherent")) {
out += "coherent ";
}
out += memoryQualifier; out += memoryQualifier;
out += ' '; out += ' ';
if (!decl.qualifiers.empty()) { if (!decl.qualifiers.empty()) {
@@ -829,12 +882,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
SizeT length; SizeT length;
String text; String text;
}; };
// The offset just past the `;` that terminates the call whose argument list opens at
// `openParen`, or npos when what follows is not a plain statement. Parentheses alone
// are counted: every other bracket a GLSL argument list can contain is balanced
// inside them, and imageStore returns void, so a well-formed call site is always
// `imageStore(...);` and anything else is a shape this pass declines to edit.
SizeT FindEndOfCallStatement(const String& code, SizeT openParen) {
Int depth = 0;
SizeT scan = openParen;
for (; scan < code.size(); ++scan) {
if (code[scan] == '(') {
++depth;
} else if (code[scan] == ')' && --depth == 0) {
break;
}
}
if (scan >= code.size()) return String::npos;
const SizeT after = code.find_first_not_of(" \t\r\n", scan + 1);
if (after == String::npos || code[after] != ';') return String::npos;
return after + 1;
}
} // namespace } // namespace
String SplitReadWriteImageUniforms(const String& glslCode) { String SplitReadWriteImageUniforms(const String& glslCode, Uint* outSplitCount) {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
// Written before any early return, so the caller never reads a stale count.
if (outSplitCount != nullptr) *outSplitCount = 0;
if (glslCode.find("image") == String::npos) { if (glslCode.find("image") == String::npos) {
return glslCode; return glslCode;
} }
@@ -896,6 +972,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
SizeT declIndex; SizeT declIndex;
SizeT start; SizeT start;
SizeT length; SizeT length;
SizeT callOpen; // the '(' of the call this argument belongs to
}; };
Vector<StoreSite> storeSites; Vector<StoreSite> storeSites;
for (SizeT pos = glslCode.find("image"); pos != String::npos; pos = glslCode.find("image", pos + 1)) { for (SizeT pos = glslCode.find("image"); pos != String::npos; pos = glslCode.find("image", pos + 1)) {
@@ -945,7 +1022,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
break; break;
case ImageBuiltinAccess::Store: case ImageBuiltinAccess::Store:
decl.stored = true; decl.stored = true;
storeSites.push_back({declIndex, argStart, argEnd - argStart}); storeSites.push_back({declIndex, argStart, argEnd - argStart, openParen});
break; break;
case ImageBuiltinAccess::None: case ImageBuiltinAccess::None:
break; break;
@@ -971,9 +1048,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
decl.writeName = MakeImageWriteAliasName(decl.name, glslCode, takenAliases); decl.writeName = MakeImageWriteAliasName(decl.name, glslCode, takenAliases);
takenAliases.push_back(decl.writeName); takenAliases.push_back(decl.writeName);
decl.split = true; decl.split = true;
if (outSplitCount != nullptr) ++*outSplitCount;
// Both halves carry `coherent`; see BuildImageDeclaration. The
// single-declaration cases below stay as they were - nothing aliases them, so
// there is no visibility to restore and no reason to pay for the cache
// behaviour.
edits.push_back({decl.declStart, decl.declLength, edits.push_back({decl.declStart, decl.declLength,
BuildImageDeclaration(decl, "readonly", decl.name) + "\n" + BuildImageDeclaration(decl, "readonly", decl.name, /*forceCoherent=*/true) +
BuildImageDeclaration(decl, "writeonly", decl.writeName)}); "\n" +
BuildImageDeclaration(decl, "writeonly", decl.writeName,
/*forceCoherent=*/true)});
} else if (decl.stored) { } else if (decl.stored) {
edits.push_back({decl.declStart, decl.declLength, edits.push_back({decl.declStart, decl.declLength,
BuildImageDeclaration(decl, "writeonly", decl.name)}); BuildImageDeclaration(decl, "writeonly", decl.name)});
@@ -988,6 +1072,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
const ImageUniformDecl& decl = decls[site.declIndex]; const ImageUniformDecl& decl = decls[site.declIndex];
if (!decl.split) continue; if (!decl.split) continue;
edits.push_back({site.start, site.length, decl.writeName}); edits.push_back({site.start, site.length, decl.writeName});
// ...and an explicit barrier behind it. `coherent` on both halves is what makes
// the store VISIBLE to a load through the other variable, but it says nothing
// about ORDER within one invocation - and the whole reason a declaration is split
// is that the shader both stores and loads through it, which on the ES side is now
// a write to one variable followed by a read of another the compiler has no reason
// to believe alias. Adreno duly serves the load from before the store
// (KHR-GL4x.shader_image_load_store.advanced-memory-order's store/load/compare
// loop reads back the previous iteration's value). memoryBarrierImage() is the
// GLSL primitive for exactly that ordering, is core GLSL ES 3.10 in every stage,
// and is not an execution barrier, so it is legal in non-uniform control flow too.
//
// Confined to the split pair: a single-declaration repair has nothing aliasing it
// and must not pay for this, and a shader that never got split never sees it at
// all.
const SizeT statementEnd = FindEndOfCallStatement(glslCode, site.callOpen);
if (statementEnd != String::npos) {
edits.push_back({statementEnd, 0, " memoryBarrierImage();"});
}
} }
if (edits.empty()) { if (edits.empty()) {
return glslCode; return glslCode;
+38 -6
View File
@@ -154,6 +154,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
// extension - requesting an unadvertised extension is itself a compile error, so this is // extension - requesting an unadvertised extension is itself a compile error, so this is
// never emitted speculatively. A no-op when not needed or already present. // never emitted speculatively. A no-op when not needed or already present.
String RequestExtendedImageFormats(String glslCode, Bool needed); String RequestExtendedImageFormats(String glslCode, Bool needed);
// Adds `#extension GL_OES_viewport_array : require` when the emitted ESSL names
// gl_ViewportIndex. SPIRV-Cross prints that identifier and asks for nothing (unlike
// gl_Layer, which it backs with GL_NV_viewport_array2 on ES) and ESSL has no core
// spelling for it at any version, so the request has to be made here or the stage does
// not compile - which loses the whole program, not just the multi-viewport routing.
// `needed` is the caller's answer for the same reason as above: only it knows whether the
// driver advertises the extension, and requesting an unadvertised one is itself a compile
// error, so this is never emitted speculatively. A no-op when not needed or already
// present.
String RequestViewportArrayExtension(String glslCode, Bool needed);
// Writes a format layout qualifier into the image declarations named in // Writes a format layout qualifier into the image declarations named in
// `esslFormatByUniformName` that still have none. The completion half of the image-format // `esslFormatByUniformName` that still have none. The completion half of the image-format
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the // bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
@@ -186,11 +196,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
// * loaded only -> add `readonly` // * loaded only -> add `readonly`
// * stored only -> add `writeonly` // * stored only -> add `writeonly`
// * both -> emit TWO declarations on the same binding and of the // * both -> emit TWO declarations on the same binding and of the
// same type, `readonly <name>` and `writeonly // same type, `coherent readonly <name>` and `coherent
// <IMAGE_WRITE_ALIAS_PREFIX><name>`, and point every // writeonly <IMAGE_WRITE_ALIAS_PREFIX><name>`, point
// imageStore at the second one. Several image variables // every imageStore at the second one, and follow each of
// may share an image unit as long as they have the same // those stores with `memoryBarrierImage();`. Several image
// type and format, which is exactly what the pair is. // variables may share an image unit as long as they have
// the same type and format, which is exactly what the pair
// is.
//
// The `coherent` on both halves of the pair is load-bearing, not decoration: GLSL only
// guarantees a write through one image variable is visible to a read through a DIFFERENT
// one when both are coherent, and the split is what makes a same-variable
// read-after-write cross-variable. The single-declaration repairs above do not get it -
// nothing aliases them.
//
// The barrier is the other half of the same problem, and coherent alone did not cover it:
// visibility is not ORDER. Within one invocation the ES compiler sees a write to one
// variable and a read of another it has no reason to believe alias, and is free to serve
// the read from before the write - which is what advanced-memory-order's store/load/
// compare loop measured on Adreno. memoryBarrierImage() orders exactly those two, is core
// GLSL ES 3.10 in every stage, and is not an execution barrier, so it is legal in
// non-uniform control flow. It costs something in a shader that stores to a read+write
// image in a loop, which is why it is confined to the split pair.
// //
// Budget note: the split DOUBLES the image-uniform count of the stage it fires in, so // Budget note: the split DOUBLES the image-uniform count of the stage it fires in, so
// a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a // a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a
@@ -201,7 +228,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Runs on the transpiled ESSL, so it must see the bindings the frontend units were // Runs on the transpiled ESSL, so it must see the bindings the frontend units were
// already rewritten to and must run before those bindings are stripped - see the call // already rewritten to and must run before those bindings are stripped - see the call
// site in Managers.cpp. // site in Managers.cpp.
String SplitReadWriteImageUniforms(const String& glslCode); //
// `outSplitCount`, when given, receives the number of declarations that were actually
// doubled - i.e. exactly how many image uniforms this stage gained over what the
// application declared. Zero for every shader but a handful, and the only number the
// budget note above can be reported with.
String SplitReadWriteImageUniforms(const String& glslCode, Uint* outSplitCount = nullptr);
// Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into // Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name. // the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_"; constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
@@ -847,6 +847,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.MaxShaderStorageBufferBindings = m_dynamicParameters.MaxShaderStorageBufferBindings =
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings, clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
kMaxAdvertisedBufferBlocks); kMaxAdvertisedBufferBlocks);
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Vulkan has one descriptor limit for every
// stage (maxPerStageDescriptorStorageBuffers, which is what MaxComputeShaderStorageBlocks
// carries), so the stage limits differ only by whether the stage can have blocks at all.
//
// Deliberately NOT gated on vertexPipelineStoresAndAtomics, unlike the per-stage image
// uniforms below. That gate reads as the obvious one and is wrong here in practice: a
// Mali-G925-Immortalis reports vertexPipelineStoresAndAtomics=false (supported AND
// enabled) and yet runs all 433 KHR-GL43.constant_expressions.*_tess_* cases correctly
// through this backend - those write their result through a storage block declared in a
// tessellation stage. Gating would report 0 and turn 433 passing cases into
// "unsupported", removing function that demonstrably works.
//
// The asymmetry with DirectGLES is real and is the point. There, 0 prevents a program
// the driver refuses outright at link time; the honest limit converts a silent
// wrong-render into a capability an application can route around. Here there is no such
// failure to prevent, so the limit stays at what the device can address. If a Vulkan
// device is ever found that genuinely rejects such a pipeline, the gate belongs at
// pipeline creation where the rejection is observable, not on a feature bit this driver
// reports inaccurately.
{
const Int maxPerStageStorageBlocks =
std::min(std::max(m_dynamicParameters.MaxComputeShaderStorageBlocks, 0),
std::min(std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0),
std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)));
m_dynamicParameters.MaxVertexShaderStorageBlocks = maxPerStageStorageBlocks;
m_dynamicParameters.MaxTessControlShaderStorageBlocks = maxPerStageStorageBlocks;
m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks = maxPerStageStorageBlocks;
// The one hard capability in the set: no geometry stage means no blocks in it.
m_dynamicParameters.MaxGeometryShaderStorageBlocks =
m_vulkanCaps.SupportsGeometryShader ? maxPerStageStorageBlocks : 0;
m_dynamicParameters.MaxFragmentShaderStorageBlocks = maxPerStageStorageBlocks;
}
m_dynamicParameters.MaxTextureBufferSize = clampLimit( m_dynamicParameters.MaxTextureBufferSize = clampLimit(
"GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize); "GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize);
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment; m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
@@ -873,8 +905,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS); const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers); m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
m_dynamicParameters.MaxColorAttachments = std::min(m_vulkanCaps.MaxColorAttachments, maxSupportedDrawBuffers); m_dynamicParameters.MaxColorAttachments = std::min(m_vulkanCaps.MaxColorAttachments, maxSupportedDrawBuffers);
m_dynamicParameters.MaxClipDistances = m_vulkanCaps.MaxClipDistances; // Same shape as the image-uniform limits three lines above: maxClipDistances is reported
// by every device, but declaring ClipDistance in a module needs the shaderClipDistance
// FEATURE, which VulkanRenderer enables exactly where the physical device has it. Without
// it the limit describes a capacity no shader may use, so report none.
m_dynamicParameters.MaxClipDistances =
m_vulkanCaps.SupportsShaderClipDistance ? std::max(m_vulkanCaps.MaxClipDistances, 0) : 0;
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports; m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
// Assigned explicitly rather than left to the struct's defaults, like every other
// parameter here, so a second fill cannot inherit a stale value. GL_UNDEFINED_VERTEX is
// the truthful answer for DirectVulkan and a legal one (GL 4.6 table 23.65): which vertex
// provokes is chosen per pipeline by VulkanRenderer::SelectProvokingVertexMode out of
// VK_EXT_provoking_vertex, provokingVertexModePerPipeline and the topology, so there is no
// one convention to name. Vulkan's own default is FIRST, which is the opposite of the
// GL_LAST_VERTEX_CONVENTION this used to claim unconditionally.
m_dynamicParameters.LayerProvokingVertex = GL_UNDEFINED_VERTEX;
m_dynamicParameters.ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
m_dynamicParameters.MaxViewportWidth = m_vulkanCaps.MaxViewportWidth; m_dynamicParameters.MaxViewportWidth = m_vulkanCaps.MaxViewportWidth;
m_dynamicParameters.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight; m_dynamicParameters.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight;
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin; m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
@@ -632,15 +632,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexSubImage2D called with null GL context"); MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexSubImage2D called with null GL context");
pVulkanRenderer->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height); pVulkanRenderer->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
} }
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, void CopyImageSubData(const CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context"); MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context");
pVulkanRenderer->CopyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, pVulkanRenderer->CopyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ,
dstTexture, dstTarget, dstLevel, dstX, dstY, dstZ, dst, dstTarget, dstLevel, dstX, dstY, dstZ,
srcWidth, srcHeight, srcDepth); srcWidth, srcHeight, srcDepth);
} }
void GenerateMipmap(GLenum target) { void GenerateMipmap(GLenum target) {
@@ -82,9 +82,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLsizei height, GLint border); GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height); GLsizei height);
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, void CopyImageSubData(const CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target); void GenerateMipmap(GLenum target);
@@ -8,6 +8,7 @@
#include "VertexInputStateFactory.h" #include "VertexInputStateFactory.h"
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h" #include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
#include <MG_Backend/BackendObjects.h>
#include <utility> #include <utility>
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
@@ -107,8 +108,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue; continue;
} }
const VkFormat sourceVkFormat = VkFormat sourceVkFormat =
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong); ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
VertexStreamConversion conversion = VertexStreamConversion::None;
// Gated on the SAME flag ToVkVertexFormat gates its 64-bit path on, and that is
// load-bearing rather than belt-and-braces: the narrowing is only correct because
// DemoteFloat64Pass already turned the shader's `dvec` input into a `vec`, and that
// pass runs precisely when the backend declares no 64-bit vertex support. With the
// flag set, a dvec3/dvec4 is declined by ToVkVertexFormat AND left 64-bit in the
// module, so a float32 stream would be fed to a Float64 input.
const Bool narrowFloat64Arrays =
MG_Backend::pActiveBackendObject == nullptr ||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes;
if (sourceVkFormat == VK_FORMAT_UNDEFINED && attr.Type == DataType::Float64 && narrowFloat64Arrays) {
// No native 64-bit fetch here (see ToVkVertexFormat's Float64 case), but the
// source bytes are ordinary IEEE-754 doubles and DemoteFloat64Pass has already
// narrowed every dvec input to a vec, so the array is narrowed to match rather
// than dropped. Mirrors what DirectGLES does for the same state.
const VkFormat narrowedFormat = ToFloat32VertexFormat(attr.Size);
if (narrowedFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(narrowedFormat)) {
sourceVkFormat = narrowedFormat;
conversion = VertexStreamConversion::Float64ToFloat32;
MGLOG_W_ONCE("Vertex attribute location=%u is a 64-bit (GL_DOUBLE) array; fetching it at "
"float32 precision through format=%d (size=%d long=%s)",
location, static_cast<Int>(narrowedFormat), attr.Size, attr.IsLong ? "true" : "false");
}
}
if (sourceVkFormat == VK_FORMAT_UNDEFINED) { if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is " MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
"enabled but cannot be mapped to a VkFormat", "enabled but cannot be mapped to a VkFormat",
@@ -118,8 +143,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
VkFormat vkFormat = sourceVkFormat; VkFormat vkFormat = sourceVkFormat;
VertexStreamConversion conversion = VertexStreamConversion::None; if (conversion == VertexStreamConversion::None && !SupportsVertexBufferFormat(vkFormat)) {
if (!SupportsVertexBufferFormat(vkFormat)) {
if (IsScaledIntegerVertexFormat(vkFormat)) { if (IsScaledIntegerVertexFormat(vkFormat)) {
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size); const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) { if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
@@ -188,7 +212,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (sourceStride != 0) { if (sourceStride != 0) {
if (conversion == VertexStreamConversion::Repack) { if (conversion == VertexStreamConversion::Repack) {
stride = static_cast<Uint32>(attribByteSize); stride = static_cast<Uint32>(attribByteSize);
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) { } else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32 ||
conversion == VertexStreamConversion::Float64ToFloat32) {
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float))); stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
} }
} }
@@ -330,6 +355,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there // for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there
// while shaderFloat64 is not. Both halves key off nothing but the attribute being long, // while shaderFloat64 is not. Both halves key off nothing but the attribute being long,
// so they always agree without extra plumbing. // so they always agree without extra plumbing.
//
// ... as long as the shader half still runs. It does not when the backend has declared
// no 64-bit vertex attribute support: DemoteFloat64Pass has already narrowed every
// `dvec` input to a `vec` by then, so PackDoubleVertexInputsPass finds nothing to pack
// and a UINT-formatted attribute would be fed to a float input - garbage with no
// diagnostic anywhere. Declining here hands the attribute to the caller's
// Float64ToFloat32 fallback instead, which narrows the source doubles to match the
// demoted `vec` input - the same thing DirectGLES does for the same state. The
// frontend RECORDS the format either way, so this gate is the only thing standing
// between a legal glVertexAttribLFormat and a mismatched pipeline.
if (MG_Backend::pActiveBackendObject == nullptr ||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
return VK_FORMAT_UNDEFINED;
}
if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED; if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
switch (size) { switch (size) {
case 1: return VK_FORMAT_R32G32_UINT; case 1: return VK_FORMAT_R32G32_UINT;
@@ -23,6 +23,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
None = 0, None = 0,
Repack, Repack,
ScaledIntegerToFloat32, ScaledIntegerToFloat32,
// GL_DOUBLE source data narrowed to a tightly packed float32 stream: the fetch half
// of the fp64 demotion the shader side already does unconditionally.
Float64ToFloat32,
}; };
struct BackendVertexInputState { struct BackendVertexInputState {
@@ -1494,6 +1494,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture); const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u; const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u;
return resource.syncedContentVersion != texture.GetContentVersion() || return resource.syncedContentVersion != texture.GetContentVersion() ||
resource.syncedShapeVersion != texture.GetShapeVersion() ||
resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() || resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() ||
resource.syncedMipLevelCount != mipLevelCount; resource.syncedMipLevelCount != mipLevelCount;
} }
@@ -1593,11 +1594,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture, Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
TextureResource &outResource) { TextureResource &outResource) {
// Cross-draw fast path: if the resource is already built and neither the texture's // Cross-draw fast path: if the resource is already built and neither the texture's
// pixel content (bumped in MarkStorageDirty) nor its params changed since the last // pixel content (bumped in MarkStorageDirty), its SHAPE (bumped in BumpShapeVersion)
// sync, there is nothing to re-check or re-upload - skip CheckMipmapCompleteness, // nor its params changed since the last sync, there is nothing to re-check or
// SyncTextureResource, SyncTextureViews and the per-level dirty scan. Layout is // re-upload - skip CheckMipmapCompleteness, SyncTextureResource, SyncTextureViews and
// maintained separately by the transition path, so the resource still reflects truth. // the per-level dirty scan. Layout is maintained separately by the transition path, so
// the resource still reflects truth. The shape version is NOT redundant with the
// content one: glTexImage2D(..., nullptr) re-specifies a level's size or format
// without dirtying a texel, which is exactly how a re-specified image-unit texture used
// to keep reporting its old imageSize().
const Uint64 syncingContentVersion = texture.GetContentVersion(); const Uint64 syncingContentVersion = texture.GetContentVersion();
const Uint64 syncingShapeVersion = texture.GetShapeVersion();
const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture); const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
const Uint32 syncingMipLevelCount = const Uint32 syncingMipLevelCount =
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u; syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u;
@@ -1609,6 +1615,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end(); m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending && if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
outResource.syncedContentVersion == syncingContentVersion && outResource.syncedContentVersion == syncingContentVersion &&
outResource.syncedShapeVersion == syncingShapeVersion &&
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() && outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
outResource.syncedMipLevelCount == syncingMipLevelCount) { outResource.syncedMipLevelCount == syncingMipLevelCount) {
return true; return true;
@@ -1629,6 +1636,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false; return false;
} }
// From here down the size is VULKAN geometry, not GL's: a 1D array's layer count moves
// out of the height it occupies GL-side and into z, which is the slot
// TryResolveTextureShapeInfo reads arrayLayers from and the only one that leaves
// extent.height at the 1 a VK_IMAGE_TYPE_1D image is required to have.
texelSize = ToVulkanLevelExtent(texture.GetTarget(), texelSize);
if (!SyncTextureResource(texture, uploadTarget, texelSize, byteSize, mipLevelCount, outResource)) { if (!SyncTextureResource(texture, uploadTarget, texelSize, byteSize, mipLevelCount, outResource)) {
MGLOG_D("%s: SyncTextureResource failed", __func__); MGLOG_D("%s: SyncTextureResource failed", __func__);
return false; return false;
@@ -1660,6 +1673,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!hasDirtyMipLevel) { if (!hasDirtyMipLevel) {
outResource.syncedContentVersion = syncingContentVersion; outResource.syncedContentVersion = syncingContentVersion;
outResource.syncedMipLevelCount = syncingMipLevelCount; outResource.syncedMipLevelCount = syncingMipLevelCount;
outResource.syncedShapeVersion = syncingShapeVersion;
return true; return true;
} }
@@ -1669,6 +1683,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
outResource.syncedContentVersion = syncingContentVersion; outResource.syncedContentVersion = syncingContentVersion;
outResource.syncedMipLevelCount = syncingMipLevelCount; outResource.syncedMipLevelCount = syncingMipLevelCount;
outResource.syncedShapeVersion = syncingShapeVersion;
return true; return true;
} }
@@ -1848,6 +1863,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
} }
if (rounded == 0 && (supported & VK_SAMPLE_COUNT_1_BIT) != 0) {
// Nothing at two samples or above. Reachable because the frontend validates
// multisample allocations against the count MobileGL ADVERTISES (GL requires
// GL_MAX_SAMPLES >= 4) rather than against the device's per-format support, so
// a format this device cannot multisample at all now gets here instead of
// being refused up front. Keeping the unsupported count would hand
// vkCreateImage an invalid VkImageCreateInfo; one sample is at least a legal
// image, and the samples-08726 hazard above is the lesser of the two.
MGLOG_W_ONCE("Multisample texture format %d supports no count above one on this device; "
"backing it with a single sample",
static_cast<Int>(format));
rounded = static_cast<Uint32>(VK_SAMPLE_COUNT_1_BIT);
}
if (rounded != 0) { if (rounded != 0) {
resolvedSampleCount = static_cast<VkSampleCountFlagBits>(rounded); resolvedSampleCount = static_cast<VkSampleCountFlagBits>(rounded);
} }
@@ -2523,7 +2551,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
uploadItem.target = target; uploadItem.target = target;
uploadItem.level = level; uploadItem.level = level;
uploadItem.baseArrayLayer = ResolveUploadArrayLayer(target); uploadItem.baseArrayLayer = ResolveUploadArrayLayer(target);
uploadItem.texelSize = texelSize; // Vulkan geometry, like the image this stages into (see SyncTexture): a 1D
// array's layers move from y to z, where the copy loop's depthSelectsArrayLayer
// branch turns them into layerCount. The shadow needs no repacking to follow -
// one layer of a 1D array IS one row of `width` texels, so the tight-packed
// per-layer copy the swapped size describes reads the same bytes in the same
// order as the row-major level it replaces.
uploadItem.texelSize = ToVulkanLevelExtent(mipmapTexture.GetTarget(), texelSize);
uploadItem.source = source; uploadItem.source = source;
uploadItem.offset = stagingSize; uploadItem.offset = stagingSize;
uploadItem.uploadByteSize = byteSize; uploadItem.uploadByteSize = byteSize;
@@ -2561,6 +2595,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
uploadItem.uploadByteSize = rectTexels * uploadItem.texelBytes; uploadItem.uploadByteSize = rectTexels * uploadItem.texelBytes;
} }
// The boxes came out of the shadow in GL coordinates, where a 1D
// array's layer is the y. They have to follow texelSize across to z or
// they would address rows of an image that now has exactly one, and
// the staging walk would read the wrong bytes for them. Every byte
// count computed above is a product of the three extents, so moving
// the axes leaves all of them alone - and an OFFSET lands on a zero y,
// not on the extent's one, which is why this is spelled out rather than
// handed to ToVulkanLevelExtent.
if (mipmapTexture.GetTarget() == TextureTarget::Texture1DArray) {
uploadItem.regionLo = {uploadItem.regionLo.x(), 0, uploadItem.regionLo.y()};
uploadItem.regionSize = {uploadItem.regionSize.x(), 1,
uploadItem.regionSize.y()};
for (auto& rect : uploadItem.rects) {
rect.lo = {rect.lo.x(), 0, rect.lo.y()};
rect.hi = {rect.hi.x(), 1, rect.hi.y()};
}
}
} }
} }
if (formatInfo.expandRgbToRgba) { if (formatInfo.expandRgbToRgba) {
@@ -22,6 +22,25 @@ class ITextureObject;
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8; enum class SamplerNumericDomain : Uint8;
// A GL 1D-ARRAY level keeps its LAYER COUNT in the state-side HEIGHT: that is what
// glTexImage2D(GL_TEXTURE_1D_ARRAY, width, layers) means, and the frontend records the level
// as {width, layers, 1} (see GL_Texture.cpp's AllocateStorage and the completeness walk in
// TextureObject.cpp, which shrinks only x down the chain). Vulkan packs it the other way: a
// 1D array is a VK_IMAGE_TYPE_1D image whose extent.height MUST be 1 and whose layers live in
// arrayLayers - i.e. in the slot this backend reads out of z. So every place that turns a GL
// level size into Vulkan image geometry has to move the count across first, and every GL-space
// sub-box that rides along with it has to move its y the same way. DirectGLES performs the
// identical remap onto the ES 2D array it maps 1D arrays to (GetBackendUploadSize).
//
// Applied to nothing else: a 2D array, a cube array and a 3D texture all already carry their
// depth/layer count in z, which is where the Vulkan side expects it.
inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glTexelSize) {
if (stateTarget == TextureTarget::Texture1DArray) {
return {glTexelSize.x(), 1, glTexelSize.y()};
}
return glTexelSize;
}
class VkTextureManager { class VkTextureManager {
public: public:
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass // Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
@@ -206,6 +225,12 @@ public:
// as defense-in-depth: any path that grows the level set (which resizes the sampled view) // as defense-in-depth: any path that grows the level set (which resizes the sampled view)
// busts the skip even if it failed to bump the content version. // busts the skip even if it failed to bump the content version.
Uint32 syncedMipLevelCount = 0; Uint32 syncedMipLevelCount = 0;
// Snapshot of ITextureObject::GetShapeVersion() at the last successful sync. The content
// version alone does NOT cover a re-specification: glTexImage2D(..., nullptr) on an
// already-defined level changes its size or format and dirties no texel, so it moves the
// shape version and nothing else. Without this in the early-out key the image, its views
// and therefore imageSize() all keep answering with the texture's PREVIOUS shape.
Uint64 syncedShapeVersion = 0;
TextureResource() = default; TextureResource() = default;
TextureResource(const TextureResource&) = delete; TextureResource(const TextureResource&) = delete;
@@ -237,6 +262,7 @@ public:
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration); std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
std::swap(this->syncedContentVersion, that.syncedContentVersion); std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount); std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
} }
void Reset() { void Reset() {
@@ -300,6 +326,7 @@ public:
syncedTextureParamsVersion = 0; syncedTextureParamsVersion = 0;
syncedContentVersion = 0; syncedContentVersion = 0;
syncedMipLevelCount = 0; syncedMipLevelCount = 0;
syncedShapeVersion = 0;
} }
~TextureResource() { ~TextureResource() {
@@ -972,6 +972,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
// The fetch half of the fp64 demotion the shader side already does unconditionally
// (DemoteFloat64Pass): the source bytes are ordinary IEEE-754 doubles, so a GL_DOUBLE array is
// deinterleaved into a tightly packed float32 stream rather than dropped. `normalized` is not
// consulted - GL ignores it for floating-point array types.
static Bool ConvertFloat64VertexStreamToFloat32(
const MG_State::GLState::VertexAttribute& attribute,
const Uint8* sourceData,
SizeT sourceStride,
SizeT elementCount,
Vector<Float>& outData) {
if (sourceData == nullptr || attribute.Size < 1 || attribute.Size > 4 || sourceStride == 0) {
return false;
}
const SizeT componentCount = static_cast<SizeT>(attribute.Size);
outData.resize(elementCount * componentCount);
for (SizeT element = 0; element < elementCount; ++element) {
const Uint8* sourceElement = sourceData + element * sourceStride;
Float* destinationElement = outData.data() + element * componentCount;
for (SizeT component = 0; component < componentCount; ++component) {
// GL byte strides and offsets are arbitrary, so no component carries an 8-byte
// alignment guarantee; copy it out before narrowing it.
Double value = 0.0;
Memcpy(&value, sourceElement + component * sizeof(Double), sizeof(Double));
destinationElement[component] = static_cast<Float>(value);
}
}
return true;
}
static Bool RepackVertexStream(const Uint8* sourceData, static Bool RepackVertexStream(const Uint8* sourceData,
SizeT sourceStride, SizeT sourceStride,
SizeT elementSize, SizeT elementSize,
@@ -3595,6 +3625,14 @@ void main() {
uploadData = m_vertexConversionScratch.data(); uploadData = m_vertexConversionScratch.data();
uploadSize = static_cast<VkDeviceSize>(m_vertexConversionScratch.size() * sizeof(Float)); uploadSize = static_cast<VkDeviceSize>(m_vertexConversionScratch.size() * sizeof(Float));
break; break;
case VertexInputStateFactory::VertexStreamConversion::Float64ToFloat32:
if (!ConvertFloat64VertexStreamToFloat32(attribute, sourceData, sourceStride, elementCount,
m_vertexConversionScratch)) {
return false;
}
uploadData = m_vertexConversionScratch.data();
uploadSize = static_cast<VkDeviceSize>(m_vertexConversionScratch.size() * sizeof(Float));
break;
case VertexInputStateFactory::VertexStreamConversion::None: case VertexInputStateFactory::VertexStreamConversion::None:
return false; return false;
} }
@@ -8869,7 +8907,7 @@ void main() {
// A mixed 2D-array <-> 3D pair is legal because maintenance1 - core since Vulkan 1.1 - // A mixed 2D-array <-> 3D pair is legal because maintenance1 - core since Vulkan 1.1 -
// relaxed the old "layerCounts must match" rule into "the 3D side's extent.depth must // relaxed the old "layerCounts must match" rule into "the 3D side's extent.depth must
// equal the array side's layerCount". // equal the array side's layerCount".
struct CopyImageEndpoint { struct CopyImageSliceMapping {
// True for a VK_IMAGE_TYPE_3D image, i.e. slices ride the z axis, not the layer axis. // True for a VK_IMAGE_TYPE_3D image, i.e. slices ride the z axis, not the layer axis.
Bool slicesAreDepth = false; Bool slicesAreDepth = false;
// The GL z offset, kept in whichever field this endpoint's image type reads it from. // The GL z offset, kept in whichever field this endpoint's image type reads it from.
@@ -8883,13 +8921,35 @@ void main() {
Int32 OffsetZ() const { return slicesAreDepth ? static_cast<Int32>(baseSlice) : 0; } Int32 OffsetZ() const { return slicesAreDepth ? static_cast<Int32>(baseSlice) : 0; }
}; };
Bool TryResolveCopyImageEndpoint(TextureTarget target, // The Vulkan image one glCopyImageSubData endpoint names, after the two object kinds GL
const VkTextureManager::TextureResource& resource, Uint32 mipLevel, // 4.6 core 18.3.2 allows have been collapsed onto the fields this copy reads. A
GLint glZ, GLsizei glDepth, CopyImageEndpoint& outEndpoint) { // renderbuffer is a single-level, single-layer 2D image, so its shape answers are
// constants rather than a mip walk. `trackedLayout` points AT the owning resource's own
// layout field - both resource maps are node-based, so the pointer survives the further
// lookups the clear materialization below makes.
struct CopyImageVkImage {
Bool isRenderbuffer = false;
VkImage image = VK_NULL_HANDLE;
VkImageLayout* trackedLayout = nullptr;
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
Uint32 mipLevels = 1;
VkExtent2D extent = {0, 0};
Uint32 depth = 1;
Uint32 arrayLayers = 1;
};
Bool TryResolveCopyImageSliceMapping(TextureTarget target, const CopyImageVkImage& image, Uint32 mipLevel,
GLint glZ, GLsizei glDepth, CopyImageSliceMapping& outMapping) {
if (glZ < 0 || glDepth <= 0) { if (glZ < 0 || glDepth <= 0) {
return false; return false;
} }
const Uint32 baseSlice = static_cast<Uint32>(glZ); const Uint32 baseSlice = static_cast<Uint32>(glZ);
if (image.isRenderbuffer) {
// A renderbuffer holds one 2D image and nothing else; GL still requires the
// z/depth pair and it can only name that one slice.
outMapping = {};
return baseSlice == 0 && glDepth == 1;
}
switch (target) { switch (target) {
case TextureTarget::Texture1D: case TextureTarget::Texture1D:
case TextureTarget::Texture2D: case TextureTarget::Texture2D:
@@ -8897,13 +8957,14 @@ void main() {
case TextureTarget::Texture2DMultisample: case TextureTarget::Texture2DMultisample:
// Not layered at all: GL still requires the z/depth pair, and it can only name the // Not layered at all: GL still requires the z/depth pair, and it can only name the
// one slice these targets have. // one slice these targets have.
outEndpoint = {}; outMapping = {};
return baseSlice == 0 && glDepth == 1; return baseSlice == 0 && glDepth == 1;
case TextureTarget::Texture3D: case TextureTarget::Texture3D:
outEndpoint.slicesAreDepth = true; outMapping.slicesAreDepth = true;
outEndpoint.baseSlice = baseSlice; outMapping.baseSlice = baseSlice;
outEndpoint.availableSlices = std::max(1u, resource.depth >> mipLevel); outMapping.availableSlices = std::max(1u, image.depth >> mipLevel);
return true; return true;
case TextureTarget::Texture1DArray:
case TextureTarget::Texture2DArray: case TextureTarget::Texture2DArray:
case TextureTarget::Texture2DMultisampleArray: case TextureTarget::Texture2DMultisampleArray:
case TextureTarget::TextureCubeMap: case TextureTarget::TextureCubeMap:
@@ -8911,27 +8972,35 @@ void main() {
// A cube map is an array of six faces here (see TryResolveTextureShapeInfo), and GL // A cube map is an array of six faces here (see TryResolveTextureShapeInfo), and GL
// numbers its faces on the same z axis an array texture numbers its layers, so both // numbers its faces on the same z axis an array texture numbers its layers, so both
// arrive as a plain layer range. // arrive as a plain layer range.
outEndpoint.slicesAreDepth = false; //
outEndpoint.baseSlice = baseSlice; // GL_TEXTURE_1D_ARRAY belongs here too, and needs no remap: this backend STORES it
outEndpoint.availableSlices = resource.arrayLayers; // as a VK_IMAGE_TYPE_1D image whose layers live in arrayLayers (ToVulkanLevelExtent
// moves the count across), and GL 4.6 core 18.3.2 ADDRESSES it as a stack of slices
// on z with an image height of 1 - so the frontend's y/height are already the 0/1
// Vulkan requires and the layer lands in baseArrayLayer either way.
outMapping.slicesAreDepth = false;
outMapping.baseSlice = baseSlice;
outMapping.availableSlices = image.arrayLayers;
return true; return true;
default: default:
// GL_TEXTURE_1D_ARRAY carries its layers on the Y axis (srcY/srcHeight), which // GL_TEXTURE_BUFFER has no image at all. Declined rather than mis-addressed.
// would have to be remapped against a Vulkan extent that also has to stay height 1
// for a VK_IMAGE_TYPE_1D image; GL_TEXTURE_BUFFER has no image at all. Declined
// rather than mis-addressed.
return false; return false;
} }
} }
Uint CopyImageEndpointName(const CopyImageEndpoint& endpoint) {
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetExternalIndex();
return endpoint.Texture ? endpoint.Texture->GetExternalIndex() : 0u;
}
} // namespace } // namespace
void VulkanRenderer::CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, void VulkanRenderer::CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, const CopyImageEndpoint& dstEndpoint,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
MOBILEGL_ASSERT(srcTexture != nullptr && dstTexture != nullptr, MOBILEGL_ASSERT(srcEndpoint.Exists() && dstEndpoint.Exists(),
"CopyImageSubData requires valid source and destination textures."); "CopyImageSubData requires valid source and destination images.");
// The frontend already declines a zero or negative extent, so anything else here is a // The frontend already declines a zero or negative extent, so anything else here is a
// caller MobileGL wrote - but it still reaches vkCmdCopyImage in a release build, and a // caller MobileGL wrote - but it still reaches vkCmdCopyImage in a release build, and a
// zero extent.depth is as invalid as a zero width. // zero extent.depth is as invalid as a zero width.
@@ -8948,9 +9017,9 @@ void main() {
// and an overlap check). Refused outright, and refused for real rather than through an // and an overlap check). Refused outright, and refused for real rather than through an
// assertion the release build drops: recording the pair anyway is a validation error and, // assertion the release build drops: recording the pair anyway is a validation error and,
// on a tiler, a copy whose source has already been overwritten. // on a tiler, a copy whose source has already been overwritten.
if (srcTexture.get() == dstTexture.get()) { if (srcEndpoint.Texture == dstEndpoint.Texture && srcEndpoint.Renderbuffer == dstEndpoint.Renderbuffer) {
MGLOG_E_ONCE("%s: in-place copy on textureId=%d is not supported; declining the copy", __func__, MGLOG_E_ONCE("%s: in-place copy on objectId=%u is not supported; declining the copy", __func__,
srcTexture->GetExternalIndex()); CopyImageEndpointName(srcEndpoint));
return; return;
} }
@@ -8963,8 +9032,42 @@ void main() {
VkRenderPassManager::EndRenderPass(frame.commandBuffer); VkRenderPassManager::EndRenderPass(frame.commandBuffer);
} }
auto* srcResource = m_textureManager->SyncTextureAndGetDescriptor(*srcTexture); // One resolver for both object kinds. The texture arm is the same
auto* dstResource = m_textureManager->SyncTextureAndGetDescriptor(*dstTexture); // SyncTextureAndGetDescriptor the copy always used; the renderbuffer arm goes through the
// render-pass manager, which is where a renderbuffer's VkImage lives.
const auto resolveImage = [this](const CopyImageEndpoint& endpoint, CopyImageVkImage& out) {
if (endpoint.IsRenderbuffer()) {
auto* resource = m_renderPassManager->GetOrCreateRenderbufferResource(endpoint.Renderbuffer);
if (resource == nullptr) return false;
out.isRenderbuffer = true;
out.image = resource->image;
out.trackedLayout = &resource->layout;
out.aspect = resource->aspect;
out.mipLevels = 1;
out.extent = resource->extent;
out.depth = 1;
out.arrayLayers = 1;
return out.image != VK_NULL_HANDLE;
}
// An endpoint that named nothing is the frontend validator's INVALID_VALUE and never
// reaches here - but the assertion that says so is compiled out of a release build.
if (endpoint.Texture == nullptr) return false;
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*endpoint.Texture);
if (resource == nullptr) return false;
out.isRenderbuffer = false;
out.image = resource->image;
out.trackedLayout = &resource->layout;
out.aspect = resource->aspect;
out.mipLevels = resource->mipLevels;
out.extent = resource->extent;
out.depth = resource->depth;
out.arrayLayers = resource->arrayLayers;
return true;
};
CopyImageVkImage srcImage{};
CopyImageVkImage dstImage{};
const Bool srcResolved = resolveImage(srcEndpoint, srcImage);
const Bool dstResolved = resolveImage(dstEndpoint, dstImage);
// Real checks, not MOBILEGL_ASSERT: the assertions this replaces compile to nothing in // Real checks, not MOBILEGL_ASSERT: the assertions this replaces compile to nothing in
// a release build, which is where both observed failures happened - a null resource // a release build, which is where both observed failures happened - a null resource
// dereferenced right below (lavapipe) and a mip level the VkImage does not have handed // dereferenced right below (lavapipe) and a mip level the VkImage does not have handed
@@ -8980,29 +9083,29 @@ void main() {
// The frontend validator (ValidateTextureLevelExists) is what produces the // The frontend validator (ValidateTextureLevelExists) is what produces the
// GL_INVALID_VALUE the application is actually owed. This guard exists so the next gap // GL_INVALID_VALUE the application is actually owed. This guard exists so the next gap
// up there declines a copy instead of taking the process down. // up there declines a copy instead of taking the process down.
if (srcResource == nullptr || dstResource == nullptr) { if (!srcResolved || !dstResolved) {
MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__); MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
return; return;
} }
if (srcLevel < 0 || dstLevel < 0 || static_cast<Uint32>(srcLevel) >= srcResource->mipLevels || if (srcLevel < 0 || dstLevel < 0 || static_cast<Uint32>(srcLevel) >= srcImage.mipLevels ||
static_cast<Uint32>(dstLevel) >= dstResource->mipLevels) { static_cast<Uint32>(dstLevel) >= dstImage.mipLevels) {
MGLOG_E_ONCE("%s: mip level out of range (src %d of %u, dst %d of %u); declining the copy", __func__, MGLOG_E_ONCE("%s: mip level out of range (src %d of %u, dst %d of %u); declining the copy", __func__,
srcLevel, srcResource->mipLevels, dstLevel, dstResource->mipLevels); srcLevel, srcImage.mipLevels, dstLevel, dstImage.mipLevels);
return; return;
} }
const VkImageAspectFlags copyAspectMask = const VkImageAspectFlags copyAspectMask =
srcResource->aspect & dstResource->aspect & srcImage.aspect & dstImage.aspect &
(VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT); (VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
MOBILEGL_ASSERT(copyAspectMask != 0 && MOBILEGL_ASSERT(copyAspectMask != 0 &&
(srcResource->aspect & copyAspectMask) == srcResource->aspect && (srcImage.aspect & copyAspectMask) == srcImage.aspect &&
(dstResource->aspect & copyAspectMask) == dstResource->aspect, (dstImage.aspect & copyAspectMask) == dstImage.aspect,
"CopyImageSubData source and destination aspects are incompatible."); "CopyImageSubData source and destination aspects are incompatible.");
const Uint32 srcMipLevel = static_cast<Uint32>(srcLevel); const Uint32 srcMipLevel = static_cast<Uint32>(srcLevel);
const Uint32 dstMipLevel = static_cast<Uint32>(dstLevel); const Uint32 dstMipLevel = static_cast<Uint32>(dstLevel);
const Uint32 srcMipWidth = std::max(1u, srcResource->extent.width >> srcMipLevel); const Uint32 srcMipWidth = std::max(1u, srcImage.extent.width >> srcMipLevel);
const Uint32 srcMipHeight = std::max(1u, srcResource->extent.height >> srcMipLevel); const Uint32 srcMipHeight = std::max(1u, srcImage.extent.height >> srcMipLevel);
const Uint32 dstMipWidth = std::max(1u, dstResource->extent.width >> dstMipLevel); const Uint32 dstMipWidth = std::max(1u, dstImage.extent.width >> dstMipLevel);
const Uint32 dstMipHeight = std::max(1u, dstResource->extent.height >> dstMipLevel); const Uint32 dstMipHeight = std::max(1u, dstImage.extent.height >> dstMipLevel);
// Promoted for the same reason as the level range above, and it is the same bug class: // Promoted for the same reason as the level range above, and it is the same bug class:
// a VkImageCopy whose region runs past the image is an out-of-bounds promise to the // a VkImageCopy whose region runs past the image is an out-of-bounds promise to the
// driver, and the frontend does not check the region at all (there is a CTS sibling, // driver, and the frontend does not check the region at all (there is a CTS sibling,
@@ -9025,10 +9128,10 @@ void main() {
// here: every target whose slices this function can address on one of the two Vulkan axes. // here: every target whose slices this function can address on one of the two Vulkan axes.
// A refusal has to be a real decline, not an assertion - the assertion compiled to nothing // A refusal has to be a real decline, not an assertion - the assertion compiled to nothing
// in a release build and the unsupported shape reached vkCmdCopyImage anyway. // in a release build and the unsupported shape reached vkCmdCopyImage anyway.
CopyImageEndpoint srcEndpoint; CopyImageSliceMapping srcSlices;
CopyImageEndpoint dstEndpoint; CopyImageSliceMapping dstSlices;
if (!TryResolveCopyImageEndpoint(srcTextureTarget, *srcResource, srcMipLevel, srcZ, srcDepth, srcEndpoint) || if (!TryResolveCopyImageSliceMapping(srcTextureTarget, srcImage, srcMipLevel, srcZ, srcDepth, srcSlices) ||
!TryResolveCopyImageEndpoint(dstTextureTarget, *dstResource, dstMipLevel, dstZ, srcDepth, dstEndpoint)) { !TryResolveCopyImageSliceMapping(dstTextureTarget, dstImage, dstMipLevel, dstZ, srcDepth, dstSlices)) {
MGLOG_E_ONCE("%s: unsupported target pair src=%s dst=%s (srcZ=%d dstZ=%d depth=%d); declining the copy", MGLOG_E_ONCE("%s: unsupported target pair src=%s dst=%s (srcZ=%d dstZ=%d depth=%d); declining the copy",
__func__, MG_Util::ConvertTextureTargetToString(srcTextureTarget).c_str(), __func__, MG_Util::ConvertTextureTargetToString(srcTextureTarget).c_str(),
MG_Util::ConvertTextureTargetToString(dstTextureTarget).c_str(), srcZ, dstZ, srcDepth); MG_Util::ConvertTextureTargetToString(dstTextureTarget).c_str(), srcZ, dstZ, srcDepth);
@@ -9039,40 +9142,53 @@ void main() {
// shrinks) and a 3D texture by the selected level's depth (which every level halves), so // shrinks) and a 3D texture by the selected level's depth (which every level halves), so
// both come from the endpoint that resolved them. // both come from the endpoint that resolved them.
const Uint32 copySliceCount = static_cast<Uint32>(srcDepth); const Uint32 copySliceCount = static_cast<Uint32>(srcDepth);
if (srcEndpoint.baseSlice + copySliceCount > srcEndpoint.availableSlices || if (srcSlices.baseSlice + copySliceCount > srcSlices.availableSlices ||
dstEndpoint.baseSlice + copySliceCount > dstEndpoint.availableSlices) { dstSlices.baseSlice + copySliceCount > dstSlices.availableSlices) {
MGLOG_E_ONCE("%s: slice range outside image bounds (srcZ=%d of %u, dstZ=%d of %u, depth=%d); " MGLOG_E_ONCE("%s: slice range outside image bounds (srcZ=%d of %u, dstZ=%d of %u, depth=%d); "
"declining the copy", "declining the copy",
__func__, srcZ, srcEndpoint.availableSlices, dstZ, dstEndpoint.availableSlices, srcDepth); __func__, srcZ, srcSlices.availableSlices, dstZ, dstSlices.availableSlices, srcDepth);
return; return;
} }
const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *srcTexture); const auto materializeClear = [this, &frame](const CopyImageEndpoint& endpoint) {
MOBILEGL_ASSERT(clearReady, "%s: failed to materialize pending clear for source textureId=%d", if (endpoint.IsRenderbuffer()) {
__func__, srcTexture->GetExternalIndex()); return MaterializePendingClearForRenderbuffer(frame.commandBuffer, endpoint.Renderbuffer);
}
return MaterializePendingClearForTexture(frame.commandBuffer, *endpoint.Texture);
};
const Bool clearReady = materializeClear(srcEndpoint);
MOBILEGL_ASSERT(clearReady, "%s: failed to materialize pending clear for source objectId=%u",
__func__, CopyImageEndpointName(srcEndpoint));
// A clear still parked on the destination would otherwise materialize AFTER this copy and // A clear still parked on the destination would otherwise materialize AFTER this copy and
// wipe the texels it just wrote. // wipe the texels it just wrote.
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *dstTexture); const Bool dstClearReady = materializeClear(dstEndpoint);
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination textureId=%d", MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination objectId=%u",
__func__, dstTexture->GetExternalIndex()); __func__, CopyImageEndpointName(dstEndpoint));
const VkImageLayout srcOriginalLayout = srcResource->layout; const VkImageLayout srcOriginalLayout = *srcImage.trackedLayout;
const VkImageLayout dstOriginalLayout = dstResource->layout; const VkImageLayout dstOriginalLayout = *dstImage.trackedLayout;
// A layout of UNDEFINED means nothing has ever been written to the image, which on the // A layout of UNDEFINED means nothing has ever been written to the image, which on the
// SOURCE side is glTexStorage without an upload: legal GL, and the texels it copies are // SOURCE side is glTexStorage without an upload: legal GL, and the texels it copies are
// undefined by the same spec sentence that lets the application ask. Both sides therefore // undefined by the same spec sentence that lets the application ask. Both sides therefore
// take the same shape - transition the whole image out of UNDEFINED and settle it on a // take the same shape - transition the whole image out of UNDEFINED and settle it on a
// real layout afterwards, since UNDEFINED is not a layout a barrier may transition BACK to. // real layout afterwards, since UNDEFINED is not a layout a barrier may transition BACK to.
const auto resolveRestoreLayout = [copyAspectMask](VkImageLayout originalLayout) { // A renderbuffer settles on its ATTACHMENT layout instead: it is never sampled, and that is
// the layout MaterializePendingClearForRenderbuffer leaves it in.
const auto resolveRestoreLayout = [copyAspectMask](VkImageLayout originalLayout, Bool isRenderbuffer) {
if (originalLayout != VK_IMAGE_LAYOUT_UNDEFINED) { if (originalLayout != VK_IMAGE_LAYOUT_UNDEFINED) {
return originalLayout; return originalLayout;
} }
return (copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0 const Bool depthStencil =
? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL (copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0;
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; if (isRenderbuffer) {
return depthStencil ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
: VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
}
return depthStencil ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
}; };
const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout); const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout, srcImage.isRenderbuffer);
const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout); const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout, dstImage.isRenderbuffer);
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags srcAccessMask = 0; VkAccessFlags srcAccessMask = 0;
@@ -9083,15 +9199,15 @@ void main() {
// [baseSlice, baseSlice + depth) the slice mapping above hands the copy. // [baseSlice, baseSlice + depth) the slice mapping above hands the copy.
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool srcReady = VkTextureManager::TransitionImageLayout( Bool srcReady = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcResource->image, srcResource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, frame.commandBuffer, srcImage.image, *srcImage.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT,
srcResource->aspect, 0, srcResource->mipLevels); srcImage.aspect, 0, srcImage.mipLevels);
MOBILEGL_ASSERT(srcReady, "%s: failed to transition undefined source image", __func__); MOBILEGL_ASSERT(srcReady, "%s: failed to transition undefined source image", __func__);
srcCopyLayout = srcResource->layout; srcCopyLayout = *srcImage.trackedLayout;
} else { } else {
Bool srcReady = VkTextureManager::TransitionImageLayout( Bool srcReady = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcResource->image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, frame.commandBuffer, srcImage.image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1); srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1);
MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__); MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__);
@@ -9103,15 +9219,15 @@ void main() {
VkImageLayout dstCopyLayout = dstOriginalLayout; VkImageLayout dstCopyLayout = dstOriginalLayout;
if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool dstReady = VkTextureManager::TransitionImageLayout( Bool dstReady = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, dstResource->image, dstResource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, frame.commandBuffer, dstImage.image, *dstImage.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
dstResource->aspect, 0, dstResource->mipLevels); dstImage.aspect, 0, dstImage.mipLevels);
MOBILEGL_ASSERT(dstReady, "%s: failed to transition undefined destination image", __func__); MOBILEGL_ASSERT(dstReady, "%s: failed to transition undefined destination image", __func__);
dstCopyLayout = dstResource->layout; dstCopyLayout = *dstImage.trackedLayout;
} else { } else {
Bool dstReady = VkTextureManager::TransitionImageLayout( Bool dstReady = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, dstResource->image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, frame.commandBuffer, dstImage.image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1); dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1);
MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__); MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__);
@@ -9121,18 +9237,18 @@ void main() {
// on extent.depth as soon as either endpoint IS: a 3D image's subresource is always the // on extent.depth as soon as either endpoint IS: a 3D image's subresource is always the
// single layer (0, 1) and its slices are counted by the depth of the copy extent. With two // single layer (0, 1) and its slices are counted by the depth of the copy extent. With two
// non-3D endpoints both layer counts carry it and extent.depth stays 1. // non-3D endpoints both layer counts carry it and extent.depth stays 1.
const Bool copyCrossesDepthAxis = srcEndpoint.slicesAreDepth || dstEndpoint.slicesAreDepth; const Bool copyCrossesDepthAxis = srcSlices.slicesAreDepth || dstSlices.slicesAreDepth;
VkImageCopy copyRegion{}; VkImageCopy copyRegion{};
copyRegion.srcSubresource.aspectMask = copyAspectMask; copyRegion.srcSubresource.aspectMask = copyAspectMask;
copyRegion.srcSubresource.mipLevel = srcMipLevel; copyRegion.srcSubresource.mipLevel = srcMipLevel;
copyRegion.srcSubresource.baseArrayLayer = srcEndpoint.BaseArrayLayer(); copyRegion.srcSubresource.baseArrayLayer = srcSlices.BaseArrayLayer();
copyRegion.srcSubresource.layerCount = srcEndpoint.slicesAreDepth ? 1u : copySliceCount; copyRegion.srcSubresource.layerCount = srcSlices.slicesAreDepth ? 1u : copySliceCount;
copyRegion.srcOffset = {srcX, srcY, srcEndpoint.OffsetZ()}; copyRegion.srcOffset = {srcX, srcY, srcSlices.OffsetZ()};
copyRegion.dstSubresource.aspectMask = copyAspectMask; copyRegion.dstSubresource.aspectMask = copyAspectMask;
copyRegion.dstSubresource.mipLevel = dstMipLevel; copyRegion.dstSubresource.mipLevel = dstMipLevel;
copyRegion.dstSubresource.baseArrayLayer = dstEndpoint.BaseArrayLayer(); copyRegion.dstSubresource.baseArrayLayer = dstSlices.BaseArrayLayer();
copyRegion.dstSubresource.layerCount = dstEndpoint.slicesAreDepth ? 1u : copySliceCount; copyRegion.dstSubresource.layerCount = dstSlices.slicesAreDepth ? 1u : copySliceCount;
copyRegion.dstOffset = {dstX, dstY, dstEndpoint.OffsetZ()}; copyRegion.dstOffset = {dstX, dstY, dstSlices.OffsetZ()};
copyRegion.extent = {static_cast<Uint32>(srcWidth), static_cast<Uint32>(srcHeight), copyRegion.extent = {static_cast<Uint32>(srcWidth), static_cast<Uint32>(srcHeight),
copyCrossesDepthAxis ? copySliceCount : 1u}; copyCrossesDepthAxis ? copySliceCount : 1u};
MGLOG_D("CopyImageSubData: src(target=%s level=%u layer=%u+%u z=%d) -> dst(target=%s level=%u layer=%u+%u " MGLOG_D("CopyImageSubData: src(target=%s level=%u layer=%u+%u z=%d) -> dst(target=%s level=%u layer=%u+%u "
@@ -9143,8 +9259,8 @@ void main() {
copyRegion.dstSubresource.baseArrayLayer, copyRegion.dstSubresource.layerCount, copyRegion.dstSubresource.baseArrayLayer, copyRegion.dstSubresource.layerCount,
copyRegion.dstOffset.z, srcWidth, srcHeight, copyRegion.extent.depth); copyRegion.dstOffset.z, srcWidth, srcHeight, copyRegion.extent.depth);
vkCmdCopyImage(frame.commandBuffer, vkCmdCopyImage(frame.commandBuffer,
srcResource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcImage.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dstResource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstImage.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &copyRegion); 1, &copyRegion);
VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
@@ -9152,14 +9268,14 @@ void main() {
GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStageMask, srcRestoreAccessMask); GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStageMask, srcRestoreAccessMask);
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool srcRestored = VkTextureManager::TransitionImageLayout( Bool srcRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcResource->image, srcResource->layout, srcRestoreLayout, frame.commandBuffer, srcImage.image, *srcImage.trackedLayout, srcRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask,
srcResource->aspect, 0, srcResource->mipLevels); srcImage.aspect, 0, srcImage.mipLevels);
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore undefined source image layout", __func__); MOBILEGL_ASSERT(srcRestored, "%s: failed to restore undefined source image layout", __func__);
} else { } else {
Bool srcRestored = VkTextureManager::TransitionImageLayout( Bool srcRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcResource->image, srcCopyLayout, srcRestoreLayout, frame.commandBuffer, srcImage.image, srcCopyLayout, srcRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1); VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1);
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__); MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__);
@@ -9170,14 +9286,14 @@ void main() {
GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask); GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask);
if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool dstRestored = VkTextureManager::TransitionImageLayout( Bool dstRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, dstResource->image, dstResource->layout, dstRestoreLayout, frame.commandBuffer, dstImage.image, *dstImage.trackedLayout, dstRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask,
dstResource->aspect, 0, dstResource->mipLevels); dstImage.aspect, 0, dstImage.mipLevels);
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore undefined destination image layout", __func__); MOBILEGL_ASSERT(dstRestored, "%s: failed to restore undefined destination image layout", __func__);
} else { } else {
Bool dstRestored = VkTextureManager::TransitionImageLayout( Bool dstRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, dstResource->image, dstCopyLayout, dstRestoreLayout, frame.commandBuffer, dstImage.image, dstCopyLayout, dstRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1); VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1);
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__); MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
@@ -9739,7 +9855,7 @@ void main() {
VkImageAspectFlags imageAspect, Uint32 mipLevel, VkImageAspectFlags imageAspect, Uint32 mipLevel,
Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width, Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width,
GLsizei height, GLenum format, GLenum type, void* pixels, GLsizei height, GLenum format, GLenum type, void* pixels,
Bool defaultFramebufferOrientation) { Bool defaultFramebufferOrientation, Uint32 sourceLayerCount) {
const Bool wantDepth = format != GL_STENCIL_INDEX; const Bool wantDepth = format != GL_STENCIL_INDEX;
const Bool wantStencil = format != GL_DEPTH_COMPONENT; const Bool wantStencil = format != GL_DEPTH_COMPONENT;
auto& frame = m_frameContext.GetCurrent(); auto& frame = m_frameContext.GetCurrent();
@@ -9818,6 +9934,10 @@ void main() {
if (!mapped) return; if (!mapped) return;
} }
// See the header: a stack of one-row layers and a single multi-row layer copy out to the
// same tightly-packed bytes, so only the region's shape splits the two cases.
const Uint32 copyLayerCount = std::max<Uint32>(sourceLayerCount, 1u);
const Uint32 copyRowCount = copyLayerCount > 1u ? 1u : copyExtent.height;
VkBufferImageCopy regions[2]{}; VkBufferImageCopy regions[2]{};
Uint32 regionCount = 0; Uint32 regionCount = 0;
if (wantDepth) { if (wantDepth) {
@@ -9826,9 +9946,9 @@ void main() {
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
region.imageSubresource.mipLevel = mipLevel; region.imageSubresource.mipLevel = mipLevel;
region.imageSubresource.baseArrayLayer = baseArrayLayer; region.imageSubresource.baseArrayLayer = baseArrayLayer;
region.imageSubresource.layerCount = 1; region.imageSubresource.layerCount = copyLayerCount;
region.imageOffset = {copyOffset.x, copyOffset.y, 0}; region.imageOffset = {copyOffset.x, copyOffset.y, 0};
region.imageExtent = {copyExtent.width, copyExtent.height, 1}; region.imageExtent = {copyExtent.width, copyRowCount, 1};
} }
if (wantStencil) { if (wantStencil) {
auto& region = regions[regionCount++]; auto& region = regions[regionCount++];
@@ -9836,9 +9956,9 @@ void main() {
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
region.imageSubresource.mipLevel = mipLevel; region.imageSubresource.mipLevel = mipLevel;
region.imageSubresource.baseArrayLayer = baseArrayLayer; region.imageSubresource.baseArrayLayer = baseArrayLayer;
region.imageSubresource.layerCount = 1; region.imageSubresource.layerCount = copyLayerCount;
region.imageOffset = {copyOffset.x, copyOffset.y, 0}; region.imageOffset = {copyOffset.x, copyOffset.y, 0};
region.imageExtent = {copyExtent.width, copyExtent.height, 1}; region.imageExtent = {copyExtent.width, copyRowCount, 1};
} }
vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(), vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(),
regionCount, regions); regionCount, regions);
@@ -10073,9 +10193,17 @@ void main() {
? static_cast<Uint32>(textureUploadTarget) - ? static_cast<Uint32>(textureUploadTarget) -
static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX) static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX)
: 0; : 0;
// A 1D array's levelSize.y() is its LAYER count, and those layers are the rows
// GL wants back - but in Vulkan they are array layers of a one-row image, not
// rows of layer 0, so the read has to be told which of the two it is looking at.
const Uint32 sourceLayers =
textureObject->GetTarget() == TextureTarget::Texture1DArray
? static_cast<Uint32>(std::max<Int>(levelSize.y(), 1))
: 1u;
ReadDepthStencilImageToClient(resource->image, resource->format, &resource->layout, resource->aspect, ReadDepthStencilImageToClient(resource->image, resource->format, &resource->layout, resource->aspect,
static_cast<Uint32>(level), arrayLayer, 0, 0, levelSize.x(), static_cast<Uint32>(level), arrayLayer, 0, 0, levelSize.x(),
levelSize.y(), format, type, pixels); levelSize.y(), format, type, pixels,
/*defaultFramebufferOrientation=*/false, sourceLayers);
} else { } else {
MGLOG_E_ONCE("DirectVulkan::GetTexImage skipped: color query of a non-color texture"); MGLOG_E_ONCE("DirectVulkan::GetTexImage skipped: color query of a non-color texture");
} }
@@ -10093,12 +10221,19 @@ void main() {
// destination layout (GL 3.3 section 6.1.4). // destination layout (GL 3.3 section 6.1.4).
const auto imageTextureTarget = textureObject->GetTarget(); const auto imageTextureTarget = textureObject->GetTarget();
const Bool is3dImage = imageTextureTarget == TextureTarget::Texture3D; const Bool is3dImage = imageTextureTarget == TextureTarget::Texture3D;
const Bool isArrayImage = imageTextureTarget == TextureTarget::Texture1DArray || const Bool is1dArrayImage = imageTextureTarget == TextureTarget::Texture1DArray;
const Bool isArrayImage = is1dArrayImage ||
imageTextureTarget == TextureTarget::Texture2DArray || imageTextureTarget == TextureTarget::Texture2DArray ||
imageTextureTarget == TextureTarget::TextureCubeMapArray; imageTextureTarget == TextureTarget::TextureCubeMapArray;
const GLsizei depthSlices = is3dImage ? std::max<GLsizei>(texelSize.z(), 1) : 1; const GLsizei depthSlices = is3dImage ? std::max<GLsizei>(texelSize.z(), 1) : 1;
const GLsizei arrayLayers = isArrayImage ? static_cast<GLsizei>(resource->arrayLayers) : 1; const GLsizei arrayLayers = isArrayImage ? static_cast<GLsizei>(resource->arrayLayers) : 1;
const GLsizei sliceCount = std::max<GLsizei>(depthSlices * arrayLayers, 1); // A 1D array level comes back as ONE two-dimensional image whose rows are its layers
// (GL 4.6 core 8.11.4), so its layers are already counted by `height` above and must not
// multiply the slice count the way a 2D-array's or a cube-array's do. Vulkan still keeps
// them in arrayLayers on a one-row image, which is what the copy region below says - the
// two describe the same tightly-packed bytes.
const GLsizei sliceCount =
std::max<GLsizei>(depthSlices * (is1dArrayImage ? 1 : arrayLayers), 1);
if (bufSize >= 0) { if (bufSize >= 0) {
const Int dstChannels = GetReadbackChannelCount(format); const Int dstChannels = GetReadbackChannelCount(format);
if ((type == GL_UNSIGNED_BYTE || type == GL_FLOAT) && dstChannels > 0) { if ((type == GL_UNSIGNED_BYTE || type == GL_FLOAT) && dstChannels > 0) {
@@ -10151,7 +10286,8 @@ void main() {
copyRegion.imageSubresource.mipLevel = static_cast<Uint32>(level); copyRegion.imageSubresource.mipLevel = static_cast<Uint32>(level);
copyRegion.imageSubresource.baseArrayLayer = 0; copyRegion.imageSubresource.baseArrayLayer = 0;
copyRegion.imageSubresource.layerCount = static_cast<Uint32>(arrayLayers); copyRegion.imageSubresource.layerCount = static_cast<Uint32>(arrayLayers);
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), copyRegion.imageExtent = {static_cast<Uint32>(width),
is1dArrayImage ? 1u : static_cast<Uint32>(height),
static_cast<Uint32>(depthSlices)}; static_cast<Uint32>(depthSlices)};
vkCmdCopyImageToBuffer(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, vkCmdCopyImageToBuffer(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
readback.GetHandle(), 1, &copyRegion); readback.GetHandle(), 1, &copyRegion);
@@ -23,6 +23,7 @@
#include "VkTimerQueryManager.h" #include "VkTimerQueryManager.h"
#include "MG_Util/Math/VectorTypes.h" #include "MG_Util/Math/VectorTypes.h"
#include <Includes.h> #include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include <vk_mem_alloc.h> #include <vk_mem_alloc.h>
#include "../VkIncludes.h" #include "../VkIncludes.h"
@@ -197,9 +198,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLbitfield mask, GLenum filter); GLbitfield mask, GLenum filter);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
GLint x, GLint y, GLsizei width, GLsizei height); GLint x, GLint y, GLsizei width, GLsizei height);
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, void CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, const CopyImageEndpoint& dstEndpoint,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target); void GenerateMipmap(GLenum target);
@@ -216,10 +217,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// depth/stencil image, which this renderer stores display-side-up: the copy rect then // depth/stencil image, which this renderer stores display-side-up: the copy rect then
// has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on // has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on
// the way back, exactly as the colour ReadPixels path does. // the way back, exactly as the colour ReadPixels path does.
// `sourceLayerCount` above 1 says the `height` rows the client is owed are stored as that
// many ARRAY LAYERS of a one-row image rather than as rows of one layer - the shape a GL
// 1D array has in Vulkan. The two produce byte-identical tightly-packed readbacks, so
// only the copy region differs; everything after it is written against `height`.
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout, void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer, VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
void* pixels, Bool defaultFramebufferOrientation = false); void* pixels, Bool defaultFramebufferOrientation = false,
Uint32 sourceLayerCount = 1);
// Same-extent depth blit between images of different depth formats: host // Same-extent depth blit between images of different depth formats: host
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer). // round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat, Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
+3 -1
View File
@@ -43,4 +43,6 @@ set_tests_properties(SanityBench PROPERTIES LABELS benchmark)
add_subdirectory(Program) add_subdirectory(Program)
add_subdirectory(Buffer) add_subdirectory(Buffer)
add_subdirectory(Driver) add_subdirectory(Driver)
add_subdirectory(Container) add_subdirectory(Container)
add_subdirectory(ShaderCache)
add_subdirectory(Transpile)
@@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.24)
add_executable(
TranslationCacheBench
TranslationCacheBench.cpp
)
target_include_directories(TranslationCacheBench PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(
TranslationCacheBench PRIVATE
benchmark::benchmark
${LINK_LIBRARIES}
)
add_test(NAME TranslationCacheBench COMMAND TranslationCacheBench --benchmark_counters_tabular=true)
set_tests_properties(TranslationCacheBench PROPERTIES LABELS benchmark)
@@ -0,0 +1,457 @@
// MobileGL - MobileGL/MG_Benchmark/ShaderCache/TranslationCacheBench.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// What the two-level shader translation memo is worth, measured on the workload that
// motivated it: the KHR-GL33.texture_swizzle.smoke_* shape, where one case builds 2592
// programs out of a handful of distinct sources.
//
// Four pairs of cases, each Off/On:
//
// ProgramLink - the whole glCompileShader + glLinkProgram path for one program, with
// FRESH SHADER OBJECTS every iteration. This is the CTS shape exactly,
// and it is the headline case now. It used to be the PESSIMISTIC one:
// a hit still paid for both glslang parses, because the parse happens
// at glCompileShader - a different entry point from the one L1
// memoizes - and fresh shader objects meant ShaderCompileAdoptionMap
// could not hand the earlier parse over either. L1c is what closed
// that: the compile half of the memo recognises each stage's source
// and publishes its verdict without parsing, so on a hit this case now
// constructs no glslang object at all.
//
// SharedShaderLink - the same program population with the shader objects KEPT ALIVE, so
// the parses happen once outside the measured loop whatever the cache
// does. That makes it the CONTROL for L1c rather than a target: its
// numbers should not move, and if they do, L1c has added cost to a
// path it was supposed to leave alone.
//
// DeferredParseLink - the shape where L1c could LOSE: a constant vertex source (which
// hits L1c and therefore skips its parse) against a fresh fragment
// source every iteration (which makes the PROGRAM key miss, so the
// skipped parse has to happen inside the link after all). Same parse
// count either way, so the pair should land within noise; see its own
// header below.
//
// EsslTranspile - the DirectGLES backend segment: the SPIR-V pass chain plus
// SPIRV-Cross. Runs the driver-INDEPENDENT half of the real chain (the
// passes SyncToBackend runs unconditionally, plus the two stage-gated
// ones a fragment module reaches) so the miss path costs what
// production costs; the capability-gated passes need a live ES driver
// and are not reachable from a benchmark process.
//
// Every On case runs with a warm cache: the first iteration misses and every one after it
// hits, which is exactly the steady state of a 2592-program smoke case.
#include <benchmark/benchmark.h>
#include <string>
#include "Config.h"
#include "Includes.h"
#include "Init.h"
#include "MG_Impl/GLImpl/Program/GL_Program.h"
#include "MG_State/GLState/Core.h"
#include "MG_State/GLState/ProgramState/ProgramTranslationCache.h"
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
#include "MG_Util/ShaderTranspiler/TranslationCache.h"
#include "MG_Util/ShaderTranspiler/Types.h"
using namespace MobileGL;
using namespace MobileGL::MG_Util::ShaderTranspiler;
namespace {
const char* kVertexSource = R"(#version 460
layout(location = 0) in vec3 aPos;
out vec3 vPos;
out vec2 vUv;
void main() {
vPos = aPos;
vUv = aPos.xy * 0.5 + 0.5;
gl_Position = vec4(aPos, 1.0);
}
)";
// Shaped after gl3cTextureSwizzleTests.cpp's template: a sampler of one type, one
// TEXTURE_ACCESS, one CHANNEL, and an output whose BASIC_TYPE is the only thing that
// varies within a case. Padded with enough real arithmetic that the translation chain
// is doing work rather than measuring fixed overheads.
// `padLines` = 0 is the honest CTS size: gl3cTextureSwizzleTests' smoke template is a
// handful of lines, and that is the workload the memo exists for. The padded variant is
// kept alongside it because a shaderpack stage is orders of magnitude bigger, and the
// two bracket the ratio the cache is worth in practice.
String SwizzleLikeFragment(const String& prefix, const int padLines) {
String source = "#version 460\n";
source += "in vec3 vPos;\n";
source += "in vec2 vUv;\n";
source += "layout(location = 0) out " + prefix + "vec4 fragColor;\n";
source += "uniform sampler2D uTex;\n";
source += "uniform vec4 uTint;\n";
source += "uniform mat4 uModel;\n";
source += "uniform float uArr[8];\n";
source += "void main() {\n";
source += " vec4 s = texture(uTex, vUv);\n";
source += " float acc = s.r;\n";
for (int i = 0; i < padLines; ++i) {
source += " acc = acc * 1.0001 + sin(acc + " + std::to_string(i) + ".0) * cos(acc);\n";
}
source += " for (int i = 0; i < 8; ++i) acc += uArr[i];\n";
source += " vec4 p = uModel * vec4(vPos, 1.0);\n";
source += " fragColor = " + prefix + "vec4((s + uTint) * acc + p);\n";
source += "}\n";
return source;
}
class CacheModeScope {
public:
explicit CacheModeScope(const Bool enabled)
: m_saved(MG_Config::Features.ShaderTranslationCache) {
MG_Config::Features.ShaderTranslationCache =
enabled ? MG_Config::QuirkOverride::ForceOn : MG_Config::QuirkOverride::ForceOff;
}
~CacheModeScope() { MG_Config::Features.ShaderTranslationCache = m_saved; }
private:
const MG_Config::QuirkOverride m_saved;
};
class SyncCompileScope {
public:
SyncCompileScope() : m_saved(MG_Config::Features.AsyncShaderCompile) {
MG_Config::Features.AsyncShaderCompile = MG_Config::QuirkOverride::ForceOff;
}
~SyncCompileScope() { MG_Config::Features.AsyncShaderCompile = m_saved; }
private:
const MG_Config::QuirkOverride m_saved;
};
// One program, built the way the CTS builds one: fresh shader objects every time.
void LinkOneProgram(const String& vertexSource, const String& fragmentSource) {
using namespace MG_Impl::GLImpl;
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
const char* vsText = vertexSource.c_str();
ShaderSource(vs, 1, &vsText, nullptr);
CompileShader(vs);
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
const char* fsText = fragmentSource.c_str();
ShaderSource(fs, 1, &fsText, nullptr);
CompileShader(fs);
const GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, fs);
LinkProgram(program);
benchmark::DoNotOptimize(program);
DeleteProgram(program);
DeleteShader(vs);
DeleteShader(fs);
}
Vector<Uint32> BuildSanitizedFragmentSpirv(const String& fragmentSource) {
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fragmentSource};
auto shader = ShaderCompiler::CompileShader(attrib);
if (!shader) return {};
ProgramAttrib programAttrib{.shaders = {shader.value()}};
auto program = ShaderCompiler::LinkProgram(programAttrib);
if (!program) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *program.value()};
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
if (!binary || binary->empty()) return {};
Vector<Uint32> sanitized;
if (!ShaderCompiler::SanitizeAndOptimizeBinary(binary->front(), sanitized)) return {};
return sanitized;
}
// The driver-independent part of BackendProgramObjectImpl::TranspileSpirvToEssl, in the
// same order. What is missing is only the capability-gated passes (viewport lowering,
// multisample clamping, noperspective emulation, the image-format bake), which cannot
// fire without a live ES driver to arm them.
Bool TranspileLikeDirectGles(const Vector<Uint32>& spirv, const Uint esslVersion, String& outEssl) {
Vector<Uint32> a;
const Vector<Uint32>* effective = &spirv;
if (ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(*effective, a, false) && !a.empty()) {
effective = &a;
}
Vector<Uint32> b;
if (ShaderCompiler::LowerRectImages(*effective, b, false) && !b.empty()) effective = &b;
Vector<Uint32> c;
if (ShaderCompiler::Lower1DArrayImagesForEssl(*effective, c, false) && !c.empty()) effective = &c;
Vector<Uint32> d;
if (ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(*effective, d, false) && !d.empty()) {
effective = &d;
}
SpvcSession session(*effective, SessionUsageBit::Transpile);
spvc_compiler_options options;
if (session.CreateOptions(&options) != SPVC_SUCCESS) return false;
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, esslVersion);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
session.SetOptions(options);
const char* result = nullptr;
session.Compile(&result);
if (!result) return false;
outEssl = result;
return true;
}
EsslTranslationKeyInputs EsslInputsFor(const Vector<Uint32>& spirv) {
EsslTranslationKeyInputs inputs;
inputs.spirv = &spirv;
inputs.shaderType = GL_FRAGMENT_SHADER;
inputs.maxColorTextureSamples = 4;
inputs.maxIntegerSamples = 1;
inputs.maxDepthTextureSamples = 4;
inputs.advertisedMaxSamples = 4;
inputs.esslVersion = 320;
return inputs;
}
} // namespace
// ---------------------------------------------------------------------------------------
// L1, in situ: the full glCompileShader + glLinkProgram path for a repeated program.
// ---------------------------------------------------------------------------------------
// Arg(0) = the CTS smoke size; Arg(120) = a heavy stage, bracketing the ratio.
static void BM_ProgramLink_CacheOff(benchmark::State& state) {
MobileGL::Initialize();
const SyncCompileScope sync;
const CacheModeScope cache(false);
const String vs = kVertexSource;
const String fs = SwizzleLikeFragment("", static_cast<int>(state.range(0)));
for (auto _ : state) {
LinkOneProgram(vs, fs);
}
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
}
BENCHMARK(BM_ProgramLink_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
static void BM_ProgramLink_CacheOn(benchmark::State& state) {
MobileGL::Initialize();
const SyncCompileScope sync;
const CacheModeScope cache(true);
const String vs = kVertexSource;
const String fs = SwizzleLikeFragment("", static_cast<int>(state.range(0)));
LinkOneProgram(vs, fs); // prime, so the measured loop is the steady state
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
const TranslationCacheStats parseBefore = GetShaderParseVerdictCache().Stats();
for (auto _ : state) {
LinkOneProgram(vs, fs);
}
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
const TranslationCacheStats parseStats = GetShaderParseVerdictCache().Stats();
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
// Two stages per iteration, so a clean run shows L1c_hits == 2 * iterations and zero
// misses: every glCompileShader in the loop skipped its parse.
state.counters["L1c_hits"] = static_cast<double>(parseStats.hits - parseBefore.hits);
state.counters["L1c_misses"] = static_cast<double>(parseStats.misses - parseBefore.misses);
}
BENCHMARK(BM_ProgramLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
// ---------------------------------------------------------------------------------------
// L1, the shape the memo actually exists for: MANY PROGRAMS OUT OF THE SAME SHADERS.
//
// The pair above deletes its shader objects every iteration, which forces a fresh glslang
// parse per iteration no matter what the link does - glCompileShader parses, and that is a
// DIFFERENT entry point from the one L1 memoizes. It is a real workload (what an application
// that never reuses a shader object pays) but it is the pessimistic one, and the residual it
// leaves is the parse, not the link.
//
// This pair keeps the shader objects alive, so the parses happen once before the measured
// loop and the L1 hit then skips the link, mapIO, the SPIR-V, the reflection and the routing
// outright.
//
// SINCE L1c THIS IS THE CONTROL, NOT THE TARGET. Nothing inside the measured loop calls
// glCompileShader, so L1c cannot fire here at all - which is exactly what makes the pair
// useful: it is the shape that says whether the compile-side memo has slowed the LINK path
// down. Its numbers should be indistinguishable from the pre-L1c ones.
// ---------------------------------------------------------------------------------------
namespace {
struct SharedShaders {
GLuint vs = 0;
GLuint fs = 0;
};
SharedShaders MakeSharedShaders(const String& vertexSource, const String& fragmentSource) {
using namespace MG_Impl::GLImpl;
SharedShaders shaders;
shaders.vs = CreateShader(GL_VERTEX_SHADER);
const char* vsText = vertexSource.c_str();
ShaderSource(shaders.vs, 1, &vsText, nullptr);
CompileShader(shaders.vs);
shaders.fs = CreateShader(GL_FRAGMENT_SHADER);
const char* fsText = fragmentSource.c_str();
ShaderSource(shaders.fs, 1, &fsText, nullptr);
CompileShader(shaders.fs);
return shaders;
}
void LinkFromSharedShaders(const SharedShaders& shaders) {
using namespace MG_Impl::GLImpl;
const GLuint program = CreateProgram();
AttachShader(program, shaders.vs);
AttachShader(program, shaders.fs);
LinkProgram(program);
benchmark::DoNotOptimize(program);
DeleteProgram(program);
}
} // namespace
static void BM_SharedShaderLink_CacheOff(benchmark::State& state) {
MobileGL::Initialize();
const SyncCompileScope sync;
const CacheModeScope cache(false);
const SharedShaders shaders =
MakeSharedShaders(kVertexSource, SwizzleLikeFragment("", static_cast<int>(state.range(0))));
for (auto _ : state) {
LinkFromSharedShaders(shaders);
}
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
}
BENCHMARK(BM_SharedShaderLink_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
static void BM_SharedShaderLink_CacheOn(benchmark::State& state) {
MobileGL::Initialize();
const SyncCompileScope sync;
const CacheModeScope cache(true);
const SharedShaders shaders =
MakeSharedShaders(kVertexSource, SwizzleLikeFragment("", static_cast<int>(state.range(0))));
LinkFromSharedShaders(shaders); // prime, so the measured loop is the steady state
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
for (auto _ : state) {
LinkFromSharedShaders(shaders);
}
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
}
BENCHMARK(BM_SharedShaderLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
// ---------------------------------------------------------------------------------------
// L2, component: the DirectGLES SPIR-V pass chain plus SPIRV-Cross for one stage.
// ---------------------------------------------------------------------------------------
static void BM_EsslTranspile_CacheOff(benchmark::State& state) {
MobileGL::Initialize();
const Vector<Uint32> spirv =
BuildSanitizedFragmentSpirv(SwizzleLikeFragment("", static_cast<int>(state.range(0))));
if (spirv.empty()) {
state.SkipWithError("could not build the fragment module");
return;
}
String essl;
for (auto _ : state) {
if (!TranspileLikeDirectGles(spirv, 320, essl)) {
state.SkipWithError("transpile failed");
break;
}
benchmark::DoNotOptimize(essl.data());
}
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
}
BENCHMARK(BM_EsslTranspile_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
static void BM_EsslTranspile_CacheOn(benchmark::State& state) {
MobileGL::Initialize();
const Vector<Uint32> spirv =
BuildSanitizedFragmentSpirv(SwizzleLikeFragment("", static_cast<int>(state.range(0))));
if (spirv.empty()) {
state.SkipWithError("could not build the fragment module");
return;
}
BoundedTranslationCache<EsslTranslationResult> cache("bench L2", 64, 8u << 20);
const EsslTranslationKeyInputs inputs = EsslInputsFor(spirv);
for (auto _ : state) {
const TranslationCacheKey key = BuildEsslTranslationKey(inputs);
EsslTranslationResultPtr hit = cache.Find(key);
if (!hit) {
auto payload = MakeShared<EsslTranslationResult>();
if (!TranspileLikeDirectGles(spirv, inputs.esslVersion, payload->essl)) {
state.SkipWithError("transpile failed");
break;
}
cache.Insert(key, EsslTranslationResultPtr(payload), EsslTranslationResultBytes(*payload));
hit = payload;
}
benchmark::DoNotOptimize(hit->essl.data());
}
const TranslationCacheStats stats = cache.Stats();
state.counters["L2_hits"] = static_cast<double>(stats.hits);
state.counters["L2_misses"] = static_cast<double>(stats.misses);
}
BENCHMARK(BM_EsslTranspile_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
// ---------------------------------------------------------------------------------------
// L1c, the shape where it could LOSE rather than win: the DEFERRED PARSE.
// ---------------------------------------------------------------------------------------
// A stage whose compile hits L1c holds no AST, so if the program-level key then MISSES, the
// parse it skipped has to happen anyway - inside the link, via ClaimParsedShader. The parse
// is moved, not removed, and this pair is what says whether moving it costs anything.
//
// The shape forces exactly that, every iteration: one CONSTANT vertex source (hits L1c after
// the first iteration) linked against a FRESH fragment source each time (misses L1c, and
// makes the program key miss too). So:
//
// cache off - two parses at glCompileShader, then the link.
// cache on - one parse at glCompileShader (the fragment), one deferred parse inside the
// link (the vertex), then the link.
//
// The parse count is identical, so these two should land within noise of each other. If the
// On arm is materially SLOWER, L1c is charging for something - the per-compile key build and
// hash over the full preprocessed source, or the loss of the claim-CAS reuse - and that cost
// shows up here and nowhere else.
//
// The distinct fragment sources also churn both front-end levels through their FIFO caps,
// which is the eviction behaviour a real shaderpack load produces; over a long run the
// constant vertex entry is occasionally evicted by that churn and re-inserted, so the L1c
// hit rate reported below is high but not exactly 1.0 per iteration.
namespace {
String UniqueFragmentSource(const Uint64 serial, const int padLines) {
return SwizzleLikeFragment("", padLines) +
"\n// unique-" + std::to_string(serial) + "\n";
}
} // namespace
static void BM_DeferredParseLink_CacheOff(benchmark::State& state) {
MobileGL::Initialize();
const SyncCompileScope sync;
const CacheModeScope cache(false);
const String vs = kVertexSource;
Uint64 serial = 0;
for (auto _ : state) {
LinkOneProgram(vs, UniqueFragmentSource(serial++, static_cast<int>(state.range(0))));
}
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
}
BENCHMARK(BM_DeferredParseLink_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
static void BM_DeferredParseLink_CacheOn(benchmark::State& state) {
MobileGL::Initialize();
const SyncCompileScope sync;
const CacheModeScope cache(true);
const String vs = kVertexSource;
Uint64 serial = 0;
LinkOneProgram(vs, UniqueFragmentSource(~0ull, static_cast<int>(state.range(0)))); // prime the vertex entry
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
const TranslationCacheStats parseBefore = GetShaderParseVerdictCache().Stats();
for (auto _ : state) {
LinkOneProgram(vs, UniqueFragmentSource(serial++, static_cast<int>(state.range(0))));
}
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
const TranslationCacheStats parseStats = GetShaderParseVerdictCache().Stats();
// Expected shape: L1 all misses (every program is new), L1c one hit (vertex) and one miss
// (fragment) per iteration.
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
state.counters["L1c_hits"] = static_cast<double>(parseStats.hits - parseBefore.hits);
state.counters["L1c_misses"] = static_cast<double>(parseStats.misses - parseBefore.misses);
}
BENCHMARK(BM_DeferredParseLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
BENCHMARK_MAIN();
@@ -0,0 +1,20 @@
cmake_minimum_required(VERSION 3.24)
# Deliberately NOT a google-benchmark target: the interesting quantity is a per-stage
# breakdown of one program build, which needs its own clock around sub-steps that share
# set-up, and a plain main() keeps the output a table this can be read straight out of.
add_executable(
TranspileProfile
TranspileProfile.cpp
)
target_include_directories(TranspileProfile PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(
TranspileProfile PRIVATE
${LINK_LIBRARIES}
)
File diff suppressed because it is too large Load Diff
@@ -13,6 +13,7 @@
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h> #include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h> #include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
#include <MG_Util/Converters/MGToStr/BufferEnumConverter.h> #include <MG_Util/Converters/MGToStr/BufferEnumConverter.h>
#include <MG_Util/ShaderTranspiler/Types.h>
namespace MobileGL::MG_Impl::GLImpl::BufferImpl { namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
Bool ValidateBufferTarget(BufferTarget target) { Bool ValidateBufferTarget(BufferTarget target) {
@@ -67,6 +68,13 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
// binding points in GL 3.3 (no ARB_transform_feedback3). // binding points in GL 3.3 (no ARB_transform_feedback3).
pointCount = std::min<SizeT>(pointCount, 4); pointCount = std::min<SizeT>(pointCount, 4);
} }
if (target == BufferTarget::AtomicCounter) {
// GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, which is NOT the state layer's array
// size: a counter buffer reaches a shader only as a lowered storage block, so the
// reserved range is the ceiling, and glGetIntegerv advertises the same number.
pointCount = std::min<SizeT>(
pointCount, static_cast<SizeT>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS));
}
return pointCount; return pointCount;
} }
} // namespace } // namespace
+119 -11
View File
@@ -108,6 +108,12 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram(); const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
if (program != nullptr) { 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. // Capacity in captured vertices = the tightest bound buffer.
Uint64 capacityVertices = ~0ull; Uint64 capacityVertices = ~0ull;
for (SizeT i = 0; i < program->GetTransformFeedbackBufferCount(); ++i) { 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->AddTransformFeedbackPrimitives(primitives);
MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive); 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 // Every primitive mode a draw command accepts (GL 4.6 core table 10.1, plus
@@ -151,11 +162,23 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} }
// The `mode` INVALID_ENUM in isolation, so a draw entry point can raise it BEFORE any of the
// state-dependent INVALID_OPERATIONs below. GL 4.6 core 10.4 makes a bad mode INVALID_ENUM
// unconditionally, while "no current program" is not even a spec-listed draw error - it is
// MobileGL's own null-dereference guard - so it must never shadow the enum check
// (KHR-GL31.api.coverage calls glDrawArraysInstanced/glDrawElementsInstanced with mode
// GL_POINTS-1 against a bare context and pins GL_INVALID_ENUM).
static Bool ValidatePrimitiveModeEnum(const char* functionName, GLenum mode) {
if (IsAcceptedPrimitiveMode(mode)) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "mode is not an accepted primitive type."));
return false;
}
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) { static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
if (!IsAcceptedPrimitiveMode(mode)) { if (!ValidatePrimitiveModeEnum(functionName, mode)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "mode is not an accepted primitive type."));
return false; return false;
} }
@@ -176,11 +199,37 @@ namespace MobileGL::MG_Impl::GLImpl {
return false; return false;
} }
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
// GL 4.6 core 10.1: the tessellation pipeline's only input primitive is GL_PATCHES, and
// GL_PATCHES has no meaning without it. Both directions are INVALID_OPERATION, and
// neither was implemented - which is two of the four sites
// KHR-GL43.transform_feedback.api_errors_test checks with one shared message string.
// The EVALUATION stage is what decides: a control stage cannot run without one, and a
// program carrying only an evaluation stage still tessellates, through GL's
// fixed-function pass-through control stage (11.2.2).
const Bool tessellationActive =
currentProgram && currentProgram->GetShaderIndexByStage(ShaderStage::TessEval) >= 0;
if (tessellationActive && mode != GL_PATCHES) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"A program with a tessellation evaluation shader can only be drawn with GL_PATCHES."));
return false;
}
if (!tessellationActive && mode == GL_PATCHES) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"GL_PATCHES requires an active tessellation evaluation shader."));
return false;
}
// A geometry stage only accepts the primitive types that decompose into its declared // A geometry stage only accepts the primitive types that decompose into its declared
// input primitive (GL 4.6 core 11.3.1); anything else is INVALID_OPERATION. GL_PATCHES // input primitive (GL 4.6 core 11.3.1); anything else is INVALID_OPERATION. GL_PATCHES
// is the tessellation pipeline's input and reaches the geometry stage already // is the tessellation pipeline's input and reaches the geometry stage already
// converted, so it is not constrained here. // converted, so it is not constrained here.
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE; const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE;
if (gsInput != GL_NONE && mode != GL_PATCHES) { if (gsInput != GL_NONE && mode != GL_PATCHES) {
Bool compatible = false; Bool compatible = false;
@@ -216,13 +265,17 @@ namespace MobileGL::MG_Impl::GLImpl {
// While transform feedback is active the draw's primitive type must match // 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 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 constraint moves to the shader's output primitive type instead, so
// the draw mode itself is unconstrained here. A paused span is exempt: it // the draw mode itself is unconstrained here - and a TESSELLATION EVALUATION
// captures nothing, so there is nothing for the mode to be incompatible with // stage relocates it exactly the same way (GL 4.6 core 13.2.2 names both):
// (GL 4.6 core 13.2.3). // what is captured is the tessellator's output primitive, and the draw mode
// can only ever be GL_PATCHES. 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).
const auto& feedbackProgram = MG_State::pGLContext->GetTransformFeedbackProgram();
const Bool feedbackModeIsProgramDriven =
feedbackProgram && (feedbackProgram->GetShaderIndexByStage(ShaderStage::Geometry) >= 0 ||
feedbackProgram->GetShaderIndexByStage(ShaderStage::TessEval) >= 0);
if (MG_State::pGLContext->IsTransformFeedbackActive() && if (MG_State::pGLContext->IsTransformFeedbackActive() &&
!MG_State::pGLContext->IsTransformFeedbackPaused() && !MG_State::pGLContext->IsTransformFeedbackPaused() && !feedbackModeIsProgramDriven) {
!(MG_State::pGLContext->GetTransformFeedbackProgram() &&
MG_State::pGLContext->GetTransformFeedbackProgram()->GetShaderIndexByStage(ShaderStage::Geometry) >= 0)) {
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode(); const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
Bool compatible = false; Bool compatible = false;
switch (feedbackMode) { switch (feedbackMode) {
@@ -303,10 +356,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) { void Clear_Backend(GLbitfield mask) {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.Clear(mask); MG_Backend::gBackendFunctionsTable.GL.Clear(mask);
} }
@@ -314,6 +380,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices); MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices);
} }
@@ -322,6 +389,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount); MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
} }
@@ -330,6 +398,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount, MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
basevertex); basevertex);
} }
@@ -338,6 +407,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count); MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
} }
@@ -345,6 +415,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount); MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
} }
@@ -353,6 +424,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex); MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
} }
@@ -361,6 +433,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride); MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
} }
@@ -368,6 +441,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride); MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
} }
@@ -376,6 +450,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount, MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
maxdrawcount, stride); maxdrawcount, stride);
} }
@@ -385,6 +460,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount, MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
stride); stride);
} }
@@ -394,6 +470,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices, MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices,
basevertex); basevertex);
} }
@@ -403,6 +480,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices); MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices);
} }
@@ -412,6 +490,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance( MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
mode, count, type, indices, instancecount, basevertex, baseinstance); mode, count, type, indices, instancecount, basevertex, baseinstance);
} }
@@ -421,6 +500,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount,
basevertex); basevertex);
} }
@@ -430,6 +510,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices, MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
instancecount, baseinstance); instancecount, baseinstance);
} }
@@ -439,6 +520,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount); MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount);
} }
@@ -446,6 +528,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect); MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect);
} }
void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
@@ -453,6 +536,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount, MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
baseinstance); baseinstance);
} }
@@ -461,6 +545,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount); MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount);
} }
@@ -468,6 +553,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect); MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect);
} }
@@ -496,6 +582,9 @@ namespace MobileGL::MG_Impl::GLImpl {
return; 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); dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
} }
@@ -547,6 +636,7 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
if (!ValidateCurrentProgramForCompute(__func__)) return; if (!ValidateCurrentProgramForCompute(__func__)) return;
if (ConditionalRenderDiscardsCommand()) return;
dispatchComputeIndirect(indirect); dispatchComputeIndirect(indirect);
} }
@@ -596,12 +686,14 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) { void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride); MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride);
} }
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) { void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride); MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
@@ -715,12 +807,14 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex) { const void* indices, GLint basevertex) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawRangeElementsBaseVertex_Backend(mode, start, end, count, type, indices, basevertex); DrawRangeElementsBaseVertex_Backend(mode, start, end, count, type, indices, basevertex);
} }
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) { void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawRangeElements_Backend(mode, start, end, count, type, indices); DrawRangeElements_Backend(mode, start, end, count, type, indices);
@@ -728,6 +822,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex, GLuint baseinstance) { GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex, DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex,
@@ -736,6 +831,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex) { GLsizei instancecount, GLint basevertex) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstancedBaseVertex_Backend(mode, count, type, indices, instancecount, basevertex); DrawElementsInstancedBaseVertex_Backend(mode, count, type, indices, instancecount, basevertex);
@@ -743,18 +839,21 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLuint baseinstance) { GLsizei instancecount, GLuint baseinstance) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance); DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance);
} }
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) { void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstanced_Backend(mode, count, type, indices, instancecount); DrawElementsInstanced_Backend(mode, count, type, indices, instancecount);
} }
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) { void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) return; if (!ValidateDrawElementsIndexType(__func__, type)) return;
@@ -764,18 +863,21 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
GLuint baseinstance) { GLuint baseinstance) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance); DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance);
} }
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) { void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawArraysInstanced_Backend(mode, first, count, instancecount); DrawArraysInstanced_Backend(mode, first, count, instancecount);
} }
void DrawArraysIndirect(GLenum mode, const void* indirect) { void DrawArraysIndirect(GLenum mode, const void* indirect) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return; if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return;
@@ -783,6 +885,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) { void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count); AccountTransformFeedbackPrimitives(mode, count);
@@ -790,6 +893,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void DrawArrays(GLenum mode, GLint first, GLsizei count) { void DrawArrays(GLenum mode, GLint first, GLsizei count) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count); AccountTransformFeedbackPrimitives(mode, count);
@@ -797,6 +901,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) { void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (drawcount < 0) { if (drawcount < 0) {
@@ -810,6 +915,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices, void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
GLsizei drawcount) { GLsizei drawcount) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawElements_Backend(mode, count, type, indices, drawcount); MultiDrawElements_Backend(mode, count, type, indices, drawcount);
@@ -817,6 +923,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices, void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
GLsizei drawcount, const GLint* basevertex) { GLsizei drawcount, const GLint* basevertex) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawElementsBaseVertex_Backend(mode, count, type, indices, drawcount, basevertex); MultiDrawElementsBaseVertex_Backend(mode, count, type, indices, drawcount, basevertex);
@@ -827,6 +934,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) { void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count); AccountTransformFeedbackPrimitives(mode, count);
@@ -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, 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_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_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_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_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, 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, 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, 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) 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)
@@ -982,7 +982,7 @@ DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdoub
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance) DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance)
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, 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_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_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
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, 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_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_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data) DECLARE_GL_FUNCTION_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
@@ -13,6 +13,7 @@
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_Util/Metrics/TextureMetrics.h> #include <MG_Util/Metrics/TextureMetrics.h>
#include <MG_Impl/GLImpl/Texture/Validators.h> #include <MG_Impl/GLImpl/Texture/Validators.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_State/GLState/ErrorState/Error.h> #include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h> #include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h> #include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
@@ -617,16 +618,17 @@ namespace MobileGL::MG_Impl::GLImpl {
if (MG_Backend::pActiveBackendObject == nullptr) { if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::max(); return std::numeric_limits<Int>::max();
} }
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, 1); return GetAdvertisedMaxSamples();
} }
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own, lower // GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own
// one (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION. // (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
// The multisample TEXTURE path already resolves the limit per format // The multisample TEXTURE path resolves the limit per format the same way
// (GL_Texture.cpp, GetMaxTextureSamplesForFormat); renderbuffers only ever compared // (GL_Texture.cpp, GetMaxSupportedTextureSamples). Both are floored to the value MobileGL
// against GL_MAX_SAMPLES, so on a driver where the two differ - Adreno reports // advertises: on a driver where the two differ - Adreno reports GL_MAX_SAMPLES 4 and
// GL_MAX_SAMPLES 4 and GL_MAX_INTEGER_SAMPLES 1 - an integer renderbuffer accepted a // GL_MAX_INTEGER_SAMPLES 1 - rejecting the advertised count here only moves the failure
// sample count the format cannot deliver, and said GL_NO_ERROR about it. // from the driver into MobileGL, so the frontend accepts it and the backend clamps the
// count it actually hands the driver.
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) { Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
if (MG_Backend::pActiveBackendObject == nullptr) { if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::max(); return std::numeric_limits<Int>::max();
@@ -645,7 +647,10 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!isIntegerFormat) { if (!isIntegerFormat) {
return GetMaxRenderbufferSamples_State(); return GetMaxRenderbufferSamples_State();
} }
return std::max(dynamicParameters.MaxIntegerSamples, 1); // Per-format still, but never below the ceiling glGetIntegerv(GL_MAX_SAMPLES) promised:
// the driver's raw GL_MAX_INTEGER_SAMPLES stays the *backend* limit and the backend
// clamps to it, while the frontend honours what it advertised.
return std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
} }
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) { Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
@@ -2608,18 +2613,26 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void ClearBufferfi_Backend(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { 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); MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil);
} }
void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) { 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); MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value);
} }
void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* 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); MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value);
} }
void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* 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); MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value);
} }
@@ -3148,15 +3161,55 @@ namespace MobileGL::MG_Impl::GLImpl {
GetNamedFramebufferAttachmentParameteriv_State(framebuffer, attachment, pname, params); GetNamedFramebufferAttachmentParameteriv_State(framebuffer, attachment, pname, params);
} }
// The three argument errors GL 4.6 core 18.3.1 asks a blit for. They have to be raised here,
// in the backend-independent frontend: DirectGLES drains the driver's error queue around the
// blit on purpose (that is how the resolve fallback probes the driver), so an ES-side
// rejection never reaches the application and glGetError() answered GL_NO_ERROR for a call
// the spec requires to fail (KHR-GL30.api.coverage's glBlitFramebuffer sub-check). DirectVulkan
// already dropped the bad-filter and LINEAR-with-depth/stencil calls on the floor with a log
// line (VulkanRenderer::BlitFramebuffer), so the only thing that changes for it is that the
// error is now visible where the spec says it should be.
static Bool ValidateBlitMaskAndFilter(const char* functionName, GLbitfield mask, GLenum filter) {
constexpr GLbitfield kBlitMaskBits = GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
if ((mask & ~kBlitMaskBits) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"mask contains bits other than GL_COLOR_BUFFER_BIT, "
"GL_DEPTH_BUFFER_BIT and GL_STENCIL_BUFFER_BIT."));
return false;
}
if (filter != GL_NEAREST && filter != GL_LINEAR) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"filter must be GL_NEAREST or GL_LINEAR."));
return false;
}
// Depth and stencil have no meaningful interpolation, so GL_LINEAR is rejected outright
// rather than downgraded - even when the mask also carries the colour bit.
if (filter == GL_LINEAR && (mask & (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"GL_LINEAR filtering is not allowed when mask includes "
"GL_DEPTH_BUFFER_BIT or GL_STENCIL_BUFFER_BIT."));
return false;
}
return true;
}
void BlitNamedFramebuffer(GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1, void BlitNamedFramebuffer(GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1,
GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask,
GLenum filter) { GLenum filter) {
if (!ValidateBlitMaskAndFilter(__func__, mask, filter)) return;
BlitNamedFramebuffer_State(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, BlitNamedFramebuffer_State(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
dstY1, mask, filter); dstY1, mask, filter);
} }
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter) { GLint dstY1, GLbitfield mask, GLenum filter) {
if (!ValidateBlitMaskAndFilter(__func__, mask, filter)) return;
BlitFramebuffer_Backend(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); BlitFramebuffer_Backend(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
} }
+134 -34
View File
@@ -25,6 +25,7 @@
#include <MG_State/GLState/FramebufferState/FramebufferObject.h> #include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_Util/Texture/TextureFormatProcessor.h> #include <MG_Util/Texture/TextureFormatProcessor.h>
#include <MG_Util/Async/ShaderCompilePool.h> #include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
@@ -46,13 +47,29 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} }
constexpr GLint kFrontendMaxComputeUniformComponents = 1024; // Shared with the glslang resource table for the same reason as the atomic-counter
constexpr GLint kFrontendMaxComputeAtomicCounters = 8; // limits below: gl_MaxComputeUniformComponents expands from BuildTBuiltInResource.
constexpr GLint kFrontendMaxComputeAtomicCounterBuffers = 8; constexpr GLint kFrontendMaxComputeUniformComponents =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_COMPUTE_UNIFORM_COMPONENTS);
// Every atomic-counter limit is shared with the glslang resource table
// (BuildTBuiltInResource) through MG_Util/ShaderTranspiler/Types.h: GL 4.6 requires
// glGetIntegerv and the gl_MaxAtomicCounter* built-in constants to agree, and the two
// used to be independent tables that disagreed on both the binding count and the buffer
// size. Never move one of these without the other.
constexpr GLint kFrontendMaxComputeAtomicCounters =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
constexpr GLint kFrontendMaxComputeAtomicCounterBuffers =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
constexpr GLint kFrontendMaxComputeSharedMemorySize = 32768; constexpr GLint kFrontendMaxComputeSharedMemorySize = 32768;
constexpr GLint kFrontendMaxComputeWorkGroupInvocations = 1024; constexpr GLint kFrontendMaxComputeWorkGroupInvocations = 1024;
constexpr GLint kFrontendMaxCombinedAtomicCounters = 8; constexpr GLint kFrontendMaxCombinedAtomicCounters =
constexpr GLint kFrontendMaxFragmentAtomicCounters = 8; static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
constexpr GLint kFrontendMaxCombinedAtomicCounterBuffers =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
constexpr GLint kFrontendMaxFragmentAtomicCounters =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
constexpr GLint kFrontendMaxFragmentAtomicCounterBuffers =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
constexpr GLint kFrontendMaxGeometryAtomicCounters = 0; constexpr GLint kFrontendMaxGeometryAtomicCounters = 0;
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0; constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0; constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
@@ -66,10 +83,11 @@ namespace MobileGL::MG_Impl::GLImpl {
constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0; constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0;
constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0; constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0;
constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0; constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0;
// One atomic counter is a uint, and a buffer never has to hold more counters than the // GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE: the byte offset ceiling a counter may be declared
// combined limit the frontend advertises. GL 4.6 table 23.63 floors this at 32 bytes. // at. The matching binding count is applied in GetIndexedBufferQueryPointCount, so that
// the getter, the indexed queries and glBindBufferBase all share one ceiling.
constexpr GLint kFrontendMaxAtomicCounterBufferSize = constexpr GLint kFrontendMaxAtomicCounterBufferSize =
kFrontendMaxCombinedAtomicCounters * static_cast<GLint>(sizeof(GLuint)); static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE);
// KHR_debug minima (GL 4.6 table 23.66); the debug entry points are stubs, but the // KHR_debug minima (GL 4.6 table 23.66); the debug entry points are stubs, but the
// limits they advertise still have to be legal. // limits they advertise still have to be legal.
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64; constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
@@ -103,12 +121,16 @@ namespace MobileGL::MG_Impl::GLImpl {
constexpr GLint kFrontendSubpixelBits = 4; constexpr GLint kFrontendSubpixelBits = 4;
constexpr GLint kFrontendMaxSamples = 4; constexpr GLint kFrontendMaxSamples = 4;
// The floors under GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE. Shared with the compile
// pipeline (CaptureCompileEnv floors the same driver answers at them, and
// BuildTBuiltInResource expands gl_MaxComputeWorkGroup* from the result), because a
// shader is allowed to compare the built-in constant against this query.
constexpr GLint GetMinComputeWorkGroupCount(GLuint index) { constexpr GLint GetMinComputeWorkGroupCount(GLuint index) {
return index < 3 ? 65535 : 0; return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_COUNT[index]) : 0;
} }
constexpr GLint GetMinComputeWorkGroupSize(GLuint index) { constexpr GLint GetMinComputeWorkGroupSize(GLuint index) {
return index < 2 ? 1024 : (index == 2 ? 64 : 0); return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_SIZE[index]) : 0;
} }
GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks, GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks,
@@ -186,6 +208,16 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings; MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
return std::min(frontendCount, static_cast<SizeT>(std::max(backendCount, 0))); return std::min(frontendCount, static_cast<SizeT>(std::max(backendCount, 0)));
} }
if (bufferTarget == BufferTarget::AtomicCounter) {
// The counter family's binding count is NOT the state layer's array size: a
// counter buffer only reaches a shader as a lowered storage block, so what an
// implementation can serve is the reserved range, and that number is also what
// glslang compiles a layout(binding = N) atomic_uint against. Clamped here so
// GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, the indexed getters' index check and
// glBindBufferBase's all report the same ceiling.
return std::min(frontendCount,
static_cast<SizeT>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS));
}
return frontendCount; return frontendCount;
} }
@@ -213,6 +245,23 @@ namespace MobileGL::MG_Impl::GLImpl {
return ClampBlockCountToBindingPoints(blockCount, BufferTarget::ShaderStorage); return ClampBlockCountToBindingPoints(blockCount, BufferTarget::ShaderStorage);
} }
// The per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS answers. Backend-derived, and NOT a
// constant to be "restored" - these used to return a flat 16 for vertex, geometry and
// both tessellation stages, which is wrong on any host that does not serve storage
// blocks in those stages. Zero is a legal answer: GL 4.6 table 23.64 and ES 3.2 table
// 21.44 both set the minimum at 0 for every graphics stage except fragment, which is
// why the conformance suite gates each such test on the query instead of assuming it.
// ARM's GLES driver reports 0 for all four (a Mali-G925 does), and advertising 16 there
// bought nothing: the program still failed to link inside the backend, the frontend
// still reported LINK_STATUS as true, and every draw with it silently rendered nothing.
GLint StageStorageBlockCount(Int MG_Backend::DynamicBackendParameters::*stageLimit) {
static const MG_Backend::DynamicBackendParameters kBackendlessDefaults{};
const MG_Backend::DynamicBackendParameters& parameters =
MG_Backend::pActiveBackendObject ? MG_Backend::pActiveBackendObject->GetDynamicParameters()
: kBackendlessDefaults;
return ClampStorageBlockCount(static_cast<GLint>(parameters.*stageLimit));
}
bool TryDecodeDrawBufferQuery(GLenum pname, SizeT& drawBufferIndex) { bool TryDecodeDrawBufferQuery(GLenum pname, SizeT& drawBufferIndex) {
if (pname == GL_DRAW_BUFFER) { if (pname == GL_DRAW_BUFFER) {
drawBufferIndex = 0; drawBufferIndex = 0;
@@ -422,6 +471,18 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} // namespace } // namespace
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so the driver's value is floored
// before it is advertised. Every other multisample ceiling MobileGL advertises has to be
// floored the same way: promising 4 samples globally while answering GL_MAX_INTEGER_SAMPLES
// 1 - which is exactly what Adreno reports - makes the frontend reject the very count it
// just told the application to use. The backends clamp the realised count instead.
GLint GetAdvertisedMaxSamples() {
if (MG_Backend::pActiveBackendObject == nullptr) {
return kFrontendMaxSamples;
}
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, kFrontendMaxSamples);
}
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */ /* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
const GLubyte* GetString(GLenum name) { const GLubyte* GetString(GLenum name) {
static String vendorString; static String vendorString;
@@ -1511,15 +1572,15 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_LINE_WIDTH: case GL_LINE_WIDTH:
*params = static_cast<GLint>(MG_State::pGLContext->GetLineWidth()); *params = static_cast<GLint>(MG_State::pGLContext->GetLineWidth());
return; return;
case GL_LAYER_PROVOKING_VERTEX:
*params = GL_LAST_VERTEX_CONVENTION;
return;
case GL_LOGIC_OP_MODE: case GL_LOGIC_OP_MODE:
*params = static_cast<GLint>(MG_Util::ConvertLogicOperationToGLEnum(MG_State::pGLContext->GetLogicOp())); *params = static_cast<GLint>(MG_Util::ConvertLogicOperationToGLEnum(MG_State::pGLContext->GetLogicOp()));
return; return;
case GL_MAX_COMBINED_ATOMIC_COUNTERS: case GL_MAX_COMBINED_ATOMIC_COUNTERS:
*params = kFrontendMaxCombinedAtomicCounters; *params = kFrontendMaxCombinedAtomicCounters;
return; return;
case GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxCombinedAtomicCounterBuffers;
return;
case GL_MAX_COMBINED_UNIFORM_BLOCKS: case GL_MAX_COMBINED_UNIFORM_BLOCKS:
*params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks); *params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks);
return; return;
@@ -1535,8 +1596,11 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_FRAGMENT_ATOMIC_COUNTERS: case GL_MAX_FRAGMENT_ATOMIC_COUNTERS:
*params = kFrontendMaxFragmentAtomicCounters; *params = kFrontendMaxFragmentAtomicCounters;
return; return;
case GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxFragmentAtomicCounterBuffers;
return;
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS: case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
*params = ClampStorageBlockCount(16); // TODO *params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxFragmentShaderStorageBlocks);
return; return;
case GL_MAX_FRAGMENT_INPUT_COMPONENTS: case GL_MAX_FRAGMENT_INPUT_COMPONENTS:
*params = kFrontendMaxFragmentInputComponents; *params = kFrontendMaxFragmentInputComponents;
@@ -1562,7 +1626,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxGeometryAtomicCounterBuffers; *params = kFrontendMaxGeometryAtomicCounterBuffers;
return; return;
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS: case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
*params = ClampStorageBlockCount(16); // TODO *params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxGeometryShaderStorageBlocks);
return; return;
case GL_MAX_GEOMETRY_INPUT_COMPONENTS: case GL_MAX_GEOMETRY_INPUT_COMPONENTS:
*params = kFrontendMaxGeometryInputComponents; *params = kFrontendMaxGeometryInputComponents;
@@ -1597,7 +1661,11 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample) ? GL_TRUE : GL_FALSE; *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample) ? GL_TRUE : GL_FALSE;
return; return;
case GL_MIN_MAP_BUFFER_ALIGNMENT: case GL_MIN_MAP_BUFFER_ALIGNMENT:
*params = 64; // TODO // The same constant the map paths align to (MG_State/GLState/BufferState/
// PipeResource.h), never a literal: this number is a PROMISE about the pointers
// glMapBuffer and glMapBufferRange return, and the two used to be unrelated - the
// query said 64 while the pointers came out of a std::vector aligned to 16.
*params = static_cast<GLint>(MG_State::GLState::MIN_MAP_BUFFER_ALIGNMENT);
return; return;
case GL_MAX_LABEL_LENGTH: case GL_MAX_LABEL_LENGTH:
*params = 256; // TODO *params = 256; // TODO
@@ -1633,16 +1701,18 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = 0; *params = 0;
return; return;
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS: case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
*params = ClampStorageBlockCount(16); // TODO *params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessControlShaderStorageBlocks);
return; return;
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS: case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
*params = ClampStorageBlockCount(16); // TODO *params =
StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessEvaluationShaderStorageBlocks);
return; return;
case GL_MAX_TEXTURE_LOD_BIAS: case GL_MAX_TEXTURE_LOD_BIAS:
*params = 15; // TODO *params = 15; // TODO
return; return;
case GL_MAX_UNIFORM_LOCATIONS: case GL_MAX_UNIFORM_LOCATIONS:
*params = 1024 * 4; // TODO // The same constant the link's location allocator enforces - see ProgramObject.
*params = MG_State::GLState::ProgramObject::MAX_UNIFORM_LOCATIONS;
return; return;
case GL_MAX_VARYING_COMPONENTS: case GL_MAX_VARYING_COMPONENTS:
*params = kFrontendMaxVaryingComponents; *params = kFrontendMaxVaryingComponents;
@@ -1662,7 +1732,7 @@ namespace MobileGL::MG_Impl::GLImpl {
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms; : MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
return; return;
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
*params = ClampStorageBlockCount(16); // TODO *params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxVertexShaderStorageBlocks);
return; return;
case GL_MAX_VERTEX_UNIFORM_COMPONENTS: case GL_MAX_VERTEX_UNIFORM_COMPONENTS:
*params = kFrontendMaxVertexUniformComponents; *params = kFrontendMaxVertexUniformComponents;
@@ -1972,6 +2042,24 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_UNIFORM_BUFFER_START: case GL_UNIFORM_BUFFER_START:
RecordIndexedOnlyGetterError(__func__, pname); RecordIndexedOnlyGetterError(__func__, pname);
return; return;
// glBindBufferBase/Range set the GENERIC binding point too (GL 4.6 core 6.1.1), and this
// is the one indexed-buffer family whose non-indexed query was never answered - so it
// fell through to INVALID_ENUM and left the caller's variable holding whatever was in its
// stack slot. _START/_SIZE stay indexed-only, exactly like their uniform-buffer siblings.
case GL_ATOMIC_COUNTER_BUFFER_BINDING:
if (const auto& obj =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::AtomicCounter).GetBoundObject()) {
*params = static_cast<GLint>(obj->GetExternalIndex());
} else {
*params = 0;
}
return;
case GL_ATOMIC_COUNTER_BUFFER_START:
RecordIndexedOnlyGetterError(__func__, pname);
return;
case GL_ATOMIC_COUNTER_BUFFER_SIZE:
RecordIndexedOnlyGetterError(__func__, pname);
return;
case GL_UNPACK_ALIGNMENT: case GL_UNPACK_ALIGNMENT:
*params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackAlignment); *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackAlignment);
return; return;
@@ -2026,9 +2114,6 @@ namespace MobileGL::MG_Impl::GLImpl {
params[3] = vp.w(); params[3] = vp.w();
return; return;
} }
case GL_VIEWPORT_INDEX_PROVOKING_VERTEX:
*params = GL_LAST_VERTEX_CONVENTION;
return;
case GL_MAX_ELEMENT_INDEX: case GL_MAX_ELEMENT_INDEX:
*params = 1024 * 1024; // TODO *params = 1024 * 1024; // TODO
return; return;
@@ -2116,8 +2201,22 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_CLIP_DISTANCES: case GL_MAX_CLIP_DISTANCES:
*params = dynamicParameters.MaxClipDistances; *params = dynamicParameters.MaxClipDistances;
break; break;
// Both were a hard-coded GL_LAST_VERTEX_CONVENTION, derived from nothing. GL 4.6 table
// 23.65 permits GL_UNDEFINED_VERTEX for either, and that is what the backends report
// wherever they do not actually pin a convention - claiming one is a statement about
// which vertex of a primitive supplies gl_Layer / gl_ViewportIndex, and DirectGLES
// rasterizes only viewport 0 on a driver without GL_OES_viewport_array while
// DirectVulkan picks its provoking mode per pipeline. KHR-GLxx.viewport_array.query
// accepts all four values, and .provoking_vertex - which failed on both devices, in
// OPPOSITE directions - stops verifying as soon as either answer is undefined.
case GL_LAYER_PROVOKING_VERTEX:
*params = static_cast<GLint>(dynamicParameters.LayerProvokingVertex);
break;
case GL_VIEWPORT_INDEX_PROVOKING_VERTEX:
*params = static_cast<GLint>(dynamicParameters.ViewportIndexProvokingVertex);
break;
case GL_MAX_COLOR_TEXTURE_SAMPLES: case GL_MAX_COLOR_TEXTURE_SAMPLES:
*params = dynamicParameters.MaxColorTextureSamples; *params = std::max(dynamicParameters.MaxColorTextureSamples, GetAdvertisedMaxSamples());
break; break;
case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS: case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS:
*params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents, *params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents,
@@ -2147,7 +2246,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = dynamicParameters.MaxCubeMapTextureSize; *params = dynamicParameters.MaxCubeMapTextureSize;
break; break;
case GL_MAX_DEPTH_TEXTURE_SAMPLES: case GL_MAX_DEPTH_TEXTURE_SAMPLES:
*params = dynamicParameters.MaxDepthTextureSamples; *params = std::max(dynamicParameters.MaxDepthTextureSamples, GetAdvertisedMaxSamples());
break; break;
case GL_MAX_FRAMEBUFFER_WIDTH: case GL_MAX_FRAMEBUFFER_WIDTH:
*params = dynamicParameters.MaxFramebufferWidth; *params = dynamicParameters.MaxFramebufferWidth;
@@ -2174,7 +2273,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = dynamicParameters.MaxComputeImageUniforms; *params = dynamicParameters.MaxComputeImageUniforms;
break; break;
case GL_MAX_INTEGER_SAMPLES: case GL_MAX_INTEGER_SAMPLES:
*params = dynamicParameters.MaxIntegerSamples; *params = std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
break; break;
case GL_MAX_RENDERBUFFER_SIZE: case GL_MAX_RENDERBUFFER_SIZE:
*params = dynamicParameters.MaxRenderbufferSize; *params = dynamicParameters.MaxRenderbufferSize;
@@ -2207,18 +2306,19 @@ namespace MobileGL::MG_Impl::GLImpl {
static_cast<Uint64>(INT32_MAX))); static_cast<Uint64>(INT32_MAX)));
break; break;
case GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS: case GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS:
// NOT the frontend's binding-point array size: GetIndexedBufferQueryPointCount
// clamps this family to the range a lowered counter block can actually be served
// from, which is the same number glslang compiles a layout(binding = N) atomic_uint
// against and the same one glBindBufferBase validates an index against.
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter)); *params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
break; break;
case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE: case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
// The conformance suite splits this evenly across every advertised binding point and // The conformance suite splits this evenly across every advertised binding point and
// binds all of them in one glBindBuffersRange // binds all of them in one glBindBuffersRange
// (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide: // (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide -
// 32 bytes over 36 binding points is a zero-sized range, which BindBufferRange // a zero-sized range is INVALID_VALUE before BindBufferRange binds anything. The
// rejects with INVALID_VALUE before it binds anything. Floor the advertised size at // shared constant is 16384 over 8 binding points, which divides.
// one counter per binding point. *params = kFrontendMaxAtomicCounterBufferSize;
*params = std::max<GLint>(
kFrontendMaxAtomicCounterBufferSize,
static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter) * sizeof(GLuint)));
break; break;
case GL_MAX_TEXTURE_BUFFER_SIZE: case GL_MAX_TEXTURE_BUFFER_SIZE:
*params = dynamicParameters.MaxTextureBufferSize; *params = dynamicParameters.MaxTextureBufferSize;
@@ -2340,7 +2440,7 @@ namespace MobileGL::MG_Impl::GLImpl {
: dynamicParameters.MaxDrawBuffers; : dynamicParameters.MaxDrawBuffers;
break; break;
case GL_MAX_SAMPLES: case GL_MAX_SAMPLES:
*params = std::max(dynamicParameters.MaxSamples, kFrontendMaxSamples); *params = GetAdvertisedMaxSamples();
break; break;
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT: case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT:
// Float state (see GetFloatv); rounded to nearest for the integer query per GL 3.3 6.1.2. // Float state (see GetFloatv); rounded to nearest for the integer query per GL 3.3 6.1.2.
@@ -24,4 +24,8 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data); void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
GLenum GetError(); GLenum GetError();
GLenum GetGraphicsResetStatus(); GLenum GetGraphicsResetStatus();
// The GL_MAX_SAMPLES value MobileGL advertises, i.e. the driver's value floored to the GL
// core minimum. Frontend multisample validators have to honour this ceiling for every
// format, otherwise MobileGL rejects a sample count it advertised itself.
GLint GetAdvertisedMaxSamples();
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
+103 -18
View File
@@ -21,6 +21,9 @@
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
// The flattened uniform type these helpers used to take as a raw glslang::TType*
// pointing into the TProgram's pool allocator. See ProgramObject::TypeFacts.
using TypeFactsRef = const MG_State::GLState::ProgramObject::TypeFacts&;
static GLint BoolToGLInt(bool value) { static GLint BoolToGLInt(bool value) {
return value ? GL_TRUE : GL_FALSE; return value ? GL_TRUE : GL_FALSE;
} }
@@ -223,14 +226,14 @@ namespace MobileGL::MG_Impl::GLImpl {
return false; return false;
} }
GLint GetOpaqueUniformUnitLimit(const glslang::TType* type) { GLint GetOpaqueUniformUnitLimit(const TypeFactsRef type) {
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters(); const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
if (type && type->isImage()) return dynamicParameters.MaxImageUnits; if (type.isImage) return dynamicParameters.MaxImageUnits;
if (type && type->isTexture()) return dynamicParameters.MaxCombinedTextureImageUnits; if (type.isTexture) return dynamicParameters.MaxCombinedTextureImageUnits;
return 0; return 0;
} }
bool ValidateOpaqueUniformUnit(const char* functionName, const glslang::TType* type, GLint unit) { bool ValidateOpaqueUniformUnit(const char* functionName, const TypeFactsRef type, GLint unit) {
const GLint limit = GetOpaqueUniformUnitLimit(type); const GLint limit = GetOpaqueUniformUnitLimit(type);
if (unit < 0 || unit >= limit) { if (unit < 0 || unit >= limit) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -525,6 +528,10 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_UNIFORM_ARRAY_STRIDE: case GL_UNIFORM_ARRAY_STRIDE:
case GL_UNIFORM_MATRIX_STRIDE: case GL_UNIFORM_MATRIX_STRIDE:
case GL_UNIFORM_IS_ROW_MAJOR: case GL_UNIFORM_IS_ROW_MAJOR:
// GL 4.2 / ARB_shader_atomic_counters adds this one to the accepted set. Leaving it
// out did not merely lose the answer: the leftover GL_INVALID_ENUM is what made
// KHR-GL43.shader_atomic_counters.basic-program-query force a FAIL.
case GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX:
break; break;
default: default:
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -580,6 +587,11 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_UNIFORM_IS_ROW_MAJOR: case GL_UNIFORM_IS_ROW_MAJOR:
params[i] = programObject->GetActiveUniformIsRowMajor(idx); params[i] = programObject->GetActiveUniformIsRowMajor(idx);
break; break;
case GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX:
// Index into the GL_ACTIVE_ATOMIC_COUNTER_BUFFERS list, -1 for every uniform
// that is not an atomic counter (GL 4.6 core table 7.6).
params[i] = programObject->GetActiveUniformAtomicCounterBufferIndex(idx);
break;
default: default:
break; break;
} }
@@ -642,7 +654,13 @@ namespace MobileGL::MG_Impl::GLImpl {
break; break;
} }
case GL_ACTIVE_ATOMIC_COUNTER_BUFFERS: case GL_ACTIVE_ATOMIC_COUNTER_BUFFERS:
*params = programObject->GetActiveAtomicCounterCount(); // Counter BUFFERS, not counters, and glslang's own getNumAtomicCounters() answers
// neither: the relaxed parse has already turned every atomic_uint into a plain uint
// member of a synthesized storage block by the time it builds its reflection, so it
// reports zero. The interface-query model recovers the buffers from those blocks and
// is what glGetProgramInterfaceiv(GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES)
// already answers - the two queries are required to agree.
*params = ProgramInterface::GetActiveResourceCount(*programObject, GL_ATOMIC_COUNTER_BUFFER);
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params); MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break; break;
case GL_ACTIVE_ATTRIBUTES: case GL_ACTIVE_ATTRIBUTES:
@@ -856,10 +874,10 @@ namespace MobileGL::MG_Impl::GLImpl {
// demotion makes a dmat4 a mat4 in the shader and a mat4-shaped slot here - but because it // demotion makes a dmat4 a mat4 in the shader and a mat4-shaped slot here - but because it
// is ROUTED differently: the caller's component-by-component EbtDouble branch has to widen // is ROUTED differently: the caller's component-by-component EbtDouble branch has to widen
// each float back to the queried type, and it undoes the same padding itself. // each float back to the queried type, and it undoes the same padding itself.
Bool TryGatherFloatMatrixColumns(const glslang::TType* ttype, const char* pBase, void* params) { Bool TryGatherFloatMatrixColumns(const TypeFactsRef ttype, const char* pBase, void* params) {
if (ttype == nullptr || !ttype->isMatrix() || ttype->getBasicType() == glslang::EbtDouble) return false; if (!ttype.isMatrix || ttype.isDouble) return false;
const Int columns = ttype->getMatrixCols(); const Int columns = ttype.matrixCols;
const Int rows = ttype->getMatrixRows(); const Int rows = ttype.matrixRows;
for (Int column = 0; column < columns; ++column) { for (Int column = 0; column < columns; ++column) {
Memcpy(static_cast<char*>(params) + static_cast<SizeT>(column) * rows * sizeof(GLfloat), Memcpy(static_cast<char*>(params) + static_cast<SizeT>(column) * rows * sizeof(GLfloat),
pBase + static_cast<SizeT>(column) * 4 * sizeof(GLfloat), rows * sizeof(GLfloat)); pBase + static_cast<SizeT>(column) * 4 * sizeof(GLfloat), rows * sizeof(GLfloat));
@@ -871,7 +889,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// everything except a float matrix, whose padded columns make it wider. The rule itself // everything except a float matrix, whose padded columns make it wider. The rule itself
// lives on ProgramObject, because the pipeline composite's uniform refresh needs the same // lives on ProgramObject, because the pipeline composite's uniform refresh needs the same
// one and two copies of a layout rule is one too many. // one and two copies of a layout rule is one too many.
SizeT UniformStorageSpanInBytes(const glslang::TType* ttype, SizeT tightSize) { SizeT UniformStorageSpanInBytes(const TypeFactsRef ttype, SizeT tightSize) {
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize); return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize);
} }
@@ -904,7 +922,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto offset = programObject->GetUniformOffset(location); auto offset = programObject->GetUniformOffset(location);
auto size = programObject->GetUniformSizesInBytes(location); auto size = programObject->GetUniformSizesInBytes(location);
char* pUBO = (char*)programObject->MapUBO(); char* pUBO = (char*)programObject->MapUBO();
auto* ttype = programObject->GetUniformTType(location); const auto& ttype = programObject->GetUniformTypeFacts(location);
const SizeT span = UniformStorageSpanInBytes(ttype, size); const SizeT span = UniformStorageSpanInBytes(ttype, size);
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset || if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
offset + span > programObject->GetUBOSize()) { offset + span > programObject->GetUBOSize()) {
@@ -958,7 +976,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto offset = programObject->GetUniformOffset(location); auto offset = programObject->GetUniformOffset(location);
auto size = programObject->GetUniformSizesInBytes(location); auto size = programObject->GetUniformSizesInBytes(location);
char* pUBO = static_cast<char*>(programObject->MapUBO()); char* pUBO = static_cast<char*>(programObject->MapUBO());
auto* ttype = programObject->GetUniformTType(location); const auto& ttype = programObject->GetUniformTypeFacts(location);
const SizeT span = UniformStorageSpanInBytes(ttype, size); const SizeT span = UniformStorageSpanInBytes(ttype, size);
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset || if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
offset + span > programObject->GetUBOSize()) { offset + span > programObject->GetUBOSize()) {
@@ -981,10 +999,10 @@ namespace MobileGL::MG_Impl::GLImpl {
// conversion rules (7.6: round to nearest for the integer queries) apply; the value // conversion rules (7.6: round to nearest for the integer queries) apply; the value
// widens back to the queried type, having lost precision at the glUniform*d that // widens back to the queried type, having lost precision at the glUniform*d that
// stored it and not here. // stored it and not here.
if (ttype->getBasicType() == glslang::EbtDouble) { if (ttype.isDouble) {
const Int columns = ttype->isMatrix() ? ttype->getMatrixCols() : 1; const Int columns = ttype.isMatrix ? ttype.matrixCols : 1;
const Int rows = ttype->isMatrix() ? ttype->getMatrixRows() const Int rows = ttype.isMatrix ? ttype.matrixRows
: (ttype->isVector() ? ttype->getVectorSize() : 1); : (ttype.isVector ? ttype.vectorSize : 1);
// std140 gives every matrix column its own 16-byte slot; a non-matrix is one // std140 gives every matrix column its own 16-byte slot; a non-matrix is one
// tightly packed run and never reaches the stride at all. // tightly packed run and never reaches the stride at all.
const SizeT columnStride = 4 * sizeof(GLfloat); const SizeT columnStride = 4 * sizeof(GLfloat);
@@ -1191,8 +1209,8 @@ namespace MobileGL::MG_Impl::GLImpl {
Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize); Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize);
programObject.MarkUBOContentDirty(); programObject.MarkUBOContentDirty();
} else { } else {
auto* ttype = programObject.GetUniformTType(location); const auto& ttype = programObject.GetUniformTypeFacts(location);
if (!ttype->isTexture() && !ttype->isImage()) return; if (!ttype.isTexture && !ttype.isImage) return;
if constexpr (!std::is_same_v<std::remove_cv_t<T>, GLint> || ItemCount != 1) { if constexpr (!std::is_same_v<std::remove_cv_t<T>, GLint> || ItemCount != 1) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
@@ -2835,6 +2853,73 @@ namespace MobileGL::MG_Impl::GLImpl {
return ProgramInterface::GetResourceLocationIndex(*programObject, programInterface, name); return ProgramInterface::GetResourceLocationIndex(*programObject, programInterface, name);
} }
// GL 4.6 §7.7. Every property this reports is one the GL_ATOMIC_COUNTER_BUFFER interface
// already carries, so this is a rename of glGetProgramResourceiv's props onto the older
// entry point's - and the two are required to agree, which is only true while both read the
// same model. It was a silent stub: it wrote nothing, raised nothing, and left every probe
// reading its own uninitialised output.
static Bool TryMapActiveAtomicCounterBufferProp(GLenum pname, GLenum& outProp) {
switch (pname) {
case GL_ATOMIC_COUNTER_BUFFER_BINDING:
outProp = GL_BUFFER_BINDING;
return true;
case GL_ATOMIC_COUNTER_BUFFER_DATA_SIZE:
outProp = GL_BUFFER_DATA_SIZE;
return true;
case GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS:
outProp = GL_NUM_ACTIVE_VARIABLES;
return true;
case GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES:
outProp = GL_ACTIVE_VARIABLES;
return true;
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_VERTEX_SHADER:
outProp = GL_REFERENCED_BY_VERTEX_SHADER;
return true;
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_CONTROL_SHADER:
outProp = GL_REFERENCED_BY_TESS_CONTROL_SHADER;
return true;
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_EVALUATION_SHADER:
outProp = GL_REFERENCED_BY_TESS_EVALUATION_SHADER;
return true;
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_GEOMETRY_SHADER:
outProp = GL_REFERENCED_BY_GEOMETRY_SHADER;
return true;
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_FRAGMENT_SHADER:
outProp = GL_REFERENCED_BY_FRAGMENT_SHADER;
return true;
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_COMPUTE_SHADER:
outProp = GL_REFERENCED_BY_COMPUTE_SHADER;
return true;
default:
return false;
}
}
void GetActiveAtomicCounterBufferiv(GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) {
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
if (!programObject) return;
GLenum prop = GL_NONE;
if (!TryMapActiveAtomicCounterBufferProp(pname, prop)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"pname is not an active atomic counter buffer property."));
return;
}
Vector<GLint> values;
if (!ProgramInterface::GetResourceProp(*programObject, GL_ATOMIC_COUNTER_BUFFER, bufferIndex, prop, values)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"bufferIndex is not an active atomic counter buffer index."));
return;
}
if (params == nullptr) return;
// GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES is the only multi-value property
// here, and the caller sized its array from _ACTIVE_ATOMIC_COUNTERS.
for (SizeT i = 0; i < values.size(); ++i) params[i] = values[i];
}
// GL 4.6 §7.6.2: <storageBlockIndex> is an active shader storage block index of <program> // GL 4.6 §7.6.2: <storageBlockIndex> is an active shader storage block index of <program>
// - that is, exactly what glGetProgramResourceIndex(GL_SHADER_STORAGE_BLOCK) returned. // - that is, exactly what glGetProgramResourceIndex(GL_SHADER_STORAGE_BLOCK) returned.
// Since wave 2 that index is the interface-query layer's, so this is where the one index // Since wave 2 that index is the interface-query layer's, so this is where the one index
@@ -140,6 +140,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params); const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name); GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name); GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
void GetActiveAtomicCounterBufferiv(GLuint program, GLuint bufferIndex, GLenum pname, GLint* params);
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding); void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
void Uniform1d(GLint location, GLdouble v0); void Uniform1d(GLint location, GLdouble v0);
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value); void Uniform1dv(GLint location, GLsizei count, const GLdouble* value);
@@ -19,7 +19,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
// "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL // "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL
// atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource // atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource
// and its trailing number IS GL_BUFFER_BINDING; its members stay GL_UNIFORMs. // and its trailing number IS GL_BUFFER_BINDING; its members stay GL_UNIFORMs.
constexpr const char* kAtomicCounterBlockPrefix = "gl_AtomicCounterBlock"; constexpr const char* kAtomicCounterBlockPrefix = MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX;
enum class BlockKind { enum class BlockKind {
Uniform, // a real GL uniform block Uniform, // a real GL uniform block
@@ -81,19 +81,18 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
// The enumerated spelling of an array resource is "name[0]". glslang already applies // The enumerated spelling of an array resource is "name[0]". glslang already applies
// that to uniforms and buffer variables (EShReflectionBasicArraySuffix), but never to // that to uniforms and buffer variables (EShReflectionBasicArraySuffix), but never to
// stage inputs/outputs, so those get it here. // stage inputs/outputs, so those get it here.
String WithArraySuffix(const String& name, const glslang::TType* type) { String WithArraySuffix(const String& name, const ProgramObject::TypeFacts& type) {
if (type == nullptr || !type->isArray() || EndsWithZeroSubscript(name)) return name; if (!type.isArray || EndsWithZeroSubscript(name)) return name;
return name + "[0]"; return name + "[0]";
} }
// GL_ARRAY_SIZE: element count for a sized array, 0 for a runtime-sized one // GL_ARRAY_SIZE: element count for a sized array, 0 for a runtime-sized one
// (a shader storage block's unsized trailing member), 1 for a non-array. // (a shader storage block's unsized trailing member), 1 for a non-array.
GLint ArraySizeOf(const glslang::TType* type, GLint reflectedSize) { // `record.arraySize` is already the sized-array/reflected-size resolution; the only
if (type != nullptr && type->isArray()) { // extra rule here is GL's 0 for a runtime-sized array.
if (!type->isSizedArray()) return 0; GLint ArraySizeOf(const ProgramObject::ResourceReflection& record) {
return type->getOuterArraySize(); if (record.type.isArray && !record.type.isSizedArray) return 0;
} return record.arraySize;
return reflectedSize < 1 ? 1 : reflectedSize;
} }
// Two spellings name the same resource when they are equal, or differ only by the // Two spellings name the same resource when they are equal, or differ only by the
@@ -174,22 +173,21 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
return static_cast<GLint>(element); return static_cast<GLint>(element);
} }
BlockKind ClassifyBlock(const glslang::TObjectReflection& block) { BlockKind ClassifyBlock(const ProgramObject::BlockReflection& block) {
if (std::strstr(block.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) { if (std::strstr(block.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) {
return BlockKind::GlobalUbo; return BlockKind::GlobalUbo;
} }
if (IsAtomicCounterBlockName(block.name)) return BlockKind::AtomicCounter; if (IsAtomicCounterBlockName(block.name)) return BlockKind::AtomicCounter;
const glslang::TType* type = block.getType(); if (block.type.isBuffer) return BlockKind::Storage;
if (type != nullptr && type->getQualifier().storage == glslang::EvqBuffer) return BlockKind::Storage;
return BlockKind::Uniform; return BlockKind::Uniform;
} }
// std140/std430 column stride, the same vec4-rounded rule ProgramObject applies to // std140/std430 column stride, the same vec4-rounded rule ProgramObject applies to
// uniform matrices. 0 for a non-matrix. // uniform matrices. 0 for a non-matrix.
GLint MatrixStrideOf(const glslang::TType* type) { GLint MatrixStrideOf(const ProgramObject::TypeFacts& type) {
if (type == nullptr || !type->isMatrix()) return 0; if (!type.isMatrix) return 0;
const bool rowMajor = type->getQualifier().layoutMatrix == glslang::ElmRowMajor; const bool rowMajor = type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor);
const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows(); const int strideVectorComponents = rowMajor ? type.matrixCols : type.matrixRows;
constexpr int scalarSize = 4; constexpr int scalarSize = 4;
const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize
: (strideVectorComponents == 2) ? 2 * scalarSize : (strideVectorComponents == 2) ? 2 * scalarSize
@@ -197,9 +195,9 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
return (vectorAlignment + 15) & ~15; return (vectorAlignment + 15) & ~15;
} }
GLint IsRowMajorOf(const glslang::TType* type) { GLint IsRowMajorOf(const ProgramObject::TypeFacts& type) {
if (type == nullptr || !type->isMatrix()) return 0; if (!type.isMatrix) return 0;
return type->getQualifier().layoutMatrix == glslang::ElmRowMajor ? 1 : 0; return type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor) ? 1 : 0;
} }
GLint MappedLocation(Int rawLocation) { GLint MappedLocation(Int rawLocation) {
@@ -227,12 +225,12 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
// Note the union is used even when it is empty: an array element nobody dereferenced has // Note the union is used even when it is empty: an array element nobody dereferenced has
// no member bits and is genuinely referenced by nobody, which is the whole point - falling // no member bits and is genuinely referenced by nobody, which is the whole point - falling
// back to the block's own mask there would restore the over-approximation. // back to the block's own mask there would restore the over-approximation.
Vector<Uint32> BuildBlockStagesFromMembers(const glslang::TProgram& reflection, Int blockCount) { Vector<Uint32> BuildBlockStagesFromMembers(const ProgramObject::LinkArtifacts& reflection,
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection); Int blockCount) {
Vector<Uint32> stagesByBlock(static_cast<SizeT>(blockCount < 0 ? 0 : blockCount), 0u); Vector<Uint32> stagesByBlock(static_cast<SizeT>(blockCount < 0 ? 0 : blockCount), 0u);
const Int uniformCount = mutableReflection.getNumUniformVariables(); const Int uniformCount = static_cast<Int>(reflection.uniformReflection.size());
for (Int index = 0; index < uniformCount; ++index) { for (Int index = 0; index < uniformCount; ++index) {
const auto& uniform = mutableReflection.getUniform(index); const auto& uniform = reflection.uniformReflection[index];
const Int owner = uniform.index; const Int owner = uniform.index;
if (owner < 0 || owner >= blockCount) continue; if (owner < 0 || owner >= blockCount) continue;
stagesByBlock[static_cast<SizeT>(owner)] |= static_cast<Uint32>(uniform.stages); stagesByBlock[static_cast<SizeT>(owner)] |= static_cast<Uint32>(uniform.stages);
@@ -250,7 +248,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
// ss[1] and requires both to report the fragment stage, which only glslang's own // ss[1] and requires both to report the fragment stage, which only glslang's own
// (deliberately over-approximating) block mask gets right. Storage and atomic-counter // (deliberately over-approximating) block mask gets right. Storage and atomic-counter
// blocks therefore keep that mask untouched. // blocks therefore keep that mask untouched.
Uint32 UniformBlockStages(const glslang::TObjectReflection& block, const Vector<Uint32>& stagesFromMembers, Uint32 UniformBlockStages(const ProgramObject::BlockReflection& block, const Vector<Uint32>& stagesFromMembers,
Int tIndex) { Int tIndex) {
String arrayBase; String arrayBase;
Uint element = 0; Uint element = 0;
@@ -264,15 +262,15 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
return stagesFromMembers[static_cast<SizeT>(tIndex)]; return stagesFromMembers[static_cast<SizeT>(tIndex)];
} }
void BuildBlocks(ProgramObject& program, const glslang::TProgram& reflection, Model& model, void BuildBlocks(ProgramObject& program, const ProgramObject::LinkArtifacts& reflection, Model& model,
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) { Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
const Int blockCount = const_cast<glslang::TProgram&>(reflection).getNumUniformBlocks(); const Int blockCount = static_cast<Int>(reflection.blockReflection.size());
blockKind.assign(blockCount, BlockKind::Uniform); blockKind.assign(blockCount, BlockKind::Uniform);
blockInterfaceIndex.assign(blockCount, -1); blockInterfaceIndex.assign(blockCount, -1);
const Vector<Uint32> stagesFromMembers = BuildBlockStagesFromMembers(reflection, blockCount); const Vector<Uint32> stagesFromMembers = BuildBlockStagesFromMembers(reflection, blockCount);
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) { for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
const auto& block = const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex); const auto& block = reflection.blockReflection[tIndex];
const BlockKind kind = ClassifyBlock(block); const BlockKind kind = ClassifyBlock(block);
blockKind[tIndex] = kind; blockKind[tIndex] = kind;
if (kind == BlockKind::AtomicCounter) { if (kind == BlockKind::AtomicCounter) {
@@ -293,7 +291,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
// glShaderStorageBlockBinding wins over the declaration (GL 4.6 §7.6.2 - // glShaderStorageBlockBinding wins over the declaration (GL 4.6 §7.6.2 -
// exactly the same rule GL_UNIFORM_BLOCK follows through // exactly the same rule GL_UNIFORM_BLOCK follows through
// GetUniformBlockBinding below). // GetUniformBlockBinding below).
const GLint declared = block.getBinding(); const GLint declared = block.binding;
resource.bufferBinding = declared < 0 ? 0 : declared + BlockArrayElement(block.name); resource.bufferBinding = declared < 0 ? 0 : declared + BlockArrayElement(block.name);
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name); const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
if (rebound >= 0) resource.bufferBinding = static_cast<GLint>(rebound); if (rebound >= 0) resource.bufferBinding = static_cast<GLint>(rebound);
@@ -315,21 +313,22 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex)); resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex));
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex)); const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex));
if (tIndex >= 0 && tIndex < blockCount) { if (tIndex >= 0 && tIndex < blockCount) {
resource.stages = UniformBlockStages(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex), resource.stages = UniformBlockStages(reflection.blockReflection[tIndex],
stagesFromMembers, tIndex); stagesFromMembers, tIndex);
} }
model.uniformBlocks.push_back(Move(resource)); model.uniformBlocks.push_back(Move(resource));
} }
} }
void BuildUniformsAndBufferVariables(ProgramObject& program, const glslang::TProgram& reflection, Model& model, void BuildUniformsAndBufferVariables(ProgramObject& program,
const ProgramObject::LinkArtifacts& reflection, Model& model,
const Vector<BlockKind>& blockKind, const Vector<BlockKind>& blockKind,
const Vector<Int>& blockInterfaceIndex) { const Vector<Int>& blockInterfaceIndex) {
const Uint uniformCount = program.GetUniformCount(); const Uint uniformCount = program.GetUniformCount();
for (Uint glIndex = 0; glIndex < uniformCount; ++glIndex) { for (Uint glIndex = 0; glIndex < uniformCount; ++glIndex) {
const Int tIndex = program.TProgramUniformIndex(glIndex); const Int tIndex = program.TProgramUniformIndex(glIndex);
const auto& refl = const_cast<glslang::TProgram&>(reflection).getUniform(tIndex); const auto& refl = ProgramObject::UniformAtIn(reflection, tIndex);
const glslang::TType* type = refl.getType(); const auto& type = refl.type;
const Int owner = refl.index; const Int owner = refl.index;
const BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size())) const BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size()))
? blockKind[owner] ? blockKind[owner]
@@ -338,7 +337,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
Resource resource; Resource resource;
resource.name = refl.name; resource.name = refl.name;
resource.type = static_cast<GLenum>(refl.glDefineType); resource.type = static_cast<GLenum>(refl.glDefineType);
resource.arraySize = ArraySizeOf(type, refl.size); resource.arraySize = ArraySizeOf(refl);
resource.stages = static_cast<Uint32>(refl.stages); resource.stages = static_cast<Uint32>(refl.stages);
if (kind == BlockKind::Storage) { if (kind == BlockKind::Storage) {
@@ -414,17 +413,13 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
// program that redeclares `out gl_PerVertex { vec4 gl_Position; }` still carries // program that redeclares `out gl_PerVertex { vec4 gl_Position; }` still carries
// gl_PointSize and gl_ClipDistance through the block-unwrapping reflection, and they // gl_PointSize and gl_ClipDistance through the block-unwrapping reflection, and they
// are not part of its output interface. // are not part of its output interface.
Bool IsHiddenBlockMember(const glslang::TType* type) { Bool IsHiddenBlockMember(const ProgramObject::TypeFacts& type) { return type.isVoid; }
return type != nullptr && type->getBasicType() == glslang::EbtVoid;
}
void BuildStageIO(ProgramObject& program, const glslang::TProgram& reflection, Model& model) { void BuildStageIO(ProgramObject& program, const ProgramObject::LinkArtifacts& reflection, Model& model) {
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection); const Int inputCount = static_cast<Int>(reflection.pipeInputReflection.size());
const Int inputCount = mutableReflection.getNumPipeInputs();
for (Int index = 0; index < inputCount; ++index) { for (Int index = 0; index < inputCount; ++index) {
const auto& refl = mutableReflection.getPipeInput(index); const auto& refl = reflection.pipeInputReflection[index];
const glslang::TType* type = refl.getType(); const auto& type = refl.type;
if (IsHiddenBlockMember(type)) continue; if (IsHiddenBlockMember(type)) continue;
Resource resource; Resource resource;
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V // The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
@@ -432,10 +427,10 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
const String& glName = ProgramObject::NormalizeBuiltinPipeInputName(refl.name); const String& glName = ProgramObject::NormalizeBuiltinPipeInputName(refl.name);
resource.name = WithArraySuffix(glName, type); resource.name = WithArraySuffix(glName, type);
resource.type = static_cast<GLenum>(refl.glDefineType); resource.type = static_cast<GLenum>(refl.glDefineType);
resource.arraySize = ArraySizeOf(type, refl.size); resource.arraySize = ArraySizeOf(refl);
resource.location = program.GetAttributeLocation(refl.name); resource.location = program.GetAttributeLocation(refl.name);
if (resource.location < 0) resource.location = MappedLocation(static_cast<Int>(refl.layoutLocation())); if (resource.location < 0) resource.location = MappedLocation(refl.location);
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0; resource.isPerPatch = type.isPatch ? 1 : 0;
resource.stages = static_cast<Uint32>(refl.stages); resource.stages = static_cast<Uint32>(refl.stages);
model.programInputs.push_back(Move(resource)); model.programInputs.push_back(Move(resource));
} }
@@ -447,16 +442,16 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
// carries its own layout(location=N)), and a location then manufactures a color // carries its own layout(location=N)), and a location then manufactures a color
// index of 0 where GL requires -1 // index of 0 where GL requires -1
// (KHR-GL43.program_interface_query.separate-programs-tess-control). // (KHR-GL43.program_interface_query.separate-programs-tess-control).
const Bool lastStageIsFragment = mutableReflection.getIntermediate(EShLangFragment) != nullptr; const Bool lastStageIsFragment = reflection.lastStageIsFragment;
const Int outputCount = mutableReflection.getNumPipeOutputs(); const Int outputCount = static_cast<Int>(reflection.pipeOutputReflection.size());
for (Int index = 0; index < outputCount; ++index) { for (Int index = 0; index < outputCount; ++index) {
const auto& refl = mutableReflection.getPipeOutput(index); const auto& refl = reflection.pipeOutputReflection[index];
const glslang::TType* type = refl.getType(); const auto& type = refl.type;
if (IsHiddenBlockMember(type)) continue; if (IsHiddenBlockMember(type)) continue;
Resource resource; Resource resource;
resource.name = WithArraySuffix(refl.name, type); resource.name = WithArraySuffix(refl.name, type);
resource.type = static_cast<GLenum>(refl.glDefineType); resource.type = static_cast<GLenum>(refl.glDefineType);
resource.arraySize = ArraySizeOf(type, refl.size); resource.arraySize = ArraySizeOf(refl);
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str())); resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
if (resource.location < 0 || !lastStageIsFragment) { if (resource.location < 0 || !lastStageIsFragment) {
// A built-in output (gl_FragDepth, gl_SampleMask) has no location, and a // A built-in output (gl_FragDepth, gl_SampleMask) has no location, and a
@@ -467,11 +462,11 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str()); resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
// glBindFragDataLocationIndexed wins; otherwise the shader's // glBindFragDataLocationIndexed wins; otherwise the shader's
// layout(index = N), which the frag-data maps never saw. // layout(index = N), which the frag-data maps never saw.
if (resource.locationIndex == 0 && type != nullptr && type->getQualifier().hasIndex()) { if (resource.locationIndex == 0 && type.hasIndex) {
resource.locationIndex = static_cast<GLint>(type->getQualifier().layoutIndex); resource.locationIndex = static_cast<GLint>(type.layoutIndex);
} }
} }
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0; resource.isPerPatch = type.isPatch ? 1 : 0;
resource.stages = static_cast<Uint32>(refl.stages); resource.stages = static_cast<Uint32>(refl.stages);
model.programOutputs.push_back(Move(resource)); model.programOutputs.push_back(Move(resource));
} }
@@ -511,15 +506,14 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
Model BuildModel(ProgramObject& program) { Model BuildModel(ProgramObject& program) {
Model model; Model model;
if (!program.GetLinkStatus()) return model; if (!program.GetLinkStatus()) return model;
const glslang::TProgram* reflection = program.GetReflection(); const ProgramObject::LinkArtifacts& reflection = program.GetLinkReflection();
if (reflection == nullptr) return model;
model.valid = true; model.valid = true;
Vector<BlockKind> blockKind; Vector<BlockKind> blockKind;
Vector<Int> blockInterfaceIndex; Vector<Int> blockInterfaceIndex;
BuildBlocks(program, *reflection, model, blockKind, blockInterfaceIndex); BuildBlocks(program, reflection, model, blockKind, blockInterfaceIndex);
BuildUniformsAndBufferVariables(program, *reflection, model, blockKind, blockInterfaceIndex); BuildUniformsAndBufferVariables(program, reflection, model, blockKind, blockInterfaceIndex);
BuildStageIO(program, *reflection, model); BuildStageIO(program, reflection, model);
BuildXfb(program, model); BuildXfb(program, model);
return model; return model;
} }
+135 -6
View File
@@ -31,8 +31,15 @@ namespace MobileGL::MG_Impl::GLImpl {
Bool ended = false; Bool ended = false;
Bool resultCached = false; Bool resultCached = false;
Uint64 cachedResult = 0; 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; 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 // Query calls may arrive from any thread (launchers migrate the context
@@ -122,6 +129,46 @@ namespace MobileGL::MG_Impl::GLImpl {
g_activeTimeElapsedQueryId = 0; 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 // Shared GetQueryObject* implementation. Returns false when an error
// was recorded and no value should be written back. `outValueProduced`, when given, // 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 // 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; const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
queryObject->backendHandle = queryObject->backendHandle =
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr; 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) { } else if (isOcclusionQuery) {
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery(); queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
} else { } else {
@@ -448,12 +499,21 @@ namespace MobileGL::MG_Impl::GLImpl {
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) { if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
endXfbPrimitivesQuery(queryObject->backendHandle); endXfbPrimitivesQuery(queryObject->backendHandle);
} }
// Result comes from the GPU query at read time. }
} else { // A backend query that is not going to be read is released here, not left to be
queryObject->cachedResult = // collected later: the span is over, the driver object has nothing left to say.
MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter() - queryObject->counterSnapshot; // 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; queryObject->resultCached = true;
} }
// Otherwise the result comes from the GPU query at read time.
queryObject->active = false; queryObject->active = false;
queryObject->ended = true; queryObject->ended = true;
activeQueryId = 0; activeQueryId = 0;
@@ -505,6 +565,75 @@ namespace MobileGL::MG_Impl::GLImpl {
queryObject->ended = true; 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<std::mutex> 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) { void GetQueryiv(GLenum target, GLenum pname, GLint* params) {
if (!params) { if (!params) {
return; return;
+5
View File
@@ -29,6 +29,11 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset); void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset); void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
void QueryCounter(GLuint id, GLenum target); 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. // Destroys every still-registered query object exactly as DeleteQueries would.
// GL requires queries to die with their context; called only from full library // GL requires queries to die with their context; called only from full library
// teardown (DestroyImpl), where no context survives on any thread, so the // teardown (DestroyImpl), where no context survives on any thread, so the
+29
View File
@@ -8,6 +8,7 @@
#include "GL_Sync.h" #include "GL_Sync.h"
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
namespace { namespace {
@@ -35,6 +36,22 @@ namespace MobileGL::MG_Impl::GLImpl {
} // namespace } // namespace
GLsync FenceSync(GLenum condition, GLbitfield flags) { GLsync FenceSync(GLenum condition, GLbitfield flags) {
// GL 4.6 core 4.1.2: GL_SYNC_GPU_COMMANDS_COMPLETE is the only condition and the only
// legal flags value is zero; both violations return 0 rather than a handle. A caller that
// then hands the 0 back to glDeleteSync hits the glDeleteSync(0) no-op below.
if (condition != GL_SYNC_GPU_COMMANDS_COMPLETE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"condition must be GL_SYNC_GPU_COMMANDS_COMPLETE."));
return nullptr;
}
if (flags != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "flags must be zero."));
return nullptr;
}
auto* syncObject = new SyncObject; auto* syncObject = new SyncObject;
syncObject->condition = condition; syncObject->condition = condition;
syncObject->flags = flags; syncObject->flags = flags;
@@ -64,6 +81,18 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) { void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
// GL 4.6 core 4.1.2: the server-side wait takes no flags and no finite timeout - both
// arguments exist only to be forward-compatible, and anything else is INVALID_VALUE.
// Neither backend ever honored a nonzero timeout (DirectGLES hard-codes
// 0/GL_TIMEOUT_IGNORED, DirectVulkan's queue ordering makes the wait implicit), so
// rejecting the call loses no wait that used to happen.
if (flags != 0 || timeout != GL_TIMEOUT_IGNORED) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"flags must be zero and timeout must be GL_TIMEOUT_IGNORED."));
return;
}
const auto* syncObject = FindSyncObject(sync); const auto* syncObject = FindSyncObject(sync);
if (!syncObject) { if (!syncObject) {
return; return;
+533 -104
View File
@@ -474,15 +474,48 @@ namespace MobileGL::MG_Impl::GLImpl {
target == TextureTarget::Texture2DMultisampleArray; target == TextureTarget::Texture2DMultisampleArray;
} }
Int GetMaxSupportedTextureSamples(TextureInternalFormat textureInternalFormat) { // The largest count the backend actually probed for this format on this target, or 0 when
// it has no answer for the pair. Both backends build the list in descending order.
Int GetProbedMaxTextureSamples(TextureTarget textureTarget, TextureInternalFormat textureInternalFormat) {
if (MG_Backend::pActiveBackendObject == nullptr) {
return 0;
}
const SizeT targetIndex = MG_Backend::GetFormatCapabilityTargetIndex(textureTarget);
const SizeT formatIndex = static_cast<SizeT>(textureInternalFormat);
if (targetIndex >= MG_Backend::kFormatCapabilityTargetCount ||
formatIndex >= MG_Backend::kFormatCapabilityFormatCount) {
return 0;
}
const auto& sampleCounts =
MG_Backend::pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
return sampleCounts.empty() ? 0 : sampleCounts.front();
}
// The ceiling the frontend enforces, which must never be lower than the one MobileGL
// advertises: the CTS - and real applications - read GL_MAX_SAMPLES once and hand that
// exact count to glTexImage*Multisample for every format. Answering 4 there and then
// rejecting 4 here because the ES driver reports GL_MAX_INTEGER_SAMPLES 1 (Adreno) is a
// self-inconsistency, not a spec-mandated error. The backends clamp the count they hand
// the driver; the shadow state keeps reporting what the application asked for.
Int GetMaxSupportedTextureSamples(TextureTarget textureTarget,
TextureInternalFormat textureInternalFormat) {
if (MG_Backend::pActiveBackendObject == nullptr) { if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::max(); return std::numeric_limits<Int>::max();
} }
const Int advertisedMaxSamples = GetAdvertisedMaxSamples();
// glGetInternalformativ(GL_SAMPLES) is answered from this very list (GetInternalformativ
// below), and GL 4.6 core 8.8 makes that query the definition of the per-format
// maximum - validating against anything else is how the two answers drifted apart.
const Int probedMaxSamples = GetProbedMaxTextureSamples(textureTarget, textureInternalFormat);
if (probedMaxSamples > 0) {
return std::max(probedMaxSamples, advertisedMaxSamples);
}
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters(); const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
if (MG_Util::IsDepthFormatInternalFormat(textureInternalFormat) || if (MG_Util::IsDepthFormatInternalFormat(textureInternalFormat) ||
MG_Util::IsStencilFormatInternalFormat(textureInternalFormat)) { MG_Util::IsStencilFormatInternalFormat(textureInternalFormat)) {
return std::max(dynamicParameters.MaxDepthTextureSamples, 1); return std::max(dynamicParameters.MaxDepthTextureSamples, advertisedMaxSamples);
} }
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat); GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat);
@@ -495,7 +528,7 @@ namespace MobileGL::MG_Impl::GLImpl {
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER; normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
return std::max(isIntegerFormat ? dynamicParameters.MaxIntegerSamples return std::max(isIntegerFormat ? dynamicParameters.MaxIntegerSamples
: dynamicParameters.MaxColorTextureSamples, : dynamicParameters.MaxColorTextureSamples,
1); advertisedMaxSamples);
} }
Bool ValidateTextureMultisampleStorage(TextureTarget textureTarget, GLsizei samples, GLsizei width, Bool ValidateTextureMultisampleStorage(TextureTarget textureTarget, GLsizei samples, GLsizei width,
@@ -532,7 +565,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// dimensions, and GL CTS's per-case state reset (gluStateReset) clears the default // dimensions, and GL CTS's per-case state reset (gluStateReset) clears the default
// GL_TEXTURE_2D_MULTISAMPLE_ARRAY texture with glTexImage3DMultisample(..., 0, 0, 0). // GL_TEXTURE_2D_MULTISAMPLE_ARRAY texture with glTexImage3DMultisample(..., 0, 0, 0).
const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat); const Int maxSamples = GetMaxSupportedTextureSamples(textureTarget, textureInternalFormat);
if (samples > maxSamples) { if (samples > maxSamples) {
// GL specifies INVALID_OPERATION - not INVALID_VALUE - when the sample count // GL specifies INVALID_OPERATION - not INVALID_VALUE - when the sample count
// exceeds what the format supports, and the native Adreno driver agrees. // exceeds what the format supports, and the native Adreno driver agrees.
@@ -557,6 +590,20 @@ namespace MobileGL::MG_Impl::GLImpl {
"AllocateMultisampleTextureStorage requires mipmap-backed storage"); "AllocateMultisampleTextureStorage requires mipmap-backed storage");
auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()); auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
// GL 4.6 core 8.8: a zero-sized image DEALLOCATES the image rather than defining an
// empty one. Only the multisample pair cares, and it cares a great deal: the CTS's
// per-case state reset clears both DEFAULT multisample textures this way on every
// texture unit, and a "defined" 0x0 default texture stops being skipped by
// IsUndefinedDefaultTexture - it then joins the per-draw sync and bind passes on
// every unit the reset touched, and reaches an ES glTexStorage*Multisample(..., 0, 0)
// that ES 3.1 8.19 makes INVALID_VALUE on every driver there is. A proxy target holds
// no image at all, only the query result, so it keeps recording what was asked for.
if ((width <= 0 || height <= 0 || depth <= 0) &&
!TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
textureObject->SetInternalFormat(TextureInternalFormat::Unknown);
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, 0);
return;
}
textureObject->SetInternalFormat(textureInternalFormat); textureObject->SetInternalFormat(textureInternalFormat);
textureObject->SetSamples(samples); textureObject->SetSamples(samples);
textureObject->SetFixedSampleLocations(fixedsamplelocations == GL_TRUE); textureObject->SetFixedSampleLocations(fixedsamplelocations == GL_TRUE);
@@ -614,21 +661,42 @@ namespace MobileGL::MG_Impl::GLImpl {
"Compressed texture formats are not supported.")); "Compressed texture formats are not supported."));
} }
// glGetTexLevelParameter{i,f}v answers WIDTH/HEIGHT/DEPTH out of the mipmap chain. The only // GL_TEXTURE_WIDTH of a buffer texture: how many texels of the texture's internal format fit
// other storage type the state layer knows is GL_TEXTURE_BUFFER (TextureStorageType is // in the buffer range it addresses, CLAMPED to GL_MAX_TEXTURE_BUFFER_SIZE. Attaching a larger
// {Mipmap, Buffer}), whose level geometry this stack does not track yet. Report that instead // buffer is legal (GL 4.6 core 8.9) - the texture simply addresses the first
// of throwing: THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and takes // MAX_TEXTURE_BUFFER_SIZE texels of it, and that clamped count is what WIDTH reports.
// the process down, which is never an acceptable answer to a query - see the same reasoning //
// above for the compressed-format path. // GL_TEXTURE_BUFFER_SIZE is deliberately NOT clamped the same way: it reports the range in
// basic machine units exactly as glTexBuffer/glTexBufferRange were given it. Swapping the two
// fails KHR-GL43.texture_buffer.texture_buffer_max_size in the opposite direction.
GLint GetBufferTextureTexelWidth(const MG_State::GLState::ITextureObject* textureObject) {
const SizeT texelByteSize = MG_Util::GetSizedInternalFormatSizeInBytes(textureObject->GetFormat());
// A format with no known footprint has no texel count to report; answering 0 beats
// dividing by it.
if (texelByteSize == 0) return 0;
const auto* bufferTextureObject =
static_cast<const MG_State::GLState::TextureObjectBuffer*>(textureObject);
const SizeT texelCount = bufferTextureObject->GetBufferRangeSizeInBytes() / texelByteSize;
const SizeT maxTexelCount = static_cast<SizeT>(
std::max(0, MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxTextureBufferSize));
return static_cast<GLint>(std::min(texelCount, maxTexelCount));
}
// glGetTexLevelParameter{i,f}v answers WIDTH/HEIGHT/DEPTH out of the mipmap chain, and (since
// the buffer-texture arms above) out of the attached buffer range for GL_TEXTURE_BUFFER. This
// is what is left: a storage class with no level geometry at all. Report it instead of
// throwing - THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and takes the
// process down, which is never an acceptable answer to a query - see the same reasoning above
// for the compressed-format path.
void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) { void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) {
MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for texture-buffer " MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for this texture's "
"storage; recording GL_INVALID_OPERATION instead of terminating", "storage class; recording GL_INVALID_OPERATION instead of terminating",
caller, MG_Util::ConvertGLEnumToString(pname).c_str()); caller, MG_Util::ConvertGLEnumToString(pname).c_str());
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller, "MG_Impl/GLImpl", caller,
"Level queries are not supported for texture-buffer storage.")); "Level queries are not supported for this texture's storage class."));
} }
} // namespace } // namespace
@@ -644,6 +712,34 @@ namespace MobileGL::MG_Impl::GLImpl {
return textureObject; return textureObject;
} }
// Whether a raw internalformat enum names a compressed format - the question GL asks whenever an
// entry point is forbidden on a compressed image: glTexStorage3D on TEXTURE_3D (no
// block-compressed format is defined for a three-dimensional image, so it is INVALID_OPERATION
// rather than the INVALID_ENUM an unknown sized format gets - GL 4.6 core 8.19 / Khronos bug
// 11239, KHR-GLxx.texture_storage.compressed_data) and the clear-texture pair (8.19 again).
// Written against the enum ranges rather than a name list because the families are contiguous
// and MobileGL's own internal-format enum drops the ones it cannot carry, which would make this
// check silently narrower than the API surface.
static Bool IsCompressedGLInternalFormat(GLenum internalformat) {
switch (internalformat) {
case 0x8225: // GL_COMPRESSED_RED
case 0x8226: // GL_COMPRESSED_RG
case 0x84ED: // GL_COMPRESSED_RGB
case 0x84EE: // GL_COMPRESSED_RGBA
case 0x8C48: // GL_COMPRESSED_SRGB
case 0x8C49: // GL_COMPRESSED_SRGB_ALPHA
return true;
default:
break;
}
return (internalformat >= 0x83F0 && internalformat <= 0x83F3) || // S3TC / DXT
(internalformat >= 0x8DBB && internalformat <= 0x8DBE) || // RGTC
(internalformat >= 0x8E8C && internalformat <= 0x8E8F) || // BPTC
(internalformat >= 0x9270 && internalformat <= 0x9279) || // ETC2 / EAC
(internalformat >= 0x93B0 && internalformat <= 0x93BD) || // ASTC LDR
(internalformat >= 0x93D0 && internalformat <= 0x93DD); // ASTC sRGB
}
namespace { namespace {
void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) { void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -679,6 +775,21 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture level {} is not defined.", level)); std::format("Texture level {} is not defined.", level));
return nullptr; return nullptr;
} }
// GL 4.6 core 8.19: a compressed internal format is INVALID_OPERATION for both clear
// entry points. Two tags to ask, because they answer different questions: the stored
// one covers a level glCompressedTexImage* or a SPECIFIC compressed internalformat
// defined, the requested one covers the six generic GL_COMPRESSED_* enums that MobileGL
// deliberately backs with uncompressed storage (see MipmapStorage) and that would
// otherwise look like an ordinary RGBA8 image by the time the clear runs.
const auto& uploadTargets = mipmapTexture->GetUploadTargets();
if (!uploadTargets.empty() &&
(mipmapTexture->GetMipmapCompressedFormat(uploadTargets[0], static_cast<Uint>(level)) != GL_NONE ||
mipmapTexture->GetMipmapRequestedCompressedFormat(uploadTargets[0], static_cast<Uint>(level)) !=
GL_NONE)) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"Compressed textures cannot be cleared.");
return nullptr;
}
return mipmapTexture; return mipmapTexture;
} }
@@ -2150,6 +2261,26 @@ namespace MobileGL::MG_Impl::GLImpl {
} else { } else {
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level); DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes}); textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
// The same specific-compressed-format tag glTexImage2D records (see TexImage2D_State):
// GL 4.6 core 8.5 commits the level to that format, so GL_TEXTURE_COMPRESSED and
// GL_TEXTURE_INTERNAL_FORMAT must report it - and, less obviously, glCopyImageSubData
// sizes the level's texel BLOCK from it. Without the tag a GL_COMPRESSED_RG_RGTC2
// array level measured as the RG8 storage it resolved to, 2 bytes instead of 16, and
// the copy-compatibility rule refused a pairing 18.3.2 requires. AllocateStorage above
// clears the tag, so this has to follow it.
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(static_cast<GLenum>(internalformat));
if (compressedInfo.blockWidth != 0) {
textureMipmapObject->SetMipmapCompressedImage(
textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr,
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, depth}));
}
// Also after AllocateStorage, which clears it. Records the generic GL_COMPRESSED_*
// enums too, which the tag above deliberately skips - glClearTexImage has to refuse
// them all (GL 4.6 core 8.19).
if (IsCompressedGLInternalFormat(static_cast<GLenum>(internalformat))) {
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
static_cast<GLenum>(internalformat));
}
} }
if (!originalPixels) { if (!originalPixels) {
@@ -2296,6 +2427,13 @@ namespace MobileGL::MG_Impl::GLImpl {
textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr, textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr,
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1})); MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1}));
} }
// Also after AllocateStorage, which clears it. Records the generic GL_COMPRESSED_*
// enums too, which the tag above deliberately skips - glClearTexImage has to refuse
// them all (GL 4.6 core 8.19).
if (IsCompressedGLInternalFormat(static_cast<GLenum>(internalformat))) {
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
static_cast<GLenum>(internalformat));
}
} }
if (!originalPixels) { if (!originalPixels) {
@@ -2384,6 +2522,13 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!isProxy) { if (!isProxy) {
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level); DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes}); textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes});
// After AllocateStorage, which clears the tag. No block-compressed format has a 1D
// layout, so only the specific-format tag the 2D/3D paths record is skipped here - the
// request itself still has to be remembered for glClearTexImage (GL 4.6 core 8.19).
if (IsCompressedGLInternalFormat(static_cast<GLenum>(internalFormat))) {
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
static_cast<GLenum>(internalFormat));
}
} }
if (!originalPixels) { if (!originalPixels) {
@@ -2935,6 +3080,15 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = textureObject->GetSamplerObject()->GetMaxAnisotropy(); *params = textureObject->GetSamplerObject()->GetMaxAnisotropy();
} }
break; break;
// GL 4.6 core 8.11 lists this among the parameters EVERY GetTexParameter form answers.
// It was handled by the iv/Iiv/Iuiv getters and missed by this one, so the float query
// raised GL_INVALID_ENUM and left the caller's float untouched - which is what
// KHR-GL4x.shader_image_load_store.basic-api-texParam reads back.
case GL_IMAGE_FORMAT_COMPATIBILITY_TYPE:
if (params) {
*params = static_cast<GLfloat>(GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE);
}
break;
case GL_DEPTH_STENCIL_TEXTURE_MODE: case GL_DEPTH_STENCIL_TEXTURE_MODE:
if (params) { if (params) {
*params = static_cast<GLfloat>(textureObject->GetDepthStencilTextureMode()); *params = static_cast<GLfloat>(textureObject->GetDepthStencilTextureMode());
@@ -2984,6 +3138,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x(); *params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x();
break; break;
} }
case TextureStorageType::Buffer:
*params = GetBufferTextureTexelWidth(textureObject.get());
break;
default: default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname); RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
break; break;
@@ -2999,6 +3156,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y(); *params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y();
break; break;
} }
case TextureStorageType::Buffer:
*params = 1; // a buffer texture is one-dimensional
break;
default: default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname); RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
break; break;
@@ -3014,6 +3174,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z(); *params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z();
break; break;
} }
case TextureStorageType::Buffer:
*params = 1; // a buffer texture is one-dimensional
break;
default: default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname); RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
break; break;
@@ -3083,6 +3246,31 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
break; break;
} }
case GL_TEXTURE_BUFFER_SIZE:
case GL_TEXTURE_BUFFER_OFFSET: {
// GL 4.6 core 8.9: both describe the window of the attached buffer a GL_TEXTURE_BUFFER
// texture addresses, so there is nothing to report for any other storage - which is
// INVALID_OPERATION, the same shape GL_TEXTURE_COMPRESSED_IMAGE_SIZE guards itself with
// above.
if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "GetTexLevelParameteriv_State",
"GL_TEXTURE_BUFFER_SIZE / GL_TEXTURE_BUFFER_OFFSET need a buffer texture."));
return;
}
if (params) {
const auto* bufferTextureObject =
static_cast<MG_State::GLState::TextureObjectBuffer*>(textureObject.get());
// Basic machine units, and UNCLAMPED - see GetBufferTextureTexelWidth for why this
// half does not take the GL_MAX_TEXTURE_BUFFER_SIZE clamp that WIDTH does.
*params = static_cast<GLint>(pname == GL_TEXTURE_BUFFER_SIZE
? bufferTextureObject->GetBufferRangeSizeInBytes()
: bufferTextureObject->GetBufferRangeOffset());
}
break;
}
default: default:
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameteriv_State", ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameteriv_State",
@@ -3122,6 +3310,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x(); *params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x();
break; break;
} }
case TextureStorageType::Buffer:
*params = (GLfloat)GetBufferTextureTexelWidth(textureObject.get());
break;
default: default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname); RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
break; break;
@@ -3137,6 +3328,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y(); *params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y();
break; break;
} }
case TextureStorageType::Buffer:
*params = 1.0f; // a buffer texture is one-dimensional
break;
default: default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname); RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
break; break;
@@ -3152,6 +3346,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z(); *params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z();
break; break;
} }
case TextureStorageType::Buffer:
*params = 1.0f; // a buffer texture is one-dimensional
break;
default: default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname); RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
break; break;
@@ -3219,6 +3416,27 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
break; break;
} }
case GL_TEXTURE_BUFFER_SIZE:
case GL_TEXTURE_BUFFER_OFFSET: {
// See GetTexLevelParameteriv_State: both describe the attached buffer range of a
// GL_TEXTURE_BUFFER texture, so any other storage makes the query INVALID_OPERATION.
if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "GetTexLevelParameterfv_State",
"GL_TEXTURE_BUFFER_SIZE / GL_TEXTURE_BUFFER_OFFSET need a buffer texture."));
return;
}
if (params) {
const auto* bufferTextureObject =
static_cast<MG_State::GLState::TextureObjectBuffer*>(textureObject.get());
*params = static_cast<GLfloat>(pname == GL_TEXTURE_BUFFER_SIZE
? bufferTextureObject->GetBufferRangeSizeInBytes()
: bufferTextureObject->GetBufferRangeOffset());
}
break;
}
default: default:
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameterfv_State", ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameterfv_State",
@@ -3365,9 +3583,9 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height); MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
} }
void CopyImageSubData_Backend(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, void CopyImageSubData_Backend(const MG_Backend::CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, const MG_Backend::CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
auto copyImageSubData = MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData; auto copyImageSubData = MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData;
@@ -3378,7 +3596,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"Backend does not support image-to-image copies.")); "Backend does not support image-to-image copies."));
return; return;
} }
copyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel, dstX, copyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel, dstX,
dstY, dstZ, srcWidth, srcHeight, srcDepth); dstY, dstZ, srcWidth, srcHeight, srcDepth);
} }
@@ -3425,9 +3643,9 @@ namespace MobileGL::MG_Impl::GLImpl {
// the ~30 entry points that reach it through a BOUND object (where the name was never // the ~30 entry points that reach it through a BOUND object (where the name was never
// in question and the fault is the binding), so this is a local rule rather than a // in question and the fault is the binding), so this is a local rule rather than a
// change to the helper. // change to the helper.
Bool ValidateCopyImageObjectExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Bool ValidateCopyImageObjectExists(const MG_Backend::CopyImageEndpoint& endpoint,
const char* endpointName) { const char* endpointName) {
if (textureObject) return true; if (endpoint.Exists()) return true;
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
@@ -3451,21 +3669,166 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Util::ConvertTextureTargetToString(textureObject->GetTarget())))); MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
return false; return false;
} }
} // namespace
Bool ValidateCopyImageSubData_State(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, // ---- The questions ValidateCopyImageSubData_State asks of one endpoint. ---------------
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, // A renderbuffer answers all of them directly: it has exactly one image, no mip chain and
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, // no sampler state, and it carries its own internal format and extent.
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { Int GetCopyImageEndpointSamples(const MG_Backend::CopyImageEndpoint& endpoint) {
if (!ValidateCopyImageObjectExists(srcTexture, "source") || if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetSamples();
!ValidateCopyImageObjectExists(dstTexture, "destination")) { return endpoint.Texture->GetSamples();
}
TextureInternalFormat GetCopyImageEndpointFormat(const MG_Backend::CopyImageEndpoint& endpoint) {
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetInternalFormat();
return endpoint.Texture->GetFormat();
}
// A renderbuffer has level 0 and nothing else, and the failure is the same INVALID_VALUE
// ValidateTextureLevelExists records for a level a texture does not have.
Bool ValidateCopyImageEndpointLevelExists(const MG_Backend::CopyImageEndpoint& endpoint, GLint level,
const char* caller) {
if (!endpoint.IsRenderbuffer()) {
return TextureImpl::ValidateTextureLevelExists(endpoint.Texture, level, caller);
}
if (level == 0) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "A renderbuffer has only level 0."));
return false; return false;
} }
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
const auto dstTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(dstTarget); // Targets with no mip chain have q == level_base by definition (GL 4.6 core 8.17), so no
if (!TextureImpl::ValidateTextureTarget(srcTextureTarget) || // minification filter can make them mipmap incomplete - while the shared predicate derives
!TextureImpl::ValidateTextureTarget(dstTextureTarget)) { // q from the base level's size alone and would call a 16x16 multisample image incomplete.
Bool CopyImageTargetHasMipmapChain(TextureTarget target) {
switch (target) {
case TextureTarget::TextureRectangle:
case TextureTarget::TextureBuffer:
case TextureTarget::Texture2DMultisample:
case TextureTarget::Texture2DMultisampleArray:
return false;
default:
return true;
}
}
Bool IsCopyImageEndpointComplete(const MG_Backend::CopyImageEndpoint& endpoint) {
// A renderbuffer is complete exactly when it has storage - there is nothing else it
// could be missing.
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->IsAllocated();
const auto* texture = endpoint.Texture.get();
if (!texture) return false;
// 18.3.2 asks for TEXTURE completeness, which GL 4.6 core 8.17 defines to include the
// MIP CHAIN whenever the minification filter samples it - and ITextureObject::
// IsComplete() only answers the storage half (an internal format, and no zero-size
// level in the middle of the chain). A texture with level 0 alone and the default
// NEAREST_MIPMAP_LINEAR filter is incomplete, which is exactly how
// KHR-GL43.copy_image.incomplete_tex builds its subject.
//
// The filter is the texture's OWN: copy-image never goes through a texture unit, so no
// sampler object is in play. An immutable texture is unaffected - glTexStorage clamps
// TEXTURE_MAX_LEVEL to levels-1, which is what makes a single-level immutable texture
// mipmap complete under any filter.
const auto& sampler = texture->GetSamplerObject();
const Bool mipmapped = CopyImageTargetHasMipmapChain(texture->GetTarget()) && sampler &&
sampler->GetMipmapMode() != SamplerMipmapMode::None;
return MG_State::GLState::IsMipmapCompleteForFilter(texture, mipmapped);
}
GLenum GetCopyImageEndpointCompressedFormat(const MG_Backend::CopyImageEndpoint& endpoint,
TextureUploadTarget uploadTarget, GLint level) {
if (endpoint.IsRenderbuffer()) return GL_NONE;
return GetCompressedLevelFormat(endpoint.Texture, uploadTarget, level);
}
IntVec3 GetCopyImageEndpointLevelSize(const MG_Backend::CopyImageEndpoint& endpoint,
TextureUploadTarget uploadTarget, GLint level) {
if (endpoint.IsRenderbuffer()) {
return {endpoint.Renderbuffer->GetWidth(), endpoint.Renderbuffer->GetHeight(), 1};
}
return GetCopyImageLevelSize(endpoint.Texture, uploadTarget, level);
}
// The per-axis extent of one endpoint's image AS THIS ENTRY POINT ADDRESSES IT, which is
// not always the level extent this frontend stores.
//
// GL 4.6 core 18.3.2 treats EVERY array texture as a stack of slices addressed by z, and
// gives a 1D array an image height of 1. This frontend stores a 1D array the way
// glTexImage2D(GL_TEXTURE_1D_ARRAY, w, layers) writes it instead - layers on y - so the
// two views have to be told apart here. Measuring y against the LAYER count is what let
// srcY = 14 on a 16-wide, 16-layer 1D array come back GL_NO_ERROR
// (KHR-GL43.copy_image.exceeding_boundaries, the src_test_case y variants); the CTS is
// unambiguous about the convention, forcing height = 1 for 1D and 1D_ARRAY and listing
// 1D_ARRAY as multilayer.
//
// A CUBE MAP is the other target whose z bound is not the level extent: this frontend
// keeps its six faces as six separate one-slice upload targets, so the level says 1 and
// the real bound is 6. A cube-map ARRAY is one upload target whose depth already counts
// layer-faces, and every remaining target is answered by the level extent verbatim.
IntVec3 GetCopyImageEndpointRegionBounds(const MG_Backend::CopyImageEndpoint& endpoint,
const IntVec3& levelSize) {
const TextureTarget target = (!endpoint.IsRenderbuffer() && endpoint.Texture)
? endpoint.Texture->GetTarget()
: TextureTarget::Unknown;
if (target == TextureTarget::TextureCubeMap) {
return {levelSize.x(), levelSize.y(), 6};
}
if (target == TextureTarget::Texture1DArray) {
return {levelSize.x(), 1, std::max(levelSize.y(), 1)};
}
return {levelSize.x(), levelSize.y(), std::max(levelSize.z(), 1)};
}
// GL 4.6 core 18.3.2 requires INVALID_VALUE when the region exceeds either image's
// boundaries. The only bounds-shaped call this validator used to make was
// ValidateCopyImageBlockAlignment, whose first line returns true for every UNCOMPRESSED
// format - so no uncompressed copy was bounded at all, and the z extent could not be
// bounded even in principle because srcZ/dstZ never reached the validator. Texture
// endpoints were covered only by accident, through the ES driver's own error, which the
// DirectGLES backend logs and swallows rather than reporting; a GL_RENDERBUFFER endpoint
// got neither (KHR-GL43.copy_image.exceeding_boundaries).
Bool ValidateCopyImageRegionBounds(const MG_Backend::CopyImageEndpoint& endpoint, const IntVec3& levelSize,
GLint x, GLint y, GLint z, GLsizei width, GLsizei height, GLsizei depth,
const char* endpointName) {
// An extent this frontend does not know cannot bound anything, and guessing would
// reject a copy GL allows. Every caller has already established that the level
// exists and that the image is complete, so this is a belt-and-braces guard.
if (levelSize.x() <= 0 || levelSize.y() <= 0) return true;
const IntVec3 bounds = GetCopyImageEndpointRegionBounds(endpoint, levelSize);
if (x >= 0 && y >= 0 && z >= 0 && static_cast<Int64>(x) + width <= bounds.x() &&
static_cast<Int64>(y) + height <= bounds.y() && static_cast<Int64>(z) + depth <= bounds.z()) {
return true;
}
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
std::format("The {} region [{}, {}, {}] + [{} x {} x {}] does not fit inside the {} x {} x {} "
"image.",
endpointName, x, y, z, width, height, depth, bounds.x(), bounds.y(), bounds.z())));
return false;
}
} // namespace
Bool ValidateCopyImageSubData_State(const MG_Backend::CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const MG_Backend::CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
if (!ValidateCopyImageObjectExists(src, "source") ||
!ValidateCopyImageObjectExists(dst, "destination")) {
return false;
}
// GL_RENDERBUFFER has no TextureTarget to convert to, and it needs none: it is its own
// whole-image target, and the endpoint that carries it was resolved from the renderbuffer
// namespace, so it matches its object by construction.
const auto srcTextureTarget =
src.IsRenderbuffer() ? TextureTarget::Unknown : MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
const auto dstTextureTarget =
dst.IsRenderbuffer() ? TextureTarget::Unknown : MG_Util::ConvertGLEnumToTextureTarget(dstTarget);
if ((!src.IsRenderbuffer() && !TextureImpl::ValidateTextureTarget(srcTextureTarget)) ||
(!dst.IsRenderbuffer() && !TextureImpl::ValidateTextureTarget(dstTextureTarget))) {
return false; return false;
} }
// GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but // GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but
@@ -3473,8 +3836,8 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) { if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) {
return false; return false;
} }
if (!ValidateCopyImageTargetMatchesObject(srcTexture, srcTextureTarget, "source") || if (!ValidateCopyImageTargetMatchesObject(src.Texture, srcTextureTarget, "source") ||
!ValidateCopyImageTargetMatchesObject(dstTexture, dstTextureTarget, "destination")) { !ValidateCopyImageTargetMatchesObject(dst.Texture, dstTextureTarget, "destination")) {
return false; return false;
} }
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) || if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
@@ -3488,8 +3851,8 @@ namespace MobileGL::MG_Impl::GLImpl {
// driver as an out-of-range mip index - on Adreno that is a SIGSEGV inside // driver as an out-of-range mip index - on Adreno that is a SIGSEGV inside
// vkCmdCopyImage, which is what KHR-GL43.copy_image.non_existent_mipmap used to do to // vkCmdCopyImage, which is what KHR-GL43.copy_image.non_existent_mipmap used to do to
// the whole glcts process. The answer the spec asks for is GL_INVALID_VALUE. // the whole glcts process. The answer the spec asks for is GL_INVALID_VALUE.
if (!TextureImpl::ValidateTextureLevelExists(srcTexture, srcLevel, __func__) || if (!ValidateCopyImageEndpointLevelExists(src, srcLevel, __func__) ||
!TextureImpl::ValidateTextureLevelExists(dstTexture, dstLevel, __func__)) { !ValidateCopyImageEndpointLevelExists(dst, dstLevel, __func__)) {
return false; return false;
} }
if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) { if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) {
@@ -3505,43 +3868,55 @@ namespace MobileGL::MG_Impl::GLImpl {
// A multisample image can only be copied to one with the same sample count, and a // A multisample image can only be copied to one with the same sample count, and a
// single-sample image reports zero - so this one comparison is also what rejects // single-sample image reports zero - so this one comparison is also what rejects
// copying between a multisample target and a non-multisample one. // copying between a multisample target and a non-multisample one.
if (srcTexture->GetSamples() != dstTexture->GetSamples()) { const Int srcSamples = GetCopyImageEndpointSamples(src);
const Int dstSamples = GetCopyImageEndpointSamples(dst);
if (srcSamples != dstSamples) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__, "MG_Impl/GLImpl", __func__,
std::format("The two images have different sample counts ({} vs. {}).", std::format("The two images have different sample counts ({} vs. {}).",
srcTexture->GetSamples(), dstTexture->GetSamples()))); srcSamples, dstSamples)));
return false; return false;
} }
// 18.3.2: both images must be complete. An incomplete one has no defined texels to copy // 18.3.2: both images must be complete. An incomplete one has no defined texels to copy
// and no defined storage to copy into. // and no defined storage to copy into.
if (!srcTexture->IsComplete() || !dstTexture->IsComplete()) { const Bool srcComplete = IsCopyImageEndpointComplete(src);
const Bool dstComplete = IsCopyImageEndpointComplete(dst);
if (!srcComplete || !dstComplete) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__, "MG_Impl/GLImpl", __func__,
std::format("A copied image is incomplete (source complete: {}, destination complete: {}).", std::format("A copied image is incomplete (source complete: {}, destination complete: {}).",
srcTexture->IsComplete(), dstTexture->IsComplete()))); srcComplete, dstComplete)));
return false; return false;
} }
const auto srcUploadTarget = GetPrimaryUploadTarget(srcTexture); const auto srcUploadTarget = GetPrimaryUploadTarget(src.Texture);
const auto dstUploadTarget = GetPrimaryUploadTarget(dstTexture); const auto dstUploadTarget = GetPrimaryUploadTarget(dst.Texture);
const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock( const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock(
srcTexture->GetFormat(), GetCompressedLevelFormat(srcTexture, srcUploadTarget, srcLevel)); GetCopyImageEndpointFormat(src), GetCopyImageEndpointCompressedFormat(src, srcUploadTarget, srcLevel));
const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock( const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock(
dstTexture->GetFormat(), GetCompressedLevelFormat(dstTexture, dstUploadTarget, dstLevel)); GetCopyImageEndpointFormat(dst), GetCopyImageEndpointCompressedFormat(dst, dstUploadTarget, dstLevel));
if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) { if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) {
return false; return false;
} }
const IntVec3 srcLevelSize = GetCopyImageLevelSize(srcTexture, srcUploadTarget, srcLevel); const IntVec3 srcLevelSize = GetCopyImageEndpointLevelSize(src, srcUploadTarget, srcLevel);
const IntVec3 dstLevelSize = GetCopyImageLevelSize(dstTexture, dstUploadTarget, dstLevel); const IntVec3 dstLevelSize = GetCopyImageEndpointLevelSize(dst, dstUploadTarget, dstLevel);
if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight, if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight,
srcLevelSize.x(), srcLevelSize.y(), "source") || srcLevelSize.x(), srcLevelSize.y(), "source") ||
!TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight, !TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight,
dstLevelSize.x(), dstLevelSize.y(), "destination")) { dstLevelSize.x(), dstLevelSize.y(), "destination")) {
return false; return false;
} }
// One region extent, measured against both images: GL 4.6 core 18.3.2 gives the copy a
// single width/height/depth and requires it to fit in the source AND the destination.
if (!ValidateCopyImageRegionBounds(src, srcLevelSize, srcX, srcY, srcZ, srcWidth, srcHeight, srcDepth,
"source") ||
!ValidateCopyImageRegionBounds(dst, dstLevelSize, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth,
"destination")) {
return false;
}
return true; return true;
} }
@@ -4051,8 +4426,14 @@ namespace MobileGL::MG_Impl::GLImpl {
return false; return false;
} }
// For a cube map this is exactly cube completeness: IsComplete() wants all six faces. // GL 4.6 core 8.11.4 names cube completeness as the only completeness a readback requires,
if (!textureObject->IsComplete()) { // and for a cube map that is exactly what IsComplete() answers (all six faces defined at
// every level). It must not speak for any other target: on a mip chain it also rejects
// "level N defined, the levels below it not", which is a perfectly readable texture at
// level N - and the shape glClearTexImage's conformance cases build, since they define
// only the level they clear. The requested level's own existence is checked below.
if ((target == TextureTarget::TextureCubeMap || target == TextureTarget::TextureCubeMapArray) &&
!textureObject->IsComplete()) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture is incomplete")); MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture is incomplete"));
@@ -4084,8 +4465,8 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
// Shared format/type/internal-format matrix (packed-type pairing, depth-vs-color mismatch, // Shared format/type/internal-format matrix (packed-type pairing, depth-vs-color mismatch,
// integer-ness). Also rejects STENCIL_INDEX readback, which needs GL_ARB_texture_stencil8 // integer-ness). Also rejects a STENCIL_INDEX readback of anything but stencil-only
// (not advertised by MobileGL). // storage, which is the only pairing GL 4.4 / ARB_texture_stencil8 ever made legal.
if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput( if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(
textureInputFormat, textureObject->GetFormat(), texturePixelDataType)) { textureInputFormat, textureObject->GetFormat(), texturePixelDataType)) {
return false; return false;
@@ -4111,33 +4492,48 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto* textureMipmapObject = const auto* textureMipmapObject =
static_cast<const MG_State::GLState::TextureObjectMipmap*>(textureObject.get()); static_cast<const MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
const auto& uploadTargets = textureObject->GetUploadTargets(); const auto& uploadTargets = textureObject->GetUploadTargets();
if (!uploadTargets.empty() && static_cast<Uint>(level) < textureMipmapObject->GetMipmapLevelCount()) { // The half of the completeness gate above that GL does keep: the REQUESTED level has
// Tightly packed, and summed over every face because a cube map query returns all // to hold an image. A name that was never given one carries no levels at all (which is
// six. Pack pixel-store state only ever grows this, so a request rejected here // also what an Unknown internal format answers), and a chain grown to reach level N
// could not have fit under any packing. // leaves every level below it at {0, 0, 0}.
const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTargets[0], level); if (uploadTargets.empty() || static_cast<Uint>(level) >= textureMipmapObject->GetMipmapLevelCount()) {
const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat, MG_State::pGLContext->RecordError(
texturePixelDataType, texelSize) * ErrorCode::InvalidOperation,
uploadTargets.size(); MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture level has no image to read back."));
return false;
}
const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTargets[0], level);
if (texelSize.x() <= 0 || texelSize.y() <= 0 || texelSize.z() <= 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture level has no image to read back."));
return false;
}
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < required) { // Tightly packed, and summed over every face because a cube map query returns all
// six. Pack pixel-store state only ever grows this, so a request rejected here
// could not have fit under any packing.
const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat,
texturePixelDataType, texelSize) *
uploadTargets.size();
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < required) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Destination buffer is too small."));
return false;
}
if (pixelPackBufferObject) {
const SizeT bufferSize = pixelPackBufferObject->GetSize();
const SizeT offset = reinterpret_cast<SizeT>(pixels);
if (offset > bufferSize || required > bufferSize - offset) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Destination buffer is too small.")); MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Packing would write past the end of the pixel pack buffer."));
return false; return false;
} }
if (pixelPackBufferObject) {
const SizeT bufferSize = pixelPackBufferObject->GetSize();
const SizeT offset = reinterpret_cast<SizeT>(pixels);
if (offset > bufferSize || required > bufferSize - offset) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Packing would write past the end of the pixel pack buffer."));
return false;
}
}
} }
} }
@@ -4372,6 +4768,12 @@ namespace MobileGL::MG_Impl::GLImpl {
const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, 1, 1); const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, 1, 1);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize}); textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize});
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false); textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
if (IsCompressedGLInternalFormat(internalformat)) {
// After AllocateStorage, which clears the tag. See TexImage1D_State: no compressed
// format has a 1D block layout, but glClearTexImage still has to refuse the request.
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget,
static_cast<Uint>(level), internalformat);
}
} }
// Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a // Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a
// longer pre-existing chain has to be dropped explicitly. // longer pre-existing chain has to be dropped explicitly.
@@ -4440,6 +4842,12 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
{levelWidth, levelHeight, 1})); {levelWidth, levelHeight, 1}));
} }
if (IsCompressedGLInternalFormat(internalformat)) {
// Also after AllocateStorage. The generic enums land here and nowhere above,
// and glClearTexImage has to refuse them too (GL 4.6 core 8.19).
textureMipmapObject->SetMipmapRequestedCompressedFormat(uploadTarget,
static_cast<Uint>(level), internalformat);
}
} }
// See TextureStorage1D. // See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels)); textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
@@ -4447,32 +4855,6 @@ namespace MobileGL::MG_Impl::GLImpl {
textureObject->SetImmutableLevels(static_cast<Uint>(levels)); textureObject->SetImmutableLevels(static_cast<Uint>(levels));
} }
// No block-compressed format is defined for a three-dimensional image, so glTexStorage3D on
// TEXTURE_3D must reject one - and with INVALID_OPERATION, not the INVALID_ENUM an unknown
// sized format gets (GL 4.6 core 8.19 / Khronos bug 11239, KHR-GLxx.texture_storage
// .compressed_data). Written against the enum ranges rather than a name list because the
// families are contiguous and MobileGL's own internal-format enum drops the ones it cannot
// carry, which would make this check silently narrower than the API surface.
static Bool IsCompressedGLInternalFormat(GLenum internalformat) {
switch (internalformat) {
case 0x8225: // GL_COMPRESSED_RED
case 0x8226: // GL_COMPRESSED_RG
case 0x84ED: // GL_COMPRESSED_RGB
case 0x84EE: // GL_COMPRESSED_RGBA
case 0x8C48: // GL_COMPRESSED_SRGB
case 0x8C49: // GL_COMPRESSED_SRGB_ALPHA
return true;
default:
break;
}
return (internalformat >= 0x83F0 && internalformat <= 0x83F3) || // S3TC / DXT
(internalformat >= 0x8DBB && internalformat <= 0x8DBE) || // RGTC
(internalformat >= 0x8E8C && internalformat <= 0x8E8F) || // BPTC
(internalformat >= 0x9270 && internalformat <= 0x9279) || // ETC2 / EAC
(internalformat >= 0x93B0 && internalformat <= 0x93BD) || // ASTC LDR
(internalformat >= 0x93D0 && internalformat <= 0x93DD); // ASTC sRGB
}
void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
GLsizei depth) { GLsizei depth) {
auto textureObject = GetTextureObjectByName(texture, __func__); auto textureObject = GetTextureObjectByName(texture, __func__);
@@ -4515,6 +4897,10 @@ namespace MobileGL::MG_Impl::GLImpl {
// Array targets keep their layer count constant across levels; only true 3D // Array targets keep their layer count constant across levels; only true 3D
// textures halve depth per level (GL 3.3 §3.9 glTexStorage3D). // textures halve depth per level (GL 3.3 §3.9 glTexStorage3D).
const Bool depthMips = DepthParticipatesInMipmapping(textureObject->GetTarget()); const Bool depthMips = DepthParticipatesInMipmapping(textureObject->GetTarget());
// The same specific-compressed-format tag glTexStorage2D records, for the array targets a
// compressed glTexStorage3D is legal on (GL_TEXTURE_3D was refused above). Zero width means
// a generic format, which MobileGL answers with uncompressed storage, so it is not tagged.
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat);
for (GLsizei level = 0; level < levels; ++level) { for (GLsizei level = 0; level < levels; ++level) {
const GLsizei levelWidth = std::max<GLsizei>(1, width >> level); const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
const GLsizei levelHeight = std::max<GLsizei>(1, height >> level); const GLsizei levelHeight = std::max<GLsizei>(1, height >> level);
@@ -4524,6 +4910,19 @@ namespace MobileGL::MG_Impl::GLImpl {
textureMipmapObject->AllocateStorage(textureUploadTarget, level, textureMipmapObject->AllocateStorage(textureUploadTarget, level,
{{levelWidth, levelHeight, levelDepth}, byteSize}); {{levelWidth, levelHeight, levelDepth}, byteSize});
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false); textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
if (compressedInfo.blockWidth != 0) {
// After AllocateStorage, which clears the tag.
textureMipmapObject->SetMipmapCompressedImage(
textureUploadTarget, static_cast<Uint>(level), internalformat, nullptr,
MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
{levelWidth, levelHeight, levelDepth}));
}
if (IsCompressedGLInternalFormat(internalformat)) {
// Also after AllocateStorage. The generic enums land here and nowhere above,
// and glClearTexImage has to refuse them too (GL 4.6 core 8.19).
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget,
static_cast<Uint>(level), internalformat);
}
} }
// See TextureStorage1D. // See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels)); textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
@@ -4684,6 +5083,22 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureStorage3D(textureObject->GetExternalIndex(), levels, internalformat, width, height, depth); TextureStorage3D(textureObject->GetExternalIndex(), levels, internalformat, width, height, depth);
} }
// Unlike glTexImage*Multisample, where a zero-sized image is a legal deallocation (see
// AllocateMultisampleTextureStorage), the immutable forms take a strictly positive size: GL
// 4.6 core 8.19 makes width, height or depth < 1 INVALID_VALUE. Without this the shared
// _State helper would deallocate the image and TexStorageMultisample_State would then freeze
// the now-imageless texture as immutable.
static Bool ValidateTexStorageMultisampleSize(GLsizei width, GLsizei height, GLsizei depth, const char* caller) {
if (width >= 1 && height >= 1 && depth >= 1) {
return true;
}
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Immutable multisample storage requires width, height and depth >= 1."));
return false;
}
// The multisample storage forms allocate exactly what the glTexImage*Multisample ones do, and // The multisample storage forms allocate exactly what the glTexImage*Multisample ones do, and
// then freeze it: TEXTURE_IMMUTABLE_FORMAT becomes TRUE and a second call is INVALID_OPERATION // then freeze it: TEXTURE_IMMUTABLE_FORMAT becomes TRUE and a second call is INVALID_OPERATION
// (GL 4.6 core 8.19). Only the allocation was shared before, so a multisample texture stayed // (GL 4.6 core 8.19). Only the allocation was shared before, so a multisample texture stayed
@@ -4704,6 +5119,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); const TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()); auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
if (!ValidateTextureMutable(activeUnit.GetBindingSlot(textureTarget).GetBoundObject(), __func__)) return; if (!ValidateTextureMutable(activeUnit.GetBindingSlot(textureTarget).GetBoundObject(), __func__)) return;
if (!ValidateTexStorageMultisampleSize(width, height, 1, __func__)) return;
TexStorageMultisample_State( TexStorageMultisample_State(
target, TexImage2DMultisample_State(target, samples, internalformat, width, height, fixedsamplelocations), target, TexImage2DMultisample_State(target, samples, internalformat, width, height, fixedsamplelocations),
__func__); __func__);
@@ -4714,6 +5130,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); const TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()); auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
if (!ValidateTextureMutable(activeUnit.GetBindingSlot(textureTarget).GetBoundObject(), __func__)) return; if (!ValidateTextureMutable(activeUnit.GetBindingSlot(textureTarget).GetBoundObject(), __func__)) return;
if (!ValidateTexStorageMultisampleSize(width, height, depth, __func__)) return;
TexStorageMultisample_State(target, TexStorageMultisample_State(target,
TexImage3DMultisample_State(target, samples, internalformat, width, height, depth, TexImage3DMultisample_State(target, samples, internalformat, width, height, depth,
fixedsamplelocations), fixedsamplelocations),
@@ -5715,17 +6132,29 @@ namespace MobileGL::MG_Impl::GLImpl {
GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
// A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is // A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is
// INVALID_OPERATION - so resolve through the plain lookup, which answers a null // INVALID_OPERATION - so resolve through the plain lookups, which answer a null
// SharedPtr, and let the validator record the error this entry point owes. // SharedPtr, and let the validator record the error this entry point owes.
const SharedPtr<MG_State::GLState::ITextureObject> srcTexture = //
MG_State::pGLContext->GetTextureObject(srcName); // The TARGET picks the namespace: GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER, and a
const SharedPtr<MG_State::GLState::ITextureObject> dstTexture = // renderbuffer name has nothing to do with a texture name. Resolving both through
MG_State::pGLContext->GetTextureObject(dstName); // GetTextureObject made every renderbuffer endpoint INVALID_VALUE - or, when the number
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, srcX, srcY, dstTexture, dstTarget, // happened to collide with a live texture, INVALID_ENUM from the target check.
dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) { const auto resolveEndpoint = [](GLuint name, GLenum target) {
MG_Backend::CopyImageEndpoint endpoint{};
if (target == GL_RENDERBUFFER) {
endpoint.Renderbuffer = MG_State::pGLContext->GetRenderbufferObject(name);
} else {
endpoint.Texture = MG_State::pGLContext->GetTextureObject(name);
}
return endpoint;
};
const MG_Backend::CopyImageEndpoint src = resolveEndpoint(srcName, srcTarget);
const MG_Backend::CopyImageEndpoint dst = resolveEndpoint(dstName, dstTarget);
if (!ValidateCopyImageSubData_State(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget,
dstLevel, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth)) {
return; return;
} }
CopyImageSubData_Backend(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel, CopyImageSubData_Backend(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel,
dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth); dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth);
} }
@@ -313,9 +313,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return false; return false;
} }
// TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4). // The stencil-only transfer path arrived with GL 4.4 / ARB_texture_stencil8, and only ever
if (format == TextureInputFormat::StencilIndex) { // pairs with stencil-only storage: against a depth, depth-stencil or colour internal format
return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format"); // STENCIL_INDEX keeps the pre-4.4 answer (GL CTS packed_pixels feeds exactly that pairing
// and expects INVALID_OPERATION).
if (format == TextureInputFormat::StencilIndex &&
internalFormat != TextureInternalFormat::StencilIndex8) {
return recordInvalidOperation("STENCIL_INDEX requires a stencil-only internal format");
} }
if (IsDepthLikeInputFormat(format) != IsDepthLikeInternalFormat(internalFormat)) { if (IsDepthLikeInputFormat(format) != IsDepthLikeInternalFormat(internalFormat)) {
@@ -514,10 +514,17 @@ namespace MobileGL::MG_Impl::GLImpl {
// recorded DataType is always Float64 - what IsLong adds is that this is the *unconverted* form, // recorded DataType is always Float64 - what IsLong adds is that this is the *unconverted* form,
// as opposed to VertexAttribFormat(GL_DOUBLE), which asks for a float conversion. // as opposed to VertexAttribFormat(GL_DOUBLE), which asks for a float conversion.
// //
// Whether the backend can feed it is detected, not assumed: DirectVulkan needs shaderFloat64, // Whether the backend can FEED it at full precision is detected, not assumed: DirectVulkan
// and DirectGLES can never have it at all. A backend without it declines here, loudly - GL error // needs shaderFloat64, and DirectGLES can never have it at all. What that costs is PRECISION,
// plus a log line naming the reason - rather than accepting state no draw could honour and // not the call and no longer the array: GL 4.6 core 10.3.2 defines no error for a well-formed
// rendering garbage. The matching startup POST row is in MG_Util/SelfTest/DriverPost.cpp. // glVertexAttribLFormat, and a GL 4.3 context has 64-bit attributes in core, so declining the
// call would be non-conformant and would make the four pure state queries
// (VERTEX_ATTRIB_ARRAY_SIZE / _TYPE / _LONG / _RELATIVE_OFFSET) unanswerable
// (KHR-GL43.vertex_attrib_binding.basic-state1/3). The format is therefore RECORDED here and
// the array is NARROWED to float32 at draw, matching the fp64 demotion every shader already
// gets (DemoteFloat64Pass) - loudly, once, naming the cost. The matching startup POST row is in
// MG_Util/SelfTest/DriverPost.cpp; the draw-side narrowing is DirectGLES/Managers.cpp and, on
// DirectVulkan, VertexInputStateFactory's Float64 case.
static void VertexAttribLFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao, static void VertexAttribLFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
GLuint attribindex, GLint size, GLenum type, GLuint attribindex, GLint size, GLenum type,
GLuint relativeoffset) { GLuint relativeoffset) {
@@ -528,14 +535,11 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!MG_Backend::pActiveBackendObject || if (!MG_Backend::pActiveBackendObject ||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) { !MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
MGLOG_W_ONCE("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this " MGLOG_W_ONCE("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
"backend has no double-precision vertex attribute support - see the " "backend has no double-precision vertex attribute support - the format is recorded "
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs", "and queryable, and the array is FETCHED AT FLOAT32 PRECISION at draw (the same "
"narrowing the shader's dvec inputs already get); see the \"64-bit vertex "
"attributes\" / \"shaderFloat64\" POST row for what that costs",
attribindex); attribindex);
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "VertexAttribLFormat",
"64-bit vertex attributes are not supported by this backend."));
return;
} }
vao->SetAttributeFormatSeparate(attribindex, size, MG_Util::ConvertGLEnumToDataType(type), vao->SetAttributeFormatSeparate(attribindex, size, MG_Util::ConvertGLEnumToDataType(type),
@@ -59,6 +59,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/AsyncCompileScenario.cpp Scenarios/AsyncCompileScenario.cpp
Scenarios/XfbAfterClipDistanceScenario.cpp Scenarios/XfbAfterClipDistanceScenario.cpp
Scenarios/ThreeChannelAttachmentScenario.cpp Scenarios/ThreeChannelAttachmentScenario.cpp
Scenarios/SnormAttachmentScenario.cpp
Scenarios/PipelineFailureScenario.cpp Scenarios/PipelineFailureScenario.cpp
Scenarios/AdvertisedLimitsScenario.cpp Scenarios/AdvertisedLimitsScenario.cpp
Scenarios/PixelStoreSweepScenario.cpp Scenarios/PixelStoreSweepScenario.cpp
@@ -80,16 +81,25 @@ add_executable(MobileGLIntegrationTest
Scenarios/ImageLoadStoreSsoScenario.cpp Scenarios/ImageLoadStoreSsoScenario.cpp
Scenarios/ImageTargetKindScenario.cpp Scenarios/ImageTargetKindScenario.cpp
Scenarios/ImageFormatQualifierScenario.cpp Scenarios/ImageFormatQualifierScenario.cpp
Scenarios/ImageSizeAfterRespecScenario.cpp
Scenarios/SsboDeclarationFormScenario.cpp Scenarios/SsboDeclarationFormScenario.cpp
Scenarios/Glsl420DeclarationScenario.cpp Scenarios/Glsl420DeclarationScenario.cpp
Scenarios/IoBlockNameCollisionScenario.cpp
Scenarios/TessellationDrawModeScenario.cpp
Scenarios/FragmentOutputArrayIndexScenario.cpp Scenarios/FragmentOutputArrayIndexScenario.cpp
Scenarios/BufferTextureScenario.cpp Scenarios/BufferTextureScenario.cpp
Scenarios/VertexAttribBindingScenario.cpp Scenarios/VertexAttribBindingScenario.cpp
Scenarios/XfbCaptureBufferReuseScenario.cpp Scenarios/XfbCaptureBufferReuseScenario.cpp
Scenarios/XfbPrimitiveQueryScenario.cpp
Scenarios/VertexArrayEnableDisableScenario.cpp Scenarios/VertexArrayEnableDisableScenario.cpp
Scenarios/CopyImageLevelRangeScenario.cpp Scenarios/CopyImageLevelRangeScenario.cpp
Scenarios/CopyImageLayeredScenario.cpp Scenarios/CopyImageLayeredScenario.cpp
Scenarios/PackedWordReadbackScenario.cpp
Scenarios/LayeredAttachmentBarrierScenario.cpp Scenarios/LayeredAttachmentBarrierScenario.cpp
Scenarios/LayeredTextureReadbackScenario.cpp
Scenarios/AtomicCounterScenario.cpp
Scenarios/SsboArrayDynamicIndexScenario.cpp
Scenarios/StorageBufferRegrowScenario.cpp
) )
target_include_directories(MobileGLIntegrationTest PRIVATE target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -246,6 +256,19 @@ endif()
set(MGL_ITEST_VULKAN_ENV ${MGL_ITEST_COMMON_ENV}) set(MGL_ITEST_VULKAN_ENV ${MGL_ITEST_COMMON_ENV})
if (MOBILEGL_ITEST_VK_ICD) if (MOBILEGL_ITEST_VK_ICD)
list(APPEND MGL_ITEST_VULKAN_ENV "VK_ICD_FILENAMES=${MOBILEGL_ITEST_VK_ICD}") list(APPEND MGL_ITEST_VULKAN_ENV "VK_ICD_FILENAMES=${MOBILEGL_ITEST_VK_ICD}")
# The three iterationRP repairs are tri-state quirks that default to device
# auto-detection, and lavapipe is not on any auto list - so on lavapipe the
# iterationRP scenarios run unrepaired and Program 203 misses its golden
# output. CI's integration-gpu job exports these three by hand; pinning them
# to the ICD instead means a local `ctest -L integration-gpu` measures the
# same thing the gate does, with no environment to remember.
if (MOBILEGL_ITEST_VK_ICD MATCHES "lvp_icd|lavapipe")
message(STATUS "Integration tests: lavapipe ICD - forcing the iterationRP repairs on")
list(APPEND MGL_ITEST_VULKAN_ENV
"MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1"
"MOBILEGL_DERIVE_NUM_SUBGROUPS=1"
"MOBILEGL_ITERATIONRP_FIX_BARRIER=1")
endif()
endif() endif()
# The ENVIRONMENT test property is itself a `;`-list, and gtest_discover_tests # The ENVIRONMENT test property is itself a `;`-list, and gtest_discover_tests
@@ -0,0 +1,239 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - ATOMIC COUNTERS, END TO END.
//
// GL_ATOMIC_COUNTER_BUFFER does not exist in ES, and glslang does not hand one to a backend
// either: its Vulkan-relaxed parse rewrites every atomic_uint into a uint member of a
// synthesized gl_AtomicCounterBlock_<N> STORAGE block. Making counters work therefore means
// closing two open ends that used to be missing entirely -
//
// * the block's shader-storage binding, which the IO mapper picked at random and which had no
// relation to the GL binding point N the application bound its buffer to (and could alias an
// SSBO the application binds itself), is moved to a slot reserved at the top of the driver's
// range; and
// * the buffer bound at GL_ATOMIC_COUNTER_BUFFER point N, which nothing in the ES backend ever
// read, is re-issued as a shader-storage binding at that reserved slot.
//
// Neither end alone is observable: with only the first the shader increments a block nobody
// bound a buffer to, with only the second the buffer lands where the shader does not look. The
// only thing that proves both is the VALUE, so every assertion here reads the counter back.
//
// Compute rather than a draw on purpose: the invocation count is exactly what was dispatched,
// while a fragment stage's is a property of the rasterizer (helper invocations, early depth).
// Conformance cases behind this: KHR-GL42/GL43.shader_atomic_counters.basic-usage-cs,
// .advanced-usage-multi-stage and .advanced-usage-draw-update-draw.
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// Two counters share binding 0 at DIFFERENT offsets and a third sits alone on binding 1.
// The offsets are what separates "the buffer arrived" from "the buffer arrived and the
// block is laid out the way GL says": a lowering that packed the members in declaration
// order without honouring `offset` would still pass a single-counter check.
constexpr const char* kCounterComputeSource = R"(#version 430 core
layout(local_size_x = 4) in;
layout(binding = 0, offset = 0) uniform atomic_uint g_first;
layout(binding = 0, offset = 4) uniform atomic_uint g_second;
layout(binding = 1, offset = 0) uniform atomic_uint g_other;
void main() {
atomicCounterIncrement(g_first);
atomicCounterIncrement(g_second);
atomicCounterIncrement(g_second);
atomicCounterIncrement(g_other);
}
)";
constexpr int kLocalSizeX = 4;
constexpr int kWorkGroups = 2;
constexpr unsigned int kInvocations = kLocalSizeX * kWorkGroups;
// Deliberately non-zero: the shader adds to whatever the application uploaded, so a seed
// that survives is also proof that the buffer's CPU-side contents reached the driver.
constexpr unsigned int kSeedFirst = 5;
constexpr unsigned int kSeedSecond = 100;
constexpr unsigned int kSeedOther = 7;
class AtomicCounterScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
GLint counters = 0;
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTERS, &counters);
GLint buffers = 0;
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS, &buffers);
if (counters < 3 || buffers < 2) {
GTEST_SKIP() << "GL_MAX_COMPUTE_ATOMIC_COUNTERS is " << counters
<< " and GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS is " << buffers
<< "; this needs 3 and 2";
}
if (!AtomicCountersAreWired()) {
GTEST_SKIP() << "atomic counter buffers are not wired up on " << Gl().BackendName()
<< " yet: glslang lowers them onto a storage block and that block's descriptor "
<< "is still resolved from the shader-storage binding points";
}
m_program = CompileComputeProgram(kCounterComputeSource);
ASSERT_NE(m_program, 0u) << m_buildLog;
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
if (!m_buffers.empty()) glDeleteBuffers(static_cast<GLsizei>(m_buffers.size()), m_buffers.data());
if (m_program != 0) glDeleteProgram(m_program);
m_buffers.clear();
m_program = 0;
}
// Magma binds the lowered block as an ordinary storage-buffer descriptor resolved
// from GL_SHADER_STORAGE_BUFFER point N, so the counter buffer never reaches it. The
// frontend half (limits, reflection queries, the link-time offset rules) is
// backend-agnostic and is covered by the unit suites; only the VALUE is scoped here.
bool AtomicCountersAreWired() const { return Gl().BackendName() != "DirectVulkan"; }
unsigned int CompileComputeProgram(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[2048] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute shader did not compile: ") + log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[2048] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute program did not link: ") + log;
glDeleteProgram(program);
return 0;
}
return program;
}
// A counter buffer of `count` uints, seeded and bound to atomic-counter point
// `binding`.
GLuint MakeCounterBuffer(GLuint binding, const std::vector<unsigned int>& seed) {
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer);
glBufferData(GL_ATOMIC_COUNTER_BUFFER,
static_cast<GLsizeiptr>(seed.size() * sizeof(unsigned int)), seed.data(),
GL_DYNAMIC_DRAW);
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, binding, buffer);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
m_buffers.push_back(buffer);
return buffer;
}
std::vector<unsigned int> ReadCounters(GLuint buffer, int count) {
std::vector<unsigned int> values(static_cast<std::size_t>(count), 0xDEADBEEFu);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer);
glGetBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0,
static_cast<GLsizeiptr>(values.size() * sizeof(unsigned int)), values.data());
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
return values;
}
void Dispatch() {
glUseProgram(m_program);
glDispatchCompute(kWorkGroups, 1, 1);
glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
}
unsigned int m_program = 0;
std::string m_buildLog;
std::vector<GLuint> m_buffers;
};
} // namespace
// The counter values a dispatch leaves behind, per binding point and per offset within one
// binding. Nothing in the ES backend used to touch BufferTarget::AtomicCounter at all, so
// before the wiring landed every one of these read back its seed unchanged.
TEST_F(AtomicCounterScenario, DispatchIncrementsTheBoundCounterBuffers) {
if (!Ready() || IsSkipped()) return;
const GLuint zero = MakeCounterBuffer(0, {kSeedFirst, kSeedSecond});
const GLuint one = MakeCounterBuffer(1, {kSeedOther});
ASSERT_EQ(FirstGLError(), 0u) << "binding the counter buffers raised a GL error";
Dispatch();
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch raised a GL error";
const std::vector<unsigned int> zeroValues = ReadCounters(zero, 2);
const std::vector<unsigned int> oneValues = ReadCounters(one, 1);
EXPECT_EQ(FirstGLError(), 0u) << "reading the counters back raised a GL error";
EXPECT_EQ(zeroValues[0], kSeedFirst + kInvocations)
<< "binding 0 offset 0 read back " << zeroValues[0] << "; " << kSeedFirst
<< " means the shader's increments never reached the buffer the application bound";
EXPECT_EQ(zeroValues[1], kSeedSecond + 2 * kInvocations)
<< "binding 0 offset 4 read back " << zeroValues[1] << "; the seed means the counter at a NON-ZERO "
<< "offset was not carried through the lowering, even though offset 0 was";
EXPECT_EQ(oneValues[0], kSeedOther + kInvocations)
<< "binding 1 read back " << oneValues[0] << "; a counter buffer past the first binding point "
<< "resolves to a different reserved slot and is where an off-by-one shows up";
}
// A second dispatch continues from where the first left off, and a re-seed between them is
// visible to the shader. Both halves of the buffer's traffic have to work, in both
// directions: the increments are only observable through the readback path, and the re-seed
// is only observable if the upload reaches the driver AFTER the buffer has been GPU-written.
TEST_F(AtomicCounterScenario, CountersAccumulateAcrossDispatchesAndFollowAReseed) {
if (!Ready() || IsSkipped()) return;
const GLuint zero = MakeCounterBuffer(0, {0u, 0u});
MakeCounterBuffer(1, {0u});
ASSERT_EQ(FirstGLError(), 0u);
Dispatch();
Dispatch();
std::vector<unsigned int> values = ReadCounters(zero, 2);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(values[0], 2 * kInvocations) << "two dispatches did not accumulate";
EXPECT_EQ(values[1], 4 * kInvocations) << "two dispatches did not accumulate at offset 4";
const unsigned int reseed[2] = {1000u, 2000u};
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, zero);
glBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(reseed), reseed);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
ASSERT_EQ(FirstGLError(), 0u) << "re-seeding the counter buffer raised a GL error";
Dispatch();
values = ReadCounters(zero, 2);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(values[0], reseed[0] + kInvocations) << "the re-seeded value did not reach the shader";
EXPECT_EQ(values[1], reseed[1] + 2 * kInvocations) << "the re-seeded value at offset 4 did not reach the shader";
}
} // namespace MGITest
@@ -299,4 +299,99 @@ void main() {
EXPECT_EQ(FirstGLError(), 0u); EXPECT_EQ(FirstGLError(), 0u);
} }
// glGetTexLevelParameter used to refuse EVERY pname on a buffer texture: WIDTH/HEIGHT/DEPTH
// fell out of a mipmap-only switch as GL_INVALID_OPERATION, and GL_TEXTURE_BUFFER_SIZE /
// GL_TEXTURE_BUFFER_OFFSET were not in the switch at all, so they came back GL_INVALID_ENUM.
// KHR-GL43.texture_buffer wraps both queries in GLU_EXPECT_NO_ERROR, so the error alone fails
// the case before any value is compared.
//
// The two halves report DIFFERENT units and only one of them is clamped, which is the thing
// easiest to get backwards: WIDTH is a TEXEL count clamped to GL_MAX_TEXTURE_BUFFER_SIZE,
// BUFFER_SIZE is the range in basic machine units exactly as it was given.
TEST_F(BufferTextureScenario, LevelQueriesDescribeTheAttachedBufferRange) {
if (!Ready()) return;
FirstGLError();
GLint offsetAlignment = 1;
glGetIntegerv(GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT, &offsetAlignment);
if (offsetAlignment < 1) offsetAlignment = 1;
GLint maxTexels = 0;
glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTexels);
ASSERT_EQ(FirstGLError(), 0u);
ASSERT_GT(maxTexels, 0) << "an OpenGL 4.x context may not advertise a zero buffer-texture limit";
constexpr GLint kTexelBytes = 4; // GL_RGBA8
const GLsizeiptr rangeOffset = static_cast<GLsizeiptr>(offsetAlignment);
const GLsizeiptr rangeBytes = 32 * kTexelBytes;
// Deliberately bigger than the range, so a getter that answered out of the BUFFER rather
// than out of the texture's window would be caught.
const GLsizeiptr bufferBytes = rangeOffset + rangeBytes + 16 * kTexelBytes;
const std::vector<GLubyte> zeros(static_cast<size_t>(bufferBytes), 0);
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
glBufferData(GL_TEXTURE_BUFFER, bufferBytes, zeros.data(), GL_STATIC_DRAW);
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_BUFFER, texture);
glTexBufferRange(GL_TEXTURE_BUFFER, GL_RGBA8, buffer, rangeOffset, rangeBytes);
ASSERT_EQ(FirstGLError(), 0u) << "glTexBufferRange(GL_RGBA8) was refused";
const auto levelQuery = [](GLenum pname) {
GLint value = -1;
glGetTexLevelParameteriv(GL_TEXTURE_BUFFER, 0, pname, &value);
return value;
};
const auto levelQueryF = [](GLenum pname) {
GLfloat value = -1.0f;
glGetTexLevelParameterfv(GL_TEXTURE_BUFFER, 0, pname, &value);
return value;
};
EXPECT_EQ(levelQuery(GL_TEXTURE_WIDTH), static_cast<GLint>(rangeBytes / kTexelBytes))
<< "GL_TEXTURE_WIDTH is a texel count over the attached RANGE";
EXPECT_EQ(levelQuery(GL_TEXTURE_HEIGHT), 1);
EXPECT_EQ(levelQuery(GL_TEXTURE_DEPTH), 1);
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_SIZE), static_cast<GLint>(rangeBytes))
<< "GL_TEXTURE_BUFFER_SIZE reports basic machine units, not texels";
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_OFFSET), static_cast<GLint>(rangeOffset));
EXPECT_EQ(FirstGLError(), 0u) << "a buffer-texture level query raised an error";
EXPECT_LE(levelQuery(GL_TEXTURE_WIDTH), maxTexels)
<< "GL_TEXTURE_WIDTH must stay clamped to GL_MAX_TEXTURE_BUFFER_SIZE";
// The float getter is a separate switch and has drifted from the integer one before.
EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_WIDTH), static_cast<GLfloat>(rangeBytes / kTexelBytes));
EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_HEIGHT), 1.0f);
EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_BUFFER_SIZE), static_cast<GLfloat>(rangeBytes));
EXPECT_EQ(FirstGLError(), 0u) << "the float form of a buffer-texture level query raised an error";
// The whole-buffer form follows the buffer's current size instead of freezing a window.
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, buffer);
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_OFFSET), 0);
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_SIZE), static_cast<GLint>(bufferBytes));
EXPECT_EQ(levelQuery(GL_TEXTURE_WIDTH), static_cast<GLint>(bufferBytes / kTexelBytes));
EXPECT_EQ(FirstGLError(), 0u);
// Both buffer pnames belong to buffer textures alone; anything else is INVALID_OPERATION,
// the same shape GL_TEXTURE_COMPRESSED_IMAGE_SIZE uses for an uncompressed image.
GLuint plainTexture = 0;
glGenTextures(1, &plainTexture);
glBindTexture(GL_TEXTURE_2D, plainTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
EXPECT_EQ(FirstGLError(), 0u);
GLint unused = -1;
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_BUFFER_SIZE, &unused);
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_OPERATION));
glBindTexture(GL_TEXTURE_2D, 0);
glBindTexture(GL_TEXTURE_BUFFER, 0);
glBindBuffer(GL_TEXTURE_BUFFER, 0);
glDeleteTextures(1, &plainTexture);
glDeleteTextures(1, &texture);
glDeleteBuffers(1, &buffer);
EXPECT_EQ(FirstGLError(), 0u);
}
} // namespace MGITest } // namespace MGITest
@@ -151,6 +151,18 @@ void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out); glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out);
} }
// GL_MAX_CLIP_DISTANCES is a real backend answer, not a constant: DirectGLES reports
// 0 on a driver without GL_EXT_clip_cull_distance, and DirectVulkan reports 0 without
// the shaderClipDistance device feature. On such a stack the shader above cannot
// compile - and MUST not, because declaring a clip distance the backend cannot host
// is exactly what used to link cleanly and then render nothing. Skip rather than
// fail: there is no clipping to assert about.
static bool BackendHostsTwoClipDistances() {
GLint maxClipDistances = 0;
glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
return maxClipDistances >= 2;
}
// Never assume the eight start disabled - see the header note about // Never assume the eight start disabled - see the header note about
// XfbAfterClipDistanceScenario leaving one on for the rest of the process. // XfbAfterClipDistanceScenario leaving one on for the rest of the process.
static void DisableEveryClipDistance() { static void DisableEveryClipDistance() {
@@ -229,6 +241,9 @@ void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
// The claim: an enabled clip distance removes the fragments where it is negative. // The claim: an enabled clip distance removes the fragments where it is negative.
TEST_F(ClipDistanceScenario, AnEnabledClipDistanceRemovesTheNegativeHalf) { TEST_F(ClipDistanceScenario, AnEnabledClipDistanceRemovesTheNegativeHalf) {
if (!Ready()) return; if (!Ready()) return;
if (!BackendHostsTwoClipDistances()) {
GTEST_SKIP() << "this backend advertises no clip distances, so there is nothing to clip with";
}
HeadlessGL& gl = Gl(); HeadlessGL& gl = Gl();
const int width = gl.Width(); const int width = gl.Width();
const int height = gl.Height(); const int height = gl.Height();
@@ -280,6 +295,9 @@ void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
// draw simply failed - would pass the case above. // draw simply failed - would pass the case above.
TEST_F(ClipDistanceScenario, ADisabledClipDistanceRemovesNothing) { TEST_F(ClipDistanceScenario, ADisabledClipDistanceRemovesNothing) {
if (!Ready()) return; if (!Ready()) return;
if (!BackendHostsTwoClipDistances()) {
GTEST_SKIP() << "this backend advertises no clip distances, so there is nothing to clip with";
}
HeadlessGL& gl = Gl(); HeadlessGL& gl = Gl();
const int width = gl.Width(); const int width = gl.Width();
const int height = gl.Height(); const int height = gl.Height();
@@ -329,6 +347,9 @@ void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
// passes both cases above and fails this one. // passes both cases above and fails this one.
TEST_F(ClipDistanceScenario, TheEnablesAreIndependentPerDistance) { TEST_F(ClipDistanceScenario, TheEnablesAreIndependentPerDistance) {
if (!Ready()) return; if (!Ready()) return;
if (!BackendHostsTwoClipDistances()) {
GTEST_SKIP() << "this backend advertises no clip distances, so there is nothing to clip with";
}
HeadlessGL& gl = Gl(); HeadlessGL& gl = Gl();
const int width = gl.Width(); const int width = gl.Width();
const int height = gl.Height(); const int height = gl.Height();
@@ -697,24 +697,127 @@ void main() {
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)); EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
} }
TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsDeclinedOnEveryBackend) { TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsRecordedAndItsArrayIsDroppedAtDraw) {
if (!Ready()) return; if (!Ready()) return;
// The demotion leaves no 64-bit shader input to feed, so there is nothing a 64-bit // The demotion leaves no 64-bit shader input to feed, so there is nothing a 64-bit
// vertex FETCH could be fetched into - on either backend, and no longer only on the // vertex FETCH could be fetched into - on either backend, and no longer only on the
// ones whose device lacks shaderFloat64. Declined loudly rather than accepted and // ones whose device lacks shaderFloat64.
// drawn as garbage; the matching POST row says the same thing at startup. //
// What that costs is the ARRAY, not the CALL. GL 4.6 core 10.3.2 defines no error for
// a well-formed glVertexAttribLFormat and 64-bit attributes are core in the GL 4.3
// context MobileGL advertises, so refusing the call would be non-conformant and would
// leave four pure state queries unanswerable
// (KHR-GL43.vertex_attrib_binding.basic-state1/3). The format is therefore recorded and
// queryable; the enabled array is what gets dropped, and the attribute then reads its
// generic current value. The matching POST row says exactly that at startup.
GLuint vao = 0; GLuint vao = 0;
glGenVertexArrays(1, &vao); glGenVertexArrays(1, &vao);
glBindVertexArray(vao); glBindVertexArray(vao);
while (glGetError() != GL_NO_ERROR) {} while (glGetError() != GL_NO_ERROR) {}
glVertexAttribLFormat(0, 3, GL_DOUBLE, 0); glVertexAttribLFormat(1, 3, GL_DOUBLE, 8);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION)); EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< "glVertexAttribLFormat is a legal call in a GL 4.3 context";
GLint attribSize = 0;
GLint attribType = 0;
GLint attribIsLong = 0;
GLint attribRelativeOffset = 0;
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_SIZE, &attribSize);
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_TYPE, &attribType);
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_LONG, &attribIsLong);
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &attribRelativeOffset);
EXPECT_EQ(attribSize, 3);
EXPECT_EQ(attribType, static_cast<GLint>(GL_DOUBLE));
EXPECT_EQ(attribIsLong, GL_TRUE) << "GL_VERTEX_ATTRIB_ARRAY_LONG is what makes this the "
"unconverted form; without it the state is a lie";
EXPECT_EQ(attribRelativeOffset, 8);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
glBindVertexArray(0); glBindVertexArray(0);
glDeleteVertexArrays(1, &vao); glDeleteVertexArrays(1, &vao);
while (glGetError() != GL_NO_ERROR) {} while (glGetError() != GL_NO_ERROR) {}
} }
// The consequence of recording the state rather than refusing the call: a 64-bit array can
// now be ENABLED in a VAO that a draw uses, which it never could before. That must not
// take the draw down. Leaving such an array enabled with no pointer behind it is exactly
// the documented Adreno null-deref (SIGSEGV inside the next glDraw*), so DirectGLES
// disables it before glVertexAttribPointer can ever see GL_DOUBLE, and DirectVulkan maps
// the format to VK_FORMAT_UNDEFINED so it never enters the pipeline's vertex input state.
//
// The shader deliberately does NOT read location 1: that keeps the two backends on the
// same path (DirectVulkan declines a draw whose SHADER reads an unsupported enabled array,
// by design and loudly, which is a different assertion from this one) and it is the shape
// the crash needed - an enabled array nothing set a pointer for.
TEST_F(DoublePrecisionScenario, AnEnabledLongArrayDoesNotBreakADrawThatIgnoresIt) {
if (!Ready()) return;
constexpr const char* kVs = R"(#version 430 core
layout(location = 0) in vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
)";
constexpr const char* kFs = R"(#version 430 core
out vec4 o_color;
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
)";
std::string error;
const unsigned int program = CompileProgram(kVs, kFs, &error);
ASSERT_NE(program, 0u) << error;
ColorFbo target = MakeColorFbo(32, 32);
ASSERT_NE(target.fbo, 0u) << "could not create the render target";
BindFbo(target);
const float positions[8] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
const double doubles[4] = {1.0, 2.0, 3.0, 4.0};
GLuint vao = 0;
GLuint positionBuffer = 0;
GLuint doubleBuffer = 0;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &positionBuffer);
glBindBuffer(GL_ARRAY_BUFFER, positionBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);
glGenBuffers(1, &doubleBuffer);
glBindBuffer(GL_ARRAY_BUFFER, doubleBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(doubles), doubles, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribFormat(0, 2, GL_FLOAT, GL_FALSE, 0);
glVertexAttribBinding(0, 0);
glBindVertexBuffer(0, positionBuffer, 0, static_cast<GLsizei>(2 * sizeof(float)));
glEnableVertexAttribArray(0);
glVertexAttribLFormat(1, 1, GL_DOUBLE, 0);
glVertexAttribBinding(1, 1);
glBindVertexBuffer(1, doubleBuffer, 0, static_cast<GLsizei>(sizeof(double)));
glEnableVertexAttribArray(1);
EXPECT_EQ(FirstGLError(), 0u) << "setting up the 64-bit array was refused";
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glUseProgram(program);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
EXPECT_EQ(FirstGLError(), 0u) << "a draw with an enabled 64-bit array must not raise an error";
const Image image = ReadPixels(target.width, target.height);
ASSERT_FALSE(image.Empty());
EXPECT_GT(image.At(target.width / 2, target.height / 2).g, 200)
<< "the draw did not happen; the enabled 64-bit array must be dropped, not fatal";
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glBindVertexArray(0);
glDeleteVertexArrays(1, &vao);
glDeleteBuffers(1, &positionBuffer);
glDeleteBuffers(1, &doubleBuffer);
BindDefaultFramebuffer();
DestroyColorFbo(target);
glUseProgram(0);
glDeleteProgram(program);
EXPECT_EQ(FirstGLError(), 0u);
}
} // namespace } // namespace
} // namespace MGITest } // namespace MGITest
@@ -0,0 +1,234 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageSizeAfterRespecScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - A DRAW READS imageSize() AFTER THE IMAGE TEXTURE IS RE-SPECIFIED.
//
// KHR-GL43.shader_image_size.advanced-changeSize reduced to its mechanism. The application binds
// a texture to an image unit ONCE, draws, then re-specifies that same texture with a new size
// through glTexImage2D and draws again - without touching the image unit. GL says the unit
// references the texture OBJECT, so the second draw must see the new dimensions.
//
// On Espryt it did not, and the reason is two facts meeting:
//
// 1. ES 3.1 only allows IMMUTABLE storage on an image unit, so the backend forces glTexStorage
// backing on any texture that reaches one (SyncTextureObjectToBackend's
// imageBindableStorageRequired). Immutable storage cannot be redefined, so a glTexImage2D
// that changes size or format has to MINT A NEW ES TEXTURE NAME.
// 2. The draw path never re-issued glBindImageTexture. Image units were established eagerly,
// once, when the application called glBindImageTexture, and PrepareForDraw only ever
// re-synced SAMPLED textures - so the unit kept pointing at the deleted name and
// imageSize() reported whatever that stale binding still meant.
//
// A dispatch was never affected: PrepareForCompute has always swept the image units. This is a
// draw-path scenario for exactly that reason - a compute-shaped case cannot see the defect.
//
// Both backends run it. Magma re-derives its image descriptors per draw and so was never wrong
// here, which makes it the control: the two backends have to agree on what the second draw sees.
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kTargetSize = 8;
constexpr const char* kVS = R"(#version 430 core
void main()
{
// A single triangle that covers the whole target, with no vertex buffer at all: the
// scenario is about the image unit, so nothing else may be able to make it fail.
switch (gl_VertexID)
{
case 0: gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); break;
case 1: gl_Position = vec4( 3.0, -1.0, 0.0, 1.0); break;
case 2: gl_Position = vec4(-1.0, 3.0, 0.0, 1.0); break;
}
}
)";
// Green when the image the unit currently holds has the size the application last gave
// it, red otherwise - the conformance case's own comparison, and its own colours.
constexpr const char* kFS = R"(#version 430 core
layout(rgba8) readonly uniform image2D g_image;
uniform ivec2 g_expected_size;
layout(location = 0) out vec4 o_color;
void main()
{
o_color = (imageSize(g_image) == g_expected_size) ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0);
}
)";
class ImageSizeAfterRespecScenario : public ScenarioTest {
protected:
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (m_program != 0) glDeleteProgram(m_program);
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
if (m_color != 0) glDeleteTextures(1, &m_color);
if (m_image != 0) glDeleteTextures(1, &m_image);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
m_program = m_fbo = m_color = m_image = m_vao = 0;
while (glGetError() != GL_NO_ERROR) {
}
}
// imageSize() needs a fragment-stage image uniform; a driver that serves none should
// skip rather than fail.
bool FragmentImagesAreUsable() const {
GLint maxImageUnits = 0;
GLint maxFragmentImageUniforms = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImageUniforms);
while (glGetError() != GL_NO_ERROR) {
}
return maxImageUnits >= 1 && maxFragmentImageUniforms >= 1;
}
GLuint MakeProgram() {
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(vs, 1, &kVS, nullptr);
glShaderSource(fs, 1, &kFS, nullptr);
glCompileShader(vs);
glCompileShader(fs);
for (const GLuint shader : {vs, fs}) {
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[4096] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "a shader did not compile: " << log;
glDeleteShader(vs);
glDeleteShader(fs);
return 0;
}
}
const GLuint program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[4096] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the program did not link: " << log;
glDeleteProgram(program);
return 0;
}
return program;
}
void MakeRenderTarget() {
glGenTextures(1, &m_color);
glBindTexture(GL_TEXTURE_2D, m_color);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTargetSize, kTargetSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
nullptr);
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_color, 0);
}
// Draw once with `expected` pushed to the shader and report the centre pixel.
void DrawAndReadCentre(int expectedWidth, int expectedHeight, unsigned char (&centre)[4]) {
const GLint location = glGetUniformLocation(m_program, "g_expected_size");
ASSERT_NE(location, -1) << "the program has no g_expected_size uniform";
glUseProgram(m_program);
glUniform2i(location, expectedWidth, expectedHeight);
glViewport(0, 0, kTargetSize, kTargetSize);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLES, 0, 3);
ASSERT_EQ(FirstGLError(), 0u) << "the draw left a GL error";
std::vector<unsigned char> pixels(static_cast<std::size_t>(kTargetSize) * kTargetSize * 4, 0);
glReadPixels(0, 0, kTargetSize, kTargetSize, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
ASSERT_EQ(FirstGLError(), 0u) << "reading the target back errored";
const std::size_t offset =
(static_cast<std::size_t>(kTargetSize / 2) * kTargetSize + kTargetSize / 2) * 4;
for (int i = 0; i < 4; ++i) {
centre[i] = pixels[offset + static_cast<std::size_t>(i)];
}
}
GLuint m_program = 0;
GLuint m_fbo = 0;
GLuint m_color = 0;
GLuint m_image = 0;
GLuint m_vao = 0;
};
} // namespace
// The whole conformance shape: bind once, draw, re-specify the SAME texture smaller, draw
// again. The first draw is the control - it proves the binding and the shader work at all -
// and the second is the regression pin. Blue would mean the draw never ran; red means the
// image unit answered with the size the texture had BEFORE the re-spec.
TEST_F(ImageSizeAfterRespecScenario, ADrawSeesTheNewSizeOfARespecifiedImageTexture) {
if (!Ready()) return;
if (!FragmentImagesAreUsable()) GTEST_SKIP() << "no fragment-stage image uniform available";
m_program = MakeProgram();
if (m_program == 0) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
MakeRenderTarget();
ASSERT_EQ(FirstGLError(), 0u) << "setting the render target up errored";
glGenTextures(1, &m_image);
glBindTexture(GL_TEXTURE_2D, m_image);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 32, 32, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glBindImageTexture(0, m_image, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
ASSERT_EQ(FirstGLError(), 0u) << "binding the image texture errored";
unsigned char centre[4] = {0, 0, 0, 0};
DrawAndReadCentre(32, 32, centre);
EXPECT_EQ(static_cast<int>(centre[0]), 0) << "the FIRST draw already disagrees about imageSize(): got ("
<< static_cast<int>(centre[0]) << ", "
<< static_cast<int>(centre[1]) << ", "
<< static_cast<int>(centre[2]) << ")";
EXPECT_EQ(static_cast<int>(centre[1]), 255);
// The re-spec. The image unit is deliberately NOT re-bound: GL 4.6 core 8.26 says the
// unit references the texture object, so this alone has to be visible to the next draw.
glBindTexture(GL_TEXTURE_2D, m_image);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(FirstGLError(), 0u) << "re-specifying the image texture errored";
DrawAndReadCentre(16, 16, centre);
EXPECT_EQ(static_cast<int>(centre[0]), 0)
<< "after the re-spec the draw still sees the OLD image size; centre pixel was ("
<< static_cast<int>(centre[0]) << ", " << static_cast<int>(centre[1]) << ", "
<< static_cast<int>(centre[2]) << ")";
EXPECT_EQ(static_cast<int>(centre[1]), 255);
}
} // namespace MGITest
@@ -66,6 +66,9 @@ namespace MGITest {
constexpr int kExtent = 6; constexpr int kExtent = 6;
constexpr GLuint kFilledValue = 7u; constexpr GLuint kFilledValue = 7u;
constexpr GLuint kStoredValue = 13u; constexpr GLuint kStoredValue = 13u;
// What the atomic cases add to a filled texel. Distinct from both values above, so a
// wrong answer cannot be read as either the untouched fill or a plain store.
constexpr GLuint kAtomicAddend = 5u;
// Everything that differs between the eleven kinds, in one row. // Everything that differs between the eleven kinds, in one row.
struct TargetKind { struct TargetKind {
@@ -129,6 +132,25 @@ namespace MGITest {
kind.imageType + " i0;\n\nvoid main()\n{\n " + StoreStatement(kind, "i0", "13u") + "\n}\n"; kind.imageType + " i0;\n\nvoid main()\n{\n " + StoreStatement(kind, "i0", "13u") + "\n}\n";
} }
// The third direction, and the one neither of the two above can stand in for: an
// imageAtomic* reaches its texel through a SPIR-V operand path of its own
// (OpImageTexelPointer), not through OpImageRead or OpImageWrite. SPIRV-Cross's "ES has
// no 1D image, address it as 2D" coordinate widening is applied on the read and write
// paths and NOT on that one, so a 1D image whose loads and stores are both correct could
// still lose its entire stage to a single imageAtomicAdd - which is what
// KHR-GL4x.shader_image_load_store.basic-allTargets-atomic measured, with the driver
// answering "'imageAtomicAdd' : no matching overloaded function found".
//
// No readonly/writeonly here: an atomic needs both directions, and r32ui is one of the
// three formats GLSL ES exempts from the qualifier rule, so the bare declaration is legal.
// Returns the value the texel held BEFORE the add, so one dispatch checks the atomic's
// return value and the load case that follows checks its memory effect.
std::string SingleAtomicSource(const TargetKind& kind) {
return std::string(kComputePrologue) + "layout (location = 0, r32ui) coherent uniform " +
kind.imageType + " i0;\n" + kResultBlock + "void main()\n{\n ssb.sum = imageAtomicAdd(i0, " +
kind.coord + (kind.multisample ? ", 0, " : ", ") + std::to_string(kAtomicAddend) + "u);\n}\n";
}
class ImageTargetKindScenario : public ScenarioTest { class ImageTargetKindScenario : public ScenarioTest {
protected: protected:
void TearDown() override { void TearDown() override {
@@ -374,6 +396,119 @@ namespace MGITest {
glUseProgram(0); glUseProgram(0);
} }
// Fill a texture of `kind`, add to texel (0,0,0) atomically, and require BOTH the
// value the atomic returned and the value it left behind. The read-back runs as a
// second program, for the same reason the store case does: a backend that gets the
// atomic's return right and its memory effect wrong cannot cancel itself out.
void RunAtomicCase(const TargetKind& kind) {
const GLuint atomicProgram = MakeComputeProgram(SingleAtomicSource(kind));
const GLuint loadProgram = MakeComputeProgram(SingleLoadSource(kind));
if (atomicProgram == 0 || loadProgram == 0) return;
const GLuint texture = MakeTexture(kind, true);
if (texture == 0) return;
const GLuint ssbo = MakeResultBuffer();
glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_WRITE, GL_R32UI);
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": glBindImageTexture errored";
glUseProgram(atomicProgram);
glUniform1i(0, 0);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the atomic dispatch leaked a GL error";
EXPECT_EQ(ReadResult(ssbo), kFilledValue)
<< kind.name << ": imageAtomicAdd did not return the value the texel held before it";
glUseProgram(loadProgram);
glUniform1i(0, 0);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the loading dispatch leaked a GL error";
EXPECT_EQ(ReadResult(ssbo), kFilledValue + kAtomicAddend)
<< kind.name << ": imageAtomicAdd did not leave the sum in the texel";
glUseProgram(0);
}
// The same texture, bound four times over, varying nothing but `layered` and `layer`.
//
// GL 4.6 core 8.26 (and ES 3.2 8.22, word for word): "If the texture identified by
// texture does not have multiple layers or faces, the entire texture level is bound,
// regardless of the values of layered and layer." REGARDLESS means ignored - not
// clamped, and not an error - so every one of the four rows has to read the same texel
// out of a target that has no layers, including the two rows that name layer 1 on a
// texture whose only layer is 0. DirectGLES used to normalize `layered` and forward
// `layer` verbatim; Adreno honours the bogus layer by leaving the image unit reading
// zero, which is exactly the two rows KHR-GL42.bind_image_texture.single_layer failed.
//
// The bindings are checked back as well, because the fix depends on WHERE the
// normalization happens: the frontend shadow must keep echoing the application's own
// values (gl4cShaderImageLoadStoreTests' CheckBinding compares them exactly), and only
// the backend's driver call may drop the layer.
void RunNonLayerableLayerSweepCase(const TargetKind& kind) {
const GLuint program = MakeComputeProgram(SingleLoadSource(kind));
if (program == 0) return;
const GLuint texture = MakeTexture(kind, true);
if (texture == 0) return;
// A multisample texture has no TexSubImage, so MakeTexture leaves it unwritten and
// it is seeded the way the store cases do it - through a dispatch of its own.
const GLuint expected = kind.multisample ? kStoredValue : kFilledValue;
if (kind.multisample) {
const GLuint storeProgram = MakeComputeProgram(SingleStoreSource(kind));
if (storeProgram == 0) return;
glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_WRITE, GL_R32UI);
glUseProgram(storeProgram);
glUniform1i(0, 0);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": seeding the multisample texture errored";
}
const GLuint ssbo = MakeResultBuffer();
glUseProgram(program);
glUniform1i(0, 0);
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": assigning the image unit errored";
// glcBindImageTextureTests' own four rows, in its own order.
struct LayerRow {
GLboolean layered;
GLint layer;
};
static constexpr LayerRow kRows[] = {{GL_TRUE, 1}, {GL_TRUE, 0}, {GL_FALSE, 1}, {GL_FALSE, 0}};
for (const LayerRow& row : kRows) {
const std::string where = std::string(kind.name) +
": layered=" + (row.layered == GL_TRUE ? "TRUE" : "FALSE") +
" layer=" + std::to_string(row.layer);
// Re-zeroed per row, so a row whose binding reads nothing cannot pass on the
// previous row's answer.
const GLuint zero = 0u;
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(GLuint), &zero);
glBindImageTexture(0, texture, 0, row.layered, row.layer, GL_READ_ONLY, GL_R32UI);
EXPECT_EQ(FirstGLError(), 0u) << where << ": glBindImageTexture errored";
GLint reportedLayered = -1;
GLint reportedLayer = -1;
glGetIntegeri_v(GL_IMAGE_BINDING_LAYERED, 0, &reportedLayered);
glGetIntegeri_v(GL_IMAGE_BINDING_LAYER, 0, &reportedLayer);
EXPECT_EQ(reportedLayered, row.layered == GL_TRUE ? 1 : 0)
<< where << ": GL_IMAGE_BINDING_LAYERED stopped reporting the application's value";
EXPECT_EQ(reportedLayer, row.layer)
<< where << ": GL_IMAGE_BINDING_LAYER stopped reporting the application's value";
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << where << ": the dispatch leaked a GL error";
EXPECT_EQ(ReadResult(ssbo), expected)
<< where
<< ": the texel did not come back, so the binding named a layer the texture "
"does not have instead of the whole level";
}
glUseProgram(0);
}
std::vector<GLuint> m_programs; std::vector<GLuint> m_programs;
std::vector<GLuint> m_textures; std::vector<GLuint> m_textures;
std::vector<GLuint> m_buffers; std::vector<GLuint> m_buffers;
@@ -435,6 +570,54 @@ namespace MGITest {
#undef MGL_DEFINE_LOAD_CASE #undef MGL_DEFINE_LOAD_CASE
#undef MGL_DEFINE_STORE_CASE #undef MGL_DEFINE_STORE_CASE
// ---- and the atomic direction, on the two kinds ES has to emulate -------
//
// Deliberately NOT every kind. imageAtomic* takes its own SPIR-V operand path
// (OpImageTexelPointer), and the only kinds whose coordinate that path has to RESHAPE are the
// two 1D ones - everything else addresses its ES texture with the coordinate the application
// wrote. GL_TEXTURE_1D_ARRAY is the control (its reshape has been in
// Lower1DArrayImagesForEssl from the start, and basic-allTargets-atomic passes on it);
// GL_TEXTURE_1D is the one that had none, so `imageAtomicAdd(g_image_1d, coord.x, 2)` reached
// the driver as a scalar against an iimage2D and took the whole fragment stage - and its six
// other images - with it.
#define MGL_DEFINE_ATOMIC_CASE(CaseName, Kind) \
TEST_F(ImageTargetKindScenario, AtomicallyAddsTo##CaseName) { \
if (!Ready()) return; \
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms"; \
RunAtomicCase(Kind); \
}
MGL_DEFINE_ATOMIC_CASE(Texture1D, kKind1D)
MGL_DEFINE_ATOMIC_CASE(Texture1DArray, kKind1DArray)
#undef MGL_DEFINE_ATOMIC_CASE
// ---- and the same texture bound four times, varying only layered/layer ---
//
// KHR-GL42.bind_image_texture.single_layer's sweep, on the kinds whose backend target has
// neither layers nor faces. Two of its four rows name layer 1 on a single-layer texture,
// which the spec says is to be ignored outright rather than honoured or rejected - and
// which DirectGLES used to forward to the ES driver as written.
#define MGL_DEFINE_LAYER_SWEEP_CASE(CaseName, Kind) \
TEST_F(ImageTargetKindScenario, IgnoresLayerFor##CaseName) { \
if (!Ready()) return; \
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms"; \
if ((Kind).multisample && !MultisampleImagesAreUsable()) { \
GTEST_SKIP() << "GL_MAX_IMAGE_SAMPLES is 0, so the conformance case substitutes a plain 2D image " \
"here and never asks for a multisample one"; \
} \
RunNonLayerableLayerSweepCase(Kind); \
}
MGL_DEFINE_LAYER_SWEEP_CASE(Texture2D, kKind2D)
MGL_DEFINE_LAYER_SWEEP_CASE(Texture1D, kKind1D)
MGL_DEFINE_LAYER_SWEEP_CASE(TextureRectangle, kKindRect)
MGL_DEFINE_LAYER_SWEEP_CASE(Texture2DMultisample, kKind2DMS)
#undef MGL_DEFINE_LAYER_SWEEP_CASE
// ---- and all of them at once ------------------------------------------- // ---- and all of them at once -------------------------------------------
// //
// The conformance case's actual shape. The single-kind cases above cannot see a defect that // The conformance case's actual shape. The single-kind cases above cannot see a defect that
@@ -0,0 +1,389 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IoBlockNameCollisionScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - ONE BLOCK NAME USED IN BOTH DIRECTIONS BY ONE STAGE STILL CARRIES ITS PAYLOAD.
//
// Desktop GLSL keeps SEPARATE name namespaces for input and output interface blocks, so a
// single stage may legally write
//
// in TcsData { ... } tes_in[];
// out TcsData { ... } tes_out;
//
// The tessellation evaluation stage of both interface-block tests in
// KHR-GL42/43.shading_language_420pack does exactly that, and MobileGL's backend used to
// hand the shape straight through: SPIRV-Cross splits the namespace the same way glslang
// does (block_input_names vs block_output_names) and re-emits BOTH blocks under the name
// TcsData, so the generated ESSL declares two different blocks of one name in one shader.
// Adreno's ES compiler keeps them apart. Mali's does not - the stage compiles, the program
// links, and the evaluation stage's writes never reach the geometry stage, which is all 22
// of that group's Mali failures and none of Adreno's or DirectVulkan's.
//
// Both cases below drive the SAME five-stage pipeline (vertex -> tessellation control ->
// tessellation evaluation -> geometry -> fragment) and differ only in whether the
// evaluation stage reuses one name. The distinct-name case is the negative control: it is
// what says a red pixel in the colliding case is about the name and not about this machine's
// tessellation, its geometry stage, or the block mechanism in general.
//
// Colour code, so a failure names its own cause:
// green - the payload crossed all four stage boundaries, which is the pass.
// blue - the clear colour: nothing was drawn at all (the program did not link, or the
// backend program was rejected and every draw became a no-op).
// red - the pipeline ran but the plain (non-block) varying did not arrive, i.e. the
// failure is not about interface blocks.
// black - the pipeline ran, the plain varying arrived, and the BLOCK payload came back
// zeroed or garbage. That is the defect this scenario exists for.
//
// llvmpipe and lavapipe run this faithfully but do NOT reproduce the original defect - the
// aliasing is a Mali ES compiler behaviour. Read a green run here as "the rename did not
// break the ordinary path"; the claim it pins on the device is the CTS group above.
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// The payload starts here and is copied, unmodified, through every block below.
const char* const kVertexSource = R"(#version 420 core
out VsData {
vec4 payload;
} vs_out;
void main()
{
vs_out.payload = vec4(0.0, 1.0, 0.0, 1.0);
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
const char* const kTessControlSource = R"(#version 420 core
layout(vertices = 1) out;
in VsData {
vec4 payload;
} tcs_in[];
out TcsData {
vec4 payload;
} tcs_out[];
void main()
{
tcs_out[gl_InvocationID].payload = tcs_in[gl_InvocationID].payload;
gl_TessLevelOuter[0] = 1.0;
gl_TessLevelOuter[1] = 1.0;
gl_TessLevelOuter[2] = 1.0;
gl_TessLevelOuter[3] = 1.0;
gl_TessLevelInner[0] = 1.0;
gl_TessLevelInner[1] = 1.0;
}
)";
// THE CASE UNDER TEST: one name, both directions, in one stage.
const char* const kCollidingTessEvalSource = R"(#version 420 core
layout(isolines, point_mode) in;
in TcsData {
vec4 payload;
} tes_in[];
out TcsData {
vec4 payload;
} tes_out;
out float tes_gs_alive;
void main()
{
tes_out.payload = tes_in[0].payload;
tes_gs_alive = 1.0;
}
)";
// The negative control: byte-identical but for the output block's name.
const char* const kDistinctTessEvalSource = R"(#version 420 core
layout(isolines, point_mode) in;
in TcsData {
vec4 payload;
} tes_in[];
out TesData {
vec4 payload;
} tes_out;
out float tes_gs_alive;
void main()
{
tes_out.payload = tes_in[0].payload;
tes_gs_alive = 1.0;
}
)";
// One geometry source per evaluation stage, because the block it consumes is named
// after the block the evaluation stage produced.
const char* const kCollidingGeometrySource = R"(#version 420 core
layout(points) in;
layout(triangle_strip, max_vertices = 4) out;
in TcsData {
vec4 payload;
} gs_in[];
in float tes_gs_alive[];
out GsData {
vec4 payload;
} gs_out;
out float gs_fs_alive;
void EmitCorner(vec2 corner)
{
gs_out.payload = gs_in[0].payload;
gs_fs_alive = tes_gs_alive[0];
gl_Position = vec4(corner, 0.0, 1.0);
EmitVertex();
}
void main()
{
EmitCorner(vec2(-1.0, -1.0));
EmitCorner(vec2(-1.0, 1.0));
EmitCorner(vec2( 1.0, -1.0));
EmitCorner(vec2( 1.0, 1.0));
}
)";
const char* const kDistinctGeometrySource = R"(#version 420 core
layout(points) in;
layout(triangle_strip, max_vertices = 4) out;
in TesData {
vec4 payload;
} gs_in[];
in float tes_gs_alive[];
out GsData {
vec4 payload;
} gs_out;
out float gs_fs_alive;
void EmitCorner(vec2 corner)
{
gs_out.payload = gs_in[0].payload;
gs_fs_alive = tes_gs_alive[0];
gl_Position = vec4(corner, 0.0, 1.0);
EmitVertex();
}
void main()
{
EmitCorner(vec2(-1.0, -1.0));
EmitCorner(vec2(-1.0, 1.0));
EmitCorner(vec2( 1.0, -1.0));
EmitCorner(vec2( 1.0, 1.0));
}
)";
// Red when the PLAIN varying did not arrive, so "the pipeline is broken" and "the
// block payload is broken" cannot be confused for one another.
const char* const kFragmentSource = R"(#version 420 core
in GsData {
vec4 payload;
} fs_in;
in float gs_fs_alive;
out vec4 fragColor;
void main()
{
fragColor = gs_fs_alive > 0.5 ? fs_in.payload : vec4(1.0, 0.0, 0.0, 1.0);
}
)";
class IoBlockNameCollisionScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
if (!BackendHostsTessellationAndGeometry()) {
GTEST_SKIP() << "no tessellation/geometry stages on " << Gl().BackendName() << " ("
<< Gl().RendererString() << "); there is no five-stage pipeline to "
<< "carry a block through";
}
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (const GLuint program : m_programs) {
glDeleteProgram(program);
}
m_programs.clear();
glBindVertexArray(0);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
m_vao = 0;
}
// GL_MAX_TESS_GEN_LEVEL is a real backend answer, not a frontend constant: it
// reads 0 on a DirectGLES driver without GL_EXT_tessellation_shader and on a
// DirectVulkan device without the tessellationShader feature. There is no
// five-stage pipeline to assert about on such a stack.
static bool BackendHostsTessellationAndGeometry() {
GLint maxTessGenLevel = 0;
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
GLint maxGeometryOutputVertices = 0;
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
while (glGetError() != GL_NO_ERROR) {
}
return maxTessGenLevel >= 1 && maxGeometryOutputVertices >= 4;
}
GLuint BuildPipeline(const char* tessEvalSource, const char* geometrySource) {
const GLenum stages[] = {GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER,
GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER,
GL_FRAGMENT_SHADER};
const char* const sources[] = {kVertexSource, kTessControlSource, tessEvalSource,
geometrySource, kFragmentSource};
GLuint shaders[5] = {0, 0, 0, 0, 0};
bool ok = true;
for (int i = 0; i < 5; ++i) {
shaders[i] = glCreateShader(stages[i]);
glShaderSource(shaders[i], 1, &sources[i], nullptr);
glCompileShader(shaders[i]);
GLint compiled = 0;
glGetShaderiv(shaders[i], GL_COMPILE_STATUS, &compiled);
if (!compiled) {
m_buildLog = InfoLog(shaders[i], true);
ok = false;
break;
}
}
if (!ok) {
for (const GLuint shader : shaders) {
if (shader != 0) glDeleteShader(shader);
}
return 0;
}
const GLuint program = glCreateProgram();
for (const GLuint shader : shaders) {
glAttachShader(program, shader);
}
glLinkProgram(program);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
for (const GLuint shader : shaders) {
glDeleteShader(shader);
}
if (!linked) {
m_buildLog = InfoLog(program, false);
glDeleteProgram(program);
return 0;
}
m_programs.push_back(program);
return program;
}
// Clears to BLUE, so "the draw painted nothing" is a colour of its own rather
// than something that could be mistaken for a zeroed payload.
Rgba8 DrawAndReadCentre(GLuint program) const {
glViewport(0, 0, Gl().Width(), Gl().Height());
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(program);
glPatchParameteri(GL_PATCH_VERTICES, 1);
glDrawArrays(GL_PATCHES, 0, 1);
Rgba8 pixel{};
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
return pixel;
}
static bool IsGreen(const Rgba8& pixel) {
return pixel.r < 64 && pixel.g > 192 && pixel.b < 64;
}
const std::string& BuildLog() const { return m_buildLog; }
static GLenum FirstGLError() {
const GLenum first = glGetError();
while (glGetError() != GL_NO_ERROR) {
}
return first;
}
private:
static std::string InfoLog(GLuint object, bool isShader) {
GLint length = 0;
if (isShader) {
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
} else {
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
}
std::vector<char> log(static_cast<std::size_t>(length > 1 ? length : 1), '\0');
if (isShader) {
glGetShaderInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
} else {
glGetProgramInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
}
return std::string(log.data());
}
GLuint m_vao = 0;
std::vector<GLuint> m_programs;
std::string m_buildLog;
};
// The negative control, and it runs first on purpose: if this one is not green there
// is nothing to conclude from the case below it.
//
// It is also the CALIBRATION. GL_MAX_TESS_GEN_LEVEL answers for the tessellation
// stages honestly, but nothing MobileGL reports answers for the geometry stage the
// same way (GL_MAX_GEOMETRY_* are frontend constants and an ES driver may legitimately
// report zero geometry storage blocks while having geometry shaders), so a stack that
// cannot build a five-stage program at all is recognised here, by trying.
TEST_F(IoBlockNameCollisionScenario, DistinctlyNamedBlocksCarryThePayloadThroughFiveStages) {
if (!Ready()) return;
const GLuint program = BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource);
if (program == 0) {
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
<< Gl().BackendName() << ", so there is no block to carry through: "
<< BuildLog();
}
const Rgba8 centre = DrawAndReadCentre(program);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(centre)) << "the control pipeline did not deliver its payload: " << centre;
}
TEST_F(IoBlockNameCollisionScenario, OneBlockNameInBothDirectionsStillCarriesThePayload) {
if (!Ready()) return;
// Same calibration as the case above, and for the same reason: a five-stage program
// this stack cannot build at all is not evidence about block names. Only once the
// DISTINCT-name build succeeds does a failure of the colliding one mean something.
if (BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource) == 0) {
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
<< Gl().BackendName() << ", so there is no block to carry through: "
<< BuildLog();
}
// Legal desktop GLSL: input and output block names live in separate namespaces, so
// the evaluation stage below declares TcsData twice and must still compile. The
// control above having built is what makes this assertion about the NAME.
const GLuint program = BuildPipeline(kCollidingTessEvalSource, kCollidingGeometrySource);
ASSERT_NE(program, 0u)
<< "an interface block name reused across the two directions of one stage is legal "
"desktop GLSL, but the program did not build: "
<< BuildLog();
const Rgba8 centre = DrawAndReadCentre(program);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(centre))
<< "the payload did not survive the stage that names its input and output block "
"the same: "
<< centre << " (blue: nothing drew; red: the plain varying was lost too; black: "
"the block arrived empty)";
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,286 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LayeredTextureReadbackScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - READING EVERY LAYER OF A 1D-ARRAY / CUBE-MAP-ARRAY LEVEL BACK.
//
// glGetTexImage has no ES equivalent, so Espryt serves it by attaching the level to a scratch
// READ framebuffer and reading it with glReadPixels. Two of the targets it has to answer for do
// not fit that shape the way the others do, and both came back as zeroes in
// KHR-GL4x.shader_image_load_store.basic-allTargets-* and .non-layered_binding:
//
// * GL_TEXTURE_1D_ARRAY carries its LAYERS in the state-side height - that is what
// glTexImage2D(GL_TEXTURE_1D_ARRAY, w, layers) means - while the ES texture behind it is a 2D
// array of height 1 with the layers in depth. The readback used the state-side shape, so it
// asked layer 0 for a `layers`-row rectangle that layer does not have: row 0 was the only one
// that could be right, and everything past it was whatever reading outside an attachment
// produces.
// * GL_TEXTURE_CUBE_MAP_ARRAY has no glFramebufferTexture2D target token at all, so the 2D
// attach it used to take errored, the scratch FBO stayed incomplete, and every read fell
// through to the CPU shadow - which holds what was UPLOADED, i.e. the seed, not what the
// shader stored.
//
// Both cases store from a compute dispatch (so the only copy of the data is the GPU one and a
// stale shadow cannot pass) and then read the whole level back in one glGetTexImage, checking
// every layer separately so a failure names which one. r32ui throughout: it is a core GLSL ES
// image format, so nothing here can be confused with the missing-format story that
// ImageFormatQualifierScenario covers.
//
// Magma reads these back through its own path and is unaffected by the ES attachment rules, so
// both cases run on both backends and must agree.
#include <cstddef>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kExtent = 4;
constexpr int kArrayLayers = 3; // enough that "layer 0 only" is visibly wrong
constexpr int kCubeLayerFaces = 12; // two cubes, which is what the conformance case uses
// A value no store writes, so "the store never landed" and "the store wrote the wrong
// thing" cannot be confused - and so a readback served from the stale CPU shadow is
// recognisable on sight.
constexpr GLuint kSeed = 0xFEEDBEEFu;
// Deliberately not 0: the unit has to travel through glUniform1i and be baked into the
// generated ESSL, so a defect there cannot hide behind the default.
constexpr GLint kImageUnit = 1;
GLuint Expected1DArrayTexel(int x, int layer) {
return 1000u + static_cast<GLuint>(layer) * 100u + static_cast<GLuint>(x);
}
GLuint ExpectedCubeArrayTexel(int x, int y, int layerFace) {
return 1000u + static_cast<GLuint>(layerFace) * 100u + static_cast<GLuint>(y) * 10u +
static_cast<GLuint>(x);
}
// One invocation per texel, and the value it writes is a function of its coordinate - so
// a layer read from the wrong slice does not merely differ, it says which slice it came
// from.
const char* k1DArrayStoreSource = R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (r32ui) writeonly uniform uimage1DArray uni_image;
void main()
{
uint x = gl_GlobalInvocationID.x;
uint layer = gl_GlobalInvocationID.z;
imageStore(uni_image, ivec2(int(x), int(layer)), uvec4(1000u + layer * 100u + x, 0u, 0u, 0u));
}
)";
const char* kCubeArrayStoreSource = R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (r32ui) writeonly uniform uimageCubeArray uni_image;
void main()
{
uint x = gl_GlobalInvocationID.x;
uint y = gl_GlobalInvocationID.y;
uint layerFace = gl_GlobalInvocationID.z;
imageStore(uni_image, ivec3(int(x), int(y), int(layerFace)),
uvec4(1000u + layerFace * 100u + y * 10u + x, 0u, 0u, 0u));
}
)";
class LayeredTextureReadbackScenario : public ScenarioTest {
protected:
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (GLuint p : m_programs) glDeleteProgram(p);
for (GLuint t : m_textures) glDeleteTextures(1, &t);
m_programs.clear();
m_textures.clear();
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
for (GLint unit = 0; unit < maxImageUnits; ++unit) {
glBindImageTexture(static_cast<GLuint>(unit), 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
}
while (glGetError() != GL_NO_ERROR) {
}
}
bool ImagesAreUsable() const {
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
GLint maxComputeImageUniforms = 0;
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
while (glGetError() != GL_NO_ERROR) {
}
return maxImageUnits > kImageUnit && maxComputeImageUniforms >= 1;
}
GLuint MakeComputeProgram(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[4096] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute shader did not compile: " << log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
m_programs.push_back(program);
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[4096] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute program did not link: " << log;
return 0;
}
return program;
}
GLuint TrackTexture() {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
return texture;
}
// layered = GL_TRUE, i.e. the whole level: that is what makes every layer reachable
// from one dispatch, and it is what glBindImageTextures is specified to pass.
bool DispatchStore(GLuint program, GLuint texture, GLsizei groupsX, GLsizei groupsY, GLsizei groupsZ) {
glBindImageTexture(static_cast<GLuint>(kImageUnit), texture, 0, GL_TRUE, 0, GL_WRITE_ONLY, GL_R32UI);
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << "glBindImageTexture errored with " << GLErrorName(error);
return false;
}
glUseProgram(program);
const GLint location = glGetUniformLocation(program, "uni_image");
if (location < 0) {
ADD_FAILURE() << "the image uniform was not reflected";
return false;
}
glUniform1i(location, kImageUnit);
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << "assigning the image unit errored with " << GLErrorName(error);
return false;
}
glDispatchCompute(groupsX, groupsY, groupsZ);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
glUseProgram(0);
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << "the dispatch errored with " << GLErrorName(error);
return false;
}
return true;
}
std::vector<GLuint> m_programs;
std::vector<GLuint> m_textures;
};
// The 1D-array half. A layer past the first is the whole test: layer 0 lines up with the
// ES image's only row whichever way the axes are read, so a readback that never swapped
// them still got it right and only the deeper layers came back wrong.
TEST_F(LayeredTextureReadbackScenario, GetTexImageReturnsEveryLayerOfA1DArray) {
if (!Ready()) return;
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";
const GLuint program = MakeComputeProgram(k1DArrayStoreSource);
if (program == 0) return;
const GLuint texture = TrackTexture();
glBindTexture(GL_TEXTURE_1D_ARRAY, texture);
glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
const std::vector<GLuint> seed(static_cast<std::size_t>(kExtent) * kArrayLayers, kSeed);
glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_R32UI, kExtent, kArrayLayers, 0, GL_RED_INTEGER, GL_UNSIGNED_INT,
seed.data());
ASSERT_EQ(FirstGLError(), 0u) << "creating the R32UI 1D-array texture errored";
if (!DispatchStore(program, texture, kExtent, 1, kArrayLayers)) return;
std::vector<GLuint> texels(seed.size(), 0u);
glBindTexture(GL_TEXTURE_1D_ARRAY, texture);
glGetTexImage(GL_TEXTURE_1D_ARRAY, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
ASSERT_EQ(FirstGLError(), 0u) << "reading the 1D-array level back errored";
// GL hands a 1D array back as a plain two-dimensional image whose ROWS are the
// layers, so the destination index is layer * width + x.
for (int layer = 0; layer < kArrayLayers; ++layer) {
for (int x = 0; x < kExtent; ++x) {
const std::size_t index = static_cast<std::size_t>(layer) * kExtent + x;
EXPECT_EQ(texels[index], Expected1DArrayTexel(x, layer))
<< "layer " << layer << " texel " << x << " read back "
<< (texels[index] == kSeed ? "the seed (the store never reached it, or the readback came "
"from the stale CPU shadow)"
: "an unexpected value");
}
}
}
// The cube-map-array half. glFramebufferTexture2D has no token for the target, so the
// scratch FBO used to stay incomplete and every read - including layer 0 - was answered
// from the CPU shadow; the seed is what makes that visible rather than merely wrong.
TEST_F(LayeredTextureReadbackScenario, GetTexImageReturnsEveryLayerFaceOfACubeMapArray) {
if (!Ready()) return;
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";
const GLuint program = MakeComputeProgram(kCubeArrayStoreSource);
if (program == 0) return;
const GLuint texture = TrackTexture();
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
const std::vector<GLuint> seed(static_cast<std::size_t>(kExtent) * kExtent * kCubeLayerFaces, kSeed);
glTexImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, GL_R32UI, kExtent, kExtent, kCubeLayerFaces, 0, GL_RED_INTEGER,
GL_UNSIGNED_INT, seed.data());
ASSERT_EQ(FirstGLError(), 0u) << "creating the R32UI cube-map-array texture errored";
if (!DispatchStore(program, texture, kExtent, kExtent, kCubeLayerFaces)) return;
std::vector<GLuint> texels(seed.size(), 0u);
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
glGetTexImage(GL_TEXTURE_CUBE_MAP_ARRAY, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
ASSERT_EQ(FirstGLError(), 0u) << "reading the cube-map-array level back errored";
for (int layerFace = 0; layerFace < kCubeLayerFaces; ++layerFace) {
for (int y = 0; y < kExtent; ++y) {
for (int x = 0; x < kExtent; ++x) {
const std::size_t index =
(static_cast<std::size_t>(layerFace) * kExtent + y) * kExtent + x;
EXPECT_EQ(texels[index], ExpectedCubeArrayTexel(x, y, layerFace))
<< "layer-face " << layerFace << " texel (" << x << ", " << y << ") read back "
<< (texels[index] == kSeed ? "the seed (the store never reached it, or the readback "
"came from the stale CPU shadow)"
: "an unexpected value");
}
}
}
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,220 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PackedWordReadbackScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// glGetTexImage of a 32-bit packed format read with its OWN client type owes the application the
// words the image HOLDS, and KHR-GL43.copy_image compares exactly those words. Two routes used to
// answer, and both are wrong for a level glCopyImageSubData wrote:
//
// * the colour-attachment route reads GL_RGBA/GL_FLOAT and re-encodes, which canonicalizes an
// RGB9_E5 shared exponent and collapses an R11F_G11F_B10F NaN payload to 1;
// * the CPU shadow only holds what was UPLOADED, and the mirror that replays a copy into it
// declines - silently - for a renderbuffer source, which has no shadow to mirror from.
//
// Both are pinned here with words the CTS itself uses, because both failures are invisible to a
// value comparison: every assertion below is on BITS that decode to the very value the wrong
// answer also decodes to.
//
// The fix is a raw-word route (DirectGLES::ReadPackedLevelWordsViaScratch: copy the level into a
// scratch GL_R32UI image, read that back as unsigned integers), and DirectVulkan reaches the same
// place through PackReadbackToClientOrPbo's raw-word branch over the staging bytes - so these
// scenarios are backend-agnostic on purpose.
#include <cstddef>
#include <ios>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr GLsizei kExtent = 4;
// The non-canonical RGB9_E5 word KHR-GL43.copy_image writes: R=0, G=0, B mantissa 63,
// shared exponent 31, i.e. the value 8064, which the spec's own encoder would emit as
// 0xe7e00000 instead. Anything that decodes and re-encodes hands back the canonical word.
//
// Reinterpreted in the destination of an RGB9_E5 -> R11F_G11F_B10F copy it is R=0,
// G=1920, B=995 - and B's 5-bit exponent is all ones with a nonzero mantissa, i.e. a NaN
// whose payload 3 does not survive a float32 round trip (it comes back as the canonical
// payload 1, B=993, word 0xf87c0000). The two defects therefore land on the same word.
constexpr GLuint kRgb9E5Word = 0xf8fc0000u;
// The R11F_G11F_B10F word the same test pairs with it: R=0, G=0, B = exponent 12,
// mantissa 0 = 0.125. As an RGB9_E5 word it is all-zero channels with a shared exponent of
// 12, which the canonical encoder would write as 0x00000000 - so a decode/re-encode of THIS
// one loses every bit that distinguishes it.
constexpr GLuint kR11fG11fB10fWord = 0x60000000u;
class PackedWordReadbackScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
DrainErrors();
}
void TearDown() override {
if (!Ready()) return;
DeleteObjects();
DrainErrors();
ScenarioTest::TearDown();
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
void DeleteObjects() {
if (m_src != 0) glDeleteTextures(1, &m_src);
if (m_dst != 0) glDeleteTextures(1, &m_dst);
if (m_rbo != 0) glDeleteRenderbuffers(1, &m_rbo);
m_src = 0;
m_dst = 0;
m_rbo = 0;
}
// A complete single-level texture whose every texel holds `word`, uploaded through the
// packed client type so the stored bits are the client's bits and nothing has had a
// chance to re-encode them.
GLuint MakePackedTexture(GLenum internalFormat, GLenum type, GLuint word) {
const std::vector<GLuint> words(static_cast<std::size_t>(kExtent) * kExtent, word);
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(internalFormat), kExtent, kExtent, 0, GL_RGB, type,
words.data());
// What Utils::makeTextureComplete does in the conformance cases, and what
// glCopyImageSubData requires of both endpoints.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
// Every texel of level 0, as raw client words.
std::vector<GLuint> ReadPackedWords(GLuint texture, GLenum type) {
std::vector<GLuint> words(static_cast<std::size_t>(kExtent) * kExtent, 0xDEADBEEFu);
glBindTexture(GL_TEXTURE_2D, texture);
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, type, words.data());
glBindTexture(GL_TEXTURE_2D, 0);
return words;
}
// The copy under test. Returns the error it raised so a driver that cannot perform the
// move at all can skip rather than fail: the point of these cases is which BITS come
// back, and there are none to compare if the copy never happened.
GLenum CopyWholeImage(GLuint srcName, GLenum srcTarget, GLuint dstName, GLenum dstTarget) {
DrainErrors();
glCopyImageSubData(srcName, srcTarget, 0, 0, 0, 0, dstName, dstTarget, 0, 0, 0, 0, kExtent, kExtent,
1);
const GLenum error = glGetError();
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the copy recorded more than one error";
return error;
}
static void ExpectEveryTexel(const std::vector<GLuint>& words, GLuint expected, const char* what) {
for (std::size_t i = 0; i < words.size(); ++i) {
ASSERT_EQ(words[i], expected)
<< what << ": texel " << i << " read 0x" << std::hex << words[i] << ", expected 0x"
<< expected;
}
}
GLuint m_src = 0;
GLuint m_dst = 0;
GLuint m_rbo = 0;
};
// The control that has to hold before either regression means anything: a packed word
// uploaded and read straight back must be the SAME word, not merely the same colour.
TEST_F(PackedWordReadbackScenario, AnUploadedPackedWordReadsBackVerbatim) {
if (!Ready()) GTEST_SKIP();
m_src = MakePackedTexture(GL_RGB9_E5, GL_UNSIGNED_INT_5_9_9_9_REV, kRgb9E5Word);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "RGB9_E5 upload";
ExpectEveryTexel(ReadPackedWords(m_src, GL_UNSIGNED_INT_5_9_9_9_REV), kRgb9E5Word, "RGB9_E5 round trip");
m_dst = MakePackedTexture(GL_R11F_G11F_B10F, GL_UNSIGNED_INT_10F_11F_11F_REV, kR11fG11fB10fWord);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "R11F_G11F_B10F upload";
ExpectEveryTexel(ReadPackedWords(m_dst, GL_UNSIGNED_INT_10F_11F_11F_REV), kR11fG11fB10fWord,
"R11F_G11F_B10F round trip");
}
// KHR-GL43.copy_image.functional rgb9_e5 -> r11f_g11f_b10f, all nine target combinations of
// which failed on both GPUs. glCopyImageSubData is a raw block move, so the destination
// physically holds the source's word - but the readback decoded it to float and re-encoded,
// and the destination's blue field is a NaN whose payload float32 does not carry. Every
// texel came back 0xf87c0000 (payload 1) instead of 0xf8fc0000 (payload 3): the same
// "colour", two bits apart.
TEST_F(PackedWordReadbackScenario, ACopiedRgb9E5WordSurvivesInAnR11fG11fB10fDestination) {
if (!Ready()) GTEST_SKIP();
m_src = MakePackedTexture(GL_RGB9_E5, GL_UNSIGNED_INT_5_9_9_9_REV, kRgb9E5Word);
m_dst = MakePackedTexture(GL_R11F_G11F_B10F, GL_UNSIGNED_INT_10F_11F_11F_REV, 0u);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup";
const GLenum copyError = CopyWholeImage(m_src, GL_TEXTURE_2D, m_dst, GL_TEXTURE_2D);
if (copyError != static_cast<GLenum>(GL_NO_ERROR)) {
GTEST_SKIP() << "this driver declined the RGB9_E5 -> R11F_G11F_B10F copy (" << copyError << ")";
}
ExpectEveryTexel(ReadPackedWords(m_dst, GL_UNSIGNED_INT_10F_11F_11F_REV), kRgb9E5Word,
"copied word in the R11F_G11F_B10F destination");
// ...and the source is still the source. This is verify()'s FIRST check in the
// conformance case, and the half that a canonicalizing readback fails on its own.
ExpectEveryTexel(ReadPackedWords(m_src, GL_UNSIGNED_INT_5_9_9_9_REV), kRgb9E5Word,
"the RGB9_E5 source after the copy");
}
// KHR-GL43.copy_image.functional *->rgb9_e5 with a GL_RENDERBUFFER source: exactly the three
// renderbuffer combinations of each such family failed, and no texture one did. The
// destination's CPU shadow is what the readback answered from, the mirror that replays a
// copy into it declines when an endpoint is a renderbuffer (there is no shadow to mirror
// FROM), and the decline is silent - so glGetTexImage handed back the destination's
// pre-copy contents. The word chosen here makes that unmissable: it decodes to the same
// all-zero channels the canonical encoder would write as 0x00000000.
TEST_F(PackedWordReadbackScenario, ACopyThroughARenderbufferReachesAnRgb9E5Destination) {
if (!Ready()) GTEST_SKIP();
m_src = MakePackedTexture(GL_R11F_G11F_B10F, GL_UNSIGNED_INT_10F_11F_11F_REV, kR11fG11fB10fWord);
m_dst = MakePackedTexture(GL_RGB9_E5, GL_UNSIGNED_INT_5_9_9_9_REV, 0xFFFFFFFFu);
glGenRenderbuffers(1, &m_rbo);
glBindRenderbuffer(GL_RENDERBUFFER, m_rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_R11F_G11F_B10F, kExtent, kExtent);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "renderbuffer setup";
// The conformance case's own shape: texture -> renderbuffer -> texture.
const GLenum toRenderbuffer = CopyWholeImage(m_src, GL_TEXTURE_2D, m_rbo, GL_RENDERBUFFER);
if (toRenderbuffer != static_cast<GLenum>(GL_NO_ERROR)) {
GTEST_SKIP() << "this driver declined a renderbuffer copy destination (" << toRenderbuffer << ")";
}
const GLenum fromRenderbuffer = CopyWholeImage(m_rbo, GL_RENDERBUFFER, m_dst, GL_TEXTURE_2D);
if (fromRenderbuffer != static_cast<GLenum>(GL_NO_ERROR)) {
GTEST_SKIP() << "this driver declined a renderbuffer copy source (" << fromRenderbuffer << ")";
}
ExpectEveryTexel(ReadPackedWords(m_dst, GL_UNSIGNED_INT_5_9_9_9_REV), kR11fG11fB10fWord,
"copied word in the RGB9_E5 destination");
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,245 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SnormAttachmentScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - SIGNED-NORMALIZED COLOUR ATTACHMENTS, on a live driver.
//
// The bug: a GLES driver without GL_EXT_render_snorm treats every signed-normalized format as
// texture-only. DirectGLES had a colour-renderable substitute for exactly one of the eight
// (GL_RGB16_SNORM, through the three-channel widening), so an R8_SNORM or R16_SNORM attachment got
// no storage the driver would render into: the ES framebuffer was incomplete, the draw landed
// nowhere, and glGetTexImage fell through to the CPU shadow - all zeroes for a texture created with
// no data. KHR-GL4x.texture_swizzle renders into a SINGLE-CHANNEL SNORM output for every one of its
// SNORM source formats, which is why all 46 of its GL43 SNORM cases failed on Mali.
//
// THE OTHER HALF, and the reason this scenario asserts VALUES rather than only completeness: the
// substitute has to be exact. A half float's 11-bit mantissa cannot represent a 16-bit SNORM
// channel - 23451/32767 quantizes about six SNORM steps away, against a conformance window of one -
// so the 16-bit formats must land on a 32-bit float even though the 8-bit ones are fine in a half.
// Trading 46 visible failures for silent precision loss in Iris' SNORM normal buffers would be the
// worse outcome, so the round trip below is pinned tightly enough to fail on a half-float substitute
// (tolerance two SNORM steps, half-float error six).
//
// WHAT THIS GATE CAN AND CANNOT SEE. Both CI drivers (Mesa llvmpipe) and Adreno expose
// GL_EXT_render_snorm, so they take the NATIVE path here and the substitution stays dead. That is
// precisely why the assertions are written as invariants of the format rather than of the fallback:
// "a signed-normalized colour attachment is complete and round-trips its channel values" has to
// hold whichever path answers it, so the scenario fails if anyone ever routes these formats to a
// lossy storage on a driver where it IS live. The substitution itself can only be observed on a
// device without EXT_render_snorm (Mali Immortalis-G925).
//
// DirectGLES only, like the three-channel scenario next door: DirectVulkan resolves SNORM formats
// on its own terms and asserting Espryt's answers there would pin a coincidence.
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr const char* kVS = R"(#version 330 core
in vec2 aPos;
void main() {
gl_Position = vec4(aPos, 0.0, 1.0);
}
)";
// A uniform rather than a literal so nothing can constant-fold the value into a different
// precision than the one the attachment stores.
constexpr const char* kFS = R"(#version 330 core
out vec4 oColor;
uniform float uValue;
void main() { oColor = vec4(uValue, 0.0, 0.0, 1.0); }
)";
constexpr int kSize = 8;
// The two channel values the round trip is pinned on. Both are positive on purpose:
// glReadPixels applies GL_CLAMP_READ_COLOR (GL_FIXED_ONLY by default) to a fixed-point
// colour buffer, so the negative half of a SNORM attachment reads back as 0 and would
// measure the clamp instead of the storage.
constexpr int kSnorm8Value = 99;
constexpr int kSnorm16Value = 23451;
class SnormAttachmentScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
if (Gl().BackendName() != "DirectGLES") {
GTEST_SKIP() << "the signed-normalized substitution is a DirectGLES fallback; backend is "
<< Gl().BackendName();
}
}
// A single-level 2D texture in `internalFormat`, or 0 when the driver rejects the
// storage outright (which is a different failure from rejecting the ATTACHMENT).
static GLuint MakeTexture(GLenum internalFormat) {
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kSize, kSize);
if (glGetError() != GL_NO_ERROR) {
glDeleteTextures(1, &texture);
return 0;
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
static GLenum SingleAttachmentStatus(GLenum internalFormat) {
const GLuint texture = MakeTexture(internalFormat);
if (texture == 0) return GL_NONE;
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
const GLenum status = glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &texture);
return status;
}
// Renders `value` into the red channel of a fresh `internalFormat` attachment and hands
// back what glReadPixels sees. Returns false when the framebuffer never came up, which
// is the failure mode this scenario exists for - a draw into an incomplete framebuffer
// is dropped by the driver and leaves the caller reading the cleared texture.
bool RenderAndReadRed(GLenum internalFormat, float value, float* outRed) {
std::string error;
const GLuint program = CompileProgram(kVS, kFS, &error);
EXPECT_NE(program, 0u) << error;
if (program == 0) return false;
const GLint valueLocation = glGetUniformLocation(program, "uValue");
EXPECT_GE(valueLocation, 0);
const GLuint texture = MakeTexture(internalFormat);
EXPECT_NE(texture, 0u) << "the driver refused the texture storage itself";
if (texture == 0) {
glDeleteProgram(program);
return false;
}
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
const bool complete = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
if (complete) {
const float quad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
GLuint vao = 0;
GLuint vbo = 0;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
glUseProgram(program);
glUniform1f(valueLocation, value);
glViewport(0, 0, kSize, kSize);
// Cleared to zero so a dropped draw cannot be mistaken for a correct one.
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
std::vector<float> pixels(static_cast<std::size_t>(kSize) * kSize * 4, -1.0f);
glReadBuffer(GL_COLOR_ATTACHMENT0);
glReadPixels(0, 0, kSize, kSize, GL_RGBA, GL_FLOAT, pixels.data());
if (outRed) *outRed = pixels[0];
glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &texture);
glDeleteProgram(program);
return complete;
}
};
// THE regression gate for the frontend's answer. Every one of these used to be
// GL_FRAMEBUFFER_UNSUPPORTED on a driver without EXT_render_snorm, and nothing in the CTS
// (or in Iris) checks the status before drawing, so the failure was silent all the way to a
// readback of zeroes.
TEST_F(SnormAttachmentScenario, SignedNormalizedColorAttachmentsReportComplete) {
if (!Ready() || IsSkipped()) return;
// GL_R8 is the control: colour-renderable in ES core, so it must pass with or without
// any substitution. If it ever fails, nothing below means anything.
EXPECT_EQ(SingleAttachmentStatus(GL_R8), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
<< "GL_R8 is ES-core colour-renderable";
// The single-channel pair KHR-GL4x.texture_swizzle renders into for every SNORM source
// format - the whole 46-case failure.
EXPECT_EQ(SingleAttachmentStatus(GL_R8_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
EXPECT_EQ(SingleAttachmentStatus(GL_R16_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
// ...and the two- and four-channel siblings, which are what a shaderpack actually
// declares (Iris colortex buffers in RGBA16_SNORM).
EXPECT_EQ(SingleAttachmentStatus(GL_RG8_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
EXPECT_EQ(SingleAttachmentStatus(GL_RG16_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
EXPECT_EQ(SingleAttachmentStatus(GL_RGBA8_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
EXPECT_EQ(SingleAttachmentStatus(GL_RGBA16_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
}
// The other half: whatever storage answers for the attachment has to hold the channel value
// to the format's own precision. This is the assertion that fails if the 16-bit formats are
// ever routed to a half float - the substitute an implementer naturally reaches for, because
// it is what the 8-bit ones correctly use.
TEST_F(SnormAttachmentScenario, SignedNormalizedAttachmentsRoundTripTheirChannelValues) {
if (!Ready() || IsSkipped()) return;
const float snorm8Expected = static_cast<float>(kSnorm8Value) / 127.0f;
float red8 = -1.0f;
ASSERT_TRUE(RenderAndReadRed(GL_R8_SNORM, snorm8Expected, &red8))
<< "an R8_SNORM colour attachment must be complete before any value can be asserted";
// Two 8-bit SNORM steps. A half float is exact here (worst case 0.03 of a step), so this
// only has to catch a storage that quantizes harder than the format itself.
EXPECT_NEAR(red8, snorm8Expected, 2.0f / 127.0f)
<< "R8_SNORM attachment lost its channel value";
EXPECT_GT(red8, 0.5f) << "the draw never landed - this is the cleared texture, not the rendered one";
const float snorm16Expected = static_cast<float>(kSnorm16Value) / 32767.0f;
float red16 = -1.0f;
ASSERT_TRUE(RenderAndReadRed(GL_R16_SNORM, snorm16Expected, &red16))
<< "an R16_SNORM colour attachment must be complete before any value can be asserted";
// Two 16-bit SNORM steps (6.1e-5). A half float would land 1.9e-4 away - three times
// this window - which is exactly the failure this bound exists to catch.
EXPECT_NEAR(red16, snorm16Expected, 2.0f / 32767.0f)
<< "R16_SNORM attachment was stored in something that cannot hold 16 signed bits";
EXPECT_GT(red16, 0.5f) << "the draw never landed - this is the cleared texture, not the rendered one";
float red16x4 = -1.0f;
ASSERT_TRUE(RenderAndReadRed(GL_RGBA16_SNORM, snorm16Expected, &red16x4))
<< "an RGBA16_SNORM colour attachment must be complete before any value can be asserted";
EXPECT_NEAR(red16x4, snorm16Expected, 2.0f / 32767.0f)
<< "RGBA16_SNORM attachment was stored in something that cannot hold 16 signed bits";
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,189 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SsboArrayDynamicIndexScenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - A NON-CONSTANT INDEX INTO AN ARRAY OF SHADER STORAGE BLOCKS.
//
// GL 4.3 allows any dynamically-uniform expression there; GLSL ES keeps the ES 3.1 rule that the
// index must be a constant integral expression, and the Qualcomm compiler enforces it:
//
// '[' : indexing into an SSBO array using a non-constant expression is not permitted
//
// The stage then never compiles, the backend program links nothing, and every dispatch is a
// silent no-op - while glGetProgramiv(GL_LINK_STATUS) keeps reporting the successful link the
// frontend already published. That is why the conformance failures
// (KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case1/case4,
// advanced-indirectAddressing-case2, compute_shader.resources-max, 7 cases in all) read back as
// "the buffer was never written" rather than as an error, and why this scenario asserts on
// contents rather than on link status.
//
// Both index shapes the legalization has to cover are exercised in one dispatch: a loop induction
// variable (which folds when the loop unrolls) and a `uniform int` (which nothing can fold, so the
// switch/select lowering is what carries it), for a read AND for a write.
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// Bindings 0..3 are the block array, 4 is the output.
constexpr const char* kComputeSource = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Slot {
uint value;
} g_slots[4];
layout(std430, binding = 4) buffer Output {
uint g_result[];
};
uniform int g_index;
void main() {
// Loop-derived index: foldable by unrolling.
for (int i = 0; i < 4; ++i) {
g_result[i] = g_slots[i].value;
}
// Uniform-derived index: not foldable, read and write both.
g_result[4] = g_slots[g_index].value;
g_slots[g_index].value = 99u;
}
)";
constexpr int kSlotCount = 4;
constexpr int kResultCount = 5;
class SsboArrayDynamicIndexScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
GLint blocks = 0;
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &blocks);
if (blocks < kSlotCount + 1) {
GTEST_SKIP() << "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS is " << blocks << "; this needs "
<< kSlotCount + 1;
}
m_program = CompileComputeProgram(kComputeSource);
ASSERT_NE(m_program, 0u) << m_buildLog;
}
void TearDown() override {
if (!Ready()) return;
if (!m_buffers.empty()) glDeleteBuffers(static_cast<GLsizei>(m_buffers.size()), m_buffers.data());
if (m_program != 0) glDeleteProgram(m_program);
}
unsigned int CompileComputeProgram(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[2048] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute shader did not compile: ") + log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[2048] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute program did not link: ") + log;
glDeleteProgram(program);
return 0;
}
return program;
}
GLuint MakeStorageBuffer(const std::vector<unsigned int>& contents) {
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
glBufferData(GL_SHADER_STORAGE_BUFFER,
static_cast<GLsizeiptr>(contents.size() * sizeof(unsigned int)), contents.data(),
GL_DYNAMIC_COPY);
m_buffers.push_back(buffer);
return buffer;
}
static std::vector<unsigned int> ReadBuffer(GLuint buffer, int count) {
std::vector<unsigned int> values(static_cast<std::size_t>(count), 0xDEADBEEFu);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
static_cast<GLsizeiptr>(values.size() * sizeof(unsigned int)), values.data());
return values;
}
unsigned int m_program = 0;
std::string m_buildLog;
std::vector<GLuint> m_buffers;
};
} // namespace
TEST_F(SsboArrayDynamicIndexScenario, ReadsAndWritesTheBlockTheIndexNames) {
if (!Ready() || IsSkipped()) return;
GLuint slots[kSlotCount] = {};
for (int i = 0; i < kSlotCount; ++i) {
slots[i] = MakeStorageBuffer({static_cast<unsigned int>(10 + i)});
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(i), slots[i]);
}
const GLuint output = MakeStorageBuffer(std::vector<unsigned int>(kResultCount, 0u));
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, kSlotCount, output);
ASSERT_EQ(FirstGLError(), 0u);
glUseProgram(m_program);
const GLint indexLocation = glGetUniformLocation(m_program, "g_index");
ASSERT_NE(indexLocation, -1);
glUniform1i(indexLocation, 2);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
EXPECT_EQ(FirstGLError(), 0u);
const std::vector<unsigned int> result = ReadBuffer(output, kResultCount);
for (int i = 0; i < kSlotCount; ++i) {
EXPECT_EQ(result[static_cast<std::size_t>(i)], static_cast<unsigned int>(10 + i))
<< "g_slots[" << i << "] read through the loop index came back as "
<< result[static_cast<std::size_t>(i)]
<< "; 0 means the stage never compiled and the dispatch was a silent no-op";
}
EXPECT_EQ(result[4], 12u) << "g_slots[g_index] with g_index = 2 read back as " << result[4];
const std::vector<unsigned int> written = ReadBuffer(slots[2], 1);
EXPECT_EQ(written[0], 99u) << "the uniform-indexed WRITE landed as " << written[0]
<< " instead of 99 in g_slots[2]";
// The write must have gone to element 2 and nowhere else.
for (int i = 0; i < kSlotCount; ++i) {
if (i == 2) continue;
const std::vector<unsigned int> untouched = ReadBuffer(slots[i], 1);
EXPECT_EQ(untouched[0], static_cast<unsigned int>(10 + i))
<< "g_slots[" << i << "] was overwritten by a write that named element 2";
}
for (int i = 0; i <= kSlotCount; ++i) {
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(i), 0);
}
}
} // namespace MGITest
@@ -0,0 +1,156 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/StorageBufferRegrowScenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - glBufferData GROWS A BUFFER THAT IS ALREADY BOUND AT AN INDEXED POINT.
//
// GL says the indexed binding follows the buffer object, so after the store is re-specified the
// shader sees the NEW extent. DirectGLES shadows the indexed bindings so a redundant
// glBindBufferBase can be skipped, and nothing used to invalidate that shadow when the store was
// re-specified underneath it - so on a driver that resolves a whole-buffer indexed binding's
// extent at BIND time (Adreno does; Mali does not) the shader kept seeing the OLD, smaller range.
// Stores past it are dropped and loads return zero, which is exactly what
// KHR-GL43.compute_shader.dispatch-indirect reported: the first iteration's 6 elements correct and
// everything past byte 24 zero, after the same buffer was re-specified from 24 to 96 bytes.
//
// The assertion is deliberately on the WHOLE grown range, so a partial write names the byte the
// stale extent stopped at.
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr const char* kComputeSource = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Output {
uint g_data[];
};
void main() {
g_data[gl_GlobalInvocationID.x] = gl_GlobalInvocationID.x + 1u;
}
)";
constexpr int kSmallElements = 6; // 24 bytes - the first iteration's size
constexpr int kLargeElements = 24; // 96 bytes - what the second iteration grows to
class StorageBufferRegrowScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_program = CompileComputeProgram(kComputeSource);
ASSERT_NE(m_program, 0u) << m_buildLog;
glGenBuffers(1, &m_buffer);
}
void TearDown() override {
if (!Ready()) return;
if (m_buffer != 0) glDeleteBuffers(1, &m_buffer);
if (m_program != 0) glDeleteProgram(m_program);
}
unsigned int CompileComputeProgram(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[2048] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute shader did not compile: ") + log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[2048] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute program did not link: ") + log;
glDeleteProgram(program);
return 0;
}
return program;
}
void RespecifyTo(int elements) {
const std::vector<unsigned int> zeros(static_cast<std::size_t>(elements), 0u);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_buffer);
glBufferData(GL_SHADER_STORAGE_BUFFER,
static_cast<GLsizeiptr>(zeros.size() * sizeof(unsigned int)), zeros.data(),
GL_DYNAMIC_COPY);
}
std::vector<unsigned int> DispatchAndRead(int elements) {
glUseProgram(m_program);
glDispatchCompute(static_cast<GLuint>(elements), 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
std::vector<unsigned int> values(static_cast<std::size_t>(elements), 0xDEADBEEFu);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_buffer);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
static_cast<GLsizeiptr>(values.size() * sizeof(unsigned int)), values.data());
return values;
}
unsigned int m_program = 0;
GLuint m_buffer = 0;
std::string m_buildLog;
};
} // namespace
TEST_F(StorageBufferRegrowScenario, AGrownStoreIsVisibleThroughItsExistingIndexedBinding) {
if (!Ready() || IsSkipped()) return;
// Iteration one: 24 bytes, bound once, six groups.
RespecifyTo(kSmallElements);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_buffer);
ASSERT_EQ(FirstGLError(), 0u);
const std::vector<unsigned int> small = DispatchAndRead(kSmallElements);
ASSERT_EQ(FirstGLError(), 0u);
for (int i = 0; i < kSmallElements; ++i) {
ASSERT_EQ(small[static_cast<std::size_t>(i)], static_cast<unsigned int>(i + 1))
<< "the 24-byte iteration itself did not write element " << i;
}
// Iteration two: the SAME buffer grows to 96 bytes with NO new glBindBufferBase, which is
// what the application is entitled to do and what the shadow used to swallow.
RespecifyTo(kLargeElements);
ASSERT_EQ(FirstGLError(), 0u);
const std::vector<unsigned int> large = DispatchAndRead(kLargeElements);
EXPECT_EQ(FirstGLError(), 0u);
for (int i = 0; i < kLargeElements; ++i) {
EXPECT_EQ(large[static_cast<std::size_t>(i)], static_cast<unsigned int>(i + 1))
<< "element " << i << " (byte " << i * 4 << ") of the grown store came back as "
<< large[static_cast<std::size_t>(i)]
<< "; zero from element " << kSmallElements
<< " on means the shader still saw the pre-growth extent";
}
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
}
} // namespace MGITest
@@ -0,0 +1,226 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TessellationDrawModeScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - GL_PATCHES AND THE TESSELLATION PIPELINE ARE EACH OTHER'S ONLY PARTNER.
//
// GL 4.6 core 10.1 states the rule in both directions, and both are GL_INVALID_OPERATION:
// a program with a tessellation evaluation shader may only be drawn with GL_PATCHES, and
// GL_PATCHES may only be drawn with such a program. MobileGL's draw-mode validator
// implemented the geometry-shader input-primitive rule and NOTHING for tessellation, which
// is two of the four sites KHR-GL43.transform_feedback.api_errors_test checks (all four
// share one copy-pasted message string, so the trace cannot say which one it stopped at).
//
// Needs a real context: the validator returns before either rule when no backend object is
// active, so the GPU-free negative-API suite cannot reach them.
#include <string>
#include <utility>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
const char* const kVertexSource = R"(#version 420 core
void main()
{
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
const char* const kTessControlSource = R"(#version 420 core
layout(vertices = 1) out;
void main()
{
gl_TessLevelOuter[0] = 1.0;
gl_TessLevelOuter[1] = 1.0;
gl_TessLevelOuter[2] = 1.0;
gl_TessLevelInner[0] = 1.0;
gl_out[gl_InvocationID].gl_Position = gl_in[0].gl_Position;
}
)";
const char* const kTessEvalSource = R"(#version 420 core
layout(triangles, equal_spacing, cw) in;
void main()
{
gl_Position = gl_in[0].gl_Position;
}
)";
const char* const kFragmentSource = R"(#version 420 core
out vec4 fragColor;
void main()
{
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
)";
class TessellationDrawModeScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
if (!BackendHostsTessellation()) {
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " ("
<< Gl().RendererString() << "); there is no patch draw to validate";
}
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (const GLuint program : m_programs) {
glDeleteProgram(program);
}
m_programs.clear();
glBindVertexArray(0);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
m_vao = 0;
}
// The same real-backend probe IoBlockNameCollisionScenario uses: 0 on a DirectGLES
// driver without GL_EXT_tessellation_shader and on a DirectVulkan device without
// the tessellationShader feature.
static bool BackendHostsTessellation() {
GLint maxTessGenLevel = 0;
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
DrainErrors();
return maxTessGenLevel >= 1;
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
GLuint BuildProgram(const std::vector<std::pair<GLenum, const char*>>& stages) {
std::vector<GLuint> shaders;
bool ok = true;
for (const auto& [stage, source] : stages) {
const GLuint shader = glCreateShader(stage);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
shaders.push_back(shader);
if (!compiled) {
m_buildLog = InfoLog(shader, true);
ok = false;
break;
}
}
if (!ok) {
for (const GLuint shader : shaders) glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
for (const GLuint shader : shaders) glAttachShader(program, shader);
glLinkProgram(program);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
for (const GLuint shader : shaders) glDeleteShader(shader);
if (!linked) {
m_buildLog = InfoLog(program, false);
glDeleteProgram(program);
return 0;
}
m_programs.push_back(program);
return program;
}
static std::string InfoLog(GLuint object, bool isShader) {
GLint length = 0;
if (isShader) {
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
} else {
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
}
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
if (isShader) {
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
} else {
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
}
return buffer.data();
}
const std::string& BuildLog() const { return m_buildLog; }
GLuint m_vao = 0;
std::vector<GLuint> m_programs;
std::string m_buildLog;
};
// A tessellation program drawn with anything but GL_PATCHES.
TEST_F(TessellationDrawModeScenario, TessellationProgramRejectsNonPatchModes) {
if (!Ready()) GTEST_SKIP();
const GLuint program = BuildProgram({{GL_VERTEX_SHADER, kVertexSource},
{GL_TESS_CONTROL_SHADER, kTessControlSource},
{GL_TESS_EVALUATION_SHADER, kTessEvalSource},
{GL_FRAGMENT_SHADER, kFragmentSource}});
ASSERT_NE(program, 0u) << "the tessellation program did not build: " << BuildLog();
glUseProgram(program);
glPatchParameteri(GL_PATCH_VERTICES, 1);
DrainErrors();
for (const GLenum mode : {static_cast<GLenum>(GL_POINTS), static_cast<GLenum>(GL_LINES),
static_cast<GLenum>(GL_TRIANGLES)}) {
glDrawArrays(mode, 0, 1);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
<< "mode " << mode << " must not be accepted while tessellation is active";
DrainErrors();
}
// The one mode that IS accepted still is - a rule keyed any wider would break every
// patch draw in the suite.
glDrawArrays(GL_PATCHES, 0, 1);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
DrainErrors();
}
// ... and the other direction: GL_PATCHES without a tessellation evaluation stage.
TEST_F(TessellationDrawModeScenario, PatchesRejectedWithoutATessellationEvaluationStage) {
if (!Ready()) GTEST_SKIP();
const GLuint program =
BuildProgram({{GL_VERTEX_SHADER, kVertexSource}, {GL_FRAGMENT_SHADER, kFragmentSource}});
ASSERT_NE(program, 0u) << "the vertex/fragment program did not build: " << BuildLog();
glUseProgram(program);
glPatchParameteri(GL_PATCH_VERTICES, 1);
DrainErrors();
glDrawArrays(GL_PATCHES, 0, 1);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
<< "GL_PATCHES has no meaning without a tessellation evaluation stage";
DrainErrors();
// The same program with an ordinary mode is untouched.
glDrawArrays(GL_TRIANGLES, 0, 3);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
DrainErrors();
}
} // namespace
} // namespace MGITest
@@ -673,4 +673,86 @@ void main() {
glDeleteProgram(program); glDeleteProgram(program);
} }
// A GL_DOUBLE array is NARROWED to float32 and fetched, not dropped. No backend here has a
// 64-bit vertex format, but glVertexAttribFormat(GL_DOUBLE) is defined as "doubles in memory,
// converted to float" and the shader input is a plain vec4 either way, so nothing about fp64
// is needed - only the fetch conversion (KHR-GL43.vertex_attrib_binding.basic-input-case4).
// Every value here is exact in float32, so the capture is an equality test.
TEST_F(VertexAttribBindingScenario, DoubleArrayIsFetchedAtFloat32Precision) {
if (!Ready()) GTEST_SKIP();
ResetCurrentAttribs();
const double vertices[] = {100.0, 200.0, 300.0, 400.0};
GLuint vbo = 0;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexBuffer(0, vbo, 0, 2 * static_cast<GLsizei>(sizeof(double)));
glVertexAttribFormat(1, 2, GL_DOUBLE, GL_FALSE, 0);
glVertexAttribBinding(1, 0);
glEnableVertexAttribArray(1);
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
EXPECT_TRUE(Vec4Is(data, 0, 1, 100.0f, 200.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 1, 1, 300.0f, 400.0f, 0.0f, 1.0f));
glDisableVertexAttribArray(1);
glDeleteBuffers(1, &vbo);
}
// GL ignores `normalized` for floating-point array types, GL_DOUBLE included: the fetched
// values are the raw ones, not scaled into [0,1]. A conversion that forwarded the flag would
// return zeros here (KHR-GL43.vertex_attrib_binding.basic-input-case5).
TEST_F(VertexAttribBindingScenario, NormalizedIsIgnoredForDoubleArrays) {
if (!Ready()) GTEST_SKIP();
ResetCurrentAttribs();
const double vertices[] = {0.0, 10.0, 20.0, 0.0};
GLuint vbo = 0;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexBuffer(0, vbo, 0, 4 * static_cast<GLsizei>(sizeof(double)));
glVertexAttribFormat(2, 4, GL_DOUBLE, GL_TRUE, 0);
glVertexAttribBinding(2, 0);
glEnableVertexAttribArray(2);
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 1, 1);
EXPECT_TRUE(Vec4Is(data, 0, 2, 0.0f, 10.0f, 20.0f, 0.0f));
glDisableVertexAttribArray(2);
glDeleteBuffers(1, &vbo);
}
// The LONG form asks for more precision than any backend here can give and gets the same
// float32 stream. IsLong must not gate the narrowing off
// (KHR-GL43.vertex_attrib_binding.advanced-bindingUpdate feeds its dvec3 this way).
TEST_F(VertexAttribBindingScenario, LongDoubleArrayIsFetchedAtFloat32Precision) {
if (!Ready()) GTEST_SKIP();
ResetCurrentAttribs();
const double vertices[] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
GLuint vbo = 0;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexBuffer(0, vbo, 0, 3 * static_cast<GLsizei>(sizeof(double)));
glVertexAttribLFormat(3, 3, GL_DOUBLE, 0);
glVertexAttribBinding(3, 0);
glEnableVertexAttribArray(3);
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
EXPECT_TRUE(Vec4Is(data, 0, 3, 1.0f, 2.0f, 3.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 1, 3, 4.0f, 5.0f, 6.0f, 1.0f));
glDisableVertexAttribArray(3);
glDeleteBuffers(1, &vbo);
}
} // namespace MGITest } // namespace MGITest
@@ -520,5 +520,215 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
DestroyIntTarget(target); DestroyIntTarget(target);
} }
// --- 4. an explicitly EMPTY scissor box clips, it does not mean "never written" --------
//
// Deliberately NOT a ViewportArrayScenario case, because it must run on DirectGLES - the
// backend that got it wrong - and that fixture skips there. It needs none of the routing:
// one viewport, one scissor rectangle, no geometry stage.
//
// glScissor(0, 0, 0, 0) is legal GL meaning "the scissor test rejects every fragment",
// but it is byte-identical to the all-zero rectangle a context starts with, whose meaning
// is the OPPOSITE ("the whole window", which the frontend cannot spell before a surface
// exists). DirectGLES resolved the collision from the EXTENT, so it substituted the whole
// surface for a deliberately empty box and inverted the request into "clip nothing" -
// and did so on every draw, at any origin, no matter how many times the application had
// already called glScissor. KHR-GL43.viewport_array.scissor_zero_dimension is the
// conformance shape of exactly this, and it is what the written-flag now separates.
const char* const kFullScreenVertexSource = R"(#version 330 core
void main() {
// One clip-space-covering triangle straight from gl_VertexID: no buffers, no attributes,
// and nothing that could clip the draw except the scissor rectangle under test.
const vec2 corners[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
gl_Position = vec4(corners[gl_VertexID], 0.0, 1.0);
}
)";
const char* const kConstantIntFragmentSource = R"(#version 330 core
layout(location = 0) out int fragColor;
void main() { fragColor = 7; }
)";
constexpr GLint kPainted = 7;
class EmptyScissorScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_program = BuildQuadProgram();
ASSERT_NE(m_program, 0u) << "full-screen program failed to build: " << m_buildLog;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenTextures(1, &m_texture);
glBindTexture(GL_TEXTURE_2D, m_texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32I, kSurfaceSide, kSurfaceSide, 0, GL_RED_INTEGER, GL_INT,
nullptr);
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GL_FRAMEBUFFER_COMPLETE)
<< "R32I is required to be colour-renderable; an incomplete target would make every "
"assertion below vacuous";
glViewport(0, 0, kSurfaceSide, kSurfaceSide);
glDisable(GL_DEPTH_TEST);
ResetScissorState();
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "setup left a GL error behind";
}
void TearDown() override {
if (!Ready() || IsSkipped()) return;
// The context is shared with every other scenario in the process, and a leftover
// 0x0 scissor box with the test enabled would silently blank whatever runs next.
ResetScissorState();
glScissor(0, 0, kSurfaceSide, kSurfaceSide);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_program != 0) glDeleteProgram(m_program);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
if (m_texture != 0) glDeleteTextures(1, &m_texture);
while (glGetError() != GL_NO_ERROR) {
}
}
static void ResetScissorState() {
for (int i = 0; i < kViewportCount; ++i) {
glDisablei(GL_SCISSOR_TEST, static_cast<GLuint>(i));
}
glDisable(GL_SCISSOR_TEST);
}
// Uploaded, not cleared, for the reason FillIntTarget gives - and here for a second
// one that is decisive: glClear is ITSELF scissored, so a clear issued under the very
// state this case is testing would be clipped away and prove nothing.
void FillTarget() const {
const std::vector<GLint> unwritten(static_cast<size_t>(kSurfaceSide) * kSurfaceSide, kUnwritten);
glBindTexture(GL_TEXTURE_2D, m_texture);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kSurfaceSide, kSurfaceSide, GL_RED_INTEGER, GL_INT,
unwritten.data());
}
static std::vector<GLint> ReadTarget() {
std::vector<GLint> pixels(static_cast<size_t>(kSurfaceSide) * kSurfaceSide, 0);
glReadPixels(0, 0, kSurfaceSide, kSurfaceSide, GL_RED_INTEGER, GL_INT, pixels.data());
return pixels;
}
GLuint BuildQuadProgram() {
const GLuint vs = CompileOne(GL_VERTEX_SHADER, kFullScreenVertexSource);
if (vs == 0) return 0;
const GLuint fs = CompileOne(GL_FRAGMENT_SHADER, kConstantIntFragmentSource);
if (fs == 0) {
glDeleteShader(vs);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
glDeleteShader(vs);
glDeleteShader(fs);
if (linked) return program;
GLint length = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), nullptr, log.data());
m_buildLog = log.data();
glDeleteProgram(program);
return 0;
}
GLuint CompileOne(GLenum stage, const char* source) {
const GLuint shader = glCreateShader(stage);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled) return shader;
GLint length = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
m_buildLog = log.data();
glDeleteShader(shader);
return 0;
}
std::string m_buildLog;
GLuint m_program = 0;
GLuint m_vao = 0;
GLuint m_fbo = 0;
GLuint m_texture = 0;
};
TEST_F(EmptyScissorScenario, AnExplicitlyEmptyScissorBoxClipsEveryFragment) {
// Positive control FIRST. Without it a regression that simply lost the draw entirely
// would sail through the half below, which only asserts that nothing was painted.
FillTarget();
glEnable(GL_SCISSOR_TEST);
glScissor(0, 0, kSurfaceSide, kSurfaceSide);
glUseProgram(m_program);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
ASSERT_EQ(glGetError(), GL_NO_ERROR);
{
const std::vector<GLint> pixels = ReadTarget();
ASSERT_EQ(pixels.front(), kPainted) << "control: a full-surface scissor box must not clip";
ASSERT_EQ(pixels.back(), kPainted) << "control: a full-surface scissor box must not clip";
}
// The case itself, and note it runs AFTER an explicit glScissor - the old
// extent-based sentinel misfired here too, which is what made this a live rendering
// bug and not just a first-frame startup quirk.
FillTarget();
glScissor(0, 0, 0, 0);
glDrawArrays(GL_TRIANGLES, 0, 3);
ASSERT_EQ(glGetError(), GL_NO_ERROR);
{
const std::vector<GLint> pixels = ReadTarget();
for (size_t i = 0; i < pixels.size(); ++i) {
ASSERT_EQ(pixels[i], kUnwritten)
<< "texel " << i << " was painted through a 0x0 scissor box: the empty rectangle was "
"substituted with the whole surface, inverting 'clip everything' into 'clip nothing'";
}
}
}
TEST_F(EmptyScissorScenario, IndexedZeroDimensionScissorBoxesClipEveryFragment) {
// The conformance shape: setup4x4Scissor(..., set_zeros=true) writes all 16 boxes
// through glScissorArrayv with zero extents at a 4x4 grid of origins and enables the
// test on every index. Index 0's box is (0, 0, 0, 0) - byte-identical to the
// never-written default - which is precisely the collision the written flag breaks.
// Backends that collapse every index to 0 (DirectGLES today) still pass: index 0's
// box is empty, so the draw is clipped away, which is what the case requires.
FillTarget();
std::vector<GLint> boxes(static_cast<size_t>(kViewportCount) * 4, 0);
for (int i = 0; i < kViewportCount; ++i) {
boxes[static_cast<size_t>(i) * 4 + 0] = (i % kGridSide) * kCellSize;
boxes[static_cast<size_t>(i) * 4 + 1] = (i / kGridSide) * kCellSize;
// width and height stay 0 - that IS the case.
}
glScissorArrayv(0, kViewportCount, boxes.data());
for (int i = 0; i < kViewportCount; ++i) {
glEnablei(GL_SCISSOR_TEST, static_cast<GLuint>(i));
}
glUseProgram(m_program);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
ASSERT_EQ(glGetError(), GL_NO_ERROR);
const std::vector<GLint> pixels = ReadTarget();
for (size_t i = 0; i < pixels.size(); ++i) {
ASSERT_EQ(pixels[i], kUnwritten) << "texel " << i << " was painted through a zero-extent indexed "
"scissor box";
}
}
} // namespace } // namespace
} // namespace MGITest } // namespace MGITest
@@ -0,0 +1,268 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/XfbPrimitiveQueryScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// What the two transform feedback queries report for a VERTEX-ONLY capture that
// OVERFLOWS its buffer - the shape of KHR-GL30.transform_feedback.query_vertex_*,
// and the one place where the two targets must disagree:
//
// * GL_PRIMITIVES_GENERATED counts what the capture stage assembled: 4 points.
// * GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN counts what the capture buffers
// took. With room for three vertices, a full buffer stops recording whole
// primitives (GL 4.6 core 13.2.2), so the answer is 3, not 4 and not 6.
//
// Both numbers came from the backend's own GPU counter until the driver underneath
// DirectGLES was caught reporting exactly twice the written count for this shape
// (Adreno 830, vertex-only capture issued right after a large render pass). The
// frontend already computes the desktop-exact number for a capture with no geometry
// stage, so that is what answers PRIMITIVES_WRITTEN there now - and this scenario is
// what pins the value, on every backend, without a device.
//
// The non-overflowing case is the negative control: with room for all four points
// the two targets must AGREE at 4, so a "written" that silently reports the
// generated count cannot pass both cases at once.
#include <cmath>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr float kPoison = -1234.0f;
// One vec4 per captured point.
constexpr std::size_t kFloatsPerVertex = 4;
constexpr std::size_t kBytesPerVertex = kFloatsPerVertex * sizeof(float);
// The draw: four points, whichever way the capture buffer is sized.
constexpr GLsizei kDrawnPoints = 4;
GLuint CompileShader(GLenum type, const std::string& source, std::string* log) {
const GLuint shader = glCreateShader(type);
const char* text = source.c_str();
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint status = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteShader(shader);
return 0;
}
return shader;
}
// Vertex-only capture program - no geometry stage, so nothing amplifies and the
// primitives written are the primitives drawn (up to the buffer's capacity).
GLuint BuildCaptureProgram(std::string* log) {
const std::string vertexSource = R"(#version 430 core
layout(location = 0) in vec4 vs_in_value;
out vec4 vs_out_value;
void main() {
vs_out_value = vs_in_value;
}
)";
const GLuint vertexShader = CompileShader(GL_VERTEX_SHADER, vertexSource, log);
if (vertexShader == 0) return 0;
const GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
const char* varying = "vs_out_value";
glTransformFeedbackVaryings(program, 1, &varying, GL_INTERLEAVED_ATTRIBS);
glLinkProgram(program);
glDeleteShader(vertexShader);
GLint status = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteProgram(program);
return 0;
}
return program;
}
class XfbPrimitiveQueryScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
std::string log;
m_program = BuildCaptureProgram(&log);
ASSERT_NE(m_program, 0u) << "capture program failed to build: " << log;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
// Vertex i is (i, i+1, i+2, i+3), so a record that landed in the wrong slot
// is as visible as one that never landed at all.
float vertices[kDrawnPoints * kFloatsPerVertex] = {};
for (int point = 0; point < kDrawnPoints; ++point) {
for (std::size_t component = 0; component < kFloatsPerVertex; ++component) {
vertices[static_cast<std::size_t>(point) * kFloatsPerVertex + component] =
static_cast<float>(point) + static_cast<float>(component);
}
}
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glGenQueries(2, m_queries);
ASSERT_NE(m_queries[0], 0u);
ASSERT_NE(m_queries[1], 0u);
}
void TearDown() override {
if (!Ready()) return;
glDeleteQueries(2, m_queries);
glBindVertexArray(0);
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_program != 0) glDeleteProgram(m_program);
glUseProgram(0);
ScenarioTest::TearDown();
}
// A capture buffer with room for exactly `vertexCapacity` records, poisoned so
// that "captured nothing" is legible, bound to capture point 0.
GLuint MakeCaptureBuffer(std::size_t vertexCapacity) {
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer);
const std::vector<float> poison(vertexCapacity * kFloatsPerVertex, kPoison);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER,
static_cast<GLsizeiptr>(vertexCapacity * kBytesPerVertex), poison.data(),
GL_DYNAMIC_DRAW);
return buffer;
}
// ONE capture span, four points, with both query targets open across it - the
// order KHR-GL30.transform_feedback.query_vertex_interleaved_test uses: the
// queries wrap the whole span, never the other way round.
void RunQueriedSpan(GLuint* written, GLuint* generated) {
glEnable(GL_RASTERIZER_DISCARD);
glUseProgram(m_program);
glBindVertexArray(m_vao);
glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, m_queries[0]);
glBeginQuery(GL_PRIMITIVES_GENERATED, m_queries[1]);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, kDrawnPoints);
glEndTransformFeedback();
glEndQuery(GL_PRIMITIVES_GENERATED);
glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
glDisable(GL_RASTERIZER_DISCARD);
glUseProgram(0);
*written = 0xFFFFFFFFu;
*generated = 0xFFFFFFFFu;
glGetQueryObjectuiv(m_queries[0], GL_QUERY_RESULT, written);
glGetQueryObjectuiv(m_queries[1], GL_QUERY_RESULT, generated);
}
// The capture record at slot `point` must be the vertex the draw fetched there.
static ::testing::AssertionResult CapturedVertexIs(const float* record, int point) {
for (std::size_t component = 0; component < kFloatsPerVertex; ++component) {
const float expected = static_cast<float>(point) + static_cast<float>(component);
const float got = record[component];
// isfinite first: every ordered comparison against a NaN is false, so a
// pair of one-sided range tests REPORTS SUCCESS for uninitialised storage
// that happens to read as NaN.
if (!std::isfinite(got) || std::fabs(got - expected) > 0.01f) {
return ::testing::AssertionFailure()
<< "point " << point << " component " << component << " is " << got << ", expected "
<< expected << (got == kPoison ? " (the capture never reached these bytes)" : "");
}
}
return ::testing::AssertionSuccess();
}
GLuint m_program = 0;
GLuint m_vao = 0;
GLuint m_vbo = 0;
GLuint m_queries[2] = {0, 0};
};
// The negative control: the buffer holds every point the draw produces, so both
// targets must report the same 4. A "written" that is really the generated count
// passes this case and fails the next one; a "written" that is really zero fails
// this one.
TEST_F(XfbPrimitiveQueryScenario, ACaptureThatFitsReportsEveryPrimitiveOnBothTargets) {
if (!Ready()) GTEST_SKIP();
const GLuint captureBuffer = MakeCaptureBuffer(kDrawnPoints);
GLuint written = 0;
GLuint generated = 0;
RunQueriedSpan(&written, &generated);
EXPECT_EQ(written, 4u);
EXPECT_EQ(generated, 4u);
std::vector<float> readback(kDrawnPoints * kFloatsPerVertex, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(kDrawnPoints * kBytesPerVertex), readback.data());
for (int point = 0; point < kDrawnPoints; ++point) {
EXPECT_TRUE(CapturedVertexIs(readback.data() + static_cast<std::size_t>(point) * kFloatsPerVertex,
point));
}
glDeleteBuffers(1, &captureBuffer);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// The pin: four points into a buffer sized for three. The fourth is not written, so
// the two targets part ways at 3 and 4 - the exact pair
// KHR-GL30.transform_feedback.query_vertex_interleaved_test checks, and the pair the
// Adreno driver counter got wrong (it answered 6).
TEST_F(XfbPrimitiveQueryScenario, AnOverflowingVertexOnlyCaptureStopsWritingAtTheBufferCapacity) {
if (!Ready()) GTEST_SKIP();
constexpr std::size_t kCapacityVertices = 3;
const GLuint captureBuffer = MakeCaptureBuffer(kCapacityVertices);
GLuint written = 0;
GLuint generated = 0;
RunQueriedSpan(&written, &generated);
EXPECT_EQ(written, 3u) << "the capture buffer holds " << kCapacityVertices << " points";
EXPECT_EQ(generated, 4u) << "every point the draw assembled is generated, capacity or not";
// The three records that DID fit are the first three points, in order: an
// overflow truncates the capture, it does not scramble or drop what preceded it.
std::vector<float> readback(kCapacityVertices * kFloatsPerVertex, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(kCapacityVertices * kBytesPerVertex), readback.data());
for (int point = 0; point < static_cast<int>(kCapacityVertices); ++point) {
EXPECT_TRUE(CapturedVertexIs(readback.data() + static_cast<std::size_t>(point) * kFloatsPerVertex,
point));
}
glDeleteBuffers(1, &captureBuffer);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
} // namespace
} // namespace MGITest
@@ -163,7 +163,7 @@ namespace MobileGL::MG_State::GLState {
if (!m_resource.IsGpuResident() && if (!m_resource.IsGpuResident() &&
!(m_mappingAccess & BufferMappingAccessBit::FlushExplicit)) { // if we didn't flush explicitly !(m_mappingAccess & BufferMappingAccessBit::FlushExplicit)) { // if we didn't flush explicitly
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) { if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) {
Memcpy(m_resource.Bytes() + m_mappedRange.start, m_stagingData.data(), Memcpy(m_resource.Bytes() + m_mappedRange.start, m_stagingData.data() + m_stagingBias,
m_mappedRange.end - m_mappedRange.start); m_mappedRange.end - m_mappedRange.start);
} }
NotifyFlushMappedRange(m_mappedRange, m_mappingAccess); NotifyFlushMappedRange(m_mappedRange, m_mappingAccess);
@@ -175,6 +175,7 @@ namespace MobileGL::MG_State::GLState {
m_isMapped = false; m_isMapped = false;
m_mappingAccess = BufferMappingAccessBit::Null; m_mappingAccess = BufferMappingAccessBit::Null;
m_mappedRange = {0, 0}; m_mappedRange = {0, 0};
m_stagingBias = 0;
m_ownsStagingData = false; m_ownsStagingData = false;
} }
@@ -193,7 +194,7 @@ namespace MobileGL::MG_State::GLState {
// FLUSH_EXPLICIT maps are never GPU-resident (only coherent maps are adopted), so // FLUSH_EXPLICIT maps are never GPU-resident (only coherent maps are adopted), so
// the staged bytes must be copied into the shadow before the backend reads them. // the staged bytes must be copied into the shadow before the backend reads them.
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) { if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) {
Memcpy(m_resource.Bytes() + start, m_stagingData.data() + offset, length); Memcpy(m_resource.Bytes() + start, m_stagingData.data() + m_stagingBias + offset, length);
} }
NotifyFlushMappedRange({start, end}, m_mappingAccess); NotifyFlushMappedRange({start, end}, m_mappingAccess);
} }
@@ -311,6 +312,9 @@ namespace MobileGL::MG_State::GLState {
m_mappedRange = {0, m_size}; m_mappedRange = {0, m_size};
if (m_mappingAccess & BufferMappingAccessBit::Write) { if (m_mappingAccess & BufferMappingAccessBit::Write) {
// glMapBuffer maps from offset 0, so no bias: the allocation's own
// GL_MIN_MAP_BUFFER_ALIGNMENT-aligned base is what the application must get.
m_stagingBias = 0;
m_stagingData.resize(m_size); m_stagingData.resize(m_size);
m_ownsStagingData = true; m_ownsStagingData = true;
@@ -372,14 +376,21 @@ namespace MobileGL::MG_State::GLState {
} }
if (access & BufferMappingAccessBit::Write) { if (access & BufferMappingAccessBit::Write) {
m_stagingData.resize(range.end - range.start); // ARB_map_buffer_alignment constrains (returned pointer - offset), not the pointer:
// a map at offset 63 must hand back a pointer 63 bytes past the alignment grid, which
// is exactly what the read path below gets for free from shadowBase + offset. The
// staging store has to be biased by the same phase to match, so it over-allocates by
// it and the mapped bytes start at data() + m_stagingBias.
m_stagingBias = range.start % MIN_MAP_BUFFER_ALIGNMENT;
const SizeT mappedLength = range.end - range.start;
m_stagingData.resize(m_stagingBias + mappedLength);
m_ownsStagingData = true; m_ownsStagingData = true;
if (!(access & (BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer))) { if (!(access & (BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer))) {
Memcpy(m_stagingData.data(), m_resource.Bytes() + range.start, m_stagingData.size()); Memcpy(m_stagingData.data() + m_stagingBias, m_resource.Bytes() + range.start, mappedLength);
} }
return m_stagingData.data(); return m_stagingData.data() + m_stagingBias;
} else { } else {
m_ownsStagingData = false; m_ownsStagingData = false;
return m_resource.Bytes() + range.start; return m_resource.Bytes() + range.start;
@@ -438,7 +449,7 @@ namespace MobileGL::MG_State::GLState {
return const_cast<Uint8*>(m_resource.Bytes()) + m_mappedRange.start; return const_cast<Uint8*>(m_resource.Bytes()) + m_mappedRange.start;
} }
if (m_ownsStagingData) { if (m_ownsStagingData) {
return const_cast<Uint8*>(m_stagingData.data()); return const_cast<Uint8*>(m_stagingData.data()) + m_stagingBias;
} }
return const_cast<Uint8*>(m_resource.Bytes()) + m_mappedRange.start; return const_cast<Uint8*>(m_resource.Bytes()) + m_mappedRange.start;
} }
@@ -239,7 +239,14 @@ namespace MobileGL {
// Set by MarkGpuWritten, cleared by SyncGpuWrites once the shadow is refreshed. // Set by MarkGpuWritten, cleared by SyncGpuWrites once the shadow is refreshed.
Bool m_gpuWritePending = false; Bool m_gpuWritePending = false;
Range1D m_mappedRange; Range1D m_mappedRange;
Vector<Uint8> m_stagingData; // The write-map staging store. MapAlignedData because the application is handed a
// pointer into it, and biased by m_stagingBias because ARB_map_buffer_alignment
// requires (returned pointer - offset) to be aligned, not the pointer itself: a range
// map at offset 63 must hand back a pointer sitting 63 bytes past the alignment grid.
// The bias is the offset's phase, so the mapped bytes still start at
// m_stagingData.data() + m_stagingBias and the allocation is that much longer.
MapAlignedData m_stagingData;
SizeT m_stagingBias = 0;
Bool m_ownsStagingData; Bool m_ownsStagingData;
}; };
} // namespace MG_State::GLState } // namespace MG_State::GLState
@@ -10,8 +10,56 @@
#include <Includes.h> #include <Includes.h>
#include <MG_Util/Types.h> #include <MG_Util/Types.h>
#include <bit> #include <bit>
#include <new>
#include <vector>
namespace MobileGL::MG_State::GLState { namespace MobileGL::MG_State::GLState {
// GL_MIN_MAP_BUFFER_ALIGNMENT. GL 4.2 / ARB_map_buffer_alignment fix the minimum at 64 and
// MobileGL advertises exactly that (MG_Impl/GLImpl/Getter/GL_Getter.cpp reads this constant),
// so under-reporting is not available - the implementation has to be brought up to the number
// instead. The promise is about POINTERS, not just the query: glMapBuffer must return a
// 64-byte-aligned pointer, and glMapBufferRange must return one whose base - the returned
// pointer minus the offset the caller asked for - is. Every pointer the frontend hands out
// comes from the shadow below or from BufferObject's staging buffer, and std::vector only
// promises alignof(std::max_align_t) (16 on aarch64), so both allocations carry the alignment
// themselves. One constant for the getter and the allocator, because the two may never
// disagree - the same reason the atomic-counter limits are shared through
// MG_Util/ShaderTranspiler/Types.h.
inline constexpr SizeT MIN_MAP_BUFFER_ALIGNMENT = 64;
// Allocator that gives every allocation MIN_MAP_BUFFER_ALIGNMENT. Deliberately minimal: the
// vectors it backs hold raw bytes and are only ever sized, so allocate/deallocate plus the
// rebinding and equality boilerplate std::vector requires is the whole interface.
template <typename T>
struct MapAlignedAllocator {
using value_type = T;
MapAlignedAllocator() noexcept = default;
template <typename U>
MapAlignedAllocator(const MapAlignedAllocator<U>&) noexcept {}
T* allocate(SizeT count) {
if (count == 0) return nullptr;
return static_cast<T*>(
::operator new(count * sizeof(T), std::align_val_t{MIN_MAP_BUFFER_ALIGNMENT}));
}
void deallocate(T* pointer, SizeT) noexcept {
::operator delete(pointer, std::align_val_t{MIN_MAP_BUFFER_ALIGNMENT});
}
template <typename U>
Bool operator==(const MapAlignedAllocator<U>&) const noexcept {
return true;
}
template <typename U>
Bool operator!=(const MapAlignedAllocator<U>&) const noexcept {
return false;
}
};
// Byte store for anything the application may end up holding a mapped pointer into.
using MapAlignedData = std::vector<Uint8, MapAlignedAllocator<Uint8>>;
// Opaque, refcounted handle to the backend's GPU storage for one buffer // Opaque, refcounted handle to the backend's GPU storage for one buffer
// (the driver-side resource). The active backend derives from it and attaches // (the driver-side resource). The active backend derives from it and attaches
// its own payload (VkBufferResource / GLESBufferResource). Held by PipeResource. // its own payload (VkBufferResource / GLESBufferResource). Held by PipeResource.
@@ -57,8 +105,8 @@ namespace MobileGL::MG_State::GLState {
} }
// Direct shadow access, used only by the backend's upload-from-shadow path, // Direct shadow access, used only by the backend's upload-from-shadow path,
// which never runs for a GPU-resident (persistent) buffer. // which never runs for a GPU-resident (persistent) buffer.
Data& Shadow() { return *m_shadow; } MapAlignedData& Shadow() { return *m_shadow; }
const Data& Shadow() const { return *m_shadow; } const MapAlignedData& Shadow() const { return *m_shadow; }
// Transition to persistent GPU residency: adopt the backend's coherent // Transition to persistent GPU residency: adopt the backend's coherent
// mapped base as the source of truth and drop the CPU shadow. The caller // mapped base as the source of truth and drop the CPU shadow. The caller
@@ -85,7 +133,10 @@ namespace MobileGL::MG_State::GLState {
SharedPtr<BackendBufferResource> ReleaseBackend() { return std::move(m_backend); } SharedPtr<BackendBufferResource> ReleaseBackend() { return std::move(m_backend); }
private: private:
SharedPtr<Data> m_shadow = MakeShared<Data>(); // MapAlignedData, not Data: a read-only glMapBuffer hands the application this very
// pointer, and a range map hands it base + offset, so the base has to be on the
// GL_MIN_MAP_BUFFER_ALIGNMENT grid for either to satisfy ARB_map_buffer_alignment.
SharedPtr<MapAlignedData> m_shadow = MakeShared<MapAlignedData>();
void* m_gpuMapped = nullptr; void* m_gpuMapped = nullptr;
SharedPtr<BackendBufferResource> m_backend; SharedPtr<BackendBufferResource> m_backend;
}; };
+58
View File
@@ -328,6 +328,7 @@ namespace MobileGL {
// transform feedback counter cannot see them - nothing was being captured. // transform feedback counter cannot see them - nothing was being captured.
void AddTransformFeedbackPausedPrimitives(Uint64 primitives) { void AddTransformFeedbackPausedPrimitives(Uint64 primitives) {
m_transformFeedbackPausedPrimitiveCounter += primitives; m_transformFeedbackPausedPrimitiveCounter += primitives;
m_transformFeedbackGeneratedPrimitiveCounter += primitives;
} }
Uint64 GetTransformFeedbackPausedPrimitiveCounter() const { Uint64 GetTransformFeedbackPausedPrimitiveCounter() const {
return m_transformFeedbackPausedPrimitiveCounter; return m_transformFeedbackPausedPrimitiveCounter;
@@ -342,8 +343,55 @@ namespace MobileGL {
// (pre-clamp; drives the GS strip capture-order fixup at EndTF). // (pre-clamp; drives the GS strip capture-order fixup at EndTF).
void AddTransformFeedbackInputPrimitives(Uint64 primitives) { void AddTransformFeedbackInputPrimitives(Uint64 primitives) {
m_transformFeedbackInputPrimitives += primitives; m_transformFeedbackInputPrimitives += primitives;
m_transformFeedbackGeneratedPrimitiveCounter += primitives;
} }
Uint64 GetTransformFeedbackInputPrimitives() const { return m_transformFeedbackInputPrimitives; } 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;
}
// 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). // Transform feedback objects (ARB_transform_feedback2 / GL 4.0 core).
// The capture state above and the indexed GL_TRANSFORM_FEEDBACK_BUFFER // The capture state above and the indexed GL_TRANSFORM_FEEDBACK_BUFFER
@@ -439,6 +487,16 @@ namespace MobileGL {
Uint64 m_transformFeedbackPausedPrimitiveCounter = 0; Uint64 m_transformFeedbackPausedPrimitiveCounter = 0;
Uint64 m_transformFeedbackCapturedVertices = 0; Uint64 m_transformFeedbackCapturedVertices = 0;
Uint64 m_transformFeedbackInputPrimitives = 0; Uint64 m_transformFeedbackInputPrimitives = 0;
Uint64 m_transformFeedbackGeneratedPrimitiveCounter = 0;
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. // Everything a transform feedback object owns while it is NOT the bound one.
struct TransformFeedbackObjectState { struct TransformFeedbackObjectState {
@@ -8,6 +8,8 @@
#include "ProgramLinkTask.h" #include "ProgramLinkTask.h"
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h> #include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
#include <MG_Util/Async/ShaderCompilePool.h> #include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h> #include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
@@ -36,6 +38,70 @@ namespace {
return std::min(backendLimit, capacity); return std::min(backendLimit, capacity);
} }
// Everything the post-link query surface ever asks a glslang::TType, flattened into a
// POD. The list is closed and was audited call site by call site: nothing after the link
// walks a struct, a type name or the AST, so there is no recursion to mirror.
//
// Why it has to be flattened at all: TObjectReflection::type points into the TProgram's
// OWN TPoolAllocator (reflection.cpp clones each TType into it), so every one of these
// pointers dangles the moment the TProgram is released - and releasing it is exactly what
// lets a link be served from the L1 translation memo without a parse.
static MobileGL::MG_State::GLState::ProgramObject::TypeFacts MakeTypeFacts(const glslang::TType* type) {
MobileGL::MG_State::GLState::ProgramObject::TypeFacts facts;
if (type == nullptr) return facts;
facts.isArray = type->isArray();
facts.isSizedArray = type->isSizedArray();
facts.isMatrix = type->isMatrix();
facts.isVector = type->isVector();
facts.isOpaque = type->isOpaque();
facts.isTexture = type->isTexture();
facts.isImage = type->isImage();
facts.isDouble = type->getBasicType() == glslang::EbtDouble;
facts.isVoid = type->getBasicType() == glslang::EbtVoid;
facts.basicType = static_cast<MobileGL::Int>(type->getBasicType());
// Stored RAW, exactly as glslang reports them (0 for a non-matrix, 1 for a scalar),
// because the callers already gate on isMatrix()/isVector() themselves.
facts.vectorSize = type->getVectorSize();
facts.matrixCols = type->getMatrixCols();
facts.matrixRows = type->getMatrixRows();
const glslang::TQualifier& qualifier = type->getQualifier();
facts.isBuffer = qualifier.storage == glslang::EvqBuffer;
facts.isPatch = qualifier.patch;
facts.hasIndex = qualifier.hasIndex();
facts.layoutIndex = static_cast<MobileGL::Int>(qualifier.layoutIndex);
facts.hasFormat = qualifier.hasFormat();
facts.layoutFormat = static_cast<MobileGL::Uint>(qualifier.getFormat());
facts.layoutMatrix = static_cast<MobileGL::Int>(qualifier.layoutMatrix);
return facts;
}
// One glslang::TObjectReflection, flattened. Shared by uniforms, blocks, pipe inputs and
// pipe outputs, because glslang reflects all four as TObjectReflection.
static MobileGL::MG_State::GLState::ProgramObject::ResourceReflection MakeResourceReflection(
const glslang::TObjectReflection& object) {
MobileGL::MG_State::GLState::ProgramObject::ResourceReflection record;
record.name = object.name;
record.glDefineType = object.glDefineType;
record.offset = object.offset;
record.size = object.size;
record.index = object.index;
record.counterIndex = object.counterIndex;
record.arrayStride = object.arrayStride;
record.topLevelArraySize = object.topLevelArraySize;
record.topLevelArrayStride = object.topLevelArrayStride;
record.binding = object.getBinding();
record.location = object.layoutLocation();
record.stages = static_cast<MobileGL::Uint32>(object.stages);
record.type = MakeTypeFacts(object.getType());
// GL_UNIFORM_SIZE / GL_ARRAY_SIZE, resolved here so no caller needs the TType:
// TObjectReflection::size carries the element count only for a NON-block array, so
// the sized-array outer count wins whenever it exists.
const glslang::TType* type = object.getType();
record.arraySize = (type != nullptr && type->isSizedArray()) ? type->getOuterArraySize()
: (object.size < 1 ? 1 : object.size);
return record;
}
static MobileGL::String StripArrayElementSuffix(const MobileGL::String& name) { static MobileGL::String StripArrayElementSuffix(const MobileGL::String& name) {
const MobileGL::SizeT bracket = name.find('['); const MobileGL::SizeT bracket = name.find('[');
return bracket == MobileGL::String::npos ? name : name.substr(0, bracket); return bracket == MobileGL::String::npos ? name : name.substr(0, bracket);
@@ -63,6 +129,180 @@ namespace {
return element; return element;
} }
// GL 4.6 core 7.7 / ARB_shader_atomic_counters: within one binding no two atomic counters
// may occupy the same bytes, every offset is a multiple of 4, and no counter may reach past
// GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE. glslang enforces all three in fixOffset(), which the
// Vulkan-relaxed parse never reaches - vkRelaxedRemapUniformVariable folds the atomic_uint
// into a synthesized storage block and returns from declareVariable() before fixOffset()
// runs, clearing explicitOffset on the way ("xxTODO: use logic from fixOffset()"). Two
// counters declared at the same binding AND the same offset therefore linked cleanly.
//
// The offsets themselves survive that lowering (reflection and the SPIR-V generator both
// honour layoutOffset), so the check belongs here, over the same model the GL queries answer
// from. Returns the info-log line for an illegal layout, empty for a legal one.
static MobileGL::String ValidateAtomicCounterLayout(glslang::TProgram& reflection) {
using MobileGL::Bool;
using MobileGL::Int;
using MobileGL::SizeT;
using MobileGL::String;
using MobileGL::Vector;
namespace Transpiler = MobileGL::MG_Util::ShaderTranspiler;
const Int blockCount = reflection.getNumUniformBlocks();
if (blockCount <= 0) return {};
const SizeT prefixLength = std::strlen(Transpiler::ATOMIC_COUNTER_BLOCK_PREFIX);
Vector<Bool> isCounterBlock(static_cast<SizeT>(blockCount), false);
Bool anyCounterBlock = false;
for (Int i = 0; i < blockCount; ++i) {
const auto& block = reflection.getUniformBlock(i);
isCounterBlock[static_cast<SizeT>(i)] =
block.name.compare(0, prefixLength, Transpiler::ATOMIC_COUNTER_BLOCK_PREFIX) == 0;
anyCounterBlock = anyCounterBlock || isCounterBlock[static_cast<SizeT>(i)];
}
if (!anyCounterBlock) return {}; // every program that declares no atomic counter
struct CounterSpan {
Int offset = 0;
Int size = 0;
String name;
};
Vector<Vector<CounterSpan>> spansByBlock(static_cast<SizeT>(blockCount));
const Int uniformCount = reflection.getNumUniformVariables();
for (Int i = 0; i < uniformCount; ++i) {
const auto& uniform = reflection.getUniform(i);
const Int owner = uniform.index;
if (owner < 0 || owner >= blockCount || !isCounterBlock[static_cast<SizeT>(owner)]) continue;
const Int offset = uniform.offset;
if (offset < 0) continue; // no offset recorded; nothing to compare
Int elements = uniform.size > 1 ? uniform.size : 1;
if (const glslang::TType* type = uniform.getType(); type != nullptr && type->isArray()) {
elements = type->isSizedArray() ? type->getCumulativeArraySize() : 1;
}
const Int size = elements * static_cast<Int>(sizeof(MobileGL::Uint32));
if (offset % 4 != 0) {
return std::format("Atomic counter '{}' is declared at offset {}, which is not a multiple of 4.",
uniform.name, offset);
}
if (offset > Transpiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE - size) {
return std::format("Atomic counter '{}' ends at byte {}, past the {}-byte "
"GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE.",
uniform.name, offset + size, Transpiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE);
}
auto& spans = spansByBlock[static_cast<SizeT>(owner)];
for (const CounterSpan& existing : spans) {
if (offset < existing.offset + existing.size && existing.offset < offset + size) {
return std::format("Atomic counters '{}' and '{}' share a binding and overlap at byte offset {}.",
existing.name, uniform.name, std::max(offset, existing.offset));
}
}
spans.push_back({offset, size, uniform.name});
}
return {};
}
// GL 4.6 core 7.6: LinkProgram FAILS when a stage's count of active image uniforms exceeds
// GL_MAX_{VERTEX,TESS_CONTROL,TESS_EVALUATION,GEOMETRY,FRAGMENT,COMPUTE}_IMAGE_UNIFORMS, or
// when their sum exceeds GL_MAX_COMBINED_IMAGE_UNIFORMS. Nothing enforced it: glslang carries
// those numbers in TBuiltInResource only so gl_Max*ImageUniforms can expand from them, and
// its linker never counts uniforms against them - so a program declaring one image uniform
// more than the limit linked cleanly and then rendered nothing.
//
// The limits are the ones glGetIntegerv answers (MG_Impl/GLImpl/Getter/GL_Getter.cpp), the
// hardcoded tessellation zeros included: a program may not exceed a limit the implementation
// advertises, whatever the driver underneath would have taken.
//
// Counts the APPLICATION's image uniforms. The DirectGLES read/write split emits a second
// declaration for an image a stage both reads and writes (MG_Backend/DirectGLES/Utils.h), but
// that happens in the backend after this link, and counting the expanded set here would
// reject programs that are legal by the numbers GL advertises. Returns the info-log line for
// a program over a limit, empty for one within them.
static MobileGL::String ValidateImageUniformLimits(
glslang::TProgram& reflection, const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
using MobileGL::Array;
using MobileGL::Int;
using MobileGL::SizeT;
using MobileGL::UnorderedMap;
static constexpr EShLanguage kStages[] = {EShLangVertex, EShLangTessControl, EShLangTessEvaluation,
EShLangGeometry, EShLangFragment, EShLangCompute};
static constexpr const char* kLimitNames[] = {
"GL_MAX_VERTEX_IMAGE_UNIFORMS", "GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS",
"GL_MAX_TESS_EVALUATION_IMAGE_UNIFORMS", "GL_MAX_GEOMETRY_IMAGE_UNIFORMS",
"GL_MAX_FRAGMENT_IMAGE_UNIFORMS", "GL_MAX_COMPUTE_IMAGE_UNIFORMS"};
constexpr SizeT kStageCount = sizeof(kStages) / sizeof(kStages[0]);
const Int limits[kStageCount] = {env.params.MaxVertexImageUniforms,
0,
0,
env.params.MaxGeometryImageUniforms,
env.params.MaxFragmentImageUniforms,
env.params.MaxComputeImageUniforms};
// Reflection spells an image ARRAY one of two ways, and which one it picks depends on how
// the shader indexed it: a variable index makes glslang expand the array into one entry
// per element ("u_image[0]".."u_image[8]", each carrying the ELEMENT type), while an
// array never dereferenced at all stays a single entry carrying the array type. One
// program can even produce both spellings for the same array. So neither counting entries
// nor trusting the declared size is right on its own - they are reconciled per declared
// name with a max, which is exact for either spelling and cannot double-count the mixture.
struct ImageUse {
Int entries = 0; // reflection entries seen for this name in this stage
Int declared = 0; // largest element count any of them declared
};
UnorderedMap<MobileGL::String, Array<ImageUse, kStageCount>> useByName;
const Int uniformCount = reflection.getNumUniformVariables();
for (Int i = 0; i < uniformCount; ++i) {
const auto& uniform = reflection.getUniform(i);
const glslang::TType* type = uniform.getType();
if (type == nullptr || !type->isImage()) continue;
// An array occupies one image unit per element; an unsized one (never indexed, so
// never more than the single element glslang kept) counts as one.
Int elements = uniform.size > 1 ? uniform.size : 1;
if (type->isArray()) {
elements = type->isSizedArray() ? type->getCumulativeArraySize() : 1;
}
// `stages` is the set of stages that REFERENCE the uniform, which is exactly what GL
// counts: an image declared in two stages costs a unit in each, and one no stage
// reads is not active at all and costs nothing.
Array<ImageUse, kStageCount>* use = nullptr;
for (SizeT stage = 0; stage < kStageCount; ++stage) {
if ((static_cast<unsigned>(uniform.stages) & (1u << static_cast<unsigned>(kStages[stage]))) == 0) {
continue;
}
// The one insert this uniform performs, so the reference survives the rest of the
// stage loop - a flat hash map relocates on insert, never on read.
if (use == nullptr) {
use = &useByName[StripArrayElementSuffix(uniform.name)];
}
++(*use)[stage].entries;
(*use)[stage].declared = std::max((*use)[stage].declared, elements);
}
}
Int counts[kStageCount] = {};
for (const auto& entry : useByName) {
for (SizeT stage = 0; stage < kStageCount; ++stage) {
counts[stage] += std::max(entry.second[stage].entries, entry.second[stage].declared);
}
}
Int combined = 0;
for (SizeT stage = 0; stage < kStageCount; ++stage) {
combined += counts[stage];
if (counts[stage] > limits[stage]) {
return std::format("This program uses {} active image uniforms in one stage, more than the {} "
"{} allows.",
counts[stage], limits[stage], kLimitNames[stage]);
}
}
if (combined > env.params.MaxCombinedImageUniforms) {
return std::format("This program uses {} active image uniforms across its stages, more than the {} "
"GL_MAX_COMBINED_IMAGE_UNIFORMS allows.",
combined, env.params.MaxCombinedImageUniforms);
}
return {};
}
static bool IsBuiltInPipelineOutput(const glslang::TObjectReflection& output) { static bool IsBuiltInPipelineOutput(const glslang::TObjectReflection& output) {
const auto* type = output.getType(); const auto* type = output.getType();
return type && type->getQualifier().builtIn != glslang::EbvNone; return type && type->getQualifier().builtIn != glslang::EbvNone;
@@ -309,6 +549,20 @@ namespace MobileGL::MG_State::GLState {
MGLOG_D("ProgramObject %u: Link body start, shaders to link: %zu", in.externalIndex, in.shaders.size()); MGLOG_D("ProgramObject %u: Link body start, shaders to link: %zu", in.externalIndex, in.shaders.size());
if (!ValidateAttachedShaders()) return;
// The two merges below read the COMPILE snapshots only - no parsed shader - so they
// run before the L1 probe, which needs the merged opaque bindings in its key.
MergeShaderSideChannels();
if (!artifacts.infoLog.empty()) return; // a conflicting explicit uniform location
// ---- L1 of the shader translation memo ----
// Everything below this point - the parse, the link, mapIO, GlslangToSpv, spirv-opt,
// buildReflection and the global-UBO routing - is what a hit skips. See
// ProgramTranslationCache.h.
spirvHandoff.spirvCacheKey = BuildSpirvCacheKey(env);
if (TryPublishFromTranslationCache()) return;
Vector<SharedPtr<glslang::TShader>> shaders; Vector<SharedPtr<glslang::TShader>> shaders;
if (!ConsumeShaders(shaders)) return; if (!ConsumeShaders(shaders)) return;
@@ -335,30 +589,6 @@ namespace MobileGL::MG_State::GLState {
} }
} }
// Merge the shaders' lexically extracted explicit uniform locations. The same
// uniform declared in several stages must agree on its location (config-A glslang
// enforced this at mapIO; the relaxed parse no longer sees the qualifiers).
for (const auto& shader : in.shaders) {
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
for (const auto& [name, location] : compiled.explicitUniformLocations) {
const auto [it, inserted] = artifacts.linkedExplicitUniformLocations.emplace(name, location);
if (!inserted && it->second != location) {
artifacts.infoLog = std::format(
"Uniform '{}' is declared with conflicting explicit locations ({} and {}) "
"across stages.",
name, it->second, location);
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
return;
}
}
// Sampler/image layout(binding = N) initial units, likewise invisible to the
// relaxed parse. Stage order matches the old per-stage mapIO capture, so a
// name declared in several stages keeps the last stage's binding as before.
for (const auto& [name, binding] : compiled.explicitOpaqueBindings) {
artifacts.explicitOpaqueUniformBindings[name] = binding;
}
}
ProgramAttrib attrib{.shaders = Move(shaders), ProgramAttrib attrib{.shaders = Move(shaders),
.explicitVertexInLocations = in.explicitAttribLocations, .explicitVertexInLocations = in.explicitAttribLocations,
.explicitFragmentOutLocations = in.explicitFragDataLocation, .explicitFragmentOutLocations = in.explicitFragDataLocation,
@@ -497,14 +727,138 @@ namespace MobileGL::MG_State::GLState {
spirvHandoff.reflection.uniformIndexInTProgram = artifacts.uniformIndexInTProgram; spirvHandoff.reflection.uniformIndexInTProgram = artifacts.uniformIndexInTProgram;
spirvHandoff.reflection.tProgramUniformIndexToGl = artifacts.tProgramUniformIndexToGl; spirvHandoff.reflection.tProgramUniformIndexToGl = artifacts.tProgramUniformIndexToGl;
spirvHandoff.reflection.maxUniformLocation = artifacts.maxUniformLocation; spirvHandoff.reflection.maxUniformLocation = artifacts.maxUniformLocation;
// The owned reflection mirror, and the block index space its global-UBO test needs.
// BuildGlobalUboRouting reads BOTH - per-uniform array size, opaqueness, GL type and
// matrix shape, plus "is this a member of a GL-visible block". Leaving them out of the
// handoff is not a compile error, it is a SILENT one: every array collapses to a
// single element and every element past the first falls through to the fallback tail
// allocator (ProgramTest.NestedStructArrayUniformElementWrites catches exactly that).
spirvHandoff.reflection.uniformReflection = artifacts.uniformReflection;
spirvHandoff.reflection.blockReflection = artifacts.blockReflection;
spirvHandoff.reflection.tProgramBlockIndexToGl = artifacts.tProgramBlockIndexToGl;
// Phase B pairs this with its own SpirvArtifacts to insert the completed front end.
// A COPY, because the GL-thread join moves `artifacts` out of this node before phase B
// runs - and with the TProgram dropped, because a memo must never hold a glslang arena.
if (spirvHandoff.spirvCacheKey.Valid()) {
auto forCache = MakeShared<ProgramObject::LinkArtifacts>(artifacts);
forCache->program.reset();
spirvHandoff.linkArtifactsForCache = Move(forCache);
}
spirvHandoff.ready = true; spirvHandoff.ready = true;
MGLOG_D("ProgramObject %u: phase A done, %zu module(s) handed to the SPIR-V job", in.externalIndex, MGLOG_D("ProgramObject %u: phase A done, %zu module(s) handed to the SPIR-V job", in.externalIndex,
spirvHandoff.shaderTypes.size()); spirvHandoff.shaderTypes.size());
} }
Bool ProgramLinkTask::ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders) { // The L1 key. Every input below is one that can change the SPIR-V this program
outShaders.assign(in.shaders.size(), nullptr); // generates; see the key inventory on SpirvTranslationKeyInputs.
//
// Deliberately NOT keyed on: nothing that only steers a BACKEND transpile - see the
// classification on CompileEnv::frontendFingerprint, and L2's own key in
// MG_Util/ShaderTranspiler/TranslationCache.h.
MG_Util::ShaderTranspiler::TranslationCacheKey ProgramLinkTask::BuildSpirvCacheKey(
const MG_Util::ShaderTranspiler::CompileEnv& env) const {
using namespace MG_Util::ShaderTranspiler;
if (!ShaderTranslationCacheEnabled()) return {};
SpirvTranslationKeyInputs keyInputs;
// The FRONT-END fingerprint, not env.fingerprint: L1 must be shared by two contexts
// on different GPUs whenever glslang would produce the same thing for them. See the
// classification on CompileEnv::frontendFingerprint.
keyInputs.frontendFingerprint = env.frontendFingerprint;
// Always 0 on both production parse paths (ShaderCompileTask::RunCompilePipeline and
// ClaimParsedShader's re-parse). In the key regardless, so that a future non-zero
// value cannot alias a module parsed without it.
keyInputs.shaderCompileFlags = 0;
keyInputs.enableSpirvValidation = in.enableSpirvValidation;
keyInputs.stages.reserve(in.shaders.size());
for (const LinkShaderInput& shader : in.shaders) {
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
if (compiled.preprocessedSource.empty()) {
// No text to key on - an internal shader object, or an artifact this build
// did not populate. Refuse to key rather than key on nothing.
return {};
}
keyInputs.stages.push_back(SpirvTranslationKeyInputs::Stage{
.type = MG_Util::ConvertShaderStageToGLEnum(shader.stage),
.preprocessedSource = StringView(compiled.preprocessedSource)});
}
if (keyInputs.stages.empty()) return {};
keyInputs.explicitVertexInLocations = &in.explicitAttribLocations;
keyInputs.explicitFragmentOutLocations = &in.explicitFragDataLocation;
keyInputs.explicitFragmentOutIndices = &in.explicitFragDataIndex;
keyInputs.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings;
// In the key ONLY because the payload now carries the reflection: transform feedback
// is resolved by reading the linked intermediates and never perturbs the generated
// SPIR-V, but it does shape xfbVaryings / xfbStrides / xfbBufferMode /
// gsStripTriangles, and maxFragmentOutputColorNumber decides whether the link is
// rejected at all. Widening a payload means widening the key.
keyInputs.requestedXfbVaryings = &in.requestedXfbVaryings;
keyInputs.xfbBufferMode = static_cast<Uint32>(in.requestedXfbBufferMode);
keyInputs.maxFragmentOutputColorNumber = in.maxFragmentOutputColorNumber;
return BuildSpirvTranslationKey(keyInputs);
}
// The link rejections that need nothing but the compile snapshots. They run before the
// L1 memo is consulted, so a hit can never paper over a program that must fail to link.
// The two lexical side channels the relaxed parse cannot provide, merged across stages:
// explicit default-block uniform locations (which must agree, or the link fails) and
// sampler/image layout(binding = N) initial units. Reads the COMPILE snapshots only, so
// it is legal - and necessary - before any shader is parsed: the merged bindings are part
// of the L1 memo key.
void ProgramLinkTask::MergeShaderSideChannels() {
// Merge the shaders' lexically extracted explicit uniform locations. The same
// uniform declared in several stages must agree on its location (config-A glslang
// enforced this at mapIO; the relaxed parse no longer sees the qualifiers).
for (const auto& shader : in.shaders) {
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
for (const auto& [name, location] : compiled.explicitUniformLocations) {
const auto [it, inserted] = artifacts.linkedExplicitUniformLocations.emplace(name, location);
if (!inserted && it->second != location) {
artifacts.infoLog = std::format(
"Uniform '{}' is declared with conflicting explicit locations ({} and {}) "
"across stages.",
name, it->second, location);
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
return;
}
}
// Sampler/image layout(binding = N) initial units, likewise invisible to the
// relaxed parse. Stage order matches the old per-stage mapIO capture, so a
// name declared in several stages keeps the last stage's binding as before.
for (const auto& [name, binding] : compiled.explicitOpaqueBindings) {
artifacts.explicitOpaqueUniformBindings[name] = binding;
}
}
}
// An L1 hit: the entire front end, published without constructing a TShader or a
// TProgram. Everything here is a copy out of plain owned data - `link.program` is null in
// the payload by construction, and nothing reads it any more.
Bool ProgramLinkTask::TryPublishFromTranslationCache() {
if (!spirvHandoff.spirvCacheKey.Valid()) return false;
const ProgramTranslationResultPtr hit =
GetProgramTranslationCache().Find(spirvHandoff.spirvCacheKey);
if (!hit) return false;
artifacts = hit->link;
spirvHandoff.shaderTypes.resize(in.shaders.size());
for (SizeT i = 0; i < in.shaders.size(); i++) {
spirvHandoff.shaderTypes[i] = MG_Util::ConvertShaderStageToGLEnum(in.shaders[i].stage);
}
// An ALIASING SharedPtr: it points at the payload's SpirvArtifacts while sharing
// ownership of the whole payload, so phase B publishes them without a second copy and
// without any chance of the entry being evicted from under it.
spirvHandoff.cachedSpirv =
SharedPtr<const ProgramObject::SpirvArtifacts>(hit, &hit->spirv);
spirvHandoff.ready = true;
MGLOG_D("ProgramObject %u: L1 cache hit - the whole front end was reused; no parse, no "
"link, no SPIR-V generation",
in.externalIndex);
return true;
}
Bool ProgramLinkTask::ValidateAttachedShaders() {
// GL 4.6 core 7.3: a compute shader may only be linked with other compute shaders - // GL 4.6 core 7.3: a compute shader may only be linked with other compute shaders -
// the compute pipeline has no other stages to link against, so a program that mixes // the compute pipeline has no other stages to link against, so a program that mixes
// them must fail to link (KHR-GL43.compute_shader.api-program). // them must fail to link (KHR-GL43.compute_shader.api-program).
@@ -526,8 +880,6 @@ namespace MobileGL::MG_State::GLState {
const LinkShaderInput& input = in.shaders[i]; const LinkShaderInput& input = in.shaders[i];
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(input.stage); const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(input.stage);
const ShaderCompileArtifacts& compiled = CompiledArtifacts(input.compiled); const ShaderCompileArtifacts& compiled = CompiledArtifacts(input.compiled);
MGLOG_D("ProgramObject %u: Preparing shader[%zu] stage %s", in.externalIndex, i,
MG_Util::ConvertGLEnumToString(shaderType).c_str());
if (!compiled.compileStatus) { if (!compiled.compileStatus) {
// The compile log LEADS the quoted source, and that order is load-bearing: // The compile log LEADS the quoted source, and that order is load-bearing:
@@ -546,6 +898,17 @@ namespace MobileGL::MG_State::GLState {
in.externalIndex, i, artifacts.infoLog)); in.externalIndex, i, artifacts.infoLog));
return false; return false;
} }
}
return true;
}
Bool ProgramLinkTask::ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders) {
outShaders.assign(in.shaders.size(), nullptr);
for (SizeT i = 0; i < in.shaders.size(); i++) {
const LinkShaderInput& input = in.shaders[i];
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(input.stage);
MGLOG_D("ProgramObject %u: Preparing shader[%zu] stage %s", in.externalIndex, i,
MG_Util::ConvertGLEnumToString(shaderType).c_str());
String reparseLog; String reparseLog;
outShaders[i] = input.compiled->ClaimParsedShader(reparseLog); outShaders[i] = input.compiled->ClaimParsedShader(reparseLog);
if (!outShaders[i]) { if (!outShaders[i]) {
@@ -606,6 +969,22 @@ namespace MobileGL::MG_State::GLState {
return false; return false;
} }
if (String atomicCounterError = ValidateAtomicCounterLayout(*artifacts.program);
!atomicCounterError.empty()) {
artifacts.infoLog = Move(atomicCounterError);
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
ProgramObject::ResetLinkArtifacts(artifacts);
return false;
}
if (String imageUniformError = ValidateImageUniformLimits(*artifacts.program, env);
!imageUniformError.empty()) {
artifacts.infoLog = Move(imageUniformError);
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
ProgramObject::ResetLinkArtifacts(artifacts);
return false;
}
// ---------- GL-facing index spaces (relaxed-parse cleanup) ---------- // ---------- GL-facing index spaces (relaxed-parse cleanup) ----------
// Blocks first: global-UBO membership drives the uniform filter below. The // Blocks first: global-UBO membership drives the uniform filter below. The
// synthesized MGL_GLOBAL_UBO is a transpiler artifact - its members are GL // synthesized MGL_GLOBAL_UBO is a transpiler artifact - its members are GL
@@ -658,7 +1037,16 @@ namespace MobileGL::MG_State::GLState {
// MGL_GLOBAL_UBO, so reflection cannot provide them ("source-explicit"); // MGL_GLOBAL_UBO, so reflection cannot provide them ("source-explicit");
// - glslang's layoutLocation() for opaque uniforms, where the qualifier // - glslang's layoutLocation() for opaque uniforms, where the qualifier
// survives the relaxed parse (and mapIO auto-assigns the rest). // survives the relaxed parse (and mapIO auto-assigns the rest).
constexpr Uint kNoLocation = glslang::TQualifier::layoutLocationEnd; //
// "no effective location yet". Deliberately OUTSIDE the location space rather than
// glslang::TQualifier::layoutLocationEnd, which is the first location past the pool and
// therefore only one off a legal one - a sentinel that sits at the boundary it guards has
// to be re-proved safe every time the ceiling moves, and glslang uses that same value for
// "this opaque uniform has no location" as well.
constexpr Uint kNoLocation = ~static_cast<Uint>(0);
// The ceiling glGetIntegerv(GL_MAX_UNIFORM_LOCATIONS) advertises, which is what the
// allocator below has to honour: locations 0..kMaxUniformLocations-1 and no others.
constexpr Uint kMaxUniformLocations = static_cast<Uint>(ProgramObject::MAX_UNIFORM_LOCATIONS);
Vector<Uint> effectiveLocation(tProgramUniformCount, kNoLocation); Vector<Uint> effectiveLocation(tProgramUniformCount, kNoLocation);
Vector<Bool> locationIsSourceExplicit(tProgramUniformCount, false); Vector<Bool> locationIsSourceExplicit(tProgramUniformCount, false);
UnorderedMap<String, Uint> structExplicitCursor; // declared root -> next member location UnorderedMap<String, Uint> structExplicitCursor; // declared root -> next member location
@@ -695,13 +1083,19 @@ namespace MobileGL::MG_State::GLState {
cursor->second += static_cast<Uint>(GetUniformLocationSpan(uniform)); cursor->second += static_cast<Uint>(GetUniformLocationSpan(uniform));
} }
} }
if (effectiveLocation[i] == kNoLocation && type != nullptr && type->isOpaque()) { // glslang parks "no location" at layoutLocationEnd, which is a real location in this
// table's numbering - test for it explicitly rather than letting it through as one.
if (effectiveLocation[i] == kNoLocation && type != nullptr && type->isOpaque() &&
uniform.layoutLocation() != glslang::TQualifier::layoutLocationEnd) {
effectiveLocation[i] = uniform.layoutLocation(); effectiveLocation[i] = uniform.layoutLocation();
} }
if (locationIsSourceExplicit[i] && if (locationIsSourceExplicit[i] &&
effectiveLocation[i] + static_cast<Uint>(GetUniformLocationSpan(uniform)) > kNoLocation) { effectiveLocation[i] + static_cast<Uint>(GetUniformLocationSpan(uniform)) > kMaxUniformLocations) {
// Config A rejected out-of-range explicit locations at parse; keep them // Config A rejected out-of-range explicit locations at parse; keep them
// from growing the location table unboundedly. // from growing the location table unboundedly. Stated against the advertised
// GL_MAX_UNIFORM_LOCATIONS, because that is the rule being enforced (GL 4.6 core
// 7.6.1): an array whose LAST element passes the ceiling is a link error even
// though its base compiled fine.
artifacts.infoLog = std::format("Uniform '{}' explicit location {} is out of range.", uniform.name, artifacts.infoLog = std::format("Uniform '{}' explicit location {} is out of range.", uniform.name,
effectiveLocation[i]); effectiveLocation[i]);
ProgramObject::ResetLinkArtifacts(artifacts); ProgramObject::ResetLinkArtifacts(artifacts);
@@ -709,12 +1103,55 @@ namespace MobileGL::MG_State::GLState {
} }
} }
Int requiredUniformLocations = 0; // ARB_explicit_uniform_location / GL 4.6 core 7.6.1: an explicit location is RESERVED
// whether or not the uniform turned out to be active. The dead default-block uniforms
// filtered out of glUniformIndexToTProgram above are invisible to every GL query - which
// is correct - but their locations must still be kept out of the implicit allocator's
// reach, or an implicit uniform is handed a location the source already claimed.
//
// Deliberately NOT written into artifacts.uniformLocations or uniformIndexInTProgram:
// glGetUniformLocation must keep answering -1 for a dead uniform, and a location no
// application can legally obtain must not become writable through glUniform*. The
// occupancy therefore lives in its own bitset, built once the table has been sized.
Vector<Pair<Uint, Int>> deadExplicitReservations;
Int deadReservedLocationCount = 0;
for (Int i = 0; i < tProgramUniformCount; i++) {
if (artifacts.tProgramUniformIndexToGl[i] >= 0) continue; // GL-visible: handled above
const auto& uniform = artifacts.program->getUniform(i);
if (!isGlobalUboMember(uniform) || uniform.stages != 0) continue;
const Int* explicitLocation = findExplicitLocation(uniform.name);
if (explicitLocation == nullptr) continue;
const Uint location = static_cast<Uint>(*explicitLocation);
const Int locationSpan = GetUniformLocationSpan(uniform);
if (location + static_cast<Uint>(locationSpan) > kMaxUniformLocations) {
artifacts.infoLog = std::format("Uniform '{}' explicit location {} is out of range.", uniform.name,
location);
ProgramObject::ResetLinkArtifacts(artifacts);
return false;
}
deadExplicitReservations.emplace_back(location, locationSpan);
deadReservedLocationCount += locationSpan;
artifacts.maxUniformLocation = std::max(artifacts.maxUniformLocation, location + locationSpan - 1);
MGLOG_D("ProgramObject %u: Reflection - inactive uniform '%s' reserves locations %u..%u without "
"becoming GL-visible",
in.externalIndex, uniform.name.c_str(), location, location + locationSpan - 1);
}
Int requiredUniformLocations = deadReservedLocationCount;
// The same count restricted to DEFAULT-BLOCK uniforms, which is the only thing
// GL_MAX_UNIFORM_LOCATIONS bounds. requiredUniformLocations cannot serve: it also carries
// named-block members, which take a slot in this allocator's table (an implementation
// detail) but consume no GL uniform location at all, so a big UBO array would otherwise
// fail a link the spec allows.
Int defaultBlockLocationDemand = deadReservedLocationCount;
for (const Int i : artifacts.glUniformIndexToTProgram) { for (const Int i : artifacts.glUniformIndexToTProgram) {
auto& uniform = artifacts.program->getUniform(i); auto& uniform = artifacts.program->getUniform(i);
const Uint location = effectiveLocation[i]; const Uint location = effectiveLocation[i];
const Int locationSpan = GetUniformLocationSpan(uniform); const Int locationSpan = GetUniformLocationSpan(uniform);
requiredUniformLocations += locationSpan; requiredUniformLocations += locationSpan;
const Bool inNamedBlock = uniform.index >= 0 && !isGlobalUboMember(uniform);
if (!inNamedBlock) defaultBlockLocationDemand += locationSpan;
if (location != kNoLocation) { if (location != kNoLocation) {
artifacts.maxUniformLocation = std::max(artifacts.maxUniformLocation, location + locationSpan - 1); artifacts.maxUniformLocation = std::max(artifacts.maxUniformLocation, location + locationSpan - 1);
} }
@@ -727,6 +1164,22 @@ namespace MobileGL::MG_State::GLState {
MGLOG_D("ProgramObject %u: Reflection - computed maxUniformLocation=%u uniformNameMaxLength=%d", MGLOG_D("ProgramObject %u: Reflection - computed maxUniformLocation=%u uniformNameMaxLength=%d",
in.externalIndex, artifacts.maxUniformLocation, artifacts.uniformNameMaxLength); in.externalIndex, artifacts.maxUniformLocation, artifacts.uniformNameMaxLength);
// GL 4.6 core 7.6.1: explicit, implicit and reserved-but-inactive default-block uniforms
// all draw from the one GL_MAX_UNIFORM_LOCATIONS pool, and a program asking for more than
// the implementation advertises FAILS TO LINK
// (KHR-GL43.explicit_uniform_location.uniform-loc-negative-link-max-num-of-locations).
// A single uniform whose own span passes the ceiling was already rejected above; this is
// the aggregate half of the same rule.
if (defaultBlockLocationDemand > static_cast<Int>(kMaxUniformLocations)) {
artifacts.infoLog =
std::format("Uniform locations exhausted: the default-block uniforms need {} locations but "
"GL_MAX_UNIFORM_LOCATIONS is {}.",
defaultBlockLocationDemand, kMaxUniformLocations);
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
ProgramObject::ResetLinkArtifacts(artifacts);
return false;
}
if (artifacts.maxUniformLocation + 1 < requiredUniformLocations) { if (artifacts.maxUniformLocation + 1 < requiredUniformLocations) {
MGLOG_D("ProgramObject %u: Reflection - maxUniformLocation+1 (%u) < requiredUniformLocations (%d), " MGLOG_D("ProgramObject %u: Reflection - maxUniformLocation+1 (%u) < requiredUniformLocations (%d), "
"adjusting", "adjusting",
@@ -741,6 +1194,27 @@ namespace MobileGL::MG_State::GLState {
glslang::TQualifier::layoutLocationEnd); glslang::TQualifier::layoutLocationEnd);
artifacts.uniformSamplerOrImageUnitIndex.resize(artifacts.maxUniformLocation + 1, -1); artifacts.uniformSamplerOrImageUnitIndex.resize(artifacts.maxUniformLocation + 1, -1);
// Occupancy for the inactive explicit uniforms collected above: a set bit means "the
// source claimed this location", which is enough to keep the two implicit passes off it
// without making the location reachable through any GL entry point. A location the
// fallback grow path mints later is past this bitset by construction (every reservation
// was folded into maxUniformLocation before the table was sized), so the lookup treats
// out-of-range as free rather than resizing in lockstep.
// Left empty - and unallocated - when nothing reserved anything, which is every program in
// the shader-pack corpus; the lookup below reads an empty bitset as "nothing is reserved".
Vector<Bool> reservedLocation;
if (!deadExplicitReservations.empty()) {
reservedLocation.assign(artifacts.maxUniformLocation + 1, false);
for (const auto& [reservedBase, reservedSpan] : deadExplicitReservations) {
for (Int element = 0; element < reservedSpan; ++element) {
reservedLocation[reservedBase + element] = true;
}
}
}
const auto locationIsReserved = [&reservedLocation](SizeT location) {
return location < reservedLocation.size() && reservedLocation[location];
};
Vector<int> unallocatedUniformIndex; Vector<int> unallocatedUniformIndex;
// Pass 1: source-explicit locations. These are API contract // Pass 1: source-explicit locations. These are API contract
@@ -785,7 +1259,8 @@ namespace MobileGL::MG_State::GLState {
Bool spanIsFree = location + locationSpan - 1 <= artifacts.maxUniformLocation; Bool spanIsFree = location + locationSpan - 1 <= artifacts.maxUniformLocation;
for (Int element = 0; spanIsFree && element < locationSpan; ++element) { for (Int element = 0; spanIsFree && element < locationSpan; ++element) {
spanIsFree = spanIsFree =
artifacts.uniformIndexInTProgram[location + element] == glslang::TQualifier::layoutLocationEnd; artifacts.uniformIndexInTProgram[location + element] == glslang::TQualifier::layoutLocationEnd &&
!locationIsReserved(location + element);
} }
if (!spanIsFree) { if (!spanIsFree) {
artifacts.uniformLocations[uniform.name] = kNoLocation; artifacts.uniformLocations[uniform.name] = kNoLocation;
@@ -817,7 +1292,8 @@ namespace MobileGL::MG_State::GLState {
bool hasRoom = locNeedle + locationSpan - 1 <= artifacts.maxUniformLocation; bool hasRoom = locNeedle + locationSpan - 1 <= artifacts.maxUniformLocation;
for (Int element = 0; hasRoom && element < locationSpan; ++element) { for (Int element = 0; hasRoom && element < locationSpan; ++element) {
hasRoom = artifacts.uniformIndexInTProgram[locNeedle + element] == hasRoom = artifacts.uniformIndexInTProgram[locNeedle + element] ==
glslang::TQualifier::layoutLocationEnd; glslang::TQualifier::layoutLocationEnd &&
!locationIsReserved(locNeedle + element);
} }
if (!hasRoom) continue; if (!hasRoom) continue;
// Found a vacant location at locNeedle // Found a vacant location at locNeedle
@@ -837,6 +1313,24 @@ namespace MobileGL::MG_State::GLState {
// span is left; grow the table instead of leaving the uniform without // span is left; grow the table instead of leaving the uniform without
// a location (which would make it unsettable via glUniform*). // a location (which would make it unsettable via glUniform*).
const SizeT base = artifacts.uniformIndexInTProgram.size(); const SizeT base = artifacts.uniformIndexInTProgram.size();
// The growth stops at the pool GL advertises. GL 4.6 core 7.6.1 bounds every
// uniform location by GL_MAX_UNIFORM_LOCATIONS, and the conformance suite reads a
// returned location >= the advertised maximum as a failure outright
// (KHR-GLES31.explicit_uniform_location.uniform-loc-mix-with-implicit-max). Minting
// 4095, 4096, ... is strictly worse than refusing: those are locations no
// application may legally name and no later query can make legal, so they would
// only turn a link-time exhaustion into a silently unwritable uniform. Unreachable
// for any program that fits glslang's per-stage uniform-component limits - it takes
// a fragmented pool of thousands of explicitly-located slots to get here.
if (base + static_cast<SizeT>(locationSpan) > kMaxUniformLocations) {
artifacts.infoLog = std::format(
"Uniform locations exhausted: '{}' needs {} location(s) and no free span is left below "
"GL_MAX_UNIFORM_LOCATIONS ({}).",
uniform.name, locationSpan, kMaxUniformLocations);
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
ProgramObject::ResetLinkArtifacts(artifacts);
return false;
}
artifacts.uniformIndexInTProgram.resize(base + locationSpan, artifacts.uniformIndexInTProgram.resize(base + locationSpan,
glslang::TQualifier::layoutLocationEnd); glslang::TQualifier::layoutLocationEnd);
artifacts.uniformSamplerOrImageUnitIndex.resize(base + locationSpan, -1); artifacts.uniformSamplerOrImageUnitIndex.resize(base + locationSpan, -1);
@@ -987,9 +1481,78 @@ namespace MobileGL::MG_State::GLState {
MGLOG_D("ProgramObject %u: Reflection - UBO[%d] name='%s' size=%u binding=%d", in.externalIndex, i, MGLOG_D("ProgramObject %u: Reflection - UBO[%d] name='%s' size=%u binding=%d", in.externalIndex, i,
ubo.name.c_str(), ubo.size, ubo.getBinding()); ubo.name.c_str(), ubo.size, ubo.getBinding());
} }
SnapshotGlslangReflection();
return true; return true;
} }
// The last thing DoReflection does, and the thing that lets everything after it stop
// caring that a glslang::TProgram ever existed: copy every reflection record the GL query
// surface reads into LinkArtifacts' own owned tables.
//
// Indexed by TPROGRAM index throughout - the same space glUniformIndexToTProgram,
// tProgramUniformIndexToGl and uniformIndexInTProgram already speak - so the accessors
// that used to call program->getUniform(i) index uniformReflection[i] and are otherwise
// unchanged.
void ProgramLinkTask::SnapshotGlslangReflection() {
glslang::TProgram& program = *artifacts.program;
// Blocks FIRST: a uniform's effective layoutMatrix is resolved against its owning
// block below, which needs the block records to already exist.
const Int blockCount = program.getNumUniformBlocks();
artifacts.blockReflection.clear();
artifacts.blockReflection.reserve(static_cast<SizeT>(blockCount));
for (Int i = 0; i < blockCount; ++i) {
artifacts.blockReflection.push_back(MakeResourceReflection(program.getUniformBlock(i)));
}
const Int uniformCount = program.getNumUniformVariables();
artifacts.uniformReflection.clear();
artifacts.uniformReflection.reserve(static_cast<SizeT>(uniformCount));
artifacts.uniformIndexByName.clear();
artifacts.uniformIndexByName.reserve(static_cast<SizeT>(uniformCount));
for (Int i = 0; i < uniformCount; ++i) {
ProgramObject::UniformReflection record = MakeResourceReflection(program.getUniform(i));
// A block-level layout(row_major)/(column_major) that the member did not inherit
// in its own qualifier. Resolved once HERE rather than at every GL_UNIFORM_* query,
// which is what the getUniformBlock() fallback in the old accessors was doing.
if (record.type.layoutMatrix == static_cast<Int>(glslang::ElmNone) && record.index >= 0 &&
record.index < static_cast<Int>(artifacts.blockReflection.size())) {
record.type.layoutMatrix = artifacts.blockReflection[record.index].type.layoutMatrix;
}
// Keyed on the REFLECTED name and on uniforms only. That is deliberate and is the
// filtered semantics the old code hand-rolled: glslang's TReflection::nameToIndex
// also holds block and function entries, which is exactly why every
// getUniformIndex() call site re-checked getUniform(idx).name == name afterwards.
// First writer wins, so a duplicated name resolves the way a forward scan would.
artifacts.uniformIndexByName.emplace(record.name, i);
artifacts.uniformReflection.push_back(Move(record));
}
const Int pipeInputCount = program.getNumPipeInputs();
artifacts.pipeInputReflection.clear();
artifacts.pipeInputReflection.reserve(static_cast<SizeT>(pipeInputCount));
for (Int i = 0; i < pipeInputCount; ++i) {
artifacts.pipeInputReflection.push_back(MakeResourceReflection(program.getPipeInput(i)));
}
const Int pipeOutputCount = program.getNumPipeOutputs();
artifacts.pipeOutputReflection.clear();
artifacts.pipeOutputReflection.reserve(static_cast<SizeT>(pipeOutputCount));
for (Int i = 0; i < pipeOutputCount; ++i) {
artifacts.pipeOutputReflection.push_back(MakeResourceReflection(program.getPipeOutput(i)));
}
artifacts.lastStageIsFragment = program.getIntermediate(EShLangFragment) != nullptr;
for (Uint dim = 0; dim < 3u; ++dim) {
artifacts.computeLocalSize[dim] = program.getLocalSize(static_cast<Int>(dim));
}
MGLOG_D("ProgramObject %u: Reflection - snapshot: %zu uniform(s), %zu block(s), %zu input(s), "
"%zu output(s)",
in.externalIndex, artifacts.uniformReflection.size(), artifacts.blockReflection.size(),
artifacts.pipeInputReflection.size(), artifacts.pipeOutputReflection.size());
}
Bool ProgramLinkTask::ValidateFragmentOutputLocations() { Bool ProgramLinkTask::ValidateFragmentOutputLocations() {
if (!artifacts.program) return false; if (!artifacts.program) return false;
// The pipe-output list is the output interface of the program's LAST stage. Only a // The pipe-output list is the output interface of the program's LAST stage. Only a
@@ -12,6 +12,7 @@
#include <MG_State/GLState/ProgramState/ShaderCompileTask.h> #include <MG_State/GLState/ProgramState/ShaderCompileTask.h>
#include <MG_Util/Async/JobNode.h> #include <MG_Util/Async/JobNode.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h> #include <MG_Util/ShaderTranspiler/CompileEnv.h>
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
namespace MobileGL::MG_State::GLState { namespace MobileGL::MG_State::GLState {
// One attached shader, as the link sees it: never the ShaderObject, always a snapshot. // One attached shader, as the link sees it: never the ShaderObject, always a snapshot.
@@ -116,6 +117,27 @@ namespace MobileGL::MG_State::GLState {
// for phase B after the join has moved `artifacts` away. // for phase B after the join has moved `artifacts` away.
ProgramObject::LinkArtifacts reflection; ProgramObject::LinkArtifacts reflection;
// L1 shader-translation memo key for this program's SPIR-V (see
// MG_Util/ShaderTranspiler/TranslationCache.h). Built HERE, at the tail of phase
// A, and not by phase B - two reasons, both structural:
// * the key covers the four link-time request maps and the merged opaque
// bindings, and one of those (explicitOpaqueUniformBindings) lives in
// `artifacts`, which phase B is forbidden to read because the GL-thread join
// moves it out from under phase B;
// * built once, it serves both the lookup and the insert, so the program's
// sources are copied into the blob exactly once per link.
// Invalid (null blob) when the cache is disabled, or when a stage arrived
// without preprocessed source - in which case phase B simply translates.
MG_Util::ShaderTranspiler::TranslationCacheKey spirvCacheKey;
// Set on an L1 HIT: phase B publishes these SpirvArtifacts verbatim instead of
// generating anything. Null on a miss.
SharedPtr<const ProgramObject::SpirvArtifacts> cachedSpirv;
// Set on a MISS: the LinkArtifacts phase B has to pair with its own SpirvArtifacts
// to insert the completed front end. Copied here rather than read off the node,
// because the GL-thread join MOVES `artifacts` out before phase B runs.
SharedPtr<const ProgramObject::LinkArtifacts> linkArtifactsForCache;
// The one flag phase B tests before doing anything: false means this link never // The one flag phase B tests before doing anything: false means this link never
// reached the tail of RunBody (it failed, or was cancelled mid-body). // reached the tail of RunBody (it failed, or was cancelled mid-body).
Bool ready = false; Bool ready = false;
@@ -142,8 +164,31 @@ namespace MobileGL::MG_State::GLState {
// ---- the link body, split exactly as ProgramObject::Link() had it ---- // ---- the link body, split exactly as ProgramObject::Link() had it ----
// Each returns false to abort the link with `artifacts.infoLog` already set, which is // Each returns false to abort the link with `artifacts.infoLog` already set, which is
// GL's definition of a failed link: LINK_STATUS false plus a log, never a GL error. // GL's definition of a failed link: LINK_STATUS false plus a log, never a GL error.
// The two link-rejection gates that need no parsed shader: a compute stage mixed
// with any other, and an attached shader that failed to compile. Split out of
// ConsumeShaders so they still run - in the same order, with the same diagnostics -
// BEFORE the L1 memo is consulted, rather than behind a hit that would skip them.
// The two lexical side channels the relaxed parse cannot provide, merged across
// stages. Reads the compile snapshots only, so it runs before any parse - the merged
// opaque bindings are part of the L1 memo key. Sets artifacts.infoLog and leaves
// linkStatus false when two stages disagree on an explicit uniform location.
void MergeShaderSideChannels();
Bool ValidateAttachedShaders();
Bool ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders); Bool ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders);
// Publishes a whole front end straight out of the L1 memo: no TShader, no TProgram,
// no SPIR-V generation. Returns false on a miss.
Bool TryPublishFromTranslationCache();
// The L1 memo key for the SPIR-V this program is about to generate, or an invalid
// key when the cache is off or a stage has no preprocessed source to key on.
// Called at the tail of RunBody, where every input it needs is still owned by this
// node and `artifacts` has not yet been published.
MG_Util::ShaderTranspiler::TranslationCacheKey BuildSpirvCacheKey(
const MG_Util::ShaderTranspiler::CompileEnv& env) const;
Bool DoReflection(const MG_Util::ShaderTranspiler::CompileEnv& env); Bool DoReflection(const MG_Util::ShaderTranspiler::CompileEnv& env);
// Copies every reflection record the GL query surface reads out of the glslang
// TProgram into LinkArtifacts own owned tables. Runs at the tail of DoReflection.
void SnapshotGlslangReflection();
Bool ValidateFragmentOutputLocations(); Bool ValidateFragmentOutputLocations();
Bool ResolveTransformFeedbackVaryings(); Bool ResolveTransformFeedbackVaryings();
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate); void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
@@ -615,15 +615,22 @@ namespace MobileGL::MG_State::GLState {
Int ProgramObject::GetFragmentDataLocation(const char* name) { Int ProgramObject::GetFragmentDataLocation(const char* name) {
if (!Artifacts().program || !name) return -1; // Answered from the OWNED pipe-output snapshot, not from Artifacts().program. The live
// TProgram is null on a translation-cache L1 hit - that is the entire point of the memo
// - and it is also null for any program that never linked. The old `if
// (!Artifacts().program) return -1` guard silently produced the never-linked answer for
// a perfectly good cached program, so glGetFragDataLocation returned -1 for every
// fragment output of it. The empty snapshot gives the never-linked case the same -1
// without needing the guard at all.
if (!name) return -1;
const auto explicitLocation = Artifacts().linkedFragDataLocation.find(name); const auto explicitLocation = Artifacts().linkedFragDataLocation.find(name);
const Int outputCount = Artifacts().program->getNumPipeOutputs(); for (const PipeOutputReflection& output : Artifacts().pipeOutputReflection) {
for (Int index = 0; index < outputCount; ++index) {
const auto& output = Artifacts().program->getPipeOutput(index);
if (output.name != name) continue; if (output.name != name) continue;
if (explicitLocation != Artifacts().linkedFragDataLocation.end()) return static_cast<Int>(explicitLocation->second); if (explicitLocation != Artifacts().linkedFragDataLocation.end()) {
return static_cast<Int>(output.layoutLocation()); return static_cast<Int>(explicitLocation->second);
}
return output.location;
} }
return -1; return -1;
} }
@@ -24,6 +24,85 @@ namespace MobileGL::MG_State::GLState {
class ProgramObject { class ProgramObject {
public: public:
// GL_MAX_UNIFORM_LOCATIONS: locations 0 .. MAX_UNIFORM_LOCATIONS-1 are the whole legal
// range (GL 4.6 core 7.6.1 / ARB_explicit_uniform_location). Shared with GL_Getter rather
// than spelled twice, because the link and the query must agree exactly - the CTS declares
// a uniform at the advertised value minus one and expects it to link
// (KHR-GL43.explicit_uniform_location.uniform-loc-max).
//
// Tied to glslang's own ceiling and NOT raisable past it: ParseHelper rejects
// `layout(location = N)` for N >= TQualifier::layoutLocationEnd at COMPILE time, so
// layoutLocationEnd - 1 is the largest location any shader in this stack can declare -
// which makes exactly layoutLocationEnd locations, 0 .. layoutLocationEnd - 1, the pool.
// Advertising more would promise a location no shader could name. Comfortably above the
// 1024 GL 4.3 requires.
static constexpr Int MAX_UNIFORM_LOCATIONS = static_cast<Int>(glslang::TQualifier::layoutLocationEnd);
// Everything the query surface ever asked a glslang::TType, flattened. Twenty
// predicates, no recursion: nothing post-link ever walks a struct, a type name or the
// AST, so a POD covers the whole surface exactly.
struct TypeFacts {
Bool isArray = false;
// A runtime-sized array (a storage block's unsized trailing member) is an array
// that is NOT sized; GL_ARRAY_SIZE reports 0 for it.
Bool isSizedArray = false;
Bool isMatrix = false;
Bool isVector = false;
Bool isOpaque = false;
Bool isTexture = false;
Bool isImage = false;
Bool isDouble = false; // getBasicType() == EbtDouble
Bool isVoid = false; // getBasicType() == EbtVoid (hidden block members)
Bool isBuffer = false; // getQualifier().storage == EvqBuffer
Bool isPatch = false; // getQualifier().patch
Bool hasIndex = false; // getQualifier().hasIndex()
Bool hasFormat = false; // getQualifier().hasFormat()
Int vectorSize = 0;
Int matrixCols = 0;
Int matrixRows = 0;
Int layoutIndex = 0; // getQualifier().layoutIndex
Uint layoutFormat = 0; // getQualifier().getFormat()
// glslang::TLayoutMatrix, widened. For a uniform this is already RESOLVED against
// the owning block's qualifier, so the getUniformBlock() fallback the old
// accessors carried is gone.
Int layoutMatrix = 0;
// glslang::TBasicType, widened - ApplyUniformInitialValues and the typed
// glGetUniform* paths compare against a handful of enumerators.
Int basicType = 0;
};
// One glslang::TObjectReflection, flattened. Used for uniforms, blocks, pipe inputs
// and pipe outputs alike, because glslang reflects all four as TObjectReflection.
struct ResourceReflection {
String name;
GLenum glDefineType = 0;
Int offset = -1;
// TObjectReflection::size, RAW. For a uniform prefer `arraySize` below, which is
// the resolved GL_UNIFORM_SIZE answer.
Int size = 0;
// TObjectReflection::index - for a uniform, the TPROGRAM block index owning it
// (-1 for a default-block one; translate with GlBlockIndexFromTProgram).
Int index = -1;
Int counterIndex = -1;
Int arrayStride = 0;
Int topLevelArraySize = 0;
Int topLevelArrayStride = 0;
Int binding = -1;
Int location = -1; // layoutLocation()
// EShLanguageMask of the stages that reference it; 0 means "declared but read by
// nobody", which is what the dead-default-block-uniform filter tests.
Uint32 stages = 0;
// GL_UNIFORM_SIZE / GL_ARRAY_SIZE, already resolved through the
// isSizedArray()/getOuterArraySize()/size fallback.
GLint arraySize = 1;
TypeFacts type;
};
using UniformReflection = ResourceReflection;
using BlockReflection = ResourceReflection;
using PipeInputReflection = ResourceReflection;
using PipeOutputReflection = ResourceReflection;
ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {} ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {}
// Cancel-not-join, exactly like ~ShaderObject: the link job owns its inputs, so an // Cancel-not-join, exactly like ~ShaderObject: the link job owns its inputs, so an
// in-flight link whose program just went away is safe to abandon where it stands. // in-flight link whose program just went away is safe to abandon where it stands.
@@ -134,8 +213,7 @@ namespace MobileGL::MG_State::GLState {
const Int index = Artifacts().uniformIndexInTProgram[base]; const Int index = Artifacts().uniformIndexInTProgram[base];
// "[k]" only addresses arrays ("scalar[0]" is not a uniform name), and only // "[k]" only addresses arrays ("scalar[0]" is not a uniform name), and only
// in-range elements. // in-range elements.
const glslang::TType* type = Artifacts().program->getUniform(index).getType(); if (!UniformAt(index).type.isArray) return -1;
if (type == nullptr || !type->isArray()) return -1;
if (static_cast<GLint>(element) >= GetUniformArraySizeByTIndex(index)) return -1; if (static_cast<GLint>(element) >= GetUniformArraySizeByTIndex(index)) return -1;
const Int location = base + (Int)element; const Int location = base + (Int)element;
if (!UniformLocationsAliasSameUniform(base, location)) return -1; if (!UniformLocationsAliasSameUniform(base, location)) return -1;
@@ -175,44 +253,38 @@ namespace MobileGL::MG_State::GLState {
} }
Int GetActiveUniformIndex(const String& name) const { Int GetActiveUniformIndex(const String& name) const {
const Int tProgramCount = static_cast<Int>(Artifacts().tProgramUniformIndexToGl.size()); // uniformIndexByName is keyed by the REFLECTED name, so a lookup that hits is
const Int uniformIndex = Artifacts().program->getUniformIndex(name.c_str()); // already the exact-match the old code re-verified with a string compare after
if (uniformIndex >= 0 && uniformIndex < tProgramCount && // glslang's getUniformIndex(); a lookup that misses needs no bounds check.
Artifacts().program->getUniform(uniformIndex).name == name) { const auto& byName = Artifacts().uniformIndexByName;
return GlUniformIndexFromTProgram(uniformIndex); if (const auto direct = byName.find(name); direct != byName.end()) {
return GlUniformIndexFromTProgram(direct->second);
} }
// Reflection stores an array uniform under "arr[0]"; accept the bare "arr" // Reflection stores an array uniform under "arr[0]"; accept the bare "arr"
// spelling too. The reverse ("arr[0]" against a bare "arr" entry) is kept for // spelling too. The reverse ("arr[0]" against a bare "arr" entry) is kept for
// robustness against non-suffixed reflection entries. // robustness against non-suffixed reflection entries.
if (!name.empty() && name.back() != ']') { if (!name.empty() && name.back() != ']') {
const String suffixedName = name + "[0]"; const auto suffixed = byName.find(name + "[0]");
const Int suffixedIndex = Artifacts().program->getUniformIndex(suffixedName.c_str()); return suffixed != byName.end() ? GlUniformIndexFromTProgram(suffixed->second) : -1;
if (suffixedIndex >= 0 && suffixedIndex < tProgramCount &&
Artifacts().program->getUniform(suffixedIndex).name == suffixedName) {
return GlUniformIndexFromTProgram(suffixedIndex);
}
return -1;
} }
if (name.length() <= 3 || name.compare(name.length() - 3, 3, "[0]") != 0) return -1; if (name.length() <= 3 || name.compare(name.length() - 3, 3, "[0]") != 0) return -1;
const String baseName = name.substr(0, name.length() - 3); const auto base = byName.find(name.substr(0, name.length() - 3));
const Int baseIndex = Artifacts().program->getUniformIndex(baseName.c_str()); return base != byName.end() ? GlUniformIndexFromTProgram(base->second) : -1;
if (baseIndex < 0 || baseIndex >= tProgramCount) return -1;
return Artifacts().program->getUniform(baseIndex).name == baseName ? GlUniformIndexFromTProgram(baseIndex)
: -1;
} }
Bool IsValidUniformLocation(Int location) const { return IsValidUniformLocation(Artifacts(), location); } Bool IsValidUniformLocation(Int location) const { return IsValidUniformLocation(Artifacts(), location); }
GLenum GetUniformType(Uint location) const { GLenum GetUniformType(Uint location) const {
auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]); return UniformAt(Artifacts().uniformIndexInTProgram[location]).glDefineType;
return uniform.glDefineType;
} }
GLenum GetActiveUniformType(Uint index) const { GLenum GetActiveUniformType(Uint index) const {
auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); // The lowered counter is a plain uint inside a synthesized block; what the GL
return uniform.glDefineType; // client declared - and what glGetActiveUniform must report - is an atomic_uint.
if (IsActiveUniformAtomicCounter(index)) return GL_UNSIGNED_INT_ATOMIC_COUNTER;
return UniformAt(TProgramUniformIndex(index)).glDefineType;
} }
// Number of active array elements (GL_UNIFORM_SIZE / GL_ARRAY_SIZE); 1 for a non-array. // Number of active array elements (GL_UNIFORM_SIZE / GL_ARRAY_SIZE); 1 for a non-array.
@@ -229,16 +301,62 @@ namespace MobileGL::MG_State::GLState {
} }
Int GetActiveUniformBlockIndex(Uint index) const { Int GetActiveUniformBlockIndex(Uint index) const {
auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); // An atomic counter is a DEFAULT-BLOCK uniform to GL, whatever block the
// transpiler lowered it onto (GL 4.6 core 7.6, table 7.6): -1.
if (IsActiveUniformAtomicCounter(index)) return -1;
// Members of the synthesized global UBO are default-block uniforms to GL: -1. // Members of the synthesized global UBO are default-block uniforms to GL: -1.
return GlBlockIndexFromTProgram(uniform.index); return GlBlockIndexFromTProgram(UniformAt(TProgramUniformIndex(index)).index);
}
// The transpiler lowers every atomic_uint onto a synthesized gl_AtomicCounterBlock_N
// block, but GL keeps seeing an atomic counter as a default-block uniform of type
// GL_UNSIGNED_INT_ATOMIC_COUNTER that points at an atomic-counter BUFFER. These two
// answer for that GL-level declaration; without them the query surface reports the
// lowering instead (GL_UNSIGNED_INT, block index 0) and
// KHR-GL43.shader_atomic_counters.basic-program-query fails on both.
//
// The returned value is an index into the GL_ACTIVE_ATOMIC_COUNTER_BUFFERS list, i.e.
// the RANK of the owning counter block among the counter blocks in glslang's block
// order - exactly how ProgramInterface numbers the GL_ATOMIC_COUNTER_BUFFER
// resources glGetActiveAtomicCounterBufferiv answers from. -1 when this uniform is
// not an atomic counter.
// Answered from the OWNED reflection snapshot, never from Artifacts().program. This
// arrived reading the live TProgram, which is null for every program served from the
// translation cache's L1 - and unlike the other query-surface accessors that made the
// same mistake, this one DEREFERENCES it, so the second program built from a given set
// of sources would have taken the process down rather than answered wrongly. The
// snapshot carries the same three facts in the same TPROGRAM index space:
// getUniform(i).index -> UniformAt(i).index, getNumUniformBlocks() ->
// blockReflection.size(), getUniformBlock(i).name -> BlockAt(i).name.
Int GetActiveUniformAtomicCounterBufferIndex(Uint index) const {
const Int tIndex = TProgramUniformIndex(index);
if (tIndex < 0) return -1;
const Int owner = UniformAt(tIndex).index;
if (owner < 0) return -1;
const Int blockCount = static_cast<Int>(Artifacts().blockReflection.size());
if (owner >= blockCount) return -1;
const SizeT prefixLength = StringView(MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX).size();
Int counterBufferIndex = 0;
for (Int i = 0; i < blockCount; ++i) {
const auto& blockName = BlockAt(i).name;
if (blockName.compare(0, prefixLength, MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX) != 0) {
continue;
}
if (i == owner) return counterBufferIndex;
++counterBufferIndex;
}
return -1;
}
Bool IsActiveUniformAtomicCounter(Uint index) const {
return GetActiveUniformAtomicCounterBufferIndex(index) >= 0;
} }
// GL_UNIFORM_OFFSET: byte offset within the owning named block; -1 for a default-block // GL_UNIFORM_OFFSET: byte offset within the owning named block; -1 for a default-block
// uniform. The relaxed parse gives global-UBO members real byte offsets, but GL must keep // uniform. The relaxed parse gives global-UBO members real byte offsets, but GL must keep
// seeing them as default-block uniforms, so gate on the GL-visible block index. // seeing them as default-block uniforms, so gate on the GL-visible block index.
GLint GetActiveUniformOffset(Uint index) const { GLint GetActiveUniformOffset(Uint index) const {
const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); const auto& uniform = UniformAt(TProgramUniformIndex(index));
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1; if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
return uniform.offset; return uniform.offset;
} }
@@ -252,13 +370,12 @@ namespace MobileGL::MG_State::GLState {
// generated SPIR-V lay the array out with std140 16-byte-rounded strides. MobileGL's UBO // generated SPIR-V lay the array out with std140 16-byte-rounded strides. MobileGL's UBO
// layout is always std140, where every array element stride rounds up to a vec4. // layout is always std140, where every array element stride rounds up to a vec4.
GLint GetActiveUniformArrayStride(Uint index) const { GLint GetActiveUniformArrayStride(Uint index) const {
const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); const auto& uniform = UniformAt(TProgramUniformIndex(index));
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1; if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
const glslang::TType* type = uniform.getType(); if (!uniform.type.isArray) return 0;
if (type == nullptr || !type->isArray()) return 0; if (uniform.type.isMatrix) {
if (type->isMatrix()) {
const bool rowMajor = GetActiveUniformIsRowMajor(index) != 0; const bool rowMajor = GetActiveUniformIsRowMajor(index) != 0;
const int vectors = rowMajor ? type->getMatrixRows() : type->getMatrixCols(); const int vectors = rowMajor ? uniform.type.matrixRows : uniform.type.matrixCols;
return GetActiveUniformMatrixStride(index) * vectors; return GetActiveUniformMatrixStride(index) * vectors;
} }
return 16; // scalars and vectors: std140 rounds the element stride up to a vec4 return 16; // scalars and vectors: std140 rounds the element stride up to a vec4
@@ -272,15 +389,12 @@ namespace MobileGL::MG_State::GLState {
// check suffices; the getUniformBlock() fallback is defensive for a config that instead leaves // check suffices; the getUniformBlock() fallback is defensive for a config that instead leaves
// an inheriting member's layoutMatrix == ElmNone. // an inheriting member's layoutMatrix == ElmNone.
GLint GetActiveUniformIsRowMajor(Uint index) const { GLint GetActiveUniformIsRowMajor(Uint index) const {
const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); const auto& uniform = UniformAt(TProgramUniformIndex(index));
if (GlBlockIndexFromTProgram(uniform.index) < 0) return 0; if (GlBlockIndexFromTProgram(uniform.index) < 0) return 0;
const glslang::TType* type = uniform.getType(); if (!uniform.type.isMatrix) return 0;
if (type == nullptr || !type->isMatrix()) return 0; // layoutMatrix is already resolved against the owning block's qualifier at
glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix; // snapshot time, so the getUniformBlock() fallback this used to carry is gone.
if (layoutMatrix == glslang::ElmNone) { return (uniform.type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor)) ? 1 : 0;
layoutMatrix = Artifacts().program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix;
}
return (layoutMatrix == glslang::ElmRowMajor) ? 1 : 0;
} }
// GL_UNIFORM_MATRIX_STRIDE: byte stride between columns (col-major) / rows (row-major) of a // GL_UNIFORM_MATRIX_STRIDE: byte stride between columns (col-major) / rows (row-major) of a
@@ -290,16 +404,11 @@ namespace MobileGL::MG_State::GLState {
// out as std140 (packed/shared are coerced), so this matches the offsets glslang reports. For // out as std140 (packed/shared are coerced), so this matches the offsets glslang reports. For
// every GL 3.3 float matrix this evaluates to 16, independent of majorness. // every GL 3.3 float matrix this evaluates to 16, independent of majorness.
GLint GetActiveUniformMatrixStride(Uint index) const { GLint GetActiveUniformMatrixStride(Uint index) const {
const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); const auto& uniform = UniformAt(TProgramUniformIndex(index));
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1; if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
const glslang::TType* type = uniform.getType(); if (!uniform.type.isMatrix) return 0;
if (type == nullptr || !type->isMatrix()) return 0; const bool rowMajor = (uniform.type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor));
glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix; const int strideVectorComponents = rowMajor ? uniform.type.matrixCols : uniform.type.matrixRows;
if (layoutMatrix == glslang::ElmNone) {
layoutMatrix = Artifacts().program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix;
}
const bool rowMajor = (layoutMatrix == glslang::ElmRowMajor);
const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows();
constexpr int scalarSize = 4; // GL 3.3 core uniform matrices are float constexpr int scalarSize = 4; // GL 3.3 core uniform matrices are float
const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize
: (strideVectorComponents == 2) ? 2 * scalarSize : (strideVectorComponents == 2) ? 2 * scalarSize
@@ -307,21 +416,39 @@ namespace MobileGL::MG_State::GLState {
return (vectorAlignment + 15) & ~15; // std140 round-up to a vec4 return (vectorAlignment + 15) & ~15; // std140 round-up to a vec4
} }
const glslang::TType* GetUniformTType(Uint location) const { // The flattened type of the uniform at `location`. This is what replaced
auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]); // GetUniformTType(): the same information, owned by the program instead of by a
return uniform.getType(); // glslang pool, so it stays valid for a link served from the L1 translation memo.
const TypeFacts& GetUniformTypeFacts(Uint location) const {
return UniformAt(Artifacts().uniformIndexInTProgram[location]).type;
} }
Bool IsUniformOpaqueAtLocation(Uint location) const { return GetUniformTType(location)->isOpaque(); } // Replaces GetUniformTType(), which used to hand a raw glslang::TType* - into a
// pool the program no longer necessarily owns - out to the DirectGLES image-format
// bake. These are the only three things any caller ever read off it.
Bool UniformHasDeclaredImageFormat(Uint location) const {
return UniformAt(Artifacts().uniformIndexInTProgram[location]).type.hasFormat;
}
Uint GetUniformDeclaredImageFormat(Uint location) const {
return UniformAt(Artifacts().uniformIndexInTProgram[location]).type.layoutFormat;
}
// Matrix column count, 0 for a non-matrix. The global-UBO fallback allocator sizes a
// matrix slot from it.
Int GetUniformMatrixColumns(Uint location) const {
const auto& uniform = UniformAt(Artifacts().uniformIndexInTProgram[location]);
return uniform.type.isMatrix ? uniform.type.matrixCols : 0;
}
Bool IsUniformOpaqueAtLocation(Uint location) const {
return UniformAt(Artifacts().uniformIndexInTProgram[location]).type.isOpaque;
}
const String& GetUniformName(Uint location) const { const String& GetUniformName(Uint location) const {
auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]); return UniformAt(Artifacts().uniformIndexInTProgram[location]).name;
return uniform.name;
} }
const String& GetActiveUniformName(Uint index) const { const String& GetActiveUniformName(Uint index) const {
auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); return UniformAt(TProgramUniformIndex(index)).name;
return uniform.name;
} }
// Sentinel for a uniform location without global-UBO backing storage (should not // Sentinel for a uniform location without global-UBO backing storage (should not
// survive linking: GenerateBinary falls back to tail-allocated scratch storage). // survive linking: GenerateBinary falls back to tail-allocated scratch storage).
@@ -355,17 +482,17 @@ namespace MobileGL::MG_State::GLState {
// would, half its GL type size, and a `dmat4` is padded like any other matrix. Anything // would, half its GL type size, and a `dmat4` is padded like any other matrix. Anything
// reading or writing a whole uniform's storage - a bounds check, a copy between two // reading or writing a whole uniform's storage - a bounds check, a copy between two
// programs' shadows - wants this rather than GetUniformSizesInBytes. // programs' shadows - wants this rather than GetUniformSizesInBytes.
static SizeT UniformStorageSpanInBytes(const glslang::TType* type, SizeT tightSize) { static SizeT UniformStorageSpanInBytes(const TypeFacts& type, SizeT tightSize) {
if (type != nullptr && type->isMatrix()) { if (type.isMatrix) {
return static_cast<SizeT>(type->getMatrixCols()) * 4 * sizeof(Float); return static_cast<SizeT>(type.matrixCols) * 4 * sizeof(Float);
} }
if (type != nullptr && type->getBasicType() == glslang::EbtDouble) { if (type.isDouble) {
return tightSize / 2; return tightSize / 2;
} }
return tightSize; return tightSize;
} }
SizeT GetUniformStorageSpanInBytes(Uint location) const { SizeT GetUniformStorageSpanInBytes(Uint location) const {
return UniformStorageSpanInBytes(GetUniformTType(location), GetUniformSizesInBytes(location)); return UniformStorageSpanInBytes(GetUniformTypeFacts(location), GetUniformSizesInBytes(location));
} }
// ---- "written since link": the per-location dirty set the pipeline composite mirrors from ---- // ---- "written since link": the per-location dirty set the pipeline composite mirrors from ----
@@ -476,14 +603,14 @@ namespace MobileGL::MG_State::GLState {
return mask; return mask;
} }
Uint32 GetActiveFragmentOutputLocationMask() const { Uint32 GetActiveFragmentOutputLocationMask() const {
if (!Artifacts().program) { if (Artifacts().pipeOutputReflection.empty()) {
return 0; return 0;
} }
Uint32 mask = 0; Uint32 mask = 0;
const Int outputCount = Artifacts().program->getNumPipeOutputs(); const Int outputCount = static_cast<Int>(Artifacts().pipeOutputReflection.size());
for (Int index = 0; index < outputCount; ++index) { for (Int index = 0; index < outputCount; ++index) {
const Int location = static_cast<Int>(Artifacts().program->getPipeOutput(index).layoutLocation()); const Int location = Artifacts().pipeOutputReflection[index].location;
if (location >= 0 && location < 32) { if (location >= 0 && location < 32) {
mask |= (1u << location); mask |= (1u << location);
} }
@@ -491,38 +618,34 @@ namespace MobileGL::MG_State::GLState {
return mask; return mask;
} }
Int GetActiveFragmentOutputCount() const { Int GetActiveFragmentOutputCount() const {
return Artifacts().program ? Artifacts().program->getNumPipeOutputs() : 0; return static_cast<Int>(Artifacts().pipeOutputReflection.size());
} }
const String& GetActiveFragmentOutputName(Uint index) const { const String& GetActiveFragmentOutputName(Uint index) const {
MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetActiveFragmentOutputName: program is null"); MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().pipeOutputReflection.size()),
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
"ProgramObject::GetActiveFragmentOutputName: index=%u out of range", index); "ProgramObject::GetActiveFragmentOutputName: index=%u out of range", index);
return Artifacts().program->getPipeOutput(static_cast<Int>(index)).name; return Artifacts().pipeOutputReflection[index].name;
} }
Int GetFragmentOutputLocation(Uint index) const { Int GetFragmentOutputLocation(Uint index) const {
MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetFragmentOutputLocation: program is null"); MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().pipeOutputReflection.size()),
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
"ProgramObject::GetFragmentOutputLocation: index=%u out of range", "ProgramObject::GetFragmentOutputLocation: index=%u out of range",
index); index);
return static_cast<Int>(Artifacts().program->getPipeOutput(static_cast<Int>(index)).layoutLocation()); return Artifacts().pipeOutputReflection[index].location;
} }
GLint GetActiveFragmentOutputArraySize(Uint index) const { GLint GetActiveFragmentOutputArraySize(Uint index) const {
MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetActiveFragmentOutputArraySize: program is null"); MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().pipeOutputReflection.size()),
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
"ProgramObject::GetActiveFragmentOutputArraySize: index=%u out of range", index); "ProgramObject::GetActiveFragmentOutputArraySize: index=%u out of range", index);
return Artifacts().program->getPipeOutput(static_cast<Int>(index)).size; return Artifacts().pipeOutputReflection[index].size;
} }
GLenum GetFragmentOutputType(Uint index) const { GLenum GetFragmentOutputType(Uint index) const {
MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetFragmentOutputType: program is null"); MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().pipeOutputReflection.size()),
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
"ProgramObject::GetFragmentOutputType: index=%u out of range", "ProgramObject::GetFragmentOutputType: index=%u out of range",
index); index);
return Artifacts().program->getPipeOutput(static_cast<Int>(index)).glDefineType; return Artifacts().pipeOutputReflection[index].glDefineType;
} }
GLenum GetAttribType(Uint index) const { return Artifacts().attribTypes[index]; } GLenum GetAttribType(Uint index) const { return Artifacts().attribTypes[index]; }
const String& GetAttribName(Uint index) const { return Artifacts().attribs[index]; } const String& GetAttribName(Uint index) const { return Artifacts().attribs[index]; }
GLenum GetActiveAttribType(Uint index) const { return Artifacts().program->getPipeInput(static_cast<Int>(index)).glDefineType; } GLenum GetActiveAttribType(Uint index) const { return Artifacts().pipeInputReflection[index].glDefineType; }
GLint GetActiveAttribArraySize(Uint index) const { return Artifacts().program->getPipeInput(static_cast<Int>(index)).size; } GLint GetActiveAttribArraySize(Uint index) const { return Artifacts().pipeInputReflection[index].size; }
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V names; // The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V names;
// GL must keep reporting the GL spellings (glGetActiveAttrib and the program-input // GL must keep reporting the GL spellings (glGetActiveAttrib and the program-input
// resource queries enumerate builtins). // resource queries enumerate builtins).
@@ -534,7 +657,7 @@ namespace MobileGL::MG_State::GLState {
return name; return name;
} }
const String& GetActiveAttribName(Uint index) const { const String& GetActiveAttribName(Uint index) const {
return NormalizeBuiltinPipeInputName(Artifacts().program->getPipeInput(static_cast<Int>(index)).name); return NormalizeBuiltinPipeInputName(Artifacts().pipeInputReflection[index].name);
} }
// PHASE B, all three (see EnsureSpirvJoined): the shadow buffer's layout is decided // PHASE B, all three (see EnsureSpirvJoined): the shadow buffer's layout is decided
// by the OPTIMIZED SPIR-V, so it does not exist until the SPIR-V job has settled - and // by the OPTIMIZED SPIR-V, so it does not exist until the SPIR-V job has settled - and
@@ -627,7 +750,7 @@ namespace MobileGL::MG_State::GLState {
// which means the change is only honoured by regenerating the program. That // which means the change is only honoured by regenerating the program. That
// regeneration is gated on link-shaped versions, so without a counter that moves // regeneration is gated on link-shaped versions, so without a counter that moves
// here the new unit would never reach the driver. // here the new unit would never reach the driver.
if (const glslang::TType* type = GetUniformTType(location); type != nullptr && type->isImage()) { if (GetUniformTypeFacts(location).isImage) {
++m_imageUnitVersion; ++m_imageUnitVersion;
} }
} }
@@ -702,20 +825,14 @@ namespace MobileGL::MG_State::GLState {
// SIGSEGV inside glslang::TProgram::getNumPipeInputs - KHR-GL30.api.coverage does exactly // SIGSEGV inside glslang::TProgram::getNumPipeInputs - KHR-GL30.api.coverage does exactly
// this after a failed glGetAttribLocation, and reached it as soon as the CopyTexImage2D // this after a failed glGetAttribLocation, and reached it as soon as the CopyTexImage2D
// throw ahead of it stopped killing the run first. // throw ahead of it stopped killing the run first.
Int GetActiveAtomicCounterCount() const {
const auto& program = Artifacts().program;
return program ? program->getNumAtomicCounters() : 0;
}
Int GetActiveAttributesCount() const { Int GetActiveAttributesCount() const {
const auto& program = Artifacts().program; return static_cast<Int>(Artifacts().pipeInputReflection.size());
return program ? program->getNumPipeInputs() : 0;
} }
// GL-visible uniform blocks only: the synthesized MGL_GLOBAL_UBO the relaxed parse // GL-visible uniform blocks only: the synthesized MGL_GLOBAL_UBO the relaxed parse
// materializes for default-block uniforms is filtered out by DoReflection. // materializes for default-block uniforms is filtered out by DoReflection.
Int GetActiveUniformBlocksCount() const { return static_cast<Int>(Artifacts().glBlockIndexToTProgram.size()); } Int GetActiveUniformBlocksCount() const { return static_cast<Int>(Artifacts().glBlockIndexToTProgram.size()); }
GLuint GetComputeLocalSize(Uint dim) const { GLuint GetComputeLocalSize(Uint dim) const {
const auto& program = Artifacts().program; return dim < 3u ? Artifacts().computeLocalSize[dim] : 0u;
return program ? program->getLocalSize(static_cast<Int>(dim)) : 0;
} }
Int GetActiveAttributesMaxLength() const { return Artifacts().attribInNameMaxLength; } Int GetActiveAttributesMaxLength() const { return Artifacts().attribInNameMaxLength; }
Int GetActiveUniformBlocksMaxNameLength() const { return Artifacts().uniformBlockNameMaxLength; } Int GetActiveUniformBlocksMaxNameLength() const { return Artifacts().uniformBlockNameMaxLength; }
@@ -739,11 +856,11 @@ namespace MobileGL::MG_State::GLState {
// (like a std140 struct) occupies a vec4-rounded size, and that is what the // (like a std140 struct) occupies a vec4-rounded size, and that is what the
// backend compiles: ES drivers reject draws whose bound UBO range is smaller // backend compiles: ES drivers reject draws whose bound UBO range is smaller
// than the block (a block ending in ivec3 reported 12 while the driver needs 16). // than the block (a block ending in ivec3 reported 12 while the driver needs 16).
return (Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]).size + 15u) & ~15u; return (static_cast<Uint>(BlockAt(Artifacts().glBlockIndexToTProgram[index]).size) + 15u) & ~15u;
} }
const String& GetUniformBlockName(Uint index) const { const String& GetUniformBlockName(Uint index) const {
auto& ubo = Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]); const auto& ubo = BlockAt(Artifacts().glBlockIndexToTProgram[index]);
return ubo.name; return ubo.name;
} }
@@ -774,7 +891,7 @@ namespace MobileGL::MG_State::GLState {
} }
Bool IsUniformBlockReferencedByStage(Uint index, EShLanguage stage) const { Bool IsUniformBlockReferencedByStage(Uint index, EShLanguage stage) const {
const auto& ubo = Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]); const auto& ubo = BlockAt(Artifacts().glBlockIndexToTProgram[index]);
const auto stageMask = static_cast<EShLanguageMask>(1 << stage); const auto stageMask = static_cast<EShLanguageMask>(1 << stage);
return (ubo.stages & stageMask) != 0; return (ubo.stages & stageMask) != 0;
} }
@@ -855,8 +972,6 @@ namespace MobileGL::MG_State::GLState {
// (MG_Impl/GLImpl/Program/ProgramInterface.cpp), which has to enumerate buffer // (MG_Impl/GLImpl/Program/ProgramInterface.cpp), which has to enumerate buffer
// blocks, buffer variables, atomic counters and per-stage reference masks. Null // blocks, buffer variables, atomic counters and per-stage reference masks. Null
// until a link has succeeded. Read through the join gate like everything else. // until a link has succeeded. Read through the join gate like everything else.
const glslang::TProgram* GetReflection() const { return Artifacts().program.get(); }
Int GetShaderIndexByStage(ShaderStage stage) const { Int GetShaderIndexByStage(ShaderStage stage) const {
auto it = std::find_if(m_shaders.begin(), m_shaders.end(), [stage](const SharedPtr<ShaderObject>& shader) { auto it = std::find_if(m_shaders.begin(), m_shaders.end(), [stage](const SharedPtr<ShaderObject>& shader) {
return shader->GetShaderStage() == stage; return shader->GetShaderStage() == stage;
@@ -909,9 +1024,50 @@ namespace MobileGL::MG_State::GLState {
// what makes "every read of link output joins the pending link" a property the // what makes "every read of link output joins the pending link" a property the
// compiler checks rather than a review item - a new reader cannot spell the field // compiler checks rather than a review item - a new reader cannot spell the field
// without going through the gate. // without going through the gate.
// ---- the owned mirror of glslang's reflection ----
//
// WHY THIS EXISTS. Every GL query about a linked program used to be answered by
// asking the live glslang::TProgram - program->getUniform(i).getType()->isMatrix()
// and friends. That made the TProgram part of the program's PERMANENT state, which
// in turn made the whole front end (parse + link) unskippable: the L1 shader
// translation memo could hand back the SPIR-V but the reflection still had to be
// rebuilt from a freshly parsed AST.
//
// These three tables are a snapshot of everything the query surface ever reads off
// the TProgram, in PLAIN OWNED VALUES - no TType*, no TString, nothing pointing into
// a glslang pool. Taken once at the tail of DoReflection (SnapshotGlslangReflection),
// they are copyable, immutable after the link, and safe to memoize and share between
// ProgramObjects and threads. Once they are filled, `program` is dead weight to
// everything except DoReflection itself.
//
// INDEXED BY TPROGRAM INDEX, deliberately: that is the space uniformIndexInTProgram,
// glUniformIndexToTProgram and tProgramUniformIndexToGl already speak, so every
// accessor that used to call program->getUniform(i) indexes uniformReflection[i]
// instead, unchanged in every other respect.
struct LinkArtifacts { struct LinkArtifacts {
// Live only between LinkProgram() and the end of DoReflection. Everything after
// that reads the owned mirror below; a link served from the L1 memo never
// constructs one at all, so this is null for such a program and MUST NOT be
// dereferenced outside DoReflection.
SharedPtr<glslang::TProgram> program; SharedPtr<glslang::TProgram> program;
// The owned reflection snapshot. Indexed by TProgram index; see the structs above.
Vector<UniformReflection> uniformReflection;
Vector<BlockReflection> blockReflection;
Vector<PipeInputReflection> pipeInputReflection;
Vector<PipeOutputReflection> pipeOutputReflection;
// Program-level scalars glslang answers off the linked intermediates.
// Whether the program's LAST stage is the fragment stage. A color number - and so a
// color index - exists only there; a separable tess/geometry/vertex program's
// outputs are varyings and must report -1 (KHR-GL43.program_interface_query.
// separate-programs-tess-control).
Bool lastStageIsFragment = false;
Array<GLuint, 3> computeLocalSize{};
// Replaces program->getUniformIndex(name). Maps the reflected name to its
// TProgram uniform index.
UnorderedMap<String, Int> uniformIndexByName;
// Attributes (Vertex in) // Attributes (Vertex in)
Vector<String> attribs; Vector<String> attribs;
Vector<GLenum> attribTypes; Vector<GLenum> attribTypes;
@@ -1042,6 +1198,14 @@ namespace MobileGL::MG_State::GLState {
// ordering is explicit and nothing is exempt. // ordering is explicit and nothing is exempt.
static void ResetLinkArtifacts(LinkArtifacts& artifacts); static void ResetLinkArtifacts(LinkArtifacts& artifacts);
// The owned reflection snapshot, for the program-interface query layer. Replaces
// GetReflection(), which handed out the live glslang::TProgram - the last thing that
// forced a linked program to keep its parse alive.
const LinkArtifacts& GetLinkReflection() const {
EnsureLinkJoined();
return Artifacts();
}
static Bool IsValidUniformLocation(const LinkArtifacts& artifacts, Int location) { static Bool IsValidUniformLocation(const LinkArtifacts& artifacts, Int location) {
if (location < 0 || location > static_cast<Int>(artifacts.maxUniformLocation)) return false; if (location < 0 || location > static_cast<Int>(artifacts.maxUniformLocation)) return false;
if (static_cast<SizeT>(location) >= artifacts.uniformIndexInTProgram.size()) return false; if (static_cast<SizeT>(location) >= artifacts.uniformIndexInTProgram.size()) return false;
@@ -1057,12 +1221,24 @@ namespace MobileGL::MG_State::GLState {
// for both. GL 3.3 core uniforms are always sized. Takes a TProgram uniform index (the space // for both. GL 3.3 core uniforms are always sized. Takes a TProgram uniform index (the space
// the artifacts' uniformIndexInTProgram stores). // the artifacts' uniformIndexInTProgram stores).
static GLint GetUniformArraySizeByTIndex(const LinkArtifacts& artifacts, Int tIndex) { static GLint GetUniformArraySizeByTIndex(const LinkArtifacts& artifacts, Int tIndex) {
const auto& uniform = artifacts.program->getUniform(tIndex); return UniformAtIn(artifacts, tIndex).arraySize;
const glslang::TType* type = uniform.getType(); }
if (type != nullptr && type->isSizedArray()) {
return type->getOuterArraySize(); // Bounds-checked mirror lookup. Out of range yields a default-constructed entry
// rather than UB, which is the same shape the phase-B getters use: a program whose
// reflection is missing must stay answerable, not crash the query surface.
static const UniformReflection& UniformAtIn(const LinkArtifacts& artifacts, Int tIndex) {
static const UniformReflection kEmpty;
if (tIndex < 0 || static_cast<SizeT>(tIndex) >= artifacts.uniformReflection.size()) return kEmpty;
return artifacts.uniformReflection[tIndex];
}
const UniformReflection& UniformAt(Int tIndex) const { return UniformAtIn(Artifacts(), tIndex); }
const BlockReflection& BlockAt(Int tBlockIndex) const {
static const BlockReflection kEmpty;
if (tBlockIndex < 0 || static_cast<SizeT>(tBlockIndex) >= Artifacts().blockReflection.size()) {
return kEmpty;
} }
return uniform.size < 1 ? 1 : uniform.size; return Artifacts().blockReflection[tBlockIndex];
} }
// Blocks until a pending link has published its artifacts. Public because a few call // Blocks until a pending link has published its artifacts. Public because a few call
@@ -12,6 +12,8 @@
#include <MG_Util/Async/ShaderCompilePool.h> #include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h> #include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/SpvcSession.h> #include <MG_Util/ShaderTranspiler/SpvcSession.h>
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
#include <MG_Util/ShaderTranspiler/Types.h> #include <MG_Util/ShaderTranspiler/Types.h>
#include <cstring> #include <cstring>
@@ -95,11 +97,25 @@ namespace MobileGL::MG_State::GLState {
// and `diagnostics`, and this node is the sole reader of the handoff. // and `diagnostics`, and this node is the sole reader of the handoff.
ProgramLinkTask::SpirvHandoff& handoff = m_phaseA->spirvHandoff; ProgramLinkTask::SpirvHandoff& handoff = m_phaseA->spirvHandoff;
const Uint externalIndex = m_phaseA->in.externalIndex; const Uint externalIndex = m_phaseA->in.externalIndex;
if (!handoff.ready || !handoff.reflection.program) { if (!handoff.ready) {
// Phase A did not reach its tail (it failed the link, or was cancelled mid-body). // Phase A did not reach its tail (it failed the link, or was cancelled mid-body).
// Publish nothing; spirvStatus stays false. // Publish nothing; spirvStatus stays false.
return; return;
} }
// A TProgram is required only to GENERATE. A link served from the L1 memo has none by
// construction - that is the entire point of the widened payload - and its SPIR-V and
// routing tables arrive ready-made in cachedSpirv.
if (!handoff.cachedSpirv && !handoff.reflection.program) return;
// An L1 hit already carries everything this phase would have produced. Publish it
// and stop: no GlslangToSpv, no spirv-opt, no routing pass.
if (handoff.cachedSpirv) {
artifacts = *handoff.cachedSpirv;
MGLOG_D("ProgramObject %u: L1 cache hit - %zu SPIR-V module(s) and the global-UBO "
"routing reused",
externalIndex, artifacts.generatedSpirv.size());
return;
}
MGLOG_D("ProgramObject %u: Starting SPIR-V generation", externalIndex); MGLOG_D("ProgramObject %u: Starting SPIR-V generation", externalIndex);
const Bool deferOutputValidationForDirectVulkan = const Bool deferOutputValidationForDirectVulkan =
@@ -117,13 +133,18 @@ namespace MobileGL::MG_State::GLState {
// * CAS-LOSER shaders (the re-parse in ShaderCompileTask::ClaimParsedShader, i.e. // * CAS-LOSER shaders (the re-parse in ShaderCompileTask::ClaimParsedShader, i.e.
// the 2nd..Nth link of a shared shader): freed here in full. The handoff is their // the 2nd..Nth link of a shared shader): freed here in full. The handoff is their
// ONLY owner. // ONLY owner.
// * CAS-WINNER shaders (the common case - one shader object linked into one // * L1c-HIT shaders (the compile published a verdict and never parsed, so the parse
// program, which is every program of an Iris pack load): NOT freed here. The // was made on demand by ClaimParsedShader): freed here in full, exactly like a
// winner branch returns a COPY of ShaderCompileTask::artifacts.shader // CAS loser and for the same reason - the handoff is their only owner. This
// (ShaderCompileTask.cpp:320) and the node never releases its own reference, while // category did not exist before the translation memo's compile half, and it makes
// phase A holds that node through in.shaders[i].compiled for its whole life - and // the clear below strictly more effective than the paragraph below describes.
// phase A lives until PhaseAReleaser fires at the end of this body. So the // * CAS-WINNER shaders (one shader object linked into one program, whose compile
// refcount goes 2 -> 1 here and the arena dies where it would have died anyway. // MISSED L1c and therefore stored its parse): NOT freed here. The winner branch
// returns a COPY of ShaderCompileTask::artifacts.shader and the node never
// releases its own reference, while phase A holds that node through
// in.shaders[i].compiled for its whole life - and phase A lives until
// PhaseAReleaser fires at the end of this body. So the refcount goes 2 -> 1 here
// and the arena dies where it would have died anyway.
// //
// Making it free the winner's arena too means releasing whatever pins the TShader // Making it free the winner's arena too means releasing whatever pins the TShader
// inside the compile node, and neither obvious route is safe as a drive-by: moving out // inside the compile node, and neither obvious route is safe as a drive-by: moving out
@@ -137,6 +158,23 @@ namespace MobileGL::MG_State::GLState {
MGLOG_D("ProgramObject %u: Building global-UBO routing tables", externalIndex); MGLOG_D("ProgramObject %u: Building global-UBO routing tables", externalIndex);
BuildGlobalUboRouting(handoff, externalIndex); BuildGlobalUboRouting(handoff, externalIndex);
// The completed front end goes into the L1 memo HERE, where both halves exist: phase
// A's LinkArtifacts (carried in the handoff) and this phase's SpirvArtifacts.
//
// Only a clean run is memoized. A failed optimizer run leaves a module as whatever the
// chain got to before it gave up, and that is exactly the binary no other program
// should ever be handed.
if (artifacts.spirvStatus && handoff.spirvCacheKey.Valid() && handoff.linkArtifactsForCache) {
auto payload = MakeShared<ProgramTranslationResult>();
payload->link = *handoff.linkArtifactsForCache;
payload->link.program.reset(); // belt and braces: never memoize a glslang arena
payload->spirv = artifacts;
const SizeT payloadBytes = ProgramTranslationResultBytes(*payload);
GetProgramTranslationCache().Insert(handoff.spirvCacheKey,
ProgramTranslationResultPtr(Move(payload)),
payloadBytes);
}
MGLOG_D("ProgramObject %u: Binary generation finished (generatedSpirv size=%zu)", externalIndex, MGLOG_D("ProgramObject %u: Binary generation finished (generatedSpirv size=%zu)", externalIndex,
artifacts.generatedSpirv.size()); artifacts.generatedSpirv.size());
} }
@@ -296,22 +334,25 @@ namespace MobileGL::MG_State::GLState {
for (Uint location = 0; location <= reflection.maxUniformLocation; ++location) { for (Uint location = 0; location <= reflection.maxUniformLocation; ++location) {
if (artifacts.uniformOffsets[location] != ProgramObject::kInvalidUniformOffset) continue; if (artifacts.uniformOffsets[location] != ProgramObject::kInvalidUniformOffset) continue;
if (!ProgramObject::IsValidUniformLocation(reflection, static_cast<Int>(location))) continue; if (!ProgramObject::IsValidUniformLocation(reflection, static_cast<Int>(location))) continue;
const auto& uniform = reflection.program->getUniform(reflection.uniformIndexInTProgram[location]); const auto& uniform =
const glslang::TType* type = uniform.getType(); ProgramObject::UniformAtIn(reflection, reflection.uniformIndexInTProgram[location]);
if (type != nullptr && type->isOpaque()) continue; if (uniform.type.isOpaque) continue;
if (uniform.index >= 0 && uniform.index < reflection.program->getNumUniformBlocks() && // Member of a named uniform block: not settable through glUniform*, so it needs
std::strstr(reflection.program->getUniformBlock(uniform.index).name.c_str(), // no global-UBO shadow storage. tProgramBlockIndexToGl[i] >= 0 means block i is
MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) == nullptr) { // GL-visible, i.e. NOT the synthesized MGL_GLOBAL_UBO - which is exactly what the
// Member of a named uniform block: not settable through glUniform*, so it // strstr(GLOBAL_UBO_NAME) test this replaced was asking, without needing the
// needs no global-UBO shadow storage. // TProgram to spell the block name.
if (uniform.index >= 0 &&
uniform.index < static_cast<Int>(reflection.tProgramBlockIndexToGl.size()) &&
reflection.tProgramBlockIndexToGl[uniform.index] >= 0) {
continue; continue;
} }
// std140-style slot: the matrix upload paths write column vectors at // std140-style slot: the matrix upload paths write column vectors at
// 16-byte strides, so a matrix slot must cover cols * 16 bytes. // 16-byte strides, so a matrix slot must cover cols * 16 bytes.
SizeT slotSize = MG_Util::GetGLTypeSize(uniform.glDefineType); SizeT slotSize = MG_Util::GetGLTypeSize(uniform.glDefineType);
if (type != nullptr && type->isMatrix()) { if (uniform.type.isMatrix) {
slotSize = static_cast<SizeT>(type->getMatrixCols()) * 16u; slotSize = static_cast<SizeT>(uniform.type.matrixCols) * 16u;
} }
slotSize = (slotSize + 15u) & ~static_cast<SizeT>(15u); slotSize = (slotSize + 15u) & ~static_cast<SizeT>(15u);
const SizeT slotOffset = (artifacts.globalUboScratch.size() + 15u) & ~static_cast<SizeT>(15u); const SizeT slotOffset = (artifacts.globalUboScratch.size() + 15u) & ~static_cast<SizeT>(15u);
@@ -0,0 +1,82 @@
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "ProgramTranslationCache.h"
namespace MobileGL::MG_State::GLState {
namespace {
// ---- L1 caps: 48 entries / 24 MiB ----
//
// Both numbers moved when the payload grew from "the SPIR-V modules" to "the whole
// front end". An entry is now the stages' preprocessed source (the key), the SPIR-V,
// the reflection snapshot and the global-UBO shadow - roughly twice what it was - so
// the byte budget doubled and the entry count came down to keep the worst case in the
// same place on a phone.
//
// The shape of the choice has not changed: this cache exists for REPETITION, not
// coverage. A KHR-GL33.texture_swizzle smoke case builds 2592 programs out of fewer
// than ten distinct ones, so a handful of entries serves it completely; an Iris
// shaderpack load is ~300-600 MOSTLY DISTINCT programs that would never hit however
// large the cache is, so a bigger cap there buys nothing and costs resident memory.
// 48 is comfortably above the distinct-program count of every repetition workload
// measured, and 24 MiB bounds the pathological case - a pack whose ~100 KB stages
// really are re-linked - at roughly three times the existing 8 MiB
// ShaderPreprocessCache budget, which is the other memo on this path.
constexpr SizeT kMaxEntries = 48;
constexpr SizeT kMaxBytes = 24u * 1024u * 1024u;
SizeT StringsBytes(const Vector<String>& values) {
SizeT bytes = 0;
for (const String& value : values) bytes += value.size() + sizeof(String);
return bytes;
}
SizeT ResourcesBytes(const Vector<ProgramObject::ResourceReflection>& records) {
SizeT bytes = records.size() * sizeof(ProgramObject::ResourceReflection);
for (const auto& record : records) bytes += record.name.size();
return bytes;
}
} // namespace
// Approximate on purpose: it feeds a budget, not an allocator. It counts the things that
// actually scale with shader size - the SPIR-V, the reflection names, the UBO shadow -
// and ignores per-entry fixed overhead.
SizeT ProgramTranslationResultBytes(const ProgramTranslationResult& result) {
SizeT bytes = 0;
for (const auto& module : result.spirv.generatedSpirv) bytes += module.size() * sizeof(unsigned);
bytes += result.spirv.uniformOffsets.size() * sizeof(Uint);
bytes += result.spirv.globalUboScratch.size();
bytes += ResourcesBytes(result.link.uniformReflection);
bytes += ResourcesBytes(result.link.blockReflection);
bytes += ResourcesBytes(result.link.pipeInputReflection);
bytes += ResourcesBytes(result.link.pipeOutputReflection);
bytes += StringsBytes(result.link.attribs);
bytes += StringsBytes(result.link.xfbInterfaceNames);
bytes += result.link.infoLog.size();
return bytes;
}
MG_Util::ShaderTranspiler::BoundedTranslationCache<ProgramTranslationResult>&
GetProgramTranslationCache() {
// DELIBERATELY LEAKED - see the same note on the L2 cache in
// MG_Util/ShaderTranspiler/TranslationCache.cpp. A function-local static OBJECT
// registers its destructor at first use, and first use here is a ShaderCompilePool
// worker; ShaderCompilePool's own atexit drain sentinel is registered strictly
// earlier, and exit handlers run in reverse order - so the cache would be destroyed
// while workers were still inserting into it. A function-local static POINTER is
// trivially destructible and registers no exit handler at all.
static auto* const kCache =
new MG_Util::ShaderTranspiler::BoundedTranslationCache<ProgramTranslationResult>(
"ShaderTranslationCache L1 (GLSL->front end)", kMaxEntries, kMaxBytes);
return *kCache;
}
void ClearProgramTranslationCache() { GetProgramTranslationCache().Clear(); }
void LogProgramTranslationCacheStats() { GetProgramTranslationCache().LogStats(); }
} // namespace MobileGL::MG_State::GLState
@@ -0,0 +1,69 @@
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include <MG_State/GLState/ProgramState/ProgramObject.h>
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
namespace MobileGL::MG_State::GLState {
// ===================================================================================
// L1 of the shader translation memo: THE WHOLE FRONT END of one glLinkProgram.
//
// A hit skips the glslang link and mapIO, GlslangToSpv, the 11-pass
// SanitizeAndOptimizeBinary chain, buildReflection, and the global-UBO routing pass. No
// TProgram is constructed at all - which is only possible because the GL query surface no
// longer reads one (see ProgramObject::UniformReflection and
// ProgramLinkTask::SnapshotGlslangReflection).
//
// IT DOES NOT SKIP THE PARSE, and no widening of this payload could: the parse belongs to
// glCompileShader, a different entry point one job earlier, and it has already run by the
// time a link looks this key up. Skipping it is L1c's job - the compile half of the memo,
// in MG_Util/ShaderTranspiler/TranslationCache.h. The two together are what make a
// repeated program build construct no glslang object of any kind; either one alone leaves
// roughly half the front end on the hot path (~322 us of parse against a ~650 us
// CTS-shaped program build, and 1.45-1.48x measured on device with L1 alone).
//
// WHY THE PAYLOAD IS THE WHOLE THING rather than just the SPIR-V: the frontend answers
// glGetActiveUniform, glGetProgramResource*, glGetUniformLocation and the rest out of
// LinkArtifacts, and glUniform*/glGetUniform* out of SpirvArtifacts. Caching only the
// modules would have left the link on the hot path to rebuild exactly the data the
// payload can carry.
//
// WHY IT LIVES HERE AND NOT IN MG_Util: the payload is a ProgramObject::LinkArtifacts
// plus a ProgramObject::SpirvArtifacts, and MG_Util must not depend on MG_State. The
// KEY is plain bytes and stays in MG_Util (BuildSpirvTranslationKey), so both layers
// agree on exactly one definition of "the same front-end input".
//
// EVERYTHING IN THE PAYLOAD IS PLAIN OWNED DATA. `link.program` is null by construction:
// the whole point is that a hit never has a glslang arena to point into. Both structs
// were audited field by field - the only member that ever pointed into glslang-owned
// memory was `program` itself, and TUniformInitializer / XfbVarying, which look like
// glslang types, are std::string + std::vector aggregates.
struct ProgramTranslationResult {
// program == nullptr, always. Asserted at insert.
ProgramObject::LinkArtifacts link;
ProgramObject::SpirvArtifacts spirv;
};
using ProgramTranslationResultPtr = SharedPtr<const ProgramTranslationResult>;
SizeT ProgramTranslationResultBytes(const ProgramTranslationResult& result);
// Process-global, and safe to be: the FRONT-END environment fingerprint is in the key
// (see CompileEnv::frontendFingerprint), so a program built under one context's glslang
// limits can never be handed to a context with different ones - while two contexts on
// DIFFERENT GPUs that agree on those limits deliberately share entries.
//
// Global rather than per-context because the producer runs on a ShaderCompilePool worker
// and must not reach MG_State::pGLContext.
MG_Util::ShaderTranspiler::BoundedTranslationCache<ProgramTranslationResult>&
GetProgramTranslationCache();
void ClearProgramTranslationCache();
void LogProgramTranslationCacheStats();
} // namespace MobileGL::MG_State::GLState
@@ -8,13 +8,16 @@
#include "ShaderCompileTask.h" #include "ShaderCompileTask.h"
#include <MG_State/GLState/BufferState/BufferState.h>
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h> #include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h> #include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h> #include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
#include <MG_Util/ShaderTranspiler/Types.h> #include <MG_Util/ShaderTranspiler/Types.h>
#include <glslang/Include/PoolAlloc.h> #include <glslang/Include/PoolAlloc.h>
#include <algorithm>
#include <charconv> #include <charconv>
namespace { namespace {
@@ -137,8 +140,21 @@ namespace {
return std::nullopt; return std::nullopt;
} }
// What glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS) answers, recomputed rather than
// queried: the compile runs on a worker with no context, and the pname is not a plain backend
// parameter - the getter caps the backend's count by the state layer's fixed binding-point
// array (GL_Getter's GetIndexedBufferQueryPointCount). A shader must be judged against the
// number the application was told, not against either half of it.
static MobileGL::Int MaxShaderStorageBufferBindings(
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
const MobileGL::Int frontendPoints =
static_cast<MobileGL::Int>(MobileGL::MG_State::GLState::BufferBindingPointCount);
if (!env.HasBackend()) return frontendPoints;
return std::min<MobileGL::Int>(frontendPoints, std::max<MobileGL::Int>(env.params.MaxShaderStorageBufferBindings, 0));
}
// The half of a compile that depends on nothing but the source text, the stage and the // The half of a compile that depends on nothing but the source text, the stage and the
// environment snapshot: preprocessing, the two lexical rejections, and the two lexical // environment snapshot: preprocessing, the three lexical rejections, and the two lexical
// side-channel extractions. Split out so P0b layer 2 can memoize exactly this and // side-channel extractions. Split out so P0b layer 2 can memoize exactly this and
// nothing else - the glslang parse stays per-object because its TShader is consume-once. // nothing else - the glslang parse stays per-object because its TShader is consume-once.
// Deliberately free of any per-object state so the memo is sound. // Deliberately free of any per-object state so the memo is sound.
@@ -172,6 +188,20 @@ namespace {
return result; return result;
} }
if (const std::optional<String> bindingError = FindShaderStorageBindingViolation(
result.preprocessedSource, MaxShaderStorageBufferBindings(env))) {
result.outcome = ShaderPreprocessOutcome::ResourceBindingRejected;
result.infoLog = *bindingError;
return result;
}
if (const std::optional<String> counterOffsetError =
FindAtomicCounterOffsetViolation(result.preprocessedSource)) {
result.outcome = ShaderPreprocessOutcome::AtomicCounterOffsetRejected;
result.infoLog = *counterOffsetError;
return result;
}
// The parse this feeds runs in the link-compatible configuration (Vulkan-client // The parse this feeds runs in the link-compatible configuration (Vulkan-client
// env with relaxed rules): the TShader it produces is what glLinkProgram links and // env with relaxed rules): the TShader it produces is what glLinkProgram links and
// what the backends' SPIR-V is generated from - there is no second, GL-client // what the backends' SPIR-V is generated from - there is no second, GL-client
@@ -248,17 +278,80 @@ namespace MobileGL::MG_State::GLState {
return; return;
} }
ShaderAttrib attrib{.shaderType = MG_Util::ConvertShaderStageToGLEnum(stage), const GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(stage);
.sourceStr = shared.preprocessedSource, // Always 0 on both production parse paths; see the key inventory on
.flags = 0, // ShaderParseVerdictKeyInputs for why it is in the key regardless.
.env = &compileEnv}; constexpr Uint32 kShaderCompileFlags = 0;
auto result = ShaderCompiler::CompileShader(attrib); // ---- L1c of the shader translation memo: the PARSE VERDICT ----------------------
if (result) { // Everything below this probe - the glslang parse itself - is what a hit skips. What
// a hit does NOT produce is a TShader, and that is deliberate rather than a
// limitation: the TShader is consume-once, so it could never have been shared, and
// nothing on the COMPILE side of GL reads it. GL_COMPILE_STATUS, the info log,
// GL_SHADER_SOURCE, attach/detach and reuse across programs are all answered from
// what the verdict and the source-only half already carry.
//
// The parse is not skipped, it is DEFERRED: ClaimParsedShader re-parses on demand
// when a link finds no stored parse. A link that hits L1 never asks, so the parse
// never happens at all; a link that misses pays exactly one parse, where the CAS
// loser has always paid it. See TranslationCache.h's L1c section.
const TranslationCacheKey parseKey =
ShaderTranslationCacheEnabled()
? BuildShaderParseVerdictKey(ShaderParseVerdictKeyInputs{
.frontendFingerprint = compileEnv.frontendFingerprint,
.shaderType = glShaderType,
.preprocessedSource = StringView(shared.preprocessedSource),
.shaderCompileFlags = kShaderCompileFlags})
: TranslationCacheKey{};
const ShaderParseVerdictPtr verdict =
parseKey.Valid() ? GetShaderParseVerdictCache().Find(parseKey) : nullptr;
// The two branches produce exactly one thing between them - a verdict, plus a TShader
// only when this task actually parsed - and converge on one publish below. Keeping the
// publish common is what stops a hit and a miss from ever drifting on WHAT a compile
// makes observable.
Bool parsedOk = false;
String parseLog;
SharedPtr<glslang::TShader> parsedShader;
if (verdict) {
parsedOk = verdict->parsed;
parseLog = verdict->infoLog;
MGLOG_D("ShaderCompileTask: shader %u (stage %d) L1c hit - the glslang parse was skipped; "
"compileStatus = %d",
externalIndex, static_cast<Int>(stage), static_cast<Int>(parsedOk));
} else {
const ShaderAttrib attrib{.shaderType = glShaderType,
.sourceStr = shared.preprocessedSource,
.flags = kShaderCompileFlags,
.env = &compileEnv};
auto result = ShaderCompiler::CompileShader(attrib);
parsedOk = result.has_value();
if (parsedOk) {
parsedShader = result.value();
} else {
parseLog = result.error().log;
}
if (parseKey.Valid()) {
auto freshVerdict = MakeShared<ShaderParseVerdict>();
freshVerdict->parsed = parsedOk;
// Empty on success by construction, matching what the publish below does with
// the artifacts' own log; the diagnostic the application reads on failure.
freshVerdict->infoLog = parseLog;
const SizeT verdictBytes = ShaderParseVerdictBytes(*freshVerdict);
GetShaderParseVerdictCache().Insert(parseKey, ShaderParseVerdictPtr(Move(freshVerdict)),
verdictBytes);
}
}
if (parsedOk) {
artifacts.compileStatus = true; artifacts.compileStatus = true;
artifacts.shader = result.value(); // NULL ON AN L1c HIT, and that is a supported state rather than an oversight: see
// ShaderCompileArtifacts::shader and ClaimParsedShader.
artifacts.shader = Move(parsedShader);
// Copy, not move: `shared` may alias a cache entry that has to outlive us, and // Copy, not move: `shared` may alias a cache entry that has to outlive us, and
// `fresh` is about to be handed to the cache. // `fresh` is about to be handed to the cache. Populated on the hit path too - it
// is what ClaimParsedShader's deferred parse consumes.
artifacts.preprocessedSource = shared.preprocessedSource; artifacts.preprocessedSource = shared.preprocessedSource;
artifacts.explicitUniformLocations = shared.explicitUniformLocations; artifacts.explicitUniformLocations = shared.explicitUniformLocations;
artifacts.explicitOpaqueBindings = shared.explicitOpaqueBindings; artifacts.explicitOpaqueBindings = shared.explicitOpaqueBindings;
@@ -267,7 +360,7 @@ namespace MobileGL::MG_State::GLState {
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh)); cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
} }
} else { } else {
artifacts.infoLog = result.error().log; artifacts.infoLog = Move(parseLog);
// Deferred, not logged here, for two reasons. MGLOG from a pool thread interleaves // Deferred, not logged here, for two reasons. MGLOG from a pool thread interleaves
// mid-line with the GL thread's own output and lands out of order relative to the // mid-line with the GL thread's own output and lands out of order relative to the
// glCompileShader that caused it; diagnostics.logLines is replayed by the join, on // glCompileShader that caused it; diagnostics.logLines is replayed by the join, on
@@ -310,10 +403,11 @@ namespace MobileGL::MG_State::GLState {
} }
} }
// Either another link already consumed the stored parse (and mapIO mutated its // Three ways to be here: another link already consumed the stored parse (and mapIO
// intermediate), or there never was one. Re-parse the preprocessed source through the // mutated its intermediate); the compile hit L1c and never parsed at all; or there
// identical configuration; that costs one glslang parse, which is what GenerateBinary // simply never was one. All three want the same thing - parse the preprocessed source
// used to spend here on EVERY link rather than only on reuse. // through the identical configuration. That costs one glslang parse, which is what
// GenerateBinary used to spend here on EVERY link rather than only when needed.
// //
// The guard is not optional on this path: from stage 4 this runs on a pool worker, // The guard is not optional on this path: from stage 4 this runs on a pool worker,
// and TShader::parse would leave that worker's TLS allocator pointing at a pool the // and TShader::parse would leave that worker's TLS allocator pointing at a pool the
@@ -329,7 +423,12 @@ namespace MobileGL::MG_State::GLState {
.env = artifacts.env.get()}; .env = artifacts.env.get()};
auto result = ShaderCompiler::CompileShader(attrib); auto result = ShaderCompiler::CompileShader(attrib);
if (!result) { if (!result) {
// Should be unreachable: the same source parsed successfully at Compile(). // Should be unreachable. This exact (stage, preprocessed source, front-end env)
// parsed successfully once - either at this node's own Compile(), or at the
// Compile() whose verdict L1c handed this node - and every input the parse reads
// is covered by that tuple. ConsumeShaders turns a null into a failed link with a
// named internal error rather than a crash, which is the right shape for a
// "cannot happen" that would otherwise be a silent miscompile.
outReparseLog = result.error().log; outReparseLog = result.error().log;
return nullptr; return nullptr;
} }
@@ -41,6 +41,20 @@ namespace MobileGL::MG_State::GLState {
// re-parse in ClaimParsedShader() reproduces the original parse exactly, instead of // re-parse in ClaimParsedShader() reproduces the original parse exactly, instead of
// re-reading whatever the backend says now. // re-reading whatever the backend says now.
SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env; SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env;
// The parse, WHEN THIS COMPILE ACTUALLY PARSED - and null otherwise, including when
// compileStatus is true.
//
// That combination is not a half-finished compile; it is an L1c hit. The translation
// memo's compile half (TranslationCache.h) knows this exact (stage, preprocessed
// source, front-end env) parses cleanly, so the verdict is published without running
// glslang. What a hit cannot hand over is the TShader itself: mapIO mutates its
// aliased intermediate at link, so a parse feeds exactly ONE link and could never
// have been shared between compiles.
//
// Nothing on the compile side of GL reads this - GL_COMPILE_STATUS, the info log,
// GL_SHADER_SOURCE, attach/detach and reuse across programs are all answered from the
// fields below. The one reader is ClaimParsedShader, which treats null as "parse it
// now", which is the same path the consume-once CAS loser has always taken.
SharedPtr<glslang::TShader> shader; SharedPtr<glslang::TShader> shader;
// The source the parse actually consumed (after PreprocessShaderSource), kept for // The source the parse actually consumed (after PreprocessShaderSource), kept for
// ClaimParsedShader's re-parse so a later link never depends on the preprocessor // ClaimParsedShader's re-parse so a later link never depends on the preprocessor
@@ -53,8 +67,9 @@ namespace MobileGL::MG_State::GLState {
}; };
// The unit of asynchronous shader compilation: one glCompileShader's worth of pure CPU // The unit of asynchronous shader compilation: one glCompileShader's worth of pure CPU
// work - preprocess, the two lexical rejections, the two lexical extractions, and the // work - preprocess, the two lexical rejections, the two lexical extractions, and (unless
// glslang parse - with every input it needs owned by the node itself. // the translation memo's compile half already knows the answer) the glslang parse - with
// every input it needs owned by the node itself.
// //
// That ownership is the whole point. The node reads no GL-thread state (the source is a // That ownership is the whole point. The node reads no GL-thread state (the source is a
// SharedPtr<const String> snapshot, the device limits come from the CompileEnv snapshot, // SharedPtr<const String> snapshot, the device limits come from the CompileEnv snapshot,
@@ -87,22 +102,29 @@ namespace MobileGL::MG_State::GLState {
// ---- output: valid iff IsComplete(), immutable afterwards ---- // ---- output: valid iff IsComplete(), immutable afterwards ----
ShaderCompileArtifacts artifacts; ShaderCompileArtifacts artifacts;
// Hands out a link-consumable TShader, exactly once for the stored parse. // Hands out a link-consumable TShader, parsing one on demand when this node has none.
// //
// glslang's mapIO mutates the TShader's aliased intermediate, so the parse this node // TWO WAYS TO GET HERE WITHOUT A STORED PARSE, and they share one implementation:
// produced may feed exactly ONE link; every later link (a relink, or the same shader // * the CAS loser. glslang's mapIO mutates the TShader's aliased intermediate, so
// attached to a second program) needs a fresh parse. The claim is a CAS on this // the parse this node produced may feed exactly ONE link; every later link (a
// shared node rather than a flag on the ShaderObject because from stage 4 the two // relink, or the same shader attached to a second program) needs a fresh one. The
// callers can be two ProgramLinkTasks running on two workers: two programs sharing // claim is a CAS on this shared node rather than a flag on the ShaderObject
// one shader, linked back to back. Copying the parse out and tracking consumed-ness // because from stage 4 the two callers can be two ProgramLinkTasks on two
// per program would let both of them decide they were the first, run mapIO over the // workers: two programs sharing one shader, linked back to back. Copying the
// same intermediate twice, and ship silently corrupt SPIR-V. // parse out and tracking consumed-ness per program would let both of them decide
// they were the first, run mapIO over the same intermediate twice, and ship
// silently corrupt SPIR-V.
// * an L1c HIT. The compile published a verdict without parsing at all (see
// ShaderCompileArtifacts::shader), so this call IS the parse - deferred out of
// glCompileShader to the first link that genuinely needs an AST. A link served
// from L1 never gets here, which is the whole point: that program's front end
// never constructs a glslang object of any kind.
// //
// The CAS loser re-parses artifacts.preprocessedSource against THIS node's own // Either way the parse runs over artifacts.preprocessedSource against THIS node's own
// CompileEnv (not against whatever the backend reports now), through the identical // CompileEnv (not against whatever the backend reports now), through the identical
// CompileShader path - so winner and loser produce byte-identical SPIR-V. Callable // CompileShader path - so every claimant produces byte-identical SPIR-V. Callable
// only once IsComplete() and compileStatus are true. Returns null only if that // only once IsComplete() and compileStatus are true. Returns null only if that parse
// re-parse fails, and outReparseLog then carries its diagnostics. // fails, and outReparseLog then carries its diagnostics.
// //
// Const because the claim is the node's own synchronization, not a mutation of its // Const because the claim is the node's own synchronization, not a mutation of its
// published artifacts: a claim that is taken and then abandoned (its link was // published artifacts: a claim that is taken and then abandoned (its link was
@@ -26,6 +26,12 @@ namespace MobileGL::MG_State::GLState {
ComputeLocalSizeRejected, ComputeLocalSizeRejected,
// FindReservedIdentifierViolation rejected it. // FindReservedIdentifierViolation rejected it.
ReservedIdentifierRejected, ReservedIdentifierRejected,
// FindShaderStorageBindingViolation rejected it: a storage block declared a binding at or
// past GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS.
ResourceBindingRejected,
// FindAtomicCounterOffsetViolation rejected it: an atomic counter declared a
// layout(offset =) that is misaligned or reaches past GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE.
AtomicCounterOffsetRejected,
// The source-only half was clean but glslang rejected the preprocessed source. // The source-only half was clean but glslang rejected the preprocessed source.
// Memoizing this saves the parse itself on every later object with that source. // Memoizing this saves the parse itself on every later object with that source.
ParseFailed, ParseFailed,
@@ -843,6 +843,21 @@ namespace MobileGL {
stored = box; stored = box;
stateChanged = true; stateChanged = true;
} }
// "The application has written this rectangle" is a DIFFERENT predicate from "the
// value moved", and the backends need the first one: glScissor(0, 0, 0, 0) as the
// very first scissor call leaves every stored box byte-identical to its
// never-written default, and that call is precisely the one whose meaning a
// backend must stop guessing at (see ScissorBoxWrittenMask).
//
// The transition has to count as a state change for the version too. DirectGLES'
// SyncRenderState early-outs on an unchanged render-state version BEFORE it
// reaches the span memcmp that would otherwise notice the mask, so a version-less
// flag flip would sit in the parameter block and never be pushed. It is a
// once-per-index transition, so the steady state still costs nothing.
if (m_parameters.ScissorBoxWrittenMask != kAllViewportsMask) {
m_parameters.ScissorBoxWrittenMask = kAllViewportsMask;
stateChanged = true;
}
if (stateChanged) ++m_version; if (stateChanged) ++m_version;
} }
@@ -855,9 +870,15 @@ namespace MobileGL {
MOBILEGL_ASSERT(false, "Scissor box index out of range: %u", index); MOBILEGL_ASSERT(false, "Scissor box index out of range: %u", index);
return; return;
} }
if (m_parameters.ScissorBoxes[index] == box) return; // See SetScissorBox: a first write is state even when it does not move the value,
// so the unchanged-value early-out may only fire once this index is already
// marked written.
const Uint32 writtenBit = 1u << index;
const Bool alreadyWritten = (m_parameters.ScissorBoxWrittenMask & writtenBit) != 0;
if (alreadyWritten && m_parameters.ScissorBoxes[index] == box) return;
m_parameters.ScissorBoxes[index] = box; m_parameters.ScissorBoxes[index] = box;
m_parameters.ScissorBoxWrittenMask |= writtenBit;
++m_version; ++m_version;
} }
@@ -328,6 +328,18 @@ namespace MobileGL {
// turns it into a real glEnable/glDisable. // turns it into a real glEnable/glDisable.
Uint32 ScissorTestEnabledMask = 0; Uint32 ScissorTestEnabledMask = 0;
Array<IntVec4, MAX_VIEWPORTS> ScissorBoxes{}; // x, y, width, height Array<IntVec4, MAX_VIEWPORTS> ScissorBoxes{}; // x, y, width, height
// One bit per viewport, set the first time the application writes that index's scissor
// rectangle - glScissor broadcasts and sets all 16, glScissorIndexed/glScissorArrayv set
// the indices they name. It exists because the RECTANGLE cannot answer "has the
// application spoken?": ScissorBoxes starts all-zero (its spec initial value is the size
// of a window the frontend does not know yet, see the RenderState constructor), and
// glScissor(0, 0, 0, 0) is a legal GL state meaning "the scissor test rejects every
// fragment". A backend that reads an empty rectangle as the never-written sentinel
// therefore INVERTS that request into "accept every fragment"; DirectGLES did exactly
// that and KHR-GL43.viewport_array.scissor_zero_dimension caught it. Deliberately beside
// ScissorBoxes so it shares their tail span (after LogicOp) and DirectGLES' span memcmp
// picks a transition up like any other state.
Uint32 ScissorBoxWrittenMask = 0;
// glEnable(GL_CLIP_DISTANCE0 + i) for i in [0, 8), one bit each. A bitmask rather than // glEnable(GL_CLIP_DISTANCE0 + i) for i in [0, 8), one bit each. A bitmask rather than
// eight bools because every consumer wants the set, not an individual flag, and because // eight bools because every consumer wants the set, not an individual flag, and because
// the SYNC_CAPABILITY/SET_CAPABILITY macros key off a "<Name>Enabled" field name that // the SYNC_CAPABILITY/SET_CAPABILITY macros key off a "<Name>Enabled" field name that
@@ -48,6 +48,7 @@ namespace MobileGL {
m_dirtyRects.resize(requiredLevelCount); m_dirtyRects.resize(requiredLevelCount);
m_compressedData.resize(requiredLevelCount); m_compressedData.resize(requiredLevelCount);
m_compressedFormats.resize(requiredLevelCount, GL_NONE); m_compressedFormats.resize(requiredLevelCount, GL_NONE);
m_requestedCompressedFormats.resize(requiredLevelCount, GL_NONE);
} }
m_texelSizes[level] = input.texelSize; m_texelSizes[level] = input.texelSize;
@@ -79,6 +80,9 @@ namespace MobileGL {
m_compressedFormats[level] = GL_NONE; m_compressedFormats[level] = GL_NONE;
m_compressedData[level].clear(); m_compressedData[level].clear();
m_compressedData[level].shrink_to_fit(); m_compressedData[level].shrink_to_fit();
// Same story for the requested-format tag: a respecified level is whatever this
// call asked for, and the compressed entry points re-arm it right afterwards.
m_requestedCompressedFormats[level] = GL_NONE;
} }
void MipmapStorage::SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size) { void MipmapStorage::SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size) {
@@ -110,6 +114,16 @@ namespace MobileGL {
return m_compressedData[level].data(); return m_compressedData[level].data();
} }
void MipmapStorage::SetRequestedCompressedFormat(Uint level, GLenum internalFormat) {
if (level >= m_requestedCompressedFormats.size()) return;
m_requestedCompressedFormats[level] = internalFormat;
}
GLenum MipmapStorage::GetRequestedCompressedFormat(Uint level) const {
if (level >= m_requestedCompressedFormats.size()) return GL_NONE;
return m_requestedCompressedFormats[level];
}
void MipmapStorage::TruncateToLevelCount(SizeT levelCount) { void MipmapStorage::TruncateToLevelCount(SizeT levelCount) {
if (levelCount >= m_data.size()) return; if (levelCount >= m_data.size()) return;
@@ -120,6 +134,7 @@ namespace MobileGL {
m_dirtyRects.resize(levelCount); m_dirtyRects.resize(levelCount);
m_compressedData.resize(levelCount); m_compressedData.resize(levelCount);
m_compressedFormats.resize(levelCount); m_compressedFormats.resize(levelCount);
m_requestedCompressedFormats.resize(levelCount);
} }
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) { void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
@@ -96,6 +96,18 @@ namespace MobileGL {
SizeT GetCompressedByteSize(Uint level) const; SizeT GetCompressedByteSize(Uint level) const;
const void* MapCompressedData(Uint level) const; const void* MapCompressedData(Uint level) const;
// The compressed internalformat the application ASKED for, which is not the same
// question as the one above: the six generic GL_COMPRESSED_* enums let the
// implementation choose, MobileGL chooses uncompressed storage, and the level is
// deliberately left untagged so GL_TEXTURE_COMPRESSED keeps answering false and
// glGetCompressedTexImage is not handed a blob nothing ever compressed. The entry
// points that must refuse a compressed image outright (glClearTexImage /
// glClearTexSubImage, GL 4.6 core 8.19) still need to know, so the request is
// recorded separately. Set right after AllocateLevel, which clears it.
void SetRequestedCompressedFormat(Uint level, GLenum internalFormat);
// GL_NONE when the level was not requested with a compressed internalformat.
GLenum GetRequestedCompressedFormat(Uint level) const;
protected: protected:
// Insert one clamped, non-empty write box, keeping the list disjoint // Insert one clamped, non-empty write box, keeping the list disjoint
// and bounded (see kMaxDirtyRects). // and bounded (see kMaxDirtyRects).
@@ -115,6 +127,7 @@ namespace MobileGL {
Vector<Vector<MipmapDirtyRegion>> m_dirtyRects; Vector<Vector<MipmapDirtyRegion>> m_dirtyRects;
Vector<Vector<Uint8>> m_compressedData; Vector<Vector<Uint8>> m_compressedData;
Vector<GLenum> m_compressedFormats; Vector<GLenum> m_compressedFormats;
Vector<GLenum> m_requestedCompressedFormats;
}; };
} // namespace GLState } // namespace GLState
} // namespace MG_State } // namespace MG_State
@@ -111,6 +111,16 @@ namespace MobileGL {
return m_storage[targetIndex].MapCompressedData(level); return m_storage[targetIndex].MapCompressedData(level);
} }
void SetRequestedCompressedFormat(Uint targetIndex, Uint level, GLenum internalFormat) {
MOBILEGL_ASSERT(targetIndex < TargetCount, "SetRequestedCompressedFormat: target invalid");
m_storage[targetIndex].SetRequestedCompressedFormat(level, internalFormat);
}
GLenum GetRequestedCompressedFormat(Uint targetIndex, Uint level) const {
MOBILEGL_ASSERT(targetIndex < TargetCount, "GetRequestedCompressedFormat: target invalid");
return m_storage[targetIndex].GetRequestedCompressedFormat(level);
}
protected: protected:
Array<MipmapStorage, TargetCount> m_storage; Array<MipmapStorage, TargetCount> m_storage;
}; };
@@ -250,6 +250,10 @@ namespace MobileGL {
return m_contentVersion; return m_contentVersion;
} }
Uint64 TextureObjectBase::GetShapeVersion() const {
return m_shapeVersion;
}
Bool TextureObjectBase::IsMipmapCompleteForFilterCached(Bool mipmapped) const { Bool TextureObjectBase::IsMipmapCompleteForFilterCached(Bool mipmapped) const {
const int slot = mipmapped ? 1 : 0; const int slot = mipmapped ? 1 : 0;
if (m_completeMemoShapeVersion[slot] == m_shapeVersion) { if (m_completeMemoShapeVersion[slot] == m_shapeVersion) {
@@ -373,6 +377,18 @@ namespace MobileGL {
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
} }
void TextureObjectWithOneMipmap::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel, GLenum internalFormat) {
m_textureStorage.SetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
internalFormat);
}
GLenum TextureObjectWithOneMipmap::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget),
mipmapLevel);
}
IntVec3 TextureObjectWithOneMipmap::GetBaseSize() const { IntVec3 TextureObjectWithOneMipmap::GetBaseSize() const {
if (m_textureStorage.GetLevelCount() == 0) { if (m_textureStorage.GetLevelCount() == 0) {
return {0, 0, 0}; return {0, 0, 0};
@@ -55,6 +55,12 @@ namespace MobileGL::MG_State::GLState {
// Backends compare it against a per-resource snapshot to skip re-syncing unchanged // Backends compare it against a per-resource snapshot to skip re-syncing unchanged
// textures across draws (e.g. the block atlas bound across a whole terrain batch). // textures across draws (e.g. the block atlas bound across a whole terrain batch).
virtual Uint64 GetContentVersion() const = 0; virtual Uint64 GetContentVersion() const = 0;
// Monotonic counter bumped on every SHAPE mutation - level sizes, the stored level
// set, the internal format, the level range (see BumpShapeVersion). Disjoint from the
// content version on purpose: glTexImage2D(..., nullptr) re-specifies a level's size
// without dirtying a single texel, so a backend that keys its "nothing changed since
// the last sync" skip on content alone keeps a resource of the OLD size alive.
virtual Uint64 GetShapeVersion() const = 0;
// Answers IsMipmapCompleteForFilter() from a memo. Sampling completeness is a // Answers IsMipmapCompleteForFilter() from a memo. Sampling completeness is a
// property of the texture's SHAPE - level sizes, level count, level range, // property of the texture's SHAPE - level sizes, level count, level range,
// internal format - and never of its texel content, but every draw asks about // internal format - and never of its texel content, but every draw asks about
@@ -106,6 +112,7 @@ namespace MobileGL::MG_State::GLState {
void SetImmutableLevels(Uint levels) override; void SetImmutableLevels(Uint levels) override;
Uint16 GetTextureParamsVersion() const override; Uint16 GetTextureParamsVersion() const override;
Uint64 GetContentVersion() const override; Uint64 GetContentVersion() const override;
Uint64 GetShapeVersion() const override;
Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const override; Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const override;
// Bumps the content version without touching per-level storage-dirty flags. Used when the // Bumps the content version without touching per-level storage-dirty flags. Used when the
// set of defined mip levels grows via GPU-side mip generation (glGenerateMipmap): the level // set of defined mip levels grows via GPU-side mip generation (glGenerateMipmap): the level
@@ -220,6 +227,15 @@ namespace MobileGL::MG_State::GLState {
virtual GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; virtual GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
virtual SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; virtual SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
virtual const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; virtual const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
// The compressed internalformat the level was REQUESTED with, recorded even when MobileGL
// answered it with uncompressed storage (the six generic GL_COMPRESSED_* enums) - see
// MipmapStorage. Only the entry points GL forbids on a compressed image read it.
virtual void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat) = 0;
// GL_NONE when the level was not requested with a compressed internalformat.
virtual GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const = 0;
}; };
// Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the // Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the
@@ -286,6 +302,9 @@ namespace MobileGL::MG_State::GLState {
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat) override;
GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
IntVec3 GetBaseSize() const override; IntVec3 GetBaseSize() const override;
Bool IsComplete() const override; Bool IsComplete() const override;
@@ -96,6 +96,18 @@ namespace MobileGL {
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
} }
void TextureObject2DCube::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel, GLenum internalFormat) {
m_textureStorage.SetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
internalFormat);
}
GLenum TextureObject2DCube::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget),
mipmapLevel);
}
Uint TextureObject2DCube::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const { Uint TextureObject2DCube::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const {
MOBILEGL_ASSERT(TextureUploadTarget::CubeMapPositiveX <= target && MOBILEGL_ASSERT(TextureUploadTarget::CubeMapPositiveX <= target &&
target <= TextureUploadTarget::CubeMapNegativeZ, target <= TextureUploadTarget::CubeMapNegativeZ,
@@ -39,6 +39,10 @@ namespace MobileGL {
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const override; Uint mipmapLevel) const override;
void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat) override;
GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const override;
IntVec3 GetBaseSize() const override; IntVec3 GetBaseSize() const override;
Bool IsComplete() const override; Bool IsComplete() const override;
@@ -0,0 +1,167 @@
// MobileGL - MobileGL/MG_Test/Backend/DirectGLES/BaseInstanceInjectionTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// The gate on the gl_BaseInstance indirect lowering in
// MG_Backend/DirectGLES/Managers.cpp. That lowering declares a std430 storage block in the
// VERTEX stage, and a vertex-stage storage block is optional in both APIs: the minimum for
// GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS is 0 (GL 4.6 table 23.64, ES 3.2 table 21.44), and ARM's
// GLES driver takes that allowance - a Mali-G925-Immortalis reports 0 for it and for all three
// other graphics stages.
//
// Emitting the block on such a driver does not make it work. The driver refuses the program at
// link time ("The number of vertex shader storage blocks (1) is greater than the maximum number
// allowed (0)"), and because MobileGL's frontend GL_LINK_STATUS is glslang's rather than the
// driver's, the application is told the program linked and then every draw with it renders
// nothing. Dropping the indirect half instead keeps ordinary draws working and costs only the
// per-command baseInstance of an indirect draw.
//
// No GL context and no driver: the lowering is a pure String -> String pass over one capability.
#include <gtest/gtest.h>
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h>
using MobileGL::Bool;
using MobileGL::String;
using MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
using MobileGL::MG_Backend::DirectGLES::PromoteDrawParameterGlobalsToUniforms;
using MobileGL::MG_Backend::DirectGLES::VertexStageStorageBlockUsable;
namespace {
// The capability block is a process-global the backend fills in at init; restore whatever
// was there so ordering between this suite and any other that touches it cannot matter.
struct ScopedGLESCapabilitiesOverride {
ScopedGLESCapabilitiesOverride(): saved(g_GLESCapabilities) {}
~ScopedGLESCapabilitiesOverride() { g_GLESCapabilities = saved; }
ScopedGLESCapabilitiesOverride(const ScopedGLESCapabilitiesOverride&) = delete;
ScopedGLESCapabilitiesOverride& operator=(const ScopedGLESCapabilitiesOverride&) = delete;
MobileGL::MG_External::GLESCapabilities saved;
};
Bool Contains(const String& haystack, const String& needle) {
return haystack.find(needle) != String::npos;
}
// What SPIRV-Cross hands the backend after LowerDrawParametersPass has demoted
// gl_BaseInstance to a Private global.
constexpr const char* kLoweredBaseInstanceVertexShader = R"(#version 310 es
highp int mg_BaseInstanceLowered;
void main() {
int instance = gl_InstanceID + mg_BaseInstanceLowered;
gl_Position = vec4(float(instance));
}
)";
} // namespace
// One block is all the indirect view needs, so the predicate is a >= 1 test.
TEST(VertexStageStorageBlockUsableTest, RequiresAtLeastOneBlock) {
EXPECT_FALSE(VertexStageStorageBlockUsable(0));
EXPECT_TRUE(VertexStageStorageBlockUsable(1));
EXPECT_TRUE(VertexStageStorageBlockUsable(16));
}
// A driver that leaves the out-param untouched tells us nothing, and guessing "yes" is exactly
// what produces the unlinkable program. Unusable, not clamped up to one.
TEST(VertexStageStorageBlockUsableTest, ANegativeCountIsUnusableRatherThanClamped) {
EXPECT_FALSE(VertexStageStorageBlockUsable(-1));
EXPECT_FALSE(VertexStageStorageBlockUsable(-2147483647 - 1));
}
TEST(BaseInstanceInjectionGate, DriverWithVertexStorageBlocksGetsTheIndirectView) {
const ScopedGLESCapabilitiesOverride capsGuard;
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 1;
const String rewritten =
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
EXPECT_TRUE(Contains(rewritten, "layout(std430, binding = 12) readonly buffer mg_IndirectParams"));
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseInstanceWordIndex;"));
EXPECT_TRUE(Contains(rewritten, "#define mg_BaseInstanceLowered ((mg_BaseInstanceWordIndex > 0) ? "
"int(mg_indirectWords[uint(mg_BaseInstanceWordIndex - 1)]) : mg_BaseInstance)"))
<< rewritten;
}
// The bug this gate exists for. The block must not appear at all - not at a different binding,
// not behind a preprocessor guard: a declaration the driver counts is a declaration that makes
// the whole program unlinkable, and the frontend never surfaces that failure.
TEST(BaseInstanceInjectionGate, DriverWithoutVertexStorageBlocksDeclaresNoBlockAtAll) {
const ScopedGLESCapabilitiesOverride capsGuard;
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
const String rewritten =
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
EXPECT_FALSE(Contains(rewritten, "mg_IndirectParams")) << rewritten;
EXPECT_FALSE(Contains(rewritten, "buffer"));
EXPECT_FALSE(Contains(rewritten, "mg_indirectWords"));
// Nothing reads the word index any more, so nothing may declare it either - its presence is
// what BackendProgramObjectImpl uses to decide whether to bind an indirect params buffer.
EXPECT_FALSE(Contains(rewritten, "mg_BaseInstanceWordIndex"));
}
// Degraded, but still correct for every non-indirect draw: the plain mg_BaseInstance uniform is
// what the non-indirect draw entry points already write.
TEST(BaseInstanceInjectionGate, WithoutTheBlockBaseInstanceFallsBackToThePlainUniform) {
const ScopedGLESCapabilitiesOverride capsGuard;
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
const String rewritten =
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseInstance;")) << rewritten;
EXPECT_TRUE(Contains(rewritten, "#define mg_BaseInstanceLowered (mg_BaseInstance)")) << rewritten;
// The global declaration must be gone; leaving it would shadow the define.
EXPECT_FALSE(Contains(rewritten, "highp int mg_BaseInstanceLowered;\n"));
}
// On a driver that both leaks baseInstance into gl_InstanceID and has no vertex storage block,
// the rebase has nothing to subtract. Subtracting the uniform instead would remove the base
// twice from every non-indirect draw, which is worse than not rebasing at all.
TEST(BaseInstanceInjectionGate, WithoutTheBlockInstanceIdRebaseCollapsesToIdentity) {
const ScopedGLESCapabilitiesOverride capsGuard;
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = true;
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
const String rewritten =
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
EXPECT_TRUE(Contains(rewritten, "#define mg_ZeroBasedInstanceID gl_InstanceID")) << rewritten;
EXPECT_FALSE(Contains(rewritten, "gl_InstanceID - ("));
EXPECT_FALSE(Contains(rewritten, "mg_indirectWords"));
}
// The gate is scoped to the block, not to the whole pass: mg_DrawID and mg_BaseVertex are plain
// uniforms with no storage block behind them and must still be promoted on such a driver.
TEST(BaseInstanceInjectionGate, DrawIdAndBaseVertexArePromotedRegardless) {
const ScopedGLESCapabilitiesOverride capsGuard;
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
const String source = R"(#version 310 es
highp int mg_DrawID;
highp int mg_BaseVertex;
void main() {
gl_Position = vec4(float(mg_DrawID + mg_BaseVertex));
}
)";
const String rewritten = PromoteDrawParameterGlobalsToUniforms(source, GL_VERTEX_SHADER);
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_DrawID;")) << rewritten;
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseVertex;")) << rewritten;
}
@@ -16,5 +16,22 @@ target_link_libraries(
${LINK_LIBRARIES} ${LINK_LIBRARIES}
) )
add_executable(
BaseInstanceInjectionTest
BaseInstanceInjectionTest.cpp
)
target_include_directories(BaseInstanceInjectionTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
BaseInstanceInjectionTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
include(GoogleTest) include(GoogleTest)
gtest_discover_tests(EsslShaderPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) gtest_discover_tests(EsslShaderPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(BaseInstanceInjectionTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -21,6 +21,7 @@ using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ForceFlatIntegerVaryings;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestViewportArrayExtension;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::SplitReadWriteImageUniforms; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::SplitReadWriteImageUniforms;
namespace { namespace {
@@ -58,9 +59,11 @@ void main()
const String out = SplitReadWriteImageUniforms(source); const String out = SplitReadWriteImageUniforms(source);
// Both halves: same binding, same format, same type - which is what makes two image // Both halves: same binding, same format, same type - which is what makes two image
// variables on one image unit legal. // variables on one image unit legal - and both `coherent`, which is what makes the store
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform readonly highp image2D goku;")); // through one of them visible to the load through the other.
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D " + WriteAlias("goku") + ";")); EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform coherent readonly highp image2D goku;"));
EXPECT_TRUE(Contains(
out, "layout(binding = 2, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";"));
// The load keeps the original name, the store moves to the writeonly half. // The load keeps the original name, the store moves to the writeonly half.
EXPECT_TRUE(Contains(out, "imageLoad(goku,")); EXPECT_TRUE(Contains(out, "imageLoad(goku,"));
@@ -151,9 +154,9 @@ void main()
} }
)"; )";
const String out = SplitReadWriteImageUniforms(source); const String out = SplitReadWriteImageUniforms(source);
EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform readonly highp image2D gohan[3];")); EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform coherent readonly highp image2D gohan[3];"));
EXPECT_TRUE(Contains(out, EXPECT_TRUE(Contains(
"layout(binding = 6, rgba8) uniform writeonly highp image2D " + WriteAlias("gohan") + "[3];")); out, "layout(binding = 6, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("gohan") + "[3];"));
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("gohan") + "[1],")); EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("gohan") + "[1],"));
EXPECT_TRUE(Contains(out, "imageLoad(gohan[2],")); EXPECT_TRUE(Contains(out, "imageLoad(gohan[2],"));
} }
@@ -173,9 +176,11 @@ void main()
)"; )";
const String out = SplitReadWriteImageUniforms(source); const String out = SplitReadWriteImageUniforms(source);
// goku is read+write -> split; goku_hd is write-only -> qualified in place, not split. // goku is read+write -> split (and coherent with it); goku_hd is write-only -> qualified in
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform readonly highp image2D goku;")); // place, not split, and left non-coherent because nothing aliases it.
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform writeonly highp image2D " + WriteAlias("goku") + ";")); EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform coherent readonly highp image2D goku;"));
EXPECT_TRUE(Contains(
out, "layout(binding = 1, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";"));
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D goku_hd;")); EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D goku_hd;"));
EXPECT_TRUE(Contains(out, "imageStore(goku_hd,")); EXPECT_TRUE(Contains(out, "imageStore(goku_hd,"));
EXPECT_FALSE(Contains(out, WriteAlias("goku") + "_hd")); EXPECT_FALSE(Contains(out, WriteAlias("goku") + "_hd"));
@@ -196,6 +201,131 @@ void main()
EXPECT_TRUE(Contains(out, "uniform readonly coherent restrict highp image2D goku;")); EXPECT_TRUE(Contains(out, "uniform readonly coherent restrict highp image2D goku;"));
EXPECT_TRUE( EXPECT_TRUE(
Contains(out, "uniform writeonly coherent restrict highp image2D " + WriteAlias("goku") + ";")); Contains(out, "uniform writeonly coherent restrict highp image2D " + WriteAlias("goku") + ";"));
// ...and the coherent the split adds is not a SECOND one: a repeated memory qualifier is a
// compile error in ESSL, so the source's own has to be recognized.
EXPECT_EQ(CountOf(out, "coherent"), 2u);
}
// The visibility half of the split, and the reason it is not cosmetic: GLSL orders a
// same-variable read-after-write within one invocation by construction, but once the store goes
// through `mg_imageWrite_goku` and the load through `goku` the two are DIFFERENT variables, and
// the ordering only holds if both are coherent. Desktop sources almost never say so - they had
// no reason to - which is how KHR-GL4x.shader_image_load_store.advanced-memory-order's
// store/load/compare loop started reading back the value it had not stored yet.
TEST(SplitReadWriteImageUniformsTest, SplitPairIsMadeCoherentEvenWhenTheSourceIsNot) {
const String source = R"(#version 320 es
layout(binding = 2, rgba8) uniform highp image2D goku;
layout(binding = 3, rgba8) uniform highp image2D storeOnly;
layout(location = 0) out highp vec4 mg_FragColor;
void main()
{
imageStore(goku, ivec2(0), vec4(1.0));
mg_FragColor = imageLoad(goku, ivec2(0));
imageStore(storeOnly, ivec2(0), vec4(2.0));
}
)";
const String out = SplitReadWriteImageUniforms(source);
EXPECT_TRUE(Contains(out, "uniform coherent readonly highp image2D goku;")) << out;
EXPECT_TRUE(Contains(out, "uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";")) << out;
// Exactly the two halves of the pair, and nothing else: the store-only image is repaired in
// place, has no alias to stay visible to, and must not pay for uncached access.
EXPECT_EQ(CountOf(out, "coherent"), 2u);
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D storeOnly;")) << out;
}
// The ORDERING half of the split, which `coherent` alone does not buy. Coherent makes the store
// through one variable VISIBLE to a load through the other; it says nothing about the order of
// the two within a single invocation, and the ES compiler - seeing a write to one variable and a
// read of another it has no reason to believe alias - is free to serve the read from before the
// write. That is what advanced-memory-order measured on Adreno with the coherent pair already in
// place. memoryBarrierImage() is the primitive that orders them.
TEST(SplitReadWriteImageUniformsTest, EverySplitStoreIsFollowedByAnImageMemoryBarrier) {
const String source = R"(#version 320 es
layout(binding = 2, rgba8) uniform highp image2D goku;
layout(location = 0) out highp vec4 mg_FragColor;
void main()
{
imageStore(goku, ivec2(0), vec4(1.0));
highp vec4 first = imageLoad(goku, ivec2(0));
imageStore(goku, ivec2(0), vec4(2.0));
mg_FragColor = first + imageLoad(goku, ivec2(0));
}
)";
const String out = SplitReadWriteImageUniforms(source);
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("goku") + ", ivec2(0), vec4(1.0)); memoryBarrierImage();"))
<< out;
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("goku") + ", ivec2(0), vec4(2.0)); memoryBarrierImage();"))
<< out;
// One per store, not one per shader and not one per load.
EXPECT_EQ(CountOf(out, "memoryBarrierImage();"), 2u) << out;
}
// The barrier belongs to the SPLIT alone. A store-only image was repaired in place, nothing
// aliases it, and paying for a barrier there would slow down every shader that merely writes an
// image - which is most of them.
TEST(SplitReadWriteImageUniformsTest, ARepairedButUnsplitStoreGetsNoBarrier) {
const String source = R"(#version 320 es
layout(binding = 3, rgba8) uniform highp image2D storeOnly;
void main()
{
imageStore(storeOnly, ivec2(0), vec4(1.0));
}
)";
const String out = SplitReadWriteImageUniforms(source);
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D storeOnly;")) << out;
EXPECT_FALSE(Contains(out, "memoryBarrierImage")) << out;
}
// The store site is found by matching the call's own parentheses, not by looking for the next
// ')', so a nested call in the value argument does not truncate the statement and the barrier
// still lands after the whole thing.
TEST(SplitReadWriteImageUniformsTest, TheBarrierLandsAfterAStoreWithNestedParentheses) {
const String source = R"(#version 320 es
layout(binding = 6, rgba8) uniform highp image2D gohan[3];
void main()
{
imageStore(gohan[1], ivec2(0), max(imageLoad(gohan[2], ivec2(0)), vec4(0.5)));
}
)";
const String out = SplitReadWriteImageUniforms(source);
EXPECT_TRUE(Contains(out, "max(imageLoad(gohan[2], ivec2(0)), vec4(0.5))); memoryBarrierImage();")) << out;
EXPECT_EQ(CountOf(out, "memoryBarrierImage();"), 1u) << out;
}
// The split is the one thing that makes a stage declare MORE image uniforms than the application
// did, and MobileGL keeps advertising GL_MAX_*_IMAGE_UNIFORMS unadjusted (lowering it would fail
// basic-api and NotSupported-out every case that only uses readonly/writeonly images). So the
// count has to be reportable, or a link failure caused by the doubling looks like a driver
// mystery - which is what KHR-GL4x.shader_image_load_store.multiple-uniforms will hit the moment
// the format work stops masking it.
TEST(SplitReadWriteImageUniformsTest, TheSplitCountIsReportedToTheCaller) {
const String twoSplits = R"(#version 320 es
layout(binding = 0, rgba8) uniform highp image2D goku;
layout(binding = 1, rgba16f) uniform highp image2D gohan;
layout(binding = 2, rgba8) uniform highp image2D storeOnly;
void main()
{
imageStore(goku, ivec2(0), imageLoad(goku, ivec2(0)));
imageStore(gohan, ivec2(0), imageLoad(gohan, ivec2(0)));
imageStore(storeOnly, ivec2(0), vec4(0.0));
}
)";
Uint splitCount = 99u;
SplitReadWriteImageUniforms(twoSplits, &splitCount);
EXPECT_EQ(splitCount, 2u) << "only the read+write pair counts; the store-only repair adds no uniform";
// Every early return has to write the count too, or a caller reads whatever was there before.
const String noImages = R"(#version 320 es
layout(location = 0) out highp vec4 mg_FragColor;
void main()
{
mg_FragColor = vec4(1.0);
}
)";
splitCount = 99u;
SplitReadWriteImageUniforms(noImages, &splitCount);
EXPECT_EQ(splitCount, 0u);
} }
// imageSize reads no texels and writes none, so it decides nothing; readonly is what keeps // imageSize reads no texels and writes none, so it decides nothing; readonly is what keeps
@@ -550,3 +680,55 @@ void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
EXPECT_EQ(out, source); EXPECT_EQ(out, source);
EXPECT_EQ(CountOf(out, "GL_NV_image_formats"), 1u) << out; EXPECT_EQ(CountOf(out, "GL_NV_image_formats"), 1u) << out;
} }
// --- GL_OES_viewport_array directive -------------------------------------------------------------
// SPIRV-Cross prints gl_ViewportIndex bare and requests nothing for it, and ESSL has no core
// spelling at any version - so without this directive the stage fails to compile, the program is
// marked unusable and every draw made with it silently renders nothing.
TEST(RequestViewportArrayExtensionTest, TheDirectiveGoesRightAfterTheVersionLine) {
const String source = R"(#version 320 es
layout(points) in;
layout(points, max_vertices = 1) out;
void main() { gl_ViewportIndex = gl_InvocationID; EmitVertex(); }
)";
const String out = RequestViewportArrayExtension(source, true);
EXPECT_TRUE(Contains(out, "#version 320 es\n#extension GL_OES_viewport_array : require\n")) << out;
}
// Never speculatively: ARM's compiler hard-errors on an `#extension` naming a string the driver
// does not advertise, so the caller's "not needed" answer has to be honoured exactly. A driver
// without the extension gets the LowerViewportIndexPass fallback instead.
TEST(RequestViewportArrayExtensionTest, NotNeededMeansNotEmitted) {
const String source = R"(#version 320 es
layout(points) in;
layout(points, max_vertices = 1) out;
void main() { gl_ViewportIndex = gl_InvocationID; EmitVertex(); }
)";
EXPECT_EQ(RequestViewportArrayExtension(source, false), source);
}
TEST(RequestViewportArrayExtensionTest, AnAlreadyPresentDirectiveIsNotDuplicated) {
const String source = R"(#version 320 es
#extension GL_OES_viewport_array : require
layout(points) in;
layout(points, max_vertices = 1) out;
void main() { gl_ViewportIndex = gl_InvocationID; EmitVertex(); }
)";
const String out = RequestViewportArrayExtension(source, true);
EXPECT_EQ(out, source);
EXPECT_EQ(CountOf(out, "GL_OES_viewport_array"), 1u) << out;
}
// The two image directives and this one share the insertion point, so a shader that needs both
// must end up with both - and with #version still first.
TEST(RequestViewportArrayExtensionTest, CoexistsWithTheImageFormatDirective) {
const String source = R"(#version 320 es
layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { gl_ViewportIndex = 1; imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
const String out = RequestViewportArrayExtension(RequestExtendedImageFormats(source, true), true);
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
EXPECT_TRUE(Contains(out, "#extension GL_NV_image_formats : require\n")) << out;
EXPECT_TRUE(Contains(out, "#extension GL_OES_viewport_array : require\n")) << out;
}
@@ -34,6 +34,42 @@ namespace {
GLint maxFragmentImageUniforms = 4; GLint maxFragmentImageUniforms = 4;
GLint maxComputeImageUniforms = 5; GLint maxComputeImageUniforms = 5;
bool maxGeometryImageUniformsQueried = false; bool maxGeometryImageUniformsQueried = false;
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. The vertex and fragment pnames are ES 3.1,
// but the tessellation and geometry ones only exist from ES 3.2 on, so asking for them
// on an older context raises GL_INVALID_ENUM - the same shape as the buffer-texture and
// anisotropy probes. The "queried" flags are what pin that gating; the "raises error"
// knob is what pins the drain.
GLint maxTessControlSsboBlocks = 6;
GLint maxTessEvaluationSsboBlocks = 7;
GLint maxGeometrySsboBlocks = 8;
GLint maxFragmentSsboBlocks = 9;
bool tessAndGeometrySsboBlocksQueried = false;
bool perStageSsboBlockQueryRaisesError = false;
// GL_MAX_CLIP_DISTANCES. Not ES core in any version - it exists only as
// GL_MAX_CLIP_DISTANCES_EXT under GL_EXT_clip_cull_distance - so asking a driver without
// the extension raises GL_INVALID_ENUM and leaves the out-param untouched. The "queried"
// flag is what pins the gating; the "raises error" knob is what pins the drain.
GLint maxClipDistances = 8;
bool maxClipDistancesQueried = false;
bool clipDistanceQueryRaisesError = false;
// GL_MAX_VIEWPORTS / GL_VIEWPORT_SUBPIXEL_BITS / GL_VIEWPORT_BOUNDS_RANGE are
// GL_OES_viewport_array state and, like the clip-distance pname, exist nowhere in ES core.
GLint maxViewports = 32;
GLint viewportSubpixelBits = 8;
bool viewportArrayLimitsQueried = false;
// GL_LAYER_PROVOKING_VERTEX is ES 3.2 core; GL_VIEWPORT_INDEX_PROVOKING_VERTEX comes with
// GL_OES_viewport_array. Both must go unasked where they do not exist, and a driver answer
// outside the four legal conventions must not be forwarded as one.
GLint layerProvokingVertex = GL_FIRST_VERTEX_CONVENTION;
GLint viewportIndexProvokingVertex = GL_LAST_VERTEX_CONVENTION;
bool layerProvokingVertexQueried = false;
// A driver rejecting one of the UNCONDITIONAL probes. GL_SMOOTH_LINE_WIDTH_RANGE is the
// realistic one - it is desktop-only state that every GLES driver refuses - and it stands
// in for the whole run: whatever it leaves behind must not reach the application.
bool smoothLineWidthQueryRaisesError = false;
// What the driver answers for the four multisample ceilings. Zero is the value that has
// to be floored away: the frontend would otherwise advertise a sample count it rejects.
GLint multisampleCeiling = 4;
GLfloat minFragmentInterpolationOffset = -0.75f; GLfloat minFragmentInterpolationOffset = -0.75f;
GLfloat maxFragmentInterpolationOffset = 0.625f; GLfloat maxFragmentInterpolationOffset = 0.625f;
GLint fragmentInterpolationOffsetBits = 6; GLint fragmentInterpolationOffsetBits = 6;
@@ -111,7 +147,30 @@ namespace {
funcs.glGetIntegerv = [](GLenum pname, GLint* data) { funcs.glGetIntegerv = [](GLenum pname, GLint* data) {
switch (pname) { switch (pname) {
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
*data = g_fake.maxVertexSsboBlocks; if (g_fake.perStageSsboBlockQueryRaisesError) {
g_fake.pendingError = GL_INVALID_ENUM;
} else {
*data = g_fake.maxVertexSsboBlocks;
}
break;
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
if (g_fake.perStageSsboBlockQueryRaisesError) {
g_fake.pendingError = GL_INVALID_ENUM;
} else {
*data = g_fake.maxFragmentSsboBlocks;
}
break;
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
g_fake.tessAndGeometrySsboBlocksQueried = true;
*data = g_fake.maxTessControlSsboBlocks;
break;
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
g_fake.tessAndGeometrySsboBlocksQueried = true;
*data = g_fake.maxTessEvaluationSsboBlocks;
break;
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
g_fake.tessAndGeometrySsboBlocksQueried = true;
*data = g_fake.maxGeometrySsboBlocks;
break; break;
case GL_MAX_VERTEX_IMAGE_UNIFORMS: case GL_MAX_VERTEX_IMAGE_UNIFORMS:
*data = g_fake.maxVertexImageUniforms; *data = g_fake.maxVertexImageUniforms;
@@ -126,6 +185,37 @@ namespace {
case GL_MAX_COMPUTE_IMAGE_UNIFORMS: case GL_MAX_COMPUTE_IMAGE_UNIFORMS:
*data = g_fake.maxComputeImageUniforms; *data = g_fake.maxComputeImageUniforms;
break; break;
case GL_MAX_CLIP_DISTANCES:
g_fake.maxClipDistancesQueried = true;
if (g_fake.clipDistanceQueryRaisesError) {
g_fake.pendingError = GL_INVALID_ENUM;
} else {
*data = g_fake.maxClipDistances;
}
break;
case GL_MAX_VIEWPORTS:
g_fake.viewportArrayLimitsQueried = true;
*data = g_fake.maxViewports;
break;
case GL_VIEWPORT_SUBPIXEL_BITS:
g_fake.viewportArrayLimitsQueried = true;
*data = g_fake.viewportSubpixelBits;
break;
case GL_VIEWPORT_INDEX_PROVOKING_VERTEX:
g_fake.viewportArrayLimitsQueried = true;
*data = g_fake.viewportIndexProvokingVertex;
break;
case GL_LAYER_PROVOKING_VERTEX:
g_fake.layerProvokingVertexQueried = true;
*data = g_fake.layerProvokingVertex;
break;
case GL_MAX_COLOR_TEXTURE_SAMPLES:
case GL_MAX_DEPTH_TEXTURE_SAMPLES:
case GL_MAX_FRAMEBUFFER_SAMPLES:
case GL_MAX_INTEGER_SAMPLES:
case GL_MAX_SAMPLES:
*data = g_fake.multisampleCeiling;
break;
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
g_fake.fragmentInterpolationLimitsQueried = true; g_fake.fragmentInterpolationLimitsQueried = true;
if (g_fake.fragmentInterpolationQueryRaisesError) { if (g_fake.fragmentInterpolationQueryRaisesError) {
@@ -205,11 +295,22 @@ namespace {
data[0] = g_fake.maxFragmentInterpolationOffset; data[0] = g_fake.maxFragmentInterpolationOffset;
} }
break; break;
case GL_SMOOTH_LINE_WIDTH_RANGE:
if (g_fake.smoothLineWidthQueryRaisesError) {
g_fake.pendingError = GL_INVALID_ENUM;
} else {
data[0] = 0.0f;
data[1] = 0.0f;
}
break;
case GL_VIEWPORT_BOUNDS_RANGE:
g_fake.viewportArrayLimitsQueried = true;
data[0] = 0.0f;
data[1] = 0.0f;
break;
// Two-component range queries. // Two-component range queries.
case GL_ALIASED_LINE_WIDTH_RANGE: case GL_ALIASED_LINE_WIDTH_RANGE:
case GL_SMOOTH_LINE_WIDTH_RANGE:
case GL_ALIASED_POINT_SIZE_RANGE: case GL_ALIASED_POINT_SIZE_RANGE:
case GL_VIEWPORT_BOUNDS_RANGE:
data[0] = 0.0f; data[0] = 0.0f;
data[1] = 0.0f; data[1] = 0.0f;
break; break;
@@ -400,6 +501,10 @@ namespace {
MobileGL::MG_External::GLESCapabilities MakeEs31Capabilities() { MobileGL::MG_External::GLESCapabilities MakeEs31Capabilities() {
MobileGL::MG_External::GLESCapabilities caps; MobileGL::MG_External::GLESCapabilities caps;
caps.GLESVersion = {3, 1, 0}; caps.GLESVersion = {3, 1, 0};
// The probe reads its vertex storage-block gate from caps rather than re-querying the
// driver (FillInGLESCapabilities resolves the per-stage limits before calling it), so a
// caps struct handed to the probe directly has to carry what the fake reports.
caps.MaxVertexShaderStorageBlocks = g_fake.maxVertexSsboBlocks;
return caps; return caps;
} }
@@ -527,6 +632,263 @@ TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGe
EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried); EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried);
} }
// The per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS probes. These decide whether an application is
// told it may declare a storage block in a graphics stage, and on a driver that cannot serve one
// a wrong answer is not a cosmetic mis-report: the program is built, the driver refuses it at
// link time, the frontend reports LINK_STATUS true anyway, and every draw with it renders
// nothing. A Mali-G925-Immortalis reports 0 for vertex, both tessellation stages and geometry.
TEST(PerStageStorageBlockCapabilities, TakesTheDriverValuesAndGatesTessAndGeometryOnEs32) {
const auto funcs = MakeFakeGLESFunctions();
// ES 3.1: the tessellation and geometry pnames do not exist, so they must not be asked for
// and the stages must report the spec minimum of 0 rather than a hopeful driver number.
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 3;
MobileGL::MG_External::GLESCapabilities es31Caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es31Caps, funcs));
EXPECT_EQ(es31Caps.MaxVertexShaderStorageBlocks, 3);
EXPECT_EQ(es31Caps.MaxFragmentShaderStorageBlocks, g_fake.maxFragmentSsboBlocks);
EXPECT_EQ(es31Caps.MaxTessControlShaderStorageBlocks, 0);
EXPECT_EQ(es31Caps.MaxTessEvaluationShaderStorageBlocks, 0);
EXPECT_EQ(es31Caps.MaxGeometryShaderStorageBlocks, 0);
EXPECT_FALSE(g_fake.tessAndGeometrySsboBlocksQueried);
// ES 3.2: all five are real pnames and all five driver values must come through verbatim.
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 3;
g_fake.glesMinorVersion = 2;
MobileGL::MG_External::GLESCapabilities es32Caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es32Caps, funcs));
EXPECT_EQ(es32Caps.MaxVertexShaderStorageBlocks, 3);
EXPECT_EQ(es32Caps.MaxTessControlShaderStorageBlocks, g_fake.maxTessControlSsboBlocks);
EXPECT_EQ(es32Caps.MaxTessEvaluationShaderStorageBlocks, g_fake.maxTessEvaluationSsboBlocks);
EXPECT_EQ(es32Caps.MaxGeometryShaderStorageBlocks, g_fake.maxGeometrySsboBlocks);
EXPECT_EQ(es32Caps.MaxFragmentShaderStorageBlocks, g_fake.maxFragmentSsboBlocks);
EXPECT_TRUE(g_fake.tessAndGeometrySsboBlocksQueried);
}
// Zero has to survive the round trip intact. It is the answer that matters most - it is what
// ARM's driver actually reports - so a probe that silently substituted a floor would put the
// bug straight back.
TEST(PerStageStorageBlockCapabilities, AZeroFromTheDriverIsReportedAsZero) {
const auto funcs = MakeFakeGLESFunctions();
ResetFakeDriver();
g_fake.glesMinorVersion = 2;
g_fake.maxVertexSsboBlocks = 0;
g_fake.maxTessControlSsboBlocks = 0;
g_fake.maxTessEvaluationSsboBlocks = 0;
g_fake.maxGeometrySsboBlocks = 0;
g_fake.maxFragmentSsboBlocks = 16;
MobileGL::MG_External::GLESCapabilities maliLikeCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(maliLikeCaps, funcs));
EXPECT_EQ(maliLikeCaps.MaxVertexShaderStorageBlocks, 0);
EXPECT_EQ(maliLikeCaps.MaxTessControlShaderStorageBlocks, 0);
EXPECT_EQ(maliLikeCaps.MaxTessEvaluationShaderStorageBlocks, 0);
EXPECT_EQ(maliLikeCaps.MaxGeometryShaderStorageBlocks, 0);
EXPECT_EQ(maliLikeCaps.MaxFragmentShaderStorageBlocks, 16);
}
// A rejected query must leave no error behind for the application's first glGetError to find,
// and must fall back to the spec minimums rather than to whatever the untouched out-param held.
TEST(PerStageStorageBlockCapabilities, ARejectedQueryIsDrainedAndFallsBackToTheSpecMinimums) {
const auto funcs = MakeFakeGLESFunctions();
ResetFakeDriver();
g_fake.perStageSsboBlockQueryRaisesError = true;
g_fake.maxVertexSsboBlocks = 12;
g_fake.maxFragmentSsboBlocks = 12;
MobileGL::MG_External::GLESCapabilities caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
EXPECT_EQ(caps.MaxVertexShaderStorageBlocks, 0);
EXPECT_EQ(caps.MaxFragmentShaderStorageBlocks, 4);
EXPECT_EQ(g_fake.pendingError, static_cast<GLenum>(GL_NO_ERROR));
}
// GL_MAX_CLIP_DISTANCES is the same defect as the per-stage storage blocks above, one pname
// over: the query does not exist without GL_EXT_clip_cull_distance, so an unguarded probe left
// an optimistic 8 behind on every ARM driver. Advertising eight clip planes a driver cannot host
// does not make gl_ClipDistance work - SPIRV-Cross emits it behind an `#extension ... : require`
// the ESSL compiler rejects, DirectGLES has nowhere to put the per-distance enables, and the
// draw renders nothing while LINK_STATUS says everything is fine.
TEST(ClipDistanceCapabilities, NoExtensionMeansNoClipDistancesAndNoQuery) {
const auto funcs = MakeFakeGLESFunctions();
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
MobileGL::MG_External::GLESCapabilities caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
EXPECT_FALSE(caps.SupportsClipDistance);
EXPECT_EQ(caps.MaxClipDistances, 0);
EXPECT_FALSE(g_fake.maxClipDistancesQueried)
<< "GL_MAX_CLIP_DISTANCES is not ES core; asking for it without the extension only leaks "
"a GL_INVALID_ENUM";
}
// The other half of the same claim, and the one that keeps this from being a blanket zero: a
// driver that HAS the extension must have its real limit come through untouched. Adreno does,
// and it passes the clip-distance conformance cases on the strength of it.
TEST(ClipDistanceCapabilities, TheExtensionIsQueriedAndItsLimitIsReportedVerbatim) {
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.extensions.emplace_back("GL_EXT_clip_cull_distance");
g_fake.maxClipDistances = 6;
const auto funcs = MakeFakeGLESFunctions();
MobileGL::MG_External::GLESCapabilities caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
EXPECT_TRUE(caps.SupportsClipDistance);
EXPECT_TRUE(g_fake.maxClipDistancesQueried);
EXPECT_EQ(caps.MaxClipDistances, 6);
}
// A driver that advertises the extension and then refuses the query is a driver fault, not a
// missing feature - but the answer has to be the honest zero either way, and the error must not
// be left for the application's first glGetError to find.
TEST(ClipDistanceCapabilities, ARejectedQueryIsDrainedAndReportsZero) {
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.extensions.emplace_back("GL_EXT_clip_cull_distance");
g_fake.clipDistanceQueryRaisesError = true;
const auto funcs = MakeFakeGLESFunctions();
MobileGL::MG_External::GLESCapabilities caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
EXPECT_TRUE(g_fake.maxClipDistancesQueried);
EXPECT_EQ(caps.MaxClipDistances, 0);
EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR) << "the failed query must not leave an error behind";
}
// The same defect one more time, for the three GL_OES_viewport_array pnames. Their advertised
// values do not come from the driver (GL_Getter answers GL_MAX_VIEWPORTS from the frontend state
// width and floors GL_SUBPIXEL_BITS at its own constant), so what this pins is the other half of
// the class defect: a pname that does not exist must not be asked for, because the GL_INVALID_ENUM
// it raises is then attributed to whatever the application calls next.
TEST(ViewportArrayCapabilities, TheLimitsAreOnlyAskedForWhenTheExtensionIsPresent) {
const auto funcs = MakeFakeGLESFunctions();
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
MobileGL::MG_External::GLESCapabilities withoutCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(withoutCaps, funcs));
EXPECT_FALSE(withoutCaps.SupportsViewportArray);
EXPECT_FALSE(g_fake.viewportArrayLimitsQueried);
EXPECT_EQ(withoutCaps.MaxViewports, 16) << "the OpenGL core minimum, not a driver answer";
EXPECT_FLOAT_EQ(withoutCaps.ViewportBoundsRangeMin, -32768.0f);
EXPECT_FLOAT_EQ(withoutCaps.ViewportBoundsRangeMax, 32767.0f);
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.extensions.emplace_back("GL_OES_viewport_array");
MobileGL::MG_External::GLESCapabilities withCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(withCaps, funcs));
EXPECT_TRUE(withCaps.SupportsViewportArray);
EXPECT_TRUE(g_fake.viewportArrayLimitsQueried);
EXPECT_EQ(withCaps.MaxViewports, g_fake.maxViewports);
EXPECT_EQ(withCaps.ViewportSubpixelBits, g_fake.viewportSubpixelBits);
}
// GL_LAYER_PROVOKING_VERTEX and GL_VIEWPORT_INDEX_PROVOKING_VERTEX name which vertex of a
// primitive supplies gl_Layer and gl_ViewportIndex. MobileGL used to answer a hard-coded
// GL_LAST_VERTEX_CONVENTION for both, derived from nothing, and got it wrong on both test devices
// in OPPOSITE directions. GL_UNDEFINED_VERTEX is a legal answer (GL 4.6 table 23.65) and it is
// the honest one wherever the capability that would give the convention meaning is absent.
TEST(ProvokingVertexConventions, AreTakenFromTheDriverOnlyWhereThePnameExists) {
const auto funcs = MakeFakeGLESFunctions();
// ES 3.1, no viewport array: neither pname exists, so neither is asked for.
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
MobileGL::MG_External::GLESCapabilities es31Caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es31Caps, funcs));
EXPECT_FALSE(g_fake.layerProvokingVertexQueried);
EXPECT_EQ(es31Caps.LayerProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
EXPECT_EQ(es31Caps.ViewportIndexProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
// ES 3.2 with the viewport array: both exist and both driver answers come through verbatim.
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.glesMinorVersion = 2;
g_fake.extensions.emplace_back("GL_OES_viewport_array");
MobileGL::MG_External::GLESCapabilities es32Caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es32Caps, funcs));
EXPECT_TRUE(g_fake.layerProvokingVertexQueried);
EXPECT_EQ(es32Caps.LayerProvokingVertex, static_cast<GLenum>(GL_FIRST_VERTEX_CONVENTION));
EXPECT_EQ(es32Caps.ViewportIndexProvokingVertex, static_cast<GLenum>(GL_LAST_VERTEX_CONVENTION));
// ES 3.2 WITHOUT the viewport array - the shape of both test devices. The layer convention is
// real and comes from the driver; the viewport-index one describes a selection that never
// happens, because only viewport 0 is ever rasterized, and stays undefined.
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.glesMinorVersion = 2;
MobileGL::MG_External::GLESCapabilities deviceLikeCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(deviceLikeCaps, funcs));
EXPECT_EQ(deviceLikeCaps.LayerProvokingVertex, static_cast<GLenum>(GL_FIRST_VERTEX_CONVENTION));
EXPECT_EQ(deviceLikeCaps.ViewportIndexProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
}
// A driver answering something that is not one of the four legal conventions must not have it
// forwarded as one: GL_UNDEFINED_VERTEX describes "MobileGL cannot tell you" exactly.
TEST(ProvokingVertexConventions, AnIllegalDriverAnswerBecomesUndefined) {
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.glesMinorVersion = 2;
g_fake.layerProvokingVertex = 0x1234;
const auto funcs = MakeFakeGLESFunctions();
MobileGL::MG_External::GLESCapabilities caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
EXPECT_TRUE(g_fake.layerProvokingVertexQueried);
EXPECT_EQ(caps.LayerProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
}
// The multisample ceilings are ES 3.1 state; a driver that answers zero - or an older context
// that answers nothing - must not have that reach GL_Getter, which would then reject the sample
// count it just advertised.
TEST(MultisampleCapabilities, TheAdvertisedSampleCountsNeverFallBelowOne) {
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.multisampleCeiling = 0;
const auto funcs = MakeFakeGLESFunctions();
MobileGL::MG_External::GLESCapabilities caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
EXPECT_EQ(caps.MaxColorTextureSamples, 1);
EXPECT_EQ(caps.MaxDepthTextureSamples, 1);
EXPECT_EQ(caps.MaxFramebufferSamples, 1);
EXPECT_EQ(caps.MaxIntegerSamples, 1);
EXPECT_EQ(caps.MaxSamples, 1);
EXPECT_EQ(caps.MaxSampleMaskWords, 1);
}
// The whole point of the drain, stated once at the level that matters: capability init is the
// first thing that ever touches the driver, so an error it leaves behind surfaces at the
// APPLICATION's first glGetError and is blamed on an unrelated call. GL_SMOOTH_LINE_WIDTH_RANGE
// is the stand-in because it is desktop-only state that every real GLES driver refuses.
TEST(CapabilityProbeHygiene, ARejectedUnconditionalProbeLeavesNoErrorBehind) {
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.smoothLineWidthQueryRaisesError = true;
const auto funcs = MakeFakeGLESFunctions();
MobileGL::MG_External::GLESCapabilities caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR)
<< "capability init must not hand the application an error it never caused";
}
TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) { TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) {
const auto funcs = MakeFakeGLESFunctions(); const auto funcs = MakeFakeGLESFunctions();
+111
View File
@@ -8,6 +8,7 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <cstdint>
#include <limits> #include <limits>
#include "Includes.h" #include "Includes.h"
@@ -267,6 +268,116 @@ TEST_F(BufferTest, AcquireMemoryRangeWithExplicit) {
ASSERT_EQ(actual, expected); ASSERT_EQ(actual, expected);
} }
// GL_MIN_MAP_BUFFER_ALIGNMENT is a promise about POINTERS, and MobileGL used to keep only the
// query half of it: glGetIntegerv answered 64 while every mapped pointer came out of a plain
// std::vector, aligned to alignof(std::max_align_t) - 16 on aarch64. GL 4.2 /
// ARB_map_buffer_alignment fix the minimum at 64, so under-reporting is not available and the
// implementation has to be brought up to the number instead. Note the two different constraints:
// glMapBuffer's pointer must be aligned outright, while glMapBufferRange's must be aligned AFTER
// subtracting the offset the caller asked for - i.e. it sits at the offset's own alignment phase.
// KHR-GLxx.map_buffer_alignment.functional asserts exactly these two, at offset 63, for 24
// storage-flag combinations across 14 targets, and failed identically on both test devices.
TEST_F(BufferTest, MappedPointersHonourTheAdvertisedMapBufferAlignment) {
GLint advertisedAlignment = 0;
MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_MIN_MAP_BUFFER_ALIGNMENT, &advertisedAlignment);
ASSERT_EQ(advertisedAlignment, static_cast<GLint>(MobileGL::MG_State::GLState::MIN_MAP_BUFFER_ALIGNMENT))
<< "the query and the allocator must read the same constant";
ASSERT_GE(advertisedAlignment, 64) << "GL 4.2 fixes the minimum at 64";
const SizeT alignment = static_cast<SizeT>(advertisedAlignment);
auto& slot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Uniform);
Vector<Uint> bufferNames;
MobileGL::MG_State::pGLContext->GenBufferNames(1, bufferNames);
auto bufObj = MobileGL::MG_State::pGLContext->CreateBufferObject(bufferNames[0]);
slot.Bind(bufObj);
// The conformance test's own shape: a buffer two alignments long, mapped from the last byte
// inside the first alignment - the offset most likely to expose a base-aligned-only fix.
const SizeT bufferSize = 2 * alignment;
const SizeT offset = alignment - 1;
bufObj->Resize(bufferSize);
Vector<Uint8> initData(bufferSize);
for (SizeT i = 0; i < bufferSize; ++i) initData[i] = static_cast<Uint8>(i);
bufObj->UploadData(DataPtr{.data = initData.data(), .size = bufferSize}, 0);
const auto addressOf = [](const void* pointer) { return reinterpret_cast<std::uintptr_t>(pointer); };
// glMapBuffer, read-only: the shadow base itself is handed out.
void* readMapped = bufObj->AcquireMemory(true, true, false);
ASSERT_NE(readMapped, nullptr);
EXPECT_EQ(addressOf(readMapped) % alignment, 0u) << "glMapBuffer(GL_READ_ONLY) returned an unaligned pointer";
bufObj->ReleaseMemory();
// glMapBuffer, write: the staging store is handed out instead.
void* writeMapped = bufObj->AcquireMemory(true, false, true);
ASSERT_NE(writeMapped, nullptr);
EXPECT_EQ(addressOf(writeMapped) % alignment, 0u) << "glMapBuffer(GL_WRITE_ONLY) returned an unaligned pointer";
EXPECT_EQ(bufObj->GetMappedPointer(), writeMapped)
<< "GL_BUFFER_MAP_POINTER must report the pointer the map returned";
bufObj->ReleaseMemory();
// glMapBufferRange, read-only: shadow base + offset, so the phase falls out for free.
const Range1D mapRange{.start = offset, .end = bufferSize};
void* rangeRead = bufObj->AcquireMemoryRange(mapRange, BufferMappingAccessBit::Read);
ASSERT_NE(rangeRead, nullptr);
EXPECT_EQ((addressOf(rangeRead) - offset) % alignment, 0u)
<< "glMapBufferRange(READ) returned a pointer whose base is unaligned";
bufObj->ReleaseMemory();
// glMapBufferRange, write: the staging store has to be biased to the same phase, and the
// write-back has to follow the bias or the bytes land at the wrong place in the shadow.
Uint8* rangeWrite = static_cast<Uint8*>(bufObj->AcquireMemoryRange(mapRange, BufferMappingAccessBit::Write));
ASSERT_NE(rangeWrite, nullptr);
EXPECT_EQ((addressOf(rangeWrite) - offset) % alignment, 0u)
<< "glMapBufferRange(WRITE) returned a pointer whose base is unaligned";
EXPECT_EQ(bufObj->GetMappedPointer(), rangeWrite)
<< "GL_BUFFER_MAP_POINTER must report the pointer the map returned";
// Seeded from the shadow, so the mapped view starts at the offset's byte.
EXPECT_EQ(rangeWrite[0], static_cast<Uint8>(offset));
rangeWrite[0] = 0xAB;
rangeWrite[bufferSize - offset - 1] = 0xCD;
bufObj->ReleaseMemory();
Vector<Uint8> readBack(bufferSize);
bufObj->DownloadSubData(readBack.data(), 0, bufferSize);
EXPECT_EQ(readBack[offset], 0xAB) << "the biased staging write-back landed at the wrong offset";
EXPECT_EQ(readBack[bufferSize - 1], 0xCD) << "the biased staging write-back landed at the wrong offset";
EXPECT_EQ(readBack[offset - 1], static_cast<Uint8>(offset - 1)) << "the write-back overran the mapped range";
}
// The explicit-flush path reads through the same bias, one flush offset further in: a flush of
// [offset + 4, offset + 8) must copy the bytes the application wrote at rangeWrite[4..8), not the
// ones sitting four bytes into the raw allocation.
TEST_F(BufferTest, ExplicitFlushOfARangeMapFollowsTheAlignmentBias) {
auto& slot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Uniform);
Vector<Uint> bufferNames;
MobileGL::MG_State::pGLContext->GenBufferNames(1, bufferNames);
auto bufObj = MobileGL::MG_State::pGLContext->CreateBufferObject(bufferNames[0]);
slot.Bind(bufObj);
const SizeT alignment = MobileGL::MG_State::GLState::MIN_MAP_BUFFER_ALIGNMENT;
const SizeT bufferSize = 2 * alignment;
const SizeT offset = alignment - 1;
bufObj->Resize(bufferSize);
Vector<Uint8> initData(bufferSize, 0);
bufObj->UploadData(DataPtr{.data = initData.data(), .size = bufferSize}, 0);
const Range1D mapRange{.start = offset, .end = bufferSize};
Uint8* mapped = static_cast<Uint8*>(bufObj->AcquireMemoryRange(
mapRange, BufferMappingAccessBit::Write | BufferMappingAccessBit::FlushExplicit));
ASSERT_NE(mapped, nullptr);
mapped[4] = 0x5A;
mapped[5] = 0x5B;
bufObj->FlushMemoryRange(4, 2);
bufObj->ReleaseMemory();
Vector<Uint8> readBack(bufferSize);
bufObj->DownloadSubData(readBack.data(), 0, bufferSize);
EXPECT_EQ(readBack[offset + 4], 0x5A);
EXPECT_EQ(readBack[offset + 5], 0x5B);
EXPECT_EQ(readBack[offset + 3], 0x00) << "the explicit flush copied bytes outside the flushed range";
}
TEST_F(BufferTest, CopyBufferSubData) { TEST_F(BufferTest, CopyBufferSubData) {
auto& srcSlot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::CopyRead); auto& srcSlot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::CopyRead);
auto& dstSlot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::CopyWrite); auto& dstSlot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::CopyWrite);
@@ -716,6 +716,257 @@ void main() {
EXPECT_EQ(TakeError(), GL_INVALID_ENUM); EXPECT_EQ(TakeError(), GL_INVALID_ENUM);
} }
// The CLASSIC query surface has to agree with the interface query above. MobileGL lowers
// every atomic_uint onto a synthesized gl_AtomicCounterBlock_N, and glGetActiveUniform /
// glGetActiveUniformsiv used to report that lowering: GL_UNSIGNED_INT instead of
// GL_UNSIGNED_INT_ATOMIC_COUNTER, the synthesized block's index instead of the -1 a
// default-block uniform owes, and GL_INVALID_ENUM for
// GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX - the last of which is what made
// KHR-GL43.shader_atomic_counters.basic-program-query a forced FAIL.
TEST_F(ProgramInterfaceTest, AtomicCounterClassicUniformQueries) {
const char* fs = R"(#version 430
out vec4 color;
layout (binding = 0, offset = 0) uniform atomic_uint ac_counter0;
layout (binding = 1, offset = 0) uniform atomic_uint ac_counter1;
uniform float plain;
void main() {
color = vec4(float(atomicCounterIncrement(ac_counter0) + atomicCounterIncrement(ac_counter1)) + plain);
}
)";
const GLuint p = MakeProgram(kSimpleVs, fs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
const auto indexOf = [p](const char* name) {
const GLchar* names[1] = {name};
GLuint index = GL_INVALID_INDEX;
GetUniformIndices(p, 1, names, &index);
return index;
};
const auto uniformiv = [p](GLuint index, GLenum pname) {
GLint value = -12345;
const GLuint indices[1] = {index};
GetActiveUniformsiv(p, 1, indices, pname, &value);
return value;
};
const GLuint counter0 = indexOf("ac_counter0");
const GLuint counter1 = indexOf("ac_counter1");
const GLuint plain = indexOf("plain");
ASSERT_NE(counter0, GL_INVALID_INDEX);
ASSERT_NE(counter1, GL_INVALID_INDEX);
ASSERT_NE(plain, GL_INVALID_INDEX);
// (a) glGetActiveUniform and glGetActiveUniformsiv(GL_UNIFORM_TYPE) both report the
// GL-level type.
GLint size = 0;
GLenum type = 0;
GLchar nameBuffer[64] = {'\0'};
GetActiveUniform(p, counter0, sizeof(nameBuffer), nullptr, &size, &type, nameBuffer);
EXPECT_EQ(type, static_cast<GLenum>(GL_UNSIGNED_INT_ATOMIC_COUNTER));
EXPECT_EQ(std::string(nameBuffer), "ac_counter0");
EXPECT_EQ(uniformiv(counter0, GL_UNIFORM_TYPE), GL_UNSIGNED_INT_ATOMIC_COUNTER);
EXPECT_EQ(uniformiv(counter1, GL_UNIFORM_TYPE), GL_UNSIGNED_INT_ATOMIC_COUNTER);
EXPECT_EQ(uniformiv(plain, GL_UNIFORM_TYPE), GL_FLOAT);
// (b) an atomic counter is a DEFAULT-BLOCK uniform, whatever it was lowered onto.
EXPECT_EQ(uniformiv(counter0, GL_UNIFORM_BLOCK_INDEX), -1);
EXPECT_EQ(uniformiv(counter1, GL_UNIFORM_BLOCK_INDEX), -1);
EXPECT_EQ(uniformiv(plain, GL_UNIFORM_BLOCK_INDEX), -1);
// (c) the pname is accepted, answers with the buffer's index, and reports -1 for a
// uniform that is not a counter. Two bindings mean two distinct buffers.
const GLint buffer0 = uniformiv(counter0, GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX);
const GLint buffer1 = uniformiv(counter1, GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX);
EXPECT_GE(buffer0, 0);
EXPECT_GE(buffer1, 0);
EXPECT_NE(buffer0, buffer1);
EXPECT_LT(buffer0, Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES));
EXPECT_LT(buffer1, Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES));
EXPECT_EQ(uniformiv(plain, GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX), -1);
// No leftover error: the CTS harness fails the subcase on one.
EXPECT_EQ(TakeError(), GL_NO_ERROR);
// The classic surface and the interface query name the same buffer.
const std::vector<GLint> interfaceBuffer =
PropsOf(p, GL_UNIFORM, "ac_counter0", {GL_ATOMIC_COUNTER_BUFFER_INDEX});
ASSERT_EQ(interfaceBuffer.size(), 1u);
EXPECT_EQ(interfaceBuffer[0], buffer0);
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// Two counters that share a binding AND an offset must fail to link. glslang's own check
// lives in fixOffset(), which the Vulkan-relaxed parse never reaches - it folds the
// atomic_uint into a storage block and returns from declareVariable() first - so the pair
// used to link cleanly and then increment the same four bytes.
TEST_F(ProgramInterfaceTest, OverlappingAtomicCounterOffsetsFailToLink) {
const char* fs = R"(#version 430
out vec4 color;
layout (binding = 0, offset = 0) uniform atomic_uint a;
layout (binding = 0, offset = 0) uniform atomic_uint b;
void main() { color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b))); }
)";
const GLuint p = MakeProgram(kSimpleVs, fs);
LinkProgram(p);
GLint status = -1;
GetProgramiv(p, GL_LINK_STATUS, &status);
EXPECT_EQ(status, GL_FALSE);
char log[4096] = "";
GetProgramInfoLog(p, sizeof(log), nullptr, log);
EXPECT_NE(std::string(log).find("overlap"), std::string::npos) << "info log was: " << log;
ClearErrors();
// Distinct offsets at one binding, and the same offset at two different bindings, are
// both legal and must still link - a check keyed any wider would reject them.
const char* legalFs = R"(#version 430
out vec4 color;
layout (binding = 0, offset = 0) uniform atomic_uint a;
layout (binding = 0, offset = 4) uniform atomic_uint b;
layout (binding = 1, offset = 0) uniform atomic_uint c;
void main() {
color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b) + atomicCounterIncrement(c)));
}
)";
const GLuint legal = MakeProgram(kSimpleVs, legalFs);
LinkProgram(legal);
ExpectLinked(legal);
ClearErrors();
}
// GL 4.6 core 7.6 fails the link when a stage's active image uniforms exceed
// GL_MAX_*_IMAGE_UNIFORMS, or when their sum exceeds GL_MAX_COMBINED_IMAGE_UNIFORMS. Nothing
// counted them - glslang keeps those numbers only so gl_Max*ImageUniforms can expand from
// them - so every deliberately-oversized program in
// KHR-GL4x.shader_image_load_store.uniform-limits linked cleanly and then rendered nothing.
//
// Sized off the ADVERTISED limits rather than a constant, because the numbers come from the
// active backend and the whole point of the check is that the two agree.
TEST_F(ProgramInterfaceTest, ImageUniformsOverAStageLimitFailToLink) {
GLint maxFragmentImages = 0;
GLint maxCombinedImages = 0;
GetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImages);
GetIntegerv(GL_MAX_COMBINED_IMAGE_UNIFORMS, &maxCombinedImages);
ClearErrors();
ASSERT_GT(maxFragmentImages, 0);
// The fragment stage is compiled explicitly so a COMPILE failure can never be mistaken
// for the link failure under test.
const auto linkWithFragmentImages = [](GLint count) {
const std::string n = std::to_string(count);
const std::string source = std::string(R"(#version 430
out vec4 color;
layout(r32i) uniform iimage2D u_image[)") + n + R"(];
void main() {
int value = 1;
for (int i = 0; i < )" + n + R"(; ++i) {
value = imageAtomicAdd(u_image[i], ivec2(0), value);
}
color = vec4(float(value));
}
)";
const char* sourcePtr = source.c_str();
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &sourcePtr, nullptr);
CompileShader(fs);
GLint compiled = 0;
GetShaderiv(fs, GL_COMPILE_STATUS, &compiled);
EXPECT_EQ(compiled, GL_TRUE) << "the fragment stage with " << count << " image uniforms must compile";
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &kSimpleVs, nullptr);
CompileShader(vs);
const GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, fs);
LinkProgram(program);
return program;
};
const GLuint over = linkWithFragmentImages(maxFragmentImages + 1);
GLint status = -1;
GetProgramiv(over, GL_LINK_STATUS, &status);
EXPECT_EQ(status, GL_FALSE);
char log[4096] = "";
GetProgramInfoLog(over, sizeof(log), nullptr, log);
EXPECT_NE(std::string(log).find("GL_MAX_FRAGMENT_IMAGE_UNIFORMS"), std::string::npos)
<< "info log was: " << log;
ClearErrors();
// Exactly AT the limit is legal and must still link: the comparison is strictly
// greater-than, and the conformance suite's combined-stage subcase builds a program that
// fills every stage to its own limit and expects it to link whenever the combined limit
// can hold them.
if (maxFragmentImages <= maxCombinedImages) {
const GLuint atLimit = linkWithFragmentImages(maxFragmentImages);
ExpectLinked(atLimit);
ClearErrors();
}
}
// glGetProgramiv(GL_ACTIVE_ATOMIC_COUNTER_BUFFERS) and glGetActiveAtomicCounterBufferiv are
// the pre-4.3 spelling of the interface above, and the spec requires the two to agree.
// Neither did: the first counted glslang's atomic counter UNIFORMS - zero, because the
// relaxed parse folds every atomic_uint into a storage block before reflection runs - and
// the second was a stub that wrote nothing and raised nothing.
TEST_F(ProgramInterfaceTest, ActiveAtomicCounterBufferQueriesMatchTheInterface) {
const char* fs = R"(#version 430
out vec4 color;
layout (binding = 1, offset = 0) uniform atomic_uint a;
layout (binding = 2, offset = 0) uniform atomic_uint b;
layout (binding = 2, offset = 4) uniform atomic_uint c;
void main() {
color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b) + atomicCounterIncrement(c)));
}
)";
const GLuint p = MakeProgram(kSimpleVs, fs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
GLint bufferCount = -12345;
GetProgramiv(p, GL_ACTIVE_ATOMIC_COUNTER_BUFFERS, &bufferCount);
EXPECT_EQ(bufferCount, Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES));
ASSERT_EQ(bufferCount, 2);
const auto activeBufferiv = [p](GLuint index, GLenum pname) {
GLint value = -12345;
GetActiveAtomicCounterBufferiv(p, index, pname, &value);
return value;
};
for (GLuint index = 0; index < static_cast<GLuint>(bufferCount); ++index) {
const std::vector<GLint> viaInterface =
Props(p, GL_ATOMIC_COUNTER_BUFFER, index,
{GL_BUFFER_BINDING, GL_BUFFER_DATA_SIZE, GL_NUM_ACTIVE_VARIABLES,
GL_REFERENCED_BY_VERTEX_SHADER, GL_REFERENCED_BY_FRAGMENT_SHADER});
ASSERT_EQ(viaInterface.size(), 5u);
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_BINDING), viaInterface[0]);
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_DATA_SIZE), viaInterface[1]);
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS), viaInterface[2]);
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_VERTEX_SHADER), viaInterface[3]);
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_FRAGMENT_SHADER), viaInterface[4]);
// The counter indices are the GL_UNIFORM indices, in the same order.
const std::vector<GLint> expectedIndices = Props(p, GL_ATOMIC_COUNTER_BUFFER, index, {GL_ACTIVE_VARIABLES});
ASSERT_FALSE(expectedIndices.empty());
std::vector<GLint> indices(expectedIndices.size(), -12345);
GetActiveAtomicCounterBufferiv(p, index, GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES,
indices.data());
EXPECT_EQ(indices, expectedIndices);
}
EXPECT_EQ(TakeError(), GL_NO_ERROR);
GLint sink = -12345;
GetActiveAtomicCounterBufferiv(p, static_cast<GLuint>(bufferCount), GL_ATOMIC_COUNTER_BUFFER_BINDING, &sink);
EXPECT_EQ(TakeError(), GL_INVALID_VALUE);
EXPECT_EQ(sink, -12345) << "a rejected query must not write the caller's output";
// The interface-query spelling of the same property is NOT accepted here.
GetActiveAtomicCounterBufferiv(p, 0, GL_BUFFER_BINDING, &sink);
EXPECT_EQ(TakeError(), GL_INVALID_ENUM);
EXPECT_EQ(sink, -12345);
}
// --------------------------------------------------------- transform-feedback ------ // --------------------------------------------------------- transform-feedback ------
TEST_F(ProgramInterfaceTest, TransformFeedbackVaryingTypes) { TEST_F(ProgramInterfaceTest, TransformFeedbackVaryingTypes) {
const char* vs = R"(#version 430 const char* vs = R"(#version 430
+177 -15
View File
@@ -1934,10 +1934,19 @@ TEST_F(ProgramTest, GetActiveUniformsivErrors) {
EXPECT_EQ(GetError(), GL_INVALID_VALUE); EXPECT_EQ(GetError(), GL_INVALID_VALUE);
EXPECT_EQ(params[0], -999); EXPECT_EQ(params[0], -999);
// E3: GL 4.2 token -> GL_INVALID_ENUM here. // E3: the GL 4.2 / ARB_shader_atomic_counters token is ACCEPTED, not rejected.
//
// This case used to assert GL_INVALID_ENUM, which was right only while the token was
// unimplemented. It is implemented now, and `validIndex` names an ordinary uniform rather
// than an atomic counter, so the spec answer is -1 with no error (GL 4.6 core table 7.6).
// ProgramInterfaceTest's atomic-counter case asserts the same -1 for a non-counter
// uniform; leaving this one inverted made the two contradict each other.
GetActiveUniformsiv(program, 1, &validIndex, GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX, params); GetActiveUniformsiv(program, 1, &validIndex, GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX, params);
EXPECT_EQ(GetError(), GL_INVALID_ENUM); EXPECT_EQ(GetError(), GL_NO_ERROR);
EXPECT_EQ(params[0], -999); EXPECT_EQ(params[0], -1);
// Restored: the cases below assert that a REJECTED call leaves params untouched, and this
// one legitimately wrote to it.
params[0] = -999;
// E4a: a live shader name -> GL_INVALID_OPERATION. // E4a: a live shader name -> GL_INVALID_OPERATION.
GLuint shader = CreateShader(GL_VERTEX_SHADER); GLuint shader = CreateShader(GL_VERTEX_SHADER);
@@ -2162,6 +2171,69 @@ void main() {
EXPECT_EQ(GetError(), GL_NO_ERROR); EXPECT_EQ(GetError(), GL_NO_ERROR);
} }
// Repro for KHR-GLES31.explicit_uniform_location.uniform-loc-arrays-of-arrays: an
// array-of-arrays uniform reaches the GL surface as one entry PER SUB-ARRAY ("u0[0]",
// "u0[1]" - glslang stops expanding at reflection granularity), while SPIRV-Reflect keeps
// it as a single leaf carrying every dimension. Routing the single leaf only ever covered
// the first sub-array, so every element from u0[1][0] on found no UBO offset and fell
// through to the fallback scratch at the tail of the shadow - storage the GPU never reads,
// which made those glUniform writes silently vanish.
TEST_F(ProgramTest, ArrayOfArraysUniformElementOffsets) {
// Arrays of arrays need GLSL 4.30; both stages take the same version.
const char* vsSource = R"(#version 430 core
in vec4 a_position;
void main() {
gl_Position = a_position;
})";
const char* fsSource = R"(#version 430 core
uniform float u0[2][3];
uniform vec3 u1[2][2];
out vec4 o_color;
void main() {
float s = 0.0;
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 3; ++j) s += u0[i][j];
}
vec3 v = vec3(0.0);
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 2; ++j) v += u1[i][j];
}
o_color = vec4(v, s);
})";
GLuint program = LinkVsFsProgram(vsSource, fsSource);
UseProgram(program);
auto programObject = MG_State::pGLContext->GetProgramObject(program);
ASSERT_NE(programObject, nullptr);
// std140 gives a float array element and a vec3 array element the same 16-byte slot,
// and a flattened array-of-arrays is one contiguous run of those slots.
constexpr Uint kStd140ElementStride = 16u;
const auto checkFlattenedRun = [&](const char* base, int outer, int inner) {
Uint firstOffset = MG_State::GLState::ProgramObject::kInvalidUniformOffset;
for (int i = 0; i < outer; ++i) {
for (int j = 0; j < inner; ++j) {
const std::string name =
std::string(base) + "[" + std::to_string(i) + "][" + std::to_string(j) + "]";
const GLint location = GetUniformLocation(program, name.c_str());
ASSERT_GE(location, 0) << name;
const Uint offset = programObject->GetUniformOffset(static_cast<Uint>(location));
ASSERT_NE(offset, MG_State::GLState::ProgramObject::kInvalidUniformOffset) << name;
const Uint element = static_cast<Uint>(i * inner + j);
if (element == 0) {
firstOffset = offset;
} else {
EXPECT_EQ(offset, firstOffset + element * kStd140ElementStride) << name;
}
}
}
};
checkFlattenedRun("u0", 2, 3);
checkFlattenedRun("u1", 2, 2);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// GL CTS KHR-GL33.shaders.uniform_block regression pack. MobileGL's SPIR-V // GL CTS KHR-GL33.shaders.uniform_block regression pack. MobileGL's SPIR-V
// pipeline lays every uniform block out as std140; the frontend implements the // pipeline lays every uniform block out as std140; the frontend implements the
@@ -3010,21 +3082,38 @@ TEST_F(ProgramTest, TwoShaderObjectsWithIdenticalSourceLinkIndependently) {
ASSERT_NE(objectA, nullptr); ASSERT_NE(objectA, nullptr);
ASSERT_NE(objectB, nullptr); ASSERT_NE(objectB, nullptr);
EXPECT_EQ(objectA->GetShaderSource(), objectB->GetShaderSource()); EXPECT_EQ(objectA->GetShaderSource(), objectB->GetShaderSource());
// P0b's layer 2 shares the PREPROCESS and never the parse: glslang's TShader is // WHAT THIS CASE IS ACTUALLY ABOUT: two GL shader names holding the same text must never
// consume-once, so a memo hit still has to parse for itself. // end up feeding one TShader to two links, because mapIO mutates the aliased intermediate
// and the second link would get a corrupted one. There are now three mechanisms that keep
// that true, and which one is in play depends on the mode - so the assertion below is on
// the PARSES NOT BEING SHARED, never on where each object's parse came from:
// //
// P1 stage 6 shares something stronger when it is active - the whole compile JOB, and // * P0b layer 2 shares the PREPROCESS and never the parse, so each object parses for
// therefore the single parse that job produced - and that sharing is made safe by // itself. This was the only mechanism when the case was written.
// ShaderCompileTask::ClaimParsedShader's CAS instead, exactly as it already was for one // * P1 stage 6, when async is active, shares the whole compile JOB and therefore its
// shader object attached to two programs. ShaderCompileAdoptionTest is where that is // single parse - made safe by ClaimParsedShader's CAS, exactly as it already was for
// pinned down (it links both objects and compares the generated SPIR-V). So the // one shader object attached to two programs. ShaderCompileAdoptionTest pins that
// one-parse-per-object assertion belongs to the non-adopting path; the two independent // down by linking both objects and comparing the generated SPIR-V.
// LINKS below are what both modes have to agree on, and they are the point of this case. // * The translation memo's compile half (L1c) recognises the second object's source and
// publishes its verdict WITHOUT parsing, so that object legitimately holds no TShader
// at all until a link asks ClaimParsedShader for one. Asserting a non-null parse here
// would be asserting that the parse had NOT been skipped - i.e. testing the absence
// of the optimisation rather than the invariant.
//
// So the pointer assertion applies only where the two objects are genuinely INDEPENDENT,
// i.e. where job adoption is not in play. What every mode has to agree on is the two
// independent LINKS below, and they are the real point of this case.
if (!MG_Util::Async::AsyncShaderCompileActive()) { if (!MG_Util::Async::AsyncShaderCompileActive()) {
EXPECT_NE(objectA->GetCompiledShader(), objectB->GetCompiledShader()); const auto& shaderA = objectA->GetCompiledShader();
const auto& shaderB = objectB->GetCompiledShader();
// Either may legitimately hold NO parse: that is an L1c hit, where the AST is made on
// demand at link instead. So this asserts they are not the SAME non-null parse, and
// deliberately not that both have one - the latter would be asserting that the
// optimisation had not happened.
if (shaderA != nullptr && shaderB != nullptr) {
EXPECT_NE(shaderA, shaderB) << "two independent shader objects share one consume-once parse";
}
} }
EXPECT_NE(objectA->GetCompiledShader(), nullptr);
EXPECT_NE(objectB->GetCompiledShader(), nullptr);
GLuint programA = LinkVsFs(vsA, fsA, GL_TRUE); GLuint programA = LinkVsFs(vsA, fsA, GL_TRUE);
GLuint programB = LinkVsFs(vsB, fsB, GL_TRUE); GLuint programB = LinkVsFs(vsB, fsB, GL_TRUE);
@@ -3239,3 +3328,76 @@ TEST_F(ProgramTest, CreateShaderAndCreateShaderProgramvReportTheRightErrorClasse
EXPECT_NE(program, 0u); EXPECT_NE(program, 0u);
EXPECT_EQ(GetError(), GL_NO_ERROR); EXPECT_EQ(GetError(), GL_NO_ERROR);
} }
// ARB_explicit_uniform_location / GL 4.6 core 7.6.1: a `layout(location = N)` uniform reserves N
// EVEN WHEN IT IS INACTIVE. Dead default-block uniforms are correctly filtered off the GL surface
// (glGetUniformLocation must answer -1 for them), but the implicit allocator used to walk straight
// over the location they claimed and hand it to a uniform that never asked for it
// (KHR-GL43.explicit_uniform_location.uniform-loc-mix-with-implicit3).
TEST_F(ProgramTest, InactiveExplicitUniformLocationIsStillReserved) {
const char* vsSource = R"(#version 430 core
layout(location = 2) uniform vec4 uDeadAtTwo;
uniform vec4 uA;
uniform vec4 uB;
uniform vec4 uC;
uniform vec4 uD;
void main() { gl_Position = uA + uB + uC + uD; }
)";
const char* fsSource = R"(#version 430 core
out vec4 fragColor;
void main() { fragColor = vec4(1.0); }
)";
const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
const GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
const GLuint program = LinkVsFs(vs, fs, GL_TRUE);
// Reserving a location must not resurrect the uniform: it is still inactive to GL.
EXPECT_EQ(GetUniformLocation(program, "uDeadAtTwo"), -1);
for (const char* name : {"uA", "uB", "uC", "uD"}) {
const GLint location = GetUniformLocation(program, name);
EXPECT_GE(location, 0) << name << " lost its implicit location";
EXPECT_NE(location, 2) << name << " was handed the location uDeadAtTwo reserved";
}
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// The GL_MAX_UNIFORM_LOCATIONS boundary, from both sides. MAX_UNIFORM_LOCATIONS - 1 is the LAST
// LEGAL location: it has to link and read back verbatim
// (KHR-GL43.explicit_uniform_location.uniform-loc-max), which is only true while the advertised
// value and what the link accepts are the SAME number - the getter used to advertise one more
// location than any shader could name.
//
// The over-the-ceiling half is asserted through an ARRAY, because that is the only spelling the
// link gets to judge: a bare `layout(location = MAX)` is already a compile error inside glslang
// ("location is too large"), while an array's base compiles fine and only its last element passes
// the ceiling (...uniform-loc-negative-link-max-num-of-locations).
TEST_F(ProgramTest, ExplicitUniformLocationsHonourMaxUniformLocations) {
GLint maxLocations = 0;
GetIntegerv(GL_MAX_UNIFORM_LOCATIONS, &maxLocations);
ASSERT_GE(maxLocations, 1024) << "GL 4.3 requires at least 1024 uniform locations";
const char* fsSource = R"(#version 430 core
out vec4 fragColor;
void main() { fragColor = vec4(1.0); }
)";
const GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
{
const String source = String("#version 430 core\nlayout(location = ") +
std::to_string(maxLocations - 1) +
") uniform vec4 uAtLimit;\nvoid main() { gl_Position = uAtLimit; }\n";
const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, source.c_str());
const GLuint program = LinkVsFs(vs, fs, GL_TRUE);
EXPECT_EQ(GetUniformLocation(program, "uAtLimit"), maxLocations - 1)
<< "the last location in the pool is legal and must come back verbatim";
}
{
const String source = String("#version 430 core\nlayout(location = ") +
std::to_string(maxLocations - 4) +
") uniform vec4 uSpill[8];\nvoid main() { gl_Position = uSpill[0]; }\n";
const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, source.c_str());
(void)LinkVsFs(vs, fs, GL_FALSE);
}
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
+388 -3
View File
@@ -3348,11 +3348,38 @@ namespace {
return count; return count;
} }
// Same word walk, for the NON-arrayed half of the family (Arrayed == 0).
SizeT Count1DNonArrayedStorageImageTypes(const Vector<Uint32>& spirv) {
constexpr unsigned kOpTypeImage = 25, kDim1D = 0;
SizeT count = 0;
for (SizeT i = 5; i < spirv.size();) {
const unsigned wordCount = spirv[i] >> 16;
const unsigned opcode = spirv[i] & 0xFFFFu;
if (wordCount == 0 || i + wordCount > spirv.size()) break;
if (opcode == kOpTypeImage && wordCount >= 8 && spirv[i + 3] == kDim1D && spirv[i + 5] == 0u &&
spirv[i + 7] == 2u) {
++count;
}
i += wordCount;
}
return count;
}
const char* k1DArrayImageCompute = R"(#version 440 core const char* k1DArrayImageCompute = R"(#version 440 core
layout (local_size_x = 1) in; layout (local_size_x = 1) in;
layout (location = 0, r32ui) readonly uniform uimage1DArray i0; layout (location = 0, r32ui) readonly uniform uimage1DArray i0;
layout (std430, binding = 0) buffer SSB { uint sum; } ssb; layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
void main() { ssb.sum = imageLoad(i0, ivec2(2, 3)).r; } void main() { ssb.sum = imageLoad(i0, ivec2(2, 3)).r; }
)";
// KHR-GL4x.shader_image_load_store.basic-allTargets-atomic's own shape, minus the six other
// targets: a non-arrayed 1D storage image reached ONLY through an atomic. r32ui because ES
// defines image atomics on r32i/r32ui/r32f alone.
const char* k1DImageAtomicCompute = R"(#version 440 core
layout (local_size_x = 1) in;
layout (r32ui) coherent uniform uimage1D i0;
layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
void main() { ssb.sum = imageAtomicAdd(i0, 2, 7u); }
)"; )";
} // namespace } // namespace
@@ -3464,9 +3491,11 @@ void main() { ssb.sum = imageLoad(i0, ivec2(2, 3)).r + imageLoad(i1, ivec3(1, 1,
EXPECT_NE(essl.find("ivec3(2, 0, 3)"), String::npos) << essl; EXPECT_NE(essl.find("ivec3(2, 0, 3)"), String::npos) << essl;
} }
// Scope, half one: a NON-arrayed 1D storage image is emitted correctly by the very same // Scope, half one: a NON-arrayed 1D storage image that is only READ or WRITTEN is emitted
// SPIRV-Cross code, so the pass must not touch it - replacing working emission with our own buys // correctly by the very same SPIRV-Cross code, so the pass must not touch it - replacing working
// nothing and risks everything. // emission with our own buys nothing and risks everything. (The atomic shape below is the one
// exception, and it is gated on an OpImageTexelPointer actually being present, which is why this
// fixture still passes through byte for byte.)
TEST_F(ProgramUtilTest, Lower1DArrayImagesLeavesNonArrayed1DImagesToSpirvCross) { TEST_F(ProgramUtilTest, Lower1DArrayImagesLeavesNonArrayed1DImagesToSpirvCross) {
using namespace MG_Util::ShaderTranspiler; using namespace MG_Util::ShaderTranspiler;
@@ -3489,6 +3518,94 @@ void main() { ssb.sum = imageLoad(i0, 2).r; }
<< "SPIRV-Cross's own 1D-as-2D emulation must still be what handles this:\n" << essl; << "SPIRV-Cross's own 1D-as-2D emulation must still be what handles this:\n" << essl;
} }
// The negative control for the ATOMIC half, and the reason the non-arrayed case is in scope at
// all: SPIRV-Cross widens a 1D image coordinate in OpImageRead and OpImageWrite but not in
// OpImageTexelPointer, so the atomic comes out addressing an `uimage2D` with a scalar. Every ES
// driver answers "no matching overloaded function found" and the whole stage - with every other
// image in it - is lost. Pinning the upstream behaviour here means a future SPIRV-Cross bump that
// fixes it fails this test instead of leaving the lowering as silent dead weight.
TEST_F(ProgramUtilTest, SpirvCrossEmitsAScalarCoordinateForA1DImageAtomic) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(k1DImageAtomicCompute, GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_EQ(Count1DNonArrayedStorageImageTypes(spirv), 1u)
<< "glslang no longer emits a Dim1D/non-arrayed/Sampled=2 image for uimage1D";
const String essl = DecompileToEssl(spirv);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("uimage2D"), String::npos)
<< "SPIRV-Cross declares the 1D image as 2D on ES; that half it does do:\n" << essl;
EXPECT_NE(essl.find("imageAtomicAdd(i0, 2"), String::npos)
<< "SPIRV-Cross is expected to pass the SCALAR coordinate straight through to the atomic. "
"If this no longer happens, the non-arrayed half of Lower1DArrayImagesForEssl may no "
"longer be needed:\n"
<< essl;
EXPECT_EQ(essl.find("ivec2("), String::npos)
<< "nothing else in this fixture builds an ivec2, so its absence is the defect:\n" << essl;
}
// The fix: the type becomes a plain 2D image - which is what MobileGL stores a GL_TEXTURE_1D in,
// height 1 - and the coordinate becomes (u, 0), so the atomic type-checks against the declaration
// SPIRV-Cross was already emitting.
TEST_F(ProgramUtilTest, Lower1DArrayImagesWidensThe1DAtomicCoordinate) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> raw = BuildSpirvForStage(k1DImageAtomicCompute, GL_COMPUTE_SHADER);
ASSERT_FALSE(raw.empty());
Vector<Uint32> spirv;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
ASSERT_EQ(Count1DNonArrayedStorageImageTypes(spirv), 1u)
<< "the shared chain must leave the 1D image for this pass to handle";
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
ASSERT_FALSE(lowered.empty());
EXPECT_EQ(Count1DNonArrayedStorageImageTypes(lowered), 0u)
<< "no non-arrayed 1D storage image type may survive when an atomic reaches one:\n"
<< DisassembleSpirv(lowered);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "the lowered module must stay validator-clean";
const String essl = DecompileToEssl(lowered);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("uimage2D"), String::npos)
<< "the declaration must still be the 2D one the ES texture is:\n" << essl;
EXPECT_NE(essl.find("imageAtomicAdd(i0, ivec2(2, 0)"), String::npos)
<< "the atomic must address the image with the same (u, 0) SPIRV-Cross writes for a read "
"or a write:\n"
<< essl;
}
// The declined shape for the atomic half, for the same reason as the arrayed one: after the
// rewrite the image is 2D, so imageSize() yields two components where the shader consumes one and
// there is no correct scalar to substitute.
TEST_F(ProgramUtilTest, Lower1DArrayImagesDeclinesA1DAtomicModuleThatQueriesTheImageSize) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 440 core
layout (local_size_x = 1) in;
layout (r32ui) coherent uniform uimage1D i0;
layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
void main() { ssb.sum = imageAtomicAdd(i0, 2, 7u) + uint(imageSize(i0)); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
const auto traits = Lower1DArrayImagesPass::InspectBinary(spirv);
ASSERT_TRUE(traits.declaresImage && traits.queriesImageSize)
<< "the fixture must contain the shape the pass declines";
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
EXPECT_EQ(lowered, spirv) << "a declined module must be handed back untouched, not partly rewritten";
EXPECT_EQ(Count1DNonArrayedStorageImageTypes(lowered), 1u)
<< "declining means the 1D type is still there for the driver to reject";
}
// Scope, half two: a 1D-array SAMPLER reaches SPIRV-Cross's sampler path, which does check // Scope, half two: a 1D-array SAMPLER reaches SPIRV-Cross's sampler path, which does check
// `arrayed` and does move the layer into the third component. The pass is storage-image only. // `arrayed` and does move the layer into the third component. The pass is storage-image only.
TEST_F(ProgramUtilTest, Lower1DArrayImagesLeavesSampledImagesAlone) { TEST_F(ProgramUtilTest, Lower1DArrayImagesLeavesSampledImagesAlone) {
@@ -3840,3 +3957,271 @@ TEST_F(ProgramUtilTest, EsslCoreImageFormatSetIsTheThirteenTheSpecLists) {
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8051 /*GL_RGB8*/)); EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8051 /*GL_RGB8*/));
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0 /*GL_NONE*/)); EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0 /*GL_NONE*/));
} }
// KHR-GL43.shader_storage_buffer_object.basic-syntax iteration 6. glslang assigns a block's member
// offsets at DECLARATION time, where a member array that is still unsized contributes zero bytes -
// so `vec4 position01[]; vec4 position2;` put both members at offset 0 and the shader read
// position01[0] where it asked for position2. The preprocessor sizes the non-final member from the
// largest constant index the source uses, which is what the language says it means.
TEST_F(ProgramUtilTest, ANonFinalUnsizedBufferBlockMemberIsSizedFromItsLargestConstantIndex) {
using namespace MG_Util::ShaderTranspiler;
String source = R"(#version 430 core
layout(packed) coherent buffer Buffer {
vec4 position01[];
vec4 position2;
} g_buffer;
void main() {
if (gl_VertexID == 0) gl_Position = g_buffer.position01[0];
else if (gl_VertexID == 1) gl_Position = g_buffer.position01[1];
else if (gl_VertexID == 2) gl_Position = g_buffer.position2;
}
)";
PreprocessShaderSource(ShaderStage::Vertex, source);
EXPECT_NE(source.find("vec4 position01[2];"), String::npos) << source;
EXPECT_EQ(source.find("position01[];"), String::npos) << source;
// The LAST member of a storage block is a run-time sized array, which is legal and already
// laid out correctly - sizing it would be a wire-format change, not a repair.
String lastMember = R"(#version 430 core
buffer Buffer {
vec4 head;
vec4 tail[];
} g_buffer;
void main() {
gl_Position = g_buffer.tail[0] + g_buffer.tail[3];
}
)";
PreprocessShaderSource(ShaderStage::Vertex, lastMember);
EXPECT_NE(lastMember.find("vec4 tail[];"), String::npos) << lastMember;
// A member the shader subscripts with anything but a literal cannot be sized from the source,
// so it is left exactly as it was.
String dynamicIndex = R"(#version 430 core
buffer Buffer {
vec4 head[];
vec4 tail;
} g_buffer;
uniform int g_index;
void main() {
gl_Position = g_buffer.head[g_index] + g_buffer.tail;
}
)";
PreprocessShaderSource(ShaderStage::Vertex, dynamicIndex);
EXPECT_NE(dynamicIndex.find("vec4 head[];"), String::npos) << dynamicIndex;
// `buffer` is also a member memory qualifier; a declaration that uses it must not be mistaken
// for a block header.
String memberQualifier = R"(#version 430 core
coherent buffer Buffer {
buffer vec4 position0;
vec4 position1[];
vec4 position2;
} g_buffer;
void main() {
gl_Position = g_buffer.position0 + g_buffer.position1[2] + g_buffer.position2;
}
)";
PreprocessShaderSource(ShaderStage::Vertex, memberQualifier);
EXPECT_NE(memberQualifier.find("vec4 position1[3];"), String::npos) << memberQualifier;
}
// KHR-GL43.shader_storage_buffer_object.negative-glsl-compileTime: a storage block declared at
// GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS must fail to compile, and so must an arrayed one whose
// LAST element passes the ceiling. The relaxed Vulkan-rules parse enforces neither.
TEST_F(ProgramUtilTest, StorageBlockBindingCeilingIsCheckedAtItsExactBoundary) {
using namespace MG_Util::ShaderTranspiler;
constexpr Int kMaxBindings = 36;
const auto violation = [](const String& body) {
return FindShaderStorageBindingViolation("#version 430 core\n" + body + "void main() {}\n", kMaxBindings);
};
// The boundary itself: max - 1 is the last legal point, max is one past it.
EXPECT_FALSE(violation("layout(binding = 35) buffer Buffer { int x; };\n").has_value());
EXPECT_TRUE(violation("layout(binding = 36) buffer Buffer { int x; };\n").has_value());
// An instance array takes CONSECUTIVE points, so what has to fit is base + count - 1.
EXPECT_FALSE(violation("layout(binding = 32) buffer Buffer { int x; } g_array[4];\n").has_value());
EXPECT_TRUE(violation("layout(binding = 34) buffer Buffer { int x; } g_array[4];\n").has_value());
// Qualifiers and a second layout list may sit between the binding and the keyword.
EXPECT_TRUE(violation("layout(std430) layout(binding = 36) coherent restrict buffer B { int x; };\n")
.has_value());
// Things the scanner must NOT judge: a uniform block (a different ceiling), a storage block
// with no explicit binding, the bare default-qualifier form, and an instance array whose size
// is not a literal.
EXPECT_FALSE(violation("layout(binding = 40) uniform Block { int x; };\n"
"layout(binding = 0) buffer Buffer { int y; };\n")
.has_value());
EXPECT_FALSE(violation("buffer Buffer { int x; };\nconst int binding = 40;\n").has_value());
EXPECT_FALSE(violation("layout(binding = 1) buffer;\nbuffer Buffer { int x; };\n").has_value());
EXPECT_FALSE(violation("const int kCount = 4;\nlayout(binding = 34) buffer B { int x; } g[kCount];\n")
.has_value());
// A backend that advertises no binding points has no ceiling to enforce.
EXPECT_FALSE(FindShaderStorageBindingViolation("layout(binding = 36) buffer B { int x; };\n", 0).has_value());
}
// KHR-GL43.shader_atomic_counters.negative-offset-1: an atomic counter whose layout(offset = N)
// puts its last byte past GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE is a COMPILE-time error, and the CTS
// never links the shader at all. MobileGL only had the rule at link, because the Vulkan-relaxed
// parse never reaches glslang's fixOffset().
TEST_F(ProgramUtilTest, AtomicCounterOffsetCeilingIsCheckedAtCompile) {
using namespace MG_Util::ShaderTranspiler;
const auto violation = [](const String& body) {
return FindAtomicCounterOffsetViolation("#version 430 core\n" + body + "void main() {}\n");
};
const String maxSize = std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE);
const String lastLegal = std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 4);
// The boundary itself: the last counter that still fits, and the first that does not.
EXPECT_FALSE(violation("layout(binding = 0, offset = " + lastLegal + ") uniform atomic_uint c;\n").has_value());
EXPECT_TRUE(violation("layout(binding = 0, offset = " + maxSize + ") uniform atomic_uint c;\n").has_value());
// An array occupies one word per element, so what has to fit is the LAST one.
EXPECT_FALSE(violation("layout(offset = " + std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 16) +
") uniform atomic_uint c[4];\n")
.has_value());
EXPECT_TRUE(violation("layout(offset = " + std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 8) +
") uniform atomic_uint c[4];\n")
.has_value());
// An offset that is not a multiple of 4 (GL 4.6 core 7.7), and one that is.
EXPECT_TRUE(violation("layout(offset = 2) uniform atomic_uint c;\n").has_value());
EXPECT_FALSE(violation("layout(offset = 8) uniform atomic_uint c;\n").has_value());
// Things the scanner must NOT judge: a counter with no explicit offset, an `offset` that is
// an ordinary identifier rather than a layout qualifier, an array sized by an expression,
// and an offset qualifier that belongs to a different declaration.
EXPECT_FALSE(violation("uniform atomic_uint c;\nconst int offset = 99999;\n").has_value());
EXPECT_FALSE(violation("const int kCount = 4;\nlayout(offset = " + maxSize +
") uniform atomic_uint c[kCount];\n")
.has_value());
EXPECT_FALSE(violation("layout(offset = " + maxSize + ") uniform Block { int x; };\n"
"uniform atomic_uint c;\n")
.has_value());
// A source with no counter at all never pays for the scan and never reports one.
EXPECT_FALSE(FindAtomicCounterOffsetViolation("#version 430 core\nvoid main() {}\n").has_value());
}
// KHR-GL43.explicit_uniform_location.uniform-loc-nondecimal: GLSL integer literals are C-style, so
// layout(location = 0xA) is 10 and layout(location = 010) is OCTAL 8. The extractor used to accept
// a base-10 digit run and nothing else: the hex spelling failed the test entirely and the
// declaration silently lost its explicit location, while the octal one was read as decimal 10.
// The identical defect sat on every array dimension and on layout(binding = N).
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsReadsNonDecimalIntegerLiterals) {
using namespace MG_Util::ShaderTranspiler;
const String source = R"(#version 430 core
layout(location = 0xA) uniform vec4 hexLower;
layout(location = 0X1f) uniform vec4 hexUpper;
layout(location = 010) uniform vec4 octal;
layout(location = 3u) uniform vec4 unsignedSuffix;
layout(location = 0x2) uniform float hexArray[0x3];
layout(location = 1.0) uniform vec4 notAnInteger;
layout(location = 7f) uniform vec4 unknownSuffix;
void main() {}
)";
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(source);
ASSERT_EQ(locations.count("hexLower"), 1u);
EXPECT_EQ(locations.at("hexLower"), 10);
ASSERT_EQ(locations.count("hexUpper"), 1u);
EXPECT_EQ(locations.at("hexUpper"), 31);
ASSERT_EQ(locations.count("octal"), 1u);
EXPECT_EQ(locations.at("octal"), 8) << "a leading zero is octal in GLSL, not decimal";
ASSERT_EQ(locations.count("unsignedSuffix"), 1u);
EXPECT_EQ(locations.at("unsignedSuffix"), 3);
ASSERT_EQ(locations.count("hexArray"), 1u);
EXPECT_EQ(locations.at("hexArray"), 2);
// Still never guessed at: a float and an unknown suffix are skipped, not rounded.
EXPECT_EQ(locations.count("notAnInteger"), 0u);
EXPECT_EQ(locations.count("unknownSuffix"), 0u);
}
// A hexadecimal array dimension has to size the declarator's span too, or the declarator after it
// in the same statement starts at the wrong location.
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsSpansANonDecimalArrayDimension) {
using namespace MG_Util::ShaderTranspiler;
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(
"#version 430 core\nlayout(location = 50) uniform float first[0x3], second;\nvoid main() {}\n");
ASSERT_EQ(locations.count("first"), 1u);
EXPECT_EQ(locations.at("first"), 50);
ASSERT_EQ(locations.count("second"), 1u);
EXPECT_EQ(locations.at("second"), 53) << "0x3 is three elements, not zero and not three hundred";
}
// KHR-GL43.explicit_uniform_location.uniform-loc-array-of-arrays: glslang reflects
// `float u[2][3]` as "u[0][0]" and "u[1][0]", and the linker resolves such a name by stripping the
// single trailing "[0]" - so the map has to answer "u[1]", not just "u". Without the pre-flattened
// keys both records missed the map entirely and were first-fitted from location 0.
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsExpandsArrayOfArraysElements) {
using namespace MG_Util::ShaderTranspiler;
const String source = R"(#version 430 core
layout(location = 2) uniform float two_d[2][3];
layout(location = 20) uniform float three_d[2][2][4];
layout(location = 40) uniform float one_d[3];
void main() {}
)";
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(source);
// The root entry is unchanged - the synthesized keys are additional, never a replacement.
ASSERT_EQ(locations.count("two_d"), 1u);
EXPECT_EQ(locations.at("two_d"), 2);
// One key per outer index, each starting a run of the innermost dimension (3 here).
ASSERT_EQ(locations.count("two_d[0]"), 1u);
EXPECT_EQ(locations.at("two_d[0]"), 2);
ASSERT_EQ(locations.count("two_d[1]"), 1u);
EXPECT_EQ(locations.at("two_d[1]"), 5);
// Three dimensions: glslang expands all but the innermost, so both outer indices are spelled.
ASSERT_EQ(locations.count("three_d"), 1u);
EXPECT_EQ(locations.at("three_d"), 20);
ASSERT_EQ(locations.count("three_d[0][0]"), 1u);
EXPECT_EQ(locations.at("three_d[0][0]"), 20);
ASSERT_EQ(locations.count("three_d[0][1]"), 1u);
EXPECT_EQ(locations.at("three_d[0][1]"), 24);
ASSERT_EQ(locations.count("three_d[1][0]"), 1u);
EXPECT_EQ(locations.at("three_d[1][0]"), 28);
ASSERT_EQ(locations.count("three_d[1][1]"), 1u);
EXPECT_EQ(locations.at("three_d[1][1]"), 32);
// A 1-D array needs no expansion: stripping "[0]" already reaches the root.
ASSERT_EQ(locations.count("one_d"), 1u);
EXPECT_EQ(locations.at("one_d"), 40);
EXPECT_EQ(locations.count("one_d[0]"), 0u);
// The declarator after an array-of-arrays still advances by the WHOLE element count.
const UnorderedMap<String, Int> pair = ExtractExplicitUniformLocations(
"#version 430 core\nlayout(location = 0) uniform float a[2][3], b;\nvoid main() {}\n");
ASSERT_EQ(pair.count("b"), 1u);
EXPECT_EQ(pair.at("b"), 6);
}
// KHR-GL43.explicit_uniform_location: layout(binding = 0x2) on a sampler is the same literal defect
// as the location one, and losing it costs the sampler its initial texture unit.
TEST_F(ProgramUtilTest, ExtractExplicitOpaqueBindingsReadsNonDecimalIntegerLiterals) {
using namespace MG_Util::ShaderTranspiler;
const String source = R"(#version 430 core
layout(binding = 0x2) uniform sampler2D hexUnit;
layout(binding = 012) uniform sampler2D octalUnit;
layout(binding = 1u) uniform sampler2D suffixedUnit;
void main() {}
)";
const UnorderedMap<String, Uint> bindings = ExtractExplicitOpaqueBindings(source);
ASSERT_EQ(bindings.count("hexUnit"), 1u);
EXPECT_EQ(bindings.at("hexUnit"), 2u);
ASSERT_EQ(bindings.count("octalUnit"), 1u);
EXPECT_EQ(bindings.at("octalUnit"), 10u) << "012 is octal ten, not twelve";
ASSERT_EQ(bindings.count("suffixedUnit"), 1u);
EXPECT_EQ(bindings.at("suffixedUnit"), 1u);
}
+348
View File
@@ -19,6 +19,7 @@
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h> #include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/Query/GL_Query.h> #include <MG_Impl/GLImpl/Query/GL_Query.h>
#include <MG_State/GLState/Core.h>
using namespace MobileGL; using namespace MobileGL;
@@ -119,6 +120,79 @@ namespace {
g_stubResultObtainable = true; g_stubResultObtainable = true;
g_stubResultNs = 0; 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<MG_Backend::BackendQueryHandle>(static_cast<uintptr_t>(0x53));
}
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<MG_Backend::BackendQueryHandle>(static_cast<uintptr_t>(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;
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 } // namespace
class QueryTest : public ::testing::Test { class QueryTest : public ::testing::Test {
@@ -448,12 +522,286 @@ TEST_F(QueryTest, BackendResultsPropagateThroughFrontend) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); 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 // Environment-agnostic property test for the env -> ConfigLoader -> Features
// chain: whatever MOBILEGL_DISABLE_TIMERQUERY is set to in the environment of // chain: whatever MOBILEGL_DISABLE_TIMERQUERY is set to in the environment of
// this test process, MG_ConfigLoader::Init must have parsed it with the // this test process, MG_ConfigLoader::Init must have parsed it with the
// unified truthy rule (set, non-empty, not "0", case-insensitive not "false"). // unified truthy rule (set, non-empty, not "0", case-insensitive not "false").
// Running the binary under MOBILEGL_DISABLE_TIMERQUERY=1 therefore exercises // Running the binary under MOBILEGL_DISABLE_TIMERQUERY=1 therefore exercises
// the real end-to-end path rather than the struct field alone. // 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) { TEST_F(QueryTest, DisableTimerQueryFeatureMatchesEnvironment) {
const char* raw = std::getenv("MOBILEGL_DISABLE_TIMERQUERY"); const char* raw = std::getenv("MOBILEGL_DISABLE_TIMERQUERY");
Bool expected = false; Bool expected = false;
+221
View File
@@ -17,6 +17,7 @@
#include <MG_Backend/DirectGLES/Managers.h> #include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h> #include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h> #include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h> #include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
#include <MG_Impl/GLImpl/Texture/GL_Texture.h> #include <MG_Impl/GLImpl/Texture/GL_Texture.h>
@@ -365,6 +366,13 @@ TEST(DirectGLESSanity, RebasesInstanceIdWhenIndirectDrawsLeakBaseInstance) {
// MaxShaderStorageBufferBindings - 1 = 12, so a regression that stops reading the // MaxShaderStorageBufferBindings - 1 = 12, so a regression that stops reading the
// probed cap and falls back to the struct default would surface as "binding = 7". // probed cap and falls back to the struct default would surface as "binding = 7".
caps.MaxShaderStorageBufferBindings = 13; caps.MaxShaderStorageBufferBindings = 13;
// The indirect lowering reads its baseInstance through a storage block declared in the
// VERTEX stage, which is optional in both APIs and which the GLESCapabilities default
// (0, the spec minimum) therefore denies. This suite is pinning the shape of that
// lowering, so it has to describe a driver that can actually have it - see
// VertexStageStorageBlockUsable and the BaseInstanceInjectionGate suite for the
// zero case.
caps.MaxVertexShaderStorageBlocks = 1;
const MobileGL::String source = R"(#version 310 es const MobileGL::String source = R"(#version 310 es
highp int mg_BaseInstanceLowered; highp int mg_BaseInstanceLowered;
@@ -403,6 +411,9 @@ TEST(DirectGLESSanity, TheIndirectWordIndexIsOneBasedSoItsUnwrittenValueMeansNot
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities; auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
caps.IndirectDrawInstanceIdIncludesBaseInstance = false; caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
caps.MaxShaderStorageBufferBindings = 13; caps.MaxShaderStorageBufferBindings = 13;
// See RebasesInstanceIdWhenIndirectDrawsLeakBaseInstance: without a vertex-stage
// storage block there is no word index to be one-based about.
caps.MaxVertexShaderStorageBlocks = 1;
const MobileGL::String source = R"(#version 310 es const MobileGL::String source = R"(#version 310 es
highp int mg_BaseInstanceLowered; highp int mg_BaseInstanceLowered;
@@ -428,6 +439,10 @@ TEST(DirectGLESSanity, KeepsInstanceIdWhenIndirectDrawsAreConforming) {
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities; auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
caps.IndirectDrawInstanceIdIncludesBaseInstance = false; caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
caps.MaxShaderStorageBufferBindings = 13; caps.MaxShaderStorageBufferBindings = 13;
// Set explicitly even though the assertions below would also hold on the degraded path:
// this case is about a CONFORMING driver leaving gl_InstanceID alone, and it would be a
// silent weakening for it to be exercising the no-storage-block fallback instead.
caps.MaxVertexShaderStorageBlocks = 1;
const MobileGL::String source = R"(#version 310 es const MobileGL::String source = R"(#version 310 es
highp int mg_BaseInstanceLowered; highp int mg_BaseInstanceLowered;
@@ -442,6 +457,8 @@ void main() {
EXPECT_EQ(rewritten.find("mg_ZeroBasedInstanceID"), MobileGL::String::npos); EXPECT_EQ(rewritten.find("mg_ZeroBasedInstanceID"), MobileGL::String::npos);
EXPECT_NE(rewritten.find("int instance = gl_InstanceID + mg_BaseInstanceLowered;"), MobileGL::String::npos); EXPECT_NE(rewritten.find("int instance = gl_InstanceID + mg_BaseInstanceLowered;"), MobileGL::String::npos);
// The indirect view is present on this driver, so the fallback must NOT have fired.
EXPECT_NE(rewritten.find("buffer mg_IndirectParams"), MobileGL::String::npos);
} }
TEST(DirectGLESSanity, LeavesDrawParameterGlobalsAloneOutsideVertexShaders) { TEST(DirectGLESSanity, LeavesDrawParameterGlobalsAloneOutsideVertexShaders) {
@@ -626,6 +643,55 @@ TEST(DirectGLESSanity, PreservesHostPerStageImageUniformLimits) {
EXPECT_EQ(params.MaxComputeImageUniforms, 5); EXPECT_EQ(params.MaxComputeImageUniforms, 5);
} }
// maxClipDistances is a LIMIT every Vulkan device reports; declaring ClipDistance in a module
// needs the shaderClipDistance FEATURE, which is separate and which VulkanRenderer enables only
// where the physical device has it. Forwarding the limit without the feature advertises eight
// clip planes no shader may use - the same shape as the image-uniform limits above, and the same
// shape as the GL_EXT_clip_cull_distance lie on DirectGLES. Not a blanket zero: a device WITH the
// feature keeps its real number.
TEST(DirectVulkanSanity, GatesClipDistancesOnTheShaderClipDistanceFeature) {
using namespace MobileGL;
MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
MG_External::VulkanCapabilities caps;
caps.MaxClipDistances = 8;
caps.SupportsShaderClipDistance = false;
backend.ApplyVulkanCapabilitiesForTesting(caps);
EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 0);
caps.SupportsShaderClipDistance = true;
backend.ApplyVulkanCapabilitiesForTesting(caps);
EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8);
}
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX were a hard-coded
// GL_LAST_VERTEX_CONVENTION for both backends, derived from nothing, and wrong on both test
// devices in opposite directions. DirectGLES now forwards what its loader resolved; DirectVulkan
// reports GL_UNDEFINED_VERTEX, which GL 4.6 table 23.65 permits and which is what the backend
// honestly implements - the provoking mode is chosen per pipeline out of VK_EXT_provoking_vertex,
// provokingVertexModePerPipeline and the topology.
TEST(ProvokingVertexConventions, EachBackendReportsWhatItActuallyPins) {
using namespace MobileGL;
MG_Backend::DirectGLES::BackendObject_DirectGLES glesBackend;
MG_External::GLESCapabilities glesCaps;
glesCaps.LayerProvokingVertex = GL_FIRST_VERTEX_CONVENTION;
glesCaps.ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
glesBackend.ApplyGLESCapabilitiesForTesting(glesCaps);
EXPECT_EQ(glesBackend.GetDynamicParameters().LayerProvokingVertex,
static_cast<GLenum>(GL_FIRST_VERTEX_CONVENTION));
EXPECT_EQ(glesBackend.GetDynamicParameters().ViewportIndexProvokingVertex,
static_cast<GLenum>(GL_UNDEFINED_VERTEX));
MG_Backend::DirectVulkan::BackendObject_DirectVulkan vkBackend;
MG_External::VulkanCapabilities vkCaps;
vkBackend.ApplyVulkanCapabilitiesForTesting(vkCaps);
EXPECT_EQ(vkBackend.GetDynamicParameters().LayerProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
EXPECT_EQ(vkBackend.GetDynamicParameters().ViewportIndexProvokingVertex,
static_cast<GLenum>(GL_UNDEFINED_VERTEX));
}
TEST(FragmentInterpolationCapabilities, PlumbsGLESAndBothVulkanPropertyPaths) { TEST(FragmentInterpolationCapabilities, PlumbsGLESAndBothVulkanPropertyPaths) {
using namespace MobileGL; using namespace MobileGL;
@@ -913,6 +979,161 @@ void main() {
MG_Backend::pActiveBackendObject.reset(); MG_Backend::pActiveBackendObject.reset();
} }
// KHR-GL43.shader_atomic_counters.basic-glsl-built-in, .basic-buffer-bind and .basic-api-get.
// The atomic-counter limits used to live in two unreconciled tables - glslang compiled every
// shader against ONE binding while glGetIntegerv advertised thirty-six - and three of the enums
// had no case in the getter at all, so the query raised INVALID_ENUM and left the caller reading
// whatever was in its own stack slot.
TEST(GetterSanity, AtomicCounterQueriesMatchShaderCompilerLimits) {
using namespace MobileGL;
namespace Transpiler = MG_Util::ShaderTranspiler;
auto previousContext = Move(MG_State::pGLContext);
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(MG_Backend::DynamicBackendParameters{});
GLint reported = -1;
MG_Impl::GLImpl::GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, &reported);
EXPECT_EQ(reported, static_cast<GLint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS));
MG_Impl::GLImpl::GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, &reported);
EXPECT_EQ(reported, static_cast<GLint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE));
for (const GLenum pname : {GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS, GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS,
GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS}) {
reported = -1;
MG_Impl::GLImpl::GetIntegerv(pname, &reported);
EXPECT_EQ(reported, static_cast<GLint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE))
<< "pname " << pname;
}
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// glBindBufferBase sets the GENERIC binding point too (GL 4.6 6.1.1), and this is the one
// indexed-buffer family whose non-indexed query had no case.
reported = -1;
MG_Impl::GLImpl::GetIntegerv(GL_ATOMIC_COUNTER_BUFFER_BINDING, &reported);
EXPECT_EQ(reported, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLuint buffer = 0;
MG_Impl::GLImpl::GenBuffers(1, &buffer);
MG_Impl::GLImpl::BindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer);
MG_Impl::GLImpl::BufferData(GL_ATOMIC_COUNTER_BUFFER, 64, nullptr, GL_STATIC_DRAW);
MG_Impl::GLImpl::BindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 2, buffer);
MG_Impl::GLImpl::GetIntegerv(GL_ATOMIC_COUNTER_BUFFER_BINDING, &reported);
EXPECT_EQ(static_cast<GLuint>(reported), buffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The advertised ceiling is also the one glBindBufferBase and the indexed getter enforce.
// A limit nothing validates against is how these tables drifted apart in the first place:
// the binding-point ARRAY is 36 deep, and it used to be that number an application saw.
constexpr GLuint pastLastBinding = static_cast<GLuint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS);
MG_Impl::GLImpl::BindBufferBase(GL_ATOMIC_COUNTER_BUFFER, pastLastBinding, buffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_VALUE));
MG_Impl::GLImpl::GetIntegeri_v(GL_ATOMIC_COUNTER_BUFFER_BINDING, pastLastBinding, &reported);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_VALUE));
// ...and the shading language has to expand the same numbers. Each array is sized by a
// built-in constant and indexed at its last element with a literal, so the stage only
// compiles when that constant is at least what glGetIntegerv just reported - which it was
// not while the resource table said one.
const String lastBinding = std::to_string(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS - 1);
const String lastBuffer = std::to_string(Transpiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE - 1);
const String source = R"(#version 430 core
out vec4 color;
int mgBindings[gl_MaxAtomicCounterBindings];
int mgCombinedBuffers[gl_MaxCombinedAtomicCounterBuffers];
int mgFragmentBuffers[gl_MaxFragmentAtomicCounterBuffers];
layout(binding = )" + lastBinding + R"(, offset = 0) uniform atomic_uint mgCounter;
void main() {
color = vec4(float(mgBindings[)" + lastBinding + R"(] + mgCombinedBuffers[)" + lastBuffer +
R"(] + mgFragmentBuffers[)" + lastBuffer + R"(] + int(atomicCounterIncrement(mgCounter))));
}
)";
auto compiled = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_FRAGMENT_SHADER,
.sourceStr = source,
});
EXPECT_TRUE(compiled) << (compiled ? "" : compiled.error().log);
MG_Backend::pActiveBackendObject = Move(previousBackend);
MG_State::pGLContext = Move(previousContext);
}
// KHR-GL43.compute_shader.max: the test queries every GL_MAX_COMPUTE_* value through the API and
// then makes a compute shader compare the matching gl_MaxCompute* constant against it. The two
// used to be independent tables and gl_MaxComputeWorkGroupSize.z disagreed - glslang compiled
// against a permissive 1024 while the context advertises the 64 the GL 4.6 minimum (and every ES
// driver) reports.
TEST(GetterSanity, ComputeWorkGroupQueriesMatchShaderCompilerLimits) {
using namespace MobileGL;
auto previousContext = Move(MG_State::pGLContext);
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(MG_Backend::DynamicBackendParameters{});
GLint size[3] = {0, 0, 0};
GLint count[3] = {0, 0, 0};
for (GLuint index = 0; index < 3; ++index) {
MG_Impl::GLImpl::GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index, &size[index]);
MG_Impl::GLImpl::GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, index, &count[index]);
}
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The compile runs against a captured env, exactly as the pipeline's does. That is the whole
// invariant: the env holds the same floored driver answer GetIntegeri_v just returned, so the
// resource table and the query agree BY CONSTRUCTION rather than by two tables happening to
// carry the same literals.
const auto env = MG_Util::ShaderTranspiler::CaptureCompileEnv();
for (GLuint index = 0; index < 3; ++index) {
EXPECT_EQ(static_cast<GLint>(env->maxComputeWorkGroupSize[index]), size[index]) << "index " << index;
EXPECT_EQ(static_cast<GLint>(env->maxComputeWorkGroupCount[index]), count[index]) << "index " << index;
}
// A negative array size is a compile error, so the stage only compiles when EVERY component
// of both built-in constants equals what the query above reported. Two-sided by construction:
// a resource table that is too permissive fails it exactly like one that is too tight.
const String source = R"(#version 430 core
layout(local_size_x = 1) in;
const int mgAgree = (gl_MaxComputeWorkGroupSize == ivec3()" +
std::to_string(size[0]) + ", " + std::to_string(size[1]) + ", " +
std::to_string(size[2]) + R"() &&
gl_MaxComputeWorkGroupCount == ivec3()" +
std::to_string(count[0]) + ", " + std::to_string(count[1]) + ", " +
std::to_string(count[2]) + R"()) ? 1 : -1;
int mgProbe[mgAgree];
void main() {
mgProbe[0] = 0;
}
)";
auto compiled = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_COMPUTE_SHADER,
.sourceStr = source,
.env = env.get(),
});
EXPECT_TRUE(compiled) << (compiled ? "" : compiled.error().log);
// The z ceiling is also what glslang checks a declared local_size_z against, so it has to
// reject one invocation past the advertised limit and accept the limit itself.
const String atLimit = "#version 430 core\nlayout(local_size_z = " + std::to_string(size[2]) +
") in;\nvoid main() {}\n";
const String pastLimit = "#version 430 core\nlayout(local_size_z = " + std::to_string(size[2] + 1) +
") in;\nvoid main() {}\n";
EXPECT_TRUE(MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_COMPUTE_SHADER,
.sourceStr = atLimit,
.env = env.get(),
}));
EXPECT_FALSE(MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_COMPUTE_SHADER,
.sourceStr = pastLimit,
.env = env.get(),
}));
MG_Backend::pActiveBackendObject = Move(previousBackend);
MG_State::pGLContext = Move(previousContext);
}
TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) { TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) {
using namespace MobileGL; using namespace MobileGL;
@@ -9,6 +9,11 @@ add_executable(
EmulateSubgroupsTest.cpp EmulateSubgroupsTest.cpp
DemoteFloat64Test.cpp DemoteFloat64Test.cpp
FlattenXfbInterfaceBlocksTest.cpp FlattenXfbInterfaceBlocksTest.cpp
UniquifyIoBlockNamesTest.cpp
LowerViewportIndexTest.cpp
ClampMultisampleFetchTest.cpp
LegalizeStorageBlockArrayIndexTest.cpp
FlattenAtomicCounterBlockTest.cpp
) )
target_include_directories(SpirvPassTest PRIVATE target_include_directories(SpirvPassTest PRIVATE
@@ -27,5 +32,33 @@ if (MSVC)
target_compile_options(SpirvPassTest PRIVATE /Zc:preprocessor) target_compile_options(SpirvPassTest PRIVATE /Zc:preprocessor)
endif() endif()
# Its own binary: several cases flip MG_Config::Features.ShaderTranslationCache and
# MG_Config::Features.AsyncShaderCompile and read the PROCESS-GLOBAL L1 counters straight
# afterwards, so another suite's links running in the same process would show up as
# hits/misses this one did not cause.
add_executable(
TranslationCacheTest
TranslationCacheTest.cpp
)
target_include_directories(TranslationCacheTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(
TranslationCacheTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
if (MSVC)
target_compile_options(TranslationCacheTest PRIVATE /Zc:preprocessor)
endif()
include(GoogleTest) include(GoogleTest)
gtest_discover_tests(SpirvPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) gtest_discover_tests(SpirvPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
# Heavier than a pure-pass suite: the concurrency cases run 8 threads over thousands of
# rounds and the end-to-end cases link real programs through the compile pool.
gtest_discover_tests(TranslationCacheTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
@@ -0,0 +1,383 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/ClampMultisampleFetchTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// ClampMultisampleFetchPass exists because MobileGL advertises one multisample ceiling and the ES
// driver underneath delivers another. GL 4.6 core table 23.53 forces GL_MAX_SAMPLES and
// GL_MAX_INTEGER_SAMPLES up to 4; Adreno and Mali back an integer multisample texture with ONE
// sample, and DirectGLES quietly allocates that (ClampSamplesToBackendSupport). A CTS shader that
// bakes in `texelFetch(usampler2DMS, coord, 3)` - which is what
// KHR-GL33/40/41.texture_swizzle.functional_* and KHR-GLxx.texture_size_promotion.functional do -
// then reads a sample the storage does not have.
//
// So what has to hold is per-fetch and per-category at once: the squeezed category's Sample
// operand must come back in range, a category that is not squeezed must be untouched, a module
// with no multisampled image at all must come out byte for byte as it went in, and every result
// must still be a valid module. Real GLSL through the same glslang path the backends use, for the
// same reason LowerViewportIndexTest.cpp does it: what matters is what glslang actually emits.
#include <gtest/gtest.h>
#define SPV_ENABLE_UTILITY_CODE
#include "glslang/SPIRV/spirv.hpp11"
#undef SPV_ENABLE_UTILITY_CODE
#include "Includes.h"
#include "Init.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
#include <map>
#include <string>
#include <vector>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
// GLSL.std.450 instruction number (see 3rdparty/glslang/SPIRV/GLSL.std.450.h). The signed
// minimum, which is what a GLSL `int` sample index asks for.
constexpr Uint32 kGlslStd450SMin = 39u;
// What MobileGL tells the application GL_MAX_SAMPLES / GL_MAX_INTEGER_SAMPLES are, i.e.
// GL_Getter's kFrontendMaxSamples floor. Each test supplies its own backend-real ceilings
// against it; Adreno and Mali's Immortalis-G925 both really answer 1 for integer formats.
constexpr Int32 kAdvertisedMaxSamples = 4;
constexpr SizeT kSpirvHeaderWordCount = 5u;
template <typename Visitor>
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
const Uint32 wordCount = spirv[offset] >> 16u;
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
offset += wordCount;
}
}
Vector<Uint32> CompileFragment(const String& source) {
using namespace MobileGL::MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER},
.program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
String Disassemble(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
String text;
tools.Disassemble(spirv, &text);
return text;
}
bool Validates(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
tools.SetMessageConsumer(
[](spv_message_level_t, const char*, const spv_position_t& position, const char* message) {
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
});
return tools.Validate(spirv);
}
// OpImageFetch words: 0 opcode/count, 1 result type, 2 result id, 3 image, 4 coordinate,
// 5 the optional image-operands mask, 6.. the ids that mask asks for.
struct ImageFetch {
Uint32 resultId = 0u;
Uint32 imageId = 0u;
Uint32 mask = 0u;
Vector<Uint32> maskOperandIds;
};
Vector<ImageFetch> CollectImageFetches(const Vector<Uint32>& spirv) {
Vector<ImageFetch> fetches;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != spv::Op::OpImageFetch || wordCount < 5u) return;
ImageFetch fetch{};
fetch.resultId = words[2];
fetch.imageId = words[3];
if (wordCount > 5u) {
fetch.mask = words[5];
for (Uint32 word = 6u; word < wordCount; ++word) {
fetch.maskOperandIds.push_back(words[word]);
}
}
fetches.push_back(fetch);
});
return fetches;
}
// OpExtInst words: 0 opcode/count, 1 result type, 2 result id, 3 set, 4 instruction number,
// 5.. the operand ids.
struct ExtInst {
Uint32 resultId = 0u;
Uint32 instructionNumber = 0u;
Vector<Uint32> operandIds;
};
Vector<ExtInst> CollectExtInsts(const Vector<Uint32>& spirv) {
Vector<ExtInst> extInsts;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != spv::Op::OpExtInst || wordCount < 5u) return;
ExtInst extInst{};
extInst.resultId = words[2];
extInst.instructionNumber = words[4];
for (Uint32 word = 5u; word < wordCount; ++word) {
extInst.operandIds.push_back(words[word]);
}
extInsts.push_back(extInst);
});
return extInsts;
}
std::map<Uint32, Uint32> CollectScalarConstants(const Vector<Uint32>& spirv) {
std::map<Uint32, Uint32> values;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == spv::Op::OpConstant && wordCount == 4u) values[words[2]] = words[3];
});
return values;
}
// The one fetch carrying an explicit Sample operand. glslang emits Sample on its own for a
// multisample texelFetch - there is no texelFetchOffset for a multisampled sampler - so the
// sample id is the mask's first and only operand.
const ImageFetch* FindSampleCarryingFetch(const Vector<ImageFetch>& fetches) {
for (const ImageFetch& fetch : fetches) {
if ((fetch.mask & static_cast<Uint32>(spv::ImageOperandsMask::Sample)) != 0u) {
return &fetch;
}
}
return nullptr;
}
const ImageFetch* FindLodCarryingFetch(const Vector<ImageFetch>& fetches) {
for (const ImageFetch& fetch : fetches) {
if ((fetch.mask & static_cast<Uint32>(spv::ImageOperandsMask::Lod)) != 0u) {
return &fetch;
}
}
return nullptr;
}
// KHR-GL4x.texture_swizzle.functional's integer multisample read in miniature: the sample
// index is the advertised GL_MAX_INTEGER_SAMPLES - 1, baked in as a literal, which is exactly
// the value the one-sample allocation underneath cannot answer. The plain sampler2D fetch is
// the negative control - a NON-multisampled image whose Lod operand this pass must not touch.
const char* const kIntegerMultisampleFetch = R"(#version 410 core
uniform usampler2DMS uintMs;
uniform sampler2D plain;
out vec4 fragColor;
void main() {
uvec4 texel = texelFetch(uintMs, ivec2(gl_FragCoord.xy), 3);
vec4 other = texelFetch(plain, ivec2(gl_FragCoord.xy), 0);
fragColor = vec4(texel) * 0.5 + other;
}
)";
// The colour class, which real devices squeeze to something above 1 rather than to 1.
const char* const kColorMultisampleFetch = R"(#version 410 core
uniform sampler2DMS colorMs;
out vec4 fragColor;
void main() {
fragColor = texelFetch(colorMs, ivec2(gl_FragCoord.xy), 3);
}
)";
// Every stage on a squeezed device goes through the probe, so the one that declares no
// multisampled image has to come back untouched.
const char* const kNoMultisampleFetch = R"(#version 410 core
uniform sampler2D plain;
out vec4 fragColor;
void main() {
fragColor = texelFetch(plain, ivec2(gl_FragCoord.xy), 0);
}
)";
} // namespace
class ClampMultisampleFetchTest : public ::testing::Test {
protected:
void SetUp() override {
MobileGL::Initialize();
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
}
void TearDown() override {
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
<< "the clamped module did not survive spirv-val";
}
Uint64 m_validationFailuresAtStart = 0;
};
// The probe is the gate that keeps every ordinary stage off an optimizer round trip, so it has to
// answer no for a shader that never reads a multisample texture - and yes for the ones that do.
TEST_F(ClampMultisampleFetchTest, TheProbeAnswersOnlyForAMultisampledImage) {
const Vector<Uint32> plain = CompileFragment(kNoMultisampleFetch);
ASSERT_FALSE(plain.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresMultisampledImage(plain));
const Vector<Uint32> integerMs = CompileFragment(kIntegerMultisampleFetch);
ASSERT_FALSE(integerMs.empty());
EXPECT_TRUE(ShaderCompiler::DeclaresMultisampledImage(integerMs));
const Vector<Uint32> colorMs = CompileFragment(kColorMultisampleFetch);
ASSERT_FALSE(colorMs.empty());
EXPECT_TRUE(ShaderCompiler::DeclaresMultisampledImage(colorMs));
// Runs on every stage of every program on a squeezed device, so it must survive a stage that
// produced no SPIR-V rather than pushing a parse diagnostic for it.
EXPECT_FALSE(ShaderCompiler::DeclaresMultisampledImage({}));
}
// The combined probe answers both gate questions from one parse; it must agree with the
// per-gate probes on the same modules and stay quiet for an empty stage.
TEST_F(ClampMultisampleFetchTest, TheCombinedProbeAgreesWithThePerGateOnes) {
const Vector<Uint32> integerMs = CompileFragment(kIntegerMultisampleFetch);
ASSERT_FALSE(integerMs.empty());
const auto msFeatures = ShaderCompiler::ProbeSpirvGateFeatures(integerMs);
EXPECT_TRUE(msFeatures.DeclaresMultisampledImage);
EXPECT_FALSE(msFeatures.WritesViewportIndexOutput);
const Vector<Uint32> plain = CompileFragment(kNoMultisampleFetch);
ASSERT_FALSE(plain.empty());
const auto plainFeatures = ShaderCompiler::ProbeSpirvGateFeatures(plain);
EXPECT_FALSE(plainFeatures.DeclaresMultisampledImage);
EXPECT_FALSE(plainFeatures.WritesViewportIndexOutput);
const auto emptyFeatures = ShaderCompiler::ProbeSpirvGateFeatures({});
EXPECT_FALSE(emptyFeatures.DeclaresMultisampledImage);
EXPECT_FALSE(emptyFeatures.WritesViewportIndexOutput);
}
// The overwhelming majority of modules. Behind the probe they never reach the pass at all, but the
// pass has to be inert for them on its own, or a future caller that forgets the gate silently
// re-serialises every shader in the program.
TEST_F(ClampMultisampleFetchTest, LeavesAModuleWithoutAMultisampledImageUntouched) {
const Vector<Uint32> input = CompileFragment(kNoMultisampleFetch);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(
input, output, /*maxColorSamples=*/4, /*maxIntegerSamples=*/1, /*maxDepthSamples=*/4,
kAdvertisedMaxSamples, true));
EXPECT_EQ(output, input) << Disassemble(output);
}
// The bug itself. GL_MAX_INTEGER_SAMPLES says 4, the texture has one sample, and the shader asks
// for sample 3.
TEST_F(ClampMultisampleFetchTest, ReplacesAnOutOfRangeIntegerSampleWithZero) {
const Vector<Uint32> input = CompileFragment(kIntegerMultisampleFetch);
ASSERT_FALSE(input.empty());
const Vector<ImageFetch> before = CollectImageFetches(input);
ASSERT_EQ(before.size(), 2u) << Disassemble(input);
const ImageFetch* sampleBefore = FindSampleCarryingFetch(before);
const ImageFetch* lodBefore = FindLodCarryingFetch(before);
ASSERT_NE(sampleBefore, nullptr) << Disassemble(input);
ASSERT_NE(lodBefore, nullptr) << Disassemble(input);
ASSERT_EQ(sampleBefore->maskOperandIds.size(), 1u);
const std::map<Uint32, Uint32> constantsBefore = CollectScalarConstants(input);
ASSERT_EQ(constantsBefore.count(sampleBefore->maskOperandIds.front()), 1u);
EXPECT_EQ(constantsBefore.at(sampleBefore->maskOperandIds.front()), 3u);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(
input, output, /*maxColorSamples=*/4, /*maxIntegerSamples=*/1, /*maxDepthSamples=*/4,
kAdvertisedMaxSamples, true));
ASSERT_FALSE(output.empty());
const String dis = Disassemble(output);
ASSERT_TRUE(Validates(output)) << dis;
const Vector<ImageFetch> after = CollectImageFetches(output);
ASSERT_EQ(after.size(), 2u) << dis;
const ImageFetch* sampleAfter = FindSampleCarryingFetch(after);
ASSERT_NE(sampleAfter, nullptr) << dis;
ASSERT_EQ(sampleAfter->maskOperandIds.size(), 1u) << dis;
// Sample 0 is the only one a one-sample allocation has - and it is a CONSTANT, not a computed
// minimum: at K == 1 there is nothing to compare against. An id that resolves in the constant
// table cannot also be some OpExtInst's result.
const std::map<Uint32, Uint32> constantsAfter = CollectScalarConstants(output);
ASSERT_EQ(constantsAfter.count(sampleAfter->maskOperandIds.front()), 1u) << dis;
EXPECT_EQ(constantsAfter.at(sampleAfter->maskOperandIds.front()), 0u) << dis;
// The float sampler2D in the same module is not multisampled, so its Lod fetch has to come
// through with the same image, the same mask and the same operand.
const ImageFetch* lodAfter = FindLodCarryingFetch(after);
ASSERT_NE(lodAfter, nullptr) << dis;
EXPECT_EQ(lodAfter->imageId, lodBefore->imageId) << dis;
EXPECT_EQ(lodAfter->mask, lodBefore->mask) << dis;
EXPECT_EQ(lodAfter->maskOperandIds, lodBefore->maskOperandIds) << dis;
}
// The same shader on a device whose integer ceiling really is what MobileGL advertises. Nothing is
// out of range, so nothing may be rewritten - and the module must not even be re-serialised.
TEST_F(ClampMultisampleFetchTest, LeavesTheFetchAloneWhenTheCategoryReachesTheAdvertisedMaximum) {
const Vector<Uint32> input = CompileFragment(kIntegerMultisampleFetch);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(
input, output, /*maxColorSamples=*/4, /*maxIntegerSamples=*/4, /*maxDepthSamples=*/4,
kAdvertisedMaxSamples, true));
EXPECT_EQ(output, input) << Disassemble(output);
}
// A category squeezed to something above 1 cannot be answered with a constant: an index the
// allocation does have must survive, so only the upper bound moves.
TEST_F(ClampMultisampleFetchTest, ClampsAColorSampleWithAMinimum) {
const Vector<Uint32> input = CompileFragment(kColorMultisampleFetch);
ASSERT_FALSE(input.empty());
const Vector<ImageFetch> before = CollectImageFetches(input);
ASSERT_EQ(before.size(), 1u) << Disassemble(input);
ASSERT_EQ(before.front().maskOperandIds.size(), 1u);
const Uint32 originalSampleId = before.front().maskOperandIds.front();
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(
input, output, /*maxColorSamples=*/2, /*maxIntegerSamples=*/4, /*maxDepthSamples=*/4,
kAdvertisedMaxSamples, true));
ASSERT_FALSE(output.empty());
const String dis = Disassemble(output);
ASSERT_TRUE(Validates(output)) << dis;
const Vector<ImageFetch> after = CollectImageFetches(output);
ASSERT_EQ(after.size(), 1u) << dis;
ASSERT_EQ(after.front().maskOperandIds.size(), 1u) << dis;
const Uint32 clampedSampleId = after.front().maskOperandIds.front();
EXPECT_NE(clampedSampleId, originalSampleId) << dis;
const Vector<ExtInst> extInsts = CollectExtInsts(output);
const ExtInst* minimum = nullptr;
for (const ExtInst& extInst : extInsts) {
if (extInst.resultId == clampedSampleId) minimum = &extInst;
}
ASSERT_NE(minimum, nullptr) << dis;
EXPECT_EQ(minimum->instructionNumber, kGlslStd450SMin) << dis;
ASSERT_EQ(minimum->operandIds.size(), 2u) << dis;
EXPECT_EQ(minimum->operandIds[0], originalSampleId) << dis;
// min(sample, K - 1), i.e. the last sample a two-sample allocation has.
const std::map<Uint32, Uint32> constants = CollectScalarConstants(output);
ASSERT_EQ(constants.count(minimum->operandIds[1]), 1u) << dis;
EXPECT_EQ(constants.at(minimum->operandIds[1]), 1u) << dis;
}
@@ -8,6 +8,7 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <sstream>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -484,52 +485,165 @@ TEST_F(DemoteFloat64Test, RejectsGarbageInput) {
EXPECT_FALSE(ShaderCompiler::DemoteFloat64ToFloat32(notSpirv, output, true)); EXPECT_FALSE(ShaderCompiler::DemoteFloat64ToFloat32(notSpirv, output, true));
} }
// EliminateFloatEqualsZeroPass turns a comparison against 0.0 into an epsilon test, a // EliminateFloatEqualsZeroPass re-spells a comparison against 0.0 through GLSL.std.450 FAbs, so
// workaround for drivers whose exact float compare misbehaves. Deciding WHICH constants are // that no float-equality instruction reaches a driver that gets one wrong. Deciding WHICH
// zero used to read every float constant as though it were 32 bits wide, and on a 64-bit // constants are zero used to read every float constant as though it were 32 bits wide, and on a
// constant that reads the LOW half of the mantissa - which is zero for 1.0lf, 2.0lf, 0.5lf and // 64-bit constant that reads the LOW half of the mantissa - which is zero for 1.0lf, 2.0lf, 0.5lf
// every other round double a shader is likely to spell. Each of those was mistaken for 0.0, so // and every other round double a shader is likely to spell. Each of those was mistaken for 0.0, so
// a comparison against 1.0lf became an epsilon test against ZERO, and came out true for a // a comparison against 1.0lf became a test against ZERO, and came out true for a uniform holding
// uniform holding exactly 1.0. That is the whole of KHR-GL43.compute_shader.fp64-case2. // exactly 1.0. That is the whole of KHR-GL43.compute_shader.fp64-case2.
//
// The replacement itself used to be an epsilon ball, `abs(x) < 1e-4`, which called any legitimately
// small value zero: KHR-GL3x.buffer_objects.triangles computes a specular term of ~6e-5 at a large
// render target and rendered black. It is exact now - `abs(x) <= 0.0` / `abs(x) > 0.0` against the
// module's own zero constant - and the tests below pin both halves of that: only a genuine 0.0 is
// matched, and what the compare tests against is the constant the source itself spelled.
// //
// Asserted on the optimized module rather than through a driver, because that is where the // Asserted on the optimized module rather than through a driver, because that is where the
// rewrite happens and its fingerprint there is unambiguous: the epsilon form introduces a // rewrite happens and its fingerprint there is unambiguous: the rewrite introduces a
// GLSL.std.450 FAbs, and nothing else in these shaders would. // GLSL.std.450 FAbs, and nothing else in these shaders would.
namespace { namespace {
Bool RewritesToAnEpsilonTest(const String& source) { String OptimizedDisassembly(const String& source) {
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source); const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
EXPECT_FALSE(input.empty()); EXPECT_FALSE(input.empty());
if (input.empty()) return false; if (input.empty()) return {};
Vector<Uint32> output; Vector<Uint32> output;
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true)); EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true));
return Disassemble(output).find("FAbs") != String::npos; return Disassemble(output);
} }
String CompareAgainst(const String& type, const String& literal) { Bool RewritesToAnAbsoluteValueTest(const String& source) {
return OptimizedDisassembly(source).find("FAbs") != String::npos;
}
String CompareAgainstUsing(const String& type, const String& op, const String& literal) {
return "#version 430 core\n" return "#version 430 core\n"
"layout(local_size_x = 1) in;\n" "layout(local_size_x = 1) in;\n"
"buffer Result { int g_result; };\n" "buffer Result { int g_result; };\n"
"uniform " + type + " g_0;\n" "uniform " + type + " g_0;\n"
"void main() {\n" "void main() {\n"
" g_result = 0;\n" " g_result = 0;\n"
" if (g_0 != " + literal + ") g_result = 1;\n" " if (g_0 " + op + " " + literal + ") g_result = 1;\n"
"}\n"; "}\n";
} }
String CompareAgainst(const String& type, const String& literal) {
return CompareAgainstUsing(type, "!=", literal);
}
// Every instruction of a disassembly, split into whitespace-separated tokens, so an operand can
// be identified by position instead of by a substring another opcode might also contain -
// `OpFOrdLessThan` is a prefix of `OpFOrdLessThanEqual`, and those two are the whole difference
// between the epsilon rewrite and the exact one.
Vector<Vector<String>> TokenizedInstructions(const String& disassembly) {
Vector<Vector<String>> instructions;
StringStream lines(disassembly);
String line;
while (std::getline(lines, line)) {
Vector<String> tokens;
StringStream words(line);
String word;
while (words >> word) tokens.push_back(word);
instructions.push_back(tokens);
}
return instructions;
}
// The compare the rewrite leaves behind, e.g. `%22 = OpFOrdLessThanEqual %bool %21 %float_0`,
// or an empty vector if the module has none. These four opcodes are the only ones the pass
// emits and nothing else in these shaders produces one.
Vector<String> FindRewrittenCompare(const String& disassembly) {
for (const Vector<String>& tokens : TokenizedInstructions(disassembly)) {
if (tokens.size() < 6 || tokens[1] != "=") continue;
if (tokens[2] == "OpFOrdLessThanEqual" || tokens[2] == "OpFUnordLessThanEqual" ||
tokens[2] == "OpFOrdGreaterThan" || tokens[2] == "OpFUnordGreaterThan") {
return tokens;
}
}
return {};
}
// Result id of the module's 0.0 constant of the type FAbs produces - the constant the source
// itself spelled - found without assuming what the disassembler names it or how it prints the
// literal.
String FindZeroConstantId(const String& disassembly) {
const Vector<Vector<String>> instructions = TokenizedInstructions(disassembly);
String floatTypeId;
for (const Vector<String>& tokens : instructions) {
if (tokens.size() >= 7 && tokens[2] == "OpExtInst" && tokens[5] == "FAbs") {
floatTypeId = tokens[3];
break;
}
}
if (floatTypeId.empty()) return {};
for (const Vector<String>& tokens : instructions) {
if (tokens.size() < 5 || tokens[2] != "OpConstant" || tokens[3] != floatTypeId) continue;
char* end = nullptr;
const double value = std::strtod(tokens[4].c_str(), &end);
if (end != nullptr && *end == '\0' && value == 0.0) return tokens[0];
}
return {};
}
// The shape the pass promises: the given opcode (either NaN half of it), tested against the
// module's own zero constant rather than against anything this pass invented.
void ExpectComparedAgainstModuleZero(const String& source, const String& orderedOpcode,
const String& unorderedOpcode) {
const String disassembly = OptimizedDisassembly(source);
const Vector<String> compare = FindRewrittenCompare(disassembly);
ASSERT_FALSE(compare.empty()) << "no rewritten compare in the optimized module\n"
<< disassembly;
EXPECT_TRUE(compare[2] == orderedOpcode || compare[2] == unorderedOpcode)
<< "expected " << orderedOpcode << " (or its unordered twin), got " << compare[2] << "\n"
<< disassembly;
const String zeroId = FindZeroConstantId(disassembly);
ASSERT_FALSE(zeroId.empty()) << "the module has no 0.0 constant of the abs() type\n"
<< disassembly;
EXPECT_EQ(compare.back(), zeroId)
<< "the rewrite compares against " << compare.back()
<< " instead of the module's own zero; a synthesized threshold is the epsilon bug\n"
<< disassembly;
}
} // namespace } // namespace
TEST_F(DemoteFloat64Test, AComparisonAgainstANonZeroDoubleIsLeftAlone) { TEST_F(DemoteFloat64Test, AComparisonAgainstANonZeroDoubleIsLeftAlone) {
EXPECT_FALSE(RewritesToAnEpsilonTest(CompareAgainst("double", "1.0LF"))) EXPECT_FALSE(RewritesToAnAbsoluteValueTest(CompareAgainst("double", "1.0LF")))
<< "a double compared against 1.0lf was rewritten into an epsilon test against zero"; << "a double compared against 1.0lf was rewritten into a test against zero";
} }
TEST_F(DemoteFloat64Test, AComparisonAgainstZeroIsStillRewritten) { TEST_F(DemoteFloat64Test, AComparisonAgainstZeroIsStillRewritten) {
EXPECT_TRUE(RewritesToAnEpsilonTest(CompareAgainst("double", "0.0LF"))) EXPECT_TRUE(RewritesToAnAbsoluteValueTest(CompareAgainst("double", "0.0LF")))
<< "the rewrite must still fire for a genuine comparison against zero"; << "the rewrite must still fire for a genuine comparison against zero";
} }
TEST_F(DemoteFloat64Test, TheThirtyTwoBitBehaviourIsUnchanged) { TEST_F(DemoteFloat64Test, TheThirtyTwoBitBehaviourIsUnchanged) {
EXPECT_FALSE(RewritesToAnEpsilonTest(CompareAgainst("float", "1.0"))) EXPECT_FALSE(RewritesToAnAbsoluteValueTest(CompareAgainst("float", "1.0")))
<< "a float compared against 1.0 must not be rewritten"; << "a float compared against 1.0 must not be rewritten";
EXPECT_TRUE(RewritesToAnEpsilonTest(CompareAgainst("float", "0.0"))) EXPECT_TRUE(RewritesToAnAbsoluteValueTest(CompareAgainst("float", "0.0")))
<< "the 32-bit behaviour this pass shipped with must be preserved exactly"; << "the 32-bit behaviour this pass shipped with must be preserved exactly";
} }
// The pass matches ZERO, not "small". The old constant-is-zero test was `fabs(v) <= 1e-4`, so a
// float compared against exactly 1e-4 was declared a comparison against zero and rewritten into
// `abs(x) >= 1e-4` - a different question from the one the shader asked, against a constant that
// was never zero to begin with.
TEST_F(DemoteFloat64Test, AComparisonAgainstASmallNonZeroLiteralIsLeftAlone) {
EXPECT_FALSE(RewritesToAnAbsoluteValueTest(CompareAgainst("float", "0.0001")))
<< "a float compared against 1e-4 was treated as a comparison against zero";
EXPECT_FALSE(RewritesToAnAbsoluteValueTest(CompareAgainst("double", "0.0001LF")))
<< "the 64-bit accessor must judge the constant just as exactly as the 32-bit one";
}
// What replaces the compare, not just that something did. Both properties here are what makes the
// rewrite exact rather than a tolerance, and neither is visible in the FAbs fingerprint above.
TEST_F(DemoteFloat64Test, TheRewriteComparesAbsAgainstTheModulesOwnZero) {
// `x == 0.0` -> `abs(x) <= 0.0`. The equality has to be INSIDE the replacement: with a strict
// `<` and no epsilon left to hide behind, +/-0 would stop comparing equal to zero.
ExpectComparedAgainstModuleZero(CompareAgainstUsing("float", "==", "0.0"),
"OpFOrdLessThanEqual", "OpFUnordLessThanEqual");
// `x != 0.0` -> `abs(x) > 0.0`, the strict complement of the above.
ExpectComparedAgainstModuleZero(CompareAgainstUsing("float", "!=", "0.0"), "OpFOrdGreaterThan",
"OpFUnordGreaterThan");
}
@@ -0,0 +1,214 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/FlattenAtomicCounterBlockTest.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#define SPV_ENABLE_UTILITY_CODE
#include "glslang/SPIRV/spirv.hpp11"
#undef SPV_ENABLE_UTILITY_CODE
#include "Includes.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
#include <cstring>
#include <map>
#include <string>
#include <vector>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
constexpr SizeT kSpirvHeaderWordCount = 5u;
template <typename Visitor>
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
const Uint32 wordCount = spirv[offset] >> 16u;
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
offset += wordCount;
}
}
Vector<Uint32> CompileCompute(const String& source) {
using namespace MobileGL::MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
bool Validates(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
tools.SetMessageConsumer(
[](spv_message_level_t, const char*, const spv_position_t& position, const char* message) {
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
});
return tools.Validate(spirv);
}
// Test-side reference walker, deliberately independent of the production code.
Uint32 FindAtomicCounterBlockStructId(const Vector<Uint32>& spirv) {
const String prefix = MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX;
Uint32 structId = 0;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != spv::Op::OpName || wordCount < 3u || structId != 0u) return;
const char* text = reinterpret_cast<const char*>(&words[2]);
const SizeT available = static_cast<SizeT>(wordCount - 2u) * sizeof(Uint32);
if (available < prefix.size()) return;
if (std::strncmp(text, prefix.c_str(), prefix.size()) != 0) return;
structId = words[1];
});
return structId;
}
// The Offset of member `member` on struct `structId`, or -1.
Int64 MemberOffsetOf(const Vector<Uint32>& spirv, Uint32 structId, Uint32 member) {
Int64 offset = -1;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != spv::Op::OpMemberDecorate || wordCount < 5u) return;
if (words[1] != structId || words[2] != member) return;
if (static_cast<spv::Decoration>(words[3]) != spv::Decoration::Offset) return;
offset = words[4];
});
return offset;
}
Uint32 MemberCountOf(const Vector<Uint32>& spirv, Uint32 structId) {
Uint32 count = 0;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != spv::Op::OpTypeStruct || wordCount < 2u || words[1] != structId) return;
count = wordCount - 2u;
});
return count;
}
Uint32 MemberTypeOf(const Vector<Uint32>& spirv, Uint32 structId, Uint32 member) {
Uint32 typeId = 0;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != spv::Op::OpTypeStruct || wordCount < 3u + member || words[1] != structId) return;
typeId = words[2 + member];
});
return typeId;
}
// The declared length of an OpTypeArray, resolved through the uint constants in the module.
Int64 ArrayLengthOf(const Vector<Uint32>& spirv, Uint32 arrayTypeId) {
std::map<Uint32, Uint32> constants;
Int64 length = -1;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == spv::Op::OpConstant && wordCount >= 4u) constants[words[2]] = words[3];
if (opcode == spv::Op::OpTypeArray && wordCount >= 4u && words[1] == arrayTypeId) {
const auto it = constants.find(words[3]);
if (it != constants.end()) length = it->second;
}
});
return length;
}
// KHR-GL43.compute_shader.resources-atomic-counter's non-zero-offset shape: two counters
// declared eight bytes into the buffer, which glslang lowers to one block member at Offset 8.
constexpr const char* kOffsetCounters = R"(#version 450 core
layout(local_size_x = 1) in;
layout(binding = 1, offset = 8) uniform atomic_uint g_counter[2];
layout(std430, binding = 0) buffer Output { uint value[]; } g_out;
void main() {
g_out.value[0] = atomicCounterIncrement(g_counter[0]);
g_out.value[1] = atomicCounterIncrement(g_counter[1]);
}
)";
// The latch: offset 0 is what nearly every shader declares, and it transpiles today.
constexpr const char* kNaturalCounters = R"(#version 450 core
layout(local_size_x = 1) in;
layout(binding = 1, offset = 0) uniform atomic_uint g_counter[2];
layout(std430, binding = 0) buffer Output { uint value[]; } g_out;
void main() {
g_out.value[0] = atomicCounterIncrement(g_counter[0]);
g_out.value[1] = atomicCounterIncrement(g_counter[1]);
}
)";
constexpr const char* kNoCounters = R"(#version 450 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Output { uint value[]; } g_out;
void main() {
g_out.value[0] = 1u;
}
)";
} // namespace
TEST(FlattenAtomicCounterBlockPass, MovesTheBlockToOffsetZeroAndGrowsTheArray) {
const Vector<Uint32> input = CompileCompute(kOffsetCounters);
ASSERT_FALSE(input.empty());
const Uint32 structId = FindAtomicCounterBlockStructId(input);
ASSERT_NE(structId, 0u) << "glslang did not lower the counters onto a gl_AtomicCounterBlock_*";
ASSERT_EQ(MemberOffsetOf(input, structId, 0u), 8) << "the input's member 0 is not at the declared offset";
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(input, output, true));
ASSERT_FALSE(output.empty());
const Uint32 outStructId = FindAtomicCounterBlockStructId(output);
ASSERT_EQ(outStructId, structId) << "the block's id must not move; SetAtomicCounterBlockBindings "
"still finds it by name";
EXPECT_EQ(MemberCountOf(output, outStructId), 1u);
EXPECT_EQ(MemberOffsetOf(output, outStructId, 0u), 0)
<< "member 0 must sit at offset 0 or no std140/std430 layout can express the block";
// Two counters eight bytes in: the flattened array has to cover bytes [0, 16), i.e. 4 uints,
// so counter k lands on element 2 + k and therefore on byte 8 + 4k - where it was declared.
EXPECT_EQ(ArrayLengthOf(output, MemberTypeOf(output, outStructId, 0u)), 4);
EXPECT_TRUE(Validates(output));
}
TEST(FlattenAtomicCounterBlockPass, LeavesANaturallyPackedBlockByteIdentical) {
const Vector<Uint32> input = CompileCompute(kNaturalCounters);
ASSERT_FALSE(input.empty());
ASSERT_NE(FindAtomicCounterBlockStructId(input), 0u);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(input, output, true));
EXPECT_EQ(output, input);
}
TEST(FlattenAtomicCounterBlockPass, LeavesAShaderWithoutCountersByteIdentical) {
const Vector<Uint32> input = CompileCompute(kNoCounters);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(input, output, true));
EXPECT_EQ(output, input);
}
TEST(FlattenAtomicCounterBlockPass, IsIdempotent) {
const Vector<Uint32> input = CompileCompute(kOffsetCounters);
ASSERT_FALSE(input.empty());
Vector<Uint32> once;
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(input, once, true));
ASSERT_FALSE(once.empty());
Vector<Uint32> twice;
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(once, twice, true));
EXPECT_EQ(twice, once);
}
@@ -0,0 +1,251 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/LegalizeStorageBlockArrayIndexTest.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#define SPV_ENABLE_UTILITY_CODE
#include "glslang/SPIRV/spirv.hpp11"
#undef SPV_ENABLE_UTILITY_CODE
#include "Includes.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
#include <set>
#include <vector>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
constexpr SizeT kSpirvHeaderWordCount = 5u;
template <typename Visitor>
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
const Uint32 wordCount = spirv[offset] >> 16u;
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
offset += wordCount;
}
}
Vector<Uint32> CompileCompute(const String& source) {
using namespace MobileGL::MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
bool Validates(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
tools.SetMessageConsumer(
[](spv_message_level_t, const char*, const spv_position_t& position, const char* message) {
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
});
return tools.Validate(spirv);
}
Uint32 CountOpcode(const Vector<Uint32>& spirv, spv::Op wanted) {
Uint32 count = 0u;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
if (opcode == wanted) ++count;
});
return count;
}
// Test-side reference walker, deliberately independent of the production detection so a
// bug in the pass cannot hide behind the same helper: true when some access chain rooted
// at an array-of-storage-blocks variable carries a non-constant FIRST index, which is
// exactly what the Qualcomm ES compiler refuses.
bool HasDynamicBlockArrayIndex(const Vector<Uint32>& spirv) {
std::set<Uint32> blockStructs; // OpTypeStruct ids decorated Block / BufferBlock
std::set<Uint32> constants; // OpConstant / OpConstantNull result ids
std::set<Uint32> blockArrayTypes; // OpTypeArray ids whose element is such a struct
std::set<Uint32> blockArrayPointers;// OpTypePointer ids pointing at one of those arrays
std::set<Uint32> blockArrayVars; // OpVariable ids of one of those pointer types
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
switch (opcode) {
case spv::Op::OpDecorate:
if (wordCount >= 3u) {
const auto decoration = static_cast<spv::Decoration>(words[2]);
if (decoration == spv::Decoration::Block ||
decoration == spv::Decoration::BufferBlock) {
blockStructs.insert(words[1]);
}
}
break;
case spv::Op::OpConstant:
if (wordCount >= 3u) constants.insert(words[2]);
break;
case spv::Op::OpConstantNull:
if (wordCount >= 3u) constants.insert(words[2]);
break;
case spv::Op::OpTypeArray:
// OpTypeArray <result> <element type> <length>
if (wordCount >= 4u && blockStructs.count(words[2]) != 0u) {
blockArrayTypes.insert(words[1]);
}
break;
case spv::Op::OpTypePointer:
// OpTypePointer <result> <storage class> <pointee>
if (wordCount >= 4u && blockArrayTypes.count(words[3]) != 0u) {
blockArrayPointers.insert(words[1]);
}
break;
case spv::Op::OpVariable:
// OpVariable <result type> <result> <storage class>
if (wordCount >= 4u && blockArrayPointers.count(words[1]) != 0u) {
blockArrayVars.insert(words[2]);
}
break;
default:
break;
}
});
bool dynamic = false;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != spv::Op::OpAccessChain && opcode != spv::Op::OpInBoundsAccessChain) return;
// OpAccessChain <result type> <result> <base> <index 0> ...
if (wordCount < 5u) return;
if (blockArrayVars.count(words[3]) == 0u) return;
if (constants.count(words[4]) != 0u) return;
dynamic = true;
});
return dynamic;
}
// `for (i = 0; i < 4; ++i)` over an array of storage blocks - the shape
// KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case1 uses. Foldable: the
// induction variable is a literal after unrolling.
constexpr const char* kLoopIndexedBlockArray = R"(#version 450 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
layout(std430, binding = 8) buffer Out { uint data[4]; } g_out;
void main() {
for (int i = 0; i < 4; ++i) {
g_out.data[i] = g_blocks[i].data[0];
}
}
)";
// A uniform-sourced index - the shape
// KHR-GL43.shader_storage_buffer_object.advanced-indirectAddressing-case2 uses. Nothing
// can fold it, so the switch/select lowering is what has to carry it.
constexpr const char* kUniformIndexedBlockArray = R"(#version 450 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
layout(std430, binding = 8) buffer Out { uint value; } g_out;
uniform int g_index;
void main() {
g_blocks[g_index].data[0] = 7u;
g_out.value = g_blocks[g_index].data[1];
}
)";
// The positive control from the device run: dynamic addressing through an array MEMBER of
// ONE block is legal ES and must not be rewritten.
constexpr const char* kArrayMemberInsideOneBlock = R"(#version 450 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_block;
layout(std430, binding = 8) buffer Out { uint value; } g_out;
uniform int g_index;
void main() {
g_out.value = g_block.data[g_index];
}
)";
// A block array indexed only with literals is already legal ES.
constexpr const char* kConstantIndexedBlockArray = R"(#version 450 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
layout(std430, binding = 8) buffer Out { uint value; } g_out;
void main() {
g_out.value = g_blocks[2].data[0] + g_blocks[3].data[1];
}
)";
} // namespace
TEST(LegalizeStorageBlockArrayIndexPass, FoldsALoopIndexedBlockArray) {
const Vector<Uint32> input = CompileCompute(kLoopIndexedBlockArray);
ASSERT_FALSE(input.empty());
EXPECT_TRUE(HasDynamicBlockArrayIndex(input));
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true));
ASSERT_FALSE(output.empty());
// Either half of the legalization is an acceptable outcome here - what the ES driver
// cares about is only that no dynamic subscript survives.
EXPECT_FALSE(HasDynamicBlockArrayIndex(output));
EXPECT_TRUE(Validates(output));
}
TEST(LegalizeStorageBlockArrayIndexPass, LowersAUniformIndexedWriteToASwitchAndAReadToSelects) {
const Vector<Uint32> input = CompileCompute(kUniformIndexedBlockArray);
ASSERT_FALSE(input.empty());
EXPECT_TRUE(HasDynamicBlockArrayIndex(input));
EXPECT_EQ(CountOpcode(input, spv::Op::OpSwitch), 0u);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true));
ASSERT_FALSE(output.empty());
EXPECT_FALSE(HasDynamicBlockArrayIndex(output));
// One switch for the store, and one select per element past the first for the load.
EXPECT_EQ(CountOpcode(output, spv::Op::OpSwitch), 1u);
EXPECT_EQ(CountOpcode(output, spv::Op::OpSelect), 3u);
EXPECT_TRUE(Validates(output));
}
TEST(LegalizeStorageBlockArrayIndexPass, LeavesADynamicMemberOfOneBlockByteIdentical) {
const Vector<Uint32> input = CompileCompute(kArrayMemberInsideOneBlock);
ASSERT_FALSE(input.empty());
EXPECT_FALSE(HasDynamicBlockArrayIndex(input));
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true));
EXPECT_EQ(output, input);
}
TEST(LegalizeStorageBlockArrayIndexPass, LeavesAConstantIndexedBlockArrayByteIdentical) {
const Vector<Uint32> input = CompileCompute(kConstantIndexedBlockArray);
ASSERT_FALSE(input.empty());
EXPECT_FALSE(HasDynamicBlockArrayIndex(input));
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true));
EXPECT_EQ(output, input);
}
TEST(LegalizeStorageBlockArrayIndexPass, IsIdempotent) {
const Vector<Uint32> input = CompileCompute(kUniformIndexedBlockArray);
ASSERT_FALSE(input.empty());
Vector<Uint32> once;
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, once, true));
ASSERT_FALSE(once.empty());
Vector<Uint32> twice;
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(once, twice, true));
EXPECT_EQ(twice, once);
}
@@ -0,0 +1,237 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/LowerViewportIndexTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// LowerViewportIndexPass is the DirectGLES fallback for a driver with no GL_OES_viewport_array.
// The thing it prevents is not a wrong pixel but a missing program: ESSL has no core
// gl_ViewportIndex at any version, SPIRV-Cross prints the identifier bare, and the driver rejects
// the stage - after which DirectGLES binds program 0 and every draw renders nothing while
// GL_LINK_STATUS still answers TRUE. So what has to hold is textual and structural at once: the
// emitted ESSL must stop naming the builtin, the module must stay valid, and gl_Layer - which IS
// core in ESSL 3.20 geometry shaders - must come through untouched.
//
// Real GLSL through the same glslang path the backends use, rather than hand-assembled words, for
// the same reason MG_Test/Pipeline/ViewportIndexReflectionTest.cpp does it: what matters is what
// glslang actually emits for these shaders.
#include <gtest/gtest.h>
#include <string>
#include <vector>
#include "Includes.h"
#include "Init.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
namespace {
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
using namespace MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
String Disassemble(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
String text;
tools.Disassemble(spirv, &text);
return text;
}
// ESSL 320, i.e. exactly what the DirectGLES transpile asks SPIRV-Cross for.
String Transpile(const Vector<Uint32>& spirv) {
SpvcSession session(spirv, SessionUsageBit::Transpile);
auto essl = ShaderCompiler::DecompileShader(session);
EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log);
return essl ? essl.value() : String{};
}
Bool Contains(const String& haystack, const String& needle) {
return haystack.find(needle) != String::npos;
}
// KHR-GL4x.viewport_array.draw_to_single_layer_with_multiple_viewports' geometry stage in
// miniature: sixteen invocations, each routing its primitive to its own viewport. This is the
// shape that today loses the whole program on a driver without GL_OES_viewport_array.
const char* const kGeometryWritesViewportIndex = R"(#version 410 core
layout(points, invocations = 16) in;
layout(triangle_strip, max_vertices = 4) out;
void main() {
gl_ViewportIndex = gl_InvocationID;
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
EndPrimitive();
}
)";
// Layered rendering, not viewport routing. gl_Layer IS core in ESSL 3.20 geometry shaders, so
// demoting it would break a Minecraft-style cubemap pass that works today.
const char* const kGeometryWritesLayerOnly = R"(#version 410 core
layout(points, invocations = 6) in;
layout(triangle_strip, max_vertices = 4) out;
void main() {
gl_Layer = gl_InvocationID;
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
EndPrimitive();
}
)";
// Both at once, which is the case that separates "lowers the right builtin" from "lowers every
// builtin it can reach": KHR-GL4x.viewport_array.draw_multiple_layers writes both.
const char* const kGeometryWritesBoth = R"(#version 410 core
layout(points, invocations = 16) in;
layout(triangle_strip, max_vertices = 4) out;
void main() {
gl_ViewportIndex = gl_InvocationID;
gl_Layer = gl_InvocationID;
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
EndPrimitive();
}
)";
const char* const kPlainGeometry = R"(#version 410 core
layout(points, invocations = 1) in;
layout(triangle_strip, max_vertices = 4) out;
void main() {
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
EndPrimitive();
}
)";
} // namespace
class LowerViewportIndexTest : public ::testing::Test {
protected:
void SetUp() override {
MobileGL::Initialize();
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
}
void TearDown() override {
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
<< "the lowered module did not survive spirv-val";
}
Uint64 m_validationFailuresAtStart = 0;
};
// The probe is the gate that keeps every ordinary stage off an optimizer round trip, so it has to
// answer no for a shader that never routes a viewport - and yes for the one that does.
TEST_F(LowerViewportIndexTest, TheProbeAnswersOnlyForAViewportIndexWriter) {
const Vector<Uint32> plain = CompileToSpirv(GL_GEOMETRY_SHADER, kPlainGeometry);
ASSERT_FALSE(plain.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresViewportIndexBuiltin(plain));
const Vector<Uint32> layerOnly = CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesLayerOnly);
ASSERT_FALSE(layerOnly.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresViewportIndexBuiltin(layerOnly));
const Vector<Uint32> writer = CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesViewportIndex);
ASSERT_FALSE(writer.empty());
EXPECT_TRUE(ShaderCompiler::DeclaresViewportIndexBuiltin(writer));
// Runs on every stage of every program on a driver without the extension, so it must survive a
// stage that produced no SPIR-V rather than pushing a parse diagnostic for it.
EXPECT_FALSE(ShaderCompiler::DeclaresViewportIndexBuiltin({}));
}
// The whole point: the emitted ESSL must stop naming a builtin the language does not have.
TEST_F(LowerViewportIndexTest, DemotesTheBuiltinToAnOrdinaryGlobal) {
const Vector<Uint32> input = CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesViewportIndex);
ASSERT_FALSE(input.empty());
// Negative control, and the bug itself: untouched, SPIRV-Cross prints gl_ViewportIndex into
// ESSL 320 and asks for no extension to go with it.
const String before = Transpile(input);
EXPECT_TRUE(Contains(before, "gl_ViewportIndex")) << before;
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LowerViewportIndexForEssl(input, output, true));
ASSERT_FALSE(output.empty());
const String dis = Disassemble(output);
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
ASSERT_TRUE(tools.Validate(output)) << dis;
EXPECT_FALSE(Contains(dis, "BuiltIn ViewportIndex")) << dis;
EXPECT_TRUE(Contains(dis, "mg_ViewportIndex")) << dis;
EXPECT_TRUE(Contains(dis, "Private")) << dis;
const String after = Transpile(output);
EXPECT_TRUE(Contains(after, "mg_ViewportIndex")) << after;
EXPECT_FALSE(Contains(after, "gl_ViewportIndex")) << after;
}
// gl_Layer is core in ESSL 3.20 geometry shaders and layered rendering works on this backend
// today. Lowering it too would trade one silent failure for another.
TEST_F(LowerViewportIndexTest, LeavesGlLayerAlone) {
const Vector<Uint32> input = CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesBoth);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LowerViewportIndexForEssl(input, output, true));
ASSERT_FALSE(output.empty());
const String dis = Disassemble(output);
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
ASSERT_TRUE(tools.Validate(output)) << dis;
EXPECT_FALSE(Contains(dis, "BuiltIn ViewportIndex")) << dis;
EXPECT_TRUE(Contains(dis, "BuiltIn Layer")) << dis;
const String after = Transpile(output);
EXPECT_TRUE(Contains(after, "gl_Layer")) << after;
EXPECT_FALSE(Contains(after, "gl_ViewportIndex")) << after;
}
// Every other stage on a driver without the extension goes through this pass too (behind the
// probe), so a module it has nothing to do with must come out saying exactly what it said.
TEST_F(LowerViewportIndexTest, LeavesAModuleWithoutTheBuiltinUntouched) {
const Vector<Uint32> input = CompileToSpirv(GL_GEOMETRY_SHADER, kPlainGeometry);
ASSERT_FALSE(input.empty());
const String before = Transpile(input);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LowerViewportIndexForEssl(input, output, true));
ASSERT_FALSE(output.empty());
const String dis = Disassemble(output);
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
ASSERT_TRUE(tools.Validate(output)) << dis;
EXPECT_FALSE(Contains(dis, "mg_ViewportIndex")) << dis;
EXPECT_EQ(Transpile(output), before);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,262 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/UniquifyIoBlockNamesTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#include <map>
#include <set>
#include <string>
#include <vector>
#include "Includes.h"
#include "Init.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
namespace {
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
using namespace MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
String Disassemble(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
String text;
tools.Disassemble(spirv, &text);
return text;
}
String Transpile(const Vector<Uint32>& spirv) {
SpvcSession session(spirv, SessionUsageBit::Transpile);
auto essl = ShaderCompiler::DecompileShader(session);
EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log);
return essl ? essl.value() : String{};
}
// The tessellation evaluation stage of
// KHR-GL42/43.shading_language_420pack.length_of_vector_and_matrix_* and
// .qualifier_order_block_*, reduced to the shape that matters: ONE block name used for
// both the block this stage consumes and the block it produces. Legal desktop GLSL - the
// input and output block namespaces are separate - and something SPIRV-Cross re-emits
// verbatim, so the ESSL it produces declares two different blocks called TCSOutputBlock.
const char* kCollidingTessEvalSource = R"(#version 420 core
layout(isolines, point_mode) in;
in vec4 tcs_tes_result[];
out vec4 tes_gs_result;
in TCSOutputBlock {
vec4 tcs_tes_variable;
} input_block[];
out TCSOutputBlock {
vec4 tes_gs_variable;
} output_block;
void main()
{
tes_gs_result = tcs_tes_result[0];
output_block.tes_gs_variable = input_block[0].tcs_tes_variable;
}
)";
// The same stage with the two blocks already named apart, which is the overwhelmingly
// common shape and the one that must go through untouched.
const char* kDistinctTessEvalSource = R"(#version 420 core
layout(isolines, point_mode) in;
in vec4 tcs_tes_result[];
out vec4 tes_gs_result;
in TCSOutputBlock {
vec4 tcs_tes_variable;
} input_block[];
out TESOutputBlock {
vec4 tes_gs_variable;
} output_block;
void main()
{
tes_gs_result = tcs_tes_result[0];
output_block.tes_gs_variable = input_block[0].tcs_tes_variable;
}
)";
// gl_PerVertex is an Input block AND an Output block of one name in every tessellation
// and geometry stage. It is the language's block, not the shader's, so it must never be
// reported and never be renamed.
const char* kBuiltinBlockOnlyTessEvalSource = R"(#version 420 core
layout(isolines, point_mode) in;
void main()
{
gl_Position = gl_in[0].gl_Position;
}
)";
} // namespace
class UniquifyIoBlockNamesTest : public ::testing::Test {
protected:
void SetUp() override {
MobileGL::Initialize();
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
}
void TearDown() override {
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
<< "the renamed module did not survive spirv-val";
}
Uint64 m_validationFailuresAtStart = 0;
};
TEST_F(UniquifyIoBlockNamesTest, ProbeReportsABlockNameUsedInBothDirections) {
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kCollidingTessEvalSource);
ASSERT_FALSE(input.empty());
std::set<String> colliding;
std::set<String> declared;
ShaderCompiler::ProbeIoBlockNamesForEssl(input, colliding, declared);
EXPECT_EQ(colliding, (std::set<String>{"TCSOutputBlock"}));
// The name set the caller picks a replacement out of has to contain what the module
// already spells, or the replacement could land on top of an existing declaration.
EXPECT_NE(declared.find("TCSOutputBlock"), declared.end());
EXPECT_NE(declared.find("input_block"), declared.end());
EXPECT_NE(declared.find("output_block"), declared.end());
}
TEST_F(UniquifyIoBlockNamesTest, ProbeIgnoresAStageWhoseBlocksAlreadyHaveDistinctNames) {
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kDistinctTessEvalSource);
ASSERT_FALSE(input.empty());
std::set<String> colliding;
std::set<String> declared;
ShaderCompiler::ProbeIoBlockNamesForEssl(input, colliding, declared);
EXPECT_TRUE(colliding.empty());
EXPECT_NE(declared.find("TCSOutputBlock"), declared.end());
}
TEST_F(UniquifyIoBlockNamesTest, ProbeNeverReportsTheBuiltinBlock) {
const Vector<Uint32> input =
CompileToSpirv(GL_TESS_EVALUATION_SHADER, kBuiltinBlockOnlyTessEvalSource);
ASSERT_FALSE(input.empty());
std::set<String> colliding;
std::set<String> declared;
ShaderCompiler::ProbeIoBlockNamesForEssl(input, colliding, declared);
// gl_PerVertex is read through gl_in and written through gl_Position, i.e. it is exactly
// the in-and-out-under-one-name shape - and renaming it would invent a block no driver
// knows.
EXPECT_TRUE(colliding.empty()) << "gl_PerVertex must never enter the rename plan";
}
TEST_F(UniquifyIoBlockNamesTest, RenamesTheTwoBlocksApartInTheEmittedEssl) {
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kCollidingTessEvalSource);
ASSERT_FALSE(input.empty());
// The generated ESSL really does declare the block twice under one name before the fix -
// pinning the defect, not just the repair.
const String before = Transpile(input);
EXPECT_NE(before.find("in TCSOutputBlock"), String::npos) << before;
EXPECT_NE(before.find("out TCSOutputBlock"), String::npos) << before;
// The plan the DirectGLES program build makes for a five-stage program: what this stage
// consumes is spelled after the tessellation control stage (pipeline index 1) and what it
// produces after itself (pipeline index 2).
const std::map<String, String> inputRenames{{"TCSOutputBlock", "TCSOutputBlock_mgio1"}};
const std::map<String, String> outputRenames{{"TCSOutputBlock", "TCSOutputBlock_mgio2"}};
std::set<String> renamed;
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::UniquifyIoBlockNamesForEssl(input, inputRenames, outputRenames, renamed,
output, true));
ASSERT_FALSE(output.empty());
EXPECT_EQ(renamed, (std::set<String>{"TCSOutputBlock"}));
const String dis = Disassemble(output);
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
ASSERT_TRUE(tools.Validate(output)) << dis;
EXPECT_EQ(dis.find("\"TCSOutputBlock\""), String::npos)
<< "the colliding name is still on a block struct:\n"
<< dis;
EXPECT_NE(dis.find("\"TCSOutputBlock_mgio1\""), String::npos) << dis;
EXPECT_NE(dis.find("\"TCSOutputBlock_mgio2\""), String::npos) << dis;
const String after = Transpile(output);
EXPECT_NE(after.find("TCSOutputBlock_mgio1"), String::npos) << after;
EXPECT_NE(after.find("TCSOutputBlock_mgio2"), String::npos) << after;
// Only the block TYPE name moves: the instance names are what the body reads and writes
// through, and the member names are half of what ES matches the interface by.
EXPECT_NE(after.find("input_block"), String::npos) << after;
EXPECT_NE(after.find("output_block"), String::npos) << after;
EXPECT_NE(after.find("tcs_tes_variable"), String::npos) << after;
EXPECT_NE(after.find("tes_gs_variable"), String::npos) << after;
}
TEST_F(UniquifyIoBlockNamesTest, RenamesOnlyTheDirectionTheCallerPlanned) {
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kCollidingTessEvalSource);
ASSERT_FALSE(input.empty());
// A separate-shader-objects program that ends at this stage plans no output rename,
// because the block's consumer lives in another program that never saw the plan.
const std::map<String, String> inputRenames{{"TCSOutputBlock", "TCSOutputBlock_mgio1"}};
std::set<String> renamed;
Vector<Uint32> output;
ASSERT_TRUE(
ShaderCompiler::UniquifyIoBlockNamesForEssl(input, inputRenames, {}, renamed, output, true));
ASSERT_FALSE(output.empty());
EXPECT_EQ(renamed, (std::set<String>{"TCSOutputBlock"}));
const String dis = Disassemble(output);
EXPECT_NE(dis.find("\"TCSOutputBlock_mgio1\""), String::npos) << dis;
// The output block keeps the name the other program still spells.
EXPECT_NE(dis.find("\"TCSOutputBlock\""), String::npos) << dis;
EXPECT_EQ(dis.find("\"TCSOutputBlock_mgio2\""), String::npos) << dis;
}
TEST_F(UniquifyIoBlockNamesTest, ReportsNothingWhenThePlanNamesNoBlockThisStageDeclares) {
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kDistinctTessEvalSource);
ASSERT_FALSE(input.empty());
const std::map<String, String> renames{{"SomeOtherBlock", "SomeOtherBlock_mgio2"}};
std::set<String> renamed;
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::UniquifyIoBlockNamesForEssl(input, renames, renames, renamed, output, true));
// Empty is what tells the DirectGLES program build to keep the module it already had
// instead of adopting the optimizer's re-serialised copy.
EXPECT_TRUE(renamed.empty());
const String dis = Disassemble(output);
EXPECT_NE(dis.find("\"TCSOutputBlock\""), String::npos) << dis;
EXPECT_NE(dis.find("\"TESOutputBlock\""), String::npos) << dis;
}

Some files were not shown because too many files have changed in this diff Show More