Compare commits

..
Author SHA1 Message Date
swung0x48 666f150202 [Fix] (SelfTest): stop optional-capability failures from declaring the whole backend unsupported, and log the POST report in chunks 2026-08-22 09:08:13 -04:00
swung0x48 2f62970dd5 [Refactor] (SelfTest): give every POST capability row a PASS/WARN/FAIL verdict and keep INFO for identity 2026-08-22 08:59:03 -04:00
swung0x48 dcb568d445 [Feature, Test] (SelfTest): probe the four remaining known driver bugs from the POST 2026-08-22 08:45:05 -04:00
swung0x48 a5f36c8f8d [Feature, Test] (SelfTest): add a Known Driver Bugs POST section and probe the geometry write-after-emit drop 2026-08-22 07:49:17 -04:00
swung0x48 1ebe9d11c5 [Fix, Test] (GLState, DirectGLES): stop calling a multisample texture filter-incomplete so it still binds 2026-08-22 07:18:11 -04:00
swung0x48 a8bb63950d [Fix, Test] (ShaderTranspiler): relocate a late length constant so an offset atomic-counter block still flattens 2026-08-22 07:18:11 -04:00
swung0x48 7d68a17774 [Fix, Test] (DirectGLES): give a split buffer image its own view so the sampler still sees whole texels 2026-08-22 06:16:52 -04:00
swung0x48 415645ccdd [Merge] (ShaderTranspiler, DirectGLES): land the normalized-format carriers and the buffer-image split 2026-08-22 04:47:39 -04:00
swung0x48 e6d03eb2a1 [Merge] (DirectGLES): take the image-alias rename under the format carriers 2026-08-22 04:44:41 -04:00
swung0x48 5e29e7d266 [Feat, Test] (ShaderTranspiler, DirectGLES): split a non-core buffer image by its subscript instead of losing the stage 2026-08-22 04:30:06 -04:00
swung0x48 7eac33d17b [Feat, Fix, Test] (ShaderTranspiler, DirectGLES): carry the seven normalized image formats as their own codes in rgba16ui 2026-08-22 04:17:46 -04:00
Swung0x48 8f19ce6fa7 [Fix, Test] (DirectGLES): rename an image SPIRV-Cross already qualified so two stages cannot merge it 2026-08-22 03:46:35 -04:00
swung0x48 d0f7fb99db [Feat, Test] (ShaderTranspiler, DirectGLES): carry rgb10_a2ui storage images in rgba16ui and split its packed upload 2026-08-22 03:21:48 -04:00
swung0x48 d4247db6c3 [Fix, Test] (ShaderTranspiler, GLImpl, ProgramState, DirectVulkan): keep fp64 where the backend consumes it natively 2026-08-22 00:57:06 -04:00
swung0x48 e4f41e0fd3 [Merge] (ShaderTranspiler, DirectGLES): land the fp64 block layout, colour-index and readonly-writeonly repairs 2026-08-21 22:24:38 -04:00
swung0x48 9cf340cbef [Merge] (DirectGLES, ShaderTranspiler): take the viewport routing and image repairs under the fp64 and qualifier fixes 2026-08-21 22:21:17 -04:00
swung0x48 348a30a816 [Fix, Test] (ShaderTranspiler): keep a storage block with doubles at the byte layout it was bound with 2026-08-21 22:15:05 -04:00
swung0x48 b5e0ada97e [Merge] (DirectGLES, ShaderTranspiler, Config): land the viewport-array routing emulation 2026-08-21 22:13:50 -04:00
swung0x48 cb27ac7761 [Merge] (DirectGLES): take the image-widening repairs under the viewport routing 2026-08-21 22:10:53 -04:00
swung0x48 38c56a3d38 [Test] (DirectGLES, IntegrationTest): run the viewport-array scenarios on Espryt and pin the routing rewrites against a negative control 2026-08-21 22:08:33 -04:00
swung0x48 908172ba0f [Feat] (DirectGLES, Config, ShaderTranspiler): route gl_ViewportIndex on Espryt by replaying a draw per distinct viewport state 2026-08-21 22:08:25 -04:00
swung0x48 e18bac8cb2 [Fix, Test] (ShaderTranspiler, DirectGLES): never widen a buffer image - its texels are the application buffer, not storage we can reallocate 2026-08-21 22:02:23 -04:00
swung0x48 7b0f443d3a [Fix, Test] (ShaderTranspiler, DirectGLES): drop the inert readonly+writeonly pair a storage block cannot carry in ESSL 2026-08-21 21:43:42 -04:00
swung0x48 f1b4a5e07f [Fix, Test] (ShaderTranspiler, DirectGLES): stop printing the default fragment-output colour index into ESSL 2026-08-21 21:42:31 -04:00
swung0x48 f3cd4091bf [Fix, Test] (DirectGLES): decode the packed r11f_g11f_b10f shadow into the float level its rgba16f carrier is uploaded as 2026-08-21 21:38:45 -04:00
swung0x48 529d26f38f [Fix] (DirectGLES): arm the image-format widening for r11f_g11f_b10f in the reflection gate too 2026-08-21 21:27:14 -04:00
swung0x48 9bd125aeec [Fix, Test] (ShaderTranspiler): carry r11f_g11f_b10f storage images in rgba16f instead of losing the stage 2026-08-21 21:21:54 -04:00
swung0x48 ece9491d4b [Test] (ShaderTranspiler): pin the two capture holes the side-by-side corpus run found 2026-08-21 13:55:09 -04:00
swung0x48 51b4abd801 [Docs] (ShaderTranspiler, GLState): retire the comments that still describe the lexical side channels 2026-08-21 13:53:17 -04:00
swung0x48 cbb616093b [Refactor, Test] (ShaderTranspiler, GLState): take what the relaxed parse destroys from glslang instead of scanning the source 2026-08-21 13:51:22 -04:00
swung0x48 e5846569ca [Refactor] (ShaderTranspiler): bump the vendored glslang for the uniform-location snapshot and the atomic-counter offset check 2026-08-21 13:51:21 -04:00
swung0x48 194c2f189b [Merge] (ShaderTranspiler): land the macro-spelled storage-block binding repair 2026-08-21 12:16:52 -04:00
swung0x48 7de7cfc6eb [Fix, Test] (ShaderTranspiler): read a storage block's macro-spelled binding as declared, not as absent 2026-08-21 12:09:03 -04:00
swung0x48 03e69fc9ef [Merge] (ShaderTranspiler): land the single-implementation subroutine lowering and the imageSize select ladder 2026-08-21 11:59:49 -04:00
swung0x48 ee74c8ea3a [Merge] (DirectGLES): land the post-relink rebind repair 2026-08-21 11:59:49 -04:00
swung0x48 54bbe805e5 [Merge] (GLState): land the GL block and uniform enumeration repair 2026-08-21 11:59:49 -04:00
swung0x48 6e2a3b3496 [Fix, Test] (GLState): stop enumerating buffer variables as GL uniforms 2026-08-21 11:55:29 -04:00
swung0x48 f5a0779385 [Fix, Test] (GLState): keep the atomic-counter and storage blocks out of the GL uniform-block list 2026-08-21 11:54:45 -04:00
swung0x48 86fdc68efa [Fix, Test] (ShaderTranspiler): let the image-array select ladder carry an imageSize query, not just a read 2026-08-21 11:49:30 -04:00
swung0x48 1185265e22 [Fix, Test] (DirectGLES): re-bind the driver program after a relink so a stage the relink added reaches the draw 2026-08-21 11:47:03 -04:00
swung0x48 77c05b151a [Fix, Test] (ShaderTranspiler): lower a single-implementation GLSL subroutine to a forwarding call 2026-08-21 11:45:49 -04:00
swung0x48 fdbe0b3117 [Fix, Test] (DirectGLES, DirectVulkan, GLImpl, GLState): ask the last link, not the live attach list, what stages a program has 2026-08-21 08:01:48 -04:00
swung0x48 9c9739e1c3 [Merge] (DirectGLES, GLState, GLImpl, ShaderTranspiler): land GL43 wave6 and wave7 2026-08-21 07:15:13 -04:00
swung0x48 87548ae78a [Merge] (DirectGLES, GLState, GLImpl, ShaderTranspiler): land GL43 wave6 and wave7 with the image-uniform naming repair 2026-08-21 07:11:00 -04:00
swung0x48 2a902ff58c [Fix, Test] (ShaderTranspiler): bound the whole loop nest a fragment-output index marks for unrolling 2026-08-21 07:04:19 -04:00
swung0x48 a79eadd724 [Fix, Test] (ShaderTranspiler): bound the whole loop nest a resource-array index marks for unrolling 2026-08-21 07:04:19 -04:00
swung0x48 518e9c7796 [Fix, Test] (DirectGLES): read an image array subscript unsigned literal as the element index it is 2026-08-21 06:57:59 -04:00
swung0x48 a4c11f2603 [Fix, Test] (DirectGLES): arm the image-format widening for a baked format on every driver 2026-08-21 06:54:39 -04:00
swung0x48 37a656dedc [Fix, Test] (DirectGLES): name a repaired image uniform after its repair, not after its stage 2026-08-21 06:51:12 -04:00
swung0x48 3cc6b88767 [Fix, Test] (GLState): keep named-block members out of the GL uniform location pool 2026-08-21 06:36:47 -04:00
swung0x48 b32c35a113 [Fix, Test] (GLImpl): ask the stage, not GL_NONE, whether a geometry shader is active 2026-08-21 06:20:54 -04:00
swung0x48 8587b83be3 [Fix, Test] (ShaderTranspiler, DirectGLES): make every emitted image-array subscript a compile-time constant 2026-08-21 06:11:04 -04:00
swung0x48 0cef345d61 [Fix, Test] (GLState): give an atomic counter array its packed stride of four 2026-08-21 06:10:47 -04:00
swung0x48 31367de628 [Test] (MG_IntegrationTest): cover a storage block's default binding where another resource competes for it 2026-08-21 05:27:45 -04:00
swung0x48 bc4b62026a [Docs] (ShaderTranspiler): record the byte-exact fp64 block-layout evidence and its one misleading artifact 2026-08-21 05:24:49 -04:00
swung0x48 50da7de737 [Fix, Test] (DirectGLES, ShaderTranspiler): synthesize the pass-through tessellation control stage ES requires 2026-08-21 05:23:57 -04:00
swung0x48 d9def5c1bb [Fix, Test] (GLImpl, GLState): give a storage block with no binding qualifier GL's default binding of zero 2026-08-21 05:13:58 -04:00
swung0x48 21a4c8aa95 [Fix, Test] (ShaderTranspiler, DirectGLES): widen the offset and gradients of a 1D sampler lookup for ESSL 2026-08-21 04:57:52 -04:00
swung0x48 6317066add [Fix, Test] (DirectGLES): reach an image array's non-consecutive units by widening the array over their span 2026-08-21 04:38:59 -04:00
swung0x48 02b59bef80 [Fix, Test] (DirectGLES): give every repaired image uniform a per-stage name so no linker can merge two stages' qualifiers 2026-08-21 04:28:44 -04:00
swung0x48 668f3e90c9 [Fix] (DirectGLES, ShaderTranspiler): widen the formats SPIRV-Cross refuses to print even where GL_NV_image_formats exists 2026-08-21 04:13:01 -04:00
swung0x48 07d6277f87 [Test] (MG_IntegrationTest): cover GL's missing-channel semantics for a non-core image format 2026-08-21 03:54:16 -04:00
swung0x48 b164692387 [Test] (ShaderTranspiler): retarget the bake-decline pin at the formats no core carrier rescues 2026-08-21 03:48:59 -04:00
swung0x48 80ea44573e [Test] (ShaderTranspiler): pin the ESSL a widened image module emits, on both SPIRV-Cross failure modes 2026-08-21 03:34:27 -04:00
swung0x48 2a7d6f2e16 [Perf] (DirectGLES, ShaderTranspiler): fold the image-widening gate into the shared SPIR-V probe 2026-08-21 03:24:09 -04:00
swung0x48 3a12f6d4f3 [Fix, Test] (DirectGLES, ShaderTranspiler): emulate the 17 exactly-carriable non-core image formats by channel widening 2026-08-21 03:21:09 -04:00
swung0x48 36b9d26b9d [Fix] (DirectVulkan): match the 2_10_10_10 storage image view format to the texture's own 2026-08-21 02:54:47 -04:00
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
183 changed files with 40641 additions and 2121 deletions
+11
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
@@ -278,9 +279,12 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
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/FlattenFloat64StorageBlockPass.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
@@ -290,17 +294,23 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
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/Lower1DSampledImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.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/LegalizeResourceArrayIndexPass.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
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
MobileGL/MG_Util/SelfTest/DriverPost.cpp MobileGL/MG_Util/SelfTest/DriverPost.cpp
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
@@ -386,6 +396,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
+22
View File
@@ -203,6 +203,28 @@ 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;
// MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
// emulation - the builtin becomes a flat varying, the fragment stage gets a
// per-pass gate, and a routed draw is REPLAYED once per distinct viewport state
// with the real glViewport/glScissor/glDepthRangef set for it. Auto is ON, and
// it is ON even where the driver advertises GL_OES_viewport_array, because that
// extension only ever gave the SHADER a compilable name: MobileGL has never
// programmed a driver's INDEXED viewport state (SyncRenderState pushes index 0
// and nothing else), so on an extension-capable driver every index rasterized as
// index 0 exactly as it did without one. ForceOff returns to that behaviour -
// the pre-emulation path, extension passthrough where it exists and
// LowerViewportIndexPass' demote-to-a-plain-global where it does not - and is
// the negative control the emulation is measured against.
QuirkOverride ViewportArrayEmulation = QuirkOverride::Auto;
}; };
extern FeaturesTable Features; extern FeaturesTable Features;
} // namespace MobileGL::MG_Config } // namespace MobileGL::MG_Config
+3
View File
@@ -194,6 +194,9 @@ 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");
features.ViewportArrayEmulation =
QueryEnvQuirkOverride("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION");
} }
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) {
+88 -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;
@@ -383,12 +445,36 @@ namespace MobileGL {
const Uint32 bit = PerLayerFramebufferAttachmentBit(target); const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0; return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
} }
// Whether this backend can CONSUME a shader module that still declares 64-bit floats,
// i.e. whether `double` survives the transpile instead of being narrowed to `float`
// (ShaderTranspiler::DemoteFloat64Pass). Detected, never assumed:
// * DirectVulkan sets it from VkPhysicalDeviceFeatures::shaderFloat64, the feature
// VUID-VkShaderModuleCreateInfo-pCode-08740 requires before a module declaring
// OpCapability Float64 may be created at all. lavapipe has it; Adreno and Mali
// both report VK_FALSE, so no real mobile device does.
// * DirectGLES can NEVER have it. GLSL ES has no 64-bit float type in any version
// or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES
// profile") and the demotion there is mathematically mandatory, always.
// Defaults to false so a backend that never sets it - and the no-backend case, which
// is what standalone shader compiles and the unit tests run under - keeps the
// demotion, which is the behaviour that works everywhere.
Bool SupportsShaderFloat64 = false;
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e. // Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected, // whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the // never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the // attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at // bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
// all. Defaults to false so a backend that never sets it gets the conservative answer. // all. Defaults to false so a backend that never sets it gets the conservative answer.
//
// INDEPENDENT of SupportsShaderFloat64, and it has to be: this flag decides a VkFormat
// from the VAO ATTRIBUTE alone, which does not know what type the shader declared, and
// glVertexAttribFormat(GL_DOUBLE) feeding a plain `in vec4` is both legal and common
// (KHR-GL43.vertex_attrib_binding.basic-input-case4/5, advanced-bindingUpdate). A
// backend with native fp64 that still cannot FETCH 64 bits keeps this false and relies
// on the per-MODULE rule in ShaderCompiler::SanitizeAndOptimizeBinary instead: a vertex
// module that declares a 64-bit float INPUT is demoted whole, so the two shader-side
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
// still see one consistent world.
Bool SupportsFloat64VertexAttributes = false; Bool SupportsFloat64VertexAttributes = false;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024; SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Uint32 SubgroupSize = 0; Uint32 SubgroupSize = 0;
@@ -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
@@ -1241,14 +1331,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray); DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
} }
} }
// Not a driver question and never will be: OpenGL ES has no double-precision vertex format // Not a driver question and never will be: GLSL ES has no 64-bit float type in ANY version
// and ESSL has no fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to // or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES profile") and a
// land on this backend regardless of what the driver underneath happens to support. // module that still declared Float64 would never reach the driver at all. The demotion is
// mathematically mandatory here, on every device, forever - which is why this stays false
// regardless of what the driver underneath happens to support.
m_dynamicParameters.SupportsShaderFloat64 = false;
// Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
// fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
m_dynamicParameters.SupportsFloat64VertexAttributes = false; m_dynamicParameters.SupportsFloat64VertexAttributes = false;
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers; m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
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;
File diff suppressed because it is too large Load Diff
+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
+291 -3
View File
@@ -21,6 +21,29 @@ 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);
// The ESSL half of the gl_ViewportIndex routing emulation, in the order a program's stages
// meet it. Both are pure String -> String rewrites over what SPIRV-Cross emitted once
// LowerViewportIndexPass has demoted the builtin to the plain global `mg_ViewportIndex`.
//
// The producing stage's global becomes an ordinary flat varying; true when there was one to
// promote, which is also the answer to "does this program route viewports at all".
Bool PromoteViewportIndexGlobalToVarying(String& source);
// The fragment stage grows a matching flat input, the mg_ViewportPassMask uniform the draw
// path writes, and a wrapper entry point that discards every fragment whose primitive routed
// to an index the current replay pass is not drawing. False when the stage has no entry point
// to wrap, which leaves the program renderable but unrouted.
Bool InjectViewportIndexPassGate(String& source);
// 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
@@ -103,6 +126,58 @@ namespace MobileGL::MG_Backend::DirectGLES {
// link. // link.
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices); Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices);
// ---- gl_ViewportIndex routing emulation, draw half ---------------------------------------
//
// GLES has ONE viewport, ONE scissor rectangle and ONE depth range; GL 4.1 has sixteen of
// each, selected per primitive by gl_ViewportIndex. There is no ES entry point to program the
// other fifteen with (GL_OES_viewport_array exists but Adreno 830 does not have it, verified
// three ways), so the only way to rasterize a primitive against index i's rectangle is to
// make index i's rectangle THE viewport for the duration of a draw - which means issuing the
// draw once per distinct viewport state and letting the fragment stage throw away the
// primitives that belong to the other indices (the gate Managers.cpp injects).
//
// Indices whose whole state tuple (viewport rectangle, scissor rectangle, scissor-test enable,
// depth range) is identical share ONE pass, so the overwhelmingly common case - every index
// still holding what glViewport/glScissor/glDepthRange broadcast to all sixteen - collapses
// to a single pass with an all-ones gate mask, i.e. one draw and no behaviour change at all.
//
// Whether emulation runs. Off only under MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION falsy, which
// restores the pre-emulation path as a negative control.
Bool ViewportArrayEmulationEnabled();
// Whether ANY program built in this process has come out with a viewport gate. Sticky once
// true; it exists so that BeginViewportRoutingPasses - which runs on every draw of every
// workload - can answer with one static load in the case that matters, which is every
// application that has never heard of gl_ViewportIndex.
extern Bool g_anyProgramRoutesViewportIndex;
// Number of times the current draw has to be issued. Always >= 1, and exactly 1 - with no
// state touched - whenever the current program does not route viewports, whenever every
// configured index shares one state, and whenever replaying would multiply a side effect the
// fragment gate cannot undo (transform feedback, rasterizer discard). Also seeds the pass
// mask uniform for that single-pass case, so a gated fragment shader never runs against the
// zero every GLSL uniform starts at - which would discard the whole draw.
Uint BeginViewportRoutingPasses();
// Push pass `pass`'s viewport / scissor / scissor-test / depth range onto the ES context and
// set the gate mask to the indices it serves. Only called when the count above exceeds 1.
void ApplyViewportRoutingPass(Uint pass);
// Restore the gate mask and mark the render-state shadow dirty, so the next ordinary draw
// re-pushes index 0's state. Takes the count so it can do nothing at all in the common case.
void EndViewportRoutingPasses(Uint passCount);
// Issue one draw, replayed once per viewport-routing pass. Every application-visible draw
// entry point wraps its native glDraw* call in this; the internal blit and clear helpers
// deliberately do not, because they bind their own programs, which never route.
template <typename IssueDraw>
inline void ForEachViewportRoutingPass(IssueDraw&& issue) {
const Uint passCount = BeginViewportRoutingPasses();
for (Uint pass = 0; pass < passCount; ++pass) {
if (passCount > 1) {
ApplyViewportRoutingPass(pass);
}
issue();
}
EndViewportRoutingPasses(passCount);
}
template <typename StateObject, typename BackendObject> template <typename StateObject, typename BackendObject>
class StateBackendObjectRegistry { class StateBackendObjectRegistry {
public: public:
@@ -129,7 +204,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 +299,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 +461,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 +574,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
@@ -604,9 +745,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Returns `data` untouched when no widening applies. Pure CPU and context-free so a unit // Returns `data` untouched when no widening applies. Pure CPU and context-free so a unit
// test can exercise the exact packing the driver is handed; `widenedData` is the caller's // test can exercise the exact packing the driver is handed; `widenedData` is the caller's
// scratch buffer and has to outlive the returned pointer. // scratch buffer and has to outlive the returned pointer.
// `alphaOneCodeOverride`, when non-zero, replaces the value written into the synthetic
// alpha channel: an image carrier that holds a NORMALIZED format's channel CODES has to
// pad alpha with that channel's saturated CODE (65535, 32767, 3), which neither of the
// transfer type's own "ones" is.
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, const void* data, const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, const void* data,
SizeT byteSize, GLenum uploadType, Vector<Uint8>& widenedData, SizeT byteSize, GLenum uploadType, Vector<Uint8>& widenedData,
Bool integerData = false); Bool integerData = false, Uint32 alphaOneCodeOverride = 0u);
// Splits a GL_UNSIGNED_INT_2_10_10_10_REV shadow (rgb10_a2, rgb10_a2ui) into the four
// GL_UNSIGNED_SHORT channel CODES its GL_RGBA16UI image carrier is uploaded as: red in
// bits 0-9, green 10-19, blue 20-29, alpha 30-31. Pure CPU and context-free so a unit test
// can pin the exact fields; `widenedData` is the caller's scratch and has to outlive the
// returned pointer.
const void* PreparePackedIntWidenedUpload(const IntVec3& texelSize, const void* data, SizeT byteSize,
Vector<Uint8>& widenedData);
struct StateTextureBasicInfo { // Used for tracking texture state changes struct StateTextureBasicInfo { // Used for tracking texture state changes
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown; TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
@@ -641,10 +794,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject); void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject); void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject); void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void RequireImageBindableStorage(); // Marks the texture as one whose ES storage has to be image-bindable, which for a
// non-core image format means re-minting it in the widening's carrier. Takes the state
// object because the levels already uploaded have to be marked dirty again: the
// re-mint allocates fresh storage and only replays what the shadow still calls dirty.
void RequireImageBindableStorage(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
// Whether this texture's ES storage was minted in an image carrier rather than in the
// frontend format's own layout - the readback has to ask, because for a NORMALIZED
// carrier the storage is an integer texture holding codes and glGetTexImage still owes
// the application floats.
Bool RequiresImageBindableStorage() const { return m_imageBindableStorageRequired; }
void Bind(GLenum target, Uint unit = TempTextureUnit); void Bind(GLenum target, Uint unit = TempTextureUnit);
Uint GetBackendTextureId() const; Uint GetBackendTextureId() const;
// The id to hand glBindImageTexture for a SPLIT buffer image, or 0 when this texture
// takes no split. See m_bufferImageSplitViewId.
Uint GetBufferImageSplitViewId() const { return m_bufferImageSplitViewId; }
// Aggregate first-level clean gate for the per-draw trio // Aggregate first-level clean gate for the per-draw trio
// SyncTextureParamsToBackend + SyncBuiltinSamplerToBackend + // SyncTextureParamsToBackend + SyncBuiltinSamplerToBackend +
// SyncMipmapsToBackend: EXACTLY the conjunction of their own early-outs // SyncMipmapsToBackend: EXACTLY the conjunction of their own early-outs
@@ -681,6 +848,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
void RecreateBackendTexture(); void RecreateBackendTexture();
Uint m_backendTextureId = 0; Uint m_backendTextureId = 0;
// A SECOND buffer-texture name over the SAME buffer object, viewed in the split's
// single-channel base format, used only as the glBindImageTexture target.
//
// The split needs the view to say r32f where the application said rg32f, but a buffer
// texture that is image-bound may ALSO be read through a samplerBuffer - and the
// sampler side is not subscript-rewritten, so re-describing the application's own
// texture broke it: texelFetch(s, i) returned component 2i of the base view instead of
// texel i's pair. That is exactly and only
// KHR-GL42/43.shader_image_load_store.advanced-sync-imageAccess, which image-stores
// into a GL_RG32F buffer texture and then reads the same texture through both an
// imageBuffer and a samplerBuffer in one shader, comparing the two.
//
// Two names over one buffer cost nothing and alias exactly: a buffer texture owns no
// storage, so both views are the application's bytes, and the split's whole premise is
// that the two describe the same memory. The application's own name therefore keeps
// the format it asked for - rg32f IS a legal SAMPLED buffer-texture format in ES 3.2,
// it is only the IMAGE binding ES cannot spell - and the private name below carries
// the split the shader was rewritten against. 0 when this texture takes no split.
Uint m_bufferImageSplitViewId = 0;
// ES context generation the id was created under; a dtor running after // ES context generation the id was created under; a dtor running after
// that context died must not delete a foreign (recycled) name. // that context died must not delete a foreign (recycled) name.
Uint m_contextGeneration = 0; Uint m_contextGeneration = 0;
@@ -1027,23 +1213,51 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Image uniforms take their unit from the layout(binding=N) qualifier baked into // Image uniforms take their unit from the layout(binding=N) qualifier baked into
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be // the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
// assigned through glUniform1i. // assigned through glUniform1i.
//
// ALL THIRTY-THREE of them, in the one contiguous block ARB_shader_image_load_store allocated
// (GL_IMAGE_1D 0x904C through GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY 0x906C). The list
// used to hold only the fifteen whose TARGET exists in ES, which read as a reasonable
// shortcut and was two bugs: an image uniform this says "no" to is one
// CollectImageFormatBakeInputs never walks, so its non-core format is neither baked nor
// widened and SPIRV-Cross throws for the whole stage ("Attempting to use image format not
// supported in ES profile"), and it is also one SyncToBackend then treats as a SAMPLER and
// assigns with glUniform1i, which ES makes an INVALID_OPERATION. A GL_TEXTURE_CUBE_MAP_ARRAY
// image - which ES 3.2 has in core, so it is not even an emulated target - hit both.
inline Bool IsImageUniformType(GLenum type) { inline Bool IsImageUniformType(GLenum type) {
switch (type) { switch (type) {
case 0x904C: /*GL_IMAGE_1D*/
case 0x904D: /*GL_IMAGE_2D*/ case 0x904D: /*GL_IMAGE_2D*/
case 0x904E: /*GL_IMAGE_3D*/ case 0x904E: /*GL_IMAGE_3D*/
case 0x904F: /*GL_IMAGE_2D_RECT*/
case 0x9050: /*GL_IMAGE_CUBE*/ case 0x9050: /*GL_IMAGE_CUBE*/
case 0x9051: /*GL_IMAGE_BUFFER*/ case 0x9051: /*GL_IMAGE_BUFFER*/
case 0x9052: /*GL_IMAGE_1D_ARRAY*/
case 0x9053: /*GL_IMAGE_2D_ARRAY*/ case 0x9053: /*GL_IMAGE_2D_ARRAY*/
case 0x9054: /*GL_IMAGE_CUBE_MAP_ARRAY*/
case 0x9055: /*GL_IMAGE_2D_MULTISAMPLE*/
case 0x9056: /*GL_IMAGE_2D_MULTISAMPLE_ARRAY*/
case 0x9057: /*GL_INT_IMAGE_1D*/
case 0x9058: /*GL_INT_IMAGE_2D*/ case 0x9058: /*GL_INT_IMAGE_2D*/
case 0x9059: /*GL_INT_IMAGE_3D*/ case 0x9059: /*GL_INT_IMAGE_3D*/
case 0x905A: /*GL_INT_IMAGE_2D_RECT*/
case 0x905B: /*GL_INT_IMAGE_CUBE*/ case 0x905B: /*GL_INT_IMAGE_CUBE*/
case 0x905C: /*GL_INT_IMAGE_BUFFER*/ case 0x905C: /*GL_INT_IMAGE_BUFFER*/
case 0x905D: /*GL_INT_IMAGE_1D_ARRAY*/
case 0x905E: /*GL_INT_IMAGE_2D_ARRAY*/ case 0x905E: /*GL_INT_IMAGE_2D_ARRAY*/
case 0x905F: /*GL_INT_IMAGE_CUBE_MAP_ARRAY*/
case 0x9060: /*GL_INT_IMAGE_2D_MULTISAMPLE*/
case 0x9061: /*GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY*/
case 0x9062: /*GL_UNSIGNED_INT_IMAGE_1D*/
case 0x9063: /*GL_UNSIGNED_INT_IMAGE_2D*/ case 0x9063: /*GL_UNSIGNED_INT_IMAGE_2D*/
case 0x9064: /*GL_UNSIGNED_INT_IMAGE_3D*/ case 0x9064: /*GL_UNSIGNED_INT_IMAGE_3D*/
case 0x9065: /*GL_UNSIGNED_INT_IMAGE_2D_RECT*/
case 0x9066: /*GL_UNSIGNED_INT_IMAGE_CUBE*/ case 0x9066: /*GL_UNSIGNED_INT_IMAGE_CUBE*/
case 0x9067: /*GL_UNSIGNED_INT_IMAGE_BUFFER*/ case 0x9067: /*GL_UNSIGNED_INT_IMAGE_BUFFER*/
case 0x9068: /*GL_UNSIGNED_INT_IMAGE_1D_ARRAY*/
case 0x9069: /*GL_UNSIGNED_INT_IMAGE_2D_ARRAY*/ case 0x9069: /*GL_UNSIGNED_INT_IMAGE_2D_ARRAY*/
case 0x906A: /*GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY*/
case 0x906B: /*GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE*/
case 0x906C: /*GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY*/
return true; return true;
default: default:
return false; return false;
@@ -1051,6 +1265,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
@@ -1110,7 +1327,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendProgramObjectImpl(); BackendProgramObjectImpl();
~BackendProgramObjectImpl(); ~BackendProgramObjectImpl();
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject); void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
void Use() const; void Use();
void SetBaseInstance(Uint32 baseInstance) const; void SetBaseInstance(Uint32 baseInstance) const;
void SetBaseInstanceWordIndex(Int32 wordIndex) const; void SetBaseInstanceWordIndex(Int32 wordIndex) const;
void SetDrawID(Uint32 drawId) const; void SetDrawID(Uint32 drawId) const;
@@ -1121,6 +1338,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Same for gl_BaseVertex: only a program that reads it pays for the per-draw // Same for gl_BaseVertex: only a program that reads it pays for the per-draw
// uniform write, and only such a program needs the reset after one. // uniform write, and only such a program needs the reset after one.
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; } Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
// Which viewport indices the next draw's fragments may keep, one bit each. Written
// once per replay pass; see ForEachViewportRoutingPass.
void SetViewportPassMask(Uint32 indexMask) const;
// True when this build injected the fragment-stage viewport gate, i.e. when a
// pre-rasterization stage routes by gl_ViewportIndex AND the fragment stage can act
// on it. The uniform is the honest test for both halves: it exists only where the
// gate was injected, and the gate is injected only where a stage routes.
Bool RoutesViewportIndex() const { return m_viewportPassMaskUniformLocation >= 0; }
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; } Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
Uint GetBackendProgramId() const { return m_backendProgramId; } Uint GetBackendProgramId() const { return m_backendProgramId; }
// False when the last SyncToBackend could not produce a usable program (a // False when the last SyncToBackend could not produce a usable program (a
@@ -1136,6 +1361,22 @@ 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; }
// GL_PATCH_VERTICES the synthesized pass-through tessellation control stage was built
// for, or -1 when this program needed no such stage. Another of the same shape as the
// signatures above: the value is compiled INTO the synthesized stage as
// `layout(vertices = N) out`, so a program built for one patch size is stale for
// another and the draw path has to say so. -1 compares equal to itself for every
// program that has a control stage of its own, i.e. for all but a handful.
Int GetPassthroughTessControlPatchVertices() const {
return m_passthroughTessControlPatchVertices;
}
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 +1425,33 @@ 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);
// Builds, compiles and attaches the pass-through tessellation control stage GL 4.6
// core 11.2.2 describes, for a program that has an evaluation stage and none of its
// own - which ES 3.2 rejects outright. Called from SyncToBackend after every real
// stage has been attached and before the link; see the definition for why it cannot
// regress a program that works today.
void AttachPassthroughTessControlStage(
const MG_State::GLState::ProgramObject& stateProgramObject, Int tessEvalShaderIndex,
const Vector<Vector<unsigned int>>& shaderSpirvs, const String& vertexStageEssl,
const String& tessEvalStageEssl);
// 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
@@ -1194,6 +1462,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Int m_drawIdUniformLocation = -1; Int m_drawIdUniformLocation = -1;
Int m_baseVertexUniformLocation = -1; Int m_baseVertexUniformLocation = -1;
Int m_baseInstanceWordIndexUniformLocation = -1; Int m_baseInstanceWordIndexUniformLocation = -1;
Int m_viewportPassMaskUniformLocation = -1;
Int m_indirectParamsBinding = -1; Int m_indirectParamsBinding = -1;
Uint32 m_snormFallbackClampOutputMask = 0; Uint32 m_snormFallbackClampOutputMask = 0;
Uint32 m_unormFallbackClampOutputMask = 0; Uint32 m_unormFallbackClampOutputMask = 0;
@@ -1202,8 +1471,19 @@ 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;
// -1 for every program that has a tessellation control stage of its own (or none at
// all); otherwise the GL_PATCH_VERTICES the synthesized pass-through stage was built
// with. See GetPassthroughTessControlPatchVertices.
Int m_passthroughTessControlPatchVertices = -1;
Bool m_isInitialized = false; Bool m_isInitialized = false;
Bool m_backendProgramUsable = false; Bool m_backendProgramUsable = false;
// Set by SyncToBackend every time it relinks the driver program, cleared by the
// next Use(). Use() dedupes on a GL program NAME, and a relink replaces the
// executable behind that name without changing it - see the note at the
// glLinkProgram in SyncToBackend for what the driver runs otherwise.
Bool m_rebindAfterRelink = false;
Int m_globalUboBackendBlockIndex = -1; Int m_globalUboBackendBlockIndex = -1;
Int m_globalUboBackendBlockSize = 0; Int m_globalUboBackendBlockSize = 0;
@@ -1293,6 +1573,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Some format in play - declared or baked - is outside the GLSL ES core image // Some format in play - declared or baked - is outside the GLSL ES core image
// format set, so the emitted ESSL needs the GL_NV_image_formats directive. // format set, so the emitted ESSL needs the GL_NV_image_formats directive.
Bool needsExtendedImageFormats = false; Bool needsExtendedImageFormats = false;
// Some DECLARED format in play is one WidenImageFormatsForEssl will re-declare in a
// core carrier. Answered from the uniform reflection rather than from a module parse
// on purpose: the widening is armed on every driver, so a per-stage BuildModule to
// find out would land on every stage of every program - which is the cost
// SpirvGateFeatures exists to avoid. Program-wide, so it can over-arm a stage that
// declares no image; the pass then finds nothing, reports no change, and the caller
// keeps the module it already had.
Bool declaresWidenableImageFormat = false;
}; };
ImageFormatBakeInputs CollectImageFormatBakeInputs( ImageFormatBakeInputs CollectImageFormatBakeInputs(
const MG_State::GLState::ProgramObject& stateProgramObject); const MG_State::GLState::ProgramObject& stateProgramObject);
+18 -8
View File
@@ -414,14 +414,18 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const Uint previousIndirectBinding = BoundDrawIndirectBufferId(); const Uint previousIndirectBinding = BoundDrawIndirectBufferId();
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id); BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id);
if (batched) { if (batched) {
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase), ForEachViewportRoutingPass([&] {
drawcount, 0); g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
drawcount, 0);
});
} else { } else {
for (GLsizei i = 0; i < drawcount; ++i) { for (GLsizei i = 0; i < drawcount; ++i) {
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i)); if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0); if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand); const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset)); ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
});
} }
if (feedDrawID) SetCurrentDrawID(0); if (feedDrawID) SetCurrentDrawID(0);
if (feedBaseVertex) SetCurrentBaseVertex(0); if (feedBaseVertex) SetCurrentBaseVertex(0);
@@ -442,8 +446,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
if (count[i] <= 0) continue; if (count[i] <= 0) continue;
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i)); if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0); if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i], ForEachViewportRoutingPass([&] {
basevertex ? basevertex[i] : 0); g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
basevertex ? basevertex[i] : 0);
});
} }
if (feedDrawID) SetCurrentDrawID(0); if (feedDrawID) SetCurrentDrawID(0);
if (feedBaseVertex) SetCurrentBaseVertex(0); if (feedBaseVertex) SetCurrentBaseVertex(0);
@@ -515,8 +521,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// driver sees none - but gl_BaseVertex still has to report the value the // driver sees none - but gl_BaseVertex still has to report the value the
// application passed for this sub-draw. // application passed for this sub-draw.
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0); if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT, ForEachViewportRoutingPass([&] {
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32))); g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
});
cursor += static_cast<SizeT>(count[i]); cursor += static_cast<SizeT>(count[i]);
} }
if (feedDrawID) SetCurrentDrawID(0); if (feedDrawID) SetCurrentDrawID(0);
@@ -870,7 +878,9 @@ void main() {
if (flattened.indexCount != 0) { if (flattened.indexCount != 0) {
const Uint previousIndexBinding = BoundIndexBufferId(); const Uint previousIndexBinding = BoundIndexBufferId();
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId); BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId);
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr); ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
});
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding); BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
return; return;
} }
+627 -28
View File
@@ -12,6 +12,7 @@
#include "MG_Backend/BackendObjects.h" #include "MG_Backend/BackendObjects.h"
#include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h" #include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h"
#include "MG_Util/Texture/TextureFormatProcessor.h" #include "MG_Util/Texture/TextureFormatProcessor.h"
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h> #include <MG_Util/BackendLoaders/OpenGL/Loader.h>
@@ -171,6 +172,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;
} }
@@ -227,6 +234,105 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat) { Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat) {
return BackendFormatAddsAlpha(internalFormat, GetRenderbufferFormatCapabilityTargetIndex()); return BackendFormatAddsAlpha(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
} }
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat) {
const GLenum requested = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
const auto carrier = static_cast<GLenum>(
MG_Util::ShaderTranspiler::ShaderCompiler::WidenedCoreEsslImageFormat(requested));
if (carrier == 0) {
return {};
}
// EXACTLY the arming WidenImageFormatsForEssl uses, and it has to be: the shader, the
// storage and the bind must all widen or none of them may, or the shader addresses a
// texel size the storage does not have (which every driver tested accepts silently,
// reading and writing out of bounds).
//
// A driver WITH GL_NV_image_formats can spell the narrow format - but only for the
// formats SPIRV-Cross will actually print. It throws for its is_desktop_only_format
// set instead of emitting a token, and the throw loses the stage whatever the driver
// would have accepted: on Mesa, which advertises the extension, `layout(r8ui)
// uimage2D` still lost its whole program until the widening ran for it too.
if (g_GLESCapabilities.SupportsExtendedImageFormats &&
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(requested)) {
return {};
}
ImageBindableStorageWidening widening;
widening.InternalFormat = carrier;
widening.SourceChannels =
MG_Util::ShaderTranspiler::ShaderCompiler::ImageFormatChannelCount(requested);
switch (carrier) {
case GL_RGBA32UI:
case GL_RGBA16UI:
case GL_RGBA8UI:
case GL_RGBA32I:
case GL_RGBA16I:
case GL_RGBA8I:
widening.IntegerData = true;
break;
default:
widening.IntegerData = false;
break;
}
// The carrier is a core ES format in every case, so it needs no fallback options of
// its own; this call is only here to spell the transfer pair that describes it.
MG_Util::TextureFormatProcessor::NormalizePixelFormat(carrier, Flags<PixelFormatNormalizeOptionBit>{},
nullptr, &widening.Format, &widening.Type);
// The two carriers that are not channel widenings, whose transfer pair has to say so.
// Every other entry keeps the frontend format's own component type - a GL_RG16F shadow
// is halves and so is its GL_RGBA16F carrier, so padding the channels is the whole
// conversion. These two shadows are a PACKED 32-bit word per texel
// (TextureFormatProcessor::NormalizePixelFormat), and no ES driver accepts either
// packed type for the carrier's level, so the transfer names the carrier's own layout
// and PrepareImageWidenedUpload splits the word into it.
switch (internalFormat) {
case TextureInternalFormat::R11FG11FB10F:
// GL_UNSIGNED_INT_10F_11F_11F_REV -> GL_RGBA / GL_FLOAT, legal for GL_RGBA16F.
widening.Format = GL_RGBA;
widening.Type = GL_FLOAT;
widening.SourceEncoding = ImageWidenSourceEncoding::PackedFloat11f11f10f;
break;
case TextureInternalFormat::RGB10A2UI:
case TextureInternalFormat::RGB10A2:
// GL_UNSIGNED_INT_2_10_10_10_REV -> the GL_RGBA_INTEGER / GL_UNSIGNED_SHORT the
// GL_RGBA16UI carrier already asked for above; only the split is new. The two
// formats share it: rgb10_a2's channel codes are the same fields rgb10_a2ui's are,
// and what the shader divides them by is not the transfer's business.
widening.SourceEncoding = ImageWidenSourceEncoding::PackedInt2101010Rev;
break;
default:
break;
}
// The seven normalized formats whose carrier holds CODES rather than values. Both
// halves of the transfer need to know: a missing alpha is padded with the saturated
// code rather than the integer 1, and glGetTexImage has to divide the codes back out.
bool signedNormalized = false;
Uint32 channelMax[4] = {0u, 0u, 0u, 0u};
if (MG_Util::ShaderTranspiler::ShaderCompiler::NormalizedImageCarrierCodes(requested, channelMax,
signedNormalized)) {
for (SizeT channel = 0; channel < 4; ++channel) {
widening.ChannelMax[channel] = channelMax[channel];
}
widening.SignedNormalized = signedNormalized;
}
return widening;
}
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat) {
const GLenum requested = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
const auto base = static_cast<GLenum>(
MG_Util::ShaderTranspiler::ShaderCompiler::SplitCoreEsslBufferImageFormat(requested));
if (base == 0) {
return GL_UNKNOWN_MGL;
}
// EXACTLY the arming WidenImageFormatsForEssl uses, for the reason the widening's is:
// the shader, the glTexBuffer view and the glBindImageTexture argument must all split
// or none of them may, or the shader subscripts a view the buffer is not described as.
if (g_GLESCapabilities.SupportsExtendedImageFormats &&
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(requested)) {
return GL_UNKNOWN_MGL;
}
return base;
}
} // namespace TextureImpl } // namespace TextureImpl
namespace PrgramImpl { namespace PrgramImpl {
String ProcessOutColorLocations(const String& glslCode) { String ProcessOutColorLocations(const String& glslCode) {
@@ -569,6 +675,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
@@ -657,6 +800,82 @@ namespace MobileGL::MG_Backend::DirectGLES {
return result; return result;
} }
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, const Bool input) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// Deliberately a scan for the DECLARATION rather than a regex over the whole text:
// "gl_PerVertex" also appears inside the block's own body in some emissions, and the
// direction keyword has to be the one immediately preceding the name for the match to
// mean what this needs it to mean.
const auto isIdentifierChar = [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '_';
};
const String keyword = input ? String("in") : String("out");
SizeT pos = 0;
while ((pos = essl.find("gl_PerVertex", pos)) != String::npos) {
// Walk back over whitespace to the direction keyword.
SizeT before = pos;
while (before > 0 && std::isspace(static_cast<unsigned char>(essl[before - 1]))) --before;
const Bool matches = before >= keyword.size() &&
essl.compare(before - keyword.size(), keyword.size(), keyword) == 0 &&
(before == keyword.size() ||
!isIdentifierChar(essl[before - keyword.size() - 1]));
if (!matches) {
pos += 1;
continue;
}
const SizeT open = essl.find('{', pos);
if (open == String::npos) return std::nullopt;
const SizeT close = essl.find('}', open);
if (close == String::npos) return std::nullopt;
return essl.substr(open + 1, close - open - 1);
}
return std::nullopt;
}
String BuildPassthroughTessControlEssl(const Uint esslVersion, const Uint patchVertices,
const String& inPerVertexMembers,
const String& outPerVertexMembers) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// Tessellation is core in ES 3.2 and reachable in 3.1 only through
// GL_EXT_tessellation_shader. The caller has already established that the driver runs
// the evaluation stage at all, so the only question here is which spelling to use.
const Bool core = esslVersion >= 320;
String source = "#version " + std::to_string(core ? 320u : 310u) + " es\n";
if (!core) {
source += "#extension GL_EXT_tessellation_shader : require\n";
}
source += "precision highp float;\n";
source += "precision highp int;\n";
source += "layout(vertices = " + std::to_string(patchVertices) + ") out;\n";
// Mirrored, never invented. An empty member list means the neighbouring stage did not
// redeclare the block either, and the driver's own built-in declaration is then what
// both sides agree on - redeclaring here would be the thing that broke the match.
if (!inPerVertexMembers.empty()) {
source += "in gl_PerVertex {" + inPerVertexMembers + "} gl_in[gl_MaxPatchVertices];\n";
}
if (!outPerVertexMembers.empty()) {
source += "out gl_PerVertex {" + outPerVertexMembers + "} gl_out[];\n";
}
source += "void main() {\n";
// Only gl_Position is forwarded. That is the whole of what the pass-through owes the
// evaluation stage: a program whose evaluation stage reads anything else per-vertex
// was declined before this was ever called (ModuleReadsLocatedInput), and gl_PointSize
// from a tessellation stage is a separate capability on both targets.
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
source += " gl_TessLevelOuter[0] = 1.0;\n";
source += " gl_TessLevelOuter[1] = 1.0;\n";
source += " gl_TessLevelOuter[2] = 1.0;\n";
source += " gl_TessLevelOuter[3] = 1.0;\n";
source += " gl_TessLevelInner[0] = 1.0;\n";
source += " gl_TessLevelInner[1] = 1.0;\n";
source += "}\n";
return source;
}
namespace { namespace {
Bool IsImagePassIdentifierChar(char c) { Bool IsImagePassIdentifierChar(char c) {
return std::isalnum(static_cast<unsigned char>(c)) || c == '_'; return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
@@ -768,6 +987,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
struct ImageUniformDecl { struct ImageUniformDecl {
String name; String name;
String aliasName; // the repair-tagged name the rewritten declaration takes; empty
// for a declaration this pass leaves alone
String writeName; // the writeonly half's name, when split String writeName; // the writeonly half's name, when split
String layout; // raw contents of layout(...) String layout; // raw contents of layout(...)
String qualifiers; // memory/precision qualifiers, normalized, no trailing space String qualifiers; // memory/precision qualifiers, normalized, no trailing space
@@ -775,19 +996,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
String arraySuffix; // "" or "[7]" String arraySuffix; // "" or "[7]"
SizeT declStart = 0; SizeT declStart = 0;
SizeT declLength = 0; SizeT declLength = 0;
SizeT nameStart = 0; // the name token alone, for a rename that edits nothing else
SizeT nameLength = 0;
SizeT referenceCount = 0; // uses this pass recognized and accounted for SizeT referenceCount = 0; // uses this pass recognized and accounted for
Bool loaded = false; Bool loaded = false;
Bool stored = false; Bool stored = false;
Bool unknownUse = false; Bool unknownUse = false;
Bool split = false; Bool split = false;
// SPIRV-Cross already tagged this one readonly or writeonly, so it needs no
// qualifier repair - only the rename that keeps two stages from merging it.
Bool preTaggedReadonly = false;
Bool preTaggedWriteonly = false;
}; };
// 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()) {
@@ -802,11 +1039,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
return out; return out;
} }
// A name for the writeonly half that no identifier in the shader (and no other // A name for a rewritten declaration that no identifier in the shader (and no other
// half already minted) can collide with. // alias already minted for this stage) can collide with.
String MakeImageWriteAliasName(const String& name, const String& source, String MakeImageAliasName(const String& prefix, const String& name, const String& source,
const Vector<String>& taken) { const Vector<String>& taken) {
String candidate = String(IMAGE_WRITE_ALIAS_PREFIX) + name; String candidate = prefix + name;
// "__" anywhere in an identifier is reserved (GLSL ES 3.20 3.7), which a name // "__" anywhere in an identifier is reserved (GLSL ES 3.20 3.7), which a name
// that already starts with '_' would otherwise produce. // that already starts with '_' would otherwise produce.
for (SizeT doubled = candidate.find("__"); doubled != String::npos; for (SizeT doubled = candidate.find("__"); doubled != String::npos;
@@ -829,12 +1066,265 @@ 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) { namespace {
// The digits of an array extent or of an element subscript, or -1 for "not a plain
// decimal literal".
//
// One trailing `u`/`U` is PART of the literal rather than grounds for rejection.
// SPIRV-Cross prints an index in the type SPIR-V gave it, and
// LegalizeResourceArrayIndexPass mints its per-element constants in the type of the
// index it replaced (ConstantLikeIndex reads that index's own type_id), so an image
// array reached through anything unsigned - `for (uint i = 0u; i < 4u; ++i)`, or any
// expression on gl_LocalInvocationIndex, which is uint by definition - arrives here
// spelled `g_image[0u]`. Reading that as "not a literal" declined the array and left
// it on one layout(binding = N), which hands its elements the consecutive units
// N, N+1, ... - exactly the silently-wrong-units defect the split exists to remove.
Int ParseNonNegativeIntLiteral(const String& text) {
if (text.empty()) return -1;
SizeT digitCount = text.size();
if (text[digitCount - 1] == 'u' || text[digitCount - 1] == 'U') --digitCount;
if (digitCount == 0) return -1;
Int value = 0;
for (SizeT i = 0; i < digitCount; ++i) {
const char c = text[i];
if (c < '0' || c > '9') return -1;
value = value * 10 + (c - '0');
if (value > 4096) return -1; // no image array is anywhere near this
}
return value;
}
} // namespace
String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
Vector<String>* outDeclined) {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (outDeclined != nullptr) outDeclined->clear();
if (plans.empty() || glslCode.find("image") == String::npos) return glslCode;
// Same declaration shape as the split pass reads, with the array extent captured.
static const std::regex imageDeclRegex(
R"(layout\s*\(([^)]*)\)\s*uniform\s+)"
R"(((?:(?:readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*))"
R"(([iu]?image[A-Za-z0-9_]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:\[\s*([0-9]*)\s*\])?\s*;)");
static const std::regex bindingValueRegex(R"(binding\s*=\s*\d+)");
struct StageImageDecl {
String name;
String layout;
String qualifiers;
String type;
Int elementCount = 1;
SizeT declStart = 0;
SizeT declLength = 0;
};
// Every image declaration in the stage; the plans are program-wide and name arrays
// this stage may not declare at all.
Vector<StageImageDecl> decls;
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), imageDeclRegex), last; it != last; ++it) {
const std::smatch& match = *it;
StageImageDecl decl;
decl.layout = match[1].str();
decl.qualifiers = NormalizeDeclarationSpacing(match[2].str());
decl.type = match[3].str();
decl.name = match[4].str();
decl.elementCount = match[5].matched ? ParseNonNegativeIntLiteral(match[5].str()) : 1;
decl.declStart = static_cast<SizeT>(match.position(0));
decl.declLength = match[0].str().size();
decls.push_back(Move(decl));
}
Vector<ImageSourceEdit> edits;
Vector<String> takenNames;
for (const ImageArrayUnitPlan& plan : plans) {
const auto decline = [&](const char* why) {
if (outDeclined != nullptr) outDeclined->push_back(plan.name + ": " + why);
};
if (plan.units.size() < 2) continue;
const StageImageDecl* decl = nullptr;
for (const auto& candidate : decls) {
if (candidate.name == plan.name) {
decl = &candidate;
break;
}
}
if (decl == nullptr) {
// Absent from this stage entirely is the normal outcome - the reflection is
// program-wide and this pass runs per stage. Named but not RECOGNIZED is not:
// it means the declaration is spelled in some shape the regex above does not
// read, and staying quiet about that is how the wrong units got shipped.
if (ContainsIdentifier(glslCode, plan.name)) {
decline("the stage names it but declares it in a shape this pass cannot read");
}
continue;
}
if (decl->elementCount < 0 || static_cast<SizeT>(decl->elementCount) != plan.units.size()) {
decline("the emitted array extent disagrees with the reflected element count");
continue;
}
Bool consecutive = true;
Bool everyElementHasAUnit = true;
for (SizeT element = 0; element < plan.units.size(); ++element) {
const Int unit = plan.units[element];
if (unit < 0) {
everyElementHasAUnit = false;
break;
}
if (unit != plan.units[0] + static_cast<Int>(element)) consecutive = false;
}
if (!everyElementHasAUnit) {
decline("an element has no image unit");
continue;
}
// Already exactly what ESSL would do on its own. The caller filters these out;
// repeating the test here keeps the pass correct on its own terms.
if (consecutive) continue;
// Every use has to be `name[<literal>]`. The literal is what the split turns
// into a name, and by the time this runs there is always one:
// LegalizeResourceArrayIndexingForEssl has already folded or lowered every
// dynamic image-array subscript in the module, because ESSL forbids one
// outright ("image arrays indexed with non-constant expressions are forbidden
// in GLSL ES"). A subscript that is still an expression here is therefore a
// stage that was never going to compile, and guessing which element it meant
// would only change which unit it addressed wrongly.
struct ElementUse {
SizeT start; // the first character of the name
SizeT length; // through the closing ']'
SizeT element;
};
Vector<ElementUse> uses;
const char* refusal = nullptr;
for (SizeT pos = glslCode.find(plan.name); pos != String::npos;
pos = glslCode.find(plan.name, pos + 1)) {
if (pos > 0 && IsImagePassIdentifierChar(glslCode[pos - 1])) continue;
const SizeT after = pos + plan.name.size();
if (after < glslCode.size() && IsImagePassIdentifierChar(glslCode[after])) continue;
if (pos >= decl->declStart && pos < decl->declStart + decl->declLength) {
continue; // the declaration's own name
}
const SizeT open = glslCode.find_first_not_of(" \t\r\n", after);
if (open == String::npos || glslCode[open] != '[') {
refusal = "it is reached by something other than a subscript, so there is no "
"element index to rewrite";
break;
}
Int depth = 0;
SizeT scan = open;
for (; scan < glslCode.size(); ++scan) {
if (glslCode[scan] == '[') {
++depth;
} else if (glslCode[scan] == ']' && --depth == 0) {
break;
}
}
if (scan >= glslCode.size() || open + 1 >= scan) {
refusal = "it is reached by something other than a subscript, so there is no "
"element index to rewrite";
break;
}
const Int element = ParseNonNegativeIntLiteral(
NormalizeDeclarationSpacing(glslCode.substr(open + 1, scan - open - 1)));
if (element < 0 || element >= decl->elementCount) {
refusal = "its subscript is not a literal element index, so which unit the "
"access reaches cannot be decided here";
break;
}
uses.push_back({pos, scan + 1 - pos, static_cast<SizeT>(element)});
}
if (refusal != nullptr) {
decline(refusal);
continue;
}
// One SCALAR declaration per element, each carrying its own binding. ESSL nails
// an ARRAY's elements to consecutive units and offers no way to move them, so
// the only spelling that reaches an arbitrary set of units is one declaration
// per unit - and with every subscript a literal, every use has exactly one of
// them to be rewritten to.
//
// It costs precisely the image uniforms the application declared, which is why
// there is no budget test here: an array of four elements becomes four scalars
// however far apart their units are.
const SizeT elementCount = plan.units.size();
Vector<String> elementNames;
String replacement;
for (SizeT element = 0; element < elementCount; ++element) {
const String elementName =
MakeImageAliasName(IMAGE_ARRAY_ELEMENT_PREFIX,
plan.name + "_" + std::to_string(element), glslCode, takenNames);
takenNames.push_back(elementName);
elementNames.push_back(elementName);
String layout = decl->layout;
const String bindingText = "binding = " + std::to_string(plan.units[element]);
if (std::regex_search(layout, bindingValueRegex)) {
layout = std::regex_replace(layout, bindingValueRegex, bindingText);
} else {
layout = bindingText + (layout.empty() ? String() : ", " + layout);
}
if (element != 0) replacement += '\n';
replacement += "layout(" + layout + ") uniform ";
if (!decl->qualifiers.empty()) {
replacement += decl->qualifiers;
replacement += ' ';
}
replacement += decl->type + " " + elementName + ";";
}
edits.push_back({decl->declStart, decl->declLength, Move(replacement)});
// `name[k]` -> the scalar declared for element k, subscript and all.
for (const ElementUse& use : uses) {
edits.push_back({use.start, use.length, elementNames[use.element]});
}
}
if (edits.empty()) return glslCode;
// Back to front, so an earlier edit's offsets stay valid. No two edits overlap: each
// one covers either a whole declaration or a whole `name[k]`, the declaration's own
// name is skipped when the uses are collected, and one occurrence of a name yields at
// most one edit.
std::sort(edits.begin(), edits.end(),
[](const ImageSourceEdit& a, const ImageSourceEdit& b) { return a.start > b.start; });
String result = glslCode;
for (const ImageSourceEdit& edit : edits) {
result.replace(edit.start, edit.length, edit.text);
}
return result;
}
String SplitReadWriteImageUniforms(const String& glslCode, Uint* outSplitCount) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#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;
} }
@@ -853,10 +1343,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), imageDeclRegex), last; it != last; ++it) { for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), imageDeclRegex), last; it != last; ++it) {
const std::smatch& match = *it; const std::smatch& match = *it;
const String qualifiers = match[2].str(); const String qualifiers = match[2].str();
// Already legal: SPIRV-Cross decided one way, leave it alone. const Bool hasReadonly = ContainsIdentifier(qualifiers, "readonly");
if (ContainsIdentifier(qualifiers, "readonly") || ContainsIdentifier(qualifiers, "writeonly")) { const Bool hasWriteonly = ContainsIdentifier(qualifiers, "writeonly");
continue; // Carrying BOTH is a spelling no per-stage access analysis produces (SPIRV-Cross
} // clears one decoration or the other as soon as it sees a load or a store), so it
// came from the application and is identical in every stage. Nothing to do.
if (hasReadonly && hasWriteonly) continue;
Bool hasFormat = false; Bool hasFormat = false;
Bool exemptFormat = false; Bool exemptFormat = false;
@@ -865,10 +1357,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
hasFormat = true; hasFormat = true;
exemptFormat = IsMemoryQualifierExemptImageFormat(token); exemptFormat = IsMemoryQualifierExemptImageFormat(token);
} }
// No format qualifier at all is a different (and, in ES, unconditionally // A declaration carrying neither qualifier is illegal ES unless its format is
// illegal) shape that GL_EXT_shader_image_load_formatted would be needed for; // r32f/r32i/r32ui, and no format qualifier at all is a shape SPIRV-Cross refuses
// SPIRV-Cross refuses to emit it for an ES target, so nothing to do here. // to emit for an ES target. Either way there is no repair to make - and no rename
if (!hasFormat || exemptFormat) continue; // to make either, because a declaration with no access qualifier is spelled the
// same in every stage.
if (!hasReadonly && !hasWriteonly && (!hasFormat || exemptFormat)) continue;
ImageUniformDecl decl; ImageUniformDecl decl;
decl.layout = match[1].str(); decl.layout = match[1].str();
@@ -878,6 +1372,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
decl.arraySuffix = NormalizeDeclarationSpacing(match[5].str()); decl.arraySuffix = NormalizeDeclarationSpacing(match[5].str());
decl.declStart = static_cast<SizeT>(match.position(0)); decl.declStart = static_cast<SizeT>(match.position(0));
decl.declLength = match[0].str().size(); decl.declLength = match[0].str().size();
decl.nameStart = static_cast<SizeT>(match.position(4));
decl.nameLength = match[4].str().size();
decl.preTaggedReadonly = hasReadonly;
decl.preTaggedWriteonly = hasWriteonly;
decls.push_back(Move(decl)); decls.push_back(Move(decl));
} }
if (decls.empty()) { if (decls.empty()) {
@@ -892,12 +1390,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
}; };
// Walk every `image*(` call and attribute its first argument to a declaration. // Walk every `image*(` call and attribute its first argument to a declaration.
struct StoreSite { // EVERY recognized use is recorded, not only the stores: a declaration this pass
// renames has to take all of its uses with it, and the "every occurrence was one I
// saw" check below is what makes the recorded set provably the complete set.
struct ImageUseSite {
SizeT declIndex; SizeT declIndex;
SizeT start; SizeT start;
SizeT length; SizeT length;
SizeT callOpen; // the '(' of the call this argument belongs to
Bool stores; // an imageStore, i.e. the use a split redirects to the write half
}; };
Vector<StoreSite> storeSites; Vector<ImageUseSite> useSites;
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)) {
if (pos > 0 && IsImagePassIdentifierChar(glslCode[pos - 1])) continue; // uimage2D, myimageFoo if (pos > 0 && IsImagePassIdentifierChar(glslCode[pos - 1])) continue; // uimage2D, myimageFoo
SizeT tokenEnd = pos; SizeT tokenEnd = pos;
@@ -942,12 +1445,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
switch (ClassifyImageBuiltin(builtin)) { switch (ClassifyImageBuiltin(builtin)) {
case ImageBuiltinAccess::Load: case ImageBuiltinAccess::Load:
decl.loaded = true; decl.loaded = true;
useSites.push_back({declIndex, argStart, argEnd - argStart, openParen, false});
break; break;
case ImageBuiltinAccess::Store: case ImageBuiltinAccess::Store:
decl.stored = true; decl.stored = true;
storeSites.push_back({declIndex, argStart, argEnd - argStart}); useSites.push_back({declIndex, argStart, argEnd - argStart, openParen, true});
break; break;
case ImageBuiltinAccess::None: case ImageBuiltinAccess::None:
// imageSize/imageSamples touch nothing, but they still NAME the variable, so
// a rename has to reach them.
useSites.push_back({declIndex, argStart, argEnd - argStart, openParen, false});
break; break;
default: default:
decl.unknownUse = true; decl.unknownUse = true;
@@ -964,30 +1471,122 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
Vector<ImageSourceEdit> edits; Vector<ImageSourceEdit> edits;
Vector<String> takenAliases; Vector<String> takenNames;
for (auto& decl : decls) { for (auto& decl : decls) {
if (decl.unknownUse) continue; // leave it exactly as it was; no guessing if (decl.unknownUse) continue; // leave it exactly as it was; no guessing
// EVERY declaration this pass rewrites is also RENAMED, under the prefix of the
// repair it is about to receive - the qualifier below is a decision about ONE
// STAGE's accesses, and GLSL requires a uniform declared in two stages to be
// declared IDENTICALLY (GLSL 4.3 4.3.9 / GLSL ES 3.20 4.3.9). A shader that
// stores to an image in the vertex stage and loads it in the fragment stage gets
// `writeonly` on one and `readonly` on the other, and on Adreno the linker merges
// the two same-named declarations and SILENTLY DISCARDS the vertex-stage stores:
// no GL error, no link log, LINK_STATUS = 1, and the image still holding its
// initial contents afterwards
// (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation, and any
// shader pack that writes an image in one stage to read it in another).
//
// Keyed on the REPAIR and not on the stage, which is what makes the rename
// exactly as wide as the problem. Two stages that use the image the same way
// reach the same prefix and emit byte-identical declarations, so they keep ONE
// shared uniform and there is nothing mismatched to merge; two that use it
// differently reach different prefixes and cannot be merged at all. Tagging by
// stage instead also broke the merge - but it broke it for the agreeing stages
// too, turning one image uniform into one PER STAGE that names it, and Adreno
// allocates image locations per distinct uniform: the five stages of
// KHR-GL43.shading_language_420pack.binding_images_texture_type_* went from 6
// image uniforms to 30 and the link failed outright with "Error: Image Image
// location or component exceeds max allowed." on an Adreno 830, where Mali and
// Mesa both accept the same text.
//
// Nothing downstream reads these names: the two passes that key on the GL uniform
// name (RebindImageUniformsToFrontendUnits, BakeImageFormatQualifiers) both run
// BEFORE this one, RemoveLayoutBinding recognises an image declaration by its TYPE
// token, and CacheResourceLocations skips image uniforms outright because ES image
// units come only from layout(binding=N). The declarations this pass LEAVES ALONE -
// already readonly/writeonly in the source, or r32f/r32i/r32ui, which need no
// qualifier - keep their names, and they are exactly the ones that already match
// across stages.
if (decl.preTaggedReadonly || decl.preTaggedWriteonly) {
// No repair: SPIRV-Cross already emitted a legal qualifier. But it derived
// that qualifier from THIS STAGE's accesses, so a uniform stored in one stage
// and loaded in another arrives here `writeonly` in one and `readonly` in the
// other under ONE name - precisely the same-name/mismatched-qualifier pair
// Adreno merges while silently discarding the writing stage's stores
// (advanced-memory-dependentInvocation; a raw-ES probe reproduces it with no
// MobileGL in the process, and renaming either half fixes it). Keyed on the
// qualifier for the same reason the repair below is: two stages that agree
// spell the same alias and stay merged, so no shader gains an image uniform.
const char* preTagPrefix =
decl.preTaggedReadonly ? IMAGE_READONLY_ALIAS_PREFIX : IMAGE_WRITEONLY_ALIAS_PREFIX;
decl.aliasName = MakeImageAliasName(preTagPrefix, decl.name, glslCode, takenNames);
takenNames.push_back(decl.aliasName);
// The name token alone: the qualifiers are already right, and re-emitting the
// whole declaration would only risk changing them.
edits.push_back({decl.nameStart, decl.nameLength, decl.aliasName});
continue;
}
const char* aliasPrefix = decl.loaded && decl.stored ? IMAGE_SPLIT_READ_ALIAS_PREFIX
: decl.stored ? IMAGE_WRITEONLY_ALIAS_PREFIX
: IMAGE_READONLY_ALIAS_PREFIX;
decl.aliasName = MakeImageAliasName(aliasPrefix, decl.name, glslCode, takenNames);
takenNames.push_back(decl.aliasName);
if (decl.loaded && decl.stored) { if (decl.loaded && decl.stored) {
decl.writeName = MakeImageWriteAliasName(decl.name, glslCode, takenAliases); // Minted from the ALREADY access-tagged name, so the write half of a split
takenAliases.push_back(decl.writeName); // can never collide with the single declaration another stage's repair mints
// for the same image.
decl.writeName =
MakeImageAliasName(IMAGE_WRITE_ALIAS_PREFIX, decl.aliasName, glslCode, takenNames);
takenNames.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.aliasName,
BuildImageDeclaration(decl, "writeonly", decl.writeName)}); /*forceCoherent=*/true) +
"\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.aliasName)});
} else { } else {
// Loaded only, or only ever handed to imageSize (or unused): readonly is // Loaded only, or only ever handed to imageSize (or unused): readonly is
// the qualifier that keeps every one of those legal. // the qualifier that keeps every one of those legal.
edits.push_back({decl.declStart, decl.declLength, edits.push_back({decl.declStart, decl.declLength,
BuildImageDeclaration(decl, "readonly", decl.name)}); BuildImageDeclaration(decl, "readonly", decl.aliasName)});
} }
} }
for (const StoreSite& site : storeSites) { for (const ImageUseSite& site : useSites) {
const ImageUniformDecl& decl = decls[site.declIndex]; const ImageUniformDecl& decl = decls[site.declIndex];
if (!decl.split) continue; // Empty exactly when the declaration was poisoned above and left untouched; its
edits.push_back({site.start, site.length, decl.writeName}); // uses must keep naming the variable that is still called that.
if (decl.aliasName.empty()) continue;
edits.push_back(
{site.start, site.length, decl.split && site.stores ? decl.writeName : decl.aliasName});
if (!decl.split || !site.stores) continue;
// ...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;
+299 -11
View File
@@ -60,6 +60,115 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target); Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat); Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat); Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
// The CHANNEL WIDENING an image-bindable texture's ES storage takes, so that a format
// GLSL ES cannot spell as an image is carried by one it can.
//
// GL has forty image formats, GLSL ES core has thirteen, and no test device advertises
// GL_NV_image_formats - so a shader declaring one of the other twenty-six has no legal
// ESSL at all and glBindImageTexture rejects the narrow format outright for most of them
// (GL_INVALID_VALUE for nineteen of twenty-six on Adreno, twenty-five on both Malis).
// Seventeen have a core format of the SAME per-channel width and component type,
// differing only in channel count, and in one of those the emulation is EXACT: GL already
// defines an imageLoad from a narrower format as (r, 0, 0, 1) and an imageStore as
// dropping the components the format does not have, so the carrier's surplus channels
// hold values GL has already named. WidenImageFormatsPass pins them in the shader; this
// is the storage half, and DirectGLES::TextureImpl::SyncImageTextureBinding the bind
// half. All three ask WidenedCoreEsslImageFormat, so they cannot pick different carriers.
//
// Reports nothing (InternalFormat == GL_UNKNOWN_MGL) for a format that is core already,
// for the nine with no exact carrier (r11f_g11f_b10f, rgb10_a2, rgb10_a2ui, rgba16, rg16,
// r16, rgba16_snorm, rg16_snorm, r16_snorm - those keep the honest "no GLSL ES spelling"
// diagnostic rather than a silent approximation), and on a driver that HAS
// GL_NV_image_formats, where the shader keeps the declared format and no widening may
// happen behind it.
//
// The widened triple REPLACES what GenerateTextureFormatInfo chose, including any
// renderability substitution: an image that cannot be image-bound is useless whatever its
// attachment behaviour, so the image constraint wins. In practice that only bites
// RG8_SNORM/R8_SNORM on a driver without EXT_render_snorm, where the storage stays
// signed-normalized instead of becoming the half float that fallback would have picked -
// so an image-bound texture in one of those two formats is no longer attachable, and
// glGetTexImage on it falls through to the CPU shadow, which a shader-side imageStore
// does not update. Accepted deliberately: before the widening, an image binding in either
// format was refused outright by every driver tested and the stage that declared it never
// compiled at all, so nothing that works today is being given up.
//
// KNOWN GAP, for the same "all three layers move together" reason: a widened texture that
// is ALSO an FBO colour attachment gains one to three writable channels, and a draw into
// it can leave values in channels GL says are 0 and 1. Sampling and imageLoad are covered
// (the swizzle composition in SyncTextureParamsToBackend and the shader-side mask), but a
// glReadPixels/glGetTexImage that asks for more channels than the frontend format has
// would see them. Closing it needs the per-draw-buffer colour mask the three-channel
// widening already carries (FramebufferImpl::g_alphaWidenedDrawBufferMask) generalized
// from "alpha" to a channel count, which is its own change.
// How the FRONTEND's CPU shadow for a widened format is laid out relative to the carrier's
// transfer, i.e. what the upload has to do to it. Almost every entry is `Components`: the
// shadow already holds SourceChannels components of exactly the carrier's own type, so
// padding it out to four is the whole conversion. The packed entries do not - their shadow
// is ONE 32-bit word per texel - and reading such a word as components of the carrier's
// type takes twelve or sixteen bytes out of four and shears the level.
enum class ImageWidenSourceEncoding : Uint8 {
Components = 0,
// r11f_g11f_b10f: GL_UNSIGNED_INT_10F_11F_11F_REV -> four GL_FLOATs of an rgba16f.
PackedFloat11f11f10f,
// rgb10_a2 and rgb10_a2ui: GL_UNSIGNED_INT_2_10_10_10_REV -> four GL_UNSIGNED_SHORT
// channel CODES of an rgba16ui. The same split serves both: the two formats differ
// only in what the codes MEAN, which is the shader's business and not the transfer's.
PackedInt2101010Rev,
};
struct ImageBindableStorageWidening {
GLenum InternalFormat = GL_UNKNOWN_MGL;
GLenum Format = GL_UNKNOWN_MGL;
GLenum Type = GL_UNKNOWN_MGL;
// Channels the FRONTEND format has, i.e. how many of the carrier's four the client
// data fills. The rest are uploaded as 0, and the fourth as the format's implied 1.
Uint SourceChannels = 0;
// Whether that implied 1 is the integer one or a saturated normalized field - the
// transfer type cannot tell the two apart (GL_UNSIGNED_BYTE serves both RG8 and
// RG8UI), so the carrier decides.
Bool IntegerData = false;
// What the upload has to do to the frontend shadow before it describes the level to
// the driver (PrepareImageWidenedUpload).
ImageWidenSourceEncoding SourceEncoding = ImageWidenSourceEncoding::Components;
// Non-zero when the carrier holds this format's channels as the INTEGER CODES of a
// NORMALIZED value - the seven 16-bit and 10-bit normalized formats, which core ESSL
// has no image format of any width for and which a float carrier would requantise.
// Each entry is the largest code that channel can hold, i.e. the denominator of GL 4.6
// 2.3.5; SignedNormalized picks which of the two conversions it is the denominator of.
//
// Two things depend on it, both because the ES storage no longer shares the frontend
// format's component class: the upload pads a missing alpha with ChannelMax[3] instead
// of the transfer type's own "one" (through a uint carrier the saturated field IS the
// one), and glGetTexImage divides the codes back out into the floats the application
// is still owed.
Uint ChannelMax[4] = {0u, 0u, 0u, 0u};
Bool SignedNormalized = false;
Bool CarriesNormalizedCodes() const { return ChannelMax[0] != 0u; }
explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
};
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat);
// The single-channel core format an image-bindable BUFFER texture's view is SPLIT into, or
// GL_UNKNOWN_MGL for a format that needs no split (or has no core base).
//
// A buffer texture cannot be widened: its texels are the application's buffer object, at
// the size and layout the application gave it, and it is usually also a vertex, index or
// storage buffer whose bytes are not ours to restride. But an rg32f view of N texels and
// an r32f view of 2N texels describe exactly the SAME bytes, so the split changes only
// how the shader subscripts them - component j of texel i is texel 2i + j of the base
// view - which WidenImageFormatsPass rewrites every access to do. The same rule as the
// widening decides WHETHER: a driver that can spell rg32f for an imageBuffer needs
// nothing.
//
// KNOWN GAP, and the reason this is not applied to a texture that is merely sampled: a
// buffer texture that is BOTH image-bound and read through a samplerBuffer would have its
// sampled view split too, and the sampler side is not rewritten. Accepted for the same
// reason the storage widening's gaps are - on a driver where the split applies at all
// there is no legal ESSL for the image declaration, so such a program did not compile.
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat);
} // namespace TextureImpl } // namespace TextureImpl
namespace FramebufferImpl {} // namespace FramebufferImpl namespace FramebufferImpl {} // namespace FramebufferImpl
@@ -154,6 +263,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
@@ -168,9 +287,113 @@ namespace MobileGL::MG_Backend::DirectGLES {
// stops being safe to edit by hand. // stops being safe to edit by hand.
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName); String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
String RemoveLayoutBinding(const String& glslCode); String RemoveLayoutBinding(const String& glslCode);
// Prefix of the per-element scalar declarations RemapImageArrayElementUnits splits an
// image array into; the suffix is the array's own name and the element's index.
constexpr const char* IMAGE_ARRAY_ELEMENT_PREFIX = "mg_imageElem_";
// One image ARRAY whose elements the application pointed at units that are not
// consecutive-from-element-zero.
struct ImageArrayUnitPlan {
String name; // the array's name, exactly as the emitted ESSL declares it
Vector<Int> units; // the frontend image unit element k has to reach
};
// Desktop GL lets an application give each element of an image array an ARBITRARY unit
// (glUniform1i per element). ES has no such call at all - "ES image units come
// exclusively from the layout(binding=N) qualifier" - and one declaration carries one
// binding, so ESSL nails an array's elements to the CONSECUTIVE units N, N+1, N+2, ...
// MobileGL used to stamp element [0]'s unit as the binding and let the rest fall where
// they fell: KHR-GL4x.shader_image_load_store.advanced-sso-simple assigns 0,2,4,6 and
// 1,3,5,7, so its two programs actually addressed 0,1,2,3 and 1,2,3,4 - one layer got the
// wrong value and three were never written, with no GL error and no link log. The same
// defect for SAMPLER arrays was fixed API-side (SubscriptUniformNameForElement); an image
// array has no API side to fix, because ES makes glUniform1i on an image uniform an
// INVALID_OPERATION.
//
// Repaired by SPLITTING the array into one SCALAR image uniform per element, each with
// its own layout(binding = N), and rewriting `name[k]` to the scalar declared for
// element k. One declaration carries one binding, so one declaration per unit is the
// only spelling that reaches an arbitrary set of them.
//
// That rewrite needs every k in the emitted text to be a LITERAL, and it is:
// LegalizeResourceArrayIndexingForEssl has already folded or lowered every dynamic
// image-array subscript in the module, because ESSL forbids one outright ("image arrays
// indexed with non-constant expressions are forbidden in GLSL ES", Mesa 26.1.4 at
// ES 3.2, on a raw GLES probe with no MobileGL in the loop). The earlier shape here -
// widening the array to cover the whole span of units and routing each subscript through
// a `const highp int` offset table - was written before that pass covered images, and
// the table lookup was itself one of the non-constant expressions the same probe refuses.
// The split also costs exactly the image uniforms the application declared, where the
// widening cost the whole SPAN (seven for the four elements of
// KHR-GL42.shader_image_load_store.advanced-sso-simple), so there is no budget for it to
// fail to fit in.
//
// Declines - leaving the array exactly as it was, and naming it in `outDeclined` for the
// caller to report - when the emitted extent disagrees with the reflection, when the
// array is reached by anything other than a subscript, or when a subscript is not a
// literal element index. Silence was the whole defect here, so a decline must be audible.
//
// Must run AFTER RebindImageUniformsToFrontendUnits and BakeImageFormatQualifiers (both
// key on the GL uniform name and on a binding already being stamped) and BEFORE
// SplitReadWriteImageUniforms (so each element that is both read and written is split
// with its own binding already on it) and RemoveLayoutBinding (which is what preserves
// image bindings). Like them, it is downstream of the L2 shader-translation memo, so the
// per-program units it reads need no entry in BuildEsslTranslationKey.
String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
Vector<String>* outDeclined = nullptr);
// The member list of a `gl_PerVertex { ... }` redeclaration in already-emitted ESSL -
// the text between the braces, verbatim - or nullopt when the shader does not redeclare
// the block in that direction. `input` selects the `in gl_PerVertex` form over the
// `out` one.
//
// Exists so BuildPassthroughTessControlEssl can MIRROR the stages it has to sit between
// rather than guess at them. Whether SPIRV-Cross redeclares the built-in block, and with
// which members, depends on what the application's shader touched; a synthesized stage
// that redeclares a different shape than its neighbours is an ES link error against a
// program that has no other problem.
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, Bool input);
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes: "the input
// patch is passed through unmodified", the output patch has PATCH_VERTICES vertices, and
// the levels come from the PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
//
// Desktop GL makes the control stage OPTIONAL. OpenGL ES 3.2 does not: it has no
// PATCH_DEFAULT_*_LEVEL state at all (only glPatchParameteri, for PATCH_VERTICES) and
// rejects a program that has an evaluation stage without a control stage - with an EMPTY
// info log, verified on an Adreno 830 with no MobileGL in the process. MobileGL's own
// frontend link succeeds, so the program reports GL_LINK_STATUS = TRUE, program 0 is
// bound in its place, and every draw silently renders nothing.
//
// `inPerVertexMembers` / `outPerVertexMembers` are the member lists to redeclare gl_in
// and gl_out with - normally taken from the neighbouring stages' own emitted ESSL via
// ExtractPerVertexBlockMembers, and empty to leave the driver's built-in declaration
// alone, which is what matching a neighbour that did not redeclare requires.
//
// All four outer levels and both inner levels are written unconditionally: writing a
// level the evaluation stage's domain does not use is legal and ignored, and it saves
// this from having to know the domain. They are literal 1.0 because that is the GL
// default and glPatchParameterfv - their only setter - is a stub in this frontend
// (MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means making
// the levels a parameter here AND part of what makes a built program stale, exactly as
// PATCH_VERTICES already is; the two must move together, so they are named together.
//
// The same stage, for the same reason, that DirectVulkan synthesizes in
// ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both
// tessellation stages. Kept as two generators rather than one because the two targets
// disagree on everything but the algorithm: desktop GLSL 450 against ESSL, a fixed
// gl_PerVertex shape that Vulkan matches structurally against a mirrored one, and a
// VkShaderModule against a driver shader object.
String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices,
const String& inPerVertexMembers,
const String& outPerVertexMembers);
// Prefix of the writeonly half a read+write image uniform is split into (see // Prefix of the writeonly half a read+write image uniform is split into (see
// SplitReadWriteImageUniforms); the suffix is the image's own name. // SplitReadWriteImageUniforms); the suffix is the image's own (already access-tagged) name.
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_"; constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
// The three names SplitReadWriteImageUniforms renames a rewritten image declaration
// under, one per REPAIR it can apply. Which one a stage picks is decided by that stage's
// own accesses, so two stages that use an image the same way arrive at the SAME name and
// two that use it differently arrive at different ones - which is exactly the property
// the rename exists for, at no cost to the stages that agree. Exposed for the tests.
constexpr const char* IMAGE_READONLY_ALIAS_PREFIX = "mg_imageRo_";
constexpr const char* IMAGE_WRITEONLY_ALIAS_PREFIX = "mg_imageWo_";
constexpr const char* IMAGE_SPLIT_READ_ALIAS_PREFIX = "mg_imageRw_";
// ESSL refuses an image variable that carries a format qualifier other than r32f / // ESSL refuses an image variable that carries a format qualifier other than r32f /
// r32i / r32ui unless it also carries `readonly` or `writeonly` (GLSL ES 3.10 4.9 / // r32i / r32ui unless it also carries `readonly` or `writeonly` (GLSL ES 3.10 4.9 /
// 3.20 4.10; glslang enforces it verbatim in ParseHelper.cpp's layoutObjectCheck). // 3.20 4.10; glslang enforces it verbatim in ParseHelper.cpp's layoutObjectCheck).
@@ -182,15 +405,74 @@ namespace MobileGL::MG_Backend::DirectGLES {
// bare declaration, so the frontend raises no error and the illegal ESSL only shows // bare declaration, so the frontend raises no error and the illegal ESSL only shows
// up as a device compile failure - and then as a silently no-op draw. // up as a device compile failure - and then as a silently no-op draw.
// //
// Restores a legal declaration: // Restores a legal declaration, and RENAMES it after the repair it applied while doing so:
// * loaded only -> add `readonly` // * loaded only -> add `readonly`, rename under IMAGE_READONLY_ALIAS_PREFIX
// * stored only -> add `writeonly` // * stored only -> add `writeonly`, rename under IMAGE_WRITEONLY_ALIAS_PREFIX
// * 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
// <IMAGE_WRITE_ALIAS_PREFIX><name>`, and point every // <IMAGE_SPLIT_READ_ALIAS_PREFIX><name>` and `coherent
// imageStore at the second one. Several image variables // writeonly <IMAGE_WRITE_ALIAS_PREFIX><that name>`, point
// may share an image unit as long as they have the same // every imageStore at the second one, and follow each of
// type and format, which is exactly what the pair is. // those stores with `memoryBarrierImage();`. Several image
// variables may share an image unit as long as they have
// the same type and format, which is exactly what the pair
// is.
//
// The rename is the other half of the repair and applies to all three cases. The qualifier
// chosen above is a decision about ONE STAGE's accesses, and GLSL requires a uniform
// declared in two stages to be declared identically - so a shader that stores an image from
// the vertex stage and loads it from the fragment stage came out of here `writeonly` in one
// and `readonly` in the other. Adreno merges the two same-named declarations and silently
// drops the vertex-stage STORES: no GL error, no link log, LINK_STATUS = 1, and the image
// still reads back its initial contents
// (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation; a raw-ES probe
// isolated the trigger to the same-name/mismatched-qualifier pair, and only when both
// carry `coherent`). Renaming leaves no cross-stage variable to merge.
//
// The name is keyed on the REPAIR, not on the stage, and that distinction is the whole
// point: two stages that use an image the same way emit byte-identical declarations, so
// letting them keep one shared name costs nothing and merging them is correct, while two
// stages that use it differently land on different prefixes and cannot be merged at all.
// A per-STAGE tag also satisfied the first requirement but violated the second: it made
// the SAME image a distinct uniform in every stage that named it, and Adreno allocates
// image LOCATIONS per distinct uniform. KHR-GL43.shading_language_420pack.
// binding_images_texture_type_* declares three read+write images in each of its five
// stages; merged that is 6 image uniforms, per-stage-tagged it is 30, and the Adreno 830
// linker answered "Error: Image Image location or component exceeds max allowed. Error:
// Linking failed." - which, the frontend having already published LINK_STATUS = TRUE from
// glslang's link, surfaced only as every draw silently doing nothing and the images
// reading back zero. Mali and Mesa link the same text, so nothing but a device gate
// catches this.
//
// A declaration SPIRV-Cross already tagged `readonly` or `writeonly` needs no qualifier
// repair, but it is NOT stage-independent: that tag is derived from the accesses of the
// stage being emitted, so an image stored in the vertex stage and loaded in the fragment
// stage arrives here as `coherent writeonly g_image` and `coherent readonly g_image` -
// one name, two spellings, which is exactly the pair Adreno merges. Those declarations
// are therefore renamed too, keyed on the qualifier they already carry (readonly ->
// IMAGE_READONLY_ALIAS_PREFIX, writeonly -> IMAGE_WRITEONLY_ALIAS_PREFIX) and with
// nothing but the identifier changed. Stages that agree still reach the same alias and
// stay merged, so this costs no shader an extra image uniform.
//
// The declarations this pass still leaves untouched keep their names: one carrying BOTH
// readonly and writeonly (a spelling no access analysis produces, so it came from the
// application and is identical everywhere), and one carrying NEITHER, which is legal only
// for the r32f/r32i/r32ui formats and is likewise spelled the same in every stage.
//
// 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
@@ -200,8 +482,14 @@ 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. Its output is a function of the emitted text alone - it needs no
String SplitReadWriteImageUniforms(const String& glslCode); // stage and no per-program state - so it adds nothing to BuildEsslTranslationKey either.
//
// `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_";
@@ -562,12 +562,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (MG_Util::Async::AsyncShaderCompileEnabled()) { if (MG_Util::Async::AsyncShaderCompileEnabled()) {
extensions.push_back(E_GL_KHR_parallel_shader_compile); extensions.push_back(E_GL_KHR_parallel_shader_compile);
} }
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). Every `double` in a // GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64), and stays opt-in even on a
// shader compiles and runs already - it is narrowed to 32 bits before the module // device that HAS shaderFloat64. Every `double` in a shader compiles and runs either way
// reaches this backend - so an application that simply uses doubles needs nothing // - narrowed to 32 bits where the device has no 64-bit floats, kept whole where it does -
// advertised. What the extension additionally promises is 64-bit PRECISION, which no // so an application that simply uses doubles needs nothing advertised. What the extension
// mobile GPU has and the narrowing cannot fake, so advertising it by default would // additionally promises is the whole GL_ARB_gpu_shader_fp64 SURFACE (glUniform*d
// make an application that checks the string take a path MobileGL cannot honour. // conformance, the fp64 built-ins, the state queries), and turning the string on is a
// decision about all of it rather than about the shader path alone.
if (MG_Config::Features.AdvertiseFp64) { if (MG_Config::Features.AdvertiseFp64) {
extensions.push_back(E_GL_ARB_gpu_shader_fp64); extensions.push_back(E_GL_ARB_gpu_shader_fp64);
} }
@@ -847,6 +848,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 +906,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;
@@ -919,26 +966,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray); DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
} }
} }
// Never, on any device, and no longer for the reason it used to be. It used to track // The device feature the whole fp64 story hangs off. With it, a module keeps its
// shaderFloat64 because a `dvec3` input needed the Float64 capability to exist in the // OpCapability Float64 and real doubles reach the driver; without it the transpile
// module at all; a 64-bit vertex FETCH was already impossible (VK_FORMAT_R64*_SFLOAT is // narrows every 64-bit float to 32 (ShaderTranspiler::DemoteFloat64Pass), because
// optional and lavapipe reports zero bufferFeatures for all four), so the attribute // VUID-VkShaderModuleCreateInfo-pCode-08740 forbids the capability outright and no
// arrived as its 32-bit word pair and PackDoubleVertexInputsPass bitcast it back. // pipeline could be built from such a module. lavapipe reports it; Adreno and Mali both
// report VK_FALSE, so on every real mobile device this is false and the demotion runs
// exactly as it always has.
m_dynamicParameters.SupportsShaderFloat64 = m_vulkanCaps.SupportsShaderFloat64;
// Never, on any device, and DELIBERATELY NOT COUPLED to the line above even though it
// once tracked the same feature. It used to, because a `dvec` input needed Float64 to
// exist in the module at all; a 64-bit vertex FETCH was already impossible
// (VK_FORMAT_R64*_SFLOAT is optional and lavapipe reports zero bufferFeatures for all
// four), so the attribute arrived as its 32-bit word pair and PackDoubleVertexInputsPass
// bitcast it back.
// //
// The shader half of that is gone: every 64-bit float is narrowed before any module // Re-coupling it does not work, and the reason is worth recording because it is not
// reaches a backend (ShaderTranspiler::DemoteFloat64Pass), so there is no `double` input // obvious: this flag decides the VkFormat from the VAO ATTRIBUTE alone, and the attribute
// left to bitcast INTO, and feeding a UINT-formatted attribute to what is now a `float` // does not know what the shader declared. glVertexAttribFormat(GL_DOUBLE) against a plain
// input would be silent garbage. Reconstructing the value would mean decoding the // `in vec4` is not only legal but the common case
// IEEE-754 double bit pattern in the shader - software fp64, which is precisely what the // (KHR-GL43.vertex_attrib_binding.basic-input-case4 does exactly that, and case5 adds
// demotion exists to avoid - and on Espryt it would additionally need the ES driver to // normalized=GL_TRUE), and advanced-bindingUpdate feeds a dvec3 the same way - GL defines
// fetch 2N uint components where the application declared N doubles, which a dvec3 or // all of them as "doubles in memory, converted to float". Turning the flag on turns the
// dvec4 cannot even express within one attribute location. // narrowing OFF for every one of them and the attributes come back unfetched.
// //
// So glVertexAttribLFormat / glVertexAttribLPointer are declined here exactly as they // What keeps the two halves honest instead is a per-MODULE decision: a vertex module that
// already were on Espryt and on every real mobile device (Adreno and Mali both report // declares a 64-bit float INPUT is demoted whole, even where the backend has native fp64,
// shaderFloat64 == VK_FALSE), and for the same visible reason. A `dvec3` INPUT still // so `dvec` inputs are `vec` inputs on this backend exactly as they always were. See
// compiles and draws - it is a `vec3` after demotion - as long as the application feeds // ShaderCompiler::SanitizeAndOptimizeBinary.
// it with glVertexAttribPointer(GL_FLOAT) rather than 64-bit data.
m_dynamicParameters.SupportsFloat64VertexAttributes = false; m_dynamicParameters.SupportsFloat64VertexAttributes = false;
m_dynamicParameters.MaxShaderStorageBlockSize = m_dynamicParameters.MaxShaderStorageBlockSize =
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize); std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
@@ -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);
@@ -83,7 +83,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool isMember = false; Bool isMember = false;
}; };
ShaderStage PickClipFixupStage(const Vector<SharedPtr<ShaderObject>>& shaders); ShaderStage PickClipFixupStage(const Vector<ShaderStage>& stages);
Bool IsVec4Float32(spvtools::opt::IRContext* context, Uint32 typeId, Uint32* outFloatTypeId) { Bool IsVec4Float32(spvtools::opt::IRContext* context, Uint32 typeId, Uint32* outFloatTypeId) {
auto* vecInst = context->get_def_use_mgr()->GetDef(typeId); auto* vecInst = context->get_def_use_mgr()->GetDef(typeId);
@@ -614,15 +614,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void ReflectStageInterface(ShaderStage targetStage, void ReflectStageInterface(ShaderStage targetStage,
Bool reflectInputs, Bool reflectInputs,
const Vector<SharedPtr<ShaderObject>>& shaders, const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv, const Vector<Vector<Uint>>& spirv,
StageInterfaceSummary& outSummary, StageInterfaceSummary& outSummary,
Uint programExternalIndex, Uint programExternalIndex,
const char* stageLabel) { const char* stageLabel) {
outSummary.slotSignatures.fill(0); outSummary.slotSignatures.fill(0);
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) { for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != targetStage) { if (stages[moduleIndex] != targetStage) {
continue; continue;
} }
@@ -690,11 +690,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
void ValidateRasterizationStageInterface(const Vector<SharedPtr<ShaderObject>>& shaders, void ValidateRasterizationStageInterface(const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv, const Vector<Vector<Uint>>& spirv,
ProgramFactory::VkProgramObject& entry, ProgramFactory::VkProgramObject& entry,
Uint programExternalIndex) { Uint programExternalIndex) {
const ShaderStage producerStage = PickClipFixupStage(shaders); const ShaderStage producerStage = PickClipFixupStage(stages);
entry.rasterizationProducerStage = producerStage; entry.rasterizationProducerStage = producerStage;
entry.producerOutputComponentCount = 0; entry.producerOutputComponentCount = 0;
entry.fragmentInputComponentCount = 0; entry.fragmentInputComponentCount = 0;
@@ -703,8 +703,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
Bool hasFragmentStage = false; Bool hasFragmentStage = false;
for (const auto& shader : shaders) { for (const ShaderStage stage : stages) {
if (shader && shader->GetShaderStage() == ShaderStage::Fragment) { if (stage == ShaderStage::Fragment) {
hasFragmentStage = true; hasFragmentStage = true;
break; break;
} }
@@ -715,9 +715,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
StageInterfaceSummary producerOutputs{}; StageInterfaceSummary producerOutputs{};
StageInterfaceSummary fragmentInputs{}; StageInterfaceSummary fragmentInputs{};
ReflectStageInterface(producerStage, false, shaders, spirv, producerOutputs, programExternalIndex, ReflectStageInterface(producerStage, false, stages, spirv, producerOutputs, programExternalIndex,
"producer"); "producer");
ReflectStageInterface(ShaderStage::Fragment, true, shaders, spirv, fragmentInputs, programExternalIndex, ReflectStageInterface(ShaderStage::Fragment, true, stages, spirv, fragmentInputs, programExternalIndex,
"fragment"); "fragment");
entry.producerOutputComponentCount = CountOccupiedStageInterfaceSlots(producerOutputs); entry.producerOutputComponentCount = CountOccupiedStageInterfaceSlots(producerOutputs);
entry.fragmentInputComponentCount = CountOccupiedStageInterfaceSlots(fragmentInputs); entry.fragmentInputComponentCount = CountOccupiedStageInterfaceSlots(fragmentInputs);
@@ -1719,14 +1719,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return success; return success;
} }
ShaderStage PickClipFixupStage(const Vector<SharedPtr<ShaderObject>>& shaders) { ShaderStage PickClipFixupStage(const Vector<ShaderStage>& stages) {
Bool hasGeometry = false; Bool hasGeometry = false;
Bool hasTessEval = false; Bool hasTessEval = false;
Bool hasVertex = false; Bool hasVertex = false;
for (const auto& shader : shaders) { for (const ShaderStage stage : stages) {
if (!shader) continue;
const auto stage = shader->GetShaderStage();
hasGeometry |= (stage == ShaderStage::Geometry); hasGeometry |= (stage == ShaderStage::Geometry);
hasTessEval |= (stage == ShaderStage::TessEval); hasTessEval |= (stage == ShaderStage::TessEval);
hasVertex |= (stage == ShaderStage::Vertex); hasVertex |= (stage == ShaderStage::Vertex);
@@ -2094,7 +2092,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case SpvImageFormatR11fG11fB10f: return VK_FORMAT_B10G11R11_UFLOAT_PACK32; case SpvImageFormatR11fG11fB10f: return VK_FORMAT_B10G11R11_UFLOAT_PACK32;
case SpvImageFormatR16f: return VK_FORMAT_R16_SFLOAT; case SpvImageFormatR16f: return VK_FORMAT_R16_SFLOAT;
case SpvImageFormatRgba16: return VK_FORMAT_R16G16B16A16_UNORM; case SpvImageFormatRgba16: return VK_FORMAT_R16G16B16A16_UNORM;
case SpvImageFormatRgb10A2: return VK_FORMAT_A2R10G10B10_UNORM_PACK32; // A2**B**10G10R10, matching MGToVk::ConvertTextureInternalFormatToVkFormat's RGB10A2.
// This value becomes the storage image VIEW's format while the image itself was created
// from the texture's internal format, so the two must name the same bit layout or the
// shader reads the texel through a different component order than the host wrote it.
// GL_RGB10_A2 with GL_UNSIGNED_INT_2_10_10_10_REV puts R in bits 0-9, G in 10-19, B in
// 20-29 and A in 30-31, which is Vulkan's A2B10G10R10; A2R10G10B10 transposes R and B.
// KHR-GL43.shader_image_load_store.basic-allFormats-store read back [2,1,0,3] for an
// rgb10_a2ui image stored as [0,1,2,3] while these two converters disagreed.
case SpvImageFormatRgb10A2: return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
case SpvImageFormatRg16: return VK_FORMAT_R16G16_UNORM; case SpvImageFormatRg16: return VK_FORMAT_R16G16_UNORM;
case SpvImageFormatRg8: return VK_FORMAT_R8G8_UNORM; case SpvImageFormatRg8: return VK_FORMAT_R8G8_UNORM;
case SpvImageFormatR16: return VK_FORMAT_R16_UNORM; case SpvImageFormatR16: return VK_FORMAT_R16_UNORM;
@@ -2117,7 +2123,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case SpvImageFormatRgba16ui: return VK_FORMAT_R16G16B16A16_UINT; case SpvImageFormatRgba16ui: return VK_FORMAT_R16G16B16A16_UINT;
case SpvImageFormatRgba8ui: return VK_FORMAT_R8G8B8A8_UINT; case SpvImageFormatRgba8ui: return VK_FORMAT_R8G8B8A8_UINT;
case SpvImageFormatR32ui: return VK_FORMAT_R32_UINT; case SpvImageFormatR32ui: return VK_FORMAT_R32_UINT;
case SpvImageFormatRgb10a2ui: return VK_FORMAT_A2R10G10B10_UINT_PACK32; case SpvImageFormatRgb10a2ui: return VK_FORMAT_A2B10G10R10_UINT_PACK32; // see Rgb10A2 above
case SpvImageFormatRg32ui: return VK_FORMAT_R32G32_UINT; case SpvImageFormatRg32ui: return VK_FORMAT_R32G32_UINT;
case SpvImageFormatRg16ui: return VK_FORMAT_R16G16_UINT; case SpvImageFormatRg16ui: return VK_FORMAT_R16G16_UINT;
case SpvImageFormatRg8ui: return VK_FORMAT_R8G8_UINT; case SpvImageFormatRg8ui: return VK_FORMAT_R8G8_UINT;
@@ -2308,15 +2314,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
void ProgramFactory::ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders, void ProgramFactory::ReflectVertexInputs(const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv, const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const { VkProgramObject& entry) const {
entry.activeVertexInputLocationMask = 0; entry.activeVertexInputLocationMask = 0;
entry.vertexInputTypes.fill(0); entry.vertexInputTypes.fill(0);
entry.readsBaseVertexBuiltin = false; entry.readsBaseVertexBuiltin = false;
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) { for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Vertex) { if (stages[moduleIndex] != ShaderStage::Vertex) {
continue; continue;
} }
@@ -2393,14 +2399,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// evaluation stages. Rather than guess which one is last, every non-fragment, non-compute // evaluation stages. Rather than guess which one is last, every non-fragment, non-compute
// module is asked - one writer anywhere means this program's draws need a multi-viewport // module is asked - one writer anywhere means this program's draws need a multi-viewport
// pipeline, and a false positive costs only a wider viewportCount. // pipeline, and a false positive costs only a wider viewportCount.
void ProgramFactory::ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders, void ProgramFactory::ReflectViewportIndexUsage(const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv, const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const { VkProgramObject& entry) const {
entry.writesViewportIndexBuiltin = false; entry.writesViewportIndexBuiltin = false;
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) { for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
if (!shaders[moduleIndex]) continue; const ShaderStage stage = stages[moduleIndex];
const ShaderStage stage = shaders[moduleIndex]->GetShaderStage();
if (stage == ShaderStage::Fragment || stage == ShaderStage::Compute) continue; if (stage == ShaderStage::Fragment || stage == ShaderStage::Compute) continue;
const auto& module = spirv[moduleIndex]; const auto& module = spirv[moduleIndex];
@@ -2428,15 +2433,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
void ProgramFactory::ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders, void ProgramFactory::ReflectFragmentOutputs(const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv, const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const { VkProgramObject& entry) const {
entry.activeFragmentOutputLocationMask = 0; entry.activeFragmentOutputLocationMask = 0;
entry.fragmentOutputTypes.fill(0); entry.fragmentOutputTypes.fill(0);
entry.fragmentReplacesDepth = false; entry.fragmentReplacesDepth = false;
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) { for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Fragment) { if (stages[moduleIndex] != ShaderStage::Fragment) {
continue; continue;
} }
@@ -3142,7 +3147,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& entry = m_cache[hash]; auto& entry = m_cache[hash];
entry.hash = hash; entry.hash = hash;
entry.lastUsedFrame = m_frameCounter; entry.lastUsedFrame = m_frameCounter;
auto& shaders = program.GetAttachedShaders(); // The EXECUTABLE's stage list, not GetAttachedShaders(): `spirv` is a link artifact with
// one module per linked stage, while the attach list is live and grows on
// glAttachShader, which GL 4.6 core 7.3 says does not reach the executable until the
// next link. Sizing this loop by the attach list therefore ran it past the end of both
// `spirv` and `moduleSpirvs` for any program attached to after it linked.
const Vector<ShaderStage> stages = program.GetLinkedShaderStages();
auto& spirv = program.GetGeneratedSpirv(); auto& spirv = program.GetGeneratedSpirv();
Vector<Vector<Uint>> moduleSpirvs(spirv.size()); Vector<Vector<Uint>> moduleSpirvs(spirv.size());
const Bool enableSpirvValidation = program.GetSpirvValidationEnabled(); const Bool enableSpirvValidation = program.GetSpirvValidationEnabled();
@@ -3150,14 +3160,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation(); MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
} }
const ShaderStage fixupStage = PickClipFixupStage(shaders); const ShaderStage fixupStage = PickClipFixupStage(stages);
for (SizeT i = 0; i < shaders.size(); ++i) { // Both lists come from the same Link(), so they agree by construction; the min() is what
// makes that an assumption this loop does not have to bet the process on.
const SizeT moduleCount = std::min(stages.size(), spirv.size());
for (SizeT i = 0; i < moduleCount; ++i) {
auto& spv = spirv[i]; auto& spv = spirv[i];
if (spv.empty()) continue; if (spv.empty()) continue;
// Apply position fixup if needed // Apply position fixup if needed
if (fixupStage != ShaderStage::Unknown && shaders[i] && shaders[i]->GetShaderStage() == fixupStage) { if (fixupStage != ShaderStage::Unknown && stages[i] == fixupStage) {
const Vector<Uint>* fixupInput = &spv; const Vector<Uint>* fixupInput = &spv;
Vector<Uint> xfbSpirv; Vector<Uint> xfbSpirv;
if ((flags & ProgramFactory::CompileOptionBit::XfbCapture) && if ((flags & ProgramFactory::CompileOptionBit::XfbCapture) &&
@@ -3173,16 +3186,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
moduleSpirvs[i] = spv; moduleSpirvs[i] = spv;
} }
if ((flags & ProgramFactory::CompileOptionBit::ExplicitLod0Sampling) && shaders[i] && if ((flags & ProgramFactory::CompileOptionBit::ExplicitLod0Sampling) && stages[i] == ShaderStage::Fragment) {
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
Vector<Uint> explicitLodSpirv; Vector<Uint> explicitLodSpirv;
if (TransformSpirvForExplicitLod0Sampling(moduleSpirvs[i], explicitLodSpirv)) { if (TransformSpirvForExplicitLod0Sampling(moduleSpirvs[i], explicitLodSpirv)) {
moduleSpirvs[i] = Move(explicitLodSpirv); moduleSpirvs[i] = Move(explicitLodSpirv);
} }
} }
if ((flags & ProgramFactory::CompileOptionBit::FragCoordYFlip) && shaders[i] && if ((flags & ProgramFactory::CompileOptionBit::FragCoordYFlip) && stages[i] == ShaderStage::Fragment) {
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
Vector<Uint> fragCoordSpirv; Vector<Uint> fragCoordSpirv;
if (TransformSpirvForFragCoordYFlip(moduleSpirvs[i], fragCoordSpirv, m_defaultFramebufferHeight)) { if (TransformSpirvForFragCoordYFlip(moduleSpirvs[i], fragCoordSpirv, m_defaultFramebufferHeight)) {
moduleSpirvs[i] = Move(fragCoordSpirv); moduleSpirvs[i] = Move(fragCoordSpirv);
@@ -3193,7 +3204,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// operations execute natively; module repairs keep the GL contract intact // operations execute natively; module repairs keep the GL contract intact
// around them. The opt-in emulation path replaces them only on devices with no // around them. The opt-in emulation path replaces them only on devices with no
// subgroup support at all (MOBILEGL_MAGMA_EMULATE_SUBGROUP). // subgroup support at all (MOBILEGL_MAGMA_EMULATE_SUBGROUP).
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Compute) { if (stages[i] == ShaderStage::Compute) {
// Program 203 broadcasts the first reduction through // Program 203 broadcasts the first reduction through
// prefixSumCache[0], then lets the second reduction overwrite that // prefixSumCache[0], then lets the second reduction overwrite that
// scratch without first rendezvousing all readers. Patch that exact // scratch without first rendezvousing all readers. Patch that exact
@@ -3296,8 +3307,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// The unsupported-device counterpart of this rebase (warning when a shader reads // The unsupported-device counterpart of this rebase (warning when a shader reads
// the builtin but shaderDrawParameters is missing) rides along with // the builtin but shaderDrawParameters is missing) rides along with
// ReflectVertexInputs, which already reflects this stage. // ReflectVertexInputs, which already reflects this stage.
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex && if (stages[i] == ShaderStage::Vertex && m_shaderDrawParametersEnabled) {
m_shaderDrawParametersEnabled) {
Vector<Uint> rebasedSpirv; Vector<Uint> rebasedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i], if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
rebasedSpirv, enableSpirvValidation)) { rebasedSpirv, enableSpirvValidation)) {
@@ -3314,8 +3324,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// through CompileOptionBit::ZeroBaseVertex, so the indexed variant of the same // through CompileOptionBit::ZeroBaseVertex, so the indexed variant of the same
// program keeps the native builtin and stays correct for glDrawElementsBaseVertex // program keeps the native builtin and stays correct for glDrawElementsBaseVertex
// and for the baseVertex word of an indexed indirect command. // and for the baseVertex word of an indexed indirect command.
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex && if (stages[i] == ShaderStage::Vertex && (flags & CompileOptionBit::ZeroBaseVertex)) {
(flags & CompileOptionBit::ZeroBaseVertex)) {
Vector<Uint> zeroedSpirv; Vector<Uint> zeroedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroBaseVertexForVulkan(moduleSpirvs[i], if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroBaseVertexForVulkan(moduleSpirvs[i],
zeroedSpirv, enableSpirvValidation)) { zeroedSpirv, enableSpirvValidation)) {
@@ -3338,7 +3347,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// committed to R32G32{,B32A32}_UINT for the attribute, so a module still declaring // committed to R32G32{,B32A32}_UINT for the attribute, so a module still declaring
// `in double` would reconcile to Unknown and build a pipeline with a UINT format under a // `in double` would reconcile to Unknown and build a pipeline with a UINT format under a
// double input - garbage with no diagnostic anywhere. // double input - garbage with no diagnostic anywhere.
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) { if (stages[i] == ShaderStage::Vertex) {
Vector<Uint> packedSpirv; Vector<Uint> packedSpirv;
const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan( const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan(
moduleSpirvs[i], packedSpirv, enableSpirvValidation); moduleSpirvs[i], packedSpirv, enableSpirvValidation);
@@ -3377,17 +3386,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs); const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs);
MOBILEGL_ASSERT(remapOk, "ProgramFactory::GetOrCreateProgram: descriptor binding remap failed"); MOBILEGL_ASSERT(remapOk, "ProgramFactory::GetOrCreateProgram: descriptor binding remap failed");
for (SizeT i = 0; i < shaders.size(); ++i) { for (SizeT i = 0; i < moduleCount; ++i) {
auto& moduleSpv = moduleSpirvs[i]; auto& moduleSpv = moduleSpirvs[i];
if (moduleSpv.empty()) continue; if (moduleSpv.empty()) continue;
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex()); ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex());
#else #else
// Final module the driver receives; also checked in the INFO-level CI/test // Final module the driver receives; also checked in the INFO-level CI/test
// lanes, where the DEBUG gate above is compiled out. // lanes, where the DEBUG gate above is compiled out.
if (enableSpirvValidation) { if (enableSpirvValidation) {
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex()); ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex());
} }
#endif #endif
@@ -3399,7 +3408,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_VERIFY(vkCreateShaderModule(m_device, &smci, nullptr, &module), "vkCreateShaderModule"); VK_VERIFY(vkCreateShaderModule(m_device, &smci, nullptr, &module), "vkCreateShaderModule");
VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
ShaderStage shaderStage = shaders[i]->GetShaderStage(); ShaderStage shaderStage = stages[i];
stage.stage = ToVkStage(shaderStage); stage.stage = ToVkStage(shaderStage);
stage.module = module; stage.module = module;
stage.pName = "main"; stage.pName = "main";
@@ -3434,12 +3443,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Reflect and create layout as part of the program object // Reflect and create layout as part of the program object
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
ValidateRasterizationStageInterface(shaders, moduleSpirvs, entry, program.GetExternalIndex()); ValidateRasterizationStageInterface(stages, moduleSpirvs, entry, program.GetExternalIndex());
#endif #endif
ReflectVertexInputs(shaders, moduleSpirvs, entry); ReflectVertexInputs(stages, moduleSpirvs, entry);
ReflectViewportIndexUsage(shaders, moduleSpirvs, entry); ReflectViewportIndexUsage(stages, moduleSpirvs, entry);
ReflectFragmentOutputs(shaders, moduleSpirvs, entry); ReflectFragmentOutputs(stages, moduleSpirvs, entry);
ReflectPassthroughTessControlNeed(shaders, moduleSpirvs, entry); ReflectPassthroughTessControlNeed(stages, moduleSpirvs, entry);
ReflectLayout(program, moduleSpirvs, entry); ReflectLayout(program, moduleSpirvs, entry);
// A failed remap means the modules kept glslang's per-stage auto-mapped binding numbers - // A failed remap means the modules kept glslang's per-stage auto-mapped binding numbers -
// no cross-stage unification, no set->0 normalisation - so the bindings this layout // no cross-stage unification, no set->0 normalisation - so the bindings this layout
@@ -3651,7 +3660,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
void ProgramFactory::ReflectPassthroughTessControlNeed( void ProgramFactory::ReflectPassthroughTessControlNeed(
const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders, const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv, const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const { VkProgramObject& entry) const {
entry.needsPassthroughTessControl = false; entry.needsPassthroughTessControl = false;
@@ -3660,9 +3669,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool hasTessEval = false; Bool hasTessEval = false;
Bool hasTessControl = false; Bool hasTessControl = false;
SizeT tessEvalModuleIndex = 0; SizeT tessEvalModuleIndex = 0;
for (SizeT i = 0; i < shaders.size(); ++i) { for (SizeT i = 0; i < stages.size(); ++i) {
if (!shaders[i]) continue; const ShaderStage stage = stages[i];
const auto stage = shaders[i]->GetShaderStage();
if (stage == ShaderStage::TessControl) hasTessControl = true; if (stage == ShaderStage::TessControl) hasTessControl = true;
if (stage == ShaderStage::TessEval) { if (stage == ShaderStage::TessEval) {
hasTessEval = true; hasTessEval = true;
@@ -499,13 +499,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}; };
static TextureTarget UniformTypeToTextureTarget(GLenum glType); static TextureTarget UniformTypeToTextureTarget(GLenum glType);
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders, // `stages` is ALWAYS ProgramObject::GetLinkedShaderStages() - one entry per module of
// `spirv`, at the same index. Taking the stages rather than the shader objects is what
// keeps the program's live attach list, which is a longer and differently-indexed list
// the moment a glAttachShader lands after the link, from being passed here by mistake.
void ReflectVertexInputs(const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv, const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const; VkProgramObject& entry) const;
void ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders, void ReflectViewportIndexUsage(const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv, const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const; VkProgramObject& entry) const;
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders, void ReflectFragmentOutputs(const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv, const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const; VkProgramObject& entry) const;
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv, void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
@@ -513,7 +517,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked // Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked
// modules. Const and reflection-only: it decides nothing about the pipeline, it only // modules. Const and reflection-only: it decides nothing about the pipeline, it only
// records what the evaluation stage's input interface is made of. // records what the evaluation stage's input interface is made of.
void ReflectPassthroughTessControlNeed(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders, void ReflectPassthroughTessControlNeed(const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv, const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const; VkProgramObject& entry) const;
@@ -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,34 @@ 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 the
// shader's `dvec` input is a `vec` by the time the pipeline is built, and what
// guarantees that is the flag being clear. It is clear on every backend today, and a
// program with a 64-bit float vertex input is demoted WHOLE for the same reason even
// where the device has native fp64 (ProgramSpirvTask::GenerateSpirv). With the flag
// set, a dvec3/dvec4 would be 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 +145,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 +214,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 +357,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,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
// The fetch half of the 64-bit vertex narrowing, whose shader half is guaranteed by
// SupportsFloat64VertexAttributes staying false on this backend: any program with a Float64
// vertex INPUT is demoted whole, native fp64 or not, so the input is always a 32-bit one. 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 +3627,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;
} }
@@ -4727,10 +4767,11 @@ void main() {
// link-time properties, so this is safe to fold into a pipeline keyed on the program hash. // link-time properties, so this is safe to fold into a pipeline keyed on the program hash.
static Bool ProgramCapturesXfbFromGeometryStage(const MG_State::GLState::ProgramObject& program) { static Bool ProgramCapturesXfbFromGeometryStage(const MG_State::GLState::ProgramObject& program) {
if (program.GetTransformFeedbackVaryingCount() == 0) return false; if (program.GetTransformFeedbackVaryingCount() == 0) return false;
for (const auto& shader : program.GetAttachedShaders()) { // Both halves are link-time properties, so both are asked of the LAST LINK. Reading the
if (shader && shader->GetShaderStage() == ShaderStage::Geometry) return true; // live attach list would let a glAttachShader that has not been linked in yet - which GL
} // 4.6 core 7.3 says changes nothing about what the program runs - flip a property this
return false; // pipeline is cached under, for an executable with no geometry stage in it.
return program.HasLinkedShaderStage(ShaderStage::Geometry);
} }
VkPipeline VulkanRenderer::GetOrCreatePipeline( VkPipeline VulkanRenderer::GetOrCreatePipeline(
@@ -8869,7 +8910,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 +8924,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 +8960,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 +8975,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 +9020,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 +9035,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 +9086,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 +9131,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 +9145,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 +9202,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 +9222,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 +9240,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 +9262,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 +9271,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 +9289,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 +9858,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 +9937,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 +9949,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 +9959,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 +10196,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 +10224,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 +10289,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,
+2
View File
@@ -44,3 +44,5 @@ 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
+148 -14
View File
@@ -45,7 +45,11 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch(); const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch();
if (!ValidateProgramForExecution(currentProgram, functionName)) return false; if (!ValidateProgramForExecution(currentProgram, functionName)) return false;
if (currentProgram->GetShaderIndexByStage(ShaderStage::Compute) < 0) { // Of the EXECUTABLE, not the live attach list: attaching a compute shader to an
// already-linked graphics program does not give that program a compute stage to
// dispatch (GL 4.6 core 7.3), and letting the dispatch through on the strength of the
// attach hands the backend a program whose SPIR-V has no compute module in it.
if (!currentProgram->HasLinkedShaderStage(ShaderStage::Compute)) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
@@ -108,6 +112,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->HasLinkedShaderStage(ShaderStage::Geometry)) {
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 +137,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 +166,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,13 +203,58 @@ 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).
// Asked of the LAST LINK, not the live attach list (GL 4.6 core 7.3): attaching a
// tessellation evaluation shader to an already-linked program does not put it in the
// executable, so reading the live list here would reject every non-GL_PATCHES draw
// against a program that does not tessellate - and keep rejecting them, since a detach
// is likewise deferred to the next link.
const Bool tessellationActive = currentProgram && currentProgram->HasLinkedShaderStage(ShaderStage::TessEval);
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; // "Is there a geometry stage at all" has to be asked of the STAGE, never of the input
if (gsInput != GL_NONE && mode != GL_PATCHES) { // primitive: GL_NONE and GL_POINTS are both 0, so a `layout(points) in` geometry shader
// is indistinguishable from no geometry shader by its reflected input type alone. The
// sentinel test this replaces therefore skipped the whole rule for exactly the geometry
// shaders whose input is the most restrictive one - every mode but GL_POINTS was
// accepted (KHR-GL43.transform_feedback.api_errors_test draws a points-in geometry
// program with GL_LINES and requires INVALID_OPERATION).
//
// And it has to be asked of the LAST LINK: gsInputPrimitive is a link artifact, so
// pairing it with the live attach list would re-point the very same 0-aliasing rather
// than remove it. In the window after glAttachShader(GS) on a linked program the live
// list says "geometry present" while the artifact still reads GL_NONE == GL_POINTS, and
// the switch below would silently reject every mode but GL_POINTS.
const Bool geometryActive = currentProgram && currentProgram->HasLinkedShaderStage(ShaderStage::Geometry);
const GLenum gsInput = geometryActive ? currentProgram->GetGeometryInputType() : GL_NONE;
if (geometryActive && mode != GL_PATCHES) {
Bool compatible = false; Bool compatible = false;
switch (gsInput) { switch (gsInput) {
case GL_POINTS: case GL_POINTS:
@@ -216,13 +288,20 @@ 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();
// Both stage tests are asked of the last link, for the same reason as the two guards
// above: what relocates the constraint is a stage the program actually RUNS, and an
// attach that has not been linked in yet gives it none.
const Bool feedbackModeIsProgramDriven =
feedbackProgram && (feedbackProgram->HasLinkedShaderStage(ShaderStage::Geometry) ||
feedbackProgram->HasLinkedShaderStage(ShaderStage::TessEval));
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 +382,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 +406,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 +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.MultiDrawElements(mode, count, type, indices, drawcount); MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
} }
@@ -330,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.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount, MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
basevertex); basevertex);
} }
@@ -338,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.DrawArrays(mode, first, count); MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
} }
@@ -345,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.MultiDrawArrays(mode, first, count, drawcount); MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
} }
@@ -353,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.DrawElementsBaseVertex(mode, count, type, indices, basevertex); MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
} }
@@ -361,6 +459,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 +467,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 +476,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 +486,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 +496,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 +506,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 +516,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 +526,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 +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.DrawElementsInstancedBaseInstance(mode, count, type, indices, MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
instancecount, baseinstance); instancecount, baseinstance);
} }
@@ -439,6 +546,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 +554,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 +562,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 +571,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 +579,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 +608,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 +662,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 +712,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 +833,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 +848,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 +857,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 +865,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 +889,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 +911,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 +919,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 +927,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 +941,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 +949,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 +960,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
+274 -68
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:
@@ -662,7 +680,9 @@ namespace MobileGL::MG_Impl::GLImpl {
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_UNIFORM_BLOCKS: // GL >= 3.1 case GL_ACTIVE_UNIFORM_BLOCKS: // GL >= 3.1
*params = programObject->GetActiveUniformBlocksCount(); // Uniform blocks only. GetActiveUniformBlocksCount() is the internal block space,
// which also carries the storage blocks and the synthesized atomic counter blocks.
*params = programObject->GetGlUniformBlockCount();
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_UNIFORM_BLOCK_MAX_NAME_LENGTH: // ditto. case GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH: // ditto.
@@ -682,7 +702,11 @@ namespace MobileGL::MG_Impl::GLImpl {
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_COMPUTE_WORK_GROUP_SIZE: { // GL >= 4.3 case GL_COMPUTE_WORK_GROUP_SIZE: { // GL >= 4.3
if (!programObject->GetLinkStatus() || programObject->GetShaderIndexByStage(ShaderStage::Compute) < 0) { // "a linked program object with a compute shader" is one whose EXECUTABLE has the
// stage: the local size below is a link artifact, so an attached-but-not-yet-linked
// compute shader would answer this query with the previous link's (absent) value
// instead of the INVALID_OPERATION GL 4.6 core 7.13 asks for.
if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::Compute)) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
@@ -856,10 +880,14 @@ 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) { // Float matrices only, in both senses: a DOUBLE matrix never comes through here, whether its
if (ttype == nullptr || !ttype->isMatrix() || ttype->getBasicType() == glslang::EbtDouble) return false; // program was demoted (components are floats, the query is not) or kept its doubles (the
const Int columns = ttype->getMatrixCols(); // column stride is a dvec4's, and the caller's converting branch already walks it component
const Int rows = ttype->getMatrixRows(); // by component with the right one).
Bool TryGatherFloatMatrixColumns(const TypeFactsRef ttype, const char* pBase, void* params) {
if (!ttype.isMatrix || ttype.isDouble) return false;
const Int columns = ttype.matrixCols;
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));
@@ -868,11 +896,12 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
// Bytes a uniform actually occupies in the global UBO. It is the tight GL type size for // Bytes a uniform actually occupies in the global UBO. It is the tight GL type size for
// everything except a float matrix, whose padded columns make it wider. The rule itself // everything except a matrix, whose padded columns make it wider, and a `double` on a
// lives on ProgramObject, because the pipeline composite's uniform refresh needs the same // program whose modules were demoted, where it is half. The rule itself lives on
// one and two copies of a layout rule is one too many. // ProgramObject, because the pipeline composite's uniform refresh needs the same one and
SizeT UniformStorageSpanInBytes(const glslang::TType* ttype, SizeT tightSize) { // two copies of a layout rule is one too many.
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize); SizeT UniformStorageSpanInBytes(const TypeFactsRef ttype, SizeT tightSize, const Bool nativeFloat64) {
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize, nativeFloat64);
} }
void GetUniform_State(GLuint program, GLint location, void* params) { void GetUniform_State(GLuint program, GLint location, void* params) {
@@ -904,8 +933,9 @@ 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 Bool nativeFloat64 = programObject->UsesNativeFloat64();
const SizeT span = UniformStorageSpanInBytes(ttype, size, nativeFloat64);
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()) {
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__, MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
@@ -915,9 +945,9 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) { if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) {
// Never more than the uniform actually occupies. `size` is the GL type size, // Never more than the uniform actually occupies. `size` is the GL type size,
// which for a `double` uniform is twice its storage - every 64-bit float is // which on a DEMOTED program is twice a `double` uniform's storage - its 64-bit
// narrowed before the module reaches a backend, so the slot holds floats. The // floats were narrowed before the module reached a backend, so the slot holds
// typed entry points (glGetUniformdv and friends) go through // floats. The typed entry points (glGetUniformdv and friends) go through
// GetUniformScalar_State, which converts component by component; this raw // GetUniformScalar_State, which converts component by component; this raw
// copy has no type to convert with, so it is bounded rather than converted. // copy has no type to convert with, so it is bounded rather than converted.
Memcpy(params, pUBO + offset, std::min<SizeT>(size, span)); Memcpy(params, pUBO + offset, std::min<SizeT>(size, span));
@@ -958,8 +988,9 @@ 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 Bool nativeFloat64 = programObject->UsesNativeFloat64();
const SizeT span = UniformStorageSpanInBytes(ttype, size, nativeFloat64);
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()) {
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__, MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
@@ -971,28 +1002,38 @@ namespace MobileGL::MG_Impl::GLImpl {
if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return; if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return;
} }
// A double-precision uniform is the one case where the stored component type differs // A double-precision uniform is the one case where the stored component type can differ
// from the DECLARED one for a non-opaque uniform: the shader's 64-bit floats are // from the DECLARED one for a non-opaque uniform: on a DEMOTED program the shader's
// narrowed to 32 bits before the module reaches a backend // 64-bit floats were narrowed to 32 before the module reached the backend
// (ShaderTranspiler::DemoteFloat64Pass), so what is in the global UBO is a float per // (ShaderTranspiler::DemoteFloat64Pass), so what is in the global UBO is a float per
// component, laid out exactly like the float-typed twin of this uniform - std140 // component, laid out exactly like the float-typed twin of this uniform - std140
// 16-byte column stride for a matrix included. Reading it as a GLdouble would return // 16-byte column stride for a matrix included. Reading it as a GLdouble would return
// two components reinterpreted as one. Read component by component and let GL's // two components reinterpreted as one. A program that KEPT its doubles stores real ones
// at the dvec4 column stride instead, so the width and the stride both move; everything
// else about this walk is the same. Read component by component either way and let GL's
// 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 - where it lost any - at the
// stored it and not here. // glUniform*d that 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 // 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 =
const SizeT columnStride = 4 * sizeof(GLfloat); MG_State::GLState::ProgramObject::UniformMatrixColumnStride(ttype, nativeFloat64);
const SizeT componentSize = nativeFloat64 ? sizeof(GLdouble) : sizeof(GLfloat);
for (Int column = 0; column < columns; ++column) { for (Int column = 0; column < columns; ++column) {
for (Int row = 0; row < rows; ++row) { for (Int row = 0; row < rows; ++row) {
GLfloat component = 0.0f; GLdouble component = 0.0;
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLfloat), if (nativeFloat64) {
sizeof(component)); Memcpy(&component, pUBO + offset + column * columnStride + row * componentSize,
sizeof(GLdouble));
} else {
GLfloat narrow = 0.0f;
Memcpy(&narrow, pUBO + offset + column * columnStride + row * componentSize,
sizeof(narrow));
component = static_cast<GLdouble>(narrow);
}
if constexpr (std::is_integral_v<T>) { if constexpr (std::is_integral_v<T>) {
// Rounded to the nearest integer and clamped into the queried type's // Rounded to the nearest integer and clamped into the queried type's
// range, so a negative double read through glGetUniformuiv is 0 // range, so a negative double read through glGetUniformuiv is 0
@@ -1191,8 +1232,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,
@@ -1263,17 +1304,45 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} }
// glUniform*d / glUniformMatrix*dv. Neither needs a layout of its own any more: the // Whether the program a uniform write is about to land in stores 64-bit floats at their
// transpile chain narrows every 64-bit float in the shader to 32 bits // declared width. Answered off the PROGRAM, never off the live backend: it describes the
// modules that were actually built for it, and a backend with native fp64 still demotes a
// program whose vertex stage declares a Float64 input (see ProgramSpirvTask::GenerateSpirv).
// Nullptr - no current program, or a name that is not a program - answers false and lets the
// callee record the same error it always did.
Bool CurrentProgramUsesNativeFloat64() {
if (MG_State::pGLContext == nullptr) return false;
const auto& programObject = MG_State::pGLContext->GetProgramForUniform();
return programObject != nullptr && programObject->UsesNativeFloat64();
}
Bool NamedProgramUsesNativeFloat64(GLuint program) {
const auto& programObject = TryToGetProgramObject(program);
return programObject != nullptr && programObject->GetLinkStatus() && programObject->UsesNativeFloat64();
}
// glUniform*d / glUniformMatrix*dv. On a DEMOTED program neither needs a layout of its own:
// the transpile chain narrowed every 64-bit float in the shader to 32
// (ShaderTranspiler::DemoteFloat64Pass) and the global UBO is laid out by reflecting that // (ShaderTranspiler::DemoteFloat64Pass) and the global UBO is laid out by reflecting that
// demoted module, so a double uniform's storage IS a float uniform's - same offset, same // demoted module, so a double uniform's storage IS a float uniform's - same offset, same
// 4-byte components, same std140 column padding for matrices. Narrowing here, at the one // 4-byte components, same std140 column padding for matrices. Narrowing here, at the one
// place the 64-bit value enters, and then handing the bytes to the ordinary float upload // place the 64-bit value enters, and then handing the bytes to the ordinary float upload
// path is what keeps the two in step; a separate double-shaped layout here would write // path is what keeps the two in step; a separate double-shaped layout there would write
// 8-byte components into 4-byte slots and silently address the wrong ones. // 8-byte components into 4-byte slots and silently address the wrong ones.
// //
// The narrowing is the same static_cast the shader's own arithmetic now performs, so the // The narrowing is the same static_cast the demoted shader's own arithmetic performs, so the
// value the shader reads is the value glUniform*d was given, at float precision. // value the shader reads is the value glUniform*d was given, at float precision.
//
// On a program that KEPT its doubles the reverse is true and for the same reason: its global
// UBO really does hold 8-byte components, so narrowing would leave a float bit pattern in the
// low half of a double slot - which is not a precision loss but a garbage value. The 64-bit
// values go through unchanged then, and the upload path is width-agnostic (it is templated on
// the component type and bounded by the uniform's own slot span).
//
// Note TryToGetProgramObject / GetProgramForUniform run TWICE on this path, once for the
// width question and once inside the call below. That is a lookup and a join on an entry
// point no shader pack uses; the alternative is duplicating both functions' whole validation
// sequence here, which is the thing that must not drift.
template <GLsizei ItemCount> template <GLsizei ItemCount>
void UniformvNarrowed_State(GLint location, GLsizei count, const GLdouble* value) { void UniformvNarrowed_State(GLint location, GLsizei count, const GLdouble* value) {
if (value == nullptr || count <= 0) { if (value == nullptr || count <= 0) {
@@ -1282,6 +1351,10 @@ namespace MobileGL::MG_Impl::GLImpl {
Uniformv_State<ItemCount>(location, count, reinterpret_cast<const GLfloat*>(value)); Uniformv_State<ItemCount>(location, count, reinterpret_cast<const GLfloat*>(value));
return; return;
} }
if (location != -1 && CurrentProgramUsesNativeFloat64()) {
Uniformv_State<ItemCount>(location, count, value);
return;
}
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount); Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]); for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
Uniformv_State<ItemCount>(location, count, narrowed.data()); Uniformv_State<ItemCount>(location, count, narrowed.data());
@@ -1293,6 +1366,10 @@ namespace MobileGL::MG_Impl::GLImpl {
ProgramUniformv_State<ItemCount>(program, location, count, reinterpret_cast<const GLfloat*>(value)); ProgramUniformv_State<ItemCount>(program, location, count, reinterpret_cast<const GLfloat*>(value));
return; return;
} }
if (location != -1 && NamedProgramUsesNativeFloat64(program)) {
ProgramUniformv_State<ItemCount>(program, location, count, value);
return;
}
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount); Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]); for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
ProgramUniformv_State<ItemCount>(program, location, count, narrowed.data()); ProgramUniformv_State<ItemCount>(program, location, count, narrowed.data());
@@ -1344,15 +1421,63 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} }
// glUniformMatrix*dv / glProgramUniformMatrix*dv. Narrowed to the float form and handed // glUniformMatrix*dv / glProgramUniformMatrix*dv on a program that KEPT its doubles. Same
// straight to it: after DemoteFloat64Pass a `dmat4` uniform is a `mat4` in the shader and a // walk as UniformMatrixfv_Object down to the last branch, and deliberately a copy of it
// mat4-shaped slot in the global UBO, columns padded to a vec4 and all. Everything else // rather than a template over the component type: the two differ in exactly one number that
// about the call - transpose handling, the array-element walk, the opaque-uniform refusal - // is not derivable from the component type alone - std140 pads a double matrix's column out
// is then the one implementation both spellings share. // to a dvec4 (32 bytes) unless the column is a dvec2, which is already 16 - and folding that
// into the float version would put a per-call branch on the hot glUniformMatrix4fv path
// Minecraft calls thousands of times a frame for a case no shader pack ever takes.
template <typename Program>
void UniformMatrixdvNative_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value, Int columns, Int rows,
const String& ownerDescription) {
const SizeT columnStride = rows <= 2 ? 2 * sizeof(GLdouble) : 4 * sizeof(GLdouble);
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
GLdouble column[4] = {};
for (GLint matrix = 0; matrix < count; ++matrix) {
if (matrix > 0 && !programObject.UniformLocationsAliasSameUniform(location, location + matrix)) break;
if (!programObject.IsValidUniformLocation(location + matrix)) {
RecordInvalidUniformLocationError("glUniformMatrixdv", location + matrix, ownerDescription);
return;
}
if (programObject.IsUniformOpaqueAtLocation(location + matrix)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "glUniformMatrixdv",
"Opaque uniforms cannot be set with matrix Uniform calls."));
return;
}
const GLdouble* source = value + static_cast<SizeT>(matrix) * componentCount;
for (Int c = 0; c < columns; ++c) {
for (Int r = 0; r < rows; ++r) {
column[r] = transpose == GL_TRUE ? source[r * columns + c] : source[c * rows + r];
}
const SizeT byteOffset = static_cast<SizeT>(c) * columnStride;
switch (rows) {
case 2: Uniform_State<2>(programObject, location + matrix, column, byteOffset); break;
case 3: Uniform_State<3>(programObject, location + matrix, column, byteOffset); break;
default: Uniform_State<4>(programObject, location + matrix, column, byteOffset); break;
}
}
}
}
// glUniformMatrix*dv / glProgramUniformMatrix*dv. On a DEMOTED program this narrows to the
// float form and hands it straight over: after DemoteFloat64Pass a `dmat4` uniform is a
// `mat4` in the shader and a mat4-shaped slot in the global UBO, columns padded to a vec4
// and all. Everything else about the call - transpose handling, the array-element walk, the
// opaque-uniform refusal - is then the one implementation both spellings share. A program
// that kept its doubles gets the same walk at double width and the wider column stride.
template <typename Program> template <typename Program>
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose, void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value, Int columns, Int rows) { const GLdouble* value, Int columns, Int rows) {
if (value == nullptr || count <= 0) return; if (value == nullptr || count <= 0) return;
if (programObject.UsesNativeFloat64()) {
UniformMatrixdvNative_Object(programObject, location, count, transpose, value, columns, rows,
"the current program object");
return;
}
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows); const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * componentCount); Vector<GLfloat> narrowed(static_cast<SizeT>(count) * componentCount);
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]); for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
@@ -1694,7 +1819,10 @@ namespace MobileGL::MG_Impl::GLImpl {
return GL_INVALID_INDEX; return GL_INVALID_INDEX;
} }
const auto& index = programObject->GetUniformBlockIndex(uniformBlockName); // GetGlUniformBlockIndex, not GetUniformBlockIndex: the latter answers in the internal
// block space, which also resolves storage blocks and the synthesized atomic counter
// blocks. Neither is a uniform block (GL 4.6 core 7.6), so both are GL_INVALID_INDEX here.
const auto index = programObject->GetGlUniformBlockIndex(uniformBlockName);
MGLOG_D("GBI prog=%u name='%s' -> %d", program, uniformBlockName ? uniformBlockName : "(null)", (Int)index); MGLOG_D("GBI prog=%u name='%s' -> %d", program, uniformBlockName ? uniformBlockName : "(null)", (Int)index);
return index; return index;
} }
@@ -1708,7 +1836,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"Program object" + std::to_string(program) + " that has been linked.")); "Program object" + std::to_string(program) + " that has been linked."));
return; return;
} }
if (!programObject->IsActiveUniformBlock(uniformBlockIndex)) { if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
@@ -1719,8 +1847,11 @@ namespace MobileGL::MG_Impl::GLImpl {
std::to_string(program) + ".")); std::to_string(program) + "."));
return; return;
} }
// The GL_UNIFORM_BLOCK index space skips the storage and atomic counter blocks the
// block-keyed tables still carry; translate before touching them.
const Uint blockIndex = static_cast<Uint>(programObject->BlockIndexFromGlUniformBlock(uniformBlockIndex));
MGLOG_D("UBB prog=%u idx=%u binding=%u", program, uniformBlockIndex, uniformBlockBinding); MGLOG_D("UBB prog=%u idx=%u binding=%u", program, uniformBlockIndex, uniformBlockBinding);
programObject->SetUniformBlockBinding(uniformBlockIndex, uniformBlockBinding); programObject->SetUniformBlockBinding(blockIndex, uniformBlockBinding);
} }
void GetActiveUniformBlockiv_State(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint* params) { void GetActiveUniformBlockiv_State(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint* params) {
@@ -1732,7 +1863,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"Program object" + std::to_string(program) + " that has been linked.")); "Program object" + std::to_string(program) + " that has been linked."));
return; return;
} }
if (!programObject->IsActiveUniformBlock(uniformBlockIndex)) { if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
@@ -1743,61 +1874,68 @@ namespace MobileGL::MG_Impl::GLImpl {
std::to_string(program) + ".")); std::to_string(program) + "."));
return; return;
} }
// The GL_UNIFORM_BLOCK index space skips the storage and atomic counter blocks the
// block-keyed tables still carry; every accessor below is indexed by the block space.
const Uint blockIndex = static_cast<Uint>(programObject->BlockIndexFromGlUniformBlock(uniformBlockIndex));
switch (pname) { switch (pname) {
case GL_UNIFORM_BLOCK_DATA_SIZE: { case GL_UNIFORM_BLOCK_DATA_SIZE: {
*params = (GLint)programObject->GetUBOSizeAt(uniformBlockIndex); *params = (GLint)programObject->GetUBOSizeAt(blockIndex);
MGLOG_D("%s: GL_UNIFORM_BLOCK_DATA_SIZE = %d", __func__, *params); MGLOG_D("%s: GL_UNIFORM_BLOCK_DATA_SIZE = %d", __func__, *params);
break; break;
} }
case GL_UNIFORM_BLOCK_NAME_LENGTH: { case GL_UNIFORM_BLOCK_NAME_LENGTH: {
*params = (GLint)programObject->GetUniformBlockName(uniformBlockIndex).length() + 1; *params = (GLint)programObject->GetUniformBlockName(blockIndex).length() + 1;
MGLOG_D("%s: GL_UNIFORM_BLOCK_NAME_LENGTH = %d", __func__, *params); MGLOG_D("%s: GL_UNIFORM_BLOCK_NAME_LENGTH = %d", __func__, *params);
break; break;
} }
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS: { case GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS: {
*params = programObject->GetUniformBlockActiveUniformCount(uniformBlockIndex); *params = programObject->GetUniformBlockActiveUniformCount(blockIndex);
MGLOG_D("%s: GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS = %d", __func__, *params); MGLOG_D("%s: GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS = %d", __func__, *params);
break; break;
} }
case GL_UNIFORM_BLOCK_BINDING: { case GL_UNIFORM_BLOCK_BINDING: {
*params = static_cast<GLint>(programObject->GetUniformBlockBinding(uniformBlockIndex)); *params = static_cast<GLint>(programObject->GetUniformBlockBinding(blockIndex));
MGLOG_D("%s: GL_UNIFORM_BLOCK_BINDING = %d", __func__, *params); MGLOG_D("%s: GL_UNIFORM_BLOCK_BINDING = %d", __func__, *params);
break; break;
} }
case GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER: case GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER:
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangVertex)); *params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangVertex));
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER = %d", __func__, *params); MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER = %d", __func__, *params);
break; break;
case GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER: case GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER:
*params = *params =
BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangTessControl)); BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangTessControl));
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER = %d", __func__, *params); MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER = %d", __func__, *params);
break; break;
case GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER: case GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER:
*params = *params =
BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangTessEvaluation)); BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangTessEvaluation));
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER = %d", __func__, *params); MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER = %d", __func__, *params);
break; break;
case GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER: case GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER:
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangGeometry)); *params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangGeometry));
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER = %d", __func__, *params); MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER = %d", __func__, *params);
break; break;
case GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER: case GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER:
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangFragment)); *params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangFragment));
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER = %d", __func__, *params); MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER = %d", __func__, *params);
break; break;
case GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER: case GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER:
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangCompute)); *params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangCompute));
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER = %d", __func__, *params); MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER = %d", __func__, *params);
break; break;
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES: { case GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES: {
// Member entries of an arrayed block are recorded against the first instance; // Member entries of an arrayed block are recorded against the first instance;
// every instance of the array reports that shared member set (matches // every instance of the array reports that shared member set (matches
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, which scans with the same owner index). // GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, which scans with the same owner index).
const Int ownerIndex = static_cast<Int>(programObject->GetUniformBlockMemberOwnerIndex(uniformBlockIndex)); //
// Both sides of the comparison are BLOCK indices: GetUniformBlockMemberOwnerIndex
// answers in that space, so the scan uses GetActiveUniformOwnerBlockIndex rather
// than the GL_UNIFORM_BLOCK-space GetActiveUniformBlockIndex.
const Int ownerIndex = static_cast<Int>(programObject->GetUniformBlockMemberOwnerIndex(blockIndex));
GLint uniformIndexCount = 0; GLint uniformIndexCount = 0;
for (Uint uniformIndex = 0; uniformIndex < programObject->GetUniformCount(); ++uniformIndex) { for (Uint uniformIndex = 0; uniformIndex < programObject->GetUniformCount(); ++uniformIndex) {
if (programObject->GetActiveUniformBlockIndex(uniformIndex) != ownerIndex) { if (programObject->GetActiveUniformOwnerBlockIndex(uniformIndex) != ownerIndex) {
continue; continue;
} }
params[uniformIndexCount++] = static_cast<GLint>(uniformIndex); params[uniformIndexCount++] = static_cast<GLint>(uniformIndex);
@@ -1827,7 +1965,7 @@ namespace MobileGL::MG_Impl::GLImpl {
" is not a program object that has been linked.")); " is not a program object that has been linked."));
return; return;
} }
if (!programObject->IsActiveUniformBlock(uniformBlockIndex)) { if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
@@ -1837,7 +1975,8 @@ namespace MobileGL::MG_Impl::GLImpl {
"not the index of an active uniform block in program.")); "not the index of an active uniform block in program."));
return; return;
} }
const auto& name = programObject->GetUniformBlockName(uniformBlockIndex); const auto& name = programObject->GetUniformBlockName(
static_cast<Uint>(programObject->BlockIndexFromGlUniformBlock(uniformBlockIndex)));
CopyStr(bufSize, length, uniformBlockName, name.c_str(), (GLsizei)name.length()); CopyStr(bufSize, length, uniformBlockName, name.c_str(), (GLsizei)name.length());
MGLOG_D("%s: \"%s\" at uniformBlockIndex %02d, length = %d", __func__, uniformBlockName, uniformBlockIndex, MGLOG_D("%s: \"%s\" at uniformBlockIndex %02d, length = %d", __func__, uniformBlockName, uniformBlockIndex,
length ? *length : 0); length ? *length : 0);
@@ -2835,6 +2974,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);
@@ -307,38 +305,53 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
// GL_UNIFORM_BLOCK keeps the index space glUniformBlockBinding and // GL_UNIFORM_BLOCK keeps the index space glUniformBlockBinding and
// glGetActiveUniformBlockiv already use, so an index handed out here is usable // glGetActiveUniformBlockiv already use, so an index handed out here is usable
// with them (which is exactly what the CTS does). // with them (which is exactly what the CTS does).
const Int glBlockCount = program.GetActiveUniformBlocksCount(); const Int glBlockCount = program.GetGlUniformBlockCount();
for (Int glIndex = 0; glIndex < glBlockCount; ++glIndex) { for (Int glIndex = 0; glIndex < glBlockCount; ++glIndex) {
// The block-space index the block-keyed accessors want; the two spaces differ
// whenever the program also has a storage or atomic counter block, which
// glslang files under the same reflection list (no EShReflectionSeparateBuffers).
const Int blockIndex = program.BlockIndexFromGlUniformBlock(static_cast<Uint>(glIndex));
Resource resource; Resource resource;
resource.name = program.GetUniformBlockName(glIndex); resource.name = program.GetUniformBlockName(static_cast<Uint>(blockIndex));
resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(glIndex)); resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(static_cast<Uint>(blockIndex)));
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex)); resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(static_cast<Uint>(blockIndex)));
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex)); const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(blockIndex));
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(); // Walks the TPROGRAM uniform space, not the GL one. A buffer variable is not a GL
for (Uint glIndex = 0; glIndex < uniformCount; ++glIndex) { // uniform (GL 4.6 core 7.3.1) and DoReflection therefore keeps it out of the GL
const Int tIndex = program.TProgramUniformIndex(glIndex); // active-uniform index space - but GL_BUFFER_VARIABLE still has to enumerate it, and
const auto& refl = const_cast<glslang::TProgram&>(reflection).getUniform(tIndex); // this is the only place that does. GL uniforms keep their GL index as their
const glslang::TType* type = refl.getType(); // GL_UNIFORM resource index: the GL space is a subsequence of this one, so pushing
// the GL-visible entries in this order preserves the correspondence.
const Int tUniformCount = static_cast<Int>(reflection.uniformReflection.size());
for (Int tIndex = 0; tIndex < tUniformCount; ++tIndex) {
const auto& refl = ProgramObject::UniformAtIn(reflection, tIndex);
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]
: BlockKind::GlobalUbo; : BlockKind::GlobalUbo;
const Int glIndex = program.GlUniformIndexFromTProgram(tIndex);
// Everything except a buffer variable is enumerated through the GL space, so a
// uniform the relaxed parse swept out of it (a declared-but-dead default-block
// one) stays out of GL_UNIFORM too.
if (kind != BlockKind::Storage && glIndex < 0) continue;
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) {
@@ -366,11 +379,12 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
resource.atomicCounterBufferIndex = blockInterfaceIndex[owner]; resource.atomicCounterBufferIndex = blockInterfaceIndex[owner];
resource.location = -1; resource.location = -1;
} else { } else {
resource.blockIndex = program.GetActiveUniformBlockIndex(glIndex); const Uint glUniformIndex = static_cast<Uint>(glIndex);
resource.offset = program.GetActiveUniformOffset(glIndex); resource.blockIndex = program.GetActiveUniformBlockIndex(glUniformIndex);
resource.arrayStride = program.GetActiveUniformArrayStride(glIndex); resource.offset = program.GetActiveUniformOffset(glUniformIndex);
resource.matrixStride = program.GetActiveUniformMatrixStride(glIndex); resource.arrayStride = program.GetActiveUniformArrayStride(glUniformIndex);
resource.isRowMajor = program.GetActiveUniformIsRowMajor(glIndex); resource.matrixStride = program.GetActiveUniformMatrixStride(glUniformIndex);
resource.isRowMajor = program.GetActiveUniformIsRowMajor(glUniformIndex);
// A member of a named uniform block has no location, whatever the // A member of a named uniform block has no location, whatever the
// frontend's own location table says (it hands one out to every uniform // frontend's own location table says (it hands one out to every uniform
// so glUniform* can address block members through the global UBO). // so glUniform* can address block members through the global UBO).
@@ -389,12 +403,16 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
static_cast<GLuint>(i)); static_cast<GLuint>(i));
} }
} }
for (SizeT blockIndex = 0; blockIndex < model.uniformBlocks.size(); ++blockIndex) { for (SizeT glBlockIndex = 0; glBlockIndex < model.uniformBlocks.size(); ++glBlockIndex) {
// Members of an arrayed block are reflected once, against instance [0]. // Members of an arrayed block are reflected once, against instance [0].
const Int owner = static_cast<Int>(program.GetUniformBlockMemberOwnerIndex(static_cast<Uint>(blockIndex))); // GetUniformBlockMemberOwnerIndex takes and answers BLOCK indices, while
// Resource::blockIndex is a GL_UNIFORM_BLOCK index, so translate both ways.
const Int blockIndex = program.BlockIndexFromGlUniformBlock(static_cast<Uint>(glBlockIndex));
const Int owner = program.GlUniformBlockIndexFromBlock(
static_cast<Int>(program.GetUniformBlockMemberOwnerIndex(static_cast<Uint>(blockIndex))));
for (SizeT i = 0; i < model.uniforms.size(); ++i) { for (SizeT i = 0; i < model.uniforms.size(); ++i) {
if (model.uniforms[i].blockIndex == owner) { if (model.uniforms[i].blockIndex == owner) {
model.uniformBlocks[blockIndex].activeVariables.push_back(static_cast<GLuint>(i)); model.uniformBlocks[glBlockIndex].activeVariables.push_back(static_cast<GLuint>(i));
} }
} }
} }
@@ -414,17 +432,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 +446,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 +461,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 +481,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 +525,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,30 @@ add_executable(MobileGLIntegrationTest
Scenarios/ImageLoadStoreSsoScenario.cpp Scenarios/ImageLoadStoreSsoScenario.cpp
Scenarios/ImageTargetKindScenario.cpp Scenarios/ImageTargetKindScenario.cpp
Scenarios/ImageFormatQualifierScenario.cpp Scenarios/ImageFormatQualifierScenario.cpp
Scenarios/NonCoreImageFormatScenario.cpp
Scenarios/ImageSizeAfterRespecScenario.cpp
Scenarios/SsboDeclarationFormScenario.cpp Scenarios/SsboDeclarationFormScenario.cpp
Scenarios/Glsl420DeclarationScenario.cpp Scenarios/Glsl420DeclarationScenario.cpp
Scenarios/IoBlockNameCollisionScenario.cpp
Scenarios/TessellationDrawModeScenario.cpp
Scenarios/GeometryDrawModeScenario.cpp
Scenarios/PostLinkAttachScenario.cpp
Scenarios/FormatlessImageBakeScenario.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
Scenarios/RelinkStageSetScenario.cpp
) )
target_include_directories(MobileGLIntegrationTest PRIVATE target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -246,6 +261,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();
@@ -6,27 +6,32 @@
// SPDX-License-Identifier: LGPL-3.0-only // SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header // End of Source File Header
// //
// Scenario - GLSL DOUBLES, RUN AT SINGLE PRECISION. // Scenario - GLSL DOUBLES, AT WHATEVER PRECISION THE BACKEND CAN GIVE.
// //
// No mobile GPU has 64-bit floats. Adreno and Mali both report shaderFloat64 == VK_FALSE, so // No mobile GPU has 64-bit floats. Adreno and Mali both report shaderFloat64 == VK_FALSE, so
// Magma cannot build a module that declares the Float64 capability, and ESSL has no fp64 type // Magma cannot build a module that declares the Float64 capability there, and ESSL has no fp64
// at all, so SPIRV-Cross refuses the module outright on Espryt ("FP64 not supported in ES // type at all, so SPIRV-Cross refuses the module outright on Espryt ("FP64 not supported in ES
// profile") and the program never reaches the driver. MobileGL therefore narrows every 64-bit // profile") and the program never reaches the driver. On every such backend MobileGL narrows
// float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than declining the // every 64-bit float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than
// shader: `double` compiles and runs everywhere, at float precision. // declining the shader: `double` compiles and runs everywhere, at float precision. Where the
// backend DOES consume 64-bit floats - lavapipe is the one that does - the narrowing is skipped
// and the doubles reach the driver whole.
// //
// The narrowing is only half a contract. The other half is the API side: the global UBO is // Either way it is only half a contract. The other half is the API side: the global UBO is laid
// laid out by reflecting the DEMOTED module, so glUniform*d has to store a float where the // out by reflecting whichever module was produced, so glUniform*d has to store the width the
// shader reads a float, glGetUniform*v has to read one back, and a dmat4's columns are now // shader reads, glGetUniform*v has to read that width back, and a matrix's columns are
// std140-padded like any other matrix's. Every one of those is a byte offset that fails // std140-padded to a vec4 or a dvec4 to match. Every one of those is a byte offset that fails
// silently - the uniform simply reads as something else - so the cases below set values // silently - the uniform simply reads as something else - so the cases below set values through
// through the API and have the SHADER report what it saw. // the API and have the SHADER report what it saw.
// //
// What is deliberately NOT asserted: that the values are exact to double precision. They are // WHY ALMOST EVERY EXPECTATION HERE IS A FLOAT VALUE, and why that is not an accident of the
// not, and cannot be. Every expectation here is the float value of the double that was set, // demotion: the shader reports through a `float` SSBO, and every value chosen is exact in
// which is the whole point. // float32, so the same number is correct in both regimes and the assertions test the LAYOUT
// rather than the precision. Exactly one case (GetUniformdvReadsBackWhatWasStored) uses a value
// that is not - 0.1 - and it names both answers explicitly.
#include <cmath> #include <cmath>
#include <cstring>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -153,6 +158,151 @@ void main() {
std::string m_buildLog; std::string m_buildLog;
}; };
// A SHADER STORAGE BLOCK that holds doubles is the one place the narrowing is NOT free:
// demoting `double` to `float` also repacks the block, and the bytes the application
// wrote into the buffer do not move with it. Every member past the first double then
// reads and writes at the wrong offset, and the block is simply shorter than the one
// that was bound - the tail of it is never touched at all
// (KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case3, whose output matched its
// input up to the first double's slot and was zero from there on).
//
// The block layout is fixed by GL 4.6 core 7.6.2.2 and is asserted here as literal byte
// offsets rather than queried, so this says what the SPEC requires and not what MobileGL
// happens to report. Both packings are covered because they differ in exactly the places
// that matter: std140 rounds an array's stride and a matrix's column stride up to 16,
// std430 does not, and only std430 packs the scalars tightly.
//
// Every value is exactly representable in binary32, so a correct implementation copies
// the block BYTE FOR BYTE even though it narrows each double on the way through.
constexpr const char* kBlockCopySource = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std140, binding = 0) buffer In140 {
int data0;
float data1[3];
mat3x2 data2;
double data3;
double data4[2];
int data5;
dvec3 data6;
} g_in140;
layout(std430, binding = 1) buffer In430 {
int data0;
float data1[3];
mat3x2 data2;
double data3;
double data4[2];
int data5;
dvec3 data6;
} g_in430;
layout(std140, binding = 2) buffer Out140 {
int data0;
float data1[3];
mat3x2 data2;
double data3;
double data4[2];
int data5;
dvec3 data6;
} g_out140;
layout(std430, binding = 3) buffer Out430 {
int data0;
float data1[3];
mat3x2 data2;
double data3;
double data4[2];
int data5;
dvec3 data6;
} g_out430;
void main() {
g_out140.data0 = g_in140.data0;
for (int i = 0; i < 3; ++i) g_out140.data1[i] = g_in140.data1[i];
g_out140.data2 = g_in140.data2;
g_out140.data3 = g_in140.data3;
for (int i = 0; i < 2; ++i) g_out140.data4[i] = g_in140.data4[i];
g_out140.data5 = g_in140.data5;
g_out140.data6 = g_in140.data6;
g_out430.data0 = g_in430.data0;
for (int i = 0; i < 3; ++i) g_out430.data1[i] = g_in430.data1[i];
g_out430.data2 = g_in430.data2;
g_out430.data3 = g_in430.data3;
for (int i = 0; i < 2; ++i) g_out430.data4[i] = g_in430.data4[i];
g_out430.data5 = g_in430.data5;
g_out430.data6 = g_in430.data6;
}
)";
// GL 4.6 core 7.6.2.2 rule by rule, for the block above.
// std140: an array's element stride and a matrix's column stride round up to 16, a
// double aligns to 8 and a dvec3 to 32.
// std430: the same without the rounding - so the scalars pack tightly and only the
// dvec3's 32-byte alignment leaves a hole.
struct BlockLayout {
int data0;
int data1;
int data1Stride;
int data2;
int data2ColumnStride;
int data3;
int data4;
int data4Stride;
int data5;
int data6;
int size;
};
constexpr BlockLayout kStd140{0, 16, 16, 64, 16, 112, 128, 16, 160, 192, 216};
constexpr BlockLayout kStd430{0, 4, 4, 16, 8, 40, 48, 8, 64, 96, 120};
void PokeInt(std::vector<unsigned char>& bytes, int offset, int value) {
std::memcpy(&bytes[static_cast<std::size_t>(offset)], &value, sizeof(value));
}
void PokeFloat(std::vector<unsigned char>& bytes, int offset, float value) {
std::memcpy(&bytes[static_cast<std::size_t>(offset)], &value, sizeof(value));
}
void PokeDouble(std::vector<unsigned char>& bytes, int offset, double value) {
std::memcpy(&bytes[static_cast<std::size_t>(offset)], &value, sizeof(value));
}
// The block's contents, at the offsets the standard puts them. Padding stays zero, which
// is what makes a byte-for-byte comparison against the (zero-initialised) output buffer
// catch a member that landed somewhere it should not have.
std::vector<unsigned char> MakeBlockContents(const BlockLayout& layout) {
std::vector<unsigned char> bytes(static_cast<std::size_t>(layout.size), 0);
PokeInt(bytes, layout.data0, 1);
for (int i = 0; i < 3; ++i) {
PokeFloat(bytes, layout.data1 + i * layout.data1Stride, 2.0f + static_cast<float>(i));
}
// Column-major, two rows per column.
for (int column = 0; column < 3; ++column) {
for (int row = 0; row < 2; ++row) {
PokeFloat(bytes, layout.data2 + column * layout.data2ColumnStride + row * 4,
5.0f + static_cast<float>(column * 2 + row));
}
}
PokeDouble(bytes, layout.data3, 11.0);
for (int i = 0; i < 2; ++i) {
PokeDouble(bytes, layout.data4 + i * layout.data4Stride, 12.0 + static_cast<double>(i));
}
PokeInt(bytes, layout.data5, 14);
for (int i = 0; i < 3; ++i) {
PokeDouble(bytes, layout.data6 + i * 8, 15.0 + static_cast<double>(i));
}
return bytes;
}
// Names the first byte that differs, and which member owns it, so a failure is a
// diagnosis rather than "the buffer is wrong".
std::string DescribeOffset(const BlockLayout& layout, int offset) {
const std::pair<int, const char*> members[] = {
{layout.data0, "data0"}, {layout.data1, "data1"}, {layout.data2, "data2"},
{layout.data3, "data3"}, {layout.data4, "data4"}, {layout.data5, "data5"},
{layout.data6, "data6"}};
const char* owner = "(padding before data0)";
for (const auto& [start, name] : members) {
if (offset >= start) owner = name;
}
return std::string(owner);
}
// Every double-typed uniform shape GLSL has, all thirteen of them, in one program - the // Every double-typed uniform shape GLSL has, all thirteen of them, in one program - the
// shape of KHR-GL43.compute_shader.fp64-case2. The scalar and the square matrices are // shape of KHR-GL43.compute_shader.fp64-case2. The scalar and the square matrices are
// covered by the cases above; what only a set like this reaches is the NON-SQUARE // covered by the cases above; what only a set like this reaches is the NON-SQUARE
@@ -428,12 +578,24 @@ void main() {
glUseProgram(0); glUseProgram(0);
// The readback has to undo exactly what the write did - the same std140 column // The readback has to undo exactly what the write did - the same std140 column
// padding, the same 4-byte components - or a dmat4 comes back with its columns // padding, the same component width - or a dmat4 comes back with its columns
// shifted and nothing else in the API would say so. // shifted and nothing else in the API would say so. Every value below except the
// scalar is exact in float32, so those expectations pin the LAYOUT and hold in
// either regime; the scalar is the one that also pins the PRECISION.
GLdouble readScalar = 0.0; GLdouble readScalar = 0.0;
glGetUniformdv(m_program, scalar, &readScalar); glGetUniformdv(m_program, scalar, &readScalar);
EXPECT_DOUBLE_EQ(readScalar, static_cast<double>(static_cast<float>(0.1))) // 0.1 is not representable in float32, so what comes back names the regime: a
<< "the value is what a float can hold, not the double that was passed in"; // backend without native fp64 narrowed it at the glUniform1d above (the module's own
// doubles were demoted, so its storage is 4 bytes per component), and one with it
// stored the double whole. Both are correct; asserting only the narrow answer would
// fail the moment fp64 stops being emulated, and asserting only the wide one would
// fail on every mobile device there is.
if (readScalar == 0.1) {
SUCCEED() << "this backend consumes 64-bit floats natively; the double survived whole";
} else {
EXPECT_DOUBLE_EQ(readScalar, static_cast<double>(static_cast<float>(0.1)))
<< "the value is what a float can hold, not the double that was passed in";
}
GLdouble readVector[3] = {}; GLdouble readVector[3] = {};
glGetUniformdv(m_program, vector, readVector); glGetUniformdv(m_program, vector, readVector);
@@ -447,7 +609,8 @@ void main() {
EXPECT_DOUBLE_EQ(readMatrix[i], 100.0 + i) << "dmat4 component " << i; EXPECT_DOUBLE_EQ(readMatrix[i], 100.0 + i) << "dmat4 component " << i;
} }
// The float query sees the same storage through the type it is actually stored as. // The float query sees the same storage through a narrower type, and answers the
// same float either way: GL 4.6 core 7.6 converts on the way out.
GLfloat readFloat = 0.0f; GLfloat readFloat = 0.0f;
glGetUniformfv(m_program, scalar, &readFloat); glGetUniformfv(m_program, scalar, &readFloat);
EXPECT_FLOAT_EQ(readFloat, static_cast<float>(0.1)); EXPECT_FLOAT_EQ(readFloat, static_cast<float>(0.1));
@@ -697,24 +860,188 @@ 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);
}
TEST_F(DoublePrecisionScenario, AStorageBlockWithDoublesKeepsTheLayoutItWasBoundWith) {
if (!Ready()) return;
GLint blocks = 0;
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &blocks);
if (blocks < 4) {
GTEST_SKIP() << "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS is " << blocks << "; this needs 4";
}
const unsigned int program = CompileComputeProgram(kBlockCopySource);
ASSERT_NE(program, 0u) << m_buildLog;
const std::vector<unsigned char> in140 = MakeBlockContents(kStd140);
const std::vector<unsigned char> in430 = MakeBlockContents(kStd430);
const std::vector<unsigned char> zero140(in140.size(), 0);
const std::vector<unsigned char> zero430(in430.size(), 0);
GLuint buffers[4] = {};
glGenBuffers(4, buffers);
const std::vector<unsigned char>* contents[4] = {&in140, &in430, &zero140, &zero430};
for (int i = 0; i < 4; ++i) {
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(i), buffers[i]);
glBufferData(GL_SHADER_STORAGE_BUFFER, static_cast<GLsizeiptr>(contents[i]->size()),
contents[i]->data(), GL_DYNAMIC_COPY);
}
ASSERT_EQ(FirstGLError(), 0u);
glUseProgram(program);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
EXPECT_EQ(FirstGLError(), 0u);
for (int pass = 0; pass < 2; ++pass) {
const BlockLayout& layout = pass == 0 ? kStd140 : kStd430;
const std::vector<unsigned char>& expected = pass == 0 ? in140 : in430;
const char* packing = pass == 0 ? "std140" : "std430";
std::vector<unsigned char> observed(expected.size(), 0xEE);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffers[2 + pass]);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
static_cast<GLsizeiptr>(observed.size()), observed.data());
int mismatches = 0;
int firstMismatch = -1;
for (std::size_t i = 0; i < expected.size(); ++i) {
if (expected[i] == observed[i]) continue;
++mismatches;
if (firstMismatch < 0) firstMismatch = static_cast<int>(i);
}
EXPECT_EQ(mismatches, 0)
<< packing << " block: " << mismatches << " of " << expected.size()
<< " bytes differ, first at byte " << firstMismatch << " (in "
<< DescribeOffset(layout, firstMismatch < 0 ? 0 : firstMismatch)
<< "); a block that was repacked around its doubles reads and writes every "
"member after the first one at the wrong offset";
}
glUseProgram(0);
glDeleteProgram(program);
glDeleteBuffers(4, buffers);
EXPECT_EQ(FirstGLError(), 0u);
}
} // namespace } // namespace
} // namespace MGITest } // namespace MGITest
@@ -0,0 +1,211 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/FormatlessImageBakeScenario.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 FORMAT-LESS IMAGE UNIFORM WHOSE UNIT HOLDS A NON-CORE FORMAT.
//
// GLSL 4.20 lets a write-only image uniform omit its layout format; GLSL ES demands one, so
// DirectGLES BAKES the format of whatever glBindImageTexture put on the unit into the
// declaration. When that format is outside the GLSL ES core thirteen, the bake alone is not
// enough - the baked declaration then has to go through the same channel-widening
// WidenImageFormatsForEssl gives a DECLARED non-core format (see NonCoreImageFormatScenario for
// the widening itself).
//
// The two routes had different arming. The declared route armed the widening on the format
// alone; the baked route armed it only when the driver lacked GL_NV_image_formats. That reads
// like an optimisation and is not one: SPIRV-Cross throws for its is_desktop_only_format set the
// moment it targets ESSL, whatever the driver would have accepted, so on a driver that HAS the
// extension the shader half of the widening stayed switched off while TextureImpl's storage/bind
// half - which keys on SpirvCrossCanPrintEsslImageFormat, not on the driver bit - still ran. The
// stage threw, the program linked without it, and every dispatch silently did nothing.
//
// KHR-GL43.stencil_texturing.functional is where it surfaced: its compute half writes through a
// format-less `uimage2D` bound to an R8UI texture, and returned zeros for every texel.
//
// DISCRIMINATING ONLY WHERE THE DRIVER ADVERTISES GL_NV_image_formats - Mesa does, which is what
// the software lanes run and where this was found. On Adreno 830 and both Malis the extension is
// absent, the old code already armed the widening, and these cases pass before and after; they
// are kept running there as a guard against the opposite mistake.
#include <cstdint>
#include <cstring>
#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 = 8;
// No layout format on uni_image on purpose: that is the whole subject. uni_source is a
// plain integer texture so nothing but the image declaration is in play.
const char* const kComputeSource = R"(#version 430 core
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
writeonly uniform uimage2D uni_image;
uniform usampler2D uni_source;
void main()
{
ivec2 at = ivec2(gl_GlobalInvocationID.xy);
imageStore(uni_image, at, uvec4(texelFetch(uni_source, at, 0).r, 0u, 0u, 0u));
}
)";
class FormatlessImageBakeScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
if (!BackendHostsCompute()) {
GTEST_SKIP() << "no compute stage on " << Gl().BackendName() << " ("
<< Gl().RendererString() << ")";
}
}
static bool BackendHostsCompute() {
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
DrainErrors();
return maxImageUnits >= 2;
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
static GLuint BuildCompute(const char* source, std::string& log) {
const GLuint cs = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(cs, 1, &source, nullptr);
glCompileShader(cs);
GLint ok = 0;
glGetShaderiv(cs, GL_COMPILE_STATUS, &ok);
if (!ok) {
char buffer[2048] = "";
glGetShaderInfoLog(cs, sizeof(buffer), nullptr, buffer);
log = buffer;
glDeleteShader(cs);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, cs);
glLinkProgram(program);
glGetProgramiv(program, GL_LINK_STATUS, &ok);
glDeleteShader(cs);
if (!ok) {
char buffer[2048] = "";
glGetProgramInfoLog(program, sizeof(buffer), nullptr, buffer);
log = buffer;
glDeleteProgram(program);
return 0;
}
return program;
}
// internalFormat is the NON-CORE image format under test; the destination texture and
// the glBindImageTexture argument both use it, and the shader declares nothing.
void RunCopy(GLenum internalFormat, GLenum uploadFormat, GLenum uploadType) {
std::vector<GLuint> expected(kExtent * kExtent);
for (int i = 0; i < kExtent * kExtent; ++i) {
expected[i] = static_cast<GLuint>(1 + i);
}
// Source: a core-format integer texture holding 1..64.
std::vector<GLubyte> sourceBytes(kExtent * kExtent);
for (int i = 0; i < kExtent * kExtent; ++i) {
sourceBytes[i] = static_cast<GLubyte>(expected[i]);
}
GLuint sourceTexture = 0;
glGenTextures(1, &sourceTexture);
glBindTexture(GL_TEXTURE_2D, sourceTexture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8UI, kExtent, kExtent);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent, GL_RED_INTEGER, GL_UNSIGNED_BYTE,
sourceBytes.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
// Destination: the format under test, zero-filled so "the dispatch did nothing"
// and "the dispatch wrote zeros" are the same observation the CTS made.
GLuint destTexture = 0;
glGenTextures(1, &destTexture);
glBindTexture(GL_TEXTURE_2D, destTexture);
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kExtent, kExtent);
const std::vector<GLubyte> zeros(static_cast<std::size_t>(kExtent) * kExtent * 8, 0);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent, uploadFormat, uploadType, zeros.data());
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "destination storage";
std::string log;
const GLuint program = BuildCompute(kComputeSource, log);
ASSERT_NE(program, 0u) << "the format-less image program did not build: " << log;
glUseProgram(program);
glBindImageTexture(1, destTexture, 0, GL_FALSE, 0, GL_WRITE_ONLY, internalFormat);
glUniform1i(glGetUniformLocation(program, "uni_image"), 1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, sourceTexture);
glUniform1i(glGetUniformLocation(program, "uni_source"), 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "binding";
glDispatchCompute(kExtent, kExtent, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "dispatch";
std::vector<GLuint> readback(kExtent * kExtent, 0xFFFFFFFFu);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, destTexture);
glGetTexImage(GL_TEXTURE_2D, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, readback.data());
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "readback";
int offenders = 0;
for (int i = 0; i < kExtent * kExtent; ++i) {
if (readback[i] != expected[i]) ++offenders;
}
EXPECT_EQ(offenders, 0) << "the dispatch wrote " << offenders << " of "
<< (kExtent * kExtent) << " texels wrongly; texel 0 was "
<< readback[0] << ", expected " << expected[0]
<< ". A whole stage lost to the ESSL emitter looks exactly like this.";
glUseProgram(0);
glDeleteProgram(program);
glDeleteTextures(1, &sourceTexture);
glDeleteTextures(1, &destTexture);
DrainErrors();
}
};
// R8UI: one of the seven formats GLSL ES reaches only through GL_NV_image_formats AND one
// SPIRV-Cross refuses to print for ESSL, so it needs the widening in both driver modes.
TEST_F(FormatlessImageBakeScenario, R8uiBakedFromTheBoundUnitStillReachesTheDriver) {
if (!Ready()) GTEST_SKIP();
RunCopy(GL_R8UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE);
}
// R16UI, from the same set, carried in RGBA16UI: the fix must not be R8UI-shaped.
TEST_F(FormatlessImageBakeScenario, R16uiBakedFromTheBoundUnitStillReachesTheDriver) {
if (!Ready()) GTEST_SKIP();
RunCopy(GL_R16UI, GL_RED_INTEGER, GL_UNSIGNED_SHORT);
}
// The control: R32UI is in the GLSL ES core thirteen, so it is baked and never widened.
// It passed before the fix and has to keep passing.
TEST_F(FormatlessImageBakeScenario, CoreFormatBakedFromTheBoundUnitIsUnaffected) {
if (!Ready()) GTEST_SKIP();
RunCopy(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT);
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,413 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/GeometryDrawModeScenario.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 GEOMETRY SHADER'S INPUT PRIMITIVE CONSTRAINS THE DRAW MODE, AND
// GL_NONE IS NOT A USABLE "NO GEOMETRY SHADER" SENTINEL.
//
// GL 4.6 core 11.3.1: mode must be one of the primitive types that decomposes into the
// geometry shader's declared input primitive, or the draw is GL_INVALID_OPERATION. The
// validator asked "is there a geometry stage?" by comparing the REFLECTED INPUT PRIMITIVE
// against GL_NONE - and GL_NONE and GL_POINTS are both 0, so a `layout(points) in` geometry
// shader answered "no geometry stage" and every mode sailed through. The rule was therefore
// dead for exactly the geometry shaders whose input primitive rejects the most modes.
//
// KHR-GL43.transform_feedback.api_errors_test is where it showed: it draws a points-in
// geometry program with GL_LINES through glDrawTransformFeedbackInstanced and requires
// INVALID_OPERATION. The bug is not specific to that entry point - every draw shares this
// validator - so the ordinary glDrawArrays spelling is pinned here too, and the lines-in
// program is the control that proves the rule was not simply widened.
//
// Needs a real context: the validator returns before this rule when no backend object is
// active, so the GPU-free negative-API suite cannot reach it.
#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);
}
)";
// The input primitive the CTS case uses, and the one the GL_NONE sentinel erased.
// `result` is here so the same program can be captured with transform feedback.
const char* const kPointsInGeometrySource = R"(#version 420 core
layout(points) in;
layout(points, max_vertices = 1) out;
out float result;
void main()
{
gl_Position = gl_in[0].gl_Position;
result = 1.0;
EmitVertex();
}
)";
const char* const kLinesInGeometrySource = R"(#version 420 core
layout(lines) in;
layout(points, max_vertices = 1) out;
void main()
{
gl_Position = gl_in[0].gl_Position;
EmitVertex();
}
)";
const char* const kFragmentSource = R"(#version 420 core
out vec4 fragColor;
void main()
{
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
)";
class GeometryDrawModeScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
if (!BackendHostsGeometry()) {
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " ("
<< Gl().RendererString() << "); there is no input primitive 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_geometry_shader and on a DirectVulkan device without the
// geometryShader feature.
static bool BackendHostsGeometry() {
GLint maxGeometryOutputVertices = 0;
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
DrainErrors();
return maxGeometryOutputVertices >= 4;
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
GLuint BuildProgram(const char* geometrySource, const char* capturedVarying = nullptr) {
const std::vector<std::pair<GLenum, const char*>> stages = {
{GL_VERTEX_SHADER, kVertexSource},
{GL_GEOMETRY_SHADER, geometrySource},
{GL_FRAGMENT_SHADER, kFragmentSource}};
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);
if (capturedVarying != nullptr) {
glTransformFeedbackVaryings(program, 1, &capturedVarying, GL_INTERLEAVED_ATTRIBS);
}
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;
};
// GL_POINTS is the only mode that decomposes into a points input primitive.
TEST_F(GeometryDrawModeScenario, PointsInGeometryProgramRejectsEveryOtherMode) {
if (!Ready()) GTEST_SKIP();
const GLuint program = BuildProgram(kPointsInGeometrySource);
ASSERT_NE(program, 0u) << "the points-in geometry program did not build: " << BuildLog();
glUseProgram(program);
DrainErrors();
for (const GLenum mode :
{static_cast<GLenum>(GL_LINES), static_cast<GLenum>(GL_LINE_STRIP),
static_cast<GLenum>(GL_TRIANGLES), static_cast<GLenum>(GL_TRIANGLE_STRIP)}) {
glDrawArrays(mode, 0, 3);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
<< "mode " << mode << " does not decompose into the geometry shader's points input";
DrainErrors();
}
// The one mode that IS compatible still draws.
glDrawArrays(GL_POINTS, 0, 1);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
DrainErrors();
}
// The same rule reached through glDrawTransformFeedback*, which is the spelling the CTS
// case asks about. The capture span is really completed first, so GL_POINTS comes back
// GL_NO_ERROR: without that the draw would report INVALID_OPERATION for the
// never-ended-a-span reason instead and the case could not tell the two apart.
TEST_F(GeometryDrawModeScenario, PointsInGeometryProgramRejectsNonPointModesOnFeedbackDraws) {
if (!Ready()) GTEST_SKIP();
const GLuint program = BuildProgram(kPointsInGeometrySource, "result");
ASSERT_NE(program, 0u) << "the points-in geometry program did not build: " << BuildLog();
GLuint feedback = 0;
glGenTransformFeedbacks(1, &feedback);
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, feedback);
GLuint captureBuffer = 0;
glGenBuffers(1, &captureBuffer);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, captureBuffer);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 64, nullptr, GL_STATIC_DRAW);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBuffer);
glUseProgram(program);
DrainErrors();
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, 1);
glEndTransformFeedback();
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the capture span did not complete";
glDrawTransformFeedbackInstanced(GL_LINES, feedback, 1);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
<< "glDrawTransformFeedbackInstanced must honour the geometry input primitive";
DrainErrors();
glDrawTransformFeedbackStreamInstanced(GL_LINES, feedback, 0, 1);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
<< "glDrawTransformFeedbackStreamInstanced must honour the geometry input primitive";
DrainErrors();
// The compatible mode replays the captured span with no error at all, which is what
// makes the two assertions above about the MODE and not about the span.
glDrawTransformFeedbackInstanced(GL_POINTS, feedback, 1);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< "a compatible mode must still replay the captured span";
DrainErrors();
glUseProgram(0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
glDeleteBuffers(1, &captureBuffer);
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0);
glDeleteTransformFeedbacks(1, &feedback);
DrainErrors();
}
// The control: a lines-in geometry shader is a NON-zero input primitive, so it exercised
// the rule even before the fix. It must still accept the line modes and still reject the
// others - a fix that widened the rule instead of repairing its guard breaks this.
TEST_F(GeometryDrawModeScenario, LinesInGeometryProgramStillAcceptsLineModesOnly) {
if (!Ready()) GTEST_SKIP();
const GLuint program = BuildProgram(kLinesInGeometrySource);
ASSERT_NE(program, 0u) << "the lines-in geometry program did not build: " << BuildLog();
glUseProgram(program);
DrainErrors();
for (const GLenum mode : {static_cast<GLenum>(GL_LINES), static_cast<GLenum>(GL_LINE_STRIP),
static_cast<GLenum>(GL_LINE_LOOP)}) {
glDrawArrays(mode, 0, 2);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< "mode " << mode << " decomposes into lines and must be accepted";
DrainErrors();
}
for (const GLenum mode : {static_cast<GLenum>(GL_POINTS), static_cast<GLenum>(GL_TRIANGLES)}) {
glDrawArrays(mode, 0, 3);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
<< "mode " << mode << " does not decompose into lines";
DrainErrors();
}
}
// The other half of "ask the stage": WHICH stage list is asked. gsInputPrimitive is a
// LINK artifact, so pairing it with the live attach list re-points the GL_NONE/GL_POINTS
// aliasing instead of removing it - inside the window between glAttachShader and the
// next link, the live list says "geometry present" while the artifact still reads
// GL_NONE, which is 0, which is GL_POINTS, so every mode but GL_POINTS is rejected.
//
// GL 4.6 core 7.3 makes that window legal and ordinary: an attach affects the program's
// executable only at the next link, and leaves LINK_STATUS alone. The attached shader
// need not even compile. Worse, it does not heal - glDetachShader defers the removal to
// the next Link() too, so the program would keep failing every non-POINTS draw until the
// application happened to relink for some unrelated reason.
TEST_F(GeometryDrawModeScenario, AttachingAGeometryStageAfterTheLinkDoesNotConstrainTheDrawMode) {
if (!Ready()) GTEST_SKIP();
// Deliberately NOT BuildProgram: the executable under test has no geometry stage.
const GLuint program = glCreateProgram();
m_programs.push_back(program);
for (const auto& [stage, source] :
std::vector<std::pair<GLenum, const char*>>{{GL_VERTEX_SHADER, kVertexSource},
{GL_FRAGMENT_SHADER, kFragmentSource}}) {
const GLuint shader = glCreateShader(stage);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
glAttachShader(program, shader);
glDeleteShader(shader);
}
glLinkProgram(program);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
ASSERT_EQ(linked, GL_TRUE) << "the vertex+fragment program did not link";
glUseProgram(program);
DrainErrors();
glDrawArrays(GL_TRIANGLES, 0, 3);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< "a program with no geometry stage must draw triangles";
DrainErrors();
const GLuint geometry = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geometry, 1, &kPointsInGeometrySource, nullptr);
glCompileShader(geometry);
glAttachShader(program, geometry);
glDeleteShader(geometry);
DrainErrors();
// Same executable as three lines ago - no relink has happened.
glDrawArrays(GL_TRIANGLES, 0, 3);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< "the attach does not reach the executable until the next link, so the geometry "
"shader's points input must not constrain this draw";
DrainErrors();
// And once it IS linked in, the rule applies - the fix must not have simply disabled it.
glLinkProgram(program);
glGetProgramiv(program, GL_LINK_STATUS, &linked);
ASSERT_EQ(linked, GL_TRUE) << "the relink with the geometry stage failed";
glUseProgram(program);
DrainErrors();
glDrawArrays(GL_TRIANGLES, 0, 3);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
<< "now that the points-in geometry shader is in the executable, triangles must be rejected";
DrainErrors();
}
// The tessellation guard above the geometry one had the identical defect, and it does not
// even need the GL_NONE aliasing to misfire: it drives BOTH directions unconditionally, so
// reading the live attach list rejects every non-GL_PATCHES draw the moment an evaluation
// shader is attached, whether or not it was ever linked in.
TEST_F(GeometryDrawModeScenario, AttachingATessEvalStageAfterTheLinkDoesNotForceGlPatches) {
if (!Ready()) GTEST_SKIP();
GLint maxPatchVertices = 0;
glGetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices);
DrainErrors();
if (maxPatchVertices < 3) GTEST_SKIP() << "no tessellation stage on this backend";
const GLuint program = glCreateProgram();
m_programs.push_back(program);
for (const auto& [stage, source] :
std::vector<std::pair<GLenum, const char*>>{{GL_VERTEX_SHADER, kVertexSource},
{GL_FRAGMENT_SHADER, kFragmentSource}}) {
const GLuint shader = glCreateShader(stage);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
glAttachShader(program, shader);
glDeleteShader(shader);
}
glLinkProgram(program);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
ASSERT_EQ(linked, GL_TRUE) << "the vertex+fragment program did not link";
glUseProgram(program);
DrainErrors();
static const char* const kTessEvalSource = R"(#version 420 core
layout(triangles, equal_spacing, ccw) in;
void main()
{
gl_Position = gl_in[0].gl_Position;
}
)";
const GLuint tessEval = glCreateShader(GL_TESS_EVALUATION_SHADER);
glShaderSource(tessEval, 1, &kTessEvalSource, nullptr);
glCompileShader(tessEval);
glAttachShader(program, tessEval);
glDeleteShader(tessEval);
DrainErrors();
glDrawArrays(GL_TRIANGLES, 0, 3);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< "the executable still has no tessellation stage, so GL_PATCHES must not be required";
DrainErrors();
}
} // namespace
} // namespace MGITest
@@ -179,6 +179,13 @@ void main()
in flat uint v_index; in flat uint v_index;
out vec4 o_color; out vec4 o_color;
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); } void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
)";
// The colour index spelled out at its default value. Says nothing that
// `layout(location = 0)` alone does not, and must therefore cost nothing.
constexpr const char* kExplicitColorIndexFS = R"(#version 420 core
layout(location = 0, index = 0) out vec4 o_color;
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
)"; )";
class Glsl420DeclarationScenario : public ScenarioTest { class Glsl420DeclarationScenario : public ScenarioTest {
@@ -473,4 +480,24 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
EXPECT_EQ(centre.g, 255) << "the atomic-counter shader linked but painted nothing"; EXPECT_EQ(centre.g, 255) << "the atomic-counter shader linked but painted nothing";
} }
// `layout(location = 0, index = 0)` is the GL default written out loud, and an application
// is entitled to write it - KHR-GL43.shader_atomic_counters.basic-program-query does. It has
// to reach the driver as an ORDINARY single-source output: GLSL ES has no `index` qualifier
// in core, so a transpiler that prints the decoration back gets "index layout qualifier
// requires EXT_blend_func_extended", the stage never compiles, the program runs with a stage
// missing and the draw paints nothing at all. Black, not red - which is why the conformance
// case looked like the atomic counters had stopped counting.
TEST_F(Glsl420DeclarationScenario, AnExplicitDefaultColorIndexStillDraws) {
if (!Ready()) return;
const GLuint program = Build(kQuadVS, kExplicitColorIndexFS);
if (program == 0) return;
const Rgba8 centre = DrawAndRead(program);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(centre.g, 255) << "a fragment output declared layout(location = 0, index = 0) painted "
"nothing; its stage was almost certainly refused by the driver";
EXPECT_EQ(centre.r, 0u);
}
} // namespace MGITest } // namespace MGITest
@@ -127,14 +127,23 @@ void main()
// One qualifier is all an ARRAY declaration can carry, and ESSL then gives the // One qualifier is all an ARRAY declaration can carry, and ESSL then gives the
// array's elements the CONSECUTIVE units N, N+1, N+2, ... - so a per-element // array's elements the CONSECUTIVE units N, N+1, N+2, ... - so a per-element
// assignment that is not consecutive (the conformance case uses 0, 2, 4, 6) has no // assignment that is not consecutive (the conformance case uses 0, 2, 4, 6) has no
// spelling in a single declaration and cannot be expressed at all without splitting // spelling in a single declaration.
// the array into one declaration per element and rewriting every use of it.
// //
// Scoped rather than disabled, exactly as ProgramPipelineScenario scopes its // RemapImageArrayElementUnits repairs it by SPLITTING the array into one scalar
// storage-block rebinding cases: the defect is per-backend and the frontend // image uniform per element, each carrying its own binding, which costs exactly the
// mechanism these cases exist for - per-element units surviving the trip to the // four image uniforms the application declared. (It used to WIDEN the array to cover
// pipeline composite - is fully exercised on Magma. // the whole span instead, which cost seven for those four elements and had to be
bool PerElementImageUnitsAreHonoured() const { return Gl().BackendName() == "DirectVulkan"; } // declined on a stage that could not afford them - hence the budget gate that used
// to be here.) DirectVulkan needs no rewrite at all.
bool PerElementImageUnitsAreHonoured() const {
if (Gl().BackendName() == "DirectVulkan") return true;
GLint maxFragmentImageUniforms = 0;
glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImageUniforms);
while (glGetError() != GL_NO_ERROR) {
}
// One per element of the four-element array either fragment program declares.
return maxFragmentImageUniforms >= 4;
}
// The scenarios below need image load/store at all; a driver without it should skip // The scenarios below need image load/store at all; a driver without it should skip
// rather than fail. // rather than fail.
@@ -164,7 +173,7 @@ void main()
if (!Ready()) return; if (!Ready()) return;
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units"; if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
if (!PerElementImageUnitsAreHonoured()) { if (!PerElementImageUnitsAreHonoured()) {
GTEST_SKIP() << "non-consecutive per-element image units cannot be baked into ESSL"; GTEST_SKIP() << "fewer than 4 fragment image uniforms: the array under test does not fit";
} }
HeadlessGL& gl = Gl(); HeadlessGL& gl = Gl();
@@ -283,8 +292,12 @@ void main()
TEST_F(ImageLoadStoreSsoScenario, AnImageArrayAlongsideAnotherDescriptorKeepsBothBindings) { TEST_F(ImageLoadStoreSsoScenario, AnImageArrayAlongsideAnotherDescriptorKeepsBothBindings) {
if (!Ready()) return; if (!Ready()) return;
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units"; if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
if (!PerElementImageUnitsAreHonoured()) { // The defect this guards is the SPIR-V descriptor remap, which only Magma has; the units
GTEST_SKIP() << "non-consecutive per-element image units cannot be baked into ESSL"; // here are consecutive on purpose, so on Espryt this would exercise nothing the case
// above does not. Scoped by what it TESTS rather than by the image-array widening, which
// it deliberately never triggers.
if (Gl().BackendName() != "DirectVulkan") {
GTEST_SKIP() << "the descriptor binding remap under test is DirectVulkan's";
} }
HeadlessGL& gl = Gl(); HeadlessGL& gl = Gl();
@@ -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
File diff suppressed because it is too large Load Diff
@@ -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,325 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PostLinkAttachScenario.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 PROGRAM'S LIVE ATTACH LIST IS NOT ITS EXECUTABLE, AND THE BACKENDS MAY NOT
// INDEX ONE BY THE OTHER.
//
// GL 4.6 core 7.3: glAttachShader adds to the program's attach list immediately and affects
// what the program RUNS only at the next link (glDetachShader defers its removal the same
// way). So between an attach and the relink the two lists differ - the attach list is
// strictly longer - and the program stays perfectly drawable throughout, with the executable
// its last link produced.
//
// Both backends walked the attach list while indexing the LAST LINK's generated SPIR-V by
// the same running index:
//
// DirectGLES BackendProgramObjectImpl::SyncToBackend - `shaderSpirvs[index]` over
// `attachedShaders.size()`
// DirectVulkan ProgramFactory::GetOrCreateProgram - `spirv[i]` and `moduleSpirvs[i]`
// over `shaders.size()`
//
// One post-link attach therefore read one Vector past the end of the module array and
// copied it, which is the SIGSEGV this scenario is the regression test for (the source
// vector reported a capacity of 35177040171136). DirectGLES additionally derived
// "does this program tessellate" from the same wrong list, which would synthesize a
// pass-through tessellation control stage for an executable that does not tessellate.
//
// The repro needs the attach to land BEFORE the program's first backend build: the ES
// twin's rebuild is gated on the link version (which an attach does not move), so a program
// that was already drawn once keeps its built driver program and never re-reads the list.
// Every case below therefore attaches first and draws second.
//
// Deliberately pinned with a PIXEL and not just with glGetError. "Reject the draw earlier"
// would silence the crash while breaking the spec - GL requires this draw to execute - so
// the assertion has to be that the frame really came out, not merely that nothing complained.
//
// Needs a real context: the crash is in a backend program build, which the GPU-free suites
// never reach.
#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 kFboWidth = 64;
constexpr int kFboHeight = 64;
// A full-viewport triangle from gl_VertexID alone, so the scenario needs no vertex
// buffer and every pixel of the target is covered by the one draw.
const char* const kVertexSource = R"(#version 330 core
void main()
{
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
}
)";
const char* const kFragmentSource = R"(#version 330 core
out vec4 fragColor;
void main()
{
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
)";
// The replacement fragment stage of the last case. A different colour, so "which
// executable did this draw run" is answerable from the frame alone.
const char* const kBlueFragmentSource = R"(#version 330 core
out vec4 fragColor;
void main()
{
fragColor = vec4(0.0, 0.0, 1.0, 1.0);
}
)";
constexpr Rgba8 kGreen{0, 255, 0, 255};
constexpr Rgba8 kBlue{0, 0, 255, 255};
// The extra attaches. Each declares a stage the executable ALREADY has and no main(),
// which is what a real shader library looks like and what makes the relink at the end
// of the second case legal. Their whole job here is to make the attach list longer
// than the module array.
const char* const kVertexHelperSource = R"(#version 330 core
vec4 mgPostLinkAttachVertexHelper()
{
return vec4(0.0, 0.0, 0.0, 1.0);
}
)";
const char* const kFragmentHelperSource = R"(#version 330 core
vec4 mgPostLinkAttachFragmentHelper()
{
return vec4(1.0, 0.0, 1.0, 1.0);
}
)";
// A pass-through, so that once it IS linked in the same full-viewport triangle still
// reaches the rasterizer and the final frame is still comparable to the first one.
const char* const kGeometrySource = R"(#version 330 core
layout(triangles) in;
layout(triangle_strip, max_vertices = 3) out;
void main()
{
for (int i = 0; i < 3; ++i) {
gl_Position = gl_in[i].gl_Position;
EmitVertex();
}
EndPrimitive();
}
)";
class PostLinkAttachScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
m_target = MakeColorFbo(kFboWidth, kFboHeight);
ASSERT_NE(m_target.fbo, 0u) << "could not create the scenario's colour target";
BindFbo(m_target);
DrainErrors();
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (const GLuint program : m_programs) glDeleteProgram(program);
m_programs.clear();
for (const GLuint shader : m_shaders) glDeleteShader(shader);
m_shaders.clear();
BindDefaultFramebuffer();
DestroyColorFbo(m_target);
glBindVertexArray(0);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
m_vao = 0;
DrainErrors();
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
static bool BackendHostsGeometry() {
GLint maxGeometryOutputVertices = 0;
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
DrainErrors();
return maxGeometryOutputVertices >= 4;
}
// Kept alive until TearDown rather than flagged for deletion at attach time: a
// deleted-but-attached shader is a second, unrelated lifetime rule, and this
// scenario is about which LIST the backend reads.
GLuint MakeShader(GLenum stage, const char* source) {
const GLuint shader = glCreateShader(stage);
if (shader == 0) return 0;
m_shaders.push_back(shader);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
return shader;
}
// Vertex + fragment, linked. This is the executable every case draws with.
// `outFragmentShader` is the stage that paints green, which the last case needs a
// name for in order to detach it.
GLuint LinkBaseProgram(GLuint* outFragmentShader = nullptr) {
const GLuint program = glCreateProgram();
m_programs.push_back(program);
const GLuint fragment = MakeShader(GL_FRAGMENT_SHADER, kFragmentSource);
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexSource));
glAttachShader(program, fragment);
glLinkProgram(program);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (!linked) return 0;
if (outFragmentShader != nullptr) *outFragmentShader = fragment;
return program;
}
// Clears to red, draws the full-viewport triangle, and hands back the frame. Red
// is deliberately the clear colour: a draw that silently did not execute leaves a
// red target, which is a different failure message from a draw that executed and
// painted the wrong thing.
//
// `outDrawError` is sampled between the draw and the readback, so a rejected draw
// is never confused with a readback that went wrong afterwards.
Image DrawFullViewportTriangle(GLuint program, GLenum mode, GLenum* outDrawError = nullptr) {
glUseProgram(program);
ClearTo(1.0f, 0.0f, 0.0f, 1.0f);
DrainErrors();
glDrawArrays(mode, 0, 3);
if (outDrawError != nullptr) *outDrawError = glGetError();
return ReadPixels(kFboWidth, kFboHeight);
}
// The clear colour is red and no shader here ever writes red, so "still red" reads
// as "the draw did not execute" and any other wrong colour as "it executed against
// the wrong modules" - two failures worth telling apart.
static void ExpectFullyColored(const Image& frame, const Rgba8& expected, const char* what) {
ASSERT_FALSE(frame.Empty()) << what << ": nothing was read back";
for (const int y : {0, kFboHeight / 2, kFboHeight - 1}) {
for (const int x : {0, kFboWidth / 2, kFboWidth - 1}) {
EXPECT_EQ(frame.At(x, y), expected)
<< what << ": pixel (" << x << ", " << y << ") is " << frame.ColorName(x, y);
}
}
}
GLuint m_vao = 0;
ColorFbo m_target{};
std::vector<GLuint> m_programs;
std::vector<GLuint> m_shaders;
};
// THE REGRESSION. Up to four shaders attached after the link (the geometry one only
// where the backend has that stage), two of them duplicating a stage the executable
// already carries - so the attach list runs to five or six while the last link produced
// two modules, and the old loops read indices 2..5 of a 2-element array.
//
// Duplicating a stage is the sharp case on purpose: it is the one shape under which a
// "look the stage up in the attach list instead" repair still returns a valid-looking
// index for a module that does not exist.
TEST_F(PostLinkAttachScenario, DrawingAfterPostLinkAttachesStaysInsideTheGeneratedModules) {
if (!Ready()) GTEST_SKIP();
const GLuint program = LinkBaseProgram();
ASSERT_NE(program, 0u) << "the vertex+fragment program did not link";
// Not drawn yet: the ES backend's rebuild is gated on the link version, so a draw
// here would build the driver program from the 2-module executable and the attaches
// below would never be re-read. The repro is the FIRST build seeing the long list.
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexHelperSource));
glAttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFragmentHelperSource));
if (BackendHostsGeometry()) {
glAttachShader(program, MakeShader(GL_GEOMETRY_SHADER, kGeometrySource));
}
// The stage that made DirectGLES synthesize a pass-through control stage for a
// program whose executable does not tessellate. Attached whether or not this
// backend can tessellate - an attach needs no support and no successful compile.
const GLuint tessEval = MakeShader(GL_TESS_EVALUATION_SHADER, R"(#version 420 core
layout(triangles, equal_spacing, ccw) in;
void main()
{
gl_Position = gl_in[0].gl_Position;
}
)");
if (tessEval != 0) glAttachShader(program, tessEval);
DrainErrors();
GLint attachedCount = 0;
glGetProgramiv(program, GL_ATTACHED_SHADERS, &attachedCount);
DrainErrors();
ASSERT_GT(attachedCount, 2) << "the attaches did not land, so this case is not testing anything";
// Still the two-stage executable of three lines ago, and GL says it draws.
GLenum drawError = GL_NO_ERROR;
const Image frame = DrawFullViewportTriangle(program, GL_TRIANGLES, &drawError);
EXPECT_EQ(drawError, static_cast<GLenum>(GL_NO_ERROR))
<< "the attaches have not been linked in, so nothing about them may reject this draw";
ExpectFullyColored(frame, kGreen, "the post-attach draw");
DrainErrors();
}
// The same window, asked to prove something stronger than "it did not crash": WHICH
// modules the draw in that window ran. Between the detach+attach and the relink the
// program has three attached shaders and two modules, and GL 4.6 core 7.3 says the
// executable is still the one the last link produced - so the frame must come out in
// the OLD fragment shader's colour, not the newly attached one's and not garbage.
//
// This is also the other direction of the fix, so it cannot be "freeze the backend on
// the first link": the relink really does swap the executable, and the very next draw
// has to be rebuilt from it.
TEST_F(PostLinkAttachScenario, TheWindowKeepsTheOldExecutableAndTheRelinkSwapsIt) {
if (!Ready()) GTEST_SKIP();
GLuint greenFragment = 0;
const GLuint program = LinkBaseProgram(&greenFragment);
ASSERT_NE(program, 0u) << "the vertex+fragment program did not link";
// Both of these are deferred to the next link, in opposite directions: the green
// stage stays in the executable until then, and the blue one stays out of it.
const GLuint blueFragment = MakeShader(GL_FRAGMENT_SHADER, kBlueFragmentSource);
glDetachShader(program, greenFragment);
glAttachShader(program, blueFragment);
DrainErrors();
GLenum windowDrawError = GL_NO_ERROR;
const Image inTheWindow = DrawFullViewportTriangle(program, GL_TRIANGLES, &windowDrawError);
EXPECT_EQ(windowDrawError, static_cast<GLenum>(GL_NO_ERROR))
<< "neither the detach nor the attach has been linked in, so the draw must execute";
ExpectFullyColored(inTheWindow, kGreen, "the draw inside the attach window");
DrainErrors();
glLinkProgram(program);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
ASSERT_EQ(linked, GL_TRUE) << "the relink onto the blue fragment stage failed";
DrainErrors();
GLenum relinkedDrawError = GL_NO_ERROR;
const Image afterRelink = DrawFullViewportTriangle(program, GL_TRIANGLES, &relinkedDrawError);
EXPECT_EQ(relinkedDrawError, static_cast<GLenum>(GL_NO_ERROR)) << "the relinked program must draw";
ExpectFullyColored(afterRelink, kBlue, "the draw after the relink");
DrainErrors();
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,366 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/RelinkStageSetScenario.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 RELINK MAY CHANGE WHICH STAGES A PROGRAM HAS, AND EVERY DRAW AFTER IT RUNS
// THE NEW STAGE SET.
//
// GL 4.6 core 7.3: glLinkProgram builds an executable out of whatever is attached at that
// moment, so the stage set is a property of a LINK and not of a program. A program that
// linked vertex+fragment, drew, then had a geometry shader attached and was relinked runs
// three stages from that point on.
//
// DirectGLES rebuilds its driver program in place - same GL name, new executable - and the
// per-draw bind dedupes on that name, so a relink that changed the stage set installed
// nothing and the following draws rendered NOTHING at all: no GL error, LINK_STATUS true,
// and a framebuffer that kept its clear colour. See the note at the glLinkProgram in
// BackendProgramObjectImpl::SyncToBackend for what the driver does with such a relink.
//
// PostLinkAttachScenario pins the other half of the same rule - that the executable does
// NOT move until the relink. This one pins what happens when it does, in all three
// directions: a stage added, a stage removed, and a stage added that the ES backend has to
// synthesize a partner for.
//
// Every case asserts on a SHAPE and not merely on "something came out". The geometry and
// tessellation stages here halve the triangle, so a full-viewport green frame and a
// half-size one say which executable ran - "still drew" and "drew the right stages" are
// different claims and only the second one is worth pinning.
//
// Needs a real context: what is asserted is a rendered pixel out of a backend program build.
#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 kFboWidth = 64;
constexpr int kFboHeight = 64;
// A full-viewport triangle out of gl_VertexID alone, so no case here needs a vertex
// buffer and one draw covers every pixel of the target.
const char* const kVertexSource = R"(#version 420 core
void main()
{
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
}
)";
const char* const kFragmentSource = R"(#version 420 core
out vec4 fragColor;
void main()
{
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
)";
// Halves the triangle instead of passing it through: the centre pixel stays covered
// and all four corners fall outside, so the frame alone says whether this stage ran.
const char* const kGeometrySource = R"(#version 420 core
layout(triangles) in;
layout(triangle_strip, max_vertices = 3) out;
void main()
{
for (int i = 0; i < 3; ++i) {
gl_Position = vec4(gl_in[i].gl_Position.xy * 0.5, gl_in[i].gl_Position.zw);
EmitVertex();
}
EndPrimitive();
}
)";
// No control stage on purpose: OpenGL ES rejects that shape outright, so DirectGLES
// synthesizes a pass-through one (AttachPassthroughTessControlStage) and DirectVulkan
// does the same. Reading only gl_in[].gl_Position keeps this inside what such a
// pass-through may forward. At the tessellation levels it sets (all 1.0) the patch
// comes back out as one triangle whose gl_TessCoord values are the three corners, so
// the barycentric sum reproduces the vertex stage's triangle - halved, for the same
// reason the geometry stage above halves it.
const char* const kTessEvalSource = R"(#version 420 core
layout(triangles, equal_spacing, ccw) in;
void main()
{
vec4 p = gl_TessCoord.x * gl_in[0].gl_Position +
gl_TessCoord.y * gl_in[1].gl_Position +
gl_TessCoord.z * gl_in[2].gl_Position;
gl_Position = vec4(p.xy * 0.5, p.zw);
}
)";
constexpr Rgba8 kGreen{0, 255, 0, 255};
constexpr Rgba8 kRed{255, 0, 0, 255};
class RelinkStageSetScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
m_target = MakeColorFbo(kFboWidth, kFboHeight);
ASSERT_NE(m_target.fbo, 0u) << "could not create the scenario's colour target";
BindFbo(m_target);
DrainErrors();
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (const GLuint program : m_programs) glDeleteProgram(program);
m_programs.clear();
for (const GLuint shader : m_shaders) glDeleteShader(shader);
m_shaders.clear();
BindDefaultFramebuffer();
DestroyColorFbo(m_target);
glBindVertexArray(0);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
m_vao = 0;
DrainErrors();
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
// The same real-backend probes the other stage-gated scenarios use: 0 on a
// DirectGLES driver without the extension and on a DirectVulkan device without
// the feature.
static bool BackendHostsGeometry() {
GLint maxGeometryOutputVertices = 0;
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
DrainErrors();
return maxGeometryOutputVertices >= 4;
}
static bool BackendHostsTessellation() {
GLint maxTessGenLevel = 0;
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
DrainErrors();
return maxTessGenLevel >= 1;
}
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);
}
if (length <= 0) return {};
std::string log(static_cast<size_t>(length), '\0');
if (isShader) {
glGetShaderInfoLog(object, length, nullptr, log.data());
} else {
glGetProgramInfoLog(object, length, nullptr, log.data());
}
log.resize(std::char_traits<char>::length(log.c_str()));
return log;
}
GLuint MakeShader(GLenum stage, const char* source) {
const GLuint shader = glCreateShader(stage);
if (shader == 0) return 0;
m_shaders.push_back(shader);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
EXPECT_EQ(compiled, GL_TRUE) << "a scenario shader did not compile: " << InfoLog(shader, true);
return shader;
}
GLuint MakeProgram() {
const GLuint program = glCreateProgram();
m_programs.push_back(program);
return program;
}
bool Link(GLuint program) {
glLinkProgram(program);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked != GL_TRUE) {
ADD_FAILURE() << "the link failed: " << InfoLog(program, false);
return false;
}
return true;
}
// Clears to red and draws. Red is the clear colour deliberately: nothing here ever
// paints red inside the triangle, so a frame that is red where it should be green
// says "this draw did not execute" while a frame that is green where it should be
// red says "it executed against the wrong executable" - two failures worth telling
// apart. The error is sampled between the draw and the readback so a rejected draw
// is never confused with a readback that went wrong afterwards.
Image DrawTriangle(GLuint program, GLenum mode, GLenum* outDrawError = nullptr) {
glUseProgram(program);
ClearTo(1.0f, 0.0f, 0.0f, 1.0f);
DrainErrors();
glDrawArrays(mode, 0, 3);
if (outDrawError != nullptr) *outDrawError = glGetError();
return ReadPixels(kFboWidth, kFboHeight);
}
// The vertex stage's triangle covers the whole target, corners included.
static void ExpectFullTriangle(const Image& frame, const char* what) {
ASSERT_FALSE(frame.Empty()) << what << ": nothing was read back";
ExpectPixel(frame, kFboWidth / 2, kFboHeight / 2, kGreen, what, "centre");
for (const int y : {0, kFboHeight - 1}) {
for (const int x : {0, kFboWidth - 1}) {
ExpectPixel(frame, x, y, kGreen, what, "corner");
}
}
}
// ...and halved by a geometry or tessellation stage it no longer reaches any of
// them, which is what makes the shape readable as "that stage ran".
static void ExpectHalvedTriangle(const Image& frame, const char* what) {
ASSERT_FALSE(frame.Empty()) << what << ": nothing was read back";
ExpectPixel(frame, kFboWidth / 2, kFboHeight / 2, kGreen, what, "centre");
for (const int y : {0, kFboHeight - 1}) {
for (const int x : {0, kFboWidth - 1}) {
ExpectPixel(frame, x, y, kRed, what, "corner");
}
}
}
static void ExpectPixel(const Image& frame, int x, int y, const Rgba8& expected, const char* what,
const char* where) {
EXPECT_EQ(frame.At(x, y), expected)
<< what << ": " << where << " pixel (" << x << ", " << y << ") is " << frame.ColorName(x, y);
}
GLuint m_vao = 0;
ColorFbo m_target{};
std::vector<GLuint> m_programs;
std::vector<GLuint> m_shaders;
};
// THE REGRESSION. Vertex+fragment, linked and DRAWN - which is what puts a built driver
// program on the backend twin - then a geometry shader attached and the program
// relinked. The halved frame is the assertion: the three-stage executable really is
// what the next draw ran.
TEST_F(RelinkStageSetScenario, RelinkingToAddAGeometryStageRunsTheNewExecutable) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsGeometry()) {
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString()
<< "); there is no stage to add";
}
const GLuint program = MakeProgram();
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexSource));
glAttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFragmentSource));
ASSERT_TRUE(Link(program));
DrainErrors();
GLenum beforeError = GL_NO_ERROR;
const Image before = DrawTriangle(program, GL_TRIANGLES, &beforeError);
EXPECT_EQ(beforeError, static_cast<GLenum>(GL_NO_ERROR)) << "the vertex+fragment draw must execute";
ExpectFullTriangle(before, "the draw before the relink");
DrainErrors();
glAttachShader(program, MakeShader(GL_GEOMETRY_SHADER, kGeometrySource));
ASSERT_TRUE(Link(program));
DrainErrors();
GLenum afterError = GL_NO_ERROR;
const Image after = DrawTriangle(program, GL_TRIANGLES, &afterError);
EXPECT_EQ(afterError, static_cast<GLenum>(GL_NO_ERROR)) << "the relinked three-stage program must draw";
ExpectHalvedTriangle(after, "the draw after the geometry stage was linked in");
DrainErrors();
}
// The same move in the other direction, which no repair may confuse with "the stage
// set did not change": the geometry stage leaves the executable, so the halving has to
// stop with it.
TEST_F(RelinkStageSetScenario, RelinkingToRemoveAGeometryStageRunsTheNewExecutable) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsGeometry()) {
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString()
<< "); there is no stage to remove";
}
const GLuint program = MakeProgram();
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexSource));
const GLuint geometry = MakeShader(GL_GEOMETRY_SHADER, kGeometrySource);
glAttachShader(program, geometry);
glAttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFragmentSource));
ASSERT_TRUE(Link(program));
DrainErrors();
// Also the control for the case above: a three-stage program linked in ONE go and
// never relinked draws its halved triangle.
GLenum beforeError = GL_NO_ERROR;
const Image before = DrawTriangle(program, GL_TRIANGLES, &beforeError);
EXPECT_EQ(beforeError, static_cast<GLenum>(GL_NO_ERROR)) << "the three-stage draw must execute";
ExpectHalvedTriangle(before, "the draw before the geometry stage was dropped");
DrainErrors();
glDetachShader(program, geometry);
ASSERT_TRUE(Link(program));
DrainErrors();
GLenum afterError = GL_NO_ERROR;
const Image after = DrawTriangle(program, GL_TRIANGLES, &afterError);
EXPECT_EQ(afterError, static_cast<GLenum>(GL_NO_ERROR)) << "the relinked vertex+fragment program must draw";
ExpectFullTriangle(after, "the draw after the geometry stage was dropped");
DrainErrors();
}
// The third direction, and the one that asks the most of the rebuild: the added stage
// is a tessellation evaluation shader with no control stage, so the ES backend has to
// synthesize a pass-through control stage for an executable that had neither a moment
// ago. GL_PATCHES becomes the only legal mode with it, which is also the only draw-mode
// change any case here makes.
TEST_F(RelinkStageSetScenario, RelinkingToAddATessEvalStageRunsTheNewExecutable) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsTessellation()) {
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
<< "); there is no stage to add";
}
const GLuint program = MakeProgram();
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexSource));
glAttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFragmentSource));
ASSERT_TRUE(Link(program));
DrainErrors();
GLenum beforeError = GL_NO_ERROR;
const Image before = DrawTriangle(program, GL_TRIANGLES, &beforeError);
EXPECT_EQ(beforeError, static_cast<GLenum>(GL_NO_ERROR)) << "the vertex+fragment draw must execute";
ExpectFullTriangle(before, "the draw before the relink");
DrainErrors();
glAttachShader(program, MakeShader(GL_TESS_EVALUATION_SHADER, kTessEvalSource));
ASSERT_TRUE(Link(program));
// Three, which is already the default; spelled out because the synthesized control
// stage's output patch size is compiled from it.
glPatchParameteri(GL_PATCH_VERTICES, 3);
DrainErrors();
GLenum afterError = GL_NO_ERROR;
const Image after = DrawTriangle(program, GL_PATCHES, &afterError);
EXPECT_EQ(afterError, static_cast<GLenum>(GL_NO_ERROR)) << "the relinked tessellating program must draw";
ExpectHalvedTriangle(after, "the draw after the tessellation stage was linked in");
DrainErrors();
}
} // 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
@@ -64,6 +64,25 @@ void main() {
g_length[2] = g_input23[0].data.length(); g_length[2] = g_input23[0].data.length();
g_length[3] = g_input23[1].data.length(); g_length[3] = g_input23[1].data.length();
} }
)";
// GL 4.6 core 4.10 lets a buffer variable be declared readonly AND writeonly at once:
// it can then be neither read nor written, and `.length()` is the only thing left that
// may be asked of it. The pair is inert - and printing it into ESSL is not, because
// SPIRV-Cross hoists the qualifiers every member shares onto the BLOCK and Mesa's ES
// compiler refuses that spelling ("Interface block sets both readonly and writeonly").
// Lifted from KHR-GL43.shader_storage_buffer_object.basic-readonly-writeonly.
constexpr const char* kReadonlyWriteonlyComputeSource = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Input {
readonly writeonly int g_in[];
};
layout(std430, binding = 4) buffer Output {
int g_length[];
};
void main() {
g_length[0] = g_in.length();
}
)"; )";
constexpr int kElementBytes = 16; // ivec4, std430 constexpr int kElementBytes = 16; // ivec4, std430
@@ -212,4 +231,33 @@ void main() {
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, input0); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, input0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, input3); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, input3);
} }
// A buffer variable qualified readonly AND writeonly can only be asked its length, and that
// question still has to be answered. A stage the driver refused answers 0 - and refuses
// silently, because the program links without it and the dispatch is then a no-op.
TEST_F(SsboArrayLengthScenario, AReadonlyWriteonlyArrayStillReportsItsLength) {
if (!Ready() || IsSkipped()) return;
const GLuint program = CompileComputeProgram(kReadonlyWriteonlyComputeSource);
ASSERT_NE(program, 0u) << m_buildLog;
const GLuint input = MakeStorageBuffer(6); // 6 ivec4 = 24 ints
const GLuint output = MakeStorageBuffer(1);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, input);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, output);
ASSERT_EQ(FirstGLError(), 0u);
glUseProgram(program);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
int length = -1;
glBindBuffer(GL_SHADER_STORAGE_BUFFER, output);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(length), &length);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(length, 24) << "a readonly+writeonly runtime array reported length " << length
<< "; 0 means the stage never reached the program";
glUseProgram(m_program);
glDeleteProgram(program);
}
} // namespace MGITest } // namespace MGITest
@@ -42,10 +42,10 @@
namespace MGITest { namespace MGITest {
namespace { namespace {
// The eight vertex shaders of the conformance sweep, verbatim in shape. Each reads three // The eight vertex shaders of the conformance sweep, verbatim in shape, plus a ninth that
// vec4 positions out of a storage block on binding 0 and emits them as a triangle that // is not from the sweep (see form 8). Each reads three vec4 positions out of a storage
// covers the whole viewport. // block on binding 0 and emits them as a triangle that covers the whole viewport.
constexpr const char* kFormVS[8] = { constexpr const char* kFormVS[9] = {
// 0 - instance name, no binding qualifier, sized array member // 0 - instance name, no binding qualifier, sized array member
R"(#version 430 core R"(#version 430 core
layout(std430) buffer Buffer { layout(std430) buffer Buffer {
@@ -127,6 +127,38 @@ void main() {
case 2: gl_Position = g_buffer.position2[gl_VertexID - 2]; break; case 2: gl_Position = g_buffer.position2[gl_VertexID - 2]; break;
} }
} }
)",
// 8 - NOT from the conformance sweep. An unqualified storage block with a UNIFORM
// BLOCK beside it, which is what makes the block's DEFAULT binding observable at all.
//
// GL 4.3 core 7.8 gives a storage block with no layout(binding = N) a buffer binding
// of zero. Forms 0, 1, 3, 4 and 5 above are all unqualified and all pass, but they
// cannot prove that rule holds: they are the only resource in their shader, so the
// binding glslang's IO mapper invents for them happens to BE zero and the right answer
// arrives for the wrong reason.
//
// Every shader here is parsed as a Vulkan client, so that mapper allocates out of ONE
// flat space shared by samplers, images, uniform blocks, storage blocks and the
// synthesized global-uniform block (iomapper.cpp resolveBinding takes the `ent.newSet`
// branch, and every resource resolves to set 0), and then writes the result back into
// the type's qualifier - so the reflection cannot tell an invented binding from a
// declared one. Put anything live next to the block and it is pushed off zero, the
// draw reads a binding point nothing was ever bound to, and the triangle collapses
// with no GL error anywhere. That is
// KHR-GL43.compute_shader.resource-ubo's whole failure, in a vertex stage.
//
// The uniform block is REBOUND explicitly with glUniformBlockBinding, exactly as that
// conformance case does. That keeps this case about the storage block's default and
// not about the uniform block's - the rebinding path has always worked, and the
// uniform-block default is a separate (still open) question.
R"(#version 430 core
layout(std140) uniform ScaleBlock {
vec4 factor;
} g_scale;
layout(std430) buffer Buffer {
vec4 position[3];
} g_input_buffer;
void main() { gl_Position = g_input_buffer.position[gl_VertexID] * g_scale.factor; }
)", )",
}; };
@@ -197,6 +229,26 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
const unsigned int program = CompileProgram(kFormVS[form], kFormFS, &error); const unsigned int program = CompileProgram(kFormVS[form], kFormFS, &error);
ASSERT_NE(program, 0u) << "form " << form << " did not build: " << error; ASSERT_NE(program, 0u) << "form " << form << " did not build: " << error;
// Form 8 alone declares a uniform block, and it exists only to occupy a slot the
// storage block must not be pushed onto. Bound to a buffer of ones so it scales
// the positions by exactly 1 - the block's contribution to the IMAGE is nothing,
// and its contribution to the TEST is that it is there at all.
GLuint uniformBuffer = 0;
if (form == 8) {
const float ones[4] = {1.0f, 1.0f, 1.0f, 1.0f};
glGenBuffers(1, &uniformBuffer);
glBindBuffer(GL_UNIFORM_BUFFER, uniformBuffer);
glBufferData(GL_UNIFORM_BUFFER, sizeof(ones), ones, GL_STATIC_DRAW);
glBindBufferBase(GL_UNIFORM_BUFFER, 0, uniformBuffer);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
const GLuint blockIndex = glGetUniformBlockIndex(program, "ScaleBlock");
ASSERT_NE(blockIndex, GL_INVALID_INDEX) << "form 8: the uniform block is not active";
// Explicit, so this case cannot fail on the uniform block's own default
// binding - which is a separate question from the storage block's.
glUniformBlockBinding(program, blockIndex, 0);
ASSERT_EQ(FirstGLError(), 0u) << "form 8: uniform block setup errored";
}
GLuint vao = 0; GLuint vao = 0;
glGenVertexArrays(1, &vao); glGenVertexArrays(1, &vao);
glBindVertexArray(vao); glBindVertexArray(vao);
@@ -221,6 +273,7 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
glDeleteVertexArrays(1, &vao); glDeleteVertexArrays(1, &vao);
glDeleteProgram(program); glDeleteProgram(program);
glDeleteBuffers(1, &buffer); glDeleteBuffers(1, &buffer);
if (uniformBuffer != 0) glDeleteBuffers(1, &uniformBuffer);
gl.EndFrame(); gl.EndFrame();
} }
}; };
@@ -241,6 +294,10 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
MGL_SSBO_FORM_CASE(3, GlobalLayoutDefaultsThenAnInstanceNamedBlock) MGL_SSBO_FORM_CASE(3, GlobalLayoutDefaultsThenAnInstanceNamedBlock)
MGL_SSBO_FORM_CASE(4, BlockInstanceArrayOfOne) MGL_SSBO_FORM_CASE(4, BlockInstanceArrayOfOne)
MGL_SSBO_FORM_CASE(5, BlockInstanceArrayOfOneWithSharedLayout) MGL_SSBO_FORM_CASE(5, BlockInstanceArrayOfOneWithSharedLayout)
// The form that makes the DEFAULT binding observable rather than accidental: forms 0/1/3/4/5
// are unqualified too, but nothing competes with them for glslang's flat slot 0, so they
// would keep passing even with the default wrong. See the comment on kFormVS[8].
MGL_SSBO_FORM_CASE(8, NoBindingQualifierBesideAUniformBlock)
// ---- the two forms that do not work yet ---- // ---- the two forms that do not work yet ----
// //
// Both carry an UNSIZED array that is not the block's sole trailing member, and both fail // Both carry an UNSIZED array that is not the block's sole trailing member, and both fail
@@ -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
@@ -30,15 +30,23 @@
// applies the flip to viewport 0 and forgets the other fifteen renders a correct-looking FBO and // applies the flip to viewport 0 and forgets the other fifteen renders a correct-looking FBO and
// an upside-down window - the classic multi-viewport bug, and invisible to every FBO-only case. // an upside-down window - the classic multi-viewport bug, and invisible to every FBO-only case.
// //
// HONEST LIMIT OF THIS FILE. DirectGLES SKIPS every case: GLES has one viewport, one scissor // BOTH BACKENDS RUN EVERY CASE, by two completely different routes, which is the point of
// rectangle and no gl_ViewportIndex, so routing to index > 0 is an emulation feature that has // keeping them in one file. DirectVulkan declares sixteen viewports on the pipeline and lets the
// not been built (the Espryt half of KHR-GL43.viewport_array's rendering group is deliberately // hardware route. DirectGLES has one viewport, one scissor rectangle and one depth range and no
// still red). The skip is explicit rather than silent so a future emulation lands here as a // gl_ViewportIndex at all, so it EMULATES: the builtin becomes a flat varying, the fragment stage
// failing test and not as a test that was quietly never running. DirectVulkan additionally // gets a gate, and the draw is replayed once per distinct viewport state (Managers.h,
// skips when the device lacks the multiViewport feature - Vulkan then forbids a pipeline from // ForEachViewportRoutingPass). Every assertion below is about pixels, so it cannot tell the two
// declaring more than one viewport at all, which is a device limit and not a MobileGL bug; // apart - which is exactly what has to be true.
// lavapipe (every CI lane) and both Mali/Adreno devices support it, so the cases do run where //
// it matters. // DirectVulkan skips when the device lacks the multiViewport feature - Vulkan then forbids a
// pipeline from declaring more than one viewport at all, which is a device limit and not a
// MobileGL bug; lavapipe (every CI lane) and both Mali/Adreno devices support it, so the cases do
// run where it matters.
//
// The last case is the negative control for the emulation and runs on DirectGLES only: it builds
// the SAME program with the emulation switched off and requires the routing to collapse onto
// viewport 0. Without it every assertion above could be satisfied by a backend that happened to
// be right for some other reason, and the emulation's own switch would be untested.
#include <cmath> #include <cmath>
#include <string> #include <string>
@@ -47,6 +55,10 @@
#include "../Harness/HeadlessGL.h" #include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h" #include "../Harness/ScenarioFixture.h"
// For the emulation switch the negative-control case below flips. Nothing else in this file needs
// to know which backend it is running on.
#include <Config.h>
#ifdef GLAPI #ifdef GLAPI
#undef GLAPI #undef GLAPI
#endif #endif
@@ -142,13 +154,6 @@ void main() { fragColor = gl_FragCoord.z; }
ScenarioTest::SetUp(); ScenarioTest::SetUp();
if (!Ready()) return; if (!Ready()) return;
if (Gl().BackendName() == "DirectGLES") {
GTEST_SKIP() << "gl_ViewportIndex routing is not emulated on DirectGLES: GLES has one viewport "
"and one scissor rectangle, so every index rasterizes as index 0. The indexed "
"STATE is still asserted (MG_Test RenderStateTest); this is the deferred "
"rendering half of KHR-GL43.viewport_array.";
}
GLint maxViewports = 0; GLint maxViewports = 0;
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports); glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
ASSERT_GE(maxViewports, kViewportCount) << "GL 4.3 core requires GL_MAX_VIEWPORTS >= 16"; ASSERT_GE(maxViewports, kViewportCount) << "GL 4.3 core requires GL_MAX_VIEWPORTS >= 16";
@@ -520,5 +525,282 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
DestroyIntTarget(target); DestroyIntTarget(target);
} }
// --- 4. the negative control for the DirectGLES emulation -----------------------------
//
// Everything above is a claim about pixels, and a claim about pixels cannot tell an
// emulation that works from a backend that was going to be right anyway. This case builds
// the SAME program with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION off and requires case 1's
// result to COLLAPSE: with no routing, every geometry invocation rasterizes against
// viewport 0's rectangle, so the last invocation paints the whole surface and every cell
// reads 15 instead of its own index. That is the pre-emulation behaviour this backend had
// (and the failure signature KHR-GL43.viewport_array reported on it), pinned here so that
// (a) the three cases above are known to be testing the emulation and not the weather,
// and (b) the switch itself has a test.
//
// DirectGLES only: the flag steers nothing on DirectVulkan, which routes natively.
TEST_F(ViewportArrayScenario, WithoutTheEmulationEveryIndexCollapsesOntoViewportZero) {
if (Gl().BackendName() != "DirectGLES") {
GTEST_SKIP() << "the emulation switch is a DirectGLES concern; DirectVulkan routes "
"gl_ViewportIndex natively and ignores it";
}
// The feature table is a process-global and this fixture shares its context with every
// other scenario in the process, so the restore is not optional.
struct ScopedEmulationOff {
ScopedEmulationOff(): saved(MobileGL::MG_Config::Features.ViewportArrayEmulation) {
MobileGL::MG_Config::Features.ViewportArrayEmulation =
MobileGL::MG_Config::QuirkOverride::ForceOff;
}
~ScopedEmulationOff() { MobileGL::MG_Config::Features.ViewportArrayEmulation = saved; }
MobileGL::MG_Config::QuirkOverride saved;
};
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
SetupGridViewports(kCellSize, kCellSize);
GLuint unroutedProgram = 0;
{
const ScopedEmulationOff scopedEmulationOff;
// A FRESH program: the emitted ESSL is decided at link time and memoized on a key
// that carries this flag, so reusing m_program would just replay the routed build.
unroutedProgram = BuildProgram(kGridGeometrySource, kIntFragmentSource);
ASSERT_NE(unroutedProgram, 0u) << "unrouted program failed to build: " << m_buildLog;
glUseProgram(unroutedProgram);
glBindVertexArray(m_vao);
glDrawArrays(GL_POINTS, 0, 1);
ASSERT_EQ(glGetError(), GL_NO_ERROR);
}
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
// Cell (0, 0) IS viewport 0's rectangle, so it is the one cell an unrouted draw paints
// with something. Everything it holds comes from the last geometry invocation.
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, 0, 0), kViewportCount - 1)
<< "with the emulation off, viewport 0's rectangle must hold the LAST invocation's "
"index - if it holds 0 the routing is still happening and this control proves "
"nothing";
for (int y = 0; y < kGridSide; ++y) {
for (int x = 0; x < kGridSide; ++x) {
if (x == 0 && y == 0) continue;
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kUnwritten)
<< "cell (" << x << ", " << y << ") is outside viewport 0's rectangle and an "
<< "unrouted draw cannot reach it";
}
}
glUseProgram(0);
glDeleteProgram(unroutedProgram);
DestroyIntTarget(target);
}
// --- 5. an explicitly EMPTY scissor box clips, it does not mean "never written" --------
//
// Deliberately NOT a ViewportArrayScenario case, because that fixture's geometry stage
// routes and this claim needs none of it: one viewport, one scissor rectangle, no
// geometry stage - and it has to hold identically whether or not anything routes.
//
// 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,208 @@ namespace {
return element; return element;
} }
// Blocks come out of reflection in three kinds and only one of them is a GL uniform block.
// The same split ProgramInterface::ClassifyBlock makes (it reads the flattened
// TypeFacts::isBuffer, which is this very qualifier), reachable here from the live TProgram
// because the block index spaces are built before the reflection snapshot exists.
// The transpiler lowers every atomic_uint onto a synthesized "gl_AtomicCounterBlock_<binding>"
// buffer block, which reflection then reports as an ordinary block. It is not one: GL
// enumerates it through GL_ACTIVE_ATOMIC_COUNTER_BUFFERS instead.
static MobileGL::Bool IsAtomicCounterBlockName(const MobileGL::String& name) {
namespace Transpiler = MobileGL::MG_Util::ShaderTranspiler;
const MobileGL::SizeT prefixLength = std::strlen(Transpiler::ATOMIC_COUNTER_BLOCK_PREFIX);
return name.compare(0, prefixLength, Transpiler::ATOMIC_COUNTER_BLOCK_PREFIX) == 0;
}
// A shader storage block: GL enumerates it through GL_SHADER_STORAGE_BLOCK and its members
// through GL_BUFFER_VARIABLE. The counter blocks above are buffer blocks too, hence the
// exclusion. A block whose type reflection did not survive is treated as a uniform block,
// which is what every caller assumed before this classification existed.
static MobileGL::Bool IsStorageBlock(const glslang::TObjectReflection& block) {
if (IsAtomicCounterBlockName(block.name)) return false;
const glslang::TType* type = block.getType();
return type != nullptr && type->getQualifier().storage == glslang::EvqBuffer;
}
static MobileGL::Bool IsGlUniformBlock(const glslang::TObjectReflection& block) {
return !IsAtomicCounterBlockName(block.name) && !IsStorageBlock(block);
}
// 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 +577,21 @@ 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;
// Reads the COMPILE snapshots only - no parsed shader - so it runs before the L1
// probe: a conflicting explicit uniform location must fail the link whether or not
// the memo has an answer for this program's sources.
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,35 +618,16 @@ namespace MobileGL::MG_State::GLState {
} }
} }
// Merge the shaders' lexically extracted explicit uniform locations. The same // The last two are OUT parameters that mapIO fills, not requests it honours: the IO
// uniform declared in several stages must agree on its location (config-A glslang // mapper's collect callback is the last point at which a resource's qualifier still
// enforced this at mapIO; the relaxed parse no longer sees the qualifiers). // says what the SHADER declared rather than what glslang assigned, so both captures
for (const auto& shader : in.shaders) { // have to be taken from inside the link. See TMglGlslIoResolver::reserverResourceSlot.
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,
.explicitFragmentOutIndices = in.explicitFragDataIndex, .explicitFragmentOutIndices = in.explicitFragDataIndex,
.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings}; .explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings,
.storageBlocksWithoutBinding = &artifacts.storageBlocksWithoutBinding};
MGLOG_D("ProgramObject %u: Calling ShaderCompiler::LinkProgram", in.externalIndex); MGLOG_D("ProgramObject %u: Calling ShaderCompiler::LinkProgram", in.externalIndex);
auto result = ShaderCompiler::LinkProgram(attrib); auto result = ShaderCompiler::LinkProgram(attrib);
@@ -381,6 +645,7 @@ namespace MobileGL::MG_State::GLState {
return; return;
} }
// A compute program must have a fixed local group size, and GL states that as a // A compute program must have a fixed local group size, and GL states that as a
// property of the PROGRAM: "at least one" of its compute shaders declares it (GL 4.6 // property of the PROGRAM: "at least one" of its compute shaders declares it (GL 4.6
// core 7.13 / GLSL 4.30 4.4.1.4). MobileGL used to answer that question per SHADER, // core 7.13 / GLSL 4.30 4.4.1.4). MobileGL used to answer that question per SHADER,
@@ -497,14 +762,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: anything that only steers a BACKEND transpile - see the
// classification on CompileEnv::frontendFingerprint, and L2's own key in
// MG_Util/ShaderTranspiler/TranslationCache.h. The single capability bit that IS here
// (nativeFloat64) earns its place by changing SanitizeAndOptimizeBinary's own output,
// which is what the payload stores.
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;
// The one BACKEND capability bit in this key, and it has to be here: it reaches inside
// SanitizeAndOptimizeBinary, whose output is what the payload holds. Read from the same
// env snapshot ProgramSpirvTask hands the chain, so the key and the bytes can never
// disagree.
keyInputs.nativeFloat64 = env.ConsumesFloat64Natively();
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;
// 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 one link rejection that needs nothing but the compile snapshots. It runs before the
// L1 memo is consulted, so a hit can never paper over a program that must fail to link.
//
// Only the explicit default-block uniform locations are merged here, and only because they
// are the one piece of relaxed-parse wreckage that has to be recovered at COMPILE time:
// the snapshot is taken inside the parse, so it is per-shader by construction, and the
// same uniform declared in several stages must agree or the program cannot link. The
// opaque bindings and the unqualified storage blocks used to be merged alongside them;
// both now arrive from mapIO during LinkProgram below, straight into `artifacts`, which is
// both later and strictly better informed - the IO mapper sees macro-expanded declarations
// and a per-shader lexer never could.
void ProgramLinkTask::MergeShaderSideChannels() {
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;
}
}
}
}
// 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 +915,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 +933,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 +1004,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
@@ -623,6 +1037,30 @@ namespace MobileGL::MG_State::GLState {
artifacts.glBlockIndexToTProgram.push_back(i); artifacts.glBlockIndexToTProgram.push_back(i);
} }
// The GL_UNIFORM_BLOCK subsequence of that space. MobileGL does not pass
// EShReflectionSeparateBuffers to buildReflection above, so glslang files BUFFER blocks
// under indexToUniformBlock as well and the list just built also holds every shader
// storage block and every synthesized gl_AtomicCounterBlock_N. GL 4.6 core 7.6 says
// GL_ACTIVE_UNIFORM_BLOCKS / glGetActiveUniformBlockiv / glGetUniformBlockIndex see
// uniform blocks and nothing else; an atomic counter buffer is enumerated by
// GL_ACTIVE_ATOMIC_COUNTER_BUFFERS and a storage block by GL_SHADER_STORAGE_BLOCK.
//
// A SECOND space rather than a filter of the first, deliberately: the block space is
// what the backends walk (DirectGLES hands out one ESSL uniform-buffer binding point per
// entry as it goes) and what "tProgramBlockIndexToGl[i] < 0 means MGL_GLOBAL_UBO" reads,
// and neither may move.
artifacts.blockIndexToGlUniformBlock.assign(artifacts.glBlockIndexToTProgram.size(), -1);
artifacts.glUniformBlockIndexToBlock.clear();
for (SizeT blockIndex = 0; blockIndex < artifacts.glBlockIndexToTProgram.size(); ++blockIndex) {
const auto& block = artifacts.program->getUniformBlock(artifacts.glBlockIndexToTProgram[blockIndex]);
if (!IsGlUniformBlock(block)) continue;
artifacts.blockIndexToGlUniformBlock[blockIndex] =
static_cast<Int>(artifacts.glUniformBlockIndexToBlock.size());
artifacts.glUniformBlockIndexToBlock.push_back(static_cast<Int>(blockIndex));
}
MGLOG_D("ProgramObject %u: Reflection - %zu block(s), %zu of them GL uniform blocks", in.externalIndex,
artifacts.glBlockIndexToTProgram.size(), artifacts.glUniformBlockIndexToBlock.size());
// ------------ Uniforms (GL Plain) ---------------- // ------------ Uniforms (GL Plain) ----------------
// The relaxed parse sweeps every DECLARED default-block uniform into // The relaxed parse sweeps every DECLARED default-block uniform into
// MGL_GLOBAL_UBO whether or not any stage reads it. GL requires a // MGL_GLOBAL_UBO whether or not any stage reads it. GL requires a
@@ -637,6 +1075,26 @@ namespace MobileGL::MG_State::GLState {
return uniform.index >= 0 && uniform.index < static_cast<Int>(artifacts.tProgramBlockIndexToGl.size()) && return uniform.index >= 0 && uniform.index < static_cast<Int>(artifacts.tProgramBlockIndexToGl.size()) &&
artifacts.tProgramBlockIndexToGl[uniform.index] < 0; artifacts.tProgramBlockIndexToGl[uniform.index] < 0;
}; };
// Member of a block GL can see - a named uniform block, a buffer block, or the
// synthesized atomic-counter block. GL locations are a property of the DEFAULT uniform
// block alone (GL 4.6 core 7.6.1), so these take none.
const auto isNamedBlockMember = [&isGlobalUboMember](const glslang::TObjectReflection& uniform) {
return uniform.index >= 0 && !isGlobalUboMember(uniform);
};
// A member of a BUFFER block is a buffer variable, not a uniform: GL 4.6 core 7.3.1
// gives it the GL_BUFFER_VARIABLE interface and 7.6 keeps it out of GL_ACTIVE_UNIFORMS,
// glGetActiveUniform, glGetUniformIndices and glGetActiveUniformsiv. The relaxed parse
// reflects it as a uniform anyway (no EShReflectionSeparateBuffers), so drop it from the
// GL index space here - the same place the dead default-block uniforms are dropped, and
// the counterpart of the location half already handled by isNamedBlockMember below.
//
// Atomic counters are NOT in this set even though their synthesized owner is a buffer
// block: an atomic_uint IS a uniform (of type GL_UNSIGNED_INT_ATOMIC_COUNTER), and
// KHR-GL43.shader_atomic_counters.basic-program-query enumerates it as one.
const auto isBufferVariable = [this](const glslang::TObjectReflection& uniform) {
if (uniform.index < 0 || uniform.index >= artifacts.program->getNumUniformBlocks()) return false;
return IsStorageBlock(artifacts.program->getUniformBlock(uniform.index));
};
for (Int i = 0; i < tProgramUniformCount; i++) { for (Int i = 0; i < tProgramUniformCount; i++) {
const auto& uniform = artifacts.program->getUniform(i); const auto& uniform = artifacts.program->getUniform(i);
if (isGlobalUboMember(uniform) && uniform.stages == 0) { if (isGlobalUboMember(uniform) && uniform.stages == 0) {
@@ -645,6 +1103,12 @@ namespace MobileGL::MG_State::GLState {
in.externalIndex, uniform.name.c_str()); in.externalIndex, uniform.name.c_str());
continue; continue;
} }
if (isBufferVariable(uniform)) {
MGLOG_D("ProgramObject %u: Reflection - buffer variable '%s' filtered from the GL uniform "
"surface",
in.externalIndex, uniform.name.c_str());
continue;
}
artifacts.tProgramUniformIndexToGl[i] = static_cast<Int>(artifacts.glUniformIndexToTProgram.size()); artifacts.tProgramUniformIndexToGl[i] = static_cast<Int>(artifacts.glUniformIndexToTProgram.size());
artifacts.glUniformIndexToTProgram.push_back(i); artifacts.glUniformIndexToTProgram.push_back(i);
} }
@@ -653,12 +1117,21 @@ namespace MobileGL::MG_State::GLState {
artifacts.activeUniformCount, tProgramUniformCount); artifacts.activeUniformCount, tProgramUniformCount);
// Effective explicit location per TProgram uniform, from two sources: // Effective explicit location per TProgram uniform, from two sources:
// - the lexical side-channel for default-block uniforms - the relaxed parse // - the parse-time snapshot for default-block uniforms - the relaxed parse
// dropped their layout(location = N) qualifiers when collecting them into // dropped their layout(location = N) qualifiers when collecting them into
// 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
@@ -674,8 +1147,7 @@ namespace MobileGL::MG_State::GLState {
for (const Int i : artifacts.glUniformIndexToTProgram) { for (const Int i : artifacts.glUniformIndexToTProgram) {
const auto& uniform = artifacts.program->getUniform(i); const auto& uniform = artifacts.program->getUniform(i);
const glslang::TType* type = uniform.getType(); const glslang::TType* type = uniform.getType();
const Bool inNamedBlock = uniform.index >= 0 && !isGlobalUboMember(uniform); if (isNamedBlockMember(uniform)) continue; // block members never take glUniform locations
if (inNamedBlock) continue; // block members never take glUniform locations
if (const Int* explicitLocation = findExplicitLocation(uniform.name)) { if (const Int* explicitLocation = findExplicitLocation(uniform.name)) {
effectiveLocation[i] = static_cast<Uint>(*explicitLocation); effectiveLocation[i] = static_cast<Uint>(*explicitLocation);
@@ -695,13 +1167,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 +1187,58 @@ 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);
}
// Counts ONLY default-block uniforms, which is the whole of what a GL uniform location
// is and the whole of what GL_MAX_UNIFORM_LOCATIONS bounds (GL 4.6 core 7.6.1). A
// named-block member used to be counted here too and used to be handed a location by the
// first-fit pass below, which is a spec violation twice over: glGetUniformLocation must
// answer -1 for it (glGetProgramResourceLocation already did), and every slot it took
// pushed a real default-block uniform one location further up. On a program with a
// buffer block that is exactly how a location EQUAL to the advertised maximum got minted
// - the table's ceiling is raised to hold this count, so one extra block member raised it
// to MAX and the first-fit pass then filled the last slot
// (KHR-GL43.explicit_uniform_location.uniform-loc-mix-with-implicit-max, whose compute
// program carries an SSBO; its -max-array sibling ran the pool out and failed to link).
Int requiredUniformLocations = 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; if (!isNamedBlockMember(uniform)) requiredUniformLocations += 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 +1251,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 (requiredUniformLocations > static_cast<Int>(kMaxUniformLocations)) {
artifacts.infoLog =
std::format("Uniform locations exhausted: the default-block uniforms need {} locations but "
"GL_MAX_UNIFORM_LOCATIONS is {}.",
requiredUniformLocations, 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 +1281,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
@@ -773,6 +1334,10 @@ namespace MobileGL::MG_State::GLState {
// is demoted to the first-fit pass below instead of failing the link. // is demoted to the first-fit pass below instead of failing the link.
for (const Int i : artifacts.glUniformIndexToTProgram) { for (const Int i : artifacts.glUniformIndexToTProgram) {
auto& uniform = artifacts.program->getUniform(i); auto& uniform = artifacts.program->getUniform(i);
// Same rule the effective-location loop applies: a block member has no GL location,
// so it must not reach the first-fit pass either. Its uniformLocations entry stays
// at kNoLocation, which glGetUniformLocation reads back as the -1 the spec wants.
if (isNamedBlockMember(uniform)) continue;
if (locationIsSourceExplicit[i]) continue; if (locationIsSourceExplicit[i]) continue;
const Uint location = effectiveLocation[i]; const Uint location = effectiveLocation[i];
if (location == kNoLocation) { if (location == kNoLocation) {
@@ -785,7 +1350,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 +1383,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 +1404,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);
@@ -959,14 +1544,21 @@ namespace MobileGL::MG_State::GLState {
} }
// ---------- UBO ---------- // ---------- UBO ----------
// GL-visible blocks only (MGL_GLOBAL_UBO was filtered out above). // The BLOCK space (MGL_GLOBAL_UBO was filtered out above, storage and atomic counter
// blocks were not): these tables are what the backends index, and what the GL
// uniform-block entry points reach after translating out of the GL_UNIFORM_BLOCK space.
const Int uboCount = static_cast<Int>(artifacts.glBlockIndexToTProgram.size()); const Int uboCount = static_cast<Int>(artifacts.glBlockIndexToTProgram.size());
MGLOG_D("ProgramObject %u: Reflection - uniform block count (UBO) = %d", in.externalIndex, uboCount); MGLOG_D("ProgramObject %u: Reflection - uniform block count (UBO) = %d", in.externalIndex, uboCount);
artifacts.uniformBlockBinding.resize(uboCount, -1); artifacts.uniformBlockBinding.resize(uboCount, -1);
for (Int i = 0; i < uboCount; i++) { for (Int i = 0; i < uboCount; i++) {
auto& ubo = artifacts.program->getUniformBlock(artifacts.glBlockIndexToTProgram[i]); auto& ubo = artifacts.program->getUniformBlock(artifacts.glBlockIndexToTProgram[i]);
artifacts.uniformBlockNameMaxLength = // GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH is measured over the names
std::max(artifacts.uniformBlockNameMaxLength, (Int)ubo.name.length()); // glGetActiveUniformBlockName can report, so only the GL uniform blocks count -
// a long storage-block name must not size the caller's buffer.
if (artifacts.blockIndexToGlUniformBlock[i] >= 0) {
artifacts.uniformBlockNameMaxLength =
std::max(artifacts.uniformBlockNameMaxLength, (Int)ubo.name.length());
}
artifacts.uniformBlockIndexByName[ubo.name] = i; artifacts.uniformBlockIndexByName[ubo.name] = i;
// if there's binding defined in shader as layout(binding = ...), // if there's binding defined in shader as layout(binding = ...),
// retrieve it here. // retrieve it here.
@@ -987,9 +1579,140 @@ 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)));
}
SeedDefaultStorageBlockBindings();
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());
}
// GL 4.3 core 7.8: a shader storage block declared without a layout(binding = N) qualifier
// has a buffer binding of ZERO. MobileGL could not report that, because by the time this
// reflection is built the number in the block's qualifier is one glslang INVENTED.
//
// Every shader is parsed as a Vulkan client, so glslang's IO mapper takes the `set = openGl
// ? resource : ent.newSet` branch with openGl == 0 (iomapper.cpp resolveBinding) - i.e. it
// allocates out of ONE flat binding space shared by every sampler, image, uniform block,
// storage block and the synthesized MGL_GLOBAL_UBO - and then writes the result back into
// the type's qualifier (iomapper.cpp, `base->getWritableType().getQualifier().layoutBinding =
// at->second.newBinding`). getBinding() therefore answers with the auto-assigned slot and
// cannot be distinguished from a declared one. An unqualified block lands on 0 only when
// nothing else in the program claimed 0 first, which is why a lone storage block in a
// trivial shader looked correct and KHR-GL43.compute_shader.resource-ubo - whose shader also
// declares twelve uniform blocks - wrote everything to a binding nothing was bound at.
//
// THE FLAT SPACE IS LEFT ALONE. It is load-bearing: DirectVulkan indexes bindingKinds[],
// uniformBlockIndexByBinding[] and storageBlockIndexByBinding[] by that one number and
// asserts when two resources collide on it, so forcing the SPIR-V decoration to 0 would
// collide an unqualified block with the global UBO and take working programs down. What is
// repaired is the GL-VISIBLE binding, through the record GL already has for exactly this -
// the same per-name map glShaderStorageBlockBinding writes, which both backends already
// consult (ProgramInterface's GL_BUFFER_BINDING, DirectGLES's SPIRV-Cross binding rewrite,
// DirectVulkan's GetShaderStorageBlockBinding). Seeding it here means the default and a
// later rebind travel the same path, and basic-noBindingLayout - which rebinds all three of
// its unqualified blocks - keeps working because a rebind simply overwrites the seed.
//
// Seeded INSIDE `artifacts`, so an L1 translation-cache hit that republishes the artifacts
// wholesale carries it too; a seed applied outside them would silently vanish on a hit.
//
// The blocks are named by TMglGlslIoResolver at mapIO's collect callback, which runs over
// every declared block of every stage BEFORE the write-back above happens - so "declared no
// binding" is a fact read off the AST, not a guess made about the text. The lexical scanner
// this replaced could only report positively, dropping any declaration whose grammar it did
// not fully recognise, and could not read `binding = SOME_MACRO` at all (it ran on
// macro-unexpanded source, and reading "no literal" as "no binding" once aliased eight
// Flywheel storage blocks onto 0).
//
// THE COLLISION IS DELIBERATE, and it is GL's. Several unqualified blocks all default to 0
// and alias there until the application rebinds them; a real GL driver does the same, which
// is why every program that has more than one either rebinds or uses one of them.
// basic-noBindingLayout is that regression test - it rebinds all three of its blocks
// immediately after linking, and the DirectGLES transpile is lazy (first use, not link), so
// the ESSL it eventually emits already carries the rebound 0/1/2 and never the aliased seed.
// What this replaces was not a safer arrangement, only an accidental one: the three blocks
// got glslang's 0/1/2 and an application that rebound them to anything else still wrote to
// the wrong buffers.
void ProgramLinkTask::SeedDefaultStorageBlockBindings() {
if (artifacts.storageBlocksWithoutBinding.empty()) return;
for (const ProgramObject::BlockReflection& block : artifacts.blockReflection) {
if (!block.type.isBuffer) continue;
// An instance array reflects as "B[0]", "B[1]", ... and each element is its own GL
// resource with its own binding; the scanner keys on the block TYPE name, so the
// subscript is stripped before the lookup. GL gives element k of an unqualified
// array binding 0 + k, the same base + element rule a declared binding follows.
const String base = StripArrayElementSuffix(block.name);
if (!artifacts.storageBlocksWithoutBinding.contains(base)) continue;
// First writer wins: never overwrite a binding the application has already chosen.
artifacts.shaderStorageBlockBinding.emplace(block.name, BlockArrayElement(block.name));
}
}
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,26 @@ 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 three link-time request maps, which live in `in` - and
// it has to be built before the link, because a hit is what makes the link
// unnecessary;
// * 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 +163,37 @@ 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.
// Merges the per-stage explicit default-block uniform locations glslang recorded at
// compile time. Reads the compile snapshots only, so it runs before any parse - and
// before the L1 memo, so a hit can never paper over a program that must fail to link.
// 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();
// Gives every storage block whose shader declared no layout(binding = N) the binding
// GL 4.3 core 7.8 says it has - zero - because glslang's IO mapper has by then invented
// one and overwritten the qualifier. See the definition for why the invented binding is
// deliberately left in place for the backends' own use.
void SeedDefaultStorageBlockBindings();
Bool ValidateFragmentOutputLocations(); Bool ValidateFragmentOutputLocations();
Bool ResolveTransformFeedbackVaryings(); Bool ResolveTransformFeedbackVaryings();
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate); void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
@@ -155,6 +155,10 @@ namespace MobileGL::MG_State::GLState {
Uint8* const scratch = m_spirv.globalUboScratch.data(); Uint8* const scratch = m_spirv.globalUboScratch.data();
const SizeT uboSize = m_spirv.globalUboScratch.size(); const SizeT uboSize = m_spirv.globalUboScratch.size();
// Read straight off m_spirv, not through UsesNativeFloat64(): this runs INSIDE the
// phase-B publish, where the join gate is not re-entrant. Same reason the scratch above
// is taken directly.
const Bool nativeFloat64 = m_spirv.nativeFloat64;
for (const auto& init : initializers) { for (const auto& init : initializers) {
// Scalars per array ELEMENT. A matrix element carries cols * rows of them, laid // Scalars per array ELEMENT. A matrix element carries cols * rows of them, laid
@@ -165,12 +169,13 @@ namespace MobileGL::MG_State::GLState {
const Int elements = init.arraySize; const Int elements = init.arraySize;
if (componentsPerElement <= 0 || elements <= 0) continue; if (componentsPerElement <= 0 || elements <= 0) continue;
// EbtDouble belongs with the floats now, not with the skipped types: every 64-bit // EbtDouble belongs with the floats, not with the skipped types. On a DEMOTED
// float in a shader is narrowed to 32 bits before the module reaches a backend // program its 64-bit floats were narrowed to 32 before the module reached a backend
// (ShaderTranspiler::DemoteFloat64Pass), so a `uniform double d = 1.5;` has exactly // (ShaderTranspiler::DemoteFloat64Pass), so a `uniform double d = 1.5;` has exactly
// the 32-bit shadow encoding a `uniform float` does - and glslang already folded its // the 32-bit shadow encoding a `uniform float` does; on a program that kept them it
// value into floatValues, which is a vector<double> either way. Leaving it out meant // has an 8-byte one, which the store width below picks up. glslang folded the value
// the initializer was silently dropped and the uniform came up zero. // into floatValues, a vector<double>, in both cases. Leaving it out meant the
// initializer was silently dropped and the uniform came up zero.
const Bool isFloat = init.basicType == glslang::EbtFloat || const Bool isFloat = init.basicType == glslang::EbtFloat ||
init.basicType == glslang::EbtFloat16 || init.basicType == glslang::EbtFloat16 ||
init.basicType == glslang::EbtDouble; init.basicType == glslang::EbtDouble;
@@ -195,22 +200,36 @@ namespace MobileGL::MG_State::GLState {
// std140 pads every column of a float matrix out to a vec4, so the columns of // std140 pads every column of a float matrix out to a vec4, so the columns of
// a mat3 are 16 bytes apart even though each carries 12. The slot's own span // a mat3 are 16 bytes apart even though each carries 12. The slot's own span
// states the stride the rest of the pipeline agreed on rather than guessing it. // states the stride the rest of the pipeline agreed on rather than guessing it.
const SizeT slotSpan = GetUniformStorageSpanInBytes(static_cast<Uint>(location)); // The static form, with the width taken from m_spirv directly: the member
// overload asks UsesNativeFloat64(), which joins phase B - and phase B is what
// is publishing right now.
const SizeT slotSpan =
UniformStorageSpanInBytes(GetUniformTypeFacts(static_cast<Uint>(location)),
GetUniformSizesInBytes(static_cast<Uint>(location)), nativeFloat64);
const SizeT columnStride = const SizeT columnStride =
columns > 0 ? slotSpan / static_cast<SizeT>(columns) : slotSpan; columns > 0 ? slotSpan / static_cast<SizeT>(columns) : slotSpan;
const Int componentsPerColumn = columns > 0 ? rows : componentsPerElement; const Int componentsPerColumn = columns > 0 ? rows : componentsPerElement;
const Int columnCount = columns > 0 ? columns : 1; const Int columnCount = columns > 0 ? columns : 1;
// A `double` initializer on a program that KEPT its doubles lands in an 8-byte
// component, not a 4-byte one; every other basic type - and every double on a
// demoted program - stays one 32-bit word. glslang folded the value into
// floatValues (a vector<double>) either way, so only the store width moves.
const Bool isWideDouble = init.basicType == glslang::EbtDouble && nativeFloat64;
const SizeT componentSize = isWideDouble ? sizeof(Double) : sizeof(Uint32);
for (Int column = 0; column < columnCount; ++column) { for (Int column = 0; column < columnCount; ++column) {
const SizeT byteOffset = static_cast<SizeT>(offset) + static_cast<SizeT>(column) * columnStride; const SizeT byteOffset = static_cast<SizeT>(offset) + static_cast<SizeT>(column) * columnStride;
const SizeT writeSize = static_cast<SizeT>(componentsPerColumn) * sizeof(Uint32); const SizeT writeSize = static_cast<SizeT>(componentsPerColumn) * componentSize;
if (byteOffset + writeSize > uboSize) break; if (byteOffset + writeSize > uboSize) break;
const SizeT firstComponent = static_cast<SizeT>(element) * componentsPerElement + const SizeT firstComponent = static_cast<SizeT>(element) * componentsPerElement +
static_cast<SizeT>(column) * componentsPerColumn; static_cast<SizeT>(column) * componentsPerColumn;
for (Int component = 0; component < componentsPerColumn; ++component) { for (Int component = 0; component < componentsPerColumn; ++component) {
const SizeT source = firstComponent + static_cast<SizeT>(component); const SizeT source = firstComponent + static_cast<SizeT>(component);
Uint8* const destination = scratch + byteOffset + component * sizeof(Uint32); Uint8* const destination = scratch + byteOffset + component * componentSize;
if (isFloat) { if (isWideDouble) {
const Double value = init.floatValues[source];
std::memcpy(destination, &value, sizeof(value));
} else if (isFloat) {
const Float value = static_cast<Float>(init.floatValues[source]); const Float value = static_cast<Float>(init.floatValues[source]);
std::memcpy(destination, &value, sizeof(value)); std::memcpy(destination, &value, sizeof(value));
} else { } else {
@@ -331,6 +350,8 @@ namespace MobileGL::MG_State::GLState {
artifacts.tProgramUniformIndexToGl.clear(); artifacts.tProgramUniformIndexToGl.clear();
artifacts.glBlockIndexToTProgram.clear(); artifacts.glBlockIndexToTProgram.clear();
artifacts.tProgramBlockIndexToGl.clear(); artifacts.tProgramBlockIndexToGl.clear();
artifacts.glUniformBlockIndexToBlock.clear();
artifacts.blockIndexToGlUniformBlock.clear();
artifacts.linkedExplicitUniformLocations.clear(); artifacts.linkedExplicitUniformLocations.clear();
artifacts.uniformInitialValues.clear(); artifacts.uniformInitialValues.clear();
artifacts.uniformIndexInTProgram.clear(); artifacts.uniformIndexInTProgram.clear();
@@ -344,6 +365,10 @@ namespace MobileGL::MG_State::GLState {
artifacts.uniformBlockIndexByName.clear(); artifacts.uniformBlockIndexByName.clear();
artifacts.uniformBlockBinding.clear(); artifacts.uniformBlockBinding.clear();
artifacts.shaderStorageBlockBinding.clear(); artifacts.shaderStorageBlockBinding.clear();
// Cleared with it: the seed above is re-derived from the newly attached shaders on every
// link, so a stale set would otherwise default a block the new sources do declare a
// binding for.
artifacts.storageBlocksWithoutBinding.clear();
artifacts.attribs.clear(); artifacts.attribs.clear();
artifacts.attribTypes.clear(); artifacts.attribTypes.clear();
artifacts.activeUniformCount = 0; artifacts.activeUniformCount = 0;
@@ -615,15 +640,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.
@@ -76,6 +155,46 @@ namespace MobileGL::MG_State::GLState {
// The last link's full input set; empty when this program has never linked (or its // The last link's full input set; empty when this program has never linked (or its
// last link had no shaders attached). GL-thread-owned, rebuilt in Link()'s prologue. // last link had no shaders attached). GL-thread-owned, rebuilt in Link()'s prologue.
const Vector<LinkedShaderRef>& GetLinkedShaderSnapshot() const { return m_linkedShaderSnapshot; } const Vector<LinkedShaderRef>& GetLinkedShaderSnapshot() const { return m_linkedShaderSnapshot; }
// "Does this program's EXECUTABLE have this stage" - the only form of the question a
// draw may ask. GetShaderIndexByStage answers it of the live attach list, which by the
// rule above is a different set: glAttachShader adds to that list immediately while
// leaving the executable (and LINK_STATUS) alone, and glDetachShader defers the removal
// to the next Link(), so between an attach and the relink the two disagree in both
// directions. A draw-time stage test that reads the live list therefore starts rejecting
// draws GL requires to execute, against an executable that does not carry the stage at
// all - and stays wrong until the application happens to relink.
Bool HasLinkedShaderStage(ShaderStage stage) const {
return std::any_of(m_linkedShaderSnapshot.begin(), m_linkedShaderSnapshot.end(),
[stage](const LinkedShaderRef& ref) {
return ref.shader && ref.shader->GetShaderStage() == stage;
});
}
// The stage of each module of GetGeneratedSpirv(), at the SAME index and with the same
// size: phase B emits exactly one module per entry of the snapshot above, in that order
// (Link() fills ProgramLinkTask::in.shaders from the snapshot loop, phase A copies the
// stages straight across into SpirvHandoff::shaderTypes, and GetSpirvBinaryFromProgram
// walks that list). This - never GetAttachedShaders() - is what a consumer of the
// generated SPIR-V must size its loop by and index alongside.
//
// The two lists are NOT interchangeable and cannot be made so: the attach list is live
// and the SPIR-V is a link artifact, so a glAttachShader after a link grows one and not
// the other, with no link in between at which they could be reconciled. A loop that runs
// over the attach list and indexes the SPIR-V therefore reads off the end of it - which
// is a plain out-of-bounds Vector read, not a wrong answer.
//
// Deliberately a Vector<ShaderStage> and not the shader objects: every consumer wants
// only the stage, and a distinct type is what makes handing it the attach list by
// mistake a compile error rather than a segfault. Built on demand because these callers
// are program-BUILD paths (a backend rebuild, a pipeline cache miss), each of which then
// spends milliseconds compiling the very modules this indexes.
Vector<ShaderStage> GetLinkedShaderStages() const {
Vector<ShaderStage> stages;
stages.reserve(m_linkedShaderSnapshot.size());
for (const LinkedShaderRef& ref : m_linkedShaderSnapshot) {
stages.push_back(ref.shader ? ref.shader->GetShaderStage() : ShaderStage::Unknown);
}
return stages;
}
// Pipeline-composite attach: AttachShader plus a pin that makes THIS program's // Pipeline-composite attach: AttachShader plus a pin that makes THIS program's
// Link() consume ref's (source, node) instead of the shader's current ones, so a // Link() consume ref's (source, node) instead of the shader's current ones, so a
// post-link recompile of the stage program's shader cannot leak into the composite. // post-link recompile of the stage program's shader cannot leak into the composite.
@@ -134,8 +253,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;
@@ -161,12 +279,12 @@ namespace MobileGL::MG_State::GLState {
if (tIndex < 0 || tIndex >= static_cast<Int>(Artifacts().tProgramUniformIndexToGl.size())) return -1; if (tIndex < 0 || tIndex >= static_cast<Int>(Artifacts().tProgramUniformIndexToGl.size())) return -1;
return Artifacts().tProgramUniformIndexToGl[tIndex]; return Artifacts().tProgramUniformIndexToGl[tIndex];
} }
// GL uniform-block index -> glslang TProgram block index (the inverse of // Block index -> glslang TProgram block index (the inverse of
// GlBlockIndexFromTProgram). The interface-query layer needs it to reach block // GlBlockIndexFromTProgram). The interface-query layer needs it to reach block
// properties glslang exposes but no typed getter here does. // properties glslang exposes but no typed getter here does.
Int TProgramBlockIndex(Uint glBlockIndex) const { Int TProgramBlockIndex(Uint blockIndex) const {
return glBlockIndex < Artifacts().glBlockIndexToTProgram.size() return blockIndex < Artifacts().glBlockIndexToTProgram.size()
? Artifacts().glBlockIndexToTProgram[glBlockIndex] ? Artifacts().glBlockIndexToTProgram[blockIndex]
: -1; : -1;
} }
Int GlBlockIndexFromTProgram(Int tBlockIndex) const { Int GlBlockIndexFromTProgram(Int tBlockIndex) const {
@@ -174,45 +292,74 @@ namespace MobileGL::MG_State::GLState {
return Artifacts().tProgramBlockIndexToGl[tBlockIndex]; return Artifacts().tProgramBlockIndexToGl[tBlockIndex];
} }
// ---- GL_UNIFORM_BLOCK index <-> block index translation ----
// The block index space above carries the storage blocks and the synthesized atomic
// counter blocks as well; GL_ACTIVE_UNIFORM_BLOCKS counts only actual uniform blocks
// (GL 4.6 core 7.6). Every glGetActiveUniformBlock* / glGetUniformBlockIndex /
// glUniformBlockBinding entry point speaks THIS space and translates into the block
// space before touching any of the block-keyed tables; the backends keep speaking the
// block space directly. See LinkArtifacts::glUniformBlockIndexToBlock.
Int GetGlUniformBlockCount() const {
return static_cast<Int>(Artifacts().glUniformBlockIndexToBlock.size());
}
Bool IsActiveGlUniformBlock(Uint glUniformBlockIndex) const {
return glUniformBlockIndex < Artifacts().glUniformBlockIndexToBlock.size();
}
Int BlockIndexFromGlUniformBlock(Uint glUniformBlockIndex) const {
return glUniformBlockIndex < Artifacts().glUniformBlockIndexToBlock.size()
? Artifacts().glUniformBlockIndexToBlock[glUniformBlockIndex]
: -1;
}
Int GlUniformBlockIndexFromBlock(Int blockIndex) const {
if (blockIndex < 0 || blockIndex >= static_cast<Int>(Artifacts().blockIndexToGlUniformBlock.size())) {
return -1;
}
return Artifacts().blockIndexToGlUniformBlock[blockIndex];
}
// glGetUniformBlockIndex: GL_INVALID_INDEX for a name that is not an active UNIFORM
// block, which includes every storage block and every atomic counter block even though
// GetUniformBlockIndex() below resolves them (it answers in the block space, which the
// backends need to keep reaching them by name).
Uint GetGlUniformBlockIndex(const char* name) const {
const Uint blockIndex = GetUniformBlockIndex(name);
if (blockIndex == 0xFFFFFFFFu) return 0xFFFFFFFFu;
const Int glIndex = GlUniformBlockIndexFromBlock(static_cast<Int>(blockIndex));
return glIndex < 0 ? 0xFFFFFFFFu : static_cast<Uint>(glIndex);
}
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.
@@ -228,17 +375,73 @@ namespace MobileGL::MG_State::GLState {
return GetUniformArraySizeByTIndex(TProgramUniformIndex(index)); return GetUniformArraySizeByTIndex(TProgramUniformIndex(index));
} }
Int GetActiveUniformBlockIndex(Uint index) const { // The BLOCK index of the block owning this active uniform, or -1 when it owns none as
auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); // far as GL is concerned. Internal: pair it with another block-space index, never with
// a GL_UNIFORM_BLOCK one (GetActiveUniformBlockIndex below is that one).
Int GetActiveUniformOwnerBlockIndex(Uint index) const {
// 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);
}
// GL_UNIFORM_BLOCK_INDEX: an index into the GL_ACTIVE_UNIFORM_BLOCKS list, or -1. A
// buffer variable owns a storage block, which is not in that list, so it answers -1 too
// (and after the enumeration filter it is not an active uniform in the first place).
Int GetActiveUniformBlockIndex(Uint index) const {
return GlUniformBlockIndexFromBlock(GetActiveUniformOwnerBlockIndex(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 +455,19 @@ 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; // An atomic counter reaches the std140 branch below only because the transpiler
if (type->isMatrix()) { // lowered it onto a synthesized block; the buffer it actually addresses is an
// ATOMIC COUNTER buffer, whose elements are tightly packed uints (GL 4.6 core 7.6:
// "each counter is a single 4-byte value"). Its array stride is therefore 4, not the
// vec4 round-up std140 would apply
// (KHR-GL43.shader_atomic_counters.basic-program-query wants 4 for ac_counter67[0]).
if (IsActiveUniformAtomicCounter(index)) return 4;
if (uniform.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 +481,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 +496,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 +508,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).
@@ -346,26 +565,47 @@ namespace MobileGL::MG_State::GLState {
: kInvalidUniformOffset; : kInvalidUniformOffset;
} }
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); } Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
// Bytes a uniform actually occupies in the global UBO, which is not its GL type size, // std140 column stride of a matrix uniform in the global UBO: every column is padded out
// for two reasons. std140 pads each column of a matrix out to a vec4, so a mat3 spans // to the base alignment of a vec4 for 32-bit components, and of a dvec4 for 64-bit ones -
// 48 bytes even though only 36 of them carry components. And every 64-bit float in a // except that a 2-ROW double column is a dvec2, whose base alignment is already 16.
// shader is narrowed to 32 bits before the module reaches a backend // (GL 4.6 core 7.6.2.2 rules 2-4; SPIRV-Cross derives the same numbers, which is what
// (ShaderTranspiler::DemoteFloat64Pass) - the global UBO is laid out by reflecting that // makes this agree with the reflected module.)
// demoted module - so a `double` uniform occupies exactly what its float-typed twin static SizeT UniformMatrixColumnStride(const TypeFacts& type, const Bool nativeFloat64) {
// would, half its GL type size, and a `dmat4` is padded like any other matrix. Anything if (type.isDouble && nativeFloat64) {
// reading or writing a whole uniform's storage - a bounds check, a copy between two return type.matrixRows <= 2 ? 2 * sizeof(GLdouble) : 4 * sizeof(GLdouble);
// programs' shadows - wants this rather than GetUniformSizesInBytes.
static SizeT UniformStorageSpanInBytes(const glslang::TType* type, SizeT tightSize) {
if (type != nullptr && type->isMatrix()) {
return static_cast<SizeT>(type->getMatrixCols()) * 4 * sizeof(Float);
} }
if (type != nullptr && type->getBasicType() == glslang::EbtDouble) { return 4 * sizeof(Float);
}
// Bytes a uniform actually occupies in the global UBO, which is not its GL type size,
// for two reasons. std140 pads each column of a matrix out to a vec4 (or a dvec4), so a
// mat3 spans 48 bytes even though only 36 of them carry components. And a 64-bit float
// may have been narrowed to 32 before the module reached the backend
// (ShaderTranspiler::DemoteFloat64Pass) - the global UBO is laid out by reflecting
// whichever module was produced - so on a DEMOTED program a `double` uniform occupies
// exactly what its float-typed twin would, half its GL type size, and a `dmat4` is padded
// like any other 32-bit matrix. On a program that kept its doubles it occupies the full
// GL type size and its matrix columns are twice as far apart. `nativeFloat64` is the
// program's own SpirvArtifacts flag, never a live backend read: it describes the modules
// that were actually built. Anything reading or writing a whole uniform's storage - a
// bounds check, a copy between two programs' shadows - wants this rather than
// GetUniformSizesInBytes.
static SizeT UniformStorageSpanInBytes(const TypeFacts& type, SizeT tightSize,
const Bool nativeFloat64 = false) {
if (type.isMatrix) {
return static_cast<SizeT>(type.matrixCols) * UniformMatrixColumnStride(type, nativeFloat64);
}
if (type.isDouble && !nativeFloat64) {
return tightSize / 2; return tightSize / 2;
} }
return tightSize; return tightSize;
} }
// Whether this program's modules KEPT their 64-bit floats. Joins phase B, like every
// other question about the global UBO's layout - and it is one: it decides how wide a
// `double` uniform's slot is.
Bool UsesNativeFloat64() const { return Spirv().nativeFloat64; }
SizeT GetUniformStorageSpanInBytes(Uint location) const { SizeT GetUniformStorageSpanInBytes(Uint location) const {
return UniformStorageSpanInBytes(GetUniformTType(location), GetUniformSizesInBytes(location)); return UniformStorageSpanInBytes(GetUniformTypeFacts(location), GetUniformSizesInBytes(location),
UsesNativeFloat64());
} }
// ---- "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 +716,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 +731,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 +770,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 +863,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,23 +938,22 @@ 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 // Size of the BLOCK index space - every block the relaxed parse produced except the
// materializes for default-block uniforms is filtered out by DoReflection. // synthesized MGL_GLOBAL_UBO, which DoReflection filters out. NOT the answer to
// glGetProgramiv(GL_ACTIVE_UNIFORM_BLOCKS): storage blocks and atomic counter blocks
// live in here too, and GetGlUniformBlockCount() is the one that excludes them.
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; }
// Answers in the BLOCK space, so it resolves storage and atomic counter blocks too -
// the backends reach those by name. glGetUniformBlockIndex must NOT: use
// GetGlUniformBlockIndex() for the GL entry point.
Uint GetUniformBlockIndex(const char* name) const { Uint GetUniformBlockIndex(const char* name) const {
auto it = Artifacts().uniformBlockIndexByName.find(name); auto it = Artifacts().uniformBlockIndexByName.find(name);
if (it != Artifacts().uniformBlockIndexByName.end()) return it->second; if (it != Artifacts().uniformBlockIndexByName.end()) return it->second;
@@ -729,21 +964,20 @@ namespace MobileGL::MG_State::GLState {
if (it != Artifacts().uniformBlockIndexByName.end()) return it->second; if (it != Artifacts().uniformBlockIndexByName.end()) return it->second;
return 0xFFFFFFFFu; // GL_INVALID_INDEX return 0xFFFFFFFFu; // GL_INVALID_INDEX
} }
Bool IsActiveUniformBlock(Uint index) const { // Takes a BLOCK index. The GL entry points validate their argument against the
if (index >= GetActiveUniformBlocksCount()) return false; // GL_UNIFORM_BLOCK space with IsActiveGlUniformBlock() first and translate; the bound
return true; // test here is only the range of the space this index actually lives in.
}
Uint GetUBOSizeAt(Uint index) const { Uint GetUBOSizeAt(Uint index) const {
if (!IsActiveUniformBlock(index)) return 0; if (index >= Artifacts().glBlockIndexToTProgram.size()) return 0;
// glslang reports the unpadded end offset of the last member, but a std140 block // glslang reports the unpadded end offset of the last member, but a std140 block
// (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;
} }
@@ -764,17 +998,20 @@ namespace MobileGL::MG_State::GLState {
// fills GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, so the two queries always agree // fills GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, so the two queries always agree
// (glslang's numMembers counts declared members, which diverges from the reflected // (glslang's numMembers counts declared members, which diverges from the reflected
// entry list for struct arrays and arrayed block instances). // entry list for struct arrays and arrayed block instances).
// Takes a BLOCK index, and scans in the block space: GetUniformBlockMemberOwnerIndex
// answers there, so pairing it with the GL_UNIFORM_BLOCK-space
// GetActiveUniformBlockIndex would compare two different numberings.
Int GetUniformBlockActiveUniformCount(Uint index) const { Int GetUniformBlockActiveUniformCount(Uint index) const {
const Int ownerIndex = static_cast<Int>(GetUniformBlockMemberOwnerIndex(index)); const Int ownerIndex = static_cast<Int>(GetUniformBlockMemberOwnerIndex(index));
Int count = 0; Int count = 0;
for (Uint uniformIndex = 0; uniformIndex < Artifacts().activeUniformCount; ++uniformIndex) { for (Uint uniformIndex = 0; uniformIndex < Artifacts().activeUniformCount; ++uniformIndex) {
if (GetActiveUniformBlockIndex(uniformIndex) == ownerIndex) ++count; if (GetActiveUniformOwnerBlockIndex(uniformIndex) == ownerIndex) ++count;
} }
return count; return count;
} }
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 +1092,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 +1144,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;
@@ -928,9 +1204,32 @@ namespace MobileGL::MG_State::GLState {
Vector<Int> tProgramUniformIndexToGl; Vector<Int> tProgramUniformIndexToGl;
Vector<Int> glBlockIndexToTProgram; Vector<Int> glBlockIndexToTProgram;
Vector<Int> tProgramBlockIndexToGl; Vector<Int> tProgramBlockIndexToGl;
// Per-link merged snapshot of the attached shaders' lexically extracted // GL_UNIFORM_BLOCK index space: ACTUAL uniform blocks only, a strict subsequence of
// layout(location = N) default-block uniform qualifiers (the relaxed parse drops // glBlockIndexToTProgram above.
// them from reflection; the DoReflection assigner restores them from here). //
// That list is the BLOCK space - everything the relaxed parse produced except
// MGL_GLOBAL_UBO - and it is what the backends walk and what every block-keyed table
// here (uniformBlockBinding, uniformBlockIndexByName, blockReflection ordering) is
// indexed by. It is NOT the GL uniform-block list: MobileGL does not pass
// EShReflectionSeparateBuffers to buildReflection, so glslang routes BUFFER blocks
// through indexToUniformBlock too, and the list therefore also carries every shader
// storage block and every synthesized gl_AtomicCounterBlock_N. GL 4.6 core 7.6 gives
// those their own enumerations (GL_SHADER_STORAGE_BLOCK and
// GL_ACTIVE_ATOMIC_COUNTER_BUFFERS respectively), and GL_ACTIVE_UNIFORM_BLOCKS /
// glGetActiveUniformBlock*/glGetUniformBlockIndex must not see either.
//
// Kept as a SECOND space rather than filtering the first in place: DirectGLES assigns
// one ESSL uniform-buffer binding point per entry of the block list as it walks it
// (Managers.cpp CacheResourceLocations and the matching per-draw loop in
// DirectGLES.cpp), so compacting that list would renumber every backend binding
// point, and tProgramBlockIndexToGl[i] < 0 is what DoReflection and
// BuildGlobalUboRouting read as "member of the synthesized global UBO".
Vector<Int> glUniformBlockIndexToBlock; // GL uniform-block index -> block index
Vector<Int> blockIndexToGlUniformBlock; // block index -> GL uniform-block index (-1)
// Per-link merged snapshot of the layout(location = N) qualifiers the attached
// shaders' default-block uniforms declared, as glslang recorded them at the point
// its relaxed remap dropped them (the relaxed parse drops them from reflection; the
// DoReflection assigner restores them from here).
UnorderedMap<String, Int> linkedExplicitUniformLocations; UnorderedMap<String, Int> linkedExplicitUniformLocations;
// Per-link snapshot of the default-block uniform INITIALIZERS the attached shaders // Per-link snapshot of the default-block uniform INITIALIZERS the attached shaders
// declared ("uniform int i = 1;"). Desktop GLSL says that value is what the uniform // declared ("uniform int i = 1;"). Desktop GLSL says that value is what the uniform
@@ -955,6 +1254,10 @@ namespace MobileGL::MG_State::GLState {
Vector<Int> uniformIndexInTProgram; Vector<Int> uniformIndexInTProgram;
// ditto. Will be set at glUniform1i // ditto. Will be set at glUniform1i
Vector<Int> uniformSamplerOrImageUnitIndex; Vector<Int> uniformSamplerOrImageUnitIndex;
// Sampler/image layout(binding = N) initial texture/image units, captured by
// TMglGlslIoResolver at mapIO's collect callback - the last point at which the
// qualifier still says what the shader declared. An OUTPUT of the link, not an
// input to it: nothing supplies this map, the resolver fills it.
UnorderedMap<String, Uint> explicitOpaqueUniformBindings; UnorderedMap<String, Uint> explicitOpaqueUniformBindings;
// Ordered by uniform block index // Ordered by uniform block index
@@ -969,7 +1272,21 @@ namespace MobileGL::MG_State::GLState {
Vector<Int> uniformBlockBinding; Vector<Int> uniformBlockBinding;
// glShaderStorageBlockBinding overrides, keyed by GL block name. See // glShaderStorageBlockBinding overrides, keyed by GL block name. See
// SetShaderStorageBlockBinding for why this one is by name and not by index. // SetShaderStorageBlockBinding for why this one is by name and not by index.
//
// ALSO SEEDED AT LINK, by ProgramLinkTask::SeedDefaultStorageBlockBindings, with the
// GL-mandated binding 0 for every storage block whose shader declared no
// layout(binding = N). Those blocks have no other way to be told apart from a block
// that declared one: glslang's IO mapper invents a binding and writes it into the
// qualifier, so the reflection reports the invention. A seed is therefore "GL's
// default binding for this block", and a later glShaderStorageBlockBinding simply
// overwrites it - default and rebind travel one path.
UnorderedMap<String, Int> shaderStorageBlockBinding; UnorderedMap<String, Int> shaderStorageBlockBinding;
// Block type names of the storage blocks the program's shaders declared with NO
// layout(binding = N). Input to the seeding above; filled during mapIO by
// TMglGlslIoResolver, which is the last observer that can still tell a declared
// binding from an invented one - and, unlike the per-shader lexer this replaced,
// sees the declaration with its macros expanded.
std::set<String> storageBlocksWithoutBinding;
Uint activeUniformCount = 0; Uint activeUniformCount = 0;
Uint maxUniformLocation = 0; Uint maxUniformLocation = 0;
@@ -1027,6 +1344,15 @@ namespace MobileGL::MG_State::GLState {
// not drawable, which the backends already express through their link-status // not drawable, which the backends already express through their link-status
// gates. // gates.
Bool spirvStatus = false; Bool spirvStatus = false;
// Whether these modules KEPT their 64-bit floats instead of being narrowed to 32
// (ShaderTranspiler::DemoteFloat64Pass). Decided per PROGRAM, never per module - the
// global UBO is one buffer all stages read, so two stages disagreeing about whether a
// `uniform double` occupies 4 or 8 bytes would put every uniform after it at a
// different offset in each. Recorded here rather than re-derived from the backend
// because it is the layout THESE modules were built with: it is what the routing
// table's offsets mean, and glUniform*d / glGetUniform*v have to write and read the
// width the shader actually declares.
Bool nativeFloat64 = false;
}; };
// ---- artifacts-only helpers, shared with ProgramLinkTask ---- // ---- artifacts-only helpers, shared with ProgramLinkTask ----
@@ -1042,6 +1368,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 +1391,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,18 +97,39 @@ 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 =
m_phaseA->in.env != nullptr && m_phaseA->in.env->backend == BackendType::DirectVulkan; m_phaseA->in.env != nullptr && m_phaseA->in.env->backend == BackendType::DirectVulkan;
const Bool enableSpirvValidation = m_phaseA->in.enableSpirvValidation; const Bool enableSpirvValidation = m_phaseA->in.enableSpirvValidation;
artifacts.enableSpirvValidation = enableSpirvValidation; artifacts.enableSpirvValidation = enableSpirvValidation;
GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation); // Whether this backend consumes 64-bit floats itself. Read off the SNAPSHOT, like every
// other environment question this node asks: a worker may not touch
// MG_Backend::pActiveBackendObject, and the answer has to be the one the L1 key was built
// with (ProgramLinkTask::BuildSpirvCacheKey reads the same env) or a memo written under
// one answer could be handed back under the other.
const Bool nativeFloat64 = m_phaseA->in.env != nullptr && m_phaseA->in.env->ConsumesFloat64Natively();
GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation,
nativeFloat64);
// GlslangToSpv was the only consumer of the parsed ASTs; everything after this point // GlslangToSpv was the only consumer of the parsed ASTs; everything after this point
// works on the SPIR-V and on the TProgram's own self-contained reflection pool. Drop // works on the SPIR-V and on the TProgram's own self-contained reflection pool. Drop
// them here rather than at the end of the body, which is ~87% of this node's runtime // them here rather than at the end of the body, which is ~87% of this node's runtime
@@ -117,13 +140,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,13 +165,30 @@ 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());
} }
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex, void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex,
const Bool deferOutputValidationForDirectVulkan, const Bool deferOutputValidationForDirectVulkan,
const Bool enableSpirvValidation) { const Bool enableSpirvValidation, const Bool nativeFloat64) {
/* As we passed first stage compilation/linking, /* As we passed first stage compilation/linking,
* we'll assume all the operations here should * we'll assume all the operations here should
* pass. We may be able to employ some optimizations * pass. We may be able to employ some optimizations
@@ -171,12 +216,39 @@ namespace MobileGL::MG_State::GLState {
MGLOG_D("ProgramObject %u: GenerateSpirv - generated %zu SPIR-V modules", externalIndex, MGLOG_D("ProgramObject %u: GenerateSpirv - generated %zu SPIR-V modules", externalIndex,
artifacts.generatedSpirv.size()); artifacts.generatedSpirv.size());
// The fp64 verdict, taken ONCE for the whole program and before any module is touched.
//
// Per program rather than per module, and that is forced by the global UBO: all stages
// read one buffer whose layout is derived by reflecting the modules, so a vertex stage
// that stored a `uniform double` as 4 bytes next to a fragment stage that stored it as 8
// would put every uniform after it somewhere different in each, and the routing table
// (one offset per location) could only describe one of them.
//
// The exception itself is the vertex INPUT: no backend here can fetch a 64-bit attribute,
// and VertexInputStateFactory picks the format from the VAO attribute without ever seeing
// what the shader declared, so a Float64 input would meet a narrowed float32 stream. One
// such stage demotes the whole program, which is exactly what every backend without
// native fp64 does to it anyway.
Bool keepFloat64 = nativeFloat64;
if (keepFloat64) {
for (const auto& spv : artifacts.generatedSpirv) {
if (ShaderCompiler::ModuleDeclaresFloat64VertexInput(spv)) {
keepFloat64 = false;
MGLOG_D("ProgramObject %u: a vertex stage declares a 64-bit float input; demoting the "
"whole program despite native fp64",
externalIndex);
break;
}
}
}
artifacts.nativeFloat64 = keepFloat64;
// Linked SPIR-V generated, sanitize and optimize it // Linked SPIR-V generated, sanitize and optimize it
Bool allOptimized = true; Bool allOptimized = true;
{ {
for (auto& spv : artifacts.generatedSpirv) { for (auto& spv : artifacts.generatedSpirv) {
auto success = ShaderCompiler::SanitizeAndOptimizeBinary( auto success = ShaderCompiler::SanitizeAndOptimizeBinary(
spv, spv, !deferOutputValidationForDirectVulkan, enableSpirvValidation); spv, spv, !deferOutputValidationForDirectVulkan, enableSpirvValidation, keepFloat64);
if (!success) { if (!success) {
// The one genuine phase-B failure mode: one of the seven optimizer passes // The one genuine phase-B failure mode: one of the seven optimizer passes
// reported failure, so `spv` is whatever the run left behind. A fordebug // reported failure, so `spv` is whatever the run left behind. A fordebug
@@ -296,22 +368,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);
@@ -66,7 +66,8 @@ namespace MobileGL::MG_State::GLState {
void RunBody() override; void RunBody() override;
void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex, void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex,
Bool deferOutputValidationForDirectVulkan, Bool enableSpirvValidation); Bool deferOutputValidationForDirectVulkan, Bool enableSpirvValidation,
Bool nativeFloat64);
void BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex); void BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
// Worker-side MGLOG replacement, replayed by the join on the GL thread. Same reason as // Worker-side MGLOG replacement, replayed by the join on the GL thread. Same reason as
@@ -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,11 +140,30 @@ 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 and the three lexical rejections. Split out so P0b
// side-channel extractions. Split out so P0b layer 2 can memoize exactly this and // layer 2 can memoize exactly this and nothing else - the glslang parse stays per-object
// nothing else - the glslang parse stays per-object because its TShader is consume-once. // because its TShader is consume-once. Deliberately free of any per-object state so the
// Deliberately free of any per-object state so the memo is sound. // memo is sound.
//
// The side-channel EXTRACTIONS that used to live here are gone: what the relaxed parse
// destroys is now recovered from glslang itself, at the two points where it is destroyed
// (see ShaderCompileArtifacts::explicitUniformLocations and
// TMglGlslIoResolver::reserverResourceSlot). They could not stay here anyway - none of
// them is a function of the unexpanded source text, which is all this half can see.
// //
// The compute local-size verdict reads `env` rather than the live backend, and // The compute local-size verdict reads `env` rather than the live backend, and
// env.fingerprint is part of the P0b cache key, so a memo can never be returned against // env.fingerprint is part of the P0b cache key, so a memo can never be returned against
@@ -172,15 +194,28 @@ 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;
}
// NO ATOMIC-COUNTER OFFSET SCAN HERE ANY MORE: glslang raises both rules itself now, at
// the site where its relaxed remap folds the counter into a synthesized block
// (ParseHelper.cpp atomicCounterOffsetCheck, called from vkRelaxedRemapUniformVariable).
// A violation is an ordinary parse failure, so it reaches GL through the same path every
// other compile error does - and, unlike a scan of unexpanded text, it sees an offset
// spelled as a macro or a const expression.
// 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
// parse. The GL frontend semantics the relaxed parse cannot provide are restored // parse. The GL frontend semantics the relaxed parse cannot provide are restored
// on top: explicit default-block uniform locations through the lexical // on top, all of them out of glslang: explicit default-block uniform locations from
// side-channels below, dead-uniform/global-UBO filtering in // the snapshot the parse takes, opaque bindings and unqualified storage blocks from
// the IO mapper's collect callback, dead-uniform/global-UBO filtering in
// ProgramObject::DoReflection. // ProgramObject::DoReflection.
result.explicitUniformLocations = ExtractExplicitUniformLocations(result.preprocessedSource);
result.explicitOpaqueBindings = ExtractExplicitOpaqueBindings(result.preprocessedSource);
result.outcome = ShaderPreprocessOutcome::Preprocessed; result.outcome = ShaderPreprocessOutcome::Preprocessed;
return result; return result;
} }
@@ -248,26 +283,94 @@ 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;
UnorderedMap<String, Int> explicitUniformLocations;
if (verdict) {
parsedOk = verdict->parsed;
parseLog = verdict->infoLog;
// From the verdict, not from a parse - see ShaderParseVerdict for why they had to
// move into the payload when their origin moved into glslang.
explicitUniformLocations = verdict->explicitUniformLocations;
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();
explicitUniformLocations = CollectExplicitUniformLocations(*parsedShader);
} 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;
freshVerdict->explicitUniformLocations = explicitUniformLocations;
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 = Move(explicitUniformLocations);
artifacts.explicitOpaqueBindings = shared.explicitOpaqueBindings;
artifacts.infoLog.clear(); artifacts.infoLog.clear();
if (shouldPopulateCache) { if (shouldPopulateCache) {
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
@@ -286,8 +389,6 @@ namespace MobileGL::MG_State::GLState {
if (shouldPopulateCache) { if (shouldPopulateCache) {
fresh->outcome = ShaderPreprocessOutcome::ParseFailed; fresh->outcome = ShaderPreprocessOutcome::ParseFailed;
fresh->infoLog = artifacts.infoLog; fresh->infoLog = artifacts.infoLog;
fresh->explicitUniformLocations.clear();
fresh->explicitOpaqueBindings.clear();
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh)); cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
} }
} }
@@ -310,10 +411,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 +431,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,20 +41,42 @@ 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
// being deterministic across backend-state changes. // being deterministic across backend-state changes.
String preprocessedSource; String preprocessedSource;
// The explicit layout(location = N) qualifiers this stage's default-block uniforms
// declared, as glslang recorded them at the point its Vulkan-relaxed remap dropped
// them (CollectExplicitUniformLocations).
//
// Populated on the L1c HIT path too, out of the cached verdict rather than out of a
// parse - which is why the verdict carries them. Everything else the relaxed parse
// destroys is recovered at LINK instead, from the IO mapper's collect callback, and so
// has no field here at all.
UnorderedMap<String, Int> explicitUniformLocations; UnorderedMap<String, Int> explicitUniformLocations;
UnorderedMap<String, Uint> explicitOpaqueBindings;
String infoLog; String infoLog;
Bool compileStatus = false; Bool compileStatus = false;
}; };
// 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 lexical rejections, and (unless the translation memo's compile
// glslang parse - with every input it needs owned by the node itself. // half already knows the answer) the glslang parse plus the explicit-uniform-location
// snapshot it yields - 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 +109,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
@@ -155,7 +155,7 @@ namespace MobileGL::MG_State::GLState {
// //
// The failure case is covered too: the info log stays queryable because nothing is // The failure case is covered too: the info log stays queryable because nothing is
// cleared. And if the stored TShader already fed a link, the no-op leaves // cleared. And if the stored TShader already fed a link, the no-op leaves
// preprocessedSource and both side-channel maps intact, which is precisely what // preprocessedSource and the explicit-location snapshot intact, which is precisely what
// ClaimParsedShader's on-demand re-parse needs - a real recompile would have handed // ClaimParsedShader's on-demand re-parse needs - a real recompile would have handed
// the next link a fresh parse, the no-op hands it a fresh re-parse of the identical // the next link a fresh parse, the no-op hands it a fresh re-parse of the identical
// source instead. Same result, one parse either way. // source instead. Same result, one parse either way.
@@ -101,17 +101,11 @@ namespace MobileGL {
const SharedPtr<glslang::TShader>& GetCompiledShader() const { return Compiled().shader; } const SharedPtr<glslang::TShader>& GetCompiledShader() const { return Compiled().shader; }
const String& GetInfoLog() const { return Compiled().infoLog; } const String& GetInfoLog() const { return Compiled().infoLog; }
// Explicit layout(location = N) qualifiers on this shader's default-block // Explicit layout(location = N) qualifiers on this shader's default-block
// uniforms, captured lexically at Compile() because the relaxed parse drops // uniforms, as glslang recorded them at the point its Vulkan-relaxed remap
// them from reflection (see ExtractExplicitUniformLocations). // discarded them (see CollectExplicitUniformLocations).
const UnorderedMap<String, Int>& GetExplicitUniformLocations() const { const UnorderedMap<String, Int>& GetExplicitUniformLocations() const {
return Compiled().explicitUniformLocations; return Compiled().explicitUniformLocations;
} }
// Explicit layout(binding = N) on sampler/image uniforms - their initial
// texture/image units - captured lexically for the same reason (see
// ExtractExplicitOpaqueBindings).
const UnorderedMap<String, Uint>& GetExplicitOpaqueBindings() const {
return Compiled().explicitOpaqueBindings;
}
Bool GetCompileStatus() const { return Compiled().compileStatus; } Bool GetCompileStatus() const { return Compiled().compileStatus; }
Bool GetDeleteStatus() const { return m_deleteStatus; } Bool GetDeleteStatus() const { return m_deleteStatus; }
@@ -26,6 +26,9 @@ 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,
// 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,
@@ -33,13 +36,23 @@ namespace MobileGL::MG_State::GLState {
// Everything ShaderObject::Compile() derives from the source text alone, i.e. // Everything ShaderObject::Compile() derives from the source text alone, i.e.
// everything that is identical for two shader objects holding byte-identical source. // everything that is identical for two shader objects holding byte-identical source.
//
// "The source text alone" is now literally true: the preprocessed text, an accept/reject
// verdict, and the log that explains a rejection. Anything that needs to know what the
// shader MEANS is derived from the parse instead - see the note on the missing fields.
struct ShaderPreprocessResult { struct ShaderPreprocessResult {
ShaderPreprocessOutcome outcome = ShaderPreprocessOutcome::Preprocessed; ShaderPreprocessOutcome outcome = ShaderPreprocessOutcome::Preprocessed;
// Valid unless the preprocessor itself never ran; kept even for the rejection // Valid unless the preprocessor itself never ran; kept even for the rejection
// outcomes because that is the text the diagnostics refer to. // outcomes because that is the text the diagnostics refer to.
String preprocessedSource; String preprocessedSource;
UnorderedMap<String, Int> explicitUniformLocations; // NO EXTRACTED SIDE CHANNELS ANY MORE, and their absence is the point. Explicit
UnorderedMap<String, Uint> explicitOpaqueBindings; // uniform locations, explicit opaque bindings and unqualified storage blocks used to be
// lexed out of the text here, which meant reading MACRO-UNEXPANDED source: MobileGL's
// preprocessor rewrites the text, it does not run the C preprocessor, so
// `binding = SOME_MACRO` reached the scanners verbatim. All three now come from
// glslang - the first from a snapshot taken inside the parse, the other two from the
// IO mapper's collect callback - and none of them is a function of the source text
// ALONE any more, which is the only thing this struct is allowed to hold.
// The compile info log to publish; empty when outcome == Preprocessed. // The compile info log to publish; empty when outcome == Preprocessed.
String infoLog; String infoLog;
@@ -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};
@@ -418,8 +434,28 @@ namespace MobileGL {
} }
Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler) { Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler) {
const Bool mipmapped = // A multisample texture is fetched, never filtered. GL 4.6 core 8.17 gives it exactly
effectiveSampler != nullptr && effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None; // one level and says its sampler state is not used at all - texelFetch is the only way
// a shader can read it - so 8.14's filter-completeness rules, which is what the
// `mipmapped` branch below asks about, never apply to it.
//
// Deriving `mipmapped` from that unused sampler is what made EVERY multisample texture
// look incomplete: MIN_FILTER's initial value is NEAREST_MIPMAP_LINEAR, and a texture
// that can only ever have one level never satisfies the mip-chain check. Both backends
// treat "samples as incomplete" as "do not bind it" (DirectGLES's per-unit walk in
// ResolveAndBindUnitTextures, DirectVulkan's UniformManager), so the sampler2DMS the
// shader declared was left pointing at nothing and every texelFetch read zero. That is
// the sampler2DMS/sampler2DMSArray half of KHR-GL43.compute_shader.resource-texture,
// which fails at the first data7 element with the multisample texture correctly
// cleared and simply never bound.
//
// IsCopyImageEndpointComplete already spells the same guard as
// CopyImageTargetHasMipmapChain; this was the one place that asked without it.
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
const Bool filtered = target != TextureTarget::Texture2DMultisample &&
target != TextureTarget::Texture2DMultisampleArray;
const Bool mipmapped = filtered && effectiveSampler != nullptr &&
effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None;
return !IsMipmapCompleteForFilter(texture, mipmapped); return !IsMipmapCompleteForFilter(texture, mipmapped);
} }
@@ -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,39 @@ 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}
)
add_executable(
ViewportIndexRoutingTest
ViewportIndexRoutingTest.cpp
)
target_include_directories(ViewportIndexRoutingTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
ViewportIndexRoutingTest 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)
gtest_discover_tests(ViewportIndexRoutingTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)

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