Compare commits

...
Author SHA1 Message Date
swung0x48 9eae98581f [Test] (SelfTest, POST): probe upload/draw ordering in persistently mapped vertex arenas - native GLES controls distinguish mapped destination corruption from staging and synchronization failures
Add the persistent buffer ordering probe to the GLES POST's known-driver-bug inventory. Queue updates and draws into independent FBOs before reading them back, covering SubData and copies from both coherent persistent and ordinary staging buffers. Retry fresh mapped allocations for intermittent corruption.

Require passing never-mapped and fully serialized controls before reporting a finding. Include finish-before, map-then-unmap and barrier diagnostics, preserve caller GL state, and treat setup, allocation and GL errors as inconclusive. This adds detection and reporting only; no rendering workaround is enabled.

Validation: all 46 DriverBugProbesTest tests pass, including 11 new ordering, control, collector and cleanup cases. The Android API 26 / NDK 27 native probe detects all three upload paths on Mali-G1-Ultra r54p1 with clean controls and no GL errors. llvmpipe reports no finding after 240 mapped FBO checks per upload path.
2026-09-07 00:44:08 -04:00
swung0x48 d7655247f7 [Fix, Test] (DirectGLES, Integration): rebind VAOs when an adopted buffer is respecified - the immediate retire path forgot the buffer-id generation, so cached vertex and element bindings kept the deleted store
Advance the buffer-id generation when Ops_Respecify retires immutable storage on the context thread, matching the existing adoption and deferred-retirement paths.

Add pixel regression coverage for unchanged VBO/IBO attachments across same-size redefinition, growth and shrinkage, including bound and unbound VAOs sharing a vertex arena and an index arena returning to shadow storage.
2026-09-06 22:55:06 -04:00
swung0x48 50fb13430f [Merge] (MG_State, ShaderTranspiler, tools/cts): land the write-map landing and open-ended fp64 storage block fixes that take KHR-Single-GL45.subgroups to 100% on Magma 2026-09-05 05:43:34 -04:00
swung0x48 d4f8adcf6d [Tools, Test] (tools/cts, MG_Test): make the CTS runner's chunk timeout idle-based so a healthy 20-minute invocation is no longer killed and its in-flight case mis-recorded as a crash, and link MSVC test executables with /WHOLEARCHIVE so the dllimport gl* references in GetProcAddress.cpp resolve 2026-09-05 05:36:50 -04:00
swung0x48 795e08f7e6 [Fix, Test] (ShaderTranspiler): flatten fp64 storage blocks whose last member is a runtime array - the pass declined them, so the fp64 demotion re-derived ArrayStride 4 over the application's 8/16/32-byte double buffer and every double/dvecN data[] SSBO read raw words 2026-09-05 05:36:49 -04:00
swung0x48 1e7ecab4db [Fix, Test] (MG_State, BufferObject): land a non-persistent write map's staged bytes into a GPU-resident store at unmap and explicit flush instead of dropping them - SSBO binding and large-store adoption make resident stores reachable through glMapBufferRange, so every per-draw re-initialisation was silently lost 2026-09-05 05:36:48 -04:00
swung0x48 81b17c0b75 [Test] (ShaderTranspiler, Integration): pin the five reworked shapes - the read-only capture stage through the real link, the seeded carrier's ESSL declaration, the metadata-driven block redeclaration and its decline, the control-stage-less evaluation decline, and a carrier clearing an i64vec4 2026-08-28 06:22:12 -04:00
swung0x48 d1edf765f5 [Fix] (Link, Async): carry the resolved gl_PointSize capture request into the SPIR-V handoff and let a deferred verdict name its own severity - the demotion read the request off a reflection slice phase A never fills it into, so its forced carrier was dead code, and its decline reason replayed at a level no shipped build keeps 2026-08-28 06:22:12 -04:00
swung0x48 97e07190ac [Fix] (ShaderTranspiler): seed a forced point-size carrier nothing writes and decline a control stage whose live clip/cull distance would still print gl_PointSize - an ES front end deletes a never-written output, and SPIRV-Cross redeclares that block from member decorations rather than access 2026-08-28 06:22:11 -04:00
swung0x48 a4dcdf989e [Fix] (ShaderTranspiler): stop the point-size carrier landing on a location a live varying owns, and decline the evaluation stage a synthesized pass-through control stage cannot feed - an i64vec4 counted as one location, and a located input carrier trips both backends' pass-through guard 2026-08-28 06:22:11 -04:00
swung0x48 1c113e4b26 [Fix] (ShaderTranspiler): count a 64-bit integer vector as two locations in the XFB flattener's LocationSpan - it read 64-bitness off the element's float type alone, so an i64vec3/4 member packed the members after it onto locations it already owns 2026-08-28 06:22:11 -04:00
swung0x48 bf9cfb3079 [Test] (Integration): PointSizeDemotionScenario - the demoted value chain is client-invisible in both configurations, with pinned per-backend lanes and a log-latch arming guard 2026-08-28 06:22:10 -04:00
swung0x48 e1818d497a [Test] (ShaderTranspiler): pin the point-size demotion's shapes - capability stripped, carriers named and located past the program's varyings, byte-identical no-ops, whole-struct-copy decline, and the two new L1 key bits 2026-08-28 06:21:31 -04:00
swung0x48 d7f66722d1 [Fix] (ShaderTranspiler, Link, DirectGLES, DirectVulkan): demote tessellation/geometry gl_PointSize to an ordinary varying where the device cannot host the built-in - the value survives for gl_in reads and by-name capture, both backends' declines stay for shapes the pass refuses, and the verdict rides the L1 key 2026-08-28 06:21:30 -04:00
swung0x48 92dc41ebf9 [Test] (DirectVulkan, SelfTest): pin the probe's fence-timeout teardown against a fake driver, the never-worse arming refusal, and paused-span patch/instanced counting against an unpaused control 2026-08-28 06:19:41 -04:00
swung0x48 feea131d8b [Fix] (DirectVulkan, SelfTest): honour the primitives-generated probe's leak-on-timeout contract in both of its callers, count paused-span draws the frontend cannot price, refuse a substitute measured worse than the stream query, and derive the POST row's failure clause from the measurement 2026-08-28 06:19:40 -04:00
swung0x48 19f4402fbf [Test] (DirectVulkan): pin the primitives-generated probe's verdict and override mapping, and hold the reroute's GL answers and arming observable on the integration lane 2026-08-28 06:19:40 -04:00
swung0x48 1350031368 [Fix] (DirectVulkan): count GL_PRIMITIVES_GENERATED for transform-feedback-inactive draws - a bring-up probe measures the silent stream query and reroutes such draws through the dedicated primitives-generated query or a clipping-statistics pool, reported from the Vulkan POST 2026-08-28 06:19:39 -04:00
swung0x48 0ee3384b22 [Fix] (Espryt): land a SubData into an adopted store as a GPU-ordered copy - the in-place coherent write tore the frames still reading the old section bytes 2026-08-28 04:50:07 -04:00
swung0x48 ba3f8d6774 [Test] (Integration): pin the adopted mesh-arena store - cross-frame SubData visibility, readback identity, and the GPU-written readback 2026-08-28 04:19:11 -04:00
swung0x48 3327784fd0 [Fix] (Espryt): adopt mesh-arena-sized stores into coherent persistent maps at definition, and stop the flush tiers from re-synchronizing them 2026-08-28 04:19:11 -04:00
swung0x48 ff426da3a9 [Fix, Test] (MG_State, DirectVulkan): order host writes to an adopted store after recorded GPU work - a SubData issued after a dispatch landed in coherent memory before the deferred dispatch executed, so its increments overwrote the newer bytes; un-skip the DirectVulkan half of the SubData-after-dispatch scenario 2026-08-28 03:01:15 -04:00
swung0x48 08419a1fe6 [Chore] (Config): point the two stale comments at the renamed feature fields 2026-08-28 00:47:35 -04:00
swung0x48 5d51372c44 [Chore] (Config): triage backend-scoped env toggles under MOBILEGL_ESPRYT_ and MOBILEGL_MAGMA_ prefixes 2026-08-28 00:38:17 -04:00
swung0x48 7fd4550968 [Fix] (Espryt): copy before any FBO attach in the packed16 probe - attaching the array relayouts it to the plain order and was neutralizing the subject 2026-08-28 00:22:16 -04:00
swung0x48 971537058e [Fix] (Espryt): probe every allocation recipe of the packed16 shape - the Mali layout heuristic inverts between contexts, so one recipe cannot speak for the storage 2026-08-28 00:22:16 -04:00
swung0x48 dd98c450ad [Test] (SelfTest): model the params-before-upload escape so a params-first probe regression reads as a red test 2026-08-28 00:22:15 -04:00
swung0x48 5dbbbbd7eb [Fix] (Espryt): allocate the packed16 probe's textures uploads-first, the order a minted backend texture performs and the one the Mali layout choice keys on 2026-08-28 00:22:15 -04:00
swung0x48 5d140a41ce [Fix] (Espryt): re-arm the packed16 probe on the allocation-scoped mirror the device actually has and let any mirrored level trigger the widening 2026-08-28 00:22:15 -04:00
swung0x48 bf376b230f [CI] (Integration): rerun the buffer scenarios with the map flush disabled so the upload-ring tier keeps coverage 2026-08-28 00:15:15 -04:00
swung0x48 29599dcf90 [Fix] (DirectGLES): flush queued buffer ranges through a range-invalidating map first, so a partial write into a busy store is priced by the range 2026-08-27 23:54:19 -04:00
swung0x48 734fab9f90 [Test] (Integration): scope the SubData-after-dispatch readback case to DirectGLES, naming the DirectVulkan upload-ordering gap it exposed 2026-08-27 23:04:52 -04:00
swung0x48 2cd1809c29 [Fix] (Review): rebuild the packed16 probe on the CTS's three-level chains and let the POST row answer for the widening knob actually in force 2026-08-27 22:46:13 -04:00
swung0x48 faed498476 [Test] (IntegrationTest): shield the packed16 renderbuffer clear from an inherited scissor 2026-08-27 22:46:13 -04:00
swung0x48 8282eecbfa [Fix] (Espryt): store RGB565/RGB5_A1/RGBA4 as 8-bit channels where a POST probe measures the Mali packed16 array-mip field-order mirror 2026-08-27 22:46:12 -04:00
swung0x48 f4f3afb0b6 [Test] (Integration): pin that a queued SubData survives an immediate readback of a GPU-written buffer 2026-08-27 22:19:45 -04:00
swung0x48 a28da07641 [Fix] (DirectGLES): queue app buffer updates and flush them through a staged-copy upload ring instead of stalling in glBufferSubData 2026-08-27 22:19:44 -04:00
swung0x48 200c21336f [Fix] (Readback): size a cube-face pack buffer by one face, and give a cube view's face its owner layer 2026-08-27 22:05:23 -04:00
swung0x48 2c3fc583d5 [Test] (Readback): pin the one-face pixel pack buffer and a cube view's per-face layers 2026-08-27 21:58:51 -04:00
swung0x48 645a12d8bc [Fix] (Readback): read the cube face a readback names, and let glGetTextureSubImage name one 2026-08-27 21:54:47 -04:00
swung0x48 8e6acc5528 [Test] (Readback): pin that a cube map's readback answers the face it was asked for 2026-08-27 21:54:46 -04:00
swung0x48 525ffe0f14 [Test] (Review): pin the inverted override mapping, the probe's controls, and that the pinned-on lane is really armed 2026-08-27 21:06:01 -04:00
swung0x48 ad28d2b744 [Fix, Test] (Review): strip a block's member-level locations too, restore the probe's colour mask, and let the POST verdict follow the override 2026-08-27 20:15:31 -04:00
swung0x48 eab622388f [Test] (TranslationCache): pin the two interface-block location-strip flags as L2 key material 2026-08-27 20:07:49 -04:00
swung0x48 75e573c923 [Fix] (SelfTest): give the located-interface-block probe its own entry-point gate instead of the storage probe's 2026-08-27 19:56:26 -04:00
swung0x48 0d0ef13619 [Fix] (DirectGLES): run the interface-block location strip last, where its deliberately Vulkan-invalid module reaches no validator 2026-08-27 19:44:18 -04:00
swung0x48 23565fcacd [Fix, Test] (DirectGLES, SelfTest, MG_Test): probe the located-interface-block defect with its controls and cover the strip in both gates 2026-08-27 19:38:39 -04:00
swung0x48 5dc26e3e2c [Fix] (DirectGLES, ShaderTranspiler): drop the location qualifier from inter-stage interface blocks on a driver that loses their payload 2026-08-27 19:18:35 -04:00
swung0x48 90564aa82e [Test] (Review): give the geometry point-size case its own capability probe 2026-08-27 15:42:04 -04:00
swung0x48 7520607d47 [Fix] (Review): decline a point-size program the device cannot run, bound the compile-failure source dump, and correct the pass-through rationale 2026-08-27 15:42:04 -04:00
swung0x48 57635a9198 [Test] (Tessellation): unit and headless-GPU coverage for capturing a patch draw's per-vertex payload 2026-08-27 15:06:02 -04:00
swung0x48 c19d0f0b75 [Fix] (DirectVulkan): mirror gl_PointSize for capture and enable the feature its stages need 2026-08-27 15:06:02 -04:00
swung0x48 e42e7d00f5 [Fix] (DirectGLES): request the point-size extension a tessellation or geometry stage's ESSL needs 2026-08-27 15:06:01 -04:00
swung0x48 62695ee3c2 [Test] (Review): pin the single-sample sample mask, the completeness-flip sampled set, and the integer multisample placeholders 2026-08-27 13:17:56 -04:00
swung0x48 02cc0ce83c [Fix] (Review): scope the sample mask to a multisample target, version the sampled-set memo on completeness, clamp the mask word count, give the multisample placeholder every numeric domain, unwind the fixup revert one pass at a time, and bound the validator log 2026-08-27 13:03:51 -04:00
swung0x48 9e52a0b23e [Test] (DirectVulkan): pin the incomplete-default-texture draw, the unbound multisample sampler, and the unwritten redeclared gl_Position 2026-08-27 12:52:19 -04:00
swung0x48 9dee53337f [Fix] (DirectVulkan): give an unbound multisample sampler a placeholder, clear a multisample texture through a load-op pass, and plumb glSampleMaski into the pipeline 2026-08-27 12:52:19 -04:00
swung0x48 28c5badf8f [Fix] (SPIR-V): keep the entry-point interface consistent when the clip fixup and the XFB position mirror inject through gl_Position 2026-08-27 12:52:18 -04:00
swung0x48 01116f7b41 [Fix] (DirectVulkan): treat an incomplete sampled texture as unbound in the collect path too, and stop dereferencing a declined texture sync 2026-08-27 12:52:18 -04:00
swung0x48 05d627ba2d [Fix] (Xfb): never issue a transform feedback capture-point bind the application did not ask for, and error-check the driver span instead of losing it silently 2026-08-27 12:28:32 -04:00
swung0x48 ea5d52f126 [Test] (Tessellation): assert the patch-array cross-stage link outright now that the fork pin carries the guard 2026-08-27 10:52:00 -04:00
swung0x48 3c70b4fc0f [Fix] (Program): read an API colour index of zero as no override, matching the IO resolver and ProgramInterface 2026-08-27 10:52:00 -04:00
swung0x48 b6d6316333 [Fix] (Blend): decline a GL_SRC1_* factor on an incapable GLES driver even when blending is off 2026-08-27 10:51:59 -04:00
swung0x48 3c9ab5a68f [Test] (Blend): make the dual-source scenario's capability gate skip the case, not just the helper 2026-08-27 10:48:22 -04:00
swung0x48 532b5e9cc5 [Fix] (Program): let two fragment outputs share a colour number when their colour index differs 2026-08-27 10:48:22 -04:00
swung0x48 06605ed0ea [Test] (Tessellation): include the standard headers the link test uses directly 2026-08-27 10:48:21 -04:00
swung0x48 e1d5bdc4a5 [Fix] (Blend): decline an unsupported dual-source blend instead of throwing through the GL ABI 2026-08-27 10:48:21 -04:00
swung0x48 66867a41ba [Fix] (Program): count the tessellation control stage as a transform-feedback capture stage 2026-08-27 10:48:20 -04:00
swung0x48 ebff4b21f7 [Fix] (Query): tie the two tessellation pipeline-statistics targets to the GL_ARB_tessellation_shader advertisement 2026-08-27 10:48:20 -04:00
swung0x48 d4e7378868 [Chore] (ShaderTranspiler): pin glslang fork at d89cf443 (patch-array io-resize guard for the evaluation stage) 2026-08-27 10:22:37 -04:00
swung0x48 01d20e5c96 [Test] (Review): border-colour clamping, a readback across a mip gap, and blending on renderbuffer formats with no exact VkFormat 2026-08-27 08:35:06 -04:00
swung0x48 b04c67d9a8 [Test] (Review): pin the sampler getter matrix, the shadow-readback refusals, and the proxy and by-name parameter targets 2026-08-27 08:35:05 -04:00
swung0x48 685d83e3ec [Fix] (Review): bound a texture readback by the image's mip count, refuse cross-format depth/stencil copies, and probe the renderbuffer's real VkFormat 2026-08-27 08:35:05 -04:00
swung0x48 009b140691 [Fix] (Review): clamp integer and floating-point border colours to the sampled format's representable range 2026-08-27 08:35:05 -04:00
swung0x48 1f44e5bc1d [Fix] (Review): convert every scalar sampler query to the type the caller asked for instead of the other type's bit pattern 2026-08-27 08:35:04 -04:00
swung0x48 d9aebcba26 [Fix] (Review): refuse a CPU-shadow readback whose layout the pack path cannot produce, and apply the parameter-target rule to proxy and by-name spellings 2026-08-27 08:35:04 -04:00
swung0x48 0db666897e [Fix] (DirectGLES): key the border-colour sync memo on the authoritative representation, not just the float one 2026-08-27 08:35:03 -04:00
swung0x48 5f445e499f [Test] (Integration): integer border-colour sampling and clear-tex-image on a texture with no level 0 2026-08-27 08:35:03 -04:00
swung0x48 c52ebd5bf6 [Test] (Texture): pin the border-colour forms, the GL 4.6 conversion pair and the texparameter validation gaps 2026-08-27 08:35:02 -04:00
swung0x48 8e072bc793 [Fix] (DirectVulkan): answer a texture-image query from the CPU shadow when the texture has no VkImage 2026-08-27 08:35:02 -04:00
swung0x48 88ee75be0e [Fix] (DirectVulkan): resolve renderbuffer VkFormats through the shared texture table and police copy size-compatibility 2026-08-27 08:35:01 -04:00
swung0x48 0dbb4ceba8 [Fix] (DirectVulkan): resolve arbitrary and integer border colours through VK_EXT_custom_border_color, clamped to the sampled format 2026-08-27 08:35:01 -04:00
swung0x48 a94b3e0bd5 [Fix] (DirectGLES): forward an integer border colour through glTexParameterIiv/glSamplerParameterIiv instead of flattening it to float 2026-08-27 08:35:01 -04:00
swung0x48 764b6e044d [Fix] (Texture): reject illegal texparameter targets and sampler enum values, invert the integer border-colour read, and report multisample sampler state as INVALID_ENUM 2026-08-27 08:35:00 -04:00
swung0x48 c1d89de729 [Fix] (Sampler): carry GL_TEXTURE_BORDER_COLOR with its form, convert per GL 4.6 eq 2.2/2.3, and unify the name and scalar-pname error classes 2026-08-27 08:35:00 -04:00
swung0x48 c136384f97 [CI] (TraceReplay): keep the rd12-odinlite perf fixture out of the CI matrices until its archive is published 2026-08-27 08:23:04 -04:00
swung0x48 1920a3d16f [Test] (Review): pin the composite's linked capture list, the capture-stage rule, and the matrix uniform forms' link check 2026-08-27 06:00:54 -04:00
swung0x48 11f4b4bd3b [Fix] (Review): honour a SPIR-V module's transform-feedback decorations, take the composite's capture list from the linked snapshot, and reorder every glProgramUniformMatrix* link check 2026-08-27 05:56:45 -04:00
swung0x48 9ef33f4274 [Fix] (Review): bound copies by the requested level, reach every cube face, keep array layer counts, and give glSpecializeShader its spec error surface 2026-08-27 05:51:58 -04:00
swung0x48 e430e1b3be [Fix] (Texture): give glTexBuffer the sized-format check its TODO deferred and the target-taking forms their own INVALID_ENUM 2026-08-27 05:37:18 -04:00
swung0x48 e315d9e798 [Test] (GlSpirv): unit and headless-GPU coverage for glShaderBinary, glSpecializeShader and the SPIR_V_BINARY state 2026-08-27 05:37:18 -04:00
swung0x48 6f299372c6 [Feat] (Program): implement GL_ARB_gl_spirv - glShaderBinary, glSpecializeShader and the SPIR_V_BINARY state, feeding the module into the ordinary compile pipeline 2026-08-27 05:37:17 -04:00
swung0x48 be7bf21eb8 [Fix] (ShaderTranspiler): enforce the layout(binding) range rule for samplers, images and uniform/atomic-counter blocks, not only for SSBOs 2026-08-27 05:37:17 -04:00
swung0x48 0e4302b399 [Feat] (RenderState): implement glClipControl, glPolygonOffsetClamp and glTextureBarrier instead of stubbing them 2026-08-27 05:37:16 -04:00
swung0x48 c9c2dcb42a [Feat] (Query): accept the ARB_pipeline_statistics_query targets and report zero counter bits for them 2026-08-27 05:37:15 -04:00
swung0x48 2d938971b9 [Fix] (Getter): saturate combined uniform components in 64-bit and answer the missing GL4 state tokens 2026-08-27 05:37:15 -04:00
swung0x48 f7e23d5d83 [Fix] (DirectVulkan): generate mipmaps for cube-map-array and 1D-array targets, and stop shrinking an array's layer count down the chain 2026-08-27 05:37:14 -04:00
swung0x48 02fbb816e9 [Fix] (Texture): cube-map-array shape rules on glTexImage3D, GL_TEXTURE_SHARED_SIZE, and the unimplemented glCopyTexSubImage1D/3D 2026-08-27 05:37:14 -04:00
swung0x48 0e0882cfc6 [Test] (IntegrationTest): read cube faces back through a per-face FBO, which glGetTexImage's face token does not distinguish on DirectVulkan 2026-08-27 05:07:38 -04:00
swung0x48 de09646d5e [Fix] (DirectVulkan): one shared attachment layer count for the render pass and the clear key, and direct VkResult checks in the render-pass builder 2026-08-27 05:04:44 -04:00
swung0x48 747864777e [Test] (IntegrationTest): pin the layered 3D and cube-map-array attachment shapes and their per-layer routing 2026-08-27 04:37:29 -04:00
swung0x48 6fc3504bd9 [Fix] (Framebuffer): carry the layered flag through the GL_DEPTH_STENCIL_ATTACHMENT split 2026-08-27 04:37:28 -04:00
swung0x48 df1bcdba09 [Fix] (DirectVulkan): legal view types for layered 3D/cube attachments, and a real failure channel for the render-pass builder 2026-08-27 04:19:06 -04:00
swung0x48 843c61dee1 [Chore] (ShaderTranspiler): pin glslang fork at 7e255451 (GL_ARB_cull_distance registration and version-gated availability) 2026-08-27 04:01:43 -04:00
swung0x48 c0a3f4cc50 [Docs] (Getter): restate the uniform-binding airtightness argument against the per-stage sum, not the combined limit 2026-08-27 03:54:17 -04:00
swung0x48 06744fde7f [Fix] (Getter): close the review findings - six-stage combined uniform blocks, bounded uniform-block bindings, honest vertex-stream count, per-format sample ceilings 2026-08-27 03:54:16 -04:00
swung0x48 6cc9faf772 [Feat] (Program): report the geometry and tessellation link properties glGetProgramiv had no source for 2026-08-27 03:49:38 -04:00
swung0x48 7168f2ef77 [Fix] (Getter): answer the GL 4.6 limit surface honestly - tess/cull/subroutine pnames, TBuiltInResource drift, 84 UBO binding points, 64-bit GL_MAX_ELEMENT_INDEX, per-category sample truth 2026-08-27 03:47:16 -04:00
swung0x48 07669aacd4 [Fix] (ShaderTranspiler): fan #extension implications out to both new gates and bound the mid-line #version probe to its line 2026-08-27 03:35:58 -04:00
swung0x48 d52a3b2196 [Test] (IntegrationTest): sample-shading state reaches both backends without disturbing the draw 2026-08-27 03:35:58 -04:00
swung0x48 9e23016dd4 [Perf] (ShaderTranspiler): memchr the mid-line #version scan and gate it on an accepted directive 2026-08-27 03:35:57 -04:00
swung0x48 f1354dc25e [Fix] (DirectVulkan): hash the sample-shading state into the pipeline memo key 2026-08-27 03:35:56 -04:00
swung0x48 b7a694711a [Test] (IntegrationTest): gl_NumSamples scenario - the value must follow the draw framebuffer, not the link 2026-08-27 03:35:56 -04:00
swung0x48 2ae848ca19 [Fix] (ShaderTranspiler): route restored ES extension macros through glslang's custom preamble and detect a mid-line repeated #version 2026-08-27 03:35:55 -04:00
swung0x48 acf86d1fb6 [Feat] (RenderState): implement glMinSampleShading and the GL_SAMPLE_SHADING enable on both backends 2026-08-27 03:35:55 -04:00
swung0x48 07a0408a28 [Fix] (ShaderTranspiler): lower gl_NumSamples onto a reserved global-UBO uniform, restore ES preamble extension macros, tolerate a repeated #version 2026-08-27 03:35:47 -04:00
Swung0x48 8cf2e2aea9 [Fix] (Getter): answer GL_PATCH_DEFAULT_*_LEVEL from the float state in glGetBooleanv and write every component in glGetInteger64v 2026-08-27 03:18:15 -04:00
Swung0x48 31252cf0da [Fix] (Tessellation): bound the pass-through control-stage cache and stop baking "draw nothing" for levels GL clamps 2026-08-27 03:18:14 -04:00
Swung0x48 2635fe84b6 [Fix] (Tessellation): compare the default patch levels by bit pattern, so a NaN level stops re-linking the program on every draw 2026-08-27 03:18:13 -04:00
Swung0x48 e3163233a5 [Fix] (Framebuffer): apply the glFramebufferTexture error conditions to the 2D/3D/Layer attach paths and bound a view by its own level count 2026-08-27 03:18:12 -04:00
Swung0x48 eb9e4fdac1 [Fix] (DirectVulkan): report the index the arbitrary-restart rewrite cannot represent instead of silently drawing a different vertex 2026-08-27 03:18:10 -04:00
Swung0x48 90b7a689c5 [Fix] (Backend): resolve primitive restart per draw - never for a non-indexed one, and never on an index the type cannot hold 2026-08-27 03:18:09 -04:00
Swung0x48 e69e939d1a [Fix] (ProgramLink): fail the link when a tessellation control stage declares more output vertices than GL_MAX_PATCH_VERTICES 2026-08-27 02:11:23 -04:00
Swung0x48 b675e2a0b0 [Fix] (Drawing): refuse a draw whose program runs a geometry or tessellation stage with no vertex shader 2026-08-27 02:11:21 -04:00
Swung0x48 6979926a6f [Fix] (Framebuffer): the four glFramebufferTexture error conditions the DSA sibling already implemented 2026-08-27 02:11:20 -04:00
Swung0x48 d5286e69b6 [Feat] (Tessellation): implement glPatchParameterfv and bake the default levels into both pass-through control stages 2026-08-27 02:11:19 -04:00
Swung0x48 0f2fcbc469 [Fix] (Backend): honour desktop GL_PRIMITIVE_RESTART with an arbitrary index instead of throwing through the C GL ABI 2026-08-27 02:11:18 -04:00
swung0x48 18fccdd796 [Feat] (Backend): advertise OpenGL 4.6 (target version + OpenGL44/45/46 tokens) on both backends 2026-08-26 23:48:36 -04:00
swung0x48 0d2fceab0e [Docs] (DeviceBench): note session.sh leaves frequency pins active by design 2026-08-26 05:52:34 -04:00
swung0x48 82decded58 [Test] (TraceReplay): minecraft-1.21.4-rd12-odinlite-in-world perf fixture (campaign benchmark scene) 2026-08-26 05:52:33 -04:00
swung0x48 d04a3394de [Feat] (DeviceBench): launch retry loop (signal-34 JVM flake), session.sh runner, MSYS path-conversion guards 2026-08-26 05:52:32 -04:00
swung0x48 33eabfc2ff [Perf] (DirectGLES): upload the union dirty box instead of scatter rects when staging through the unpack ring (each PBO-sourced glTexSubImage is a GPU copy job on Mali; ~100 sprite rects per tick cost +6ms/frame of GPU time) 2026-08-26 05:52:31 -04:00
swung0x48 56e5d13dc9 [Perf] (DirectGLES): stage glTexSubImage uploads through a persistent-mapped unpack PBO ring (Mali blocks the render thread on in-flight destination textures) 2026-08-26 05:52:30 -04:00
swung0x48 34b7cc772f [Fix] (Build): compile fordebug native code as RelWithDebInfo (debuggable APK was shipping an -O0 libMobileGL.so, invalidating all performance measurements on this flavor) 2026-08-26 02:18:47 -04:00
swung0x48 c1d6a3c908 [Feat] (TraceReplay): benchmark mode with per-frame timing for device fixtures 2026-08-26 02:17:35 -04:00
swung0x48 386bd7e461 [Feat] (DeviceBench): scripted in-game FPS benchmark harness (FCLFPS logcat sampling, MTK ppm frequency pinning, thermal gate, GPU busy telemetry) 2026-08-26 01:51:04 -04:00
swung0x48 12df061e0b [Test] (SelfTest): model the vertex-input-location and layered-blit defects in the fake driver and pin both probes' controls 2026-08-25 05:57:58 -04:00
swung0x48 d83a48da5c [Fix] (SelfTest): read the probe-saved texture bindings off the same unit the restore puts them back on 2026-08-25 05:40:08 -04:00
swung0x48 b3100b0de5 [Fix] (DirectGLES): take the layered-blit substitute's colour destination from the draw buffer that is actually enabled, and leave self-overlapping copies alone 2026-08-25 05:38:12 -04:00
swung0x48 1cde801a01 [Fix] (DirectGLES): blit onto a non-zero array layer with glCopyImageSubData where the driver ignores the destination layer 2026-08-25 05:27:46 -04:00
swung0x48 52c050131e [Fix] (DirectGLES): advertise the vertex attributes whose layout(location) the driver's compiler will actually accept 2026-08-25 05:21:50 -04:00
swung0x48 ebb8a4cebf [Fix] (DirectGLES, DirectVulkan): answer GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT with the storage limit, not the uniform one 2026-08-25 05:21:42 -04:00
swung0x48 5398fb4289 [Fix] (IntegrationTest): report the backend that actually comes up when MOBILEGL_BACKEND_TYPE is unset
HeadlessGL reported the literal "<unset>" as the backend name when the variable was
not set, while MG_ConfigLoader::InitBackendType defaults it to DirectGLES and brought
DirectGLES up. Every ctest entry sets the variable through its ENVIRONMENT property,
which is why nothing ever noticed - but run straight from an adb shell, where nothing
sets it, the name matched neither backend and every case gated on DirectGLES skipped
as though DirectGLES were not running. On an Adreno 830 that silently disabled the
gl_ViewportIndex emulation control and the DirectGLES-only attachment scenarios.
2026-08-25 04:52:21 -04:00
swung0x48 7f2ca68615 [Test] (IntegrationTest): take the shader-compiler and viewport quirks from the environment instead of internal symbols
Android links this module against the SHIPPING libMobileGL.so on purpose, so the
on-device run validates the real artifact - and that library is -fvisibility=hidden.
Six symbols the scenarios reached for were therefore undefined and the executable
could not be linked at all: MG_Config::Features, SetAsyncShaderCompileSuspended,
ShaderCompilePool::Get/GetThreadCount/SetMaxConcurrency and
AsyncShaderCompileEnabled. All six are gone rather than exported.

CompilerThreadScope now restores the pool with glMaxShaderCompilerThreadsKHR
(0xFFFFFFFF), which MaxShaderCompilerThreadsKHR_State defines as exactly the two
steps it used to perform by hand, and AsyncAndSyncProgramsRenderIdenticalFrames
picks its two modes with the same entry point - a zero count compiles inline, a
nonzero one lifts that - so the switching is now itself under test.

ExtensionStringMatchesTheConfiguration stops deriving its expectation from
AsyncShaderCompileEnabled(), the very function the backends gate the extension
string on: it was asserting the implementation against itself and would have passed
however wrong both halves were. The expectation is now MOBILEGL_ASYNC_SHADER_COMPILE
as the process inherited it, and the case skips where that is unset because the
built-in default is a value only the implementation knows.

Both-mode coverage in one ctest run is preserved by registration rather than by
in-process forcing, and is wider than before. New entries, each APPENDING to the
common/Vulkan environment so the EGL-vendor and ICD pinning is not lost - a ctest
ENVIRONMENT property replaces the job environment rather than adding to it:
DirectGLES./DirectVulkan.AsyncOn. run all of AsyncCompileScenario with
MOBILEGL_ASYNC_SHADER_COMPILE=1; .AsyncOff. run the extension case with =0, which
asserts the withdrawn side that nothing covered before;
.OptimisticShaderStatus. run the Iris-shaped case with
MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1 (its own entries because the quirk is not
neutral for the rest of the scenario); DirectGLES.NoViewportArrayEmulation. runs the
emulation control with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0. Run from a device
shell with nothing set, the ambient configuration runs and the rest skip cleanly.

1836 -> 1853 ctest entries, all green.
2026-08-25 04:52:14 -04:00
swung0x48 b12ef4d717 [Test] (DirectVulkan): ask each unbound-descriptor case only for the limit its own resource kind needs 2026-08-25 03:44:36 -04:00
swung0x48 724755d9df [Test] (DirectVulkan): pin the image-uniform numeric domain the unbound placeholder is chosen from 2026-08-25 03:44:36 -04:00
swung0x48 59f7059bf4 [Fix, Test] (DirectVulkan): keep the draw when a program declares an image-backed resource the application left unbound 2026-08-24 05:31:15 -04:00
swung0x48 4446c861be [Fix, Test] (DirectVulkan): keep the draw when a program declares a storage block the application left unbound 2026-08-24 04:57:05 -04:00
swung0x48 602da1d131 [Merge] (GLImpl, DirectGLES, DirectVulkan): complete direct state access and the compressed-texture family 2026-08-22 22:47:08 -04:00
swung0x48 06bbaf32b1 [Merge] (GLImpl, DirectGLES, DirectVulkan): take the extension advertisements under the DSA completion 2026-08-22 22:42:47 -04:00
swung0x48 9bcf0a15a0 [Docs] (DirectGLES, DirectVulkan): correct what advertising cube map arrays actually unlocks 2026-08-22 22:03:26 -04:00
swung0x48 26e5a946ac [Test] (GLImpl): pin the DSA name rule on the compressed 3D by-name entry point 2026-08-22 21:56:51 -04:00
swung0x48 dc543fa905 [Feat, Test] (DirectGLES, DirectVulkan, SelfTest): advertise the implemented extensions that were never named 2026-08-22 21:54:17 -04:00
swung0x48 f559d68728 [Feat, Test] (GLImpl): store and patch compressed 3D texture images, and wire the 1D/3D named entry points 2026-08-22 21:49:33 -04:00
swung0x48 48968a663f [Fix, Test] (GLImpl, Util): let a buffer clear take a GL_INT pattern into a normalized format 2026-08-22 21:49:26 -04:00
swung0x48 0fdcb5d6c4 [Feat] (DirectGLES, DirectVulkan): advertise GL_ARB_sync and GL_ARB_shader_atomic_counters 2026-08-22 21:45:13 -04:00
swung0x48 6b25e7a7e3 [Fix] (GLState): report the storage flags glBufferData implies 2026-08-22 21:45:13 -04:00
swung0x48 50d260c840 [Fix] (GLImpl): reject memory-barrier bits the spec does not define 2026-08-22 21:45:13 -04:00
swung0x48 6a8bf4c03c [Fix, Test] (DirectVulkan): resolve a lowered atomic-counter block from the atomic-counter binding points 2026-08-22 21:45:13 -04:00
swung0x48 01098e9dd7 [Fix] (GLImpl): raise the errors ARB_sync specifies for ClientWaitSync, WaitSync and GetSynciv 2026-08-22 21:45:12 -04:00
swung0x48 82244e9048 [Merge] (GLState, GLImpl, DirectGLES, DirectVulkan, ShaderTranspiler): land texture views, the 4.3 version claim and the extension unlocks 2026-08-22 14:28:37 -04:00
swung0x48 7bd2f08313 [Fix] (DirectGLES): stop advertising GL_ARB_texture_view, whose functional half fails on a host driver that has EXT_texture_view 2026-08-22 13:40:35 -04:00
swung0x48 121b99f8c3 [Fix, Test] (DirectVulkan, GLImpl): size a 1D-array texture's mip chain and layered attachment by the axis that carries its layers 2026-08-22 12:52:10 -04:00
swung0x48 e8d79344d6 [Fix] (DirectVulkan): resolve a cube-map-array sampler to the cube-map-array texture target 2026-08-22 12:47:13 -04:00
swung0x48 88be35c9ba [Fix, Test] (Util): widen a three-channel 16-bit texture to its same-width four-channel sibling 2026-08-22 12:41:18 -04:00
swung0x48 60808b6cf2 [Feat] (DirectGLES, DirectVulkan): advertise base-vertex draws, instanced arrays, KHR_debug and the 4.1-4.3 version tokens 2026-08-22 12:41:18 -04:00
swung0x48 eb2f14e55a [Fix, Test] (GLImpl): validate count, instancecount, drawcount, type and range on the base-vertex draw family 2026-08-22 12:41:18 -04:00
swung0x48 0eb5d54bb8 [Fix, Test] (GLState, ShaderTranspiler): report GL's default binding of zero for an unqualified uniform block 2026-08-22 12:41:18 -04:00
swung0x48 6a2e9dc791 Merge branch 'target-gl43' of /mnt/c/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL into gl43-claim 2026-08-22 11:42:58 -04:00
swung0x48 d5aceebd7b Merge branch 'dev' of /mnt/c/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL into gl43-claim 2026-08-22 11:42:50 -04:00
swung0x48 473d9951b7 [Fix, Test] (DirectVulkan, GLState): apply a texture view level and layer window at every subresource boundary 2026-08-22 11:17:09 -04:00
swung0x48 13783e3aec [Feat] (DirectGLES, DirectVulkan): raise the advertised OpenGL target version to 4.3 2026-08-22 10:53:45 -04:00
swung0x48 6bb844b1c1 [Feature, Test] (GLImpl): give KHR_debug a real group stack and object labels 2026-08-22 10:42:49 -04:00
swung0x48 6162603072 [Feature, Fix, Test] (GLState, GLImpl, DirectVulkan, DirectGLES): implement glTextureView over shared texture storage 2026-08-22 10:41:52 -04:00
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
RISC-1145 ee124018a2 [Fix] (git) Added the ignored item .gradle 2026-08-20 21:46:01 +08:00
RISC-1145 d7ce0c48ef [Fix] (Cmake) Fixed the issue of including the header files of the ASIO library 2026-08-20 21:44:12 +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
swung0x48 eadf7bc474 [Fix] (TraceReplay): import iterationRP repair flags in the desktop CLI
Initialize the desktop replay Request from the three iterationRP environment flags before loading MobileGL. Without this, the Request defaults caused the replay core to unset CI's exported flags, leaving all repairs disabled despite the workflow configuration.
2026-08-20 00:42:34 -04:00
swung0x48 00a326ef78 [CI] (MG_IntegrationTest): enable iterationRP repairs in the integration gate
Run the lavapipe Program 203 golden test with the same subgroup scratch, derived topology, and missing-barrier repairs as the iterationRP retrace matrix. This keeps the prerequisite integration job from failing before retrace jobs can start.
2026-08-20 00:03:52 -04:00
swung0x48 c09045fe59 [Fix, Test] (DirectVulkan, ShaderTranspiler, TraceReplay): repair iterationRP's missing reduction barrier
Program 203 reuses prefixSumCache for a second subgroup reduction before every workgroup invocation has consumed the first result. Add a fingerprint-gated SPIR-V pass that inserts the missing Workgroup acquire-release barrier while preserving native subgroup operations.

Keep the repair opt-in behind MOBILEGL_ITERATIONRP_FIX_BARRIER, cover insertion, pass-through, and idempotence, and enable it together with the existing iterationRP subgroup repairs for the matching Linux and Android CI retraces.
2026-08-19 23:42:17 -04:00
swung0x48 5bd8ef01e5 [Test] (MG_IntegrationTest): pin Program 203's complete golden output across desktop and Android
Add a deterministic iterationRP Program 203 fixture that dispatches the original shader and compares every RG16F texel against fixed half-float golden bits. This catches both a wrong exposure result and collateral writes without retaining a serial reference shader.

Make MobileGLIntegrationTest runnable as a standalone Android executable by linking the shared MobileGL library and backing EGL with an AImageReader window; desktop keeps its static-library pbuffer path.

Validation: Adreno 830 passes with 0/262656 mismatches; lavapipe reproduces the current reduction defect with 1/262656 mismatches at the exposure texel.
2026-08-19 22:30:30 -04:00
swung0x48 3181ed2c5a [Fix] (DirectGLES): repair the emulation-guard mask restore and scissor the resolve fallback's staging blit correctly
Final audit round over the DirectGLES scratch/shadow mechanisms; three verified
defects fixed:

- ~ScopedEmulationDrawState restored the APPLICATION's per-buffer colour masks,
  not what SyncRenderState actually pushed: a widened attachment's alpha-off
  doctoring (g_syncedColorMaskAlphaWidenMask) was dropped while the memo still
  claimed it applied, so the next sync early-outed and draws wrote fragment
  alpha into the widened buffer - breaking the stored-alpha==1.0 invariant the
  widen discipline exists to protect. The restore now re-applies the doctoring.
- The same restore loop gated on the core glColorMaski name only, while the sync
  push falls back to glColorMaskiEXT/OES: EXT/OES-only devices were left holding
  buffer 0's mask broadcast across every draw buffer with the shadow recording
  the divergent set (never repaired). The restore now uses the same three-way
  pointer fallback.
- ResolveThenBlit ran its resolve-into-scratch staging blit under the
  application's scissor: a box not covering the scratch-origin rect clipped the
  resolve silently (no GL error), and the second blit then copied stale scratch
  renderbuffer texels into the destination. The staging blit now runs scissor-off
  (shadow-tracked, like ScopedScissorDisable); the caller-visible blit keeps its
  native scissor semantics.
2026-08-19 16:41:31 -04:00
swung0x48 6aed3b08f3 [Fix] (DirectVulkan, GLImpl, MG_State, ShaderTranspiler): second audit round over the remaining memo sites
Six more verified defects from the residual memo/cache mechanisms:

- Program resource cache (DirectVulkan reflection): glShaderStorageBlockBinding
  deliberately does not bump the backend state version, and the SSO pipeline
  composite is unnamed so the by-name in-place patch can never reach its slot -
  the composite kept serving pre-rebind SSBO bindings. The cache now keys on the
  program's block-binding version; a binding-only change re-applies the overrides
  by name instead of re-running spirv-reflect. SetShaderStorageBlockBinding also
  gains the equality bail-out its uniform-block sibling has, so the composite
  mirror's replay stops churning the version every draw.
- LinkProgram's allowVSOnlyPrograms function-static latch never set its own
  initialized flag (dead memo, re-read every call) - and completing it would have
  frozen a per-backend capability across re-initialization. Replaced with a fresh
  per-link read from the null-checked active backend.
- Query object registry: drained at full library teardown (DestroyAllQueryObjects,
  mirroring DestroyAllSyncObjects) - undeleted queries and their backend wrappers
  leaked across Destroy/Initialize cycles, stale ids stayed IsQuery == GL_TRUE in
  the re-initialized library, and a later delete could hand the old backend's
  wrapper to a different backend's DeleteBackendQuery.
- Converted vertex streams and the host-side EBO max-index scan now SyncGpuWrites
  before reading the coherent mapping: XFB/SSBO/image writes are merely recorded
  at that point, so the conversion read pre-write bytes (the restart-index
  rewrite already synced; these two host reads did not).
- Zero-stride converted bindings: both converters rejected stride 0, making the
  factory's documented single-element conversion unreachable and silently
  dropping every draw using such a binding; the stride is substituted with the
  element size for the one-element case.
- DemoteFloat64Pass block relayout: measurement queued into the module eagerly,
  so a mid-struct failure left a half-relaid-out block (compacted offsets before
  the failing member, 64-bit offsets after) while claiming the block was left
  alone. Decoration writes are now collected and committed only when the whole
  block measures successfully.
2026-08-19 16:41:28 -04:00
swung0x48 281467a345 [Fix] (DirectGLES, DirectVulkan, MG_State): close stale-cache, A-B-A and state-leak holes across the memo layers
Audit of every memoization implementation; sixteen verified defects fixed:

DirectGLES backend:
- Broadcast draw-buffer memo: cleared at MakeCurrent/DestroyEGLContext like its
  sibling shadows; its identity+version key is only monotonic within one GLContext,
  so a library teardown + re-init could false-hit on a recycled FBO address.
- Backend texture id re-mint (RecreateBackendTexture) now bumps an attachment
  generation that the SyncCurrentFBO gate and every FBO twin compare, so driver
  FBOs re-attach instead of keeping the deleted texture name; the attachment walk
  re-enters until the generation is quiescent (a walk itself can re-mint).
- Buffer id re-mint (persistent-map adoption, immutable-store retire) now bumps a
  generation the VAO twin sync compares, forcing a full re-emit of the baked
  glVertexAttribPointer / element-array bindings that frontend versions cannot see.
- VAO element-array sync memo: bound-object identity joins the wrapping Uint16
  slot version (same pairing the ResolvedDrawBuffers IBO memo already uses).

DirectVulkan backend:
- EBO slice memo gains the mapped-buffer guard its vertex-binding sibling has: a
  shadow-backed persistent map mutates with no epoch bump, so a hit must decline.
- VkClearManager::MergeClearPayload keeps colorEncoding/colorInt/colorUint with
  the color, so deferred glClearBufferiv/uiv no longer degrade to all-zero float.
- GetOrCreateComputePipeline no longer memoizes a failed creation (same contract
  as PipelineFactory): a transient driver failure was permanently disabling every
  dispatch of that program.
- Explicit-LOD-0 verdict memo keys on the sampling-resolution generation; sampler
  filter/aniso/LOD setters bump only that counter, so the old key served a stale
  verdict (wrong SPIR-V variant) after glTexParameter/glSamplerParameter changes.
- SetupDraw fast path declines instead of re-arming on a moved sampling-resolution
  generation (the snapshot bakes the LOD verdict into its pipeline), and
  recomputes the XfbCapture bit so the first draw after glBeginTransformFeedback
  cannot bind the undecorated variant and silently capture nothing.
- VertexInputStateFactory eviction epoch is drawn from a process-wide source: VAO
  state-pointer memos outlive the factory across renderer recreation, and a fresh
  factory restarting at epoch 1 would dereference a dead factory's entry.
- Cached render passes re-read the live renderbuffer clear payload at begin (the
  clear VALUE is not in the pass hash; the entry's inline snapshot replayed the
  creation-time color and dropped the newly queued one).
- FramebufferObject gains a never-reused lifetime id, keyed into the render-pass
  fast-path memo and the SetupDraw snapshot beside the raw pointer + Uint16
  version pair, which address reuse plus fresh version counts could equal.
- SyncTextureResource's preserved-content image goes through the deferred-release
  ring on both failure paths instead of a synchronous destructor under the GPU.

MG_State frontend:
- Layer-1 compile memo is env-disciplined like layers 2/3: a node computed against
  a dead CompileEnv (e.g. pre-capability fallback limits) no longer answers
  glCompileShader forever once the environment's content changes.
- Pipeline composite cache rebuilds from each stage program's last-link shader
  snapshot (new LinkedShaderRef list + pinned link inputs) instead of the live
  attach list and current compile nodes: post-link glAttachShader/glCompileShader
  must not leak into the composite while the (lifetimeId, linkVersion) signature
  still hits - GL's "as last linked" rule.
2026-08-19 16:41:26 -04:00
swung0x48 c7e36986e7 [Fix] (ShaderTranspiler, DirectVulkan, MG_IntegrationTest): patch both of iterationRP's under-declared subgroup scratch arrays
The previous commit's fingerprint was pinned to one array's incidental
dimensions - workgroup exactly 32x16x1, element exactly vec2, length
exactly 32 - which is the auto-exposure reduction and nothing else. The
pack ships the same idiom twice:

  - auto-exposure:  32x16 (512 invocations), shared vec2 prefixSumCache[32]
  - RTW warp:       1024 invocations,        shared float prefixSumCache[64]

so the warp kept writing 128 subgroups into 64 entries on an 8-lane
device and the retrace stayed bit-identically wrong (ssim 0.027902).

Key the fingerprint on the pack's idiom instead of one array's shape: a
workgroup array of 32-bit floats indexed by gl_SubgroupID, fed by a
subgroup scan, whose declared length is below ceil(invocations / native
width). Three properties keep that a targeted repair rather than a
general array resizer:

  - the index must BE gl_SubgroupID (through OpCopyObject, a signedness
    OpBitcast, or a spill whose every store is that id), so an index
    masked or clamped into range is left alone;
  - the >= 16-lane early-out is retained, so every module on the devices
    the pack was written for passes through byte-identical;
  - growth is certified against maxComputeSharedMemorySize using a
    natural-alignment layout model, and declined outright when a
    declaration cannot be sized, so a patched module can never fail
    pipeline creation where the original would not have.

Verified against the shaders the CI trace actually contains: of the 14
compute modules in the fixture exactly these two change, the other
twelve are byte-identical, and all fourteen pass spirv-val. The
integration scenario grows a second case for the 1024-invocation shape;
both abort with heap corruption when the patch is disabled.
2026-08-19 16:36:06 -04:00
swung0x48 d8576a2ed3 [Fix] (DirectVulkan, ShaderTranspiler, MG_IntegrationTest, SelfTest, TraceReplay): use native subgroups and patch iterationRP's under-declared scratch
iterationRP's Program 203 declares shared vec2 prefixSumCache[32] for a
512-invocation workgroup indexed by gl_SubgroupID; any device narrower
than 16 lanes partitions into more than 32 subgroups and the pack writes
shared memory out of bounds (heap corruption on lavapipe's CPU
rasterizer, ssim 0.028 on the CI retrace). Fix it where the fault lies -
in the fixture - and keep the GL contract sound everywhere else:

- FixIterationRPSubgroupScratchPass: fingerprint-gated SPIR-V pass that
  grows exactly that array to ceil(invocations/width) entries on sub-16-lane devices; every other module passes through byte-identical.
- DeriveNumSubgroupsPass stays default-on for the Adreno topology bug
  and is made spec-sound: pipelines request REQUIRE_FULL_SUBGROUPS
  whenever the workgroup shape makes the flag legal (computeFullSubgroups
  enabled, local_size_x a multiple of the native width, subgroup count
  within maxComputeWorkgroupSubgroups).
- EmulateSubgroupsPass: 32-lane virtual-subgroup lowering kept in-tree
  as a last resort, enabled only by MOBILEGL_MAGMA_EMULATE_SUBGROUP=1 on
  devices with no native subgroup support; fails closed on extended
  subgroup instructions and on modules whose added scratch would exceed
  maxComputeSharedMemorySize.
- IterationRPFirstReductionScenario skips gracefully outside the pack's
  16..256-lane source domain; the new IterationRPScratchFixScenario runs
  the fixture-shaped reduction on any width and asserts the exact
  width-independent total. DriverPost keeps reporting FAIL on
  out-of-domain devices.
- Program203 -> IterationRP rename throughout; the per-trace
  num_subgroups_quirk plumbing is removed from the trace replayer, JNI
  chain, and CI workflows.
2026-08-19 09:48:11 -04:00
swung0x48 2b6c2b561c [Fix, Test] (DirectVulkan, ShaderTranspiler, TraceReplay): derive NumSubgroups behind opt-in quirk 2026-08-18 22:31:57 -04:00
swung0x48andClaude Fable 5 12c94111b5 [Fix, Test] (DirectVulkan, SelfTest, MG_IntegrationTest, TraceReplay): snapshot sampler/image feedback and add Program 203 diagnostics
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 03:04:09 -04:00
swung0x48 7769156cfc [Fix, Test] (ShaderTranspiler, WGL, TraceReplay): remove subgroup pack quirks and tune iterationRP 2026-08-17 01:55:17 -04:00
swung0x48 0ecfdff4e7 [Fix, Test] (MG_State, MG_Util, DirectVulkan, MG_Test): replay narrow-subgroup reductions correctly 2026-08-16 13:33:01 -04:00
swung0x48 6df5a6137f [CI] (trace-replay): run iterationRP only on DirectVulkan 2026-08-16 11:09:56 -04:00
swung0x48 b3794f4e6a [Feat] (MG_Impl, MG_State, MG_Util, DirectGLES, DirectVulkan, MG_Test): implement ARB_clear_buffer_object correctly 2026-08-16 01:12:28 -04:00
swung0x48 14d3901d30 [Chore, Test] (MG_State, MG_Util, DirectGLES, DirectVulkan): make SPIR-V validation task-local 2026-08-15 22:58:28 -04:00
swung0x48 d4766513e4 [Fix, Test] (DirectVulkan): replay Photon descriptor pressure correctly 2026-08-15 22:35:39 -04:00
swung0x48 72dc7aa6aa [Chore] (MG_Config, MG_State, MG_Util): gate SPIR-V validation at startup 2026-08-15 22:35:39 -04:00
swung0x48 9d1b280375 [Fix, Test] (MG_Util, MG_Test): rename Photon-conflicting MSL identifiers 2026-08-15 09:40:13 -04:00
swung0x48 8acd885594 [Fix, Test] (DirectVulkan): preserve viewport-index program metadata 2026-08-15 09:40:13 -04:00
swung0x48 10ff5e2b18 [Fix, Test] (DirectGLES, DirectVulkan): advertise indirect draw capabilities accurately
- advertise GL_ARB_draw_indirect when supported
- gate GL_ARB_base_instance on complete non-zero firstInstance semantics
- synchronize Driver POST reporting
- add capability and extension-advertisement regression tests
2026-08-15 06:30:44 -04:00
swung0x48 a6e52476f3 [Chore] (CMake): skip embedded SPIRV-Tools executables 2026-08-15 05:48:41 -04:00
swung0x48 0deff52a1b [Fix, Test] (DirectVulkan, trace-replay): replay quarter-turn surfaces correctly 2026-08-13 06:45:28 -04:00
swung0x48 50fefca959 Merge branch "feat/cts-viewport-array" into dev 2026-08-13 04:57:40 -04:00
swung0x48 42ad62b54c [Fix, Test] (MG_Util, MG_IntegrationTest): advertise the GL 4.3 VIEWPORT_BOUNDS_RANGE floor on a GLES driver that has no such query, instead of a range admitting no origin 2026-08-13 04:44:23 -04:00
swung0x48 f41403e227 [Feat, Test] (MG_Backend/DirectVulkan, MG_Test, MG_IntegrationTest): rasterize the viewport gl_ViewportIndex selects, instead of collapsing all sixteen onto viewport 0 2026-08-13 04:44:23 -04:00
swung0x48 5fbb17f6b9 [Feat, Fix, Test] (MG_State, MG_Impl, MG_Backend, MG_Test): give ARB_viewport_array real 16-element indexed state instead of eight stubs and a viewport-0 echo 2026-08-13 04:44:23 -04:00
swung0x48 92d8f7269b [Fix] (MG_IntegrationTest): drop the executable bit a copied-in scenario file carried 2026-08-13 04:29:23 -04:00
swung0x48 822e405c77 Merge branch "feat/vk-barrier-layer-ranges" into dev 2026-08-13 04:28:57 -04:00
swung0x48 b8233f9c4e [Fix, Test] (MG_Backend/DirectVulkan, MG_IntegrationTest): the barrier before every attachment transfer only ever moved layer 0, so a copy off a non-zero layer read a layout nothing had transitioned 2026-08-13 04:01:20 -04:00
Swung0x48 91475a7b6f Merge branch feat/cts-copyimage-frontend into dev 2026-08-13 03:59:16 -04:00
352 changed files with 94810 additions and 4924 deletions
+3 -3
View File
@@ -44,12 +44,12 @@ require 'key:MOBILEGL_BACKEND_TYPE' "$plugin_resource_text" 'V2 backend variable
require 'defaultValue:DirectGLES' "$plugin_resource_text" 'V2 DirectGLES default'
require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle'
require 'key:MOBILEGL_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
require 'key:MOBILEGL_MAGMA_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle'
require 'key:MOBILEGL_MAGMA_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting'
require 'key:MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
require 'key:MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
require 'key:MOBILEGL_COHERENT_AS_FLUSH' "$plugin_resource_text" 'V2 coherent-as-flush toggle'
require 'key:MOBILEGL_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
require 'key:MOBILEGL_ESPRYT_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
if [[ $(grep -Fc 'fclPlugin_V2' <<<"$plugin_manifest") -ne 1 ]]; then
echo '::error::Plugin manifest must expose exactly one V2 descriptor' >&2
+6 -9
View File
@@ -209,7 +209,7 @@ jobs:
- name: Load trace cases
id: trace-cases
run: |
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk)" >> "$GITHUB_OUTPUT"
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk-matrix)" >> "$GITHUB_OUTPUT"
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
trace-fixtures:
@@ -337,13 +337,7 @@ jobs:
strategy:
fail-fast: false
max-parallel: 4
matrix:
backend:
- name: DirectGLES
gpu: software
- name: DirectVulkan
gpu: lavapipe
case: ${{ fromJSON(needs.trace-cases.outputs.android) }}
matrix: ${{ fromJSON(needs.trace-cases.outputs.android) }}
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@v1.0
@@ -423,9 +417,12 @@ jobs:
- name: Retrace and validate
env:
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
MOBILEGL_ESPRYT_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
run: |
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
test -f "${apk_file}"
+24 -6
View File
@@ -265,13 +265,26 @@ jobs:
# crash stack without burning a CI round on an in-workflow debugger.
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: "1"
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: "1"
run: |
ulimit -c unlimited
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
# Second, filtered pass: with the range-invalidating map flush disabled,
# the buffer scenarios run on the upload ring's staged-copy tier - which
# the default pass never reaches (the map tier absorbs every flush on
# Mesa), so without this the Mali fallback tier would have zero CI
# coverage. The flag is NOT baked into the ctest ENVIRONMENT properties,
# so an inline env reaches the test processes (unlike the ICD pin above).
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
ctest -V -L integration-gpu --no-tests=error
MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1 ctest -V -L integration-gpu \
-R 'Buffer|Readback|Atomic|Ssbo|Arena' --no-tests=error
else
ctest --output-on-failure -L integration-gpu --no-tests=error
MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1 ctest --output-on-failure -L integration-gpu \
-R 'Buffer|Readback|Atomic|Ssbo|Arena' --no-tests=error
fi
- name: Upload core dumps
@@ -491,6 +504,7 @@ jobs:
- benchmark
- integration
outputs:
matrix: ${{ steps.trace-cases.outputs.matrix }}
names: ${{ steps.trace-cases.outputs.names }}
steps:
- name: Checkout repo
@@ -498,7 +512,9 @@ jobs:
- name: Load trace cases
id: trace-cases
run: echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
run: |
echo "matrix=$(python3 tools/trace_replay/trace_cases.py --ci --format github-test-matrix)" >> "$GITHUB_OUTPUT"
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
trace-fixtures:
name: trace fixture (${{ matrix.case }})
@@ -577,11 +593,7 @@ jobs:
strategy:
fail-fast: false
max-parallel: 4
matrix:
backend:
- DirectGLES
- DirectVulkan
case: ${{ fromJSON(needs.trace-cases.outputs.names) }}
matrix: ${{ fromJSON(needs.trace-cases.outputs.matrix) }}
steps:
- name: Set Swap Space
@@ -640,6 +652,12 @@ jobs:
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
fi
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
&& [ '${{ matrix.case }}' = 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' ]; then
export MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1
export MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1
export MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1
fi
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
# runner has, so force it on for the OIT case it exists to fix. ForceOn
# bypasses only the vendor gate, so this exercises the real strip on
+1
View File
@@ -27,3 +27,4 @@ MobileGL/MG*/cmake-build*
tools/trace_replay/work/
__pycache__/
*.py[cod]
/.gradle
+31 -1
View File
@@ -182,6 +182,7 @@ set(ENABLE_SPVREMAPPER OFF CACHE BOOL "Enable SPVRemapper" FORCE)
set(ENABLE_OPT ON CACHE BOOL "Enable SPIRV-Tools opt usage in glslang" FORCE)
set(BUILD_EXTERNAL ON CACHE BOOL "Build external deps in External/" FORCE)
set(ENABLE_GLSLANG_INSTALL OFF CACHE BOOL "Install glslang targets" FORCE)
set(SPIRV_SKIP_EXECUTABLES ON CACHE BOOL "Skip building SPIRV-Tools executables" FORCE)
set(SPIRV_CROSS_C_API ON CACHE BOOL "Enable C API" FORCE)
set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE)
@@ -269,6 +270,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
@@ -277,26 +279,44 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.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/LowerViewportIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripIoBlockLocationsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.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/WidenImageFormatsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.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/ShaderTranspiler/SpirvPasses/DemotePointSizePass.cpp
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp
MobileGL/MG_Util/SelfTest/DriverPost.cpp
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
@@ -319,6 +339,7 @@ set(SOURCE_FILES
MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.cpp
MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
MobileGL/MG_Impl/GLImpl/Texture/ProxyTexture.cpp
MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp
@@ -376,10 +397,12 @@ set(SOURCE_FILES
MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp
MobileGL/MG_State/GLState/TextureState/TextureObject3D.cpp
MobileGL/MG_State/GLState/TextureState/TextureObjectBuffer.cpp
MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp
MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramObject.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/ShaderCompileTask.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
@@ -457,7 +480,7 @@ set(MOBILEGL_INCLUDE_DIR
# Header-only submodule: no add_subdirectory, no link target. Only
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
# pimpl so no consumer target needs this path.
${CMAKE_SOURCE_DIR}/3rdparty/asio/asio/include
${CMAKE_SOURCE_DIR}/3rdparty/asio/include
)
add_library(${CMAKE_PROJECT_NAME} SHARED
@@ -669,3 +692,10 @@ if (NOT ANDROID)
add_subdirectory(tools/trace_replay)
endif()
endif()
# The integration binary is also useful as a standalone adb-shell executable.
# Android cannot use the desktop-only MobileGL_s target, so its CMake module
# links libMobileGL.so and creates an AImageReader-backed window instead.
if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
add_subdirectory(MobileGL/MG_IntegrationTest)
endif()
+161 -21
View File
@@ -66,22 +66,60 @@ namespace MobileGL::MG_Config {
// - DISPLAY: X11 session variable, not MobileGL configuration.
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
// (see MG_Util/Debug/Log.cpp).
// - MOBILEGL_VALIDATE_SPIRV: test suites like SpirvPassTest exercise
// ShaderCompiler without ever running MobileGL::Initialize(), and every
// Initialize() re-runs MG_ConfigLoader::Init, which would clobber a
// programmatic override stored here (see ShaderCompiler.cpp,
// SpirvValidationEnabled).
struct FeaturesTable {
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
Bool DisableTimerQuery = false;
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
Bool UseAngle = false;
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW: advertise GL_ARB_texture_view on DirectGLES when
// the host ES driver has EXT/OES_texture_view. Off by default: the host extension is
// present on Adreno 830 and the functional half of KHR-GL4{2,3}.texture_view still fails
// there, because the view's ES internalformat is normalized independently of the storage
// it aliases (see BackendObject_DirectGLES::BuildAdvertisedExtensions). The flag exists
// so that work can be done without editing the gate.
Bool EsprytEnableTextureView = false;
// MOBILEGL_ENABLE_SPIRV_VALIDATION: validate generated and transformed SPIR-V.
// Disabled by default because validation is a diagnostics-only cost.
Bool EnableSpirvValidation = false;
// MOBILEGL_ESPRYT_USE_ANGLE: load ANGLE EGL/GLES libraries.
Bool EsprytUseAngle = false;
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
String TraceAngleVariant;
#endif
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
Bool DisableSubgroup = false;
// MOBILEGL_MAGMA_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
// including the opt-in emulated compute path below.
Bool MagmaDisableSubgroup = false;
// MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
// engages when this flag is set AND the device has no native subgroup support at
// all - a device with real subgroup operations always uses them natively,
// whatever their width (the known iterationRP defect is patched by
// MagmaFixIterationRPSubgroupScratch below instead). Off by default.
Bool MagmaEmulateSubgroup = false;
// MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
// pack declares `shared vec2 prefixSumCache[32]` for a 512-invocation exposure
// reduction and indexes it by gl_SubgroupID, so any device with sub-16-lane
// subgroups (8-lane lavapipe -> 64 subgroups) writes shared memory out of
// bounds. The pass grows that one array to what the device's topology needs and
// touches nothing else; it only rewrites modules positively matching the pack's
// reduction fingerprint (ShaderTranspiler::FixIterationRPSubgroupScratchPass),
// so every other shader passes through byte-identical - as does iterationRP
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
// verbatim.
QuirkOverride MagmaFixIterationRPSubgroupScratch = QuirkOverride::Auto;
// MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
// rendezvous between its two reductions over prefixSumCache. Off by default and
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
Bool MagmaIterationRPFixBarrier = false;
// MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
// gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
// dispatch emits IDs 0..7, and the derived value is the one Vulkan guarantees
// whenever the pipeline can request REQUIRE_FULL_SUBGROUPS (which the renderer
// does whenever local_size_x is a multiple of the native width). ForceOff returns
// to the raw driver builtin.
QuirkOverride MagmaDeriveNumSubgroups = QuirkOverride::Auto;
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
@@ -94,16 +132,39 @@ namespace MobileGL::MG_Config {
Bool MagmaR11G11B10FFallback = false;
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
Uint32 MagmaFramesInFlight = 3;
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
// resolves certain rendering bugs on ANGLE + llvmpipe.
Bool AvoidSamplerMipmapMinFilter = false;
// MOBILEGL_AVOID_EXPLICIT_LOD_BIAS: leave an already-explicit LOD argument alone when
Bool EsprytAvoidSamplerMipmapMinFilter = false;
// MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS: leave an already-explicit LOD argument alone when
// emulating GL_TEXTURE_LOD_BIAS, instead of adding the bias uniform to it. Injecting
// the uniform turns a compile-time-constant LOD into a runtime expression, which
// sends ANGLE + llvmpipe down a mip-selection path that dereferences a NULL
// descriptor and kills the process. Deviates from spec (Vulkan adds the bias to
// OpImageSampleExplicitLod), so it is an avoidance for that stack only.
Bool AvoidExplicitLodBias = false;
Bool EsprytAvoidExplicitLodBias = false;
// MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS: emit a tessellation/geometry program's
// inter-stage interface blocks WITHOUT their layout(location=) qualifier, letting ES
// match them by block name and member sequence instead. The Mali ES driver delivers
// nothing at all through a located block once a tessellation or geometry stage is in
// the pipeline; the driver POST measures that and turns this on by itself, so Auto is
// the right setting everywhere. ForceOn exists so the emulation can be exercised on a
// healthy driver - which is what the integration lane does, since llvmpipe and
// lavapipe carry a located block correctly and would otherwise never run this code -
// and ForceOff is the negative control. See StripIoBlockLocationsPass.
QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto;
// MOBILEGL_POINT_SIZE_DEMOTION: demote gl_PointSize out of tessellation/geometry
// stages into an ordinary varying (ShaderCompiler::
// DemoteTessellationGeometryPointSizeForProgram) instead of declining such programs
// on a device that advertises neither EXT/OES_tessellation_point_size /
// geometry_point_size (DirectGLES) nor shaderTessellationAndGeometryPointSize
// (DirectVulkan). Auto arms it exactly where the detection says the capability is
// absent, which is the right setting everywhere. ForceOn exists so the demotion can
// be exercised on a healthy driver - llvmpipe and lavapipe host the built-in
// natively and would otherwise never run this code, which is what the pinned
// integration lane uses - and ForceOff restores the plain declines (escape hatch /
// negative control). Cross-backend by design: the demotion runs in the shared
// phase-B chain, so one switch covers both. See DemotePointSizePass.
QuirkOverride PointSizeDemotion = QuirkOverride::Auto;
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
@@ -113,10 +174,38 @@ namespace MobileGL::MG_Config {
Bool CoherentAsFlush = false;
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
Bool TraceSkipAutodestroy = false;
// MOBILEGL_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
// MOBILEGL_ESPRYT_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
// (negative control / driver-bug escape hatch).
Bool DisableUboRing = false;
Bool EsprytDisableUboRing = false;
// MOBILEGL_ESPRYT_DISABLE_UNPACK_RING: force DirectGLES texture uploads back to
// glTexSubImage from the client pointer instead of staging them through the
// persistent-mapped unpack-PBO ring (negative control / driver-bug escape
// hatch).
Bool EsprytDisableUnpackRing = false;
// MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING: force DirectGLES app buffer updates
// (glBufferSubData / map flushes) back to the immediate driver upload instead
// of queueing them for the staged-copy flush through the persistent-mapped
// upload ring (negative control / driver-bug escape hatch; the immediate
// upload stalls on drivers that resolve the WAR hazard on the CPU, e.g. Mali).
Bool EsprytDisableUploadRing = false;
// MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH: skip the glMapBufferRange(WRITE |
// INVALIDATE_RANGE) tier of the DirectGLES pending-range flush and go straight
// to the upload ring's staged glCopyBufferSubData (negative control / escape
// hatch for a driver whose range-invalidating map misbehaves). The map tier is
// what keeps a partial write into a large in-flight buffer priced by the RANGE:
// on Mali both the immediate glBufferSubData and a staged copy into a busy
// mutable store ghost the whole destination on the CPU.
Bool EsprytDisableInvalidateFlush = false;
// MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION: keep mesh-arena-sized buffer stores
// (>= 16MiB) on the CPU-shadow model instead of backing them with the backend's
// persistently+coherently mapped storage at definition time (negative control /
// escape hatch). Frontend-scoped: it engages only where the active backend
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
// 128MB arena is a plain memcpy into GPU-visible memory; every driver-mediated
// route for the same write stalls the thread or ghost-copies the whole arena on
// this class of Mali driver, and the arena stops costing its size again in RAM.
Bool DisableLargeBufferAdoption = false;
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
@@ -130,10 +219,6 @@ namespace MobileGL::MG_Config {
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
// version request.
Bool RelaxedSemantics = false;
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
@@ -141,10 +226,10 @@ namespace MobileGL::MG_Config {
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
// MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
// feature off. It is enabled by default to match GL's defined out-of-range fetch
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
Bool DisableRobustBufferAccess = false;
Bool MagmaDisableRobustBufferAccess = false;
// MOBILEGL_MAGMA_MULTIDRAW_MODE: preferred DirectVulkan multi-draw dispatch tier
// ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support;
// unset picks the best supported tier.
@@ -176,6 +261,61 @@ namespace MobileGL::MG_Config {
// immediately stay serial by their own construction). Off by default; never
// advertise it.
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_ESPRYT_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 EsprytViewportArrayEmulation = QuirkOverride::Auto;
// MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE: DirectGLES stores GL_RGB565/GL_RGB5(A1)/GL_RGBA4
// images as 8-bit-per-channel ES storage (GL_RGB8/GL_RGBA8) instead of the driver's
// native 16-bit packed formats. Auto defers to a POST driver-bug probe
// (SelfTest::CopyImageMirrorsPacked16FieldOrder): some Mali drivers store SOME
// packed16 allocations with a MIRRORED field order (allocation-scoped and
// shape/context dependent - the failing 30x30x12 GL_TEXTURE_2D_ARRAYs are mirrored
// at every level), so glCopyImageSubData - a raw texel-block move - lands R/G/B/A
// reversed whenever exactly one endpoint sits in a mirrored allocation
// (KHR-GL4x.copy_image.functional rgb5/rgb5_a1/rgba4 x every *2d_array* pair).
// With no 16-bit packed ES image left there is no field order to disagree about; the
// client word still round-trips exactly, because the canonical shadow is already
// UNorm8 and an n-bit field encodes to UNorm8 and back losslessly for n <= 8.
// ForceOn widens on any driver (the llvmpipe suites use it to exercise the widened
// path); ForceOff keeps the native narrow storage even where the probe fires - the
// negative control that replays the corruption. Costs 2x the memory of the affected
// formats where it engages, which is why Auto is probe-gated rather than always-on.
QuirkOverride EsprytWidenPacked16Storage = QuirkOverride::Auto;
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE: DirectVulkan's GL_PRIMITIVES_GENERATED
// reroute for draws made while transform feedback is INACTIVE. The stream query
// (VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT primitivesNeeded) is defined to count
// them, but a Mali driver - and Mesa lavapipe - answers 0 unless a capture span is
// open, which is exactly the shape the CTS uses to measure the tessellator, so ~29
// tessellation tests per tree size a capture buffer from the 0 and die on the
// zero-length map. Auto defers to a device probe at renderer bring-up
// (SelfTest::RunPrimitivesGeneratedNoXfbProbe), which measures two substitutes on
// the same capture-less draws and arms the best proven one: the dedicated
// VK_EXT_primitives_generated_query (exact semantics by definition; lavapipe passes
// it, rasterizer discard included), else a clipping-invocations pipeline-statistics
// pool (see the verdict vocabulary for its rasterizer-discard split). ForceOn pins
// the reroute structurally wherever a pool can exist (the arming-observable lane,
// immune to the probe's verdict moving), and ForceOff is the negative control that
// replays the driver's silence.
QuirkOverride MagmaPrimGenQueryReroute = QuirkOverride::Auto;
};
extern FeaturesTable Features;
} // namespace MobileGL::MG_Config
+26 -8
View File
@@ -162,33 +162,51 @@ namespace MobileGL::MG_ConfigLoader {
inline void InitFeatures() {
auto& features = MG_Config::Features;
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
features.EsprytEnableTextureView = QueryEnvFlag("MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW");
features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
features.EsprytUseAngle = QueryEnvFlag("MOBILEGL_ESPRYT_USE_ANGLE");
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
#endif
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
features.MagmaDisableSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_SUBGROUP");
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
features.MagmaFixIterationRPSubgroupScratch =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
features.AvoidSamplerMipmapMinFilter =
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
features.AvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_AVOID_EXPLICIT_LOD_BIAS");
features.EsprytAvoidSamplerMipmapMinFilter =
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
features.EsprytDisableUboRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UBO_RING");
features.EsprytDisableUnpackRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UNPACK_RING");
features.EsprytDisableUploadRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING");
features.EsprytDisableInvalidateFlush = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH");
features.DisableLargeBufferAdoption = QueryEnvFlag("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION");
features.EsprytForceDepthStencilReadbackEmulation =
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
features.MagmaDisableBlendedDepthWriteQuirk =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
features.MagmaDisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS");
features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
features.AsyncOptimisticShaderStatus =
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
features.EsprytViewportArrayEmulation =
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION");
features.EsprytWidenPacked16Storage =
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE");
}
inline void InitBackendType() {
+16
View File
@@ -15,8 +15,11 @@
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
#include <MG_Impl/GLImpl/Query/GL_Query.h>
#include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
#include <atomic>
#include <mutex>
@@ -51,6 +54,11 @@ namespace MobileGL {
// before a re-initialized library could pair them with the wrong
// backend's DeleteSync).
MG_Impl::GLImpl::DestroyAllSyncObjects();
// Queries die with their contexts for the same reason, and their registry
// is the same shape of process-global map: drain it here too, while the
// function table can still pair each backend handle with the backend that
// minted it.
MG_Impl::GLImpl::DestroyAllQueryObjects();
MG_Backend::pActiveBackendObject.reset();
MG_State::pGLContext.reset();
MG_State::pEGLContext.reset();
@@ -66,6 +74,14 @@ namespace MobileGL {
// built-in symbol tables the prewarm latch stands for, so leaving it set would
// make the next Initialize() skip a prewarm it genuinely needs.
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 = {};
g_isInitialized = false;
if (logLifecycle) {
+125 -2
View File
@@ -14,6 +14,7 @@ namespace MobileGL {
namespace MG_State::GLState {
class FramebufferObject;
class ITextureObject;
class RenderbufferObject;
}
enum class BackendType {
@@ -24,6 +25,19 @@ namespace MobileGL {
};
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 {
Creatable = 1ull << 0,
@@ -160,9 +174,9 @@ namespace MobileGL {
GLsizei height, GLint border);
void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
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,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void (*GenerateMipmap)(GLenum target);
@@ -236,6 +250,14 @@ namespace MobileGL {
// (optional; null = frontend falls back to CPU accounting).
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
// Whether GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN should be answered from the
// frontend's own accounting wherever that accounting is exact - a capture with no
// geometry stage - instead of from the query above. Set by DirectGLES, whose result
// is whatever the ES driver's PRIMITIVES_WRITTEN counter says: Adreno reports twice
// the written count for a vertex-only capture that follows a large render pass,
// where the desktop-exact answer is the one the frontend already computed. Defaults
// to false, so a backend that never sets it keeps using its GPU result.
Bool PrefersCpuXfbPrimitiveAccounting = false;
// Transform feedback capture spans, for backends whose own GL/ES driver
// performs the capture (DirectGLES). Both optional; null means the backend
// drives capture from its draw recording instead (DirectVulkan). End is
@@ -279,6 +301,12 @@ namespace MobileGL {
struct DynamicBackendParameters {
SizeT UniformBufferOffsetAlignment = 256;
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which is a SEPARATE limit from the
// uniform one and is routinely larger: Adreno 830 reports 32 for uniform buffers and
// 64 for storage buffers. Answering the storage query with the uniform value let an
// application bind a storage range at an offset the driver cannot address, which it
// accepted without error and then wrote somewhere else entirely.
SizeT ShaderStorageBufferOffsetAlignment = 256;
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
// which is also why the extension is not advertised in that case.
Float MaxTextureMaxAnisotropy = 1.0f;
@@ -318,6 +346,22 @@ namespace MobileGL {
Int MaxVertexAttribs = 16;
Int MaxComputeShaderStorageBlocks = 8;
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 MaxComputeWorkGroupInvocations = 128;
Int MaxShaderStorageBufferBindings = 8;
@@ -334,8 +378,45 @@ namespace MobileGL {
Int MaxComputeImageUniforms = 8;
Int MaxDrawBuffers = 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;
// GL_MAX_CULL_DISTANCES and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, under exactly
// the contract stated for MaxClipDistances above: ZERO IS A LEGAL ANSWER and a
// backend that cannot host a cull distance MUST report it. The failure this prevents
// is worse than the clip one, because cull distance discards the whole primitive:
// glslang bounds gl_CullDistance[i] against maxCullDistances and expands
// gl_MaxCullDistances from it, SPIRV-Cross then emits
// `#extension GL_EXT_clip_cull_distance : require` into the ESSL, and a host driver
// without that extension rejects the program in an info log nobody surfaces. These
// used to be bare 8s inside BuildTBuiltInResource with no backend consulted at all.
// The DEFAULTS are the GL 4.5 core minimums for the same reason MaxClipDistances'
// is: they describe the no-backend case (standalone compiles, unit tests).
Int MaxCullDistances = 8;
Int MaxCombinedClipAndCullDistances = 8;
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 MaxViewportHeight = 16384;
Float ViewportBoundsRangeMin = 0.0f;
@@ -383,13 +464,55 @@ namespace MobileGL {
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
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 a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (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
// 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;
// Whether a TESSELLATION stage of this backend may access gl_PointSize - i.e.
// whether a module declaring OpCapability TessellationPointSize can reach the
// driver at all. DirectVulkan sets both this and the geometry twin from the one
// shaderTessellationAndGeometryPointSize feature; DirectGLES sets them
// independently from the EXT/OES_tessellation_point_size /
// geometry_point_size extension pairs (PointSizeTier), which really do come
// separately. When absent, ProgramSpirvTask demotes the built-in to an ordinary
// varying program-wide (ShaderCompiler::
// DemoteTessellationGeometryPointSizeForProgram); MOBILEGL_POINT_SIZE_DEMOTION
// overrides the detection in either direction at backend init.
//
// Defaults TRUE, deliberately against the house "assume absent" rule: false
// ARMS a rewrite, so the conservative no-backend answer (standalone compiles,
// unit tests) is the one that leaves modules untouched. A backend that never
// sets it gets standard modules and, at worst, the old honest declines.
Bool SupportsTessellationPointSize = true;
// The geometry-stage twin (OpCapability GeometryPointSize).
Bool SupportsGeometryPointSize = true;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Uint32 SubgroupSize = 0;
Uint32 SubgroupSupportedStages = 0;
@@ -8,6 +8,7 @@
#include "BackendObject_DirectGLES.h"
#include "MG_Backend/BackendObject.h"
#include "MG_Backend/BackendObjects.h"
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/Utils.h>
@@ -212,7 +213,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
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");
}
@@ -303,6 +307,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
return capabilities.MaxColorTextureSamples;
}
// The RENDERBUFFER twin, and it is a different set of pnames on purpose.
// GL_MAX_{COLOR,DEPTH}_TEXTURE_SAMPLES bound multisample TEXTURES; a renderbuffer is
// bounded by GL_MAX_SAMPLES (GL 4.6 core 9.2.4), with GL_MAX_INTEGER_SAMPLES for the
// integer formats. Using the texture ceilings here - which is what the renderbuffer probe
// did - is not merely untidy: the two texture pnames are ES 3.1 state, so on an ES 3.0
// context the loader's rejected-probe clamp leaves them at 1 (see the multisample clamps
// in the GLES loader) and the walk below would never run past one sample, recording {1}
// for EVERY colour format while GL_MAX_SAMPLES - ES 3.0 core, so genuinely answered -
// reports 4. Once the frontend validates against this list, that would reject every
// multisample renderbuffer on such a context.
Int GetGLESRenderbufferFormatMaxSamples(const MG_External::GLESCapabilities& capabilities,
GLenum imageFormat) {
const Bool isInteger = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER ||
imageFormat == GL_RGB_INTEGER || imageFormat == GL_RGBA_INTEGER;
return isInteger ? capabilities.MaxIntegerSamples : capabilities.MaxSamples;
}
Bool ProbeFramebufferCompletenessForTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
GLuint texture, TextureInternalFormat format) {
GLuint framebuffer = 0;
@@ -406,9 +427,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
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,
GLenum imageFormat, GLenum imageType, TextureInternalFormat logicalFormat,
Bool* outRenderable) {
Bool* outRenderable, Int samples = 1) {
if (!IsGLESProbeTextureTarget(target) || !gl.glGenTextures || !gl.glBindTexture || !gl.glDeleteTextures) {
return false;
}
@@ -428,10 +452,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool isMultisample = IsGLESProbeMultisampleTarget(target);
if (isMultisample) {
const auto probeSamples = static_cast<GLsizei>(std::max(samples, 1));
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) {
gl.glTexStorage3DMultisample(glTarget, 1, internalFormat, 1, 1, 1, GL_TRUE);
gl.glTexStorage3DMultisample(glTarget, probeSamples, internalFormat, 1, 1, 1, GL_TRUE);
} else {
gl.glBindTexture(glTarget, static_cast<GLuint>(previousBinding));
gl.glDeleteTextures(1, &texture);
@@ -527,6 +552,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
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,
const MG_External::GLESCapabilities& capabilities,
FormatCapabilityCache& cache) {
@@ -627,7 +675,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
AddFullFormatCaps(cache, targetIndex, formatIndex,
BuildTextureCapsFromProbe(logicalFormat, target, nativeRenderable));
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;
@@ -645,7 +697,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo);
}
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);
}
}
}
@@ -678,7 +734,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
AddFullFormatCaps(cache, renderbufferTargetIndex, formatIndex,
GetRenderbufferFeatureCaps(logicalFormat));
const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat);
GetGLESRenderbufferFormatMaxSamples(capabilities, nativeInfo.ImageFormat);
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
ProbeRenderbufferSampleCounts(gl, nativeInfo.InternalFormat, logicalFormat, maxSamples);
} else {
@@ -692,7 +748,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo);
}
const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, renderbufferFallbackInfo.ImageFormat);
GetGLESRenderbufferFormatMaxSamples(capabilities, renderbufferFallbackInfo.ImageFormat);
cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(
gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples);
}
@@ -710,11 +766,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
.ExtraVendor = Nullopt, // Extra vendor
.RendererGLInfo =
{
.TargetGLVersion = {4, 0, 0}, // GL target version
.TargetGLVersion = {4, 6, 0}, // GL target version
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
.Extensions = BuildAdvertisedExtensions(false, false),
// Baseline advertisement (no runtime capabilities yet); reconciled once
// the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
.Extensions = BuildAdvertisedExtensions(false, false, false, false, false, false),
.IsCompatibilityProfile = false // Is Compatibility Profile
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
@@ -734,9 +790,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// thread can only observe the extension string after the
// advertisement for its context has settled; rebuilding the whole
// list keeps the re-run after a context recreation idempotent.
void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) {
MutableRendererInfo().RendererGLInfo.Extensions =
BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported);
void UpdateAdvertisedCapabilityExtensions(const MG_External::GLESCapabilities& capabilities) {
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
capabilities.SupportsDrawIndirect,
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance,
capabilities.SupportsTextureView, capabilities.SupportsTextureCubeMapArray);
}
} // namespace
@@ -745,6 +804,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
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() {
DestroyEGLContext();
}
@@ -779,11 +861,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and
// GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on
// UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension
// list is first built).
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy);
// Now that g_GLESCapabilities knows the host extensions, entry points, and ES version,
// reconcile every runtime-gated advertisement (see the comment on
// UpdateAdvertisedCapabilityExtensions for why this cannot happen when the list is first
// built).
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities);
UpdateDynamicBackendParameters();
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
PrintFormatCapabilities(GetFormatCapabilities());
@@ -924,11 +1006,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
return MutableRendererInfo();
}
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
Bool drawIndirectSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool textureViewSupported, Bool cubeMapArraySupported) {
Vector<GLExtension> extensions = {
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
// The version tokens have to reach the version the backend actually claims:
// TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an
// application feature-detecting off these tokens the opposite of what
// GL_MAJOR_VERSION / GL_MINOR_VERSION told it.
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
V_OpenGL44, V_OpenGL45, V_OpenGL46,
E_GL_ARB_draw_buffers_blend,
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
E_GL_ARB_texture_storage_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_shader_draw_parameters,
@@ -951,10 +1042,94 @@ namespace MobileGL::MG_Backend::DirectGLES {
// has had the same texture parameter since ES 3.1, which every device MobileGL
// runs on provides.
E_GL_ARB_stencil_texturing,
// Core since 3.2 and implemented here on both backends - glDrawElementsBaseVertex,
// glDrawRangeElementsBaseVertex, glDrawElementsInstancedBaseVertex and
// glMultiDrawElementsBaseVertex all reach real per-draw vertex rebasing. The string
// was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests
// NotSupported on a feature that works.
E_GL_ARB_draw_elements_base_vertex,
// The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync,
// glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a
// backend fence (a host GLsync here, a VkFence on DirectVulkan), and glGetInteger64v
// answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason
// ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension
// list, and a caller that finds GL_ARB_sync missing never resolves the entry points -
// then calls through null if it uses fences anyway. Nothing in the CTS gates on this
// string, so it is advertised on the strength of the implementation, not a test unlock.
E_GL_ARB_sync,
// Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole
// GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and SyncAtomicCounterBuffers
// re-issues the counter buffer as an SSBO binding in the range reserved at the top of
// the ES driver's shader-storage points, so a counter dispatch reads and writes the
// buffer the application bound. DirectVulkan reaches the same place through its own
// descriptor resolution, so the string is symmetric.
E_GL_ARB_shader_atomic_counters,
// glVertexAttribDivisor, core since 3.3 and real on both backends. Applications
// (Better Clouds' GLCompat among them) accept the extension string as an
// ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is
// available, so withholding it makes MobileGL look less capable than it is.
E_GL_ARB_instanced_arrays,
// The whole of KHR_debug lives in GLImpl - the message log, the group stack and the
// object-label table are MobileGL's own state, not the host driver's - so it is as
// available here as it is on DirectVulkan, which has advertised it all along.
E_GL_KHR_debug,
// Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has
// existed, and that was simply never named. None of these unlocks a single CTS case -
// the conformance suite reaches all of them through the version - so they are
// advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the
// string set, and an application that gates its ENTRY POINTS on the string rather than
// on the version never resolves them and then calls through null. Each is backed by
// the entry points named beside it.
//
// glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray.
E_GL_ARB_vertex_array_object,
// The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the
// integer-valued Iiv/Iuiv forms.
E_GL_ARB_sampler_objects,
// glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent
// maps are already built on top of.
E_GL_ARB_map_buffer_range,
// glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets.
E_GL_ARB_copy_buffer,
// glCopyImageSubData, wired to a real backend hook on both backends.
E_GL_ARB_copy_image,
// GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which this backend syncs through to the ES
// driver's identical parameters.
E_GL_ARB_texture_swizzle,
// GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus
// the eight glVertexAttribP* entry points.
E_GL_ARB_vertex_type_2_10_10_10_rev,
// The R/RG internal formats. Named separately from the float ones because an
// application may check either.
E_GL_ARB_texture_rg,
// GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8.
E_GL_ARB_depth_buffer_float,
// The floating-point colour formats. Unlike the rest of this block this string DOES
// gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no
// core-version fallback, so eight cases per version list were NotSupported on formats
// the backend has always had.
E_GL_ARB_texture_float,
// glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} /
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
// 16 viewports GL_MAX_VIEWPORTS reports and the per-viewport routing emulation.
E_GL_ARB_viewport_array,
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1.
E_GL_ARB_get_program_binary};
// Minecraft 26.3 checks this prerequisite before it even considers
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
// folds the version and pointer checks into SupportsDrawIndirect.
if (drawIndirectSupported) {
extensions.push_back(E_GL_ARB_draw_indirect);
}
// ARB_base_instance also defines the last word of an indirect command. Direct calls are
// emulated on every Espryt device, but without host GL_EXT_base_instance a native indirect
// draw cannot shift divisor attributes by a GPU-authored non-zero value, so do not promise
// that incomplete case.
if (drawIndirectSupported && nonZeroIndirectBaseInstanceSupported) {
extensions.push_back(E_GL_ARB_base_instance);
}
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the host ES
// driver's: the compiler threads are MobileGL's, and glCompileShader/glLinkProgram
// are serviced entirely inside the frontend. Whether the device driver advertises
@@ -986,6 +1161,47 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query);
}
// Cube map arrays are core from GL 4.0 and from ES 3.2, but on a pre-ES-3.2 driver without
// EXT/OES_texture_cube_map_array there is nothing underneath: the texture gets no storage
// and a samplerCubeArray shader does not even compile, which is exactly what the POST
// reports. So the string follows the host capability rather than the version.
//
// Named for the application's benefit rather than the suite's: measured on Adreno 830,
// KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so
// this unlocks no conformance case. It is advertised because the feature is real and
// because an application that feature-detects cube map arrays off the string (rather than
// off the 4.0 version) would otherwise decline a path this backend serves.
if (cubeMapArraySupported) {
extensions.push_back(E_GL_ARB_texture_cube_map_array);
}
// Only advertised when the host ES driver has EXT/OES_texture_view. ES has no core
// texture views at any version and no honest emulation exists: a view is a SECOND NAME
// over the SAME storage, so that writes through either are visible through the other and
// the two carry independent per-texture parameters at the same time - which is exactly
// what applications use it for (Better Clouds samples one D24S8 through its own name with
// DEPTH_STENCIL_TEXTURE_MODE = STENCIL_INDEX and through a view with DEPTH_COMPONENT, in
// a single shading pass). A copy-based fallback satisfies neither half, and fails
// silently; withholding the string and answering glTextureView with INVALID_OPERATION is
// the only behaviour that cannot be mistaken for success.
//
// The host extension is necessary and NOT sufficient, which is why this second gate
// exists. Adreno 830 has EXT_texture_view, and on it the whole functional half of
// KHR-GL4{2,3}.texture_view fails: base_and_max_levels, reference_counting and
// view_sampling Fail and view_classes crashes, while only the two pure-API cases
// (errors, gettexparameter - neither of which touches the host view) pass. The cause is
// known and is MobileGL's, not the driver's: SyncTextureViewToBackend normalizes the
// VIEW's ES internalformat independently of the storage it aliases, so whenever the two
// land on different renderability carriers the host rejects the pair, the error is
// swallowed, and the view is left as a storage-less name that samples as zeros.
// DirectVulkan builds the view as a second VkImageView over one VkImage and has no such
// seam - it passes 5 of the 7 cases on the same device - so the string stays there.
//
// Until that reconciliation exists, advertising here would be the same lie the comment
// above refuses to tell, just with an extra prerequisite met. Set
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 to re-enable it for that work.
if (textureViewSupported && MG_Config::Features.EsprytEnableTextureView) {
extensions.push_back(E_GL_ARB_texture_view);
}
// Only advertised when the host ES driver actually filters anisotropically: the sampler
// state is accepted regardless, but forwarding it would be a no-op without the extension,
// and an app that trusts the string (LWJGL builds GLCapabilities from it) would silently
@@ -1090,6 +1306,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// geometry shader's amplification.
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
// ...but where it CAN see the whole capture - no geometry stage - the frontend's
// own count is the desktop-exact one and the ES driver's is only as good as the
// vendor made it (Adreno doubles PRIMITIVES_WRITTEN for a vertex-only capture that
// follows a large render pass). The query above stays installed: it is still what
// answers an amplifying span, and PRIMITIVES_GENERATED always.
funcsTable.GL.PrefersCpuXfbPrimitiveAccounting = true;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
@@ -1119,6 +1341,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
m_dynamicParameters.ShaderStorageBufferOffsetAlignment =
m_GLESCapabilities.ShaderStorageBufferOffsetAlignment;
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin;
m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax;
@@ -1169,9 +1393,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks;
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.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
// on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
// than a driver answer (m_GLESCapabilities.MaxTextureBufferSizeIsDriverReported says
@@ -1224,14 +1470,61 @@ namespace MobileGL::MG_Backend::DirectGLES {
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
}
}
// Not a driver question and never will be: OpenGL ES has no double-precision vertex format
// and ESSL has no fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to
// land on this backend regardless of what the driver underneath happens to support.
// Not a driver question and never will be: 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 a
// 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;
// Whether a tessellation / geometry stage's ESSL may name gl_PointSize at all: the two
// extension pairs the loader probed, independently, because they really do come
// separately. False arms the shared phase-B demotion
// (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram), whose ESSL then
// never names the built-in in those stages and needs no extension.
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends both are absent so the demotion can be
// exercised on a healthy driver (the pinned integration lane); =0 restores the
// detected answer's declines.
m_dynamicParameters.SupportsTessellationPointSize =
m_GLESCapabilities.TessellationPointSizeSupport !=
MG_External::GLESCapabilities::PointSizeTier::None;
m_dynamicParameters.SupportsGeometryPointSize =
m_GLESCapabilities.GeometryPointSizeSupport !=
MG_External::GLESCapabilities::PointSizeTier::None;
switch (MG_Config::Features.PointSizeDemotion) {
case MG_Config::QuirkOverride::ForceOn:
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
"gl_PointSize as unhosted so the demotion runs on this driver");
m_dynamicParameters.SupportsTessellationPointSize = false;
m_dynamicParameters.SupportsGeometryPointSize = false;
break;
case MG_Config::QuirkOverride::ForceOff:
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
"plain declines regardless of the driver's extensions");
m_dynamicParameters.SupportsTessellationPointSize = true;
m_dynamicParameters.SupportsGeometryPointSize = true;
break;
case MG_Config::QuirkOverride::Auto:
break;
}
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
// The loader already gated both on GL_EXT_clip_cull_distance and left 0 without it, which
// is the answer that keeps glslang from accepting a gl_CullDistance the ESSL compiler
// would reject.
m_dynamicParameters.MaxCullDistances = m_GLESCapabilities.MaxCullDistances;
m_dynamicParameters.MaxCombinedClipAndCullDistances = m_GLESCapabilities.MaxCombinedClipAndCullDistances;
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.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight;
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
@@ -18,6 +18,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
const MG_External::GLESCapabilities& capabilities,
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 {
public:
~BackendObject_DirectGLES() override;
@@ -67,9 +77,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
// for a device whose timer queries / anisotropic filtering are (or are not) usable.
// for a device whose timer queries / anisotropic filtering / native indirect draws /
// non-zero indirect baseInstance semantics / EXT-OES texture views are (or are not) usable.
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported);
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
Bool drawIndirectSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool textureViewSupported, Bool cubeMapArraySupported);
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
// initialized backend returns from GetBackendAPIVersionString (and that ends up
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);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, 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,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
File diff suppressed because it is too large Load Diff
+551 -9
View File
@@ -21,6 +21,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
String EmulateBaseInstanceInVertexShader(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
// 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
@@ -79,9 +102,98 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Brings the whole draw-relevant frontend state onto the native ES context and binds
// the program; every GL draw entry point calls it exactly once before issuing draws.
void PrepareForDraw(DrawSyncFlags syncBits);
// GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX. Throws when the app enabled
// the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index for this index type.
void CheckPrimitiveRestartSupported(GLenum indexType);
// What an indexed draw has to do about primitive restart before it can be issued.
//
// Desktop GL restarts on an application-chosen index (glPrimitiveRestartIndex under
// GL_PRIMITIVE_RESTART); GLES core restarts only on the all-ones value of the index type
// (GL_PRIMITIVE_RESTART_FIXED_INDEX), which the render-state push enables for BOTH caps.
// That leaves three cases, and the difference between the last two is not cosmetic - one
// adds restarts, the other has to take away restarts the driver would otherwise make.
enum class RestartSubstitutionKind : Uint8 {
// Nothing to do: restart is off, the fixed-index cap is on, or the application's
// restart index already IS the type's all-ones value. The overwhelmingly common answer.
None,
// The application's index is representable in this index type and differs from the
// all-ones value: the index DATA has to be rewritten so the driver restarts where the
// application asked.
RewriteIndices,
// The application's index cannot be held by this index type at all. GL 4.6 core 10.3.6
// compares the fetched index, zero-extended, against the full 32-bit
// PRIMITIVE_RESTART_INDEX, so no index can match and the draw restarts NOWHERE - but the
// render-state push has already enabled the driver's fixed-index restart, so the
// all-ones value has to be un-restarted for the duration of the draw.
SuppressRestart,
};
RestartSubstitutionKind ResolveRestartSubstitution(GLenum indexType);
// Turns the driver's fixed-index restart off for one draw and back on afterwards, for the
// SuppressRestart case above. Separate from the substitution below because the multi-draw
// tiers need it on its own: they rewrite the index stream themselves and only ever need the
// cap half. Inert for every other kind, and it never touches the render-state shadow - it
// puts the driver back exactly where SyncRenderState left it.
class ScopedSuppressedPrimitiveRestart {
public:
explicit ScopedSuppressedPrimitiveRestart(RestartSubstitutionKind kind);
~ScopedSuppressedPrimitiveRestart();
ScopedSuppressedPrimitiveRestart(const ScopedSuppressedPrimitiveRestart&) = delete;
ScopedSuppressedPrimitiveRestart& operator=(const ScopedSuppressedPrimitiveRestart&) = delete;
private:
Bool m_suppressed = false;
};
// Swaps in a scratch element array buffer holding a copy of the index data in which the
// application's restart index has been replaced by the value GLES restarts on. Inert
// (and free) unless ResolveRestartSubstitution asks for it. The swap lives for the
// object's lifetime, so it covers every pass of a viewport-routed draw, and the previous
// GL_ELEMENT_ARRAY_BUFFER name is restored on destruction - which matters beyond tidiness,
// because the VAO twin memoises that it already synced that binding.
//
// The copy may be WIDER than the source (see IndexType): when the source already contains
// the type's all-ones value as an ordinary vertex index, that value cannot double as the
// restart sentinel, and widening is the only way to keep both meanings. Callers must
// therefore take the index type from this object, not from their own argument.
class ScopedRestartIndexSubstitution {
public:
// count/indices describe the draw's index range when the CPU knows it. Pass
// count == 0 for an indirect draw, whose count lives in GPU memory: the whole bound
// element array buffer is rewritten instead, so every element keeps its position and
// a GPU-resident firstIndex - an ELEMENT index, so it survives widening too - still
// addresses the index it named.
ScopedRestartIndexSubstitution(GLenum indexType, GLsizei count, const void* indices);
~ScopedRestartIndexSubstitution();
ScopedRestartIndexSubstitution(const ScopedRestartIndexSubstitution&) = delete;
ScopedRestartIndexSubstitution& operator=(const ScopedRestartIndexSubstitution&) = delete;
// False only when a substitution was needed and could not be made. The draw must
// then be skipped: issuing it would let the driver silently drop every restart and
// weld the primitives on either side together, which is worse than drawing nothing.
Bool DrawIsValid() const { return m_valid; }
// The element-array offset (or client pointer) the draw must use. Identical to what
// was passed in unless a substitution was made.
const void* Indices() const { return m_indices; }
// The index type the draw must be issued with. Identical to the constructor's unless
// the copy had to be widened to keep an all-ones vertex index distinguishable from the
// restart sentinel.
GLenum IndexType() const { return m_indexType; }
private:
// Declared before m_capOverride so it is initialised first (members initialise in
// declaration order): the whole decision is made once, and both the cap override and the
// constructor body read the same answer.
RestartSubstitutionKind m_kind = RestartSubstitutionKind::None;
ScopedSuppressedPrimitiveRestart m_capOverride;
const void* m_indices = nullptr;
GLenum m_indexType = 0;
Uint m_previousBinding = 0;
Bool m_substituted = false;
Bool m_valid = true;
};
// Drops the scratch element array buffer the substitution above stages through. Like
// MultiDrawImpl's scratch names it is abandoned rather than deleted: the name belongs to
// the dead ES context, and deleting it would target whatever its successor handed out.
void OnRestartSubstitutionContextDestroyed();
// Feed the current program's gl_BaseInstance / gl_DrawID / gl_BaseVertex emulation
// uniforms. All are no-ops when the program does not read the corresponding builtin.
void SetCurrentBaseInstance(Uint32 baseInstance);
@@ -103,6 +215,58 @@ namespace MobileGL::MG_Backend::DirectGLES {
// link.
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_ESPRYT_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>
class StateBackendObjectRegistry {
public:
@@ -129,7 +293,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Twin creation is the moment a driver-owned id starts needing a guarded
// destructor; cold path, so the once-guard costs nothing per draw.
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()];
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()) {
// 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.
@@ -203,8 +388,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
private:
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;
Uint32 m_gcTick = 0;
Uint32 m_creationTick = 0;
Bool m_isCollecting = false;
};
@@ -272,6 +461,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the owning thread replaying them: guard both fields with pendingMutex.
Bool pendingRespecify = false;
VecRange1D pendingRanges;
// App bytes for an ADOPTED store, awaiting their GPU-ordered landing (ring
// stage + glCopyBufferSubData at the next sync; see
// BufferBackendOps::ResidentSubData). The frontend keeps such writes out of
// the coherent mapping - an in-place host write tears the in-flight frames
// still reading the old bytes. Guarded by pendingMutex like pendingRanges.
struct PendingResidentWrite {
SizeT offset = 0;
Vector<Uint8> bytes;
};
Vector<PendingResidentWrite> pendingResidentWrites;
std::mutex pendingMutex;
// Buffer-mutation epoch (see CurrentBufferMutationEpoch) at which this
// resource last probed IsBufferDrawClean == true, 0 = never (epochs start
@@ -346,13 +545,37 @@ namespace MobileGL::MG_Backend::DirectGLES {
// client-attribute staging buffers): scrub every buffer-binding shadow that
// could false-skip when the name is recycled.
void NoteBufferIdDeleted(Uint id);
// Bumped whenever a live GLESBufferResource's driver id is retired and re-minted
// while its frontend buffer stays alive (persistent-map adoption, immutable-store
// retire). The VAO twins' baked glVertexAttribPointer / element-array bindings
// key on FRONTEND versions, which a backend-side re-mint does not move - without
// this generation the driver VAO would keep fetching through the deleted id (or
// its retained store) forever. Compared and stamped by
// BackendVertexArrayObject::SyncToBackend.
extern Uint64 g_bufferBackendIdGeneration;
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
// (id, range) already at that index matches, like the array-buffer/texture/
// sampler caches already do. Invalidated on MakeCurrent (context may reset).
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER / GL_TRANSFORM_FEEDBACK_BUFFER):
// skips the GL call when the (id, range) already at that index matches, like the
// array-buffer/texture/sampler caches already do. Invalidated on MakeCurrent
// (context may reset).
// Binds the transform feedback capture points [0, bufferCount) from the frontend
// state, and touches nothing else - in particular it never binds a zero the
// application did not ask for. See the definition for why that matters on Mali.
void SyncTransformFeedbackBindingPoints(SizeT bufferCount);
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
void InvalidateIndexedBufferBindingCache();
// The transform feedback capture points are per-transform-feedback-OBJECT state, so
// every glBindTransformFeedback swaps all of them under the shadow above. XfbImpl
// calls this on each bind/delete.
void InvalidateTransformFeedbackBindingShadows();
// 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
// (called once per frame from Present); ClearBufferPool drops all pooled ids
// without glDeleteBuffers (called when the ES context is going away).
@@ -395,6 +618,57 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Present()-time upkeep: records the frame's high-water mark for reclamation
// and deletes grown-away ring stores once the GPU is done with them.
void UboRingOnPresent();
// --- Texture unpack-PBO ring ----------------------------------------------
// The same persistent-mapped bump allocator, staging TEXTURE UPLOADS. A
// glTexSubImage from client memory hands the driver a pointer it must read
// before the call returns, so the copy has to be ordered against whatever GPU
// work still reads the destination texture: Mali resolves that by BLOCKING the
// calling thread (osup_sync_object_wait) instead of ghosting, and Minecraft
// re-uploads animated atlas sprites and the lightmap every tick into textures
// the in-flight frame is still sampling. Staging the bytes into a
// GPU-visible unpack PBO and passing an OFFSET instead lets the driver queue
// the copy in the command stream with no CPU wait at all.
//
// Same reclamation contract as the UBO ring: no ring bytes are recycled before
// the frame that referenced them completed on the GPU, so a staged block stays
// intact for as long as the queued transfer can still be reading it. The store
// therefore settles at roughly (bytes staged per frame) x (frames in flight),
// which is what to watch if this ring ever shows up in an RSS regression: it
// grows on demand from 4 MiB and is capped, not unbounded.
//
// False when the feature is disabled (MOBILEGL_ESPRYT_DISABLE_UNPACK_RING),
// EXT_buffer_storage / fences are missing, the ES context is not current, or
// ring creation already failed under this context. Callers then upload from
// the client pointer exactly as before.
Bool UnpackRingAvailable();
// Bump-allocate `size` bytes aligned to 64 (a PBO-sourced glTexSubImage only
// owes the driver the pixel type's own alignment). Grows the ring when the
// in-flight span would be overrun; false when the request exceeds the ring's
// size cap or storage (re)creation fails.
Bool UnpackRingAllocate(SizeT size, SizeT& outOffset);
void* UnpackRingMappedPtr();
Uint UnpackRingBufferId();
// Largest single staging request the ring can ever satisfy.
SizeT UnpackRingMaxBytes();
void UnpackRingOnPresent();
// --- Buffer upload ring ---------------------------------------------------
// The same persistent-mapped bump allocator, staging APP BUFFER UPDATES
// (glBufferSubData / non-persistent map flushes) whose destination store may
// still be referenced by in-flight GPU work. Mali resolves that WAR hazard by
// BLOCKING the calling glBufferSubData (osup_sync_object_wait) until every
// referencing job retires - Minecraft 26.3 rewrites its chunk-section and
// dynamic-transform UBOs and streams chunk meshes with per-frame SubData, and
// each such call serialized against the whole GPU queue (~1 fps while chunks
// stream in, and again on every camera pan). App SubData ranges are queued on
// the resource instead (the frontend shadow already holds the bytes) and
// draw-time sync drains them: bytes staged into this ring, then one
// glCopyBufferSubData per merged range - the copy is ordered on the GPU
// timeline, so the hazard costs no CPU wait. Reclamation contract identical
// to the other two rings. MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING restores the
// historical immediate-upload path (negative control / escape hatch).
void UploadRingOnPresent();
} // namespace BufferImpl
namespace VertexArrayImpl {
@@ -459,12 +733,51 @@ namespace MobileGL::MG_Backend::DirectGLES {
PendingAttribValueMask& GetPendingAttribValueMaskMemo() { return m_pendingAttribValueMask; }
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;
PendingAttribValueMask m_pendingAttribValueMask;
Uint m_backendVAOId = 0;
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;
Uint16 m_syncedIndexBufferVersion = 0;
// Identity of the buffer the version above was stamped against. Raw and never
// dereferenced: the slot version is a wrapping Uint16 (see the ResolvedDrawBuffers
// IBO memo and the packed_pixels postmortem at BindCurrentFBO), so the version
// alone would read a wrapped-back count with a different buffer bound as clean.
const MG_State::GLState::BufferObject* m_syncedIndexBufferObject = nullptr;
// Aggregate gate over the per-attribute walk below: the frontend bumps its config
// version on every per-attribute version bump (the three Bump*Version functions are
// its only writers), so an unchanged config version proves every per-attribute
@@ -480,6 +793,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Kept here because it describes what was last EMITTED, which is what the next sync
// has to correct.
Uint32 m_syncedFetchBaseInstance = 0;
// BufferImpl::g_bufferBackendIdGeneration as of this twin's last emit. A
// mismatch means some live buffer's driver id was re-minted since; the ids
// baked into the driver VAO's attribute/element bindings may be dead even
// though every frontend version matches, so the next sync re-emits them all.
Uint64 m_syncedBufferIdGeneration = 0;
};
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
@@ -586,9 +904,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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
// 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,
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
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
@@ -621,12 +951,38 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendTextureObject(const BackendTextureObject&) = delete;
BackendTextureObject& operator=(const BackendTextureObject&) = delete;
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
// The storage half of the sync for a texture created by glTextureView. Instead of
// allocating storage and replaying uploads, it makes this object's ES name BE a view
// of the storage texture's ES name (EXT/OES_texture_view), which is what gives the
// two names one image and independent per-texture parameters at the same time. The
// parameter and sampler halves are unchanged and run on this name as on any other.
void SyncTextureViewToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void StampViewSyncKeys(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
// The storage half of the sync for a texture created by glTextureView. Instead of
// allocating storage and replaying uploads, it makes this object's ES name BE a view
// of the storage texture's ES name (EXT/OES_texture_view), which is what gives the
// two names one image and independent per-texture parameters at the same time. The
// parameter and sampler halves are unchanged and run on this name as on any other.
void SyncBuiltinSamplerToBackend(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);
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
// SyncTextureParamsToBackend + SyncBuiltinSamplerToBackend +
// SyncMipmapsToBackend: EXACTLY the conjunction of their own early-outs
@@ -639,6 +995,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// `contextId`/`samplingGeneration` are the frontend context's current
// values, hoisted by the caller so a per-draw list walk reads them once
// instead of per texture. `t` must be the live frontend texture.
// True while a driver-side re-mint has left the parameter caches describing a texture
// that no longer exists; SyncTextureObjectToBackend re-pushes them in the same sync.
Bool NeedsParameterResync() const { return m_forceTextureParamsResync || m_forceSamplerResync; }
Bool IsDrawSyncClean(const MG_State::GLState::ITextureObject* t, Uint64 contextId,
Uint64 samplingGeneration) const {
if (!m_isInitialized || m_syncedShapeContextId == 0 || m_syncedShapeContextId != contextId ||
@@ -663,6 +1023,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
void RecreateBackendTexture();
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;
// For a texture created by glTextureView: the ES name of the storage texture this
// one was last made a view OF. EXT_texture_view may be called only once per name, so
// a storage texture that got re-minted underneath (RecreateBackendTexture) has to be
// detected here and answered with a fresh name for the view as well - otherwise the
// view would keep aliasing storage that no longer exists.
Uint m_viewSourceBackendTextureId = 0;
// For a texture created by glTextureView: the ES name of the storage texture this
// one was last made a view OF. EXT_texture_view may be called only once per name, so
// a storage texture that got re-minted underneath (RecreateBackendTexture) has to be
// detected here and answered with a fresh name for the view as well - otherwise the
// view would keep aliasing storage that no longer exists.
// ES context generation the id was created under; a dtor running after
// that context died must not delete a foreign (recycled) name.
Uint m_contextGeneration = 0;
@@ -696,7 +1086,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint16 m_syncedShapeParamsVersion = 0;
SamplerParameters m_cacheSamplerParameters;
UintVec2 m_cacheLodRange = {0, 1000};
// All three representations plus the form, because none of them alone identifies the
// border colour the driver texture is holding: two integer borders can share one float
// (anything differing above 2^24), and a Float -> Int transition can leave every number
// unchanged while still needing a different driver entry point.
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
IntVec4 m_cacheBorderColorI = {0, 0, 0, 0};
UintVec4 m_cacheBorderColorUI = {0, 0, 0, 0};
BorderColorForm m_cacheBorderColorForm = BorderColorForm::Float;
Vec4<TextureSwizzleParam> m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green,
TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha};
// GL_DEPTH_STENCIL_TEXTURE_MODE. GL_DEPTH_COMPONENT is the GL and ES default, so a
@@ -711,6 +1108,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
// parameter already pushed onto it: the params-version early-out has to be overridden
// once, or an unchanged version would skip the re-push forever.
Bool m_forceTextureParamsResync = false;
// The same problem for the FILTER state, which lives in m_cacheSamplerParameters and
// is gated on the frontend sampler's version rather than on the params version. A
// re-mint leaves that cache describing values the new driver texture never received,
// and an unchanged sampler version would then skip re-pushing them forever. This
// matters more than mis-filtering: ES makes a texture INCOMPLETE when its filters do
// not suit its level set (any integer texture with a non-NEAREST filter, or a
// single-level texture with a mipmapping filter), and an incomplete texture samples
// (0, 0, 0, 1) rather than its contents.
Bool m_forceSamplerResync = false;
};
void ActivateTextureUnit(Uint unit);
@@ -799,6 +1205,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
using FramebufferObject = MG_State::GLState::FramebufferObject;
FramebufferObject::FramebufferAttachmentVersionArray m_syncedFrontendAttachmentVersions = {0};
// g_attachmentBackendIdGeneration as of this twin's last attachment walk. A
// mismatch means some backend texture id was re-minted since, and any of this
// twin's attachment points may still hold the dead id even though the frontend
// attachment versions match - so the walk re-attaches everything first.
Uint64 m_syncedBackendIdGeneration = 0;
};
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
@@ -888,6 +1299,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
g_fboSyncedObjects;
// Bumped whenever a live backend texture's driver id is re-minted while its
// frontend texture may still be attached to application FBOs
// (BackendTextureObject::RecreateBackendTexture - e.g. a respecify of a texture
// whose backend storage went immutable). The FBO twins' attachment memos key on
// FRONTEND attachment versions, which a backend-side re-mint does not move, so
// the driver FBO would keep the deleted texture name attached forever. The
// SyncCurrentFBO gate compares this generation (below) to re-enter the sync,
// and each twin re-arms its per-attachment memo on a mismatch (SyncToBackend).
extern Uint64 g_attachmentBackendIdGeneration;
// What g_attachmentBackendIdGeneration was when SyncCurrentFBO last stamped each
// target; part of the synced tuple above.
extern Array<Uint64, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedBackendIdGenerations;
// Driver-level READ/DRAW framebuffer-binding shadow. Every backend
// glBindFramebuffer routes through BindFramebufferId so scoped helpers can
// save/restore the current binding without a glGetIntegerv round-trip (that
@@ -991,23 +1415,51 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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
// 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) {
switch (type) {
case 0x904C: /*GL_IMAGE_1D*/
case 0x904D: /*GL_IMAGE_2D*/
case 0x904E: /*GL_IMAGE_3D*/
case 0x904F: /*GL_IMAGE_2D_RECT*/
case 0x9050: /*GL_IMAGE_CUBE*/
case 0x9051: /*GL_IMAGE_BUFFER*/
case 0x9052: /*GL_IMAGE_1D_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 0x9059: /*GL_INT_IMAGE_3D*/
case 0x905A: /*GL_INT_IMAGE_2D_RECT*/
case 0x905B: /*GL_INT_IMAGE_CUBE*/
case 0x905C: /*GL_INT_IMAGE_BUFFER*/
case 0x905D: /*GL_INT_IMAGE_1D_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 0x9064: /*GL_UNSIGNED_INT_IMAGE_3D*/
case 0x9065: /*GL_UNSIGNED_INT_IMAGE_2D_RECT*/
case 0x9066: /*GL_UNSIGNED_INT_IMAGE_CUBE*/
case 0x9067: /*GL_UNSIGNED_INT_IMAGE_BUFFER*/
case 0x9068: /*GL_UNSIGNED_INT_IMAGE_1D_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;
default:
return false;
@@ -1015,6 +1467,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
namespace PrgramImpl {
// Defined further down, next to CollectImageFormatBakeInputs; only referenced here.
struct ImageFormatBakeInputs;
class BackendProgramObjectImpl {
public:
// Per-link cache of a sampler-style uniform's backend location: built once in
@@ -1074,7 +1529,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendProgramObjectImpl();
~BackendProgramObjectImpl();
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
void Use() const;
void Use();
void SetBaseInstance(Uint32 baseInstance) const;
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
void SetDrawID(Uint32 drawId) const;
@@ -1085,6 +1540,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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.
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; }
Uint GetBackendProgramId() const { return m_backendProgramId; }
// False when the last SyncToBackend could not produce a usable program (a
@@ -1100,6 +1563,33 @@ namespace MobileGL::MG_Backend::DirectGLES {
// qualifier, so the overrides are baked into the source). A mismatch means the
// program is stale exactly like the clamp masks above.
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;
}
// GL_PATCH_DEFAULT_{OUTER,INNER}_LEVEL the same synthesized stage was built with, for
// the same reason: ES has neither the state nor an entry point to forward it to, so
// glPatchParameterfv's values are compiled in as literals and a program built with one
// set is stale for another. Meaningless (and never read) when the patch-vertices field
// above is -1, which is the gate the draw path tests first.
const FloatVec4& GetPassthroughTessControlOuterLevel() const {
return m_passthroughTessControlOuterLevel;
}
const FloatVec2& GetPassthroughTessControlInnerLevel() const {
return m_passthroughTessControlInnerLevel;
}
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
@@ -1148,6 +1638,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
private:
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,
Bool stripInputBlockLocations, Bool stripOutputBlockLocations,
Int atomicCounterEsslBindingTop, Bool enableSpirvValidation,
String& outSource,
std::set<String>& outFlattenedXfbBlockNames,
Vector<Int>& outAtomicCounterGlBindings, String& outError) const;
Uint m_backendProgramId = 0;
// 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
@@ -1158,6 +1676,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Int m_drawIdUniformLocation = -1;
Int m_baseVertexUniformLocation = -1;
Int m_baseInstanceWordIndexUniformLocation = -1;
Int m_viewportPassMaskUniformLocation = -1;
Int m_indirectParamsBinding = -1;
Uint32 m_snormFallbackClampOutputMask = 0;
Uint32 m_unormFallbackClampOutputMask = 0;
@@ -1166,8 +1685,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint m_fragColorBroadcastCount = 1;
// 0 is the signature of an empty override set, i.e. what almost every program has.
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;
// The default tessellation levels baked into that same stage. Only meaningful while
// the field above is not -1.
FloatVec4 m_passthroughTessControlOuterLevel = FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
FloatVec2 m_passthroughTessControlInnerLevel = FloatVec2(1.0f, 1.0f);
Bool m_isInitialized = 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_globalUboBackendBlockSize = 0;
@@ -1257,6 +1791,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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.
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(
const MG_State::GLState::ProgramObject& stateProgramObject);
+66 -22
View File
@@ -29,16 +29,21 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
}
}
// The all-ones value of an index type, which is what GL restarts on once
// primitive restart is in play. CheckPrimitiveRestartSupported has already
// rejected the arbitrary-index form of GL_PRIMITIVE_RESTART, so an enabled
// restart always restarts here and nowhere else.
// The index value this batch restarts on, compared at 32 bits against the zero-extended
// source index. Normally the all-ones value of the source type, which is what
// GL_PRIMITIVE_RESTART_FIXED_INDEX and GLES both restart on; with desktop
// GL_PRIMITIVE_RESTART it is instead whatever glPrimitiveRestartIndex named. The rebased
// tier turns whichever it is into 0xFFFFFFFF in its widened stream, which is what the
// driver restarts on.
//
// No truncation, deliberately, and the same rule ResolveRestartSubstitution applies: a
// restart index the source type cannot hold simply matches nothing, so returning it
// verbatim is already "this batch restarts nowhere".
Uint32 RestartSentinelFor(GLenum type) {
switch (type) {
case GL_UNSIGNED_BYTE: return 0xFFu;
case GL_UNSIGNED_SHORT: return 0xFFFFu;
default: return 0xFFFFFFFFu;
if (ResolveRestartSubstitution(type) != RestartSubstitutionKind::None) {
return MG_State::pGLContext->GetPrimitiveRestartIndex();
}
return MG_Util::FixedRestartIndexForGLType(type);
}
Bool RestartActive() {
@@ -275,10 +280,20 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// its remaining feasibility checks inside its implementation, where the data it
// has to walk is already in hand.
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex,
Bool hasIndexBuffer) {
Bool hasIndexBuffer, Bool arbitraryRestart) {
ResolveTierOnce();
GLESMultiDrawMode tier = g_resolvedTier;
// Desktop GL_PRIMITIVE_RESTART restarts on an application-chosen index; the driver
// only ever restarts on the all-ones value. Every tier but the rebased one hands
// the application's own index data to the driver, which would then see no restarts
// at all and weld the primitives together. The rebased tier is the one that
// REWRITES the stream, and RestartSentinelFor already tells it which value to
// translate, so it is the only tier this batch can take.
if (arbitraryRestart) {
return GLESMultiDrawMode::DrawElements;
}
// Batched tiers issue one driver entry for the whole batch, so the emulated
// gl_DrawID uniform can only hold one value across every sub-draw. A program
// that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its
@@ -414,14 +429,18 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const Uint previousIndirectBinding = BoundDrawIndirectBufferId();
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id);
if (batched) {
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
drawcount, 0);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
drawcount, 0);
});
} else {
for (GLsizei i = 0; i < drawcount; ++i) {
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
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 (feedBaseVertex) SetCurrentBaseVertex(0);
@@ -442,8 +461,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
if (count[i] <= 0) continue;
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
basevertex ? basevertex[i] : 0);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
basevertex ? basevertex[i] : 0);
});
}
if (feedDrawID) SetCurrentDrawID(0);
if (feedBaseVertex) SetCurrentBaseVertex(0);
@@ -482,6 +503,16 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const Bool restartActive = RestartActive();
const Uint32 restartSentinel = RestartSentinelFor(type);
// Widening to GL_UNSIGNED_INT gives a UBYTE/USHORT source a sentinel it can never
// spell, so those batches are lossless. A UINT source that already uses 0xFFFFFFFF as
// a real vertex index while restarting on a different one is the one shape 32 bits
// cannot express - the same corner the single-draw substitution reports.
if (restartActive && indexSize == 4 && restartSentinel != 0xFFFFFFFFu) {
MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over GL_UNSIGNED_INT multi-draw indices: "
"any index that is already 0xFFFFFFFF will restart too, because the rewritten stream "
"has no wider sentinel to move to.",
restartSentinel);
}
g_indexStaging.resize(total);
SizeT cursor = 0;
for (GLsizei i = 0; i < drawcount; ++i) {
@@ -515,8 +546,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// driver sees none - but gl_BaseVertex still has to report the value the
// application passed for this sub-draw.
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
});
cursor += static_cast<SizeT>(count[i]);
}
if (feedDrawID) SetCurrentDrawID(0);
@@ -844,8 +877,14 @@ void main() {
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex) {
if (drawcount <= 0 || !count || !indices) return;
// State-independent and possibly throwing, so it runs before any GL work.
CheckPrimitiveRestartSupported(type);
// Read before any GL work, because it decides the tier below: a desktop restart index
// the driver does not know about can only be honoured by the tier that rewrites the
// index stream (see ResolveTierForBatch). A restart index this index type cannot hold
// needs no rewrite at all - nothing can match it - but it does need the driver's own
// fixed-index restart held off for the batch, which is what the scope below does.
const RestartSubstitutionKind restartKind = ResolveRestartSubstitution(type);
const Bool arbitraryRestart = restartKind == RestartSubstitutionKind::RewriteIndices;
const ScopedSuppressedPrimitiveRestart restartCapOverride(restartKind);
const Bool hasIndexBuffer = BoundIndexBuffer() != nullptr;
@@ -870,7 +909,9 @@ void main() {
if (flattened.indexCount != 0) {
const Uint previousIndexBinding = BoundIndexBufferId();
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);
return;
}
@@ -879,7 +920,8 @@ void main() {
// the tier choice and the per-sub-draw feeds use those, not the guess above.
const Bool feedDrawID = CurrentProgramReadsDrawID();
const Bool feedBaseVertex = basevertex != nullptr && CurrentProgramReadsBaseVertex();
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer);
const GLESMultiDrawMode tier =
ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer, arbitraryRestart);
Bool drawn = false;
switch (tier) {
@@ -911,8 +953,10 @@ void main() {
// Every tier above may decline a batch whose shape it cannot express. The two
// below are the floor: a base-vertex replay where the driver has one, and the
// rewritten index stream where it does not. Both are safe for any batch these
// entry points can receive.
if (!drawn) {
// entry points can receive - except that the base-vertex replay hands the
// application's own indices to the driver, which cannot restart on a desktop
// restart index, so that batch has only the rewriting floor.
if (!drawn && !arbitraryRestart) {
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
}
if (!drawn) {
+713 -28
View File
@@ -11,7 +11,10 @@
#include "Managers.h"
#include "MG_Backend/BackendObjects.h"
#include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h"
#include "MG_Util/SelfTest/DriverBugProbes.h"
#include "MG_Util/Texture/TextureFormatProcessor.h"
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
#include <Config.h>
#include <MG_State/GLState/Core.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
@@ -25,6 +28,7 @@
#include <cmath>
#include <cctype>
#include <cstring>
#include <format>
#include <regex>
namespace MobileGL::MG_Backend::DirectGLES {
@@ -123,6 +127,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
requestedInternalFormat,
TextureImpl::GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
}
// Outside the caveat branch on purpose: the driver CAN create the native narrow
// storage - the capability probes say so - it just cannot be trusted as a raw-copy
// endpoint. Texture and renderbuffer targets both come through here, which is what
// keeps a renderbuffer -> texture copy of these formats same-ES-format when the
// widening engages.
if (TextureImpl::UsesWidenedPacked16NormStorage(internalFormat)) {
options |= PixelFormatNormalizeOptionBit::WidenPacked16Norm;
}
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
}
} // namespace
@@ -171,9 +183,45 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
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;
}
Bool UsesWidenedPacked16NormStorage(TextureInternalFormat internalFormat) {
switch (internalFormat) {
// TextureInternalFormat::RGB5 is both GL_RGB5 and GL_RGB565 - the GL-to-MG
// converter folds the two spellings onto one logical format.
case TextureInternalFormat::RGB5:
case TextureInternalFormat::RGB5A1:
case TextureInternalFormat::RGBA4:
break;
default:
return false;
}
switch (MG_Config::Features.EsprytWidenPacked16Storage) {
case MG_Config::QuirkOverride::ForceOn:
return true;
case MG_Config::QuirkOverride::ForceOff:
return false;
case MG_Config::QuirkOverride::Auto:
break;
}
// Behind the backend gate on purpose: the memoized probe latches its first answer
// for the whole process, and before the backend is up the GL function table may
// not be resolved yet - a probe run then would latch "cannot tell" as "clean"
// forever. Once the backend exists, the first narrow-format image this process
// creates runs the probe on a live context.
if (pActiveBackendObject == nullptr) {
return false;
}
return MG_Util::SelfTest::CopyImageMirrorsPacked16FieldOrder(g_GLESFuncs);
}
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType, TextureTarget target) {
#ifdef TRACY_ENABLE
@@ -227,6 +275,105 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat) {
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 PrgramImpl {
String ProcessOutColorLocations(const String& glslCode) {
@@ -569,6 +716,84 @@ namespace MobileGL::MG_Backend::DirectGLES {
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;
}
const char* PointSizeExtensionName(MG_External::GLESCapabilities::PointSizeTier tier, Bool tessellation) {
using Tier = MG_External::GLESCapabilities::PointSizeTier;
switch (tier) {
case Tier::ExtensionEXT:
return tessellation ? "GL_EXT_tessellation_point_size" : "GL_EXT_geometry_point_size";
case Tier::ExtensionOES:
return tessellation ? "GL_OES_tessellation_point_size" : "GL_OES_geometry_point_size";
default:
return nullptr;
}
}
String RequestPointSizeExtension(String glslCode, const char* extensionName) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// The gl_ViewportIndex story, one built-in over: ESSL 320 makes the tessellation and
// geometry STAGES core but leaves gl_PointSize out of their gl_PerVertex entirely,
// and SPIRV-Cross - which only ever sees a SPIR-V BuiltIn PointSize decoration -
// prints the identifier with no directive behind it. Same hard rule as the two
// neighbours: never emitted speculatively, because `#extension` on a name the driver
// does not advertise is a compile error of its own.
if (extensionName == nullptr || glslCode.find(extensionName) != String::npos) {
return glslCode;
}
const String directive = String("#extension ") + extensionName + " : require\n";
// Right after the #version line, the one position that must stay first;
// 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 directive + glslCode;
}
const SizeT lineEnd = glslCode.find('\n', versionPos);
if (lineEnd == String::npos) {
return glslCode + "\n" + directive;
}
glslCode.insert(lineEnd + 1, directive);
return glslCode;
}
String BakeImageFormatQualifiers(String glslCode,
const UnorderedMap<String, String>& esslFormatByUniformName) {
#ifdef TRACY_ENABLE
@@ -657,6 +882,86 @@ namespace MobileGL::MG_Backend::DirectGLES {
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,
const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel) {
#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";
for (Uint i = 0; i < 4; ++i) {
source += " gl_TessLevelOuter[" + std::to_string(i) +
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultOuterLevel[i]) + ";\n";
}
for (Uint i = 0; i < 2; ++i) {
source += " gl_TessLevelInner[" + std::to_string(i) +
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultInnerLevel[i]) + ";\n";
}
source += "}\n";
return source;
}
namespace {
Bool IsImagePassIdentifierChar(char c) {
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
@@ -768,6 +1073,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
struct ImageUniformDecl {
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 layout; // raw contents of layout(...)
String qualifiers; // memory/precision qualifiers, normalized, no trailing space
@@ -775,19 +1082,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
String arraySuffix; // "" or "[7]"
SizeT declStart = 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
Bool loaded = false;
Bool stored = false;
Bool unknownUse = 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
// 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.
//
// `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,
const String& variableName) {
const String& variableName, Bool forceCoherent = false) {
String out = "layout(" + decl.layout + ") uniform ";
if (forceCoherent && !ContainsIdentifier(decl.qualifiers, "coherent")) {
out += "coherent ";
}
out += memoryQualifier;
out += ' ';
if (!decl.qualifiers.empty()) {
@@ -802,11 +1125,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
return out;
}
// A name for the writeonly half that no identifier in the shader (and no other
// half already minted) can collide with.
String MakeImageWriteAliasName(const String& name, const String& source,
const Vector<String>& taken) {
String candidate = String(IMAGE_WRITE_ALIAS_PREFIX) + name;
// A name for a rewritten declaration that no identifier in the shader (and no other
// alias already minted for this stage) can collide with.
String MakeImageAliasName(const String& prefix, const String& name, const String& source,
const Vector<String>& taken) {
String candidate = prefix + name;
// "__" anywhere in an identifier is reserved (GLSL ES 3.20 3.7), which a name
// that already starts with '_' would otherwise produce.
for (SizeT doubled = candidate.find("__"); doubled != String::npos;
@@ -829,12 +1152,265 @@ namespace MobileGL::MG_Backend::DirectGLES {
SizeT length;
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
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
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#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) {
return glslCode;
}
@@ -853,10 +1429,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), imageDeclRegex), last; it != last; ++it) {
const std::smatch& match = *it;
const String qualifiers = match[2].str();
// Already legal: SPIRV-Cross decided one way, leave it alone.
if (ContainsIdentifier(qualifiers, "readonly") || ContainsIdentifier(qualifiers, "writeonly")) {
continue;
}
const Bool hasReadonly = ContainsIdentifier(qualifiers, "readonly");
const Bool hasWriteonly = ContainsIdentifier(qualifiers, "writeonly");
// 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 exemptFormat = false;
@@ -865,10 +1443,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
hasFormat = true;
exemptFormat = IsMemoryQualifierExemptImageFormat(token);
}
// No format qualifier at all is a different (and, in ES, unconditionally
// illegal) shape that GL_EXT_shader_image_load_formatted would be needed for;
// SPIRV-Cross refuses to emit it for an ES target, so nothing to do here.
if (!hasFormat || exemptFormat) continue;
// A declaration carrying neither qualifier is illegal ES unless its format is
// r32f/r32i/r32ui, and no format qualifier at all is a shape SPIRV-Cross refuses
// to emit for an ES target. Either way there is no repair to make - and no rename
// 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;
decl.layout = match[1].str();
@@ -878,6 +1458,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
decl.arraySuffix = NormalizeDeclarationSpacing(match[5].str());
decl.declStart = static_cast<SizeT>(match.position(0));
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));
}
if (decls.empty()) {
@@ -892,12 +1476,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
};
// 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 start;
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)) {
if (pos > 0 && IsImagePassIdentifierChar(glslCode[pos - 1])) continue; // uimage2D, myimageFoo
SizeT tokenEnd = pos;
@@ -942,12 +1531,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
switch (ClassifyImageBuiltin(builtin)) {
case ImageBuiltinAccess::Load:
decl.loaded = true;
useSites.push_back({declIndex, argStart, argEnd - argStart, openParen, false});
break;
case ImageBuiltinAccess::Store:
decl.stored = true;
storeSites.push_back({declIndex, argStart, argEnd - argStart});
useSites.push_back({declIndex, argStart, argEnd - argStart, openParen, true});
break;
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;
default:
decl.unknownUse = true;
@@ -964,30 +1557,122 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
Vector<ImageSourceEdit> edits;
Vector<String> takenAliases;
Vector<String> takenNames;
for (auto& decl : decls) {
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) {
decl.writeName = MakeImageWriteAliasName(decl.name, glslCode, takenAliases);
takenAliases.push_back(decl.writeName);
// Minted from the ALREADY access-tagged name, so the write half of a split
// 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;
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,
BuildImageDeclaration(decl, "readonly", decl.name) + "\n" +
BuildImageDeclaration(decl, "writeonly", decl.writeName)});
BuildImageDeclaration(decl, "readonly", decl.aliasName,
/*forceCoherent=*/true) +
"\n" +
BuildImageDeclaration(decl, "writeonly", decl.writeName,
/*forceCoherent=*/true)});
} else if (decl.stored) {
edits.push_back({decl.declStart, decl.declLength,
BuildImageDeclaration(decl, "writeonly", decl.name)});
BuildImageDeclaration(decl, "writeonly", decl.aliasName)});
} else {
// Loaded only, or only ever handed to imageSize (or unused): readonly is
// the qualifier that keeps every one of those legal.
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];
if (!decl.split) continue;
edits.push_back({site.start, site.length, decl.writeName});
// Empty exactly when the declaration was poisoned above and left untouched; its
// 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()) {
return glslCode;
+327 -12
View File
@@ -46,6 +46,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex);
// Whether this format's ES storage is widened to 8-bit-per-channel because the
// driver stores some packed16 allocations with a mirrored field order
// (PixelFormatNormalizeOptionBit::WidenPacked16Norm). True only for
// GL_RGB565/GL_RGB5(_A1)/GL_RGBA4, and only where the POST probe measured the
// divergence (or MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE forces it). The transfer paths
// consult it too: the packed-norm re-upload leg must stand down when the ES storage
// is no longer 16-bit packed.
Bool UsesWidenedPacked16NormStorage(TextureInternalFormat internalFormat);
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType,
TextureTarget target = TextureTarget::Unknown);
@@ -60,6 +69,115 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
Bool BackendRenderbufferFormatAddsAlpha(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 FramebufferImpl {} // namespace FramebufferImpl
@@ -154,6 +272,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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.
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);
// Adds `#extension <extensionName> : require` when a TESSELLATION or GEOMETRY stage's
// emitted ESSL names gl_PointSize. Desktop GL has that built-in in gl_PerVertex for every
// vertex-processing stage; ESSL does NOT have it in those two at any version - not even
// 320, where the stages themselves are core - until EXT/OES_tessellation_point_size resp.
// EXT/OES_geometry_point_size is requested. SPIRV-Cross prints the identifier bare and
// asks for nothing, exactly as it does for gl_ViewportIndex, so without this the stage
// fails to compile with "`gl_PointSize' undeclared" and the WHOLE program is replaced by
// program 0 - the draw renders nothing and any transform-feedback capture it was carrying
// is rejected outright. `extensionName` is the caller's answer, nullptr when the driver
// advertises neither spelling, because requesting an unadvertised extension is itself a
// compile error. A no-op when nullptr or already present.
String RequestPointSizeExtension(String glslCode, const char* extensionName);
// The extension name RequestPointSizeExtension should be given for `tier`, or nullptr for
// PointSizeTier::None. `tessellation` picks the tessellation spellings over the geometry
// ones; the two extensions are separate and neither implies the other.
const char* PointSizeExtensionName(MG_External::GLESCapabilities::PointSizeTier tier, Bool tessellation);
// Writes a format layout qualifier into the image declarations named in
// `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
@@ -168,9 +312,115 @@ namespace MobileGL::MG_Backend::DirectGLES {
// stops being safe to edit by hand.
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
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 the GL_PATCH_DEFAULT_OUTER_LEVEL /
// GL_PATCH_DEFAULT_INNER_LEVEL state, baked in as literals - ES has no such state and no
// glPatchParameterfv to forward to, so compiling them in is the only way to honour them.
// That makes them part of what a built program is stale against, exactly as PATCH_VERTICES
// is: see the staleness clause in DirectGLES.cpp's SyncCurrentProgram, which compares both.
//
// 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,
const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel);
// 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_";
// 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 /
// 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).
@@ -182,15 +432,74 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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.
//
// Restores a legal declaration:
// * loaded only -> add `readonly`
// * stored only -> add `writeonly`
// Restores a legal declaration, and RENAMES it after the repair it applied while doing so:
// * loaded only -> add `readonly`, rename under IMAGE_READONLY_ALIAS_PREFIX
// * stored only -> add `writeonly`, rename under IMAGE_WRITEONLY_ALIAS_PREFIX
// * both -> emit TWO declarations on the same binding and of the
// same type, `readonly <name>` and `writeonly
// <IMAGE_WRITE_ALIAS_PREFIX><name>`, and point every
// imageStore at the second one. 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.
// same type, `coherent readonly
// <IMAGE_SPLIT_READ_ALIAS_PREFIX><name>` and `coherent
// writeonly <IMAGE_WRITE_ALIAS_PREFIX><that name>`, point
// every imageStore at the second one, and follow each of
// 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
// a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a
@@ -200,8 +509,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
//
// 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
// site in Managers.cpp.
String SplitReadWriteImageUniforms(const String& glslCode);
// site in Managers.cpp. Its output is a function of the emitted text alone - it needs no
// 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
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
@@ -217,7 +532,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// avoidExplicitLodBias leaves lookups that already carry an explicit LOD untouched,
// so their constant level stays constant; only the implicit-LOD forms take the bias.
// Off by default and only ever set on ANGLE + llvmpipe, where injecting the uniform
// into a constant LOD crashes the driver (MOBILEGL_AVOID_EXPLICIT_LOD_BIAS).
// into a constant LOD crashes the driver (MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS).
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false);
} // namespace PrgramImpl
@@ -9,7 +9,9 @@
#include "BackendObject_DirectVulkan.h"
#include "MG_Backend/BackendObject.h"
#include "DirectVulkan.h"
#include "SubgroupSupportPolicy.h"
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include "MG_State/GLState/Core.h"
#include "MG_State/GLState/TextureState/TextureState.h"
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
@@ -383,6 +385,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MG_State::pGLContext) {
MG_State::pGLContext->InvalidateCompileEnv();
}
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
MutableFormatCapabilities());
PrintFormatCapabilities(GetFormatCapabilities());
@@ -495,22 +500,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.RendererName = "Magma",
.BackendName = "Direct (Vulkan)",
.ExtraVendor = Nullopt,
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
.RendererGLInfo = {.TargetGLVersion = {4, 6, 0},
.TargetGLSLVersion = {4, 6, 0},
// Baseline advertisement (no shader subgroup, no timer queries); a
// live backend reconciles its copy in UpdateAdvertisedExtensions.
.Extensions = BuildAdvertisedExtensions(false, false, false),
// Baseline advertisement (no runtime-gated capabilities); a live
// backend reconciles its copy in UpdateAdvertisedExtensions.
.Extensions = BuildAdvertisedExtensions(false, false, false, false, false),
.IsCompatibilityProfile = false},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
return rendererInfo;
}
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported) {
Bool anisotropicFilteringSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool cubeMapArraySupported) {
Vector<GLExtension> extensions = {
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
// The version tokens have to reach the version the backend actually claims:
// TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an
// application feature-detecting off these tokens the opposite of what
// GL_MAJOR_VERSION / GL_MINOR_VERSION told it.
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
V_OpenGL44, V_OpenGL45, V_OpenGL46,
E_GL_ARB_draw_buffers_blend,
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_multi_draw_indirect,
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_draw_indirect,
E_GL_ARB_multi_draw_indirect,
E_GL_ARB_indirect_parameters, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_texture_multisample,
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters,
@@ -526,11 +540,91 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3,
// so on a 4.0 context the string is the only way to reach it.
E_GL_ARB_stencil_texturing,
// Unconditional, unlike DirectGLES: a GL texture view is a second set of VkImageViews
// over the same VkImage with a sub-range and possibly a reinterpreted VkFormat, which
// is core Vulkan on every device MobileGL runs on. Format-reinterpreting views need
// VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT on the image, which SyncTextureResource sets for
// every immutable-storage texture (see the comment there).
E_GL_ARB_texture_view,
// Core since 3.2 and implemented here on both backends - glDrawElementsBaseVertex,
// glDrawRangeElementsBaseVertex, glDrawElementsInstancedBaseVertex and
// glMultiDrawElementsBaseVertex all reach real per-draw vertex rebasing. The string
// was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests
// NotSupported on a feature that works.
E_GL_ARB_draw_elements_base_vertex,
// The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync,
// glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a
// backend fence (a VkFence here, an EGLSync/GLsync on DirectGLES), and glGetInteger64v
// answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason
// ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension
// list, and a caller that finds GL_ARB_sync missing never resolves the entry points -
// then calls through null if it uses fences anyway. Nothing in the CTS gates on this
// string, so it is advertised on the strength of the implementation, not a test unlock.
E_GL_ARB_sync,
// Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole
// GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and the counter buffer
// now reaches the shader on BOTH backends - Magma resolves the lowered
// gl_AtomicCounterBlock_<N> from the atomic-counter binding points rather than the
// shader-storage ones (see ResolveStorageBufferDescriptor). Withheld here until that
// landed, because the counter silently read whatever was bound as SSBO N instead.
E_GL_ARB_shader_atomic_counters,
// glVertexAttribDivisor, core since 3.3 and real on both backends. Applications
// (Better Clouds' GLCompat among them) accept the extension string as an
// ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is
// available, so withholding it makes MobileGL look less capable than it is.
E_GL_ARB_instanced_arrays,
// Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has
// existed, and that was simply never named. None of these unlocks a single CTS case -
// the conformance suite reaches all of them through the version - so they are
// advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the
// string set, and an application that gates its ENTRY POINTS on the string rather than
// on the version never resolves them and then calls through null. Each is backed by
// the entry points named beside it. Kept identical to the DirectGLES block so the two
// backends do not disagree about what MobileGL is.
//
// glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray.
E_GL_ARB_vertex_array_object,
// The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the
// integer-valued Iiv/Iuiv forms.
E_GL_ARB_sampler_objects,
// glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent
// maps are already built on top of.
E_GL_ARB_map_buffer_range,
// glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets.
E_GL_ARB_copy_buffer,
// glCopyImageSubData, wired to a real backend hook on both backends.
E_GL_ARB_copy_image,
// GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which map onto a VkImageView's component swizzle.
E_GL_ARB_texture_swizzle,
// GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus
// the eight glVertexAttribP* entry points.
E_GL_ARB_vertex_type_2_10_10_10_rev,
// The R/RG internal formats. Named separately from the float ones because an
// application may check either.
E_GL_ARB_texture_rg,
// GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8.
E_GL_ARB_depth_buffer_float,
// The floating-point colour formats. Unlike the rest of this block this string DOES
// gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no
// core-version fallback, so eight cases per version list were NotSupported on formats
// the backend has always had.
E_GL_ARB_texture_float,
// glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} /
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
// 16 viewports GL_MAX_VIEWPORTS reports.
E_GL_ARB_viewport_array,
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1.
E_GL_ARB_get_program_binary};
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
// Vulkan's drawIndirectFirstInstance feature is optional. Direct base-instance calls work
// without it, but ARB_base_instance also promises non-zero firstInstance in GPU indirect
// commands; the renderer supplies true only when that word is legal and gl_InstanceID can
// be rebased to OpenGL's zero-based semantics.
if (nonZeroIndirectBaseInstanceSupported) {
extensions.push_back(E_GL_ARB_base_instance);
}
if (shaderSubgroupSupported && !MG_Config::Features.MagmaDisableSubgroup) {
extensions.push_back(E_GL_KHR_shader_subgroup);
}
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the Vulkan
@@ -548,12 +642,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
extensions.push_back(E_GL_KHR_parallel_shader_compile);
}
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). Every `double` in a
// shader compiles and runs already - it is narrowed to 32 bits before the module
// reaches this backend - so an application that simply uses doubles needs nothing
// advertised. What the extension additionally promises is 64-bit PRECISION, which no
// mobile GPU has and the narrowing cannot fake, so advertising it by default would
// make an application that checks the string take a path MobileGL cannot honour.
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64), and stays opt-in even on a
// device that HAS shaderFloat64. Every `double` in a shader compiles and runs either way
// - narrowed to 32 bits where the device has no 64-bit floats, kept whole where it does -
// so an application that simply uses doubles needs nothing advertised. What the extension
// additionally promises is the whole GL_ARB_gpu_shader_fp64 SURFACE (glUniform*d
// 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) {
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
}
@@ -570,6 +665,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
}
// A cube map array is a 6n-layer VkImage viewed as VK_IMAGE_VIEW_TYPE_CUBE_ARRAY, and that
// view type cannot be created without the imageCubeArray device feature - so the string
// follows the feature, not the version, exactly as the per-layer attachment bit does.
//
// Named for the application's benefit rather than the suite's: measured on Adreno 830,
// KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so
// this unlocks no conformance case. It is advertised because the feature is real and
// because an application that feature-detects cube map arrays off the string (rather than
// off the 4.0 version) would otherwise decline a path this backend serves.
if (cubeMapArraySupported) {
extensions.push_back(E_GL_ARB_texture_cube_map_array);
}
return extensions;
}
@@ -678,6 +785,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_vulkanCaps = capabilities;
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MG_State::pGLContext) {
MG_State::pGLContext->InvalidateCompileEnv();
}
MutableFormatCapabilities().Clear();
}
@@ -688,9 +798,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
// run without a renderer; no timer query is advertised then. Rebuilding
// the whole list keeps re-runs idempotent.
// The opt-in emulated compute path (SubgroupSupportPolicy.h) carries the
// extension by itself on devices with no native subgroup support at all; a
// device with native subgroups always advertises - and uses - those.
const Bool subgroupSupportAdvertised =
m_vulkanCaps.SupportsShaderSubgroup ||
ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup);
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported(),
m_vulkanCaps.SupportsImageCubeArray);
}
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
@@ -738,6 +856,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static constexpr SizeT kMaxAdvertisedShaderStorageBlockSize = 512ull * 1024ull * 1024ull;
m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment;
m_dynamicParameters.ShaderStorageBufferOffsetAlignment = m_vulkanCaps.ShaderStorageBufferOffsetAlignment;
m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin;
m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax;
// Without the samplerAnisotropy feature the limit is unusable, so report 1.0 (no anisotropy)
@@ -823,6 +942,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.MaxShaderStorageBufferBindings =
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
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(
"GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize);
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
@@ -849,8 +1000,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, 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;
// The cull pair, gated on its own feature. shaderCullDistance is separate from
// shaderClipDistance and VulkanRenderer enables it independently, so it gets its own
// gate rather than riding on the clip one.
m_dynamicParameters.MaxCullDistances =
m_vulkanCaps.SupportsShaderCullDistance ? std::max(m_vulkanCaps.MaxCullDistances, 0) : 0;
// GL 4.6 core 11.1.3.10: the combined limit is at least as large as either half. A device
// with only one of the two features must not report a combined capacity that implies the
// other, so the gate is "either feature" and the value never drops below what is enabled.
m_dynamicParameters.MaxCombinedClipAndCullDistances =
(m_vulkanCaps.SupportsShaderClipDistance || m_vulkanCaps.SupportsShaderCullDistance)
? std::max({m_vulkanCaps.MaxCombinedClipAndCullDistances, m_dynamicParameters.MaxClipDistances,
m_dynamicParameters.MaxCullDistances})
: 0;
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.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight;
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
@@ -895,26 +1073,59 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
}
}
// Never, on any device, and no longer for the reason it used to be. It used to track
// shaderFloat64 because a `dvec3` input needed the Float64 capability 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 device feature the whole fp64 story hangs off. With it, a module keeps its
// OpCapability Float64 and real doubles reach the driver; without it the transpile
// narrows every 64-bit float to 32 (ShaderTranspiler::DemoteFloat64Pass), because
// VUID-VkShaderModuleCreateInfo-pCode-08740 forbids the capability outright and no
// 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;
// shaderTessellationAndGeometryPointSize, both stage families from the one feature.
// False arms the shared phase-B point-size demotion, whose modules then carry no
// TessellationPointSize/GeometryPointSize capability and build without the feature.
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends it is absent so the demotion can be
// exercised on a healthy driver (lavapipe advertises the feature); =0 restores the
// detected answer's declines.
{
Bool supportsStagePointSize = m_vulkanCaps.SupportsTessellationAndGeometryPointSize;
switch (MG_Config::Features.PointSizeDemotion) {
case MG_Config::QuirkOverride::ForceOn:
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
"gl_PointSize as unhosted so the demotion runs on this driver");
supportsStagePointSize = false;
break;
case MG_Config::QuirkOverride::ForceOff:
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
"plain declines regardless of the device feature");
supportsStagePointSize = true;
break;
case MG_Config::QuirkOverride::Auto:
break;
}
m_dynamicParameters.SupportsTessellationPointSize = supportsStagePointSize;
m_dynamicParameters.SupportsGeometryPointSize = supportsStagePointSize;
}
// 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
// reaches a backend (ShaderTranspiler::DemoteFloat64Pass), so there is no `double` input
// left to bitcast INTO, and feeding a UINT-formatted attribute to what is now a `float`
// input would be silent garbage. Reconstructing the value would mean decoding the
// IEEE-754 double bit pattern in the shader - software fp64, which is precisely what the
// demotion exists to avoid - and on Espryt it would additionally need the ES driver to
// fetch 2N uint components where the application declared N doubles, which a dvec3 or
// dvec4 cannot even express within one attribute location.
// Re-coupling it does not work, and the reason is worth recording because it is not
// obvious: this flag decides the VkFormat from the VAO ATTRIBUTE alone, and the attribute
// does not know what the shader declared. glVertexAttribFormat(GL_DOUBLE) against a plain
// `in vec4` is not only legal but the common case
// (KHR-GL43.vertex_attrib_binding.basic-input-case4 does exactly that, and case5 adds
// normalized=GL_TRUE), and advanced-bindingUpdate feeds a dvec3 the same way - GL defines
// all of them as "doubles in memory, converted to float". Turning the flag on turns the
// narrowing OFF for every one of them and the attributes come back unfetched.
//
// So glVertexAttribLFormat / glVertexAttribLPointer are declined here exactly as they
// already were on Espryt and on every real mobile device (Adreno and Mali both report
// shaderFloat64 == VK_FALSE), and for the same visible reason. A `dvec3` INPUT still
// compiles and draws - it is a `vec3` after demotion - as long as the application feeds
// it with glVertexAttribPointer(GL_FLOAT) rather than 64-bit data.
// What keeps the two halves honest instead is a per-MODULE decision: a vertex module that
// declares a 64-bit float INPUT is demoted whole, even where the backend has native fp64,
// so `dvec` inputs are `vec` inputs on this backend exactly as they always were. See
// ShaderCompiler::SanitizeAndOptimizeBinary.
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
m_dynamicParameters.MaxShaderStorageBlockSize =
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
@@ -924,6 +1135,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.SubgroupSupportedFeatures =
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
} else if (ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup)) {
// MOBILEGL_MAGMA_EMULATE_SUBGROUP on a device with no native subgroups: the
// advertised values describe the 32-lane virtual subgroup the compute
// lowering implements (SubgroupSupportPolicy.h / EmulateSubgroupsPass).
// GL requires the advertisement and the execution to agree, and on this
// path the emulation is what executes; only the compute stage is offered.
m_dynamicParameters.SubgroupSize = kEmulatedSubgroupSize;
m_dynamicParameters.SubgroupSupportedStages = kEmulatedSubgroupStages;
m_dynamicParameters.SubgroupSupportedFeatures = kEmulatedSubgroupFeatures;
m_dynamicParameters.SubgroupQuadOperationsInAllStages = false;
MGLOG_I("DirectVulkan: emulating 32-lane compute subgroups "
"(MOBILEGL_MAGMA_EMULATE_SUBGROUP, no native subgroup support)");
} else {
m_dynamicParameters.SubgroupSize = 0;
m_dynamicParameters.SubgroupSupportedStages = 0;
@@ -62,19 +62,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// POST screen shows.
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
// versions, ExtraVendor) with the baseline extension advertisement (no shader
// subgroup, no timer queries). A live backend copies this in its constructor and
// versions, ExtraVendor) with the baseline extension advertisement (no runtime-gated
// capabilities). A live backend copies this in its constructor and
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
// exist; callers that need the advertised list for a known capability set must
// use BuildAdvertisedExtensions instead.
const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for
// a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and
// a device with the given raw capabilities. The MOBILEGL_MAGMA_DISABLE_SUBGROUP and
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
// the detected device support (passing an already-gated value is harmless).
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported);
Bool anisotropicFilteringSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool cubeMapArraySupported);
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
// string an initialized backend returns from GetBackendAPIVersionString (and that
@@ -69,6 +69,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// slot's ownership unambiguous.
Uint64 programLifetimeId = 0;
Uint32 backendStateVersion = 0;
// glShaderStorageBlockBinding deliberately does NOT bump the backend state
// version, and the pipeline composite is unnamed so the in-place patch in
// DirectVulkan::ShaderStorageBlockBinding can never reach its slot - the
// mirror replay bumps only the program's block-binding version. Without this
// key the composite's slot kept serving the pre-rebind block.binding.
Uint32 blockBindingVersion = 0;
Vector<StorageBlockResource> storageBlocks;
Vector<BufferVariableResource> bufferVariables;
GLint computeWorkGroupSize[3] = {1, 1, 1};
@@ -156,18 +162,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& cache = g_programResourceCaches[program.GetExternalIndex()];
const Uint64 programLifetimeId = program.GetLifetimeId();
const Uint32 backendStateVersion = program.GetBackendStateVersion();
const Uint32 blockBindingVersion = program.GetBlockBindingVersion();
// The lifetime id must match too: a new program that reuses a deleted
// program's name and happens to land on the same backendStateVersion (both
// count from zero) would otherwise be served the dead program's reflection.
if (cache.programLifetimeId == programLifetimeId &&
cache.backendStateVersion == backendStateVersion &&
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
if (cache.blockBindingVersion != blockBindingVersion) {
// Only the block bindings moved (glShaderStorageBlockBinding, or the
// pipeline composite's mirror replay - neither touches the backend
// state version): the reflection itself is unchanged, so re-apply the
// overrides by name instead of re-running spirv-reflect. Overrides
// only ever accumulate, so a block without one still holds its
// declared binding.
for (auto& block : cache.storageBlocks) {
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
if (rebound >= 0) block.binding = static_cast<Uint32>(rebound);
}
cache.blockBindingVersion = blockBindingVersion;
}
return cache;
}
cache = {};
cache.programLifetimeId = programLifetimeId;
cache.backendStateVersion = backendStateVersion;
cache.blockBindingVersion = blockBindingVersion;
Vector<SpvReflectShaderModule> modules;
Vector<Bool> validModules;
@@ -611,15 +632,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexSubImage2D called with null GL context");
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,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context");
pVulkanRenderer->CopyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ,
dstTexture, dstTarget, dstLevel, dstX, dstY, dstZ,
pVulkanRenderer->CopyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ,
dst, dstTarget, dstLevel, dstX, dstY, dstZ,
srcWidth, srcHeight, srcDepth);
}
void GenerateMipmap(GLenum target) {
@@ -1185,6 +1206,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
// Kind::Occlusion - pool slots recorded between Begin/End; summed at result time.
Vector<Uint32> occlusionSlots;
// Kind::XfbGenerated - reroute-pool slots for the span's XFB-INACTIVE
// draws, where the renderer's reroute is armed (the affected driver's
// stream query counts nothing without an open capture; see
// VulkanRenderer::BeginXfbQueryForDraw). Summed alongside the stream
// slots above, which keep the span's XFB-active draws.
Vector<Uint32> rerouteSlots;
// Renderer generation the records were written under (see
// g_rendererGeneration). A stale generation resolves as available
// with a final zero result: the records' pool indices and frame
@@ -1194,11 +1221,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// stale queries are always safe to delete.
Uint64 rendererGeneration = 0;
// Kind::XfbGenerated - the frontend's paused-draw primitive counter when the
// query began. VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT counts only what the
// capture saw, so a draw made while the span was paused is invisible to it -
// but GL_PRIMITIVES_GENERATED counts what the last vertex processing stage
// emitted regardless. The delta closes that gap at result time.
// query began. On the affected drivers VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT
// counts only what the capture saw, so a draw made while the span was paused is
// invisible to it - but GL_PRIMITIVES_GENERATED counts what the last vertex
// processing stage emitted regardless. The delta closes that gap at result time.
Uint64 pausedPrimitiveSnapshot = 0;
// ...unless the GPU already counted those paused draws when the span opened -
// through the reroute pool (VulkanRenderer::BeginXfbQueryForDraw reroutes every
// draw with no open capture, paused ones included) or, where the probe measured
// the stream query as counting capture-less draws, through the stream slot the
// paused draw still takes. Adding the CPU delta on top would count them twice,
// and the CPU counter is the weaker source anyway: only 3 of the ~15 draw entry
// points write it and it answers 0 for GL_PATCHES.
Bool pausedPrimitivesCountedByGpu = false;
};
} // namespace
@@ -1292,12 +1327,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (query->kind == VulkanTimerQuery::Kind::XfbWritten ||
query->kind == VulkanTimerQuery::Kind::XfbGenerated) {
Uint64 primitives = 0;
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots,
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots, query->rerouteSlots,
query->kind == VulkanTimerQuery::Kind::XfbGenerated,
primitives)) {
return false;
}
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated && MG_State::pGLContext != nullptr) {
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated &&
!query->pausedPrimitivesCountedByGpu && MG_State::pGLContext != nullptr) {
primitives += MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() -
query->pausedPrimitiveSnapshot;
}
@@ -1346,6 +1382,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
query->rendererGeneration = GetRendererGeneration();
query->pausedPrimitiveSnapshot =
MG_State::pGLContext ? MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() : 0;
// Read AFTER StartXfbQueryCapture, which is where a failed reroute-pool creation
// disarms: the answer is then what this span will actually do for every draw.
query->pausedPrimitivesCountedByGpu = generated && pVulkanRenderer->ArePausedDrawsGpuCounted();
return query;
}
@@ -1356,7 +1395,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return;
}
pVulkanRenderer->StopXfbQueryCapture(
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots);
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots,
query->rerouteSlots);
}
BackendQueryHandle BeginOcclusionQuery() {
@@ -82,9 +82,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, 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,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
@@ -201,11 +201,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.passthroughTessControlKey,
sizeof(payload.passthroughTessControlKey)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
@@ -406,8 +412,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
tessellation.patchControlPoints = payload.patchControlPoints;
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
vpci.viewportCount = 1;
vpci.scissorCount = 1;
// Both counts move together: GL has one scissor rectangle per viewport, and Vulkan
// requires viewportCount == scissorCount whenever both are dynamic
// (VUID-VkPipelineViewportStateCreateInfo-scissorCount-04136). The caller has already
// clamped this to the device's multiViewport capability.
vpci.viewportCount = std::max<Uint32>(payload.viewportCount, 1u);
vpci.scissorCount = vpci.viewportCount;
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
raster.polygonMode = payload.polygonMode;
@@ -430,6 +440,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
ms.rasterizationSamples = payload.rasterizationSamples;
ms.sampleShadingEnable = payload.sampleShadingEnable ? VK_TRUE : VK_FALSE;
// Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the
// struct's bytes match the hash the payload was keyed by.
ms.minSampleShading = payload.minSampleShading;
// GL_SAMPLE_MASK / glSampleMaski. Left at nullptr - which Vulkan reads as all-ones - until
// now, so glSampleMaski was a silent no-op on this backend while DirectGLES forwarded it.
// The pointer has to outlive the vkCreateGraphicsPipelines call, which the payload does.
ms.pSampleMask = payload.sampleMask;
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
@@ -37,11 +37,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkRenderPass renderPass = VK_NULL_HANDLE;
Uint32 colorAttachmentCount = 1;
VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading, which Vulkan bakes into the
// pipeline rather than exposing as dynamic state - so both are part of the pipeline's
// identity and both are hashed. The renderer leaves the enable false unless the
// device's sampleRateShading feature was enabled
// (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784).
Bool sampleShadingEnable = false;
Float minSampleShading = 0.0f;
// glEnable(GL_SAMPLE_MASK) + glSampleMaski, the fixed-function coverage mask, already
// reduced to what GL says this draw gets (VulkanRenderer::ResolveEffectiveSampleMask:
// all-ones unless the target is genuinely multisampled). Pipeline state like the two
// above - Vulkan has no dynamic sample mask before VK_EXT_extended_dynamic_state3 -
// so it is hashed with them, and all-ones has to keep producing the pipeline a null
// pSampleMask always did.
//
// TWO words, though GL only ever fills the first. GL_MAX_SAMPLE_MASK_WORDS is clamped
// to 1 on both backends, so glSampleMaski writes index 0 and nothing else - but the
// count Vulkan READS is ceil(rasterizationSamples / 32), which is 2 on a 64-sample
// target, and GetAdvertisedMaxSamples does not cap the driver's sample count. A
// single Uint32 here let such a pipeline read one word past the member (the next
// struct field). The second word is all-ones: full coverage for samples 32..63, which
// is the only honest answer when GL has no state describing them.
Uint32 sampleMask[2] = {0xffffffffu, 0xffffffffu};
Uint32 subpass = 0;
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
Bool primitiveRestartEnable = false;
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
Uint32 patchControlPoints = 3;
// ProgramFactory::ComputePassthroughTessControlKey of the synthesized pass-through
// tessellation control stage below, or 0 when this pipeline has none. Hashed, because
// the levels glPatchParameterfv set are compiled INTO that module and are not a
// function of the program or of patchControlPoints - see the note on
// passthroughTessControlStage.
Uint64 passthroughTessControlKey = 0;
// How many of ARB_viewport_array's viewports this pipeline rasterizes into. 1 for
// every program that never assigns gl_ViewportIndex, which is all of them outside the
// conformance suite - the wide shape costs a longer vkCmdSetViewport/Scissor per state
// change and can cost hardware fast paths, so it is opt-in per program. Baked into the
// pipeline (viewportCount is not dynamic without VK_EXT_extended_dynamic_state) and
// therefore hashed; the DYNAMIC viewport/scissor arrays the draw pushes must have
// exactly this many elements (VUID-vkCmdDraw-viewportCount-03417/-03418).
Uint32 viewportCount = 1;
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
@@ -79,8 +115,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// renderer could not build one, and CreatePipeline refuses the pipeline - the same
// refusal it applies when `stages` itself is half-tessellated.
//
// NOT hashed: it is a pure function of the program and of patchControlPoints, both
// of which ComputeHash already mixes in.
// NOT hashed directly: it is a pure function of the program, of patchControlPoints and
// of the default tessellation levels - the first two of which ComputeHash already
// mixes in, and the third of which arrives through passthroughTessControlKey above.
VkPipelineShaderStageCreateInfo passthroughTessControlStage{};
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
File diff suppressed because it is too large Load Diff
@@ -76,6 +76,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
using CompileOptionFlags = Flags<CompileOptionBit>;
using HashType = Uint64;
// The gl_PerVertex members a pass-through tessellation control stage may have to carry,
// in the order glslang declares them - which is the order a redeclaration must use.
// Which of them exist is a function of the neighbouring stage's GLSL VERSION
// (gl_CullDistance joins the block at #version 450), so the mask is read off that
// stage's SPIR-V rather than assumed. See ReflectPerVertexInputMembers.
enum class PerVertexMemberBit : Uint32 {
Position = 1u << 0,
PointSize = 1u << 1,
ClipDistance = 1u << 2,
CullDistance = 1u << 3,
};
// What a program parsed below #version 450 carries, and the fallback when a module's
// block cannot be read.
static constexpr Uint32 kDefaultPerVertexMembers =
static_cast<Uint32>(PerVertexMemberBit::Position) | static_cast<Uint32>(PerVertexMemberBit::PointSize) |
static_cast<Uint32>(PerVertexMemberBit::ClipDistance);
struct UpdateAfterBindLimits {
Bool enabled = false;
Uint32 maxPerStageSamplers = 0;
Uint32 maxPerStageUniformBuffers = 0;
Uint32 maxPerStageStorageBuffers = 0;
Uint32 maxPerStageSampledImages = 0;
Uint32 maxPerStageStorageImages = 0;
Uint32 maxPerStageResources = 0;
Uint32 maxSetSamplers = 0;
Uint32 maxSetUniformBuffers = 0;
Uint32 maxSetUniformBuffersDynamic = 0;
Uint32 maxSetStorageBuffers = 0;
Uint32 maxSetStorageBuffersDynamic = 0;
Uint32 maxSetSampledImages = 0;
Uint32 maxSetStorageImages = 0;
};
struct VkProgramObject {
static constexpr Uint32 kMaxVertexInputLocations = 32;
@@ -88,6 +122,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Layout data (previously in separate VkProgramLayout)
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
// True only when this layout passed every descriptor-indexing feature and
// update-after-bind limit gate at reflection time. It controls both the
// layout/binding flags and the pool class used by UniformManager.
Bool usesUpdateAfterBind = false;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
Vector<DescriptorBindingKind> bindingKinds;
// The bindings this program actually declares, ascending. bindingKinds is sized to the
@@ -151,6 +189,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// PROGRAM rather than of the variant: the zeroed variant leaves the variable
// declared, so both variants answer the same and the draw path can ask either.
Bool readsBaseVertexBuiltin = false;
// Some pre-rasterization stage assigns gl_ViewportIndex. Its pipeline declares
// viewportCount = the renderer's rasterizable viewport count instead of 1, and its
// draws push the whole viewport/scissor array; every other program keeps the
// single-viewport fast path untouched. Part of the program's identity (folded into
// the pipeline hash through programHash), so no memo can serve the wrong shape.
Bool writesViewportIndexBuiltin = false;
// This program has a tessellation EVALUATION stage and no tessellation CONTROL
// stage. GL allows that (4.6 core 11.2.2: with no control shader the input patch
// is passed through unmodified, the output patch size is PATCH_VERTICES, and the
@@ -158,6 +202,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// tessellation stages are present or neither
// (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). So the draw path has to supply
// the pass-through stage GL describes; see GetOrCreatePassthroughTessControlStage.
// True when this program was built AS a transform-feedback capture variant but its
// last pre-rasterization module does NOT carry the Xfb execution mode - so the
// renderer must decline the capture span instead of issuing
// vkCmdBeginTransformFeedbackEXT against it
// (VUID-vkCmdBeginTransformFeedbackEXT-None-04128).
//
// Two ways to get here, and neither is visible from GL state, which is all
// BeginXfbCaptureForDraw otherwise consults: the clip/XFB validation backstop had to
// rewind past the capture decoration, or XfbCaptureDecoratePass resolved none of the
// requested varyings and returned without changing anything (its own MGLOG_E path)
// while its runner still reported success. Both used to ship a non-Xfb module under
// an Xfb-flagged cache entry - the flag and the layout are part of the program cache
// key, so it was sticky for every later captured draw of the program, not a glitch.
Bool xfbCaptureDeclined = false;
// The program has a tessellation or geometry module declaring TessellationPointSize /
// GeometryPointSize on a device whose shaderTessellationAndGeometryPointSize feature
// is off, so a pipeline built from it is invalid usage
// (VUID-RuntimeSpirv-PointSize-06439). Its draws are refused in SetupDraw rather than
// handed to the driver - the same contract PipelineFactory's half-tessellated refusal
// implements one level up, and the counterpart of the DirectGLES arm that reports a
// driver with neither point-size extension by name.
//
// Sticky by construction, which is what makes ONE log line honest: the flag lives on
// the cache entry, so every later draw of the same program variant reads the same
// answer instead of re-deciding it.
Bool pointSizeCapabilityUnsupported = false;
Bool needsPassthroughTessControl = false;
// ...and the pass-through this renderer can synthesize carries gl_Position and
// nothing else, so it is only correct when the evaluation stage's inputs are
@@ -167,6 +237,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// instead (PipelineFactory::CreatePipeline refuses the pipeline and the draw is
// skipped). See ReflectPassthroughTessControlNeed.
Bool passthroughTessControlEmulatable = false;
// Which gl_PerVertex members the evaluation stage's `in gl_PerVertex gl_in[]` block
// actually carries, as a PerVertexMemberBit mask read off its SPIR-V. The synthesized
// control stage has to redeclare the SAME shape: glslang appends gl_CullDistance to
// that block from #version 450 upward, so a 450/460 program - and every ESSL program,
// which the source processor rewrites to "#version 460 core" - carries four members
// where a 430 program carries three. A fixed three-member pass-through fed the
// evaluation stage a differently-shaped block, which is the black-frame-no-error case
// this whole family is written around.
Uint32 passthroughPerVertexMembers = 0;
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
// entry pointer re-stamps use through a const reference (StampProgramUse).
@@ -190,6 +269,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// a pipeline failure would be reported against the wrong SPIR-V.
stageSpirvDigests = std::move(other.stageSpirvDigests);
descriptorSetLayout = other.descriptorSetLayout;
usesUpdateAfterBind = other.usesUpdateAfterBind;
pipelineLayout = other.pipelineLayout;
bindingKinds = std::move(other.bindingKinds);
activeBindings = std::move(other.activeBindings);
@@ -218,11 +298,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
needsPassthroughTessControl = other.needsPassthroughTessControl;
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
lastUsedFrame = other.lastUsedFrame;
other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE;
other.usesUpdateAfterBind = false;
other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false;
other.declinedDescriptors = false;
@@ -234,8 +317,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
other.readsBaseVertexBuiltin = false;
other.writesViewportIndexBuiltin = false;
other.needsPassthroughTessControl = false;
other.passthroughTessControlEmulatable = false;
other.passthroughPerVertexMembers = 0;
other.lastUsedFrame = 0;
}
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
@@ -248,6 +333,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
modules = std::move(other.modules);
stageSpirvDigests = std::move(other.stageSpirvDigests); // travels with `modules` - see the move ctor
descriptorSetLayout = other.descriptorSetLayout;
usesUpdateAfterBind = other.usesUpdateAfterBind;
pipelineLayout = other.pipelineLayout;
bindingKinds = std::move(other.bindingKinds);
activeBindings = std::move(other.activeBindings);
@@ -276,11 +362,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
needsPassthroughTessControl = other.needsPassthroughTessControl;
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
lastUsedFrame = other.lastUsedFrame;
other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE;
other.usesUpdateAfterBind = false;
other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false;
other.declinedDescriptors = false;
@@ -292,8 +381,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
other.readsBaseVertexBuiltin = false;
other.writesViewportIndexBuiltin = false;
other.needsPassthroughTessControl = false;
other.passthroughTessControlEmulatable = false;
other.passthroughPerVertexMembers = 0;
other.lastUsedFrame = 0;
return *this;
}
@@ -337,12 +428,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
};
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
Bool shaderDrawParametersEnabled = false,
Bool unformattedFloatStorageImagesEnabled = false)
// How this factory's compute modules implement GL_KHR_shader_subgroup. Computed
// once at renderer initialization (SubgroupSupportPolicy.h + the device's
// subgroup properties) so lowering can never disagree with the advertised
// capabilities. Native subgroup operations always execute natively; the two
// repair passes patch modules AROUND them, and the emulation only replaces them
// on opted-in devices with no subgroup support at all.
struct SubgroupLoweringPolicy {
Bool emulateSubgroups = false; // MOBILEGL_MAGMA_EMULATE_SUBGROUP, no-native-support devices
Bool fixIterationRPSubgroupScratch = false; // patch iterationRP's under-declared scratch
Bool fixIterationRPBarrier = false; // repair Program 203's shared-scratch race
Bool deriveNumSubgroups = false; // repair the NumSubgroups builtin
Bool requireFullSubgroups = false; // computeFullSubgroups enabled on the device
Uint32 nativeSubgroupSize = 0;
// Full-subgroup launches are bounded by this device limit; a dispatch whose
// workgroup needs more subgroups than this cannot request the flag.
Uint32 maxComputeWorkgroupSubgroups = 0;
// VkPhysicalDeviceLimits::maxComputeSharedMemorySize; bounds the scratch the
// emulation pass may add (0 falls back to the Vulkan minimum, 16384).
Uint32 maxComputeSharedMemoryBytes = 0;
};
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings,
Bool shaderDrawParametersEnabled,
Bool unformattedFloatStorageImagesEnabled,
Bool tessellationAndGeometryPointSizeEnabled,
Bool enableSpirvValidation,
UpdateAfterBindLimits updateAfterBindLimits,
SubgroupLoweringPolicy subgroupPolicy)
: m_device(device), m_maxBindings(maxBindings), m_config(config),
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled),
m_tessellationAndGeometryPointSizeEnabled(tessellationAndGeometryPointSizeEnabled),
m_enableSpirvValidation(enableSpirvValidation),
m_updateAfterBindLimits(updateAfterBindLimits),
m_subgroupPolicy(subgroupPolicy) {
VkProgramObject::s_device = device;
}
// Destroys the pass-through tessellation control modules. Runs while the device is
@@ -386,6 +506,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
// The same question for an IMAGE uniform (`image2D`, `uimageBuffer`, ...), which the
// sampler form above deliberately does not answer. Kept separate rather than folded in
// because the two are asked in different places for different reasons: a sampler's domain
// decides a sampled VIEW format, an image's decides what a placeholder descriptor for an
// UNBOUND image unit must be (see UniformManager::AcquireUnboundTexelBufferView and
// GetUnboundStorageImageTexture) - a formatless `writeonly` declaration reflects no
// format at all, and the numeric domain is then the only thing that constrains it.
static SamplerNumericDomain UniformTypeToImageNumericDomain(GLenum glType);
// True when any entry point declares the DepthReplacing execution mode, i.e. the
// shader assigns gl_FragDepth. Exposed so the blended depth-write quirk's exemption
// can be pinned by tests. A false negative loses the exemption, so such a shader is
@@ -400,6 +528,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Shared by the two above: does any entry point list an input variable decorated with
// this builtin?
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
// True when an entry point writes the ViewportIndex builtin (gl_ViewportIndex), i.e. when
// the program can route primitives to a viewport other than 0 and its pipeline therefore
// has to declare more than one. Asks about OUTPUT variables because that is the direction
// a pre-rasterization stage declares it in.
static Bool ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule);
static Bool ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes for a
// program that has an evaluation stage and no control stage, for an input patch of
@@ -409,18 +543,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// the caller then has no control stage to inject, and CreatePipeline refuses the
// pipeline rather than handing the driver a half-tessellated one.
//
// Keyed on the patch size because GL takes the output patch size from PATCH_VERTICES,
// which is draw state, not link state - the CTS case that motivated this links at the
// default 3 and draws at 4. The pipeline cache already re-keys on patchControlPoints,
// so the module a pipeline was built with is part of that pipeline's identity.
// Compiling is bounded by the number of distinct patch sizes a program draws with
// (MAX_PATCH_VERTICES = 32 in the worst case, one or two in practice) and only ever
// happens for the rare program that has no control stage at all.
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices);
// Keyed on the patch size, the six default tessellation levels AND the gl_PerVertex
// member set, because all three decide what the generator emits. The size comes from
// PATCH_VERTICES and the levels from PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL
// - draw state rather than link state, and the CTS case that motivated this links at the
// default 3 and draws at 4. The member set comes from the neighbouring evaluation stage's
// own SPIR-V, so two programs at different GLSL versions need different modules. The
// pipeline cache re-keys on the same inputs, so the module a pipeline was built with is
// part of that pipeline's identity. Compiling is bounded by the number of distinct
// (size, levels, members) combinations a program draws with - one or two in practice -
// and only ever happens for the rare program that has no control stage at all.
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices,
const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel,
Uint32 perVertexMembers);
// Source of the module above. Exposed for tests: the generated GLSL is the whole
// contract with the evaluation stage, so it is worth pinning independently of a device.
static String BuildPassthroughTessControlSource(Uint32 patchVertices);
static String BuildPassthroughTessControlSource(Uint32 patchVertices, const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel, Uint32 perVertexMembers);
// The identity of one such module: everything the generator bakes in, folded into a
// 64-bit key over the raw bits (so -0.0 and +0.0 key apart, which is harmless, and NaN
// keys to itself, which is what matters). Shared with PipelineFactory, which mixes the
// same value into the pipeline hash so a pipeline can never be handed a module built for
// different levels or a different block shape.
static Uint64 ComputePassthroughTessControlKey(Uint32 patchVertices, const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel, Uint32 perVertexMembers);
// The PerVertexMemberBit mask of the INPUT per-vertex block a module declares, read
// straight out of its SPIR-V (OpMemberDecorate ... BuiltIn on the struct behind the one
// Input variable that is an array of a Block-decorated struct). Zero when the module has
// no such block. Exposed for tests, which is the only way to pin the shape agreement
// without a device.
static Uint32 ReflectPerVertexInputMembers(const Vector<Uint>& spirv);
private:
struct ProgramLookupCache {
@@ -431,10 +587,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
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,
VkProgramObject& entry) const;
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
void ReflectViewportIndexUsage(const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
void ReflectFragmentOutputs(const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
@@ -442,7 +605,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked
// modules. Const and reflection-only: it decides nothing about the pipeline, it only
// 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,
VkProgramObject& entry) const;
@@ -456,6 +619,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// True only when the logical device enabled both
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
Bool m_unformattedFloatStorageImagesEnabled = false;
// True when the logical device enabled shaderTessellationAndGeometryPointSize. When it is
// FALSE a program whose tessellation or geometry module declares TessellationPointSize /
// GeometryPointSize is refused at build time (see VkProgramObject::
// pointSizeCapabilityUnsupported) instead of being handed to the driver as invalid usage.
Bool m_tessellationAndGeometryPointSizeEnabled = false;
// Startup snapshot used only by internally synthesized shader modules, which do not
// originate from a ProgramLinkTask.
Bool m_enableSpirvValidation = false;
// Device feature and limit gate resolved before vkCreateDevice. Keeping it in
// the factory lets each reflected layout choose ordinary descriptors when its
// own counts would exceed the update-after-bind budget.
UpdateAfterBindLimits m_updateAfterBindLimits{};
SubgroupLoweringPolicy m_subgroupPolicy{};
// See SetDefaultFramebufferHeight. 0 means "not known yet"; the FragCoordYFlip bit is
// never set before the swapchain exists, so no variant can be compiled against it.
Uint32 m_defaultFramebufferHeight = 0;
@@ -465,11 +641,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
Uint64 m_cacheStructureEpoch = 1;
IEvictionObserver* m_evictionObserver = nullptr;
// Pass-through tessellation control stages by input patch size. Never evicted: at most
// MAX_PATCH_VERTICES entries exist for the lifetime of the device, and every pipeline
// ever built from one keeps referencing its module. A failed build is cached as
// Pass-through tessellation control stages by the identity of what was compiled into
// them - the input patch size and the six default tessellation levels, folded into one
// 64-bit key by ComputePassthroughTessControlKey (the levels are float state, so the map
// cannot simply be keyed on the patch size any more). A failed build is cached as
// VK_NULL_HANDLE so a broken generator costs one compile, not one per draw.
UnorderedMap<Uint32, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
//
// Hard-capped, because the key is application-controlled: glPatchParameterfv clamps
// nothing, so an application that recomputes a level per frame mints a new key per frame.
// Reaching the cap destroys every module and starts over (see the flush in
// GetOrCreatePassthroughTessControlStage); the cap is far above what any program that
// holds its levels still will ever need. The gl_PerVertex member set is in the key too
// and adds only a handful of values, so it does not move the cap in practice.
static constexpr SizeT kMaxPassthroughTessControlStages = 64;
UnorderedMap<Uint64, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -26,12 +26,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
public:
struct SamplerBindingOverride {
Uint32 binding = 0;
Uint32 element = 0;
MG_State::GLState::ITextureObject* texture = nullptr;
const MG_State::GLState::SamplerObject* sampler = nullptr;
VkImageView imageView = VK_NULL_HANDLE;
VkImageLayout imageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
Bool forceNearestFiltering = false;
};
Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
struct SamplerImageFeedbackBinding {
Uint32 samplerBinding = 0;
Uint32 samplerElement = 0;
MG_State::GLState::ITextureObject* texture = nullptr;
const MG_State::GLState::SamplerObject* sampler = nullptr;
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
};
// `physicalDevice` is only ever asked for format properties: a placeholder descriptor for
// an unbound texel-buffer binding has to be built from a format the DEVICE accepts as a
// texel buffer, and there is no other route to that answer from here.
Bool Initialize(VkDevice device, VkPhysicalDevice physicalDevice, VkBufferManager* bufferManager,
ProgramFactory* programFactory,
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
Uint32 maxBindings = 16, Uint32 setsPerFrame = 64,
@@ -79,6 +93,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
Bool CollectSamplerImageFeedback(
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<SamplerImageFeedbackBinding>& outBindings) const;
static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
GLint imageLevel, GLenum imageAccess);
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
// every input of every combined-image-sampler resolution is unchanged since the
// previous draw's resolve - same (texture, sampler) per binding, texture params
@@ -91,7 +111,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 frameIndex,
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
const SamplerBindingOverride* samplerBindingOverride = nullptr,
Bool samplerDescriptorsUnchangedHint = false);
Bool samplerDescriptorsUnchangedHint = false,
const Vector<SamplerBindingOverride>* samplerBindingOverrides = nullptr);
// Pure format-policy helper kept public for host regression tests. Formatted storage
// images use their shader qualifier; transformed float images use glBindImageTexture's
@@ -114,6 +135,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorPool handle = VK_NULL_HANDLE;
Uint32 maxSets = 0;
Uint32 allocatedSets = 0;
Bool updateAfterBind = false;
};
// A cached descriptor set together with the pool it was allocated from, so a
@@ -157,7 +179,43 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
// `numericDomain` is the sampler's class, and it matters only for the multisample arm -
// see GetFallbackMultisampleTexture for why the single-sampled fallback can ignore it.
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(
TextureTarget target, SamplerNumericDomain numericDomain) const;
// The multisample arm of GetFallbackTexture. One object per (target, numeric domain) and
// no upload path: a multisample image cannot be written by a transfer, so its texels stay
// undefined - which is what GL promises for a texelFetch on an incomplete multisample
// texture - and it cannot carry MUTABLE_FORMAT, so its format has to match the sampler's
// class outright rather than being reinterpreted at view time.
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackMultisampleTexture(
TextureTarget target, SamplerNumericDomain numericDomain) const;
// ---- placeholders for UNBOUND image-backed descriptors -------------------------
// GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing
// to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete
// buffer texture, 8.26 for an image unit with no texture) - undefined VALUES, not a
// dropped draw. Vulkan has no unwritten descriptor, so something valid has to sit in the
// set or the whole draw or dispatch is lost, which is what these two build. Same shape as
// VkBufferManager::AcquireUnboundStorageDescriptor, one level up: per FORMAT rather than
// one shared object, because a descriptor whose format disagrees with the shader's
// declaration is invalid Vulkan even when nothing ever reads it.
//
// `declaredFormat` is the format the SHADER declared (VK_FORMAT_UNDEFINED for a sampled
// texel buffer, which never carries one, or for a formatless `writeonly` image);
// `numericDomain` decides the format when there is no declaration and is the fallback
// class when the device cannot use the declared one as a texel buffer.
VkBufferView AcquireUnboundTexelBufferView(VkFormat declaredFormat, SamplerNumericDomain numericDomain,
Bool storage);
// A 1x1 (x1 layer, or 6 faces for a cube) texture of `format`, shaped for `target` so the
// view the descriptor gets has the view type the shader's image declaration demands.
// Null for a target with no single-sampled placeholder shape - multisample images, whose
// descriptor needs a multisample view that this cannot stand in for.
SharedPtr<MG_State::GLState::ITextureObject> GetUnboundStorageImageTexture(TextureTarget target,
VkFormat format) const;
// The (target, format) pair a storage-image binding's placeholder is keyed by, resolved
// from reflection alone. False when the binding has no placeholder shape.
Bool ResolveUnboundStorageImagePlaceholder(const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
TextureTarget& outTarget, VkFormat& outFormat) const;
// `element` indexes a sampler ARRAY inside one binding; each element carries its own
// independently assigned GL texture unit, so it selects the texture, the sampler
// override and the fallback separately from its neighbours.
@@ -223,8 +281,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
const Vector<Uint32>& dynamicOffsets);
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
Bool CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const;
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind);
VkResult AllocateDescriptorSetsFromActivePool(
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
VkResult AcquireDescriptorSet(Uint32 frameIndex,
@@ -232,6 +290,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorSet& outDescriptorSet);
VkDevice m_device = VK_NULL_HANDLE;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
VkBufferManager* m_bufferManager = nullptr;
ProgramFactory* m_programFactory = nullptr;
Vector<FrameResources> m_frames;
@@ -244,6 +303,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkTextureManager* m_textureManager = nullptr;
VkSamplerManager* m_samplerManager = nullptr;
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
// Keyed by (arrayed, numeric domain); see GetFallbackMultisampleTexture. Lazily populated,
// never evicted - at most six tiny 1x1 images - and torn down with the manager.
mutable UnorderedMap<Uint32, SharedPtr<MG_State::GLState::ITextureObject>> m_fallbackMultisampleTextures;
// See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily
// populated, never evicted (a program's declared formats are a fixed, tiny set) and torn
// down with the manager. The texel views are keyed by format AND by storage-vs-sampled
// because the two descriptor kinds demand different format FEATURES of the device, so one
// format can be usable for one and not the other. Deliberately NOT the per-frame
// texelBufferViews list: those are destroyed at every frame boundary, and these must
// outlive it or the placeholder would be rebuilt for every unbound binding every frame.
UnorderedMap<Uint64, VkBufferView> m_unboundTexelBufferViews;
mutable UnorderedMap<Uint64, SharedPtr<MG_State::GLState::ITextureObject>> m_unboundStorageImageTextures;
// Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps
// capacity) so the descriptor-write path stops allocating on every draw.
@@ -341,8 +412,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// lifetime id, so a freed-and-reallocated sampler or texture at the same heap address
// always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a
// stale guess can only miss and fall through to the hash, never resolve wrong. Still
// reset each frame alongside the descriptor-set cache. Indexed by binding.
// reset each frame alongside the descriptor-set cache. Indexed by binding, but the
// whole-descriptor entry is additionally keyed by program lifetime: Vulkan binding
// numbers are layout-local and unrelated programs routinely reuse binding 0/1.
struct SamplerResolveMemo {
Uint64 infoProgramLifetimeId = 0;
Uint64 samplerLifetimeId = 0;
Uint64 textureLifetimeId = 0;
VkSampler sampler = VK_NULL_HANDLE;
@@ -8,6 +8,7 @@
#include "VertexInputStateFactory.h"
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
#include <MG_Backend/BackendObjects.h>
#include <utility>
namespace MobileGL::MG_Backend::DirectVulkan {
@@ -107,8 +108,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue;
}
const VkFormat sourceVkFormat =
VkFormat sourceVkFormat =
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) {
MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
"enabled but cannot be mapped to a VkFormat",
@@ -118,8 +145,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
VkFormat vkFormat = sourceVkFormat;
VertexStreamConversion conversion = VertexStreamConversion::None;
if (!SupportsVertexBufferFormat(vkFormat)) {
if (conversion == VertexStreamConversion::None && !SupportsVertexBufferFormat(vkFormat)) {
if (IsScaledIntegerVertexFormat(vkFormat)) {
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
@@ -188,7 +214,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (sourceStride != 0) {
if (conversion == VertexStreamConversion::Repack) {
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)));
}
}
@@ -287,8 +314,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
it = m_cache.erase(it);
// Invalidate every VAO's state-pointer memo: the erased node's
// address may be reused by a future insert.
++m_evictionEpoch;
// address may be reused by a future insert. Advance through the
// process-wide source so the value stays unique across factory
// instances (see the member comment).
m_evictionEpoch = ++s_evictionEpochSource;
} else {
++it;
}
@@ -328,6 +357,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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,
// 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;
switch (size) {
case 1: return VK_FORMAT_R32G32_UINT;
@@ -23,6 +23,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
None = 0,
Repack,
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 {
@@ -125,7 +128,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// construction); a memo is honored only while its recorded epoch
// matches, so an evicted entry can never be dereferenced through a
// stale memo.
Uint64 m_evictionEpoch = 1;
//
// Drawn from a process-wide source, never a per-instance counter: the VAO
// memos outlive this factory (they live on pGLContext's VAOs, the renderer
// is destroyed and recreated on EGL surface release/re-create), so a fresh
// factory restarting at a dead factory's epoch value would honor its
// dangling entry pointers. The constructor takes a value strictly greater
// than anything a predecessor ever stamped, so a dead factory's memo can
// never compare equal here - the same never-reused idiom as the lifetime ids.
// Single-threaded like the rest of the factory (renderer-thread only).
static inline Uint64 s_evictionEpochSource = 0;
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -16,6 +16,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
constexpr SizeT kLiveResourcePruneThreshold = 256;
// See VkBufferManager::AcquireUnboundStorageDescriptor. 256 bytes: comfortably past
// every minStorageBufferOffsetAlignment in the wild, and free.
constexpr VkDeviceSize kUnboundStorageDescriptorBytes = 256;
// See VkBufferManager::AcquireUnboundTexelBufferDescriptor. The same 256 bytes, for the
// same reason plus one: a texel buffer view's range must be a whole number of texels of
// whatever format the placeholder is asked for, and 256 divides by every texel size in
// the GL image-format table (1, 2, 4, 8 and 16 bytes).
constexpr VkDeviceSize kUnboundTexelBufferDescriptorBytes = 256;
// A zero-copy persistent buffer is created once and never recreated (the app holds
// its mapped pointer), and may be bound to any role, so it carries every usage.
// TRANSFER_DST is added by CreateResidentStorage.
@@ -130,6 +139,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
m_transientUploadArena.Shutdown();
m_unboundStorageBuffer.Destroy();
m_unboundTexelBuffer.Destroy();
DestroyAllDeferredReleases();
ReleaseAllLiveResources();
m_copyProvider = nullptr;
@@ -706,6 +717,70 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_deferredResourceReleases[frameIndex].clear();
}
BufferSlice VkBufferManager::AcquireUnboundStorageDescriptor() {
if (!m_unboundStorageBuffer.IsValid()) {
if (m_initInfo.allocator == nullptr) {
return {};
}
// Host-visible so the zero fill needs no command buffer: this can be reached from
// descriptor resolution, which runs inside an already-open recording and must not
// start a copy of its own. The size is a whole minStorageBufferOffsetAlignment-safe
// block rather than 4 bytes so that a shader which does read the block gets a
// plausible unsized-array length instead of one that rounds to zero.
const Bool created = m_unboundStorageBuffer.Create({
.allocator = m_initInfo.allocator,
.size = kUnboundStorageDescriptorBytes,
.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
VMA_ALLOCATION_CREATE_MAPPED_BIT,
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
});
if (!created) {
MGLOG_E_ONCE("VkBufferManager::AcquireUnboundStorageDescriptor: placeholder creation failed");
m_unboundStorageBuffer.Destroy();
return {};
}
if (void* mapped = m_unboundStorageBuffer.GetMappedData()) {
Memset(mapped, 0, static_cast<SizeT>(kUnboundStorageDescriptorBytes));
}
}
return m_unboundStorageBuffer.GetSlice();
}
BufferSlice VkBufferManager::AcquireUnboundTexelBufferDescriptor() {
if (!m_unboundTexelBuffer.IsValid()) {
if (m_initInfo.allocator == nullptr) {
return {};
}
// A SECOND placeholder rather than more usage bits on the storage-block one. The two
// are independent failure domains: a device that refuses this allocation must not
// take the storage-block placeholder - and with it the fix this one is a sibling of -
// down with it. Host-visible and zero-filled for the same reason as that one: this is
// reached from descriptor resolution, inside an already-open recording, which must
// not start a copy of its own.
const Bool created = m_unboundTexelBuffer.Create({
.allocator = m_initInfo.allocator,
.size = kUnboundTexelBufferDescriptorBytes,
.usage = VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_DST_BIT,
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
VMA_ALLOCATION_CREATE_MAPPED_BIT,
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
});
if (!created) {
MGLOG_E_ONCE("VkBufferManager::AcquireUnboundTexelBufferDescriptor: placeholder creation failed");
m_unboundTexelBuffer.Destroy();
return {};
}
if (void* mapped = m_unboundTexelBuffer.GetMappedData()) {
Memset(mapped, 0, static_cast<SizeT>(kUnboundTexelBufferDescriptorBytes));
}
}
return m_unboundTexelBuffer.GetSlice();
}
VkBufferUsageFlags VkBufferManager::GetVkBufferUsage(BufferKind kind) {
switch (kind) {
case BufferKind::Vertex:
@@ -120,6 +120,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size,
VkDeviceSize alignment, BufferSlice& outSlice);
// The descriptor a shader storage block gets when the program declares it and the
// application bound no buffer at its GL binding point. GL 4.6 core 7.8 makes that a
// legal state - the block simply has no store, so reads are undefined and writes go
// nowhere - whereas Vulkan has no such thing as an unwritten descriptor, so something
// real has to sit in the set or the whole draw/dispatch is lost. One zero-filled
// buffer, created once and shared by every unbound binding: bindings that are only
// declared (the case this exists for) never touch it, and one that is actually read
// sees zeros, which is inside GL's "undefined". robustBufferAccess bounds anything
// that indexes past it.
BufferSlice AcquireUnboundStorageDescriptor();
// The store a texel-buffer descriptor - `samplerBuffer` or `imageBuffer` - gets when the
// unit the program's uniform names has no buffer texture on it, or the buffer texture on
// it has no GL buffer attached. Both are legal GL states that make a fetch return
// undefined values (GL 4.6 core 8.9: a buffer texture with no attached buffer object is
// incomplete, and sampling an incomplete texture is undefined - not a lost draw), and both
// used to take the whole draw or dispatch with them. The VIEW over this - one per format,
// and the descriptor is a VkBufferView, not a buffer - is built by
// UniformManager::AcquireUnboundTexelBufferView.
BufferSlice AcquireUnboundTexelBufferDescriptor();
// Draw-time acquire for resident (device-storage) buffers: ensures the
// resource exists and is fully uploaded, marks it used this frame.
Bool AcquireResidentSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
@@ -180,6 +201,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkBufferManagerInitInfo m_initInfo{};
BufferArena m_transientUploadArena;
// See AcquireUnboundStorageDescriptor. Lazily created, never re-created, torn down
// with the manager.
VkBufferObject m_unboundStorageBuffer;
// See AcquireUnboundTexelBufferDescriptor. Same lifetime rules.
VkBufferObject m_unboundTexelBuffer;
IBufferCopyCommandProvider* m_copyProvider = nullptr;
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
@@ -8,6 +8,10 @@
#include "VkClearManager.h"
// For the shared ResolveAttachmentLayerCount (and the ToVulkanLevelExtent it is built on): the
// clear key's layer span has to be the same one the render pass builds its attachment view from.
#include "VkTextureManager.h"
#include "MG_State/GLState/Core.h"
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
@@ -100,13 +104,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return ResolveAttachmentBaseArrayLayer(uploadTarget);
}
static Uint32 ResolveAttachmentLayerCount(
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (attachment.IsLayered()) {
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1));
}
return 1u;
}
// ResolveAttachmentLayerCount used to be duplicated here, reading attachment.GetSize().z()
// raw - no ToVulkanLevelExtent remap for a 1D array, no six-faces arm for a cube map. That is
// not a cosmetic difference: the count below is not key-only, it is written straight into
// VkImageSubresourceRange::layerCount by MaterializePendingClearForTexture, which then POPS
// the entry - so a layered cube map's glClear reached one face and the other five were lost
// for good, while the very same queued clear cleared all six through the render pass's
// LOAD_OP_CLEAR. The helper now lives once, in VkTextureManager.h beside ToVulkanLevelExtent.
static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment(
const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) {
@@ -122,8 +126,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return &attachment;
}
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel,
// The texture a pending clear is actually ABOUT. A clear issued through a GL texture view
// (ARB_texture_view) targets the storage it views, so it must queue against - and be found
// by - the storage texture; keying it on the view instead left the clear invisible to every
// materialisation done through the parent's name (and vice versa), so the image stayed in
// VK_IMAGE_LAYOUT_UNDEFINED and the readback was dropped as unreadable.
static MG_State::GLState::ITextureObject* ClearStorageTextureOf(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return nullptr;
}
const auto& storageOwner = texture->GetViewStorageOwner();
return storageOwner ? storageOwner.get() : texture;
}
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel,
Uint32 baseArrayLayer, Uint32 layerCount) {
MG_State::GLState::ITextureObject* texture = ClearStorageTextureOf(rawTexture);
if (rawTexture != nullptr && texture != rawTexture) {
// The caller named a level and a layer of the VIEW; the key describes the STORAGE, so
// both have to be shifted into its numbering (GL 4.6 core 8.18). Without this a clear
// of a view's level 0 would collide with a clear of the storage's level 0 even when
// the view opened onto level 1.
mipLevel += static_cast<Uint32>(rawTexture->GetViewMinLevel());
baseArrayLayer += static_cast<Uint32>(rawTexture->GetViewMinLayer());
}
return PendingClearKey {
.texture = texture,
.textureLifetimeId = texture ? texture->GetLifetimeId() : 0,
@@ -157,6 +183,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
// Same rule as VkTextureManager::MakeTextureIdentity: a GL texture view is identified by
// the storage it views. A clear posted against a view and one posted against its parent
// target the same image, so they have to coalesce rather than queue independently.
if (texture != nullptr) {
const auto& storageOwner = texture->GetViewStorageOwner();
if (storageOwner) {
texture = storageOwner.get();
}
}
return TextureIdentity {
.texture = texture,
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
@@ -166,7 +201,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) {
dst.mask |= src.mask;
if ((src.mask & GL_COLOR_BUFFER_BIT) != 0) {
// The whole colour story travels together (same rule as
// VkRenderPassManager::QueueRenderbufferClear): a glClearBufferiv/uiv
// payload carries its value in colorInt/colorUint and its branch selector
// in colorEncoding - dropping them here would leave the pending clear
// reading as an all-zero float one.
dst.color = src.color;
dst.colorEncoding = src.colorEncoding;
dst.colorInt = src.colorInt;
dst.colorUint = src.colorUint;
}
if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) {
dst.depth = src.depth;
@@ -278,9 +321,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return;
}
const PendingClearKey key = MakePendingClearKey(texture.get());
const auto& storageOwner = texture->GetViewStorageOwner();
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
const PendingClearKey key = MakePendingClearKey(storageTexture.get());
const std::lock_guard<std::mutex> lock(m_mutex);
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
@@ -297,8 +342,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const PendingClearKey key = MakePendingClearKey(attachment);
// The alive entry must hold the STORAGE object, because the key names it:
// LockTextureIdentityLocked cross-checks the two, and registering a view here under its
// storage's identity made every lookup of this clear fail that check and silently report
// "nothing pending" - which is how a clear issued through a view's framebuffer vanished.
const auto& storageOwner = texture->GetViewStorageOwner();
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
const std::lock_guard<std::mutex> lock(m_mutex);
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
@@ -313,6 +364,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false; // per-draw hot path: nothing pending anywhere
}
texture = ClearStorageTextureOf(texture);
const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex);
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
@@ -403,6 +455,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false; // per-draw hot path: nothing pending anywhere
}
texture = ClearStorageTextureOf(texture);
const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex);
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
@@ -114,7 +114,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
class VkClearManager {
public:
static PendingClearKey MakePendingClearKey(const MG_State::GLState::FramebufferAttachmentObject& attachment);
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel = 0,
// Resolves a GL texture view to the storage it views before keying; see the definition.
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel = 0,
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
Bool Initialize();
@@ -67,28 +67,58 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
static Uint32 ResolveAttachmentBaseArrayLayer(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
// Every branch has to go through ToStorageArrayLayer, including the two that name layer 0
// implicitly: a layered attachment of a texture VIEW starts at the view's first layer, not
// at the image's, and a cube FACE index is a layer index like any other. Leaving either
// unshifted made the render pass write layers [0, n) while the clear key, the blit, the
// copy and the readback for the same attachment all addressed [minLayer, minLayer + n) -
// they resolve the layer through their own copies of this helper, which do shift.
const auto* texture = attachment.GetTexture().get();
if (attachment.IsLayered()) {
return 0;
return ToStorageArrayLayer(texture, 0);
}
const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget();
if (!IsCubeMapFaceUploadTarget(uploadTarget)) {
return static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
return ToStorageArrayLayer(texture, attachment.GetTextureLayer());
}
return static_cast<Uint32>(uploadTarget) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX);
const Int face =
static_cast<Int>(uploadTarget) - static_cast<Int>(TextureUploadTarget::CubeMapPositiveX);
return ToStorageArrayLayer(texture, face);
}
static Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (attachment.IsLayered()) {
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1));
}
return 1u;
}
// ResolveAttachmentLayerCount lives in VkTextureManager.h, beside ToVulkanLevelExtent, because
// VkClearManager needs the SAME answer: its pending-clear key's layerCount becomes a real
// VkImageSubresourceRange when a clear is materialised outside a render pass. See the header.
// VUID-VkFramebufferCreateInfo-flags-04113: every view handed to vkCreateFramebuffer must have
// been created as VK_IMAGE_VIEW_TYPE_2D or VK_IMAGE_VIEW_TYPE_2D_ARRAY. The image's OWN view
// type is not a legal answer for several of the targets GL can attach, and returning it
// unchanged is what took the process down on every layered 3D / cube-map-array attachment:
// a 3D view is refused outright by the layer-span guard in GetOrCreateAttachmentViewAtMipLevel
// (3D images have arrayLayers == 1) and a CUBE_ARRAY view is built happily and then rejected -
// or dereferenced - by the driver inside vkCreateFramebuffer.
//
// A 2D_ARRAY view is the legal spelling of all three: over a 2D-array-compatible 3D image its
// "layers" are the mip's z slices (VUID-VkImageViewCreateInfo-image-04970), and over a
// CUBE_COMPATIBLE 2D image - which is what both cube targets are - its layers are the faces.
//
// Knowingly NOT remapped: VK_IMAGE_VIEW_TYPE_1D / _1D_ARRAY, which 04113 also forbids. There is
// no legal alternative for them (a VK_IMAGE_TYPE_1D image admits no 2D-family view at all), so
// the only honest answer would be to decline the attachment - and every driver this has run on,
// lavapipe included, accepts them. Declining would turn working GL_TEXTURE_1D[_ARRAY] render
// targets into skipped draws to satisfy a VU nothing enforces. Left as-is, deliberately.
static VkImageViewType ResolveAttachmentViewType(
const MG_State::GLState::FramebufferAttachmentObject& attachment,
const VkTextureManager::TextureResource& resource) {
if (attachment.IsLayered()) {
return resource.viewType;
switch (resource.viewType) {
case VK_IMAGE_VIEW_TYPE_3D:
case VK_IMAGE_VIEW_TYPE_CUBE:
case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY:
return VK_IMAGE_VIEW_TYPE_2D_ARRAY;
default:
return resource.viewType;
}
}
// A non-layered attachment names ONE layer, so the view over it is a plain 2D view whatever
// the image's own view type is. The cube-face upload targets always meant this; a cube map
@@ -96,8 +126,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// a single layer is not a legal attachment. The CUBE arm is inert today - no frontend path
// produces a non-layered cube attachment without a face upload target - and is kept for
// symmetry with CUBE_ARRAY.
//
// 3D belongs in the same list and was missing from it, which is why the "per-slice
// attachment view is a 2D view whose array layer is the slice" branch in
// GetOrCreateAttachmentViewAtMipLevel was unreachable: glFramebufferTextureLayer on a
// GL_TEXTURE_3D asked for a 3D view (illegal as an attachment) whose span was then checked
// against arrayLayers == 1, so every slice above z = 0 came back VK_NULL_HANDLE.
if (IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ||
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE) {
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE ||
resource.viewType == VK_IMAGE_VIEW_TYPE_3D) {
return VK_IMAGE_VIEW_TYPE_2D;
}
return resource.viewType;
@@ -318,47 +355,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const auto internalFormat = renderbuffer->GetInternalFormat();
// Three-channel color formats widen to their RGBA twin exactly like textures do
// (VkTextureManager::ResolveTextureFormatInfo): blits/resolves between a
// renderbuffer and a texture of the same GL format then see one VkFormat.
const VkFormat format = [&]() -> VkFormat {
switch (internalFormat) {
case TextureInternalFormat::RGB:
case TextureInternalFormat::RGB8:
case TextureInternalFormat::R3G3B2:
case TextureInternalFormat::RGB4:
case TextureInternalFormat::RGB5:
return VK_FORMAT_R8G8B8A8_UNORM;
case TextureInternalFormat::SRGB8:
return VK_FORMAT_R8G8B8A8_SRGB;
case TextureInternalFormat::RGB8Snorm:
return VK_FORMAT_R8G8B8A8_SNORM;
case TextureInternalFormat::RGB10:
case TextureInternalFormat::RGB12:
case TextureInternalFormat::RGB16:
return VK_FORMAT_R16G16B16A16_UNORM;
case TextureInternalFormat::RGB16Snorm:
return VK_FORMAT_R16G16B16A16_SNORM;
case TextureInternalFormat::RGB16F:
return VK_FORMAT_R16G16B16A16_SFLOAT;
case TextureInternalFormat::RGB32F:
return VK_FORMAT_R32G32B32A32_SFLOAT;
case TextureInternalFormat::RGB8I:
return VK_FORMAT_R8G8B8A8_SINT;
case TextureInternalFormat::RGB8UI:
return VK_FORMAT_R8G8B8A8_UINT;
case TextureInternalFormat::RGB16I:
return VK_FORMAT_R16G16B16A16_SINT;
case TextureInternalFormat::RGB16UI:
return VK_FORMAT_R16G16B16A16_UINT;
case TextureInternalFormat::RGB32I:
return VK_FORMAT_R32G32B32A32_SINT;
case TextureInternalFormat::RGB32UI:
return VK_FORMAT_R32G32B32A32_UINT;
default:
return MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
}
}();
// ONE resolver, shared with textures (VkTextureManager::ResolveTextureFormatInfo), so a
// renderbuffer and a texture of the same GL format cannot disagree about their VkFormat.
// `expandRgbToRgba` / `componentByteCount` / `alphaBytes` describe how to reshape a SHADOW
// UPLOAD, and a renderbuffer has none, so only `.format` is taken.
//
// This used to be a hand-maintained second copy of that table, and it was missing exactly
// four rows: RGBA2 and RGBA12 fell through to ConvertTextureInternalFormatToVkEnum's
// VK_FORMAT_UNDEFINED (no image at all - bound as a draw buffer the attachment became
// VK_ATTACHMENT_UNUSED and every draw into it was dropped), while RGBA4 and RGB5A1 fell
// through to the 16-bit packed formats and then faced 32-bit R8G8B8A8_UNORM textures across
// a size-incompatible vkCmdCopyImage.
const VkFormat format = ResolveTextureFormatInfo(internalFormat).format;
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the
// usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer),
@@ -633,11 +641,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (att.IsTexture()) {
const Uint64 textureLifetimeId = att.GetTexture()->GetLifetimeId();
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLifetimeId, sizeof(textureLifetimeId)));
const Int textureLevel = att.GetTextureLevel();
const Int textureLevel = static_cast<Int>(ToStorageMipLevel(att.GetTexture().get(),
att.GetTextureLevel()));
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLevel, sizeof(textureLevel)));
const TextureUploadTarget textureUploadTarget = att.GetTextureUploadTarget();
XXHASH_VERIFY(XXH64_update(m_hashState, &textureUploadTarget, sizeof(textureUploadTarget)));
const Int textureLayer = att.GetTextureLayer();
const Int textureLayer = static_cast<Int>(ToStorageArrayLayer(att.GetTexture().get(),
att.GetTextureLayer()));
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayer, sizeof(textureLayer)));
const Bool textureLayered = att.IsLayered();
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayered, sizeof(textureLayered)));
@@ -754,7 +764,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return XXH64_digest(m_hashState);
}
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
RenderPassEntry* VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
Bool drawUsesDepthStencil) {
// Resolve the default-FBO depth flavor (see the header comment): keep the
@@ -831,6 +841,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// recreated since (texture + renderbuffer image epochs), and no pending clear (which alters
// load ops). Any of these differing forces the full recompute below. Portable to VK 1.1.
if (activeRenderPass != nullptr && m_rpFastValid && m_rpFastFbo == &fbo &&
m_rpFastFboLifetimeId == fbo.GetLifetimeId() &&
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
@@ -839,7 +850,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto activeIt = m_renderPasses.find(activeRenderPass->hash);
if (activeIt != m_renderPasses.end()) {
activeIt->second.lastUsedFrame = m_frameCounter;
return activeIt->second;
return &activeIt->second;
}
}
@@ -855,6 +866,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// epochs AFTER ComputeHash: its attachment SyncTexture can create an image (bump the epoch).
m_rpFastValid = true;
m_rpFastFbo = &fbo;
m_rpFastFboLifetimeId = fbo.GetLifetimeId();
m_rpFastFboVersion = fbo.GetObjectVersion();
m_rpFastSwapchainIndex = swapchainImageIndex;
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
@@ -862,13 +874,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_rpFastRenderPassHash = activeRenderPass->hash;
m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment;
activeIt->second.lastUsedFrame = m_frameCounter;
return activeIt->second;
return &activeIt->second;
}
auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil);
auto it = m_renderPasses.find(hash);
if (it != m_renderPasses.end()) {
it->second.lastUsedFrame = m_frameCounter;
return it->second;
return &it->second;
}
Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
@@ -991,8 +1003,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
textureResources.emplace_back(nullptr);
attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView
: rbResource->view);
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i);
if (attachmentViews.back() == VK_NULL_HANDLE) {
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u has no usable view for color attachment "
"%u on FBO %u; declining the render pass",
renderbuffer->GetExternalIndex(), i, fbo.GetExternalIndex());
return nullptr;
}
colorAttachmentRefs[i].attachment = rbAttachmentIndex;
continue;
@@ -1004,7 +1020,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue;
auto& att = fbo.GetAttachment(drawbuf);
const Uint32 attachmentMipLevel = static_cast<Uint32>(std::max(att.GetTextureLevel(), 0));
const Uint32 attachmentMipLevel = ToStorageMipLevel(att.GetTexture().get(), att.GetTextureLevel());
const auto textureTarget = texture->GetTarget();
const Uint32 attachmentIndex = static_cast<Uint32>(attachmentDescriptions.size());
attachmentDescriptions.emplace_back();
@@ -1047,8 +1063,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.key = VkClearManager::MakePendingClearKey(att)
});
}
const IntVec2 attachmentExtent =
ResolveRenderPassFramebufferExtent(isDefaultFbo, att.GetSize(), swapchainExtent);
// Same remap as ResolveAttachmentLayerCount, for the same reason: a
// 1D-array attachment's GL height is its layer count, and using it as the
// framebuffer height asks for a framebuffer taller than the VK_IMAGE_TYPE_1D
// image it is built over.
const IntVec2 attachmentExtent = ResolveRenderPassFramebufferExtent(
isDefaultFbo, ToVulkanLevelExtent(texture->GetTarget(), att.GetSize()), swapchainExtent);
if (width == 0)
width = attachmentExtent.x();
if (height == 0)
@@ -1076,8 +1096,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
attachmentViews.emplace_back(swapchainViews[swapchainImageIndex]);
} else {
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
MOBILEGL_ASSERT(textureResource,
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i);
if (textureResource == nullptr) {
// SyncTextureResource legitimately declines - an unsupported format,
// sample count or image-flag combination, or a vkCreateImage the driver
// refused. There is no image to attach, so there is no render pass.
MGLOG_E_ONCE("GetOrCreateRenderPass: textureId=%d could not be backed for color "
"attachment %u on FBO %u; declining the render pass",
texture->GetExternalIndex(), i, fbo.GetExternalIndex());
return nullptr;
}
textureResources.emplace_back(textureResource);
desc.format = ResolveSrgbAttachmentWriteFormat(
textureResource->format,
@@ -1098,8 +1125,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
attachmentViews.emplace_back(
m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
*texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
"GetOrCreateRenderPass: GetOrCreateAttachmentView failed at color attachment %d", i);
if (attachmentViews.back() == VK_NULL_HANDLE) {
MGLOG_E_ONCE("GetOrCreateRenderPass: no attachment view for textureId=%d mip=%u layers "
"[%u, %u) viewType=%d at color attachment %u on FBO %u; declining the "
"render pass",
texture->GetExternalIndex(), attachmentMipLevel, baseArrayLayer,
baseArrayLayer + layerCount, static_cast<Int>(attachmentViewType), i,
fbo.GetExternalIndex());
return nullptr;
}
}
desc.samples = attachmentSampleCount;
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
@@ -1142,7 +1176,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (a.IsTexture() && b.IsTexture()) {
return a.GetTexture().get() == b.GetTexture().get() &&
a.GetTextureUploadTarget() == b.GetTextureUploadTarget() &&
a.GetTextureLevel() == b.GetTextureLevel();
ToStorageMipLevel(a.GetTexture().get(), a.GetTextureLevel()) ==
ToStorageMipLevel(b.GetTexture().get(), b.GetTextureLevel());
}
if (a.IsRenderbuffer() && b.IsRenderbuffer()) {
return a.GetRenderbuffer().get() == b.GetRenderbuffer().get();
@@ -1191,20 +1226,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} else if (selectedDepthStencilAttachment->IsTexture()) {
auto& texture = *selectedDepthStencilAttachment->GetTexture();
depthTextureResource = m_textureManager.SyncTextureAndGetDescriptor(texture);
MOBILEGL_ASSERT(depthTextureResource,
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at depth attachment");
if (depthTextureResource == nullptr) {
MGLOG_E_ONCE("GetOrCreateRenderPass: textureId=%d could not be backed for the depth/stencil "
"attachment of FBO %u; declining the render pass",
texture.GetExternalIndex(), fbo.GetExternalIndex());
return nullptr;
}
trackedDepthLayout = depthTextureResource->layout;
depthAttachmentDescription.format = depthTextureResource->format;
depthAttachmentSampleCount = depthTextureResource->sampleCount;
depthAttachmentId = static_cast<Int>(texture.GetExternalIndex());
attachmentExtent =
ResolveRenderPassFramebufferExtent(isDefaultFbo, selectedDepthStencilAttachment->GetSize(),
swapchainExtent);
attachmentExtent = ResolveRenderPassFramebufferExtent(
isDefaultFbo,
ToVulkanLevelExtent(texture.GetTarget(), selectedDepthStencilAttachment->GetSize()),
swapchainExtent);
} else {
const auto& renderbuffer = selectedDepthStencilAttachment->GetRenderbuffer();
depthRenderbufferResource = GetOrCreateRenderbufferResource(renderbuffer);
MOBILEGL_ASSERT(depthRenderbufferResource,
"GetOrCreateRenderPass: GetOrCreateRenderbufferResource failed at depth attachment");
if (depthRenderbufferResource == nullptr) {
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u could not be backed for the depth/stencil "
"attachment of FBO %u; declining the render pass",
renderbuffer->GetExternalIndex(), fbo.GetExternalIndex());
return nullptr;
}
trackedDepthLayout = depthRenderbufferResource->layout;
depthAttachmentDescription.format = depthRenderbufferResource->format;
depthAttachmentSampleCount = depthRenderbufferResource->sampleCount;
@@ -1252,7 +1296,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} else if (selectedDepthStencilAttachment->IsTexture()) {
auto& texture = *selectedDepthStencilAttachment->GetTexture();
const Uint32 attachmentMipLevel =
static_cast<Uint32>(std::max(selectedDepthStencilAttachment->GetTextureLevel(), 0));
ToStorageMipLevel(selectedDepthStencilAttachment->GetTexture().get(),
selectedDepthStencilAttachment->GetTextureLevel());
MOBILEGL_ASSERT(depthTextureResource->layout != VK_IMAGE_LAYOUT_UNDEFINED ||
depthAttachmentDescription.loadOp != VK_ATTACHMENT_LOAD_OP_LOAD,
"GetOrCreateRenderPass: depth attachment textureId=%d has undefined tracked layout with LOAD_OP_LOAD",
@@ -1277,8 +1322,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
attachmentViews.emplace_back(
m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
"GetOrCreateRenderPass: GetOrCreateAttachmentView failed at depth attachment");
if (attachmentViews.back() == VK_NULL_HANDLE) {
MGLOG_E_ONCE("GetOrCreateRenderPass: no attachment view for textureId=%d mip=%u layers [%u, %u) "
"viewType=%d at the depth/stencil attachment of FBO %u; declining the render pass",
texture.GetExternalIndex(), attachmentMipLevel, baseArrayLayer,
baseArrayLayer + layerCount, static_cast<Int>(attachmentViewType),
fbo.GetExternalIndex());
return nullptr;
}
if (width == 0 || height == 0) {
width = attachmentExtent.x();
height = attachmentExtent.y();
@@ -1300,6 +1351,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
});
textureResources.emplace_back(nullptr);
attachmentViews.emplace_back(depthRenderbufferResource->view);
if (attachmentViews.back() == VK_NULL_HANDLE) {
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u has no usable view for the depth/stencil "
"attachment of FBO %u; declining the render pass",
renderbuffer->GetExternalIndex(), fbo.GetExternalIndex());
return nullptr;
}
if (width == 0 || height == 0) {
width = attachmentExtent.x();
height = attachmentExtent.y();
@@ -1397,8 +1454,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
renderPassCreateInfo.dependencyCount = 2;
renderPassCreateInfo.pDependencies = subpassDependencies;
// NOT VK_VERIFY. VkIncludes.h states the rule this function now lives by: VK_VERIFY is the
// INVARIANT check - a should-never-happen state, fatal-logged unlatched and trapped in a
// DEBUG build - and "a soft, recoverable failure must therefore NOT be routed through
// VK_VERIFY. Check the VkResult directly and report it with MGLOG_E_ONCE". A decline here
// is recoverable by construction: the caller drops the draw. Routing it through VK_VERIFY
// would have made the recovery dead code in a DEBUG build (the TRAP fires inside the macro,
// before the handle is ever examined) and, in an INFO build, printed an UNLATCHED fatal
// line on every draw for the life of the process - a decline caches nothing, so every
// later draw to the same framebuffer re-enters this path and fails again.
VkRenderPass renderPass = VK_NULL_HANDLE;
VK_VERIFY(vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass));
const VkResult renderPassResult =
vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass);
if (renderPassResult != VK_SUCCESS || renderPass == VK_NULL_HANDLE) {
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateRenderPass failed (%s, %d) for FBO %u; declining the "
"render pass",
VkResultToString(renderPassResult), static_cast<Int>(renderPassResult),
fbo.GetExternalIndex());
return nullptr;
}
// Framebuffer
VkFramebufferCreateInfo framebufferCreateInfo;
@@ -1411,8 +1485,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
framebufferCreateInfo.width = width;
framebufferCreateInfo.height = height;
framebufferCreateInfo.layers = framebufferLayers;
// Direct VkResult check, for the same reason as vkCreateRenderPass above.
VkFramebuffer framebuffer = VK_NULL_HANDLE;
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer));
const VkResult framebufferResult =
vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer);
if (framebufferResult != VK_SUCCESS || framebuffer == VK_NULL_HANDLE) {
// The render pass has no entry to own it yet, so it is destroyed here rather than
// leaked - RenderPassEntry's destructor is the only other thing that would.
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateFramebuffer failed (%s, %d) for FBO %u (%dx%d, "
"%u attachments, %u layers); declining the render pass",
VkResultToString(framebufferResult), static_cast<Int>(framebufferResult),
fbo.GetExternalIndex(), width, height,
static_cast<Uint32>(attachmentViews.size()), framebufferLayers);
vkDestroyRenderPass(m_device, renderPass, nullptr);
return nullptr;
}
IntVec2 extent = {width, height};
RenderPassEntry renderPassEntry {
hash,
@@ -1437,7 +1524,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
extent.y());
auto [insertedIt, _] = m_renderPasses.emplace(hash, Move(renderPassEntry));
insertedIt->second.lastUsedFrame = m_frameCounter;
return insertedIt->second;
return &insertedIt->second;
}
void VkRenderPassManager::OnPresent() {
@@ -1507,7 +1594,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ClearAttachmentPayload clearPayload{};
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
if (pending.hasInlinePayload) {
clearPayload = pending.inlinePayload;
// The inline payload was snapshotted when the entry was CREATED, but the
// clear VALUE is not part of the entry's hash - a cache hit with a newer
// glClear would replay the creation-time value and drop the new one (the
// texture path below is immune because it re-reads the live payload).
// Same defense as ClearAttachmentsOnActiveRenderPass: prefer the live
// pending clear, fall back to the snapshot only when none is queued.
if (s_renderPassManager != nullptr &&
s_renderPassManager->GetPendingRenderbufferClear(pending.renderbuffer, clearPayload)) {
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0 && pending.renderbuffer != nullptr &&
MG_Util::GetBaseInternalFormatComponentCount(pending.renderbuffer->GetInternalFormat()) ==
3) {
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
ForceOpaqueClearAlpha(clearPayload);
}
} else {
clearPayload = pending.inlinePayload;
}
} else {
if (pending.key.texture == nullptr ||
!s_clearManager->GetPendingClear(pending.key, clearPayload, liveTexture)) {
@@ -243,9 +243,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// draw against a depth-less active pass resolves to a new (incompatible)
// entry, which the caller's compatibility check turns into a pass split;
// the new pass's depth loads DONT_CARE (content was undefined all along).
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
Bool drawUsesDepthStencil = true);
//
// Returns NULLPTR when this framebuffer cannot be represented as a Vulkan render pass at
// all - a texture the texture manager declined to back (an unsupported format or sample
// count), or an attachment view it cannot construct (a layer span the image has no room
// for, a 3D image whose format was refused 2D-array compatibility). This used to be
// unrepresentable: the function returned a reference, so the only thing the two fallible
// calls it builds on could do was trip a MOBILEGL_ASSERT - which is compiled out of every
// INFO build - and then dereference the null resource, or hand VK_NULL_HANDLE to
// vkCreateFramebuffer. That took the whole process down (51 lost CTS records over 21
// bodies, one runner restart each) where a declined draw is merely a wrong picture.
//
// EVERY caller must handle nullptr by dropping the operation, exactly as the draw path
// already drops a draw whose sampler descriptor could not be resolved
// (UniformManager::BindProgramUniformBuffers). The failure paths log MGLOG_E_ONCE
// themselves, so a caller needs no message of its own.
[[nodiscard]] RenderPassEntry* GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
Bool drawUsesDepthStencil = true);
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
const MG_State::GLState::FramebufferObject& drawFbo);
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
@@ -289,6 +304,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
Bool m_rpFastValid = false;
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
// The FBO's never-reused lifetime id joins the raw pointer + Uint16 version:
// a deleted FBO reallocated at the same address whose fresh setup performed
// the same number of version bumps would otherwise compare equal (both count
// from 0), serving the dead framebuffer's pass to the new object.
Uint64 m_rpFastFboLifetimeId = 0;
Uint16 m_rpFastFboVersion = 0;
Uint32 m_rpFastSwapchainIndex = 0;
Uint64 m_rpFastTexEpoch = 0;
@@ -21,6 +21,156 @@ namespace MobileGL::MG_Backend::DirectVulkan {
sampler.GetWrapR() == SamplerWrapMode::ClampToBorder;
}
// The numeric domain the texture is SAMPLED in. Vulkan splits VkBorderColor into a float
// family and an integer family and requires the sampler's choice to match the image view's
// format (a float border on an integer view, or the reverse, is undefined) - so the domain
// comes from the TEXTURE, while the value comes from whichever GL entry point wrote it.
enum class BorderColorDomain {
Float,
SignedInteger,
UnsignedInteger
};
BorderColorDomain ResolveBorderColorDomain(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R8I:
case TextureInternalFormat::R16I:
case TextureInternalFormat::R32I:
case TextureInternalFormat::RG8I:
case TextureInternalFormat::RG16I:
case TextureInternalFormat::RG32I:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGB32I:
case TextureInternalFormat::RGBA8I:
case TextureInternalFormat::RGBA16I:
case TextureInternalFormat::RGBA32I:
return BorderColorDomain::SignedInteger;
case TextureInternalFormat::R8UI:
case TextureInternalFormat::R16UI:
case TextureInternalFormat::R32UI:
case TextureInternalFormat::RG8UI:
case TextureInternalFormat::RG16UI:
case TextureInternalFormat::RG32UI:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGB32UI:
case TextureInternalFormat::RGBA8UI:
case TextureInternalFormat::RGBA16UI:
case TextureInternalFormat::RGBA32UI:
case TextureInternalFormat::RGB10A2UI:
return BorderColorDomain::UnsignedInteger;
default:
return BorderColorDomain::Float;
}
}
Bool IsSignedNormalizedFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::R16Snorm:
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RG16Snorm:
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGB16Snorm:
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::RGBA16Snorm:
return true;
default:
return false;
}
}
// GL 4.6 core 8.14.2: "The border values are clamped before they are used, according to the
// format in which texture components are stored. For signed and unsigned normalized
// fixed-point formats, border values are clamped to [-1,1] and [0,1] respectively. For
// floating-point and integer formats, border values are clamped to the representable range of
// the format." Every clause of that sentence is a real case here - the clamp is not just the
// normalized one.
//
// Only the 32-bit float formats are genuinely unclamped: every finite float is representable
// in them. Half-float has a finite maximum, and the two packed "float" formats are UNSIGNED,
// so a negative border on them must come back as 0 rather than as a negative number the
// driver delivers verbatim through VK_BORDER_COLOR_FLOAT_CUSTOM_EXT.
struct FloatBorderRange {
Bool clamped = true;
Float minValue = 0.0f;
Float maxValue = 1.0f;
};
FloatBorderRange ResolveFloatBorderRange(TextureInternalFormat format, Bool isSignedNormalized) {
switch (format) {
case TextureInternalFormat::R32F:
case TextureInternalFormat::RG32F:
case TextureInternalFormat::RGB32F:
case TextureInternalFormat::RGBA32F:
return {false, 0.0f, 0.0f};
case TextureInternalFormat::R16F:
case TextureInternalFormat::RG16F:
case TextureInternalFormat::RGB16F:
case TextureInternalFormat::RGBA16F:
return {true, -65504.0f, 65504.0f};
// Unsigned packed floats: no sign bit at all. 65024 is the largest 11-bit float; the
// 10-bit blue channel tops out lower (64512) and RGB9E5 higher (65408), but the bound
// that matters for correctness is the lower one, and a single conservative upper bound
// costs nothing a real border colour will ever notice.
case TextureInternalFormat::R11FG11FB10F:
return {true, 0.0f, 64512.0f};
case TextureInternalFormat::RGB9E5:
return {true, 0.0f, 65408.0f};
default:
return {true, isSignedNormalized ? -1.0f : 0.0f, 1.0f};
}
}
// Per-component representable range of an integer texture format, as Int64 so that the whole
// signed and unsigned 32-bit ranges are expressible in one type and the clamp can be written
// once for both domains. Alpha is carried separately because RGB10_A2UI is the one format
// whose alpha is narrower than its colour channels.
struct IntegerBorderRange {
Int64 rgbMin = 0;
Int64 rgbMax = 0;
Int64 alphaMin = 0;
Int64 alphaMax = 0;
};
IntegerBorderRange ResolveIntegerBorderRange(TextureInternalFormat format) {
const auto uniform = [](Int64 low, Int64 high) { return IntegerBorderRange{low, high, low, high}; };
switch (format) {
case TextureInternalFormat::R8I:
case TextureInternalFormat::RG8I:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGBA8I:
return uniform(-128, 127);
case TextureInternalFormat::R16I:
case TextureInternalFormat::RG16I:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGBA16I:
return uniform(-32768, 32767);
case TextureInternalFormat::R8UI:
case TextureInternalFormat::RG8UI:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGBA8UI:
return uniform(0, 255);
case TextureInternalFormat::R16UI:
case TextureInternalFormat::RG16UI:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGBA16UI:
return uniform(0, 65535);
case TextureInternalFormat::R32UI:
case TextureInternalFormat::RG32UI:
case TextureInternalFormat::RGB32UI:
case TextureInternalFormat::RGBA32UI:
return uniform(0, 4294967295LL);
case TextureInternalFormat::RGB10A2UI:
return {0, 1023, 0, 3};
default:
// The signed 32-bit formats, and anything unexpected: the full int32 range, i.e. a
// clamp that cannot alter a value the GL entry points could have carried.
return uniform(-2147483648LL, 2147483647LL);
}
}
Bool IsDepthTextureFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::DepthComponent:
@@ -72,6 +222,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_config = initInfo.config;
m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported;
m_maxSamplerAnisotropy = std::max(initInfo.maxSamplerAnisotropy, 1.0f);
m_customBorderColorSupported = initInfo.customBorderColorSupported;
m_maxCustomBorderColorSamplers = initInfo.maxCustomBorderColorSamplers;
m_customBorderColorSamplerCount = 0;
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr,
"VkSamplerManager::Initialize failed: invalid initialization info");
return true;
@@ -102,6 +255,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device = VK_NULL_HANDLE;
m_config = nullptr;
m_frameBoundaryCounter = 0;
m_customBorderColorSupported = false;
m_maxCustomBorderColorSamplers = 0;
m_customBorderColorSamplerCount = 0;
}
void VkSamplerManager::OnFrameBoundary() {
@@ -123,6 +279,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
vkDestroySampler(m_device, entry.handle, nullptr);
}
if (entry.usesCustomBorderColor && m_customBorderColorSamplerCount > 0) {
--m_customBorderColorSamplerCount;
}
it = m_samplers.erase(it);
} else {
++it;
@@ -131,8 +290,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering, Bool singleLevelView) const {
Bool forceNearestFiltering, Bool singleLevelView,
const ResolvedBorderColor& borderColor) const {
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
@@ -166,8 +325,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
const auto compareFunc = sampler.GetSamplerCompareFunc();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
const auto borderColor = ResolveVkBorderColor(sampler, texture);
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor)));
// The resolved enum AND, when it is one of the *_CUSTOM_EXT values, the sixteen bytes of the
// colour itself: two samplers that differ only in a custom border colour carry the same enum
// and would otherwise collide onto whichever one was created first.
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.color, sizeof(borderColor.color)));
if (borderColor.isCustom) {
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.customValue, sizeof(borderColor.customValue)));
}
return XXH64_digest(m_hashState);
}
@@ -183,7 +347,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// allocation for a genuinely single-level image) and faults the GPU - the same failure
// the default-framebuffer blit shader had to work around with an explicit-LOD sample.
const Bool singleLevelView = viewLevelCount == 1;
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering, singleLevelView);
// Resolved once and used for both the key and the create-info; see ResolvedBorderColor.
const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, texture);
const Uint64 key = BuildSamplerKey(sampler, forceNearestFiltering, singleLevelView, borderColor);
auto it = m_samplers.find(key);
if (it != m_samplers.end()) {
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
@@ -211,9 +377,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Must match BuildSamplerKey's resolution exactly.
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
samplerInfo.borderColor = borderColor.color;
samplerInfo.unnormalizedCoordinates = VK_FALSE;
// VK_EXT_custom_border_color. `format` stays UNDEFINED, which is legal only because
// customBorderColorWithoutFormat was required alongside customBorderColors at device
// creation - a GL sampler object has no idea which texture it will be paired with.
VkSamplerCustomBorderColorCreateInfoEXT customBorderColorInfo{};
if (borderColor.isCustom) {
customBorderColorInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CUSTOM_BORDER_COLOR_CREATE_INFO_EXT;
customBorderColorInfo.customBorderColor = borderColor.customValue;
customBorderColorInfo.format = VK_FORMAT_UNDEFINED;
customBorderColorInfo.pNext = samplerInfo.pNext;
samplerInfo.pNext = &customBorderColorInfo;
}
VkSampler vkSampler = VK_NULL_HANDLE;
VK_VERIFY(vkCreateSampler(m_device, &samplerInfo, nullptr, &vkSampler), "vkCreateSampler(texture)");
@@ -222,6 +400,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.externalIndex = sampler.GetExternalIndex();
entry.version = sampler.GetVersion();
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
entry.usesCustomBorderColor = borderColor.isCustom;
if (entry.usesCustomBorderColor) {
++m_customBorderColorSamplerCount;
}
m_samplers[key] = entry;
return vkSampler;
}
@@ -281,39 +463,148 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
VkBorderColor VkSamplerManager::ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture) {
VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture) const {
ResolvedBorderColor resolved{};
if (!UsesBorderColor(sampler)) {
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
return resolved; // FLOAT_TRANSPARENT_BLACK, never sampled
}
// Border colour is sampler state: a bound sampler object supplies its own, and a texture
// with none reaches the very same value through the sampler object it owns.
const auto& borderColor = sampler.GetBorderColor();
const Bool isDepthTexture = IsDepthTextureFormat(texture.GetFormat());
const auto format = texture.GetFormat();
const auto domain = ResolveBorderColorDomain(format);
const Bool canUseCustom = m_customBorderColorSupported && m_maxCustomBorderColorSamplers > 0 &&
m_customBorderColorSamplerCount < m_maxCustomBorderColorSamplers;
if (isDepthTexture) {
if (domain != BorderColorDomain::Float) {
// An integer image view REQUIRES an integer border colour, whatever the value is - even
// (0,0,0,1). The value itself is whichever integer form the application wrote; a float
// border on an integer texture is nonsense GL leaves undefined, so the derived integer
// representation (a plain cast) is as good an answer as any.
//
// Clamped to the format's representable range FIRST, per GL 4.6 core 8.14.2, and read
// through Int64 so the whole signed and unsigned 32-bit ranges are expressible at once.
//
// Which representation to start from is the TEXTURE's domain, not the entry-point form
// the application used. GL 4.6 core 8.10 stores an "I"-form border colour unmodified with
// an integer internal data type and does not define a sign conversion between the two
// integer forms, so the stored bits are reinterpreted in the sampled format's own
// signedness. Measured, not assumed: a border of -1 written with glTexParameterIiv
// against a GL_R8UI texture samples as 255 on the ES driver, i.e. as 0xFFFFFFFF clamped
// to the format's maximum - see the IntegerBorderColorScenario case that pins it. Picking
// the representation by the FORM instead would answer 0 here, which is a defensible
// reading of the same spec text but puts DirectVulkan at odds with DirectGLES - and
// DirectGLES cannot deviate, it forwards the value to the driver verbatim. Cross-backend
// agreement decides it.
const auto range = ResolveIntegerBorderRange(format);
const auto& borderColorI = sampler.GetBorderColorI();
const auto& borderColorUI = sampler.GetBorderColorUI();
const Bool startFromUnsigned = domain == BorderColorDomain::UnsignedInteger;
Int64 clamped[4];
for (SizeT channel = 0; channel < 4; ++channel) {
const Int64 raw = startFromUnsigned ? static_cast<Int64>(borderColorUI[channel])
: static_cast<Int64>(borderColorI[channel]);
const Int64 low = channel == 3 ? range.alphaMin : range.rgbMin;
const Int64 high = channel == 3 ? range.alphaMax : range.rgbMax;
clamped[channel] = std::clamp(raw, low, high);
}
// Matched against the CLAMPED value, so a border the format cannot hold still lands on
// the palette entry it clamps to rather than missing every one of them.
const Bool allZeroRgb = clamped[0] == 0 && clamped[1] == 0 && clamped[2] == 0;
if (allZeroRgb && clamped[3] == 0) {
resolved.color = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK;
return resolved;
}
if (allZeroRgb && clamped[3] == 1) {
resolved.color = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
return resolved;
}
if (clamped[0] == 1 && clamped[1] == 1 && clamped[2] == 1 && clamped[3] == 1) {
resolved.color = VK_BORDER_COLOR_INT_OPAQUE_WHITE;
return resolved;
}
if (canUseCustom) {
resolved.color = VK_BORDER_COLOR_INT_CUSTOM_EXT;
resolved.isCustom = true;
for (SizeT channel = 0; channel < 4; ++channel) {
if (domain == BorderColorDomain::UnsignedInteger) {
resolved.customValue.uint32[channel] = static_cast<Uint32>(clamped[channel]);
} else {
resolved.customValue.int32[channel] = static_cast<Int32>(clamped[channel]);
}
}
return resolved;
}
// No custom colour available: pick the nearest of the three integer palette entries
// rather than always answering transparent black, which is what turned an integer border
// of (-1,-1,-1,-1) into 0 and broke the CTS's clamped-texel detection outright.
const Bool opaque = clamped[3] != 0;
const Bool bright = clamped[0] != 0 || clamped[1] != 0 || clamped[2] != 0;
resolved.color = !opaque ? VK_BORDER_COLOR_INT_TRANSPARENT_BLACK
: (bright ? VK_BORDER_COLOR_INT_OPAQUE_WHITE : VK_BORDER_COLOR_INT_OPAQUE_BLACK);
return resolved;
}
// Float domain. GL 4.6 core 8.14.2/8.23: the border colour is interpreted in the texture's
// format, so it is clamped to that format's representable range first. Without the clamp the
// CTS's border of (255,255,255,255) on a GL_RGBA8 texture matched none of the palette entries
// and fell through to transparent black - every border texel sampled 0 where the test wanted
// 255. The range is per format class, not just the normalized [0,1] / [-1,1] pair: only the
// 32-bit float formats are unclamped.
FloatVec4 borderColor = sampler.GetBorderColor();
if (const auto range = ResolveFloatBorderRange(format, IsSignedNormalizedFormat(format)); range.clamped) {
borderColor = FloatVec4(std::clamp(borderColor.x(), range.minValue, range.maxValue),
std::clamp(borderColor.y(), range.minValue, range.maxValue),
std::clamp(borderColor.z(), range.minValue, range.maxValue),
std::clamp(borderColor.w(), range.minValue, range.maxValue));
}
// A depth texture samples one component, so only x decides - and its alpha reads as 1.
if (IsDepthTextureFormat(format)) {
if (NearlyEqual(borderColor.x(), 1.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
return resolved;
}
if (NearlyEqual(borderColor.x(), 0.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
return resolved;
}
}
const Bool rgbZero = NearlyEqual(borderColor.x(), 0.0f) && NearlyEqual(borderColor.y(), 0.0f) &&
NearlyEqual(borderColor.z(), 0.0f);
if (rgbZero && NearlyEqual(borderColor.w(), 0.0f)) {
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
resolved.color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
return resolved;
}
if (rgbZero && NearlyEqual(borderColor.w(), 1.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
return resolved;
}
if (NearlyEqual(borderColor.x(), 1.0f) && NearlyEqual(borderColor.y(), 1.0f) &&
NearlyEqual(borderColor.z(), 1.0f) && NearlyEqual(borderColor.w(), 1.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
return resolved;
}
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
if (canUseCustom) {
resolved.color = VK_BORDER_COLOR_FLOAT_CUSTOM_EXT;
resolved.isCustom = true;
resolved.customValue.float32[0] = borderColor.x();
resolved.customValue.float32[1] = borderColor.y();
resolved.customValue.float32[2] = borderColor.z();
resolved.customValue.float32[3] = borderColor.w();
return resolved;
}
// Nearest of the three float palette entries. Transparent black stays the answer for a
// transparent border, which is what the old unconditional fallback got right by accident.
const Bool opaque = borderColor.w() >= 0.5f;
const Bool bright = (borderColor.x() + borderColor.y() + borderColor.z()) >= 1.5f;
resolved.color = !opaque ? VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK
: (bright ? VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE : VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK);
return resolved;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -28,6 +28,13 @@ public:
Bool samplerAnisotropySupported = false;
// VkPhysicalDeviceLimits::maxSamplerAnisotropy.
Float maxSamplerAnisotropy = 1.0f;
// VK_EXT_custom_border_color was enabled with BOTH customBorderColors and
// customBorderColorWithoutFormat; see VulkanRenderer::m_customBorderColorFeatureEnabled.
Bool customBorderColorSupported = false;
// VkPhysicalDeviceCustomBorderColorPropertiesEXT::maxCustomBorderColorSamplers. A hard device
// limit on how many LIVE samplers may carry a custom border colour, so the cache counts them
// and falls back to the snapped predefined value once it is reached.
Uint32 maxCustomBorderColorSamplers = 0;
};
Bool Initialize(const InitInfo& initInfo);
@@ -52,6 +59,21 @@ public:
// boundaries.
void OnFrameBoundary();
// What GL_TEXTURE_BORDER_COLOR resolves to for one (sampler, texture) pair. `color` is always a
// legal VkBorderColor; when `isCustom` it is one of the *_CUSTOM_EXT values and `customValue`
// carries the actual components in a VkSamplerCustomBorderColorCreateInfoEXT.
//
// Resolved ONCE per GetOrCreateSampler call and threaded into both the cache key and the
// create-info, so the two cannot disagree - the same discipline the resolved anisotropy needs,
// and here it also makes the maxCustomBorderColorSamplers fallback deterministic: whether a
// custom colour was affordable is decided before the key is built, not twice with a budget
// change in between.
struct ResolvedBorderColor {
VkBorderColor color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
VkClearColorValue customValue{};
Bool isCustom = false;
};
private:
struct SamplerCacheEntry {
VkSampler handle = VK_NULL_HANDLE;
@@ -60,17 +82,18 @@ private:
// Frame boundary of the last cache hit; entries idle past the
// OnFrameBoundary retirement age have their VkSampler destroyed.
Uint64 lastUsedFrameBoundary = 0;
// Counted against maxCustomBorderColorSamplers for as long as this entry lives.
Bool usesCustomBorderColor = false;
};
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering, Bool singleLevelView) const;
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Bool forceNearestFiltering,
Bool singleLevelView, const ResolvedBorderColor& borderColor) const;
static VkFilter ToVkFilter(SamplerFilterMode mode);
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture);
ResolvedBorderColor ResolveBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture) const;
// The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
@@ -82,6 +105,12 @@ private:
const VulkanRendererConfig* m_config = nullptr;
Bool m_samplerAnisotropySupported = false;
Float m_maxSamplerAnisotropy = 1.0f;
Bool m_customBorderColorSupported = false;
Uint32 m_maxCustomBorderColorSamplers = 0;
// Live cache entries carrying a custom border colour. Kept in step with the entries themselves
// in exactly the three places one can appear or disappear: creation, the OnFrameBoundary sweep,
// and Shutdown.
Uint32 m_customBorderColorSamplerCount = 0;
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameBoundaryCounter = 0;
@@ -46,13 +46,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return mipLevelCount;
}
struct TextureFormatInfo {
VkFormat format = VK_FORMAT_UNDEFINED;
Bool expandRgbToRgba = false;
Uint32 componentByteCount = 0;
Array<Uint8, 4> alphaBytes = {0, 0, 0, 0};
};
struct TextureShapeInfo {
VkImageType imageType = VK_IMAGE_TYPE_2D;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
@@ -220,6 +213,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkTextureManager::TextureIdentity VkTextureManager::MakeTextureIdentity(
MG_State::GLState::ITextureObject* texture) {
// A GL texture view (ARB_texture_view) is identified by the texture whose STORAGE it
// views, not by itself. Everything this identity keys - the TextureResource, the tracked
// image layout, the alive-object weak reference, the storage-usage marks, the per-draw
// sync memos - is a property of the IMAGE, and a view shares that image exactly. Doing
// the resolution here rather than at each call site is what makes it impossible to miss
// one: a layout update posted against a view's own identity would have found no resource
// at all, which is precisely how an attached view came back blank.
//
// One hop suffices and cannot recurse: glTextureView composes a view-of-a-view onto the
// root at creation, so a storage owner is never itself a view.
if (texture != nullptr) {
const auto& storageOwner = texture->GetViewStorageOwner();
if (storageOwner) {
texture = storageOwner.get();
}
}
return TextureIdentity{
.texture = texture,
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
@@ -300,8 +309,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool ok = VkTextureManager::TransitionImageLayout(
commandBuffer, newResource.image, newResource.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels,
newResource.arrayLayers);
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels);
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare destination image");
VkImageLayout srcTrackedLayout = oldResource.layout;
@@ -311,8 +319,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ok = VkTextureManager::TransitionImageLayout(
commandBuffer, oldResource.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels,
oldResource.arrayLayers);
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels);
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare source image");
Vector<VkImageCopy> copyRegions;
@@ -344,8 +351,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ok = VkTextureManager::TransitionImageLayout(
commandBuffer, newResource.image, newResource.layout, oldResource.layout,
VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask,
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels,
newResource.arrayLayers);
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels);
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to restore destination layout");
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(texture preserve)");
@@ -367,7 +373,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
static TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format) {
TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::RGB:
case TextureInternalFormat::RGB8:
@@ -697,6 +703,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkTextureManager::EraseTrackedTexture(const TextureIdentity& identity) {
m_viewRequestedImageFlags.erase(identity);
m_viewRequestedFormats.erase(identity);
auto resourceIt = m_textureResources.find(identity);
if (resourceIt != m_textureResources.end()) {
DeferResourceRelease(Move(resourceIt->second));
@@ -740,9 +748,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_drawSyncedThisDraw.clear();
}
VkTextureManager::TextureResource* VkTextureManager::SyncTextureAndGetDescriptor(MG_State::GLState::ITextureObject& texture) {
VkTextureManager::TextureResource* VkTextureManager::SyncTextureAndGetDescriptor(MG_State::GLState::ITextureObject& textureOrView) {
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureAndGetDescriptor: m_device == VK_NULL_HANDLE");
// A GL texture view has no image of its own; it resolves to - and shares - the resource
// of the texture whose storage it views, so that there is exactly one VkImage, one
// tracked layout and one upload path per storage. Everything that makes the view a
// different texture (format, level/layer window, sampled aspect) is applied where the
// VkImageViews are built, keyed in alternateSampledViews / attachmentViews.
MG_State::GLState::ITextureObject& texture = StorageTextureOf(textureOrView);
if (&texture != &textureOrView) {
NoteTextureViewImageRequirements(textureOrView, texture);
}
const TextureIdentity identity = MakeTextureIdentity(&texture);
// Per-draw memo fast path (see BeginDrawSyncScope): a texture already fully
@@ -841,7 +859,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageView VkTextureManager::GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
return VK_NULL_HANDLE;
}
// A GL texture view shares this resource with the texture it views, so it must not touch
// perMipViews: that vector is indexed by mip level alone and holds views built with the
// STORAGE texture's format and full layer range. Route it through the keyed attachment
// cache instead, where its own window is part of the key.
if (texture.IsTextureView()) {
const TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
return GetOrCreateAttachmentViewAtMipLevel(texture, mipLevel, window.baseArrayLayer, window.layerCount,
window.viewType);
}
if (mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE;
}
@@ -869,21 +899,43 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 layerCount,
VkImageViewType viewType) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
return VK_NULL_HANDLE;
}
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so a per-slice
// attachment view is a 2D view whose "array layer" is the slice - legal only on a
// 2D-array-compatible image (VUID-VkImageViewCreateInfo-image-04970), which
// SyncTextureResource asks for and may have had refused per format.
if (resource->viewType == VK_IMAGE_VIEW_TYPE_3D && viewType == VK_IMAGE_VIEW_TYPE_2D) {
// mipLevel and baseArrayLayer arrive in STORAGE space - every caller runs them through
// ToStorageMipLevel / ToStorageArrayLayer at the GL attachment boundary. What a GL texture
// view still contributes here is its own internal format, which may reinterpret the
// storage's (GL 4.6 core table 8.21) and is what the attachment must actually be written
// through.
VkFormat viewFormatOverride = VK_FORMAT_UNDEFINED;
if (texture.IsTextureView()) {
viewFormatOverride = ResolveTextureViewWindow(texture, *resource).format;
}
if (mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE;
}
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so an attachment
// view over it addresses SLICES through baseArrayLayer/layerCount: one slice for a
// non-layered attachment (a 2D view) and the whole span for a layered one (a 2D_ARRAY view,
// which is what a layered GL_TEXTURE_3D attachment plus a gl_Layer-writing geometry shader
// means). BOTH spellings are legal only on a 2D-array-compatible image
// (VUID-VkImageViewCreateInfo-image-04970 / -06723), which SyncTextureResource asks for and
// may have had refused per format.
//
// The span is validated against the MIP's slice count, never against arrayLayers: a 3D
// image's arrayLayers is 1 by construction, so measuring a layered span against it rejected
// every layered 3D attachment - the null view that used to reach vkCreateFramebuffer.
if (resource->viewType == VK_IMAGE_VIEW_TYPE_3D &&
(viewType == VK_IMAGE_VIEW_TYPE_2D || viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY)) {
const Uint32 sliceCount = std::max(resource->depth >> mipLevel, 1u);
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) == 0 ||
layerCount == 0 || baseArrayLayer >= sliceCount || baseArrayLayer + layerCount > sliceCount) {
MGLOG_D("%s: cannot name slice span [%u, %u) of 3D textureId=%d (mip %u has %u slices, "
"2D-array-compatible=%d)",
// Not an error line: the render-pass builder turns the null view into one
// MGLOG_E_ONCE and a skipped draw, which is the level this belongs at.
MGLOG_D("%s: cannot name slice span [%u, %u) of 3D textureId=%d as viewType=%d (mip %u has %u "
"slices, 2D-array-compatible=%d)",
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
mipLevel, sliceCount,
static_cast<Int>(viewType), mipLevel, sliceCount,
(int)((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0));
return VK_NULL_HANDLE;
}
@@ -897,10 +949,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool framebufferSrgbEnabled =
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
const VkFormat attachmentFormat = ResolveSrgbAttachmentWriteFormat(resource->format, framebufferSrgbEnabled);
const VkFormat baseAttachmentFormat =
viewFormatOverride != VK_FORMAT_UNDEFINED ? viewFormatOverride : resource->format;
const VkFormat attachmentFormat =
ResolveSrgbAttachmentWriteFormat(baseAttachmentFormat, framebufferSrgbEnabled);
if (attachmentFormat == resource->format && baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
viewType == resource->viewType) {
// The shortcut back to the per-mip vector is only sound for the storage texture itself;
// for a view every field below is part of what distinguishes it from its parent.
if (viewFormatOverride == VK_FORMAT_UNDEFINED && attachmentFormat == resource->format &&
baseArrayLayer == 0 && layerCount == resource->arrayLayers && viewType == resource->viewType) {
return GetOrCreateViewAtMipLevel(texture, mipLevel);
}
@@ -935,7 +992,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageView VkTextureManager::GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture,
Uint32 mipLevel) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
return VK_NULL_HANDLE;
}
// As in GetOrCreateViewAtMipLevel: perMipSampledViews belongs to the storage texture's
// own format and aspect, so a GL view has to go to the keyed cache.
if (texture.IsTextureView()) {
if (mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE;
}
TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
// Storage space already (see ToStorageMipLevel); only the level COUNT narrows.
window.baseMipLevel = mipLevel;
window.levelCount = 1;
return GetOrCreateWindowedSampledView(texture, *resource, window);
}
if (mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE;
}
@@ -963,14 +1035,76 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return perMipSampledView;
}
VkImageView VkTextureManager::GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture,
VkFormat format) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE ||
resource->sampledView == VK_NULL_HANDLE) {
// Builds (and caches) one sampled VkImageView over `resource`'s image for an arbitrary
// window - the shared back end of every GL-texture-view sampled path. Keyed by the whole
// window, which is what keeps a D24S8's depth-aspect view and its stencil-aspect view apart
// in the same cache while both name the same image, the same levels and the same layers.
VkImageView VkTextureManager::GetOrCreateWindowedSampledView(MG_State::GLState::ITextureObject& texture,
TextureResource& resource,
const TextureViewWindow& window) {
const TextureResource::SampledImageViewKey key{
.baseMipLevel = window.baseMipLevel,
.levelCount = window.levelCount,
.baseArrayLayer = window.baseArrayLayer,
.layerCount = window.layerCount,
.viewType = window.viewType,
.format = window.format,
.aspect = window.sampledAspect,
.componentSwizzle = PackComponentSwizzle(window.components),
};
const auto existing = resource.alternateSampledViews.find(key);
if (existing != resource.alternateSampledViews.end()) {
return existing->second;
}
if (window.format != resource.format &&
(resource.imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
MGLOG_E_ONCE("%s: textureId=%d needs a mutable-format image to be viewed as format=%d "
"(image format=%d)",
__func__, texture.GetExternalIndex(), static_cast<Int>(window.format),
static_cast<Int>(resource.format));
return VK_NULL_HANDLE;
}
const VkImageView view =
CreateImageView(resource.image, window.format, window.sampledAspect, window.viewType,
window.baseMipLevel, window.levelCount, window.baseArrayLayer, window.layerCount,
&window.components);
if (view == VK_NULL_HANDLE) {
MGLOG_E_ONCE("%s: failed to create sampled view for textureId=%d format=%d aspect=0x%x "
"mips=[%u,%u) layers=[%u,%u)",
__func__, texture.GetExternalIndex(), static_cast<Int>(window.format),
static_cast<Uint32>(window.sampledAspect), window.baseMipLevel,
window.baseMipLevel + window.levelCount, window.baseArrayLayer,
window.baseArrayLayer + window.layerCount);
return VK_NULL_HANDLE;
}
resource.alternateSampledViews.emplace(key, view);
return view;
}
VkImageView VkTextureManager::GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture,
VkFormat format) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
return VK_NULL_HANDLE;
}
// A GL texture view never has a sampledView of its own on this resource - that one
// belongs to the storage texture, with the storage texture's format, level range and
// depth/stencil aspect. The window is the view's whole identity, so it always goes to the
// keyed cache, even when the requested format happens to match the image's.
if (texture.IsTextureView()) {
TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
if (format != VK_FORMAT_UNDEFINED) {
window.format = format;
}
return GetOrCreateWindowedSampledView(texture, *resource, window);
}
if (resource->sampledView == VK_NULL_HANDLE) {
return VK_NULL_HANDLE;
}
if (format == VK_FORMAT_UNDEFINED || format == resource->format) {
return resource->sampledView;
}
@@ -990,8 +1124,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const TextureResource::SampledImageViewKey key{
.baseMipLevel = resource->sampledBaseMipLevel,
.levelCount = resource->sampledLevelCount,
.baseArrayLayer = 0,
.layerCount = resource->arrayLayers,
.viewType = resource->viewType,
.format = format,
.aspect = VK_IMAGE_ASPECT_COLOR_BIT,
.componentSwizzle = PackComponentSwizzle(
ResolveSampledViewComponents(texture, ResolveTextureFormatInfo(texture.GetFormat()))),
};
const auto existing = resource->alternateSampledViews.find(key);
if (existing != resource->alternateSampledViews.end()) {
@@ -1032,6 +1171,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageView VkTextureManager::GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture,
Uint32 mipLevel, VkFormat format,
Bool layered, Int32 layer) {
// mipLevel and layer arrive in STORAGE space; ResolveStorageImageDescriptor converts
// the glBindImageTexture values with ToStorageMipLevel / ToStorageArrayLayer.
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels ||
resource->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
@@ -1056,8 +1197,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return VK_NULL_HANDLE;
}
Uint32 baseArrayLayer = 0;
Uint32 layerCount = resource->arrayLayers;
// A GL texture view opens onto a WINDOW of the storage's layers; a layered image
// binding of it must not reach past that window into the parent's other layers.
Uint32 baseArrayLayer = ToStorageArrayLayer(&texture, 0);
Uint32 layerCount = texture.IsTextureView()
? std::min(static_cast<Uint32>(texture.GetViewNumLayers()),
resource->arrayLayers - baseArrayLayer)
: resource->arrayLayers;
VkImageViewType viewType = resource->viewType;
if (!layered) {
switch (resource->viewType) {
@@ -1090,7 +1236,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool isFullResourceView = baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
viewType == resource->viewType;
if (format == resource->format && isFullResourceView) {
if (format == resource->format && isFullResourceView && !texture.IsTextureView()) {
return GetOrCreateViewAtMipLevel(texture, mipLevel);
}
@@ -1191,7 +1337,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool lowerTransitioned = TransitionImageLayout(
commandBuffer, resource.image, lowerMipLayout, newLayout,
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
resource.aspect, 0, writtenMipLevel, resource.arrayLayers);
resource.aspect, 0, writtenMipLevel);
MOBILEGL_ASSERT(lowerTransitioned,
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition lower mip levels for textureId=%d",
texture->GetExternalIndex());
@@ -1203,8 +1349,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool upperTransitioned = TransitionImageLayout(
commandBuffer, resource.image, upperMipLayout, newLayout,
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel,
resource.arrayLayers);
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel);
MOBILEGL_ASSERT(upperTransitioned,
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition upper mip levels for textureId=%d",
texture->GetExternalIndex());
@@ -1257,8 +1402,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
s_sampledReadStages, srcAccessMask,
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
resource->arrayLayers);
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels);
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
StampResourceRecordingUse(*resource);
@@ -1288,7 +1432,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_IMAGE_LAYOUT_GENERAL, srcStageMask,
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, srcAccessMask,
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
resource->aspect, 0, resource->mipLevels);
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
texture.GetExternalIndex());
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
@@ -1296,6 +1440,158 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return ok;
}
Bool VkTextureManager::SnapshotTextureForSampling(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture,
SamplerNumericDomain numericDomain,
VkPipelineStageFlags consumerShaderStageMask,
SampledTextureSnapshot& outSnapshot) {
outSnapshot = {};
TextureResource* source = SyncTextureAndGetDescriptor(texture);
if (source == nullptr || source->image == VK_NULL_HANDLE || source->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
source->sampledLevelCount == 0) {
return false;
}
const VkFormat sampledFormat = ResolveSampledImageViewFormat(source->format, numericDomain);
if (sampledFormat == VK_FORMAT_UNDEFINED ||
!AreSampledImageViewFormatsCompatible(source->format, sampledFormat)) {
MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d cannot create sampled view format=%d from image format=%d",
texture.GetExternalIndex(), static_cast<Int>(sampledFormat), static_cast<Int>(source->format));
return false;
}
if (sampledFormat != source->format &&
(source->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d needs unavailable mutable image format=%d for sampled view=%d",
texture.GetExternalIndex(), static_cast<Int>(source->format), static_cast<Int>(sampledFormat));
return false;
}
VkImageType imageType = VK_IMAGE_TYPE_2D;
switch (source->viewType) {
case VK_IMAGE_VIEW_TYPE_1D:
case VK_IMAGE_VIEW_TYPE_1D_ARRAY:
imageType = VK_IMAGE_TYPE_1D;
break;
case VK_IMAGE_VIEW_TYPE_3D:
imageType = VK_IMAGE_TYPE_3D;
break;
default:
break;
}
TextureResource snapshot{};
VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.flags = source->imageCreateFlags;
imageInfo.imageType = imageType;
imageInfo.extent = {source->extent.width, source->extent.height, source->depth};
imageInfo.mipLevels = source->mipLevels;
imageInfo.arrayLayers = source->arrayLayers;
imageInfo.format = source->format;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
// Keep the temporary's view-format list just as narrow as the source's sampler use. This
// has no storage-image usage, so unlike an app image binding the exact list is knowable.
Vector<VkFormat> viewFormats;
VkImageFormatListCreateInfo formatListInfo{};
if (m_imageFormatListSupported && (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
viewFormats.push_back(source->format);
if (sampledFormat != source->format) {
viewFormats.push_back(sampledFormat);
}
formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
formatListInfo.viewFormatCount = static_cast<Uint32>(viewFormats.size());
formatListInfo.pViewFormats = viewFormats.data();
imageInfo.pNext = &formatListInfo;
}
VmaAllocationCreateInfo allocationInfo{};
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
const VkResult createResult =
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &snapshot.image, &snapshot.allocation, nullptr);
if (createResult != VK_SUCCESS) {
MGLOG_E_ONCE("SnapshotTextureForSampling: vmaCreateImage failed result=%d textureId=%d", createResult,
texture.GetExternalIndex());
return false;
}
snapshot.extent = source->extent;
snapshot.depth = source->depth;
snapshot.arrayLayers = source->arrayLayers;
snapshot.mipLevels = source->mipLevels;
snapshot.sampledBaseMipLevel = source->sampledBaseMipLevel;
snapshot.sampledLevelCount = source->sampledLevelCount;
snapshot.format = source->format;
snapshot.aspect = source->aspect;
snapshot.viewType = source->viewType;
snapshot.sampleCount = VK_SAMPLE_COUNT_1_BIT;
snapshot.imageCreateFlags = imageInfo.flags;
snapshot.usageFlags = imageInfo.usage;
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
const VkImageAspectFlags sampledAspect =
ResolveSampledImageViewAspectMask(snapshot.aspect, texture.GetDepthStencilTextureMode());
snapshot.sampledView = CreateImageView(snapshot.image, sampledFormat, sampledAspect, snapshot.viewType,
snapshot.sampledBaseMipLevel, snapshot.sampledLevelCount, 0,
snapshot.arrayLayers, &sampledComponents);
if (snapshot.sampledView == VK_NULL_HANDLE) {
MGLOG_E_ONCE("SnapshotTextureForSampling: failed to create sampled view textureId=%d", texture.GetExternalIndex());
return false;
}
VkPipelineStageFlags sourceStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags sourceAccessMask = 0;
const VkImageLayout sourceLayout = source->layout;
GetImageTransitionSourceState(sourceLayout, sourceStageMask, sourceAccessMask);
if (!TransitionImageLayout(commandBuffer, source->image, source->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
sourceStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, sourceAccessMask,
VK_ACCESS_TRANSFER_READ_BIT, source->aspect, 0, source->mipLevels) ||
!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
VK_ACCESS_TRANSFER_WRITE_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel,
snapshot.sampledLevelCount)) {
return false;
}
Vector<VkImageCopy> copyRegions;
copyRegions.reserve(snapshot.sampledLevelCount);
for (Uint32 level = snapshot.sampledBaseMipLevel;
level < snapshot.sampledBaseMipLevel + snapshot.sampledLevelCount; ++level) {
VkImageCopy copy{};
copy.srcSubresource = {source->aspect, level, 0, source->arrayLayers};
copy.dstSubresource = {snapshot.aspect, level, 0, snapshot.arrayLayers};
copy.extent = {std::max(source->extent.width >> level, 1u),
std::max(source->extent.height >> level, 1u),
std::max(source->depth >> level, 1u)};
copyRegions.push_back(copy);
}
vkCmdCopyImage(commandBuffer, source->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, snapshot.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<Uint32>(copyRegions.size()), copyRegions.data());
if (!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout,
ResolveSampledReadOnlyLayout(snapshot.aspect), VK_PIPELINE_STAGE_TRANSFER_BIT,
consumerShaderStageMask, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_SHADER_READ_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel,
snapshot.sampledLevelCount) ||
!TransitionImageLayout(commandBuffer, source->image, source->layout, sourceLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, consumerShaderStageMask,
VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
source->aspect, 0, source->mipLevels)) {
return false;
}
StampResourceRecordingUse(*source);
outSnapshot = {.imageView = snapshot.sampledView, .layout = snapshot.layout};
DeferResourceRelease(Move(snapshot));
return true;
}
void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) {
m_storageImageTextures.insert(MakeTextureIdentity(&texture));
}
@@ -1347,6 +1643,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u;
return resource.syncedContentVersion != texture.GetContentVersion() ||
resource.syncedShapeVersion != texture.GetShapeVersion() ||
resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() ||
resource.syncedMipLevelCount != mipLevelCount;
}
@@ -1355,8 +1652,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageLayout& trackedLayout, VkImageLayout newLayout,
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask,
VkImageAspectFlags aspectMask, Uint32 baseMipLevel, Uint32 levelCount,
Uint32 layerCount) {
VkImageAspectFlags aspectMask, Uint32 baseMipLevel,
Uint32 levelCount) {
MOBILEGL_ASSERT(image != VK_NULL_HANDLE, "TransitionImageLayout: m_image == VK_NULL_HANDLE");
MOBILEGL_ASSERT(!((dstAccessMask & VK_ACCESS_TRANSFER_READ_BIT) != 0 &&
(dstStageMask & VK_PIPELINE_STAGE_TRANSFER_BIT) == 0),
@@ -1381,7 +1678,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
barrier.subresourceRange.baseMipLevel = baseMipLevel;
barrier.subresourceRange.levelCount = levelCount;
barrier.subresourceRange.baseArrayLayer = 0;
barrier.subresourceRange.layerCount = layerCount;
// Every layer, always - see the declaration for why layout tracking leaves no other
// correct answer. VK_REMAINING_ARRAY_LAYERS rather than the image's own `arrayLayers`
// because those are not the same number for a 3D image: MobileGL creates 3D images
// 2D_ARRAY_COMPATIBLE and their arrayLayers is 1, which today Vulkan reads as "all depth
// slices" but will read as "depth slice 0" once VK_KHR_maintenance9 is enabled. The
// validation layer warns about that literal 1 by name.
barrier.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS;
vkCmdPipelineBarrier(commandBuffer, srcStageMask, dstStageMask, 0, 0, nullptr, 0, nullptr, 1, &barrier);
trackedLayout = newLayout;
@@ -1440,11 +1743,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
TextureResource &outResource) {
// 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
// sync, there is nothing to re-check or re-upload - skip CheckMipmapCompleteness,
// SyncTextureResource, SyncTextureViews and the per-level dirty scan. Layout is
// maintained separately by the transition path, so the resource still reflects truth.
// pixel content (bumped in MarkStorageDirty), its SHAPE (bumped in BumpShapeVersion)
// nor its params changed since the last sync, there is nothing to re-check or
// re-upload - skip CheckMipmapCompleteness, SyncTextureResource, SyncTextureViews and
// 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 syncingShapeVersion = texture.GetShapeVersion();
const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
const Uint32 syncingMipLevelCount =
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u;
@@ -1454,8 +1762,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool storageUpgradePending =
!outResource.storageUsageResolved &&
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
// Same shape for a GL texture view's demands on the image (MUTABLE_FORMAT for a
// format-reinterpreting view, CUBE_COMPATIBLE for a cube view of an array texture):
// nothing about the texture itself changed, but the live image cannot carry the view.
// Masked by what this format can actually be given: MUTABLE_FORMAT is deliberately
// withheld from formats the driver already refused it for (see SyncTextureResource), and
// without this mask the "upgrade still pending" test below could never come true again -
// costing every later sync of that texture the whole slow path, forever.
VkImageCreateFlags requestedViewFlags = GetViewRequestedImageFlags(texture);
if (m_mutableFormatUnsupported.find(outResource.format) != m_mutableFormatUnsupported.end()) {
requestedViewFlags &= ~VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
}
const Bool viewFlagUpgradePending =
(outResource.imageCreateFlags & requestedViewFlags) != requestedViewFlags;
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending && !viewFlagUpgradePending &&
outResource.syncedContentVersion == syncingContentVersion &&
outResource.syncedShapeVersion == syncingShapeVersion &&
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
outResource.syncedMipLevelCount == syncingMipLevelCount) {
return true;
@@ -1476,6 +1798,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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)) {
MGLOG_D("%s: SyncTextureResource failed", __func__);
return false;
@@ -1507,6 +1835,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!hasDirtyMipLevel) {
outResource.syncedContentVersion = syncingContentVersion;
outResource.syncedMipLevelCount = syncingMipLevelCount;
outResource.syncedShapeVersion = syncingShapeVersion;
return true;
}
@@ -1516,6 +1845,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
outResource.syncedContentVersion = syncingContentVersion;
outResource.syncedMipLevelCount = syncingMipLevelCount;
outResource.syncedShapeVersion = syncingShapeVersion;
return true;
}
@@ -1639,6 +1969,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
}
// Flags a GL texture view over this storage asked for (see NoteTextureViewImageRequirements).
// MUTABLE_FORMAT is still withheld from formats the driver has already refused it for, so a
// reinterpreting view degrades to no view rather than to no texture.
const VkImageCreateFlags requestedViewFlags = GetViewRequestedImageFlags(texture);
if (requestedViewFlags != 0) {
imageCreateFlags |= requestedViewFlags;
if (m_mutableFormatUnsupported.find(format) != m_mutableFormatUnsupported.end()) {
imageCreateFlags &= ~VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
}
}
// sRGB color images attach through their UNORM twin while GL_FRAMEBUFFER_SRGB is
// disabled (see ResolveSrgbAttachmentWriteFormat), which needs format-reinterpreting
// views - multisample sRGB render targets included.
@@ -1695,6 +2035,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) {
resolvedSampleCount = static_cast<VkSampleCountFlagBits>(rounded);
}
@@ -1786,6 +2139,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
viewFormats.push_back(viewFormat);
}
}
// ...plus every format a glTextureView over this storage reinterprets it as. Those
// are NOT enumerable from ResolveSampledImageViewFormat - an application may name any
// member of the format's view class (GL 4.6 core table 8.21) - so without this the
// list would forbid the very view the MUTABLE_FORMAT bit was requested for.
AppendViewRequestedFormats(texture, viewFormats);
formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
formatListInfo.viewFormatCount = static_cast<Uint32>(viewFormats.size());
formatListInfo.pViewFormats = viewFormats.data();
@@ -1818,12 +2176,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (imageFormatResult != VK_SUCCESS && !isMultisampleTexture &&
(imageInfo.flags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0) {
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
// format; failing creation would lose the texture entirely. Remembered so later syncs
// neither reprobe nor flag-mismatch against this image and recreate it.
// Losing 2D-array compatibility only costs framebuffer attachment of this format's
// 3D images - per-slice AND layered, since both are spelled as a 2D-family view over
// the z axis; failing creation would lose the texture entirely. Recorded here (the
// per-format set below) so later syncs neither reprobe nor flag-mismatch against this
// image and recreate it, and so GetOrCreateAttachmentViewAtMipLevel declines rather
// than handing back a view that cannot exist - the render-pass builder then turns
// that decline into a skipped draw instead of a null VkImageView in pAttachments.
MGLOG_W_ONCE("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
"unavailable for it)",
"textureId=%d; creating without it (per-slice and layered framebuffer "
"attachment of 3D textures in this format will be unavailable)",
__func__, static_cast<Int>(format), texture.GetExternalIndex());
m_2dArrayCompatibleUnsupported.insert(format);
imageInfo.flags &= ~VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
@@ -1840,6 +2202,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
texture.GetExternalIndex(),
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage));
// The preserved image was written by GPU work that may still be in flight
// (preserve requires layout != UNDEFINED); park it on the deferred ring
// like every other destruction path instead of letting the unique_ptr
// destroy it synchronously under the GPU.
if (preservedResource) {
DeferResourceRelease(Move(*preservedResource));
}
return false;
}
}
@@ -1862,6 +2231,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static_cast<Int>(imageInfo.samples), static_cast<Int>(imageInfo.format));
resource.image = VK_NULL_HANDLE;
resource.allocation = nullptr;
// Same as the probe failure above: the preserved live image must go through
// the deferred ring, never a synchronous destructor while frames that
// reference it are still in flight.
if (preservedResource) {
DeferResourceRelease(Move(*preservedResource));
}
return false;
}
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
@@ -2198,6 +2573,164 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_deferredViewReleases[m_currentFrameIndex].push_back(view);
}
MG_State::GLState::ITextureObject& VkTextureManager::StorageTextureOf(
MG_State::GLState::ITextureObject& texture) {
const auto& storageOwner = texture.GetViewStorageOwner();
return storageOwner ? *storageOwner : texture;
}
// The VkImageViewType a GL texture view's own target asks for. Deliberately derived from the
// GL target rather than inherited from the storage image: a 2D view of a 2D-array texture is
// a VK_IMAGE_VIEW_TYPE_2D over one layer, and a cube view of the same image is a
// VK_IMAGE_VIEW_TYPE_CUBE over six - which is the whole reason table 8.20 lists those pairs.
static VkImageViewType ResolveTextureViewImageViewType(TextureTarget target,
VkImageViewType storageViewType) {
switch (target) {
case TextureTarget::Texture1D:
return VK_IMAGE_VIEW_TYPE_1D;
case TextureTarget::Texture1DArray:
return VK_IMAGE_VIEW_TYPE_1D_ARRAY;
case TextureTarget::Texture2D:
case TextureTarget::TextureRectangle:
case TextureTarget::Texture2DMultisample:
return VK_IMAGE_VIEW_TYPE_2D;
case TextureTarget::Texture2DArray:
case TextureTarget::Texture2DMultisampleArray:
return VK_IMAGE_VIEW_TYPE_2D_ARRAY;
case TextureTarget::TextureCubeMap:
return VK_IMAGE_VIEW_TYPE_CUBE;
case TextureTarget::TextureCubeMapArray:
return VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
default:
return storageViewType;
}
}
VkTextureManager::TextureViewWindow VkTextureManager::ResolveTextureViewWindow(
MG_State::GLState::ITextureObject& texture, const TextureResource& resource) const {
TextureViewWindow window{};
window.format = resource.format;
window.viewType = resource.viewType;
window.baseArrayLayer = 0;
window.layerCount = resource.arrayLayers;
window.sampledAspect =
ResolveSampledImageViewAspectMask(resource.aspect, texture.GetDepthStencilTextureMode());
window.components = ResolveSampledViewComponents(texture, ResolveTextureFormatInfo(texture.GetFormat()));
ResolveViewMipRange(texture, resource.mipLevels, window.baseMipLevel, window.levelCount);
if (!texture.IsTextureView()) {
return window;
}
window.isTextureView = true;
// GL 4.6 core 8.18: the view's TEXTURE_BASE_LEVEL / TEXTURE_MAX_LEVEL are relative to the
// view, so ResolveViewMipRange above already clamped them against the view's own level
// count (TextureObjectView reports it); shifting by TEXTURE_VIEW_MIN_LEVEL puts them back
// into the storage image's numbering.
window.baseMipLevel += static_cast<Uint32>(texture.GetViewMinLevel());
window.baseArrayLayer = static_cast<Uint32>(texture.GetViewMinLayer());
window.layerCount = static_cast<Uint32>(texture.GetViewNumLayers());
window.viewType = ResolveTextureViewImageViewType(texture.GetTarget(), resource.viewType);
// The view's OWN internalformat, which may reinterpret the storage's (table 8.21).
const VkFormat viewFormat = ResolveTextureFormatInfo(texture.GetFormat()).format;
if (viewFormat != VK_FORMAT_UNDEFINED) {
window.format = viewFormat;
}
// Recomputed against the view's own format: a depth/stencil storage viewed as
// depth/stencil still has to honour the VIEW's DEPTH_STENCIL_TEXTURE_MODE, which is the
// one parameter Better Clouds deliberately sets differently on the two names.
window.sampledAspect =
ResolveSampledImageViewAspectMask(GetAspectMaskForFormat(window.format) != VK_IMAGE_ASPECT_NONE
? GetAspectMaskForFormat(window.format)
: resource.aspect,
texture.GetDepthStencilTextureMode());
// Clamp to what the image actually has; a malformed view must degrade to an empty range
// rather than reach vkCreateImageView with an out-of-bounds subresource.
if (window.baseMipLevel >= resource.mipLevels) {
window.baseMipLevel = resource.mipLevels - 1;
window.levelCount = 1;
} else {
window.levelCount = std::min(window.levelCount, resource.mipLevels - window.baseMipLevel);
}
if (window.levelCount == 0) window.levelCount = 1;
if (window.baseArrayLayer >= resource.arrayLayers) {
window.baseArrayLayer = resource.arrayLayers - 1;
window.layerCount = 1;
} else {
window.layerCount = std::min(window.layerCount, resource.arrayLayers - window.baseArrayLayer);
}
if (window.layerCount == 0) window.layerCount = 1;
return window;
}
// The extra VkImageCreateFlags a GL texture view needs on the image it views. Recorded
// BEFORE the storage texture is synced (see SyncTextureAndGetDescriptor) so the very first
// resolve of a view already creates - or recreates and copies forward - an image the view can
// legally be built over, instead of handing back VK_NULL_HANDLE for a frame.
void VkTextureManager::NoteTextureViewImageRequirements(MG_State::GLState::ITextureObject& viewTexture,
MG_State::GLState::ITextureObject& storageTexture) {
const TextureIdentity storageIdentity = MakeTextureIdentity(&storageTexture);
VkImageCreateFlags required = 0;
const VkFormat viewFormat = ResolveTextureFormatInfo(viewTexture.GetFormat()).format;
const VkFormat storageFormat = ResolveTextureFormatInfo(storageTexture.GetFormat()).format;
if (viewFormat != VK_FORMAT_UNDEFINED && storageFormat != VK_FORMAT_UNDEFINED &&
viewFormat != storageFormat) {
required |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
// The image may be created with a NARROWED format list (see SyncTextureResource), and
// that list is a promise about every format the image will ever be viewed as. Record
// this one so the promise stays true.
m_viewRequestedFormats[storageIdentity].insert(viewFormat);
}
const TextureTarget viewTarget = viewTexture.GetTarget();
if (viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::TextureCubeMapArray) {
// Only when the storage could legally carry the bit. VK_IMAGE_CREATE_CUBE_COMPATIBLE
// demands a 2D image with square levels and at least six array layers
// (VUID-VkImageCreateInfo-flags-00954), and asking for it on a storage that has fewer
// would fail vkCreateImage - which, because SyncTextureResource has already released
// the old resource by then, would leave the PARENT texture with no image at all. A
// degenerate view must not be able to destroy the texture it views; let its own view
// creation fail instead.
const IntVec3 storageSize = storageTexture.GetBaseSize();
const Bool storageCanBeCube = storageSize.x() == storageSize.y() &&
storageTexture.GetViewNumLayers() >= 6 &&
storageTexture.GetTarget() != TextureTarget::Texture3D;
if (storageCanBeCube) {
required |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
} else {
MGLOG_W_ONCE("Texture view %d wants a cube view of texture %d, whose storage is %dx%d with %u "
"layers and cannot be cube-compatible; the view will have no image view.",
viewTexture.GetExternalIndex(), storageTexture.GetExternalIndex(), storageSize.x(),
storageSize.y(), storageTexture.GetViewNumLayers());
}
}
if (required == 0) {
return;
}
VkImageCreateFlags& stored = m_viewRequestedImageFlags[storageIdentity];
stored |= required;
}
VkImageCreateFlags VkTextureManager::GetViewRequestedImageFlags(
const MG_State::GLState::ITextureObject& storageTexture) const {
const auto it = m_viewRequestedImageFlags.find(
MakeTextureIdentity(const_cast<MG_State::GLState::ITextureObject*>(&storageTexture)));
return it == m_viewRequestedImageFlags.end() ? 0 : it->second;
}
void VkTextureManager::AppendViewRequestedFormats(const MG_State::GLState::ITextureObject& storageTexture,
Vector<VkFormat>& outFormats) const {
const auto it = m_viewRequestedFormats.find(
MakeTextureIdentity(const_cast<MG_State::GLState::ITextureObject*>(&storageTexture)));
if (it == m_viewRequestedFormats.end()) {
return;
}
for (const VkFormat viewFormat : it->second) {
if (std::find(outFormats.begin(), outFormats.end(), viewFormat) == outFormats.end()) {
outFormats.push_back(viewFormat);
}
}
}
Bool VkTextureManager::SyncTextureViews(const MG_State::GLState::ITextureObject& texture, TextureResource& resource) {
MOBILEGL_ASSERT(resource.image != VK_NULL_HANDLE, "SyncTextureViews: image == VK_NULL_HANDLE");
@@ -2357,7 +2890,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
uploadItem.target = target;
uploadItem.level = level;
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.offset = stagingSize;
uploadItem.uploadByteSize = byteSize;
@@ -2395,6 +2934,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
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) {
@@ -2605,7 +3161,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_PIPELINE_STAGE_TRANSFER_BIT,
uploadSrcAccessMask,
VK_ACCESS_TRANSFER_WRITE_BIT,
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
aspectMask, 0, outResource.mipLevels);
MOBILEGL_ASSERT(ok, "TransitionImageLayout to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL failed");
// Array textures keep their GL "depth" in VkImage array layers, so the
@@ -2709,7 +3265,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
s_sampledReadStages,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_SHADER_READ_BIT,
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
aspectMask, 0, outResource.mipLevels);
MOBILEGL_ASSERT(ok, "TransitionImageLayout to sampled read-only layout failed");
outResource.layout = finalLayout;
@@ -10,8 +10,10 @@
#include "../VkIncludes.h"
#include <Includes.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_State/GLState/TextureState/TextureObject.h>
#include <vk_mem_alloc.h>
#include <algorithm>
#include <unordered_map>
#include <unordered_set>
@@ -22,6 +24,102 @@ class ITextureObject;
namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8;
// What VkFormat a GL internal format is BACKED with, and how a shadow upload has to be reshaped to
// fit it. This is not the same question as "is there an exact VkFormat for this GL format", which is
// what ConvertTextureInternalFormatToVkEnum answers: several GL formats have no Vulkan twin at all
// (RGBA2, RGBA12) and several three-channel ones are deliberately widened to their four-channel twin
// because Vulkan devices rarely support the 3-channel layouts.
//
// SHARED, and it must stay the only answer to that question. A renderbuffer and a texture of the
// same GL format have to resolve to the SAME VkFormat or every blit, resolve and glCopyImageSubData
// between them crosses a size-incompatible pair, which vkCmdCopyImage leaves undefined
// (VUID-vkCmdCopyImage-srcImage-01548). The renderbuffer path used to carry a hand-maintained second
// copy of this table that was missing four rows - RGBA2, RGBA4, RGB5A1 and RGBA12 - so those four
// renderbuffer formats either got no image at all or a 16-bit-packed one facing a 32-bit texture.
struct TextureFormatInfo {
VkFormat format = VK_FORMAT_UNDEFINED;
// The GL format has three channels and is carried in a four-channel image; a shadow upload has
// to be expanded, inserting `alphaBytes` after every `componentByteCount * 3` source bytes.
Bool expandRgbToRgba = false;
Uint32 componentByteCount = 0;
Array<Uint8, 4> alphaBytes = {0, 0, 0, 0};
};
// Callers that only need the backing VkFormat (a renderbuffer has no shadow upload to reshape) take
// `.format` and ignore the rest.
TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format);
// 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;
}
// How many Vulkan array layers (or, for a 3D image, z slices) a GL framebuffer attachment spans.
//
// THE ONE COPY, deliberately. This used to exist twice - privately in VkRenderPassManager.cpp and
// again in VkClearManager.cpp - and the two are not independent: the render pass builds the
// attachment view and VkFramebufferCreateInfo::layers from one, while the CLEAR key built from the
// other is written verbatim into VkImageSubresourceRange::layerCount when a queued glClear is
// materialised outside a render pass (MaterializePendingClearForTexture). They are two consumers
// of the same GL clear, so any disagreement means the same glClear produces two different pictures
// depending only on which path happens to consume it first - and the materialise path then POPS
// the entry, so the other one never runs. Fixing one copy and leaving the other is exactly how
// that split gets introduced; keep them the same function.
//
// Two shapes make this more than `size.z()`:
// * GL_TEXTURE_1D_ARRAY keeps its layer count in the state-side HEIGHT (see ToVulkanLevelExtent
// just above), so z reads 1 and every layer above the first was silently dropped.
// * GL_TEXTURE_CUBE_MAP is attached layered as its REPRESENTATIVE upload target, the +X face
// (ResolveRepresentableFramebufferTextureUploadTarget), and one face's level size has z = 1 -
// but a layered cube attachment names all six faces (GL 4.6 core 9.2.8), which are the image's
// six array layers. A cube ARRAY needs no such arm: its representative target carries 6n in z.
inline Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (!attachment.IsLayered()) {
return 1u;
}
const auto& texture = attachment.GetTexture();
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
if (target == TextureTarget::TextureCubeMap) {
return 6u;
}
return static_cast<Uint32>(std::max(ToVulkanLevelExtent(target, attachment.GetSize()).z(), 1));
}
// A GL framebuffer attachment's level/layer, and a GL image unit's, are relative to the texture
// the application NAMED. When that texture was created by glTextureView (ARB_texture_view) they
// are relative to the VIEW, and have to be shifted into the storage image's numbering before they
// can index a Vulkan subresource - DirectVulkan gives a view no image of its own, it shares the
// storage texture's (VkTextureManager::StorageTextureOf).
//
// Apply EXACTLY ONCE, at the boundary where a GL level/layer becomes a subresource index. Every
// GetOrCreate*View entry point below expects values that have already been through here, and so
// does everything that reads or copies an attachment directly. Both are identity on a plain
// texture (TEXTURE_VIEW_MIN_LEVEL / MIN_LAYER are 0 there), so the conversion is unconditional
// and there is no second, view-only code path to keep in step.
inline Uint32 ToStorageMipLevel(const MG_State::GLState::ITextureObject* texture, Int glLevel) {
const Uint32 level = static_cast<Uint32>(glLevel > 0 ? glLevel : 0);
return texture != nullptr ? level + static_cast<Uint32>(texture->GetViewMinLevel()) : level;
}
inline Uint32 ToStorageArrayLayer(const MG_State::GLState::ITextureObject* texture, Int glLayer) {
const Uint32 layer = static_cast<Uint32>(glLayer > 0 ? glLayer : 0);
return texture != nullptr ? layer + static_cast<Uint32>(texture->GetViewMinLayer()) : layer;
}
class VkTextureManager {
public:
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
@@ -120,17 +218,35 @@ public:
}
};
// Layer range and aspect join the key because a GL texture view (ARB_texture_view) can
// differ from its storage on either: the Better Clouds shape samples ONE D24S8 image
// through two GL names in one draw, the parent with the stencil aspect and the view with
// the depth aspect, and a layer-sliced view of an array texture names a sub-range of the
// same image. Without these two fields those views would alias each other in the cache.
struct SampledImageViewKey {
Uint32 baseMipLevel = 0;
Uint32 levelCount = 1;
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT;
// GL_TEXTURE_SWIZZLE_* is per-texture state, so two views over one storage with the
// same window but different swizzles are different views. Baked into the key because
// a GL texture view's ONLY sampled view lives in this cache: unlike the storage
// texture's own sampledView, which SyncTextureViews rebuilds whenever the params
// version moves, nothing else would ever notice a swizzle change on a view.
Uint32 componentSwizzle = 0;
Bool operator==(const SampledImageViewKey& other) const {
return baseMipLevel == other.baseMipLevel &&
levelCount == other.levelCount &&
baseArrayLayer == other.baseArrayLayer &&
layerCount == other.layerCount &&
viewType == other.viewType &&
format == other.format;
format == other.format &&
aspect == other.aspect &&
componentSwizzle == other.componentSwizzle;
}
};
@@ -138,10 +254,15 @@ public:
SizeT operator()(const SampledImageViewKey& key) const {
SizeT hash = std::hash<Uint32>{}(key.baseMipLevel);
hash ^= std::hash<Uint32>{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.aspect)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(key.componentSwizzle) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
@@ -206,6 +327,12 @@ public:
// 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.
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(const TextureResource&) = delete;
@@ -237,6 +364,7 @@ public:
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
}
void Reset() {
@@ -300,6 +428,7 @@ public:
syncedTextureParamsVersion = 0;
syncedContentVersion = 0;
syncedMipLevelCount = 0;
syncedShapeVersion = 0;
}
~TextureResource() {
@@ -310,6 +439,11 @@ public:
static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
};
struct SampledTextureSnapshot {
VkImageView imageView = VK_NULL_HANDLE;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
};
Bool Initialize(const InitInfo& initInfo);
void Shutdown();
void BeginFrame(Uint32 frameIndex);
@@ -326,6 +460,58 @@ public:
// present-less frame-boundary drain.
void CollectAllDeferredReleases();
// ---- GL texture views (ARB_texture_view / GL 4.6 core 8.18) ----
// The GL texture whose STORAGE backs the given one: itself, or - for a texture created by
// glTextureView - the texture it views. Every image-scoped question (which VkImage, its
// LAYOUT, its uploads, its extent, its usage) must be asked of this object, because a view
// has none of its own; only the VkImageViews differ per GL texture object. Sharing one
// TextureResource is not an optimisation, it is the only correct arrangement: layout is a
// property of the image, and VulkanRenderer caches raw pointers straight to the resource's
// layout field, so a second resource aliasing the same image would desynchronise the moment
// either of them transitioned it.
static MG_State::GLState::ITextureObject& StorageTextureOf(MG_State::GLState::ITextureObject& texture);
// The window a GL texture object opens onto its storage image. For a plain texture this is
// the resource's own full extent; for a view it is the sub-range, format and aspect
// glTextureView gave it. Views built from a non-default window must live in the KEYED caches
// (attachmentViews / alternateSampledViews), never in the per-mip vectors, which belong to
// the storage texture's own defaults.
struct TextureViewWindow {
Uint32 baseMipLevel = 0;
Uint32 levelCount = 1;
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkImageAspectFlags sampledAspect = VK_IMAGE_ASPECT_COLOR_BIT;
VkComponentMapping components{VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B,
VK_COMPONENT_SWIZZLE_A};
Bool isTextureView = false;
};
// The four component swizzles packed into one value, for the sampled-view cache key.
static Uint32 PackComponentSwizzle(const VkComponentMapping& components) {
return (static_cast<Uint32>(components.r) & 0xFFu) | ((static_cast<Uint32>(components.g) & 0xFFu) << 8) |
((static_cast<Uint32>(components.b) & 0xFFu) << 16) |
((static_cast<Uint32>(components.a) & 0xFFu) << 24);
}
TextureViewWindow ResolveTextureViewWindow(MG_State::GLState::ITextureObject& texture,
const TextureResource& resource) const;
// Records what a GL texture view needs of the image it views, so the next sync of the
// STORAGE texture creates (or recreates and copies forward) an image the view can be built
// over. See m_viewRequestedImageFlags for why this is lazy rather than unconditional.
void NoteTextureViewImageRequirements(MG_State::GLState::ITextureObject& viewTexture,
MG_State::GLState::ITextureObject& storageTexture);
VkImageCreateFlags GetViewRequestedImageFlags(const MG_State::GLState::ITextureObject& storageTexture) const;
// Appends every format a GL texture view reinterprets this storage as, for the narrowed
// VkImageFormatListCreateInfo the image is created with.
void AppendViewRequestedFormats(const MG_State::GLState::ITextureObject& storageTexture,
Vector<VkFormat>& outFormats) const;
// Builds (and caches, keyed by the whole window) one sampled VkImageView over a storage
// image. Shared back end of every GL-texture-view sampled path.
VkImageView GetOrCreateWindowedSampledView(MG_State::GLState::ITextureObject& texture,
TextureResource& resource, const TextureViewWindow& window);
TextureResource* SyncTextureAndGetDescriptor(
MG_State::GLState::ITextureObject& texture);
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
@@ -343,6 +529,13 @@ public:
VkImageLayout newLayout);
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
// Copies the complete sampler-visible mip range into a transient sampled image. The source is
// restored to its prior layout, so image-store descriptors continue to name the original image.
// The transient ownership is tied to the current frame slot and is safe through its submission.
Bool SnapshotTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture,
SamplerNumericDomain numericDomain,
VkPipelineStageFlags consumerShaderStageMask,
SampledTextureSnapshot& outSnapshot);
// Recording-generation bookkeeping for the pre-pass command stream. The
// generation advances every time the frame command buffer (re)begins
@@ -388,12 +581,24 @@ public:
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
// Moves `image` to `newLayout` and writes the new layout back through `trackedLayout`.
//
// The barrier covers EVERY array layer of the image, and there is deliberately no layer
// parameter to say otherwise: layout here is tracked per IMAGE (one `TextureResource::layout`,
// or one caller-owned variable), so a barrier narrower than the image would leave the layers it
// skipped in the old layout while the tracker claims they moved. Every transfer against a
// framebuffer attachment above layer 0 - glReadPixels, glBlitFramebuffer, glCopyTexSubImage,
// glCopyImageSubData - then ran its copy on a layer no barrier had transitioned.
//
// The mip range IS a parameter, because mip levels really are transitioned piecewise (see
// UpdateTrackedImageLayoutAfterAttachmentWrite and the mipmap generation loops): those callers
// move the complement of the level they wrote so the whole image converges on one layout again.
// Nothing does, or can, do that per layer.
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask,
Uint32 baseMipLevel = 0, Uint32 levelCount = 1,
Uint32 layerCount = 1);
Uint32 baseMipLevel = 0, Uint32 levelCount = 1);
SizeT CollectGarbage();
@@ -521,6 +726,19 @@ private:
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
// Extra VkImageCreateFlags a GL texture view needs on the storage image it views, keyed by
// the STORAGE texture's identity. Requested lazily, exactly like STORAGE usage above and for
// the same reason: VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT costs bandwidth compression on tilers
// (it is what VK_KHR_image_format_list exists to claw back), so setting it on every
// immutable-storage texture would tax every glTexStorage2D render target in a game for a
// feature almost none of them use. A SAME-format view - which is the common case, and the
// Better Clouds case - needs no flag at all and therefore costs nothing.
std::unordered_map<TextureIdentity, VkImageCreateFlags, TextureIdentityHash> m_viewRequestedImageFlags;
// Every VkFormat a GL texture view has asked to reinterpret this storage as. The narrowed
// VkImageFormatListCreateInfo the image is created with must name them: the list is a promise
// that NO other format will ever be viewed, and building a view outside it is
// VUID-VkImageViewCreateInfo-pNext-01585. Keyed, like the flags above, by the STORAGE texture.
std::unordered_map<TextureIdentity, std::unordered_set<VkFormat>, TextureIdentityHash> m_viewRequestedFormats;
// Supported multisample counts per format, so repeat texture syncs do not
// re-query vkGetPhysicalDeviceImageFormatProperties.
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
File diff suppressed because it is too large Load Diff
@@ -23,6 +23,8 @@
#include "VkTimerQueryManager.h"
#include "MG_Util/Math/VectorTypes.h"
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
#include <vk_mem_alloc.h>
#include "../VkIncludes.h"
@@ -197,9 +199,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLbitfield mask, GLenum filter);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
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,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
const CopyImageEndpoint& dstEndpoint,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
@@ -216,10 +218,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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
// 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,
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
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
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
@@ -229,6 +236,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
VkImageLayout dstRestoreLayout, Bool stencilAspect);
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
// Map a GL bottom-left-origin rectangle into the display-oriented swapchain image.
// Quarter-turn surface transforms swap the copy extent's axes.
static Bool MapDefaultFramebufferReadbackRect(GLint x, GLint y, GLsizei width, GLsizei height,
VkExtent2D imageExtent,
VkSurfaceTransformFlagBitsKHR preTransform,
VkOffset2D* imageOffset, VkExtent2D* imageCopyExtent);
// Reorder a tightly packed block copied with MapDefaultFramebufferReadbackRect back into
// GL row order. The input block has swapped dimensions for 90/270 degree transforms.
static Bool RemapDefaultFramebufferReadback(const Uint8* rawPixels, Uint32 logicalWidth,
Uint32 logicalHeight,
VkSurfaceTransformFlagBitsKHR preTransform,
SizeT texelSize, Uint8* outPixels);
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
GLsizei width, GLsizei height, GLenum destinationFormat,
GLenum destinationType, SizeT destinationRowStride,
@@ -298,6 +317,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
// honored rather than accepted-and-ignored.
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
// ARB_base_instance extends indirect command records with a non-zero firstInstance and
// requires gl_InstanceID to remain zero-based. Vulkan needs both features to honor that
// complete contract: one legalizes the command word, the other enables the shader rebase.
Bool IsNonZeroIndirectBaseInstanceSupported() const {
return m_drawIndirectFirstInstanceFeatureEnabled && m_shaderDrawParametersFeatureEnabled;
}
// Ensures the frame command buffer is recording (same lazy pattern as
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
// frame's pool. Null when unsupported or the pool is exhausted.
@@ -536,7 +561,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool m_samplerAnisotropyFeatureEnabled = false;
Bool m_shaderDrawParametersExtensionEnabled = false;
Bool m_shaderDrawParametersFeatureEnabled = false;
// Native subgroup topology, queried at device creation for the compute-module
// subgroup repairs (SubgroupSupportPolicy.h) and the REQUIRE_FULL_SUBGROUPS
// stage flag; 0 / false when the device has no usable compute subgroups or
// MOBILEGL_MAGMA_DISABLE_SUBGROUP forced them off.
Uint32 m_nativeSubgroupSize = 0;
Bool m_nativeSubgroupSupported = false;
Bool m_computeFullSubgroupsFeatureEnabled = false;
// VkPhysicalDeviceSubgroupSizeControlProperties::maxComputeWorkgroupSubgroups;
// 0 when the extension (and therefore the full-subgroups flag) is unavailable.
Uint32 m_maxComputeWorkgroupSubgroups = 0;
Bool m_unformattedFloatStorageImagesEnabled = false;
// Set only after descriptor-indexing feature AND property queries prove that
// update-after-bind is legal for every descriptor category this renderer emits.
ProgramFactory::UpdateAfterBindLimits m_updateAfterBindLimits{};
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
// drive a runtime fallback when the device lacks them.
@@ -547,6 +585,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
Bool m_dualSrcBlendFeatureEnabled = false;
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
// shaderTessellationAndGeometryPointSize gates the PointSize built-in in a tessellation
// or geometry stage, which desktop GL treats as an ordinary per-vertex output (writable,
// and capturable by name through transform feedback). Cached at device creation and
// handed to ProgramFactory, which refuses a program whose tessellation or geometry module
// declares the matching SPIR-V capability while this is false - SetupDraw then skips its
// draws (VkProgramObject::pointSizeCapabilityUnsupported) rather than building a pipeline
// that is invalid usage.
Bool m_tessellationAndGeometryPointSizeFeatureEnabled = false;
// VK_EXT_custom_border_color. Vulkan's four predefined VkBorderColor values cover only
// transparent/opaque black and opaque white; GL_TEXTURE_BORDER_COLOR is an arbitrary vec4 (or
// an arbitrary ivec4/uvec4 through the "I" entry points). Without this extension a border
// colour outside the palette has to be snapped to the nearest predefined one. Both features
// are required together: customBorderColorWithoutFormat is what lets a sampler carry a custom
// colour without naming the image format it will be paired with, which GL's sampler objects
// cannot know. maxCustomBorderColorSamplers is a real device limit, so the sampler cache has
// to be able to fall back to the snapped value once it is reached.
Bool m_customBorderColorFeatureEnabled = false;
Uint32 m_maxCustomBorderColorSamplers = 0;
// sampleRateShading gates VkPipelineMultisampleStateCreateInfo::sampleShadingEnable, i.e.
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading. Unlike dualSrcBlend this does NOT
// hard-fail the draw when absent: sample shading is a rate hint, and every sample-rate
// pipeline is still correct (just not per-sample) at the default rate - so the enable is
// dropped and the draw proceeds, which is what a GL implementation with SAMPLES=1 does too.
Bool m_sampleRateShadingFeatureEnabled = false;
// multiViewport gates rasterizing into more than one of ARB_viewport_array's 16 viewports
// (gl_ViewportIndex). m_maxRasterizableViewports is min(MAX_VIEWPORTS, device limit), or 1
// when the feature is off, and is the viewportCount a gl_ViewportIndex-writing pipeline
// declares - it is NOT what GL_MAX_VIEWPORTS reports, which is the frontend state width.
Bool m_multiViewportFeatureEnabled = false;
Uint32 m_maxRasterizableViewports = 1;
// Union of shader stages sampled-read barriers may name; built at device creation
// because geometry/tessellation stage bits are invalid in a barrier when their
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
@@ -650,15 +718,74 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Uint32> m_xfbQueryActiveSlots[2];
Bool m_xfbQuerySlotOpen = false;
Uint32 m_xfbQueryOpenSlot = 0;
// GL_PRIMITIVES_GENERATED reroute for draws made while transform feedback is
// INACTIVE. The stream pool's primitivesNeeded is defined to count those draws
// too, but a Mali driver (and Mesa lavapipe) answers 0 unless a capture span
// is open (the CTS's tessellator-measuring shape). Where the bring-up probe
// finds that defect with a working control - or
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE forces it - such draws accumulate the
// GENERATED count through this pool instead, whose type the arming picks:
// VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT where the device hosts the dedicated
// query with its rasterizer-discard feature (exact semantics by definition -
// the extension exists because GL needs this count without a capture), else a
// VK_QUERY_TYPE_PIPELINE_STATISTICS pool over clipping-stage invocations (one
// per primitive reaching primitive clipping - after every vertex processing
// stage, before rasterizer discard - which is the same set).
// XFB-ACTIVE draws keep the stream slot (exact today, and WRITTEN needs it);
// every draw with no open capture - a PAUSED span's draws included - takes a
// reroute slot, and the span then ignores the frontend's CPU paused-primitive
// counter rather than adding it on top (see IsPrimGenRerouteArmed): that
// counter is written by only 3 of the ~15 draw entry points and answers 0 for
// GL_PATCHES, so it cannot price the draws this reroute exists to repair. One
// GL query span may therefore hold slots of both pools.
Bool m_pipelineStatisticsQueryFeatureEnabled = false;
// VK_EXT_primitives_generated_query: base feature, and the
// ...WithRasterizerDiscard feature without which a discarding draw inside the
// query is invalid usage (so the reroute never picks the dedicated pool on a
// base-only device - GL applications toggle discard freely).
Bool m_primitivesGeneratedQueryFeatureEnabled = false;
Bool m_primitivesGeneratedQueryDiscardFeatureEnabled = false;
// tessellationShader was enabled at device creation (it is taken whenever the
// device advertises it); gates the probe's PATCHES shape.
Bool m_tessellationShaderFeatureEnabled = false;
MG_Util::SelfTest::PrimGenRerouteKind m_primGenRerouteKind =
MG_Util::SelfTest::PrimGenRerouteKind::None;
// The bring-up probe measured this device's stream query as counting draws made
// with no capture span open (the StreamCounts verdict) - so it counts the
// PAUSED-span ones too, through the stream slot they take when nothing is
// rerouted. Only the probe can know this, so it stays false wherever the probe
// is not consulted (the forced arms), which keeps those lanes' accounting as it
// was.
Bool m_primGenStreamCountsXfbInactiveDraws = false;
VkQueryPool m_primGenReroutePool = VK_NULL_HANDLE;
Uint32 m_primGenRerouteSlotCursor = 0;
Vector<Uint32> m_primGenRerouteActiveSlots;
Bool m_primGenRerouteSlotOpen = false;
Uint32 m_primGenRerouteOpenSlot = 0;
// Runs the bring-up probe (memoized per process) and decides
// m_primGenRerouteKind. Called at the end of device creation: it records on
// m_graphicsQueue, which nothing else is using yet.
void ArmPrimGenReroute();
public:
// Whether a GENERATED span opened now will have the draws made while the GL
// span is PAUSED counted on the GPU - through the reroute pool, which takes
// every draw with no open capture, or (where the reroute is not armed because
// the stream query was measured to count capture-less draws) through the stream
// slot such a draw still takes. The frontend's CPU paused-primitive counter
// must not be added on top of either: it would double count, and it cannot
// price the draws that matter anyway - only 3 of the ~15 draw entry points
// write it and it answers 0 for GL_PATCHES. Read once per span, after
// StartXfbQueryCapture (whose pool creation may disarm the reroute).
Bool ArePausedDrawsGpuCounted() const;
// kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED.
Bool StartXfbQueryCapture(Uint32 kind);
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots);
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives);
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots, Vector<Uint32>& outRerouteSlots);
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, const Vector<Uint32>& rerouteSlots,
Bool wantGenerated, Uint64& outPrimitives);
private:
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer);
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer, Bool xfbActive);
void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);
VkCommandPool m_commandPool = VK_NULL_HANDLE;
@@ -690,6 +817,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// values the memo already holds.
Uint64 pipelineStateHash = 0;
ProgramFactory::CompileOptionFlags transformFlags = {};
// Baked into the pipeline (PipelineFactory::ComputeHash mixes it), and NOT derivable
// from anything else in this key: it depends on whether the draw is indexed and on the
// index type, neither of which the mode/program/state hashes carry. Without it an
// indexed and a non-indexed draw over the same program and state collide on one entry
// and the second one gets the first one's restart setting.
Bool primitiveRestartEnable = false;
VkPipeline pipeline = VK_NULL_HANDLE;
};
static constexpr Uint32 kPipelineMemoSize = 8;
@@ -703,9 +836,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// version: the version is monotonic and bumps on every pipeline-state
// change, so an unchanged (version, colorAttachmentCount) proves the state
// bytes are unchanged and the hash can be reused without re-reading them.
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount) const;
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount,
VkSampleCountFlagBits rasterizationSamples) const;
// The effective GL_SAMPLE_MASK word for a draw at this rasterization sample count; see
// the definition for the GL-vs-Vulkan rule it reconciles. Shared by the pipeline payload
// and the pipeline-state memo word so the two cannot disagree.
Uint32 ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const;
Uint m_pipelineStateHashVersion = 0;
Uint32 m_pipelineStateHashColorCount = 0;
// The sample count the cached hash was computed at. A pipeline-state input now depends on
// it (the effective sample mask), so a draw that changes only the target's sample count
// has to recompute rather than reuse.
VkSampleCountFlagBits m_pipelineStateHashSampleCount = VK_SAMPLE_COUNT_1_BIT;
Uint64 m_pipelineStateHash = 0;
Bool m_pipelineStateHashValid = false;
// GetShaderTransformFlags memo. NOT pure in the pre-transform alone: the
@@ -747,7 +889,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Skip the per-draw CollectSampledTextures walk (~5% of the render thread) when the sampled
// texture SET is provably unchanged from the previous draw: same program (lifetime id +
// backend-state version, which covers sampler-uniform reassignment / relink) and transform
// flags, and no texture bind/unbind/delete since (GetTextureBindGeneration). On a hit,
// flags, no texture bind/unbind/delete since (GetTextureBindGeneration), and nothing that
// moves a texture's shape or a sampler's parameters since (GetSamplingResolutionGeneration
// - membership depends on mipmap-completeness, which both of those decide). On a hit,
// m_sampledTexturesScratch still holds the previous draw's list and steps 2-4 (feedback /
// layout probe / transition) re-run on it, so layout correctness is unaffected - only the GL
// walk is skipped. The program lifetime id (never reused, unlike the GL name) and the
@@ -758,6 +902,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 m_lastSampledSetProgramVersion = 0;
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
Uint64 m_lastSampledSetBindGeneration = 0;
Uint64 m_lastSampledSetSamplingGeneration = 0;
// Set from the draw's resolved VkProgramObject on both the full and the fast setup paths;
// read by BeginXfbCaptureForDraw, which has only GL state otherwise. See
// VkProgramObject::xfbCaptureDeclined.
Bool m_currentDrawXfbCaptureDeclined = false;
// Memo for the per-draw explicit-LOD-0 eligibility probe
// (ProgramSamplesOnlySingleLevelTextures): same key family as the
@@ -770,6 +919,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 m_lastLodProgramVersion = 0;
Uint64 m_lastLodBindGeneration = 0;
Uint64 m_lastLodParamsSum = 0;
// Sampling-resolution generation at probe time. The probe reads the effective
// sampler's filters/aniso/LOD range, whose setters bump only this counter -
// the params-version sum above never moves for them.
Uint64 m_lastLodSamplingGeneration = 0;
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
@@ -807,12 +960,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 vaoLifetimeId = 0;
Uint32 vaoConfigVersion = 0;
const void* drawFbo = nullptr;
// Never-reused lifetime id beside the raw pointer + Uint16 version: a
// deleted FBO recycled at the same address with the same fresh version
// count would otherwise compare equal (same ABA as the render-pass
// manager's fast-path memo).
Uint64 drawFboLifetimeId = 0;
Uint16 fboVersion = 0;
Bool drawFboIsDefault = false;
Uint renderStateVersion = 0;
Uint64 bindGeneration = 0;
Uint32 baseTransformFlags = 0;
Uint32 resolvedTransformFlags = 0;
// What ResolvePrimitiveRestartEnable answered for the draw this snapshot was taken
// from, i.e. what its pipeline's primitiveRestartEnable was built with. `aspects`
// already separates indexed from non-indexed draws, but not one index TYPE from
// another, and a restart index that fits GL_UNSIGNED_INT but not GL_UNSIGNED_SHORT
// makes those two draws want different pipelines.
Bool primitiveRestartEnable = false;
Uint64 renderPassHash = 0;
Uint32 imageIndex = 0;
Uint64 textureEraseEpoch = 0;
@@ -830,12 +994,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// re-resolve just the pipeline against the active pass; a change that
// flips it must fall back to the full path's pass selection.
Bool drawUsesDepthStencil = false;
// The snapshotting draw's pipeline viewportCount. A pure function of the PROGRAM
// (writesViewportIndexBuiltin) and of a device feature fixed at renderer init, both
// of which the programLifetimeId/programVersion guards above already pin - carried
// here so the fast path does not re-fetch the program object to re-derive it.
Uint32 viewportCount = 1;
IntVec2 renderPassExtent = {0, 0};
// colorAttachmentCount of the snapshotting draw's render pass: the
// pipeline-state hash input, so the fast path can refresh that hash and
// probe the pipeline memo after a state change without re-fetching the
// render-pass entry (the pass itself is pinned by renderPassHash above).
Uint32 renderPassColorCount = 0;
// Pinned with the colour count and for the same reason: the fast path recomputes the
// pipeline-state value hash from the snapshot, and that hash reads the sample count.
VkSampleCountFlagBits renderPassSampleCount = VK_SAMPLE_COUNT_1_BIT;
VkPipeline pipeline = VK_NULL_HANDLE;
// layoutHash of the snapshotting draw's vertex-input state. The pipeline and
// the vertex-input pre-flight depend on the VAO only through this (plus the
@@ -897,6 +1069,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// already sampleable.
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
Vector<UniformManager::SamplerImageFeedbackBinding> m_samplerImageFeedbackScratch;
Vector<UniformManager::SamplerBindingOverride> m_samplerImageBindingOverridesScratch;
Vector<VkBuffer> m_vertexBuffersScratch;
Vector<VkDeviceSize> m_vertexOffsetsScratch;
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
@@ -1023,6 +1197,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkBuffer indexVkBuffer = VK_NULL_HANDLE;
VkDeviceSize indexSliceOffset = 0;
Uint64 indexFrameSerial = 0;
// The EBO carried a host map when the slice was recorded - the mirror of
// anyBufferMapped on the vertex half. A shadow-backed (non-adopted)
// persistent map mutates its shadow with no API call and no epoch bump, so
// the one-compare rescue must decline and re-run the acquire, whose
// SyncPersistentMappedRange is the push-down. A map taken AFTER the record
// is already covered: AcquirePersistentMap bumps the slice epoch for the
// request itself, adopted or declined.
Bool indexBufferMapped = false;
// Bound per draw (first bindingCount elements).
VkBuffer vkBuffers[kMaxBindings] = {};
@@ -1101,13 +1283,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void CreateSwapchain();
void CreateCommandPool();
// Whether THIS draw's primitive stream restarts, and therefore what
// VkPipelineInputAssemblyStateCreateInfo::primitiveRestartEnable must be. Resolved by the
// caller because it needs two facts a pipeline cannot see: whether the draw is indexed at
// all (GL primitive restart acts on the index stream, so it is a no-op for glDrawArrays),
// and the index TYPE (an application restart index that does not fit the type matches no
// index, so that draw restarts nowhere - see UploadAndBindIndexBuffer).
Bool ResolvePrimitiveRestartEnable(Flags<DrawSetupAspect> aspects,
const IndexBufferView* pIndexBufferView) const;
VkPipeline GetOrCreatePipeline(
GLenum mode,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
ProgramFactory::CompileOptionFlags transformFlags,
const MG_State::GLState::VertexArrayObject& vao,
const RenderPassEntry& renderPassEntry);
const RenderPassEntry& renderPassEntry,
Bool primitiveRestartEnable);
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
void DestroyComputePipelines();
// Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to
@@ -1116,11 +1308,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
// Vulkan forbids a sampled descriptor and writable storage descriptor from naming the
// same image subresource in one shader operation. Snapshot only the sampler side; the
// storage descriptor continues to name the application texture.
Bool PrepareSamplerImageFeedbackSnapshots(
FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
VkPipelineStageFlags consumerShaderStageMask);
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
// bias, line width, stencil), gated behind one render-state-parameters-version
// compare per command buffer - see the gate fields in DynamicStateShadow.
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo);
// viewportCount is the bound pipeline's declared viewport count: 1 for every program that
// does not write gl_ViewportIndex (the memoized fast path), otherwise the renderer's
// rasterizable viewport count, which takes the unmemoized array path.
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo,
Uint32 viewportCount = 1);
void ApplyMultiViewportDynamicState(VkCommandBuffer commandBuffer, Uint32 viewportCount, const IntVec2& extent,
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo);
VkRect2D ComputeGLScissorRect(Uint32 index, const IntVec2& extent,
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo) const;
// How many viewports a draw with this program rasterizes into: 1 unless the program
// assigns gl_ViewportIndex AND the device enabled multiViewport. Both the pipeline's
// baked viewportCount and the dynamic arrays come from this one answer, so they cannot
// disagree.
Uint32 ResolveDrawViewportCount(Bool programWritesViewportIndex) const {
return programWritesViewportIndex && m_multiViewportFeatureEnabled ? m_maxRasterizableViewports : 1u;
}
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
const ProgramFactory::VkProgramObject& programObj,
@@ -1151,13 +1366,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLenum filter);
// Clears one z slice of a VK_IMAGE_TYPE_3D colour image. See the call site in
// MaterializePendingClearForTexture for why a transfer clear cannot do this.
// Clears one layer of a colour image through a throwaway render pass whose entire content
// is its LOAD_OP_CLEAR. Two callers, both of which a transfer clear cannot serve: a z
// slice of a VK_IMAGE_TYPE_3D image (vkCmdClearColorImage cannot name one), and a
// MULTISAMPLE image (which carries no TRANSFER_DST usage at all). `finalLayout` is the
// layout the caller already tracks for the whole image, so this never has to touch
// resource->layout.
Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 depthSlice, const VkClearValue& clearValue);
Uint32 depthSlice, const VkClearValue& clearValue,
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture);
// The multisample arm of the above. Split out rather than branched inline because it
// shares none of the transfer path: a multisample image carries no TRANSFER_DST usage, so
// neither the TRANSFER_DST transition nor vkCmdClearColorImage is legal on one.
Bool MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture,
VkTextureManager::TextureResource& resource,
const Vector<PendingClearEntry>& pendingClears);
Bool MaterializePendingClearForRenderbuffer(
VkCommandBuffer commandBuffer,
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
@@ -0,0 +1,63 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/SubgroupSupportPolicy.h
// 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
#pragma once
#include <Config.h>
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
// The single decision point for how DirectVulkan implements GL_KHR_shader_subgroup,
// shared by capability advertisement (BackendObject) and module lowering
// (VulkanRenderer / ProgramFactory) so the two can never disagree.
//
// Native subgroups are the implementation whenever the device has them, whatever
// their width - subgroup operations execute on the hardware paths they were made
// for. Module-level repairs keep the GL contract intact around them:
// - FixIterationRPSubgroupScratchPass patches the one known pack bug: iterationRP's
// prefixSumCache[32], under-declared for sub-16-lane devices (8-lane lavapipe);
// - FixIterationRPBarrierPass repairs Program 203's race between two reductions
// reusing that scratch, when explicitly enabled;
// - DeriveNumSubgroupsPass replaces the one builtin drivers get wrong
// (gl_NumSubgroups) with the value the rest of the topology implies.
// The 32-lane shared-memory emulation (EmulateSubgroupsPass) is a LAST RESORT for
// devices with no subgroup support at all, and only when the user opts in with
// MOBILEGL_MAGMA_EMULATE_SUBGROUP=1; it never replaces available native operations.
inline constexpr Uint32 kEmulatedSubgroupSize = 32u;
inline constexpr Uint32 kEmulatedSubgroupStages = GL_COMPUTE_SHADER_BIT;
inline constexpr Uint32 kEmulatedSubgroupFeatures =
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_VOTE_BIT_KHR |
GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR | GL_SUBGROUP_FEATURE_BALLOT_BIT_KHR |
GL_SUBGROUP_FEATURE_SHUFFLE_BIT_KHR | GL_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT_KHR |
GL_SUBGROUP_FEATURE_CLUSTERED_BIT_KHR | GL_SUBGROUP_FEATURE_QUAD_BIT_KHR;
inline Bool ShouldEmulateSubgroups(const Bool nativeSubgroupSupported) {
return MG_Config::Features.MagmaEmulateSubgroup && !nativeSubgroupSupported &&
!MG_Config::Features.MagmaDisableSubgroup;
}
inline Bool ShouldFixIterationRPSubgroupScratch() {
// Auto is ON: the patch is fingerprint-gated to iterationRP's reduction and
// grows one under-declared array; every other module passes through untouched.
return MG_Config::Features.MagmaFixIterationRPSubgroupScratch !=
MG_Config::QuirkOverride::ForceOff;
}
inline Bool ShouldFixIterationRPBarrier() {
return MG_Config::Features.MagmaIterationRPFixBarrier;
}
inline Bool ShouldDeriveNumSubgroups() {
// Auto is ON: gl_NumSubgroups must agree with the gl_SubgroupID range for the GL
// contract to hold, and the derived ceil() value is the one the renderer can pin
// with REQUIRE_FULL_SUBGROUPS - the driver builtin is the value with no
// cross-driver guarantee (Adreno returns 1 for an 8-subgroup dispatch).
return MG_Config::Features.MagmaDeriveNumSubgroups != MG_Config::QuirkOverride::ForceOff;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
+3 -1
View File
@@ -43,4 +43,6 @@ set_tests_properties(SanityBench PROPERTIES LABELS benchmark)
add_subdirectory(Program)
add_subdirectory(Buffer)
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
+85 -30
View File
@@ -18,6 +18,7 @@
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
#include <MG_Util/Texture/PixelStoreProcessor.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
@@ -31,6 +32,8 @@ namespace MobileGL::MG_Impl::GLImpl {
NamedBufferData,
NamedBufferSubData,
CopyNamedBufferSubData,
ClearBufferData,
ClearBufferSubData,
ClearNamedBufferData,
ClearNamedBufferSubData,
MapBufferRange,
@@ -65,6 +68,10 @@ namespace MobileGL::MG_Impl::GLImpl {
return "NamedBufferSubData";
case BufferOp::CopyNamedBufferSubData:
return "CopyNamedBufferSubData";
case BufferOp::ClearBufferData:
return "ClearBufferData";
case BufferOp::ClearBufferSubData:
return "ClearBufferSubData";
case BufferOp::ClearNamedBufferData:
return "ClearNamedBufferData";
case BufferOp::ClearNamedBufferSubData:
@@ -143,16 +150,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return 0;
}
// The pattern is replicated verbatim, which is only the whole story while the client
// layout already matches the internal format - the case every entry point in practice
// uses, and the only one the conversion machinery here can express. Say so rather than
// quietly writing a differently-sized pattern.
const SizeT sourceSize = MG_Util::GetInputBytesPerPixel(inputFormat, pixelType);
if (sourceSize != elementSize) {
MGLOG_W_ONCE("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
"converting between them is not implemented",
GetBufferOpName(op), sourceSize, internalformat, elementSize);
}
return elementSize;
}
@@ -194,27 +191,59 @@ namespace MobileGL::MG_Impl::GLImpl {
return true;
}
void ClearNamedBufferRange_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size,
GLenum format, GLenum type, const void* data, BufferOp op) {
Bool BuildClearPattern(GLenum internalformat, GLenum format, GLenum type, const void* data,
SizeT patternSize, BufferOp op, Vector<Uint8>& pattern) {
const TextureInternalFormat internal = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
const TexturePixelDataType inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
Vector<Uint8> zeroInput;
const void* inputPixel = data;
if (inputPixel == nullptr) {
const SizeT inputSize = MG_Util::GetInputBytesPerPixel(inputFormat, inputType);
if (inputSize == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
"format and type do not describe a source pixel."));
return false;
}
zeroInput.resize(inputSize);
inputPixel = zeroInput.data();
}
if (!MG_Util::PixelStoreProcessor::ConvertOnePixelToInternal(
internal, inputFormat, inputType, inputPixel, pattern)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", GetBufferOpName(op),
std::format("Cannot convert one ({}, {}) pixel into internalformat 0x{:X}.",
MG_Util::ConvertGLEnumToString(format), MG_Util::ConvertGLEnumToString(type),
internalformat)));
return false;
}
if (data == nullptr) {
// GL defines a null clear value as all zero bits in the destination store, while
// retaining the format/type validation above.
pattern.assign(patternSize, 0);
}
return true;
}
void ClearBufferRange_State(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
GLenum internalformat, GLintptr offset, GLsizeiptr size,
GLenum format, GLenum type, const void* data, BufferOp op) {
const SizeT patternSize = GetClearPatternSize(internalformat, format, type, op);
if (patternSize == 0) return;
auto bufferObject = GetNamedBufferObject(buffer, op);
if (!bufferObject) return;
if (!ValidateBufferClearRange(bufferObject, offset, size, patternSize, op)) return;
if (size == 0) return;
Vector<Uint8> clearData(static_cast<SizeT>(size));
if (data) {
const auto* pattern = static_cast<const Uint8*>(data);
for (SizeT at = 0; at < clearData.size(); at += patternSize) {
Memcpy(clearData.data() + at, pattern, patternSize);
}
} else {
Memset(clearData.data(), 0, clearData.size());
}
bufferObject->UploadSubData({clearData.data(), clearData.size()}, static_cast<SizeT>(offset));
Vector<Uint8> pattern;
if (!BuildClearPattern(internalformat, format, type, data, patternSize, op, pattern)) return;
bufferObject->FillSubData({pattern.data(), pattern.size()}, static_cast<SizeT>(offset),
static_cast<SizeT>(size));
}
auto& GetBufferBindingSlot(BufferTarget target) {
@@ -1197,17 +1226,34 @@ namespace MobileGL::MG_Impl::GLImpl {
static_cast<SizeT>(writeOffset), static_cast<SizeT>(size));
}
void ClearBufferData_State(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) {
auto bufferObject = GetBoundBufferObject(target, BufferOp::ClearBufferData);
if (!bufferObject) return;
ClearBufferRange_State(bufferObject, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
type, data, BufferOp::ClearBufferData);
}
void ClearBufferSubData_State(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size,
GLenum format, GLenum type, const void* data) {
auto bufferObject = GetBoundBufferObject(target, BufferOp::ClearBufferSubData);
if (!bufferObject) return;
ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
BufferOp::ClearBufferSubData);
}
void ClearNamedBufferData_State(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferData);
if (!bufferObject) return;
ClearNamedBufferRange_State(buffer, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
type, data, BufferOp::ClearNamedBufferData);
ClearBufferRange_State(bufferObject, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
type, data, BufferOp::ClearNamedBufferData);
}
void ClearNamedBufferSubData_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size,
GLenum format, GLenum type, const void* data) {
ClearNamedBufferRange_State(buffer, internalformat, offset, size, format, type, data,
BufferOp::ClearNamedBufferSubData);
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferSubData);
if (!bufferObject) return;
ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
BufferOp::ClearNamedBufferSubData);
}
void* MapNamedBuffer_State(GLuint buffer, GLenum access) {
@@ -1662,6 +1708,15 @@ namespace MobileGL::MG_Impl::GLImpl {
CopyNamedBufferSubData_State(readBuffer, writeBuffer, readOffset, writeOffset, size);
}
void ClearBufferData(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) {
ClearBufferData_State(target, internalformat, format, type, data);
}
void ClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
GLenum type, const void* data) {
ClearBufferSubData_State(target, internalformat, offset, size, format, type, data);
}
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
ClearNamedBufferData_State(buffer, internalformat, format, type, data);
}
@@ -27,6 +27,9 @@ namespace MobileGL::MG_Impl::GLImpl {
void NamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data);
void CopyNamedBufferSubData(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset,
GLsizeiptr size);
void ClearBufferData(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data);
void ClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
GLenum type, const void* data);
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data);
void ClearNamedBufferSubData(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
GLenum type, const void* data);
@@ -13,6 +13,7 @@
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
#include <MG_Util/Converters/MGToStr/BufferEnumConverter.h>
#include <MG_Util/ShaderTranspiler/Types.h>
namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
Bool ValidateBufferTarget(BufferTarget target) {
@@ -67,6 +68,13 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
// binding points in GL 3.3 (no ARB_transform_feedback3).
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;
}
} // namespace
+271
View File
@@ -0,0 +1,271 @@
// MobileGL - MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.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 "GL_Debug.h"
#include <cstring>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Impl/GLImpl/Query/GL_Query.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
// Must agree with what GL_Getter answers for GL_MAX_DEBUG_GROUP_STACK_DEPTH and
// GL_MAX_DEBUG_MESSAGE_LENGTH / GL_MAX_LABEL_LENGTH; an application that sizes a buffer
// off the query and then trips a different limit here would have no way to explain it.
constexpr SizeT kMaxDebugGroupStackDepth = 64;
constexpr GLsizei kMaxDebugMessageLength = 1024;
constexpr GLsizei kMaxLabelLength = 256;
// The debug state KHR_debug makes per-context. Held here rather than on GLContext because
// nothing else in MobileGL reads it, and it is keyed on the context id so a
// destroyed-and-recreated context starts with an empty stack and no labels - which the
// unit tests, which recreate the context between cases, depend on.
struct DebugState {
Uint64 contextId = 0;
// The messages pushed with glPushDebugGroup, innermost last. The base group GL creates
// the context with is implicit and is what makes the reported depth start at 1.
Vector<String> groupStack;
// Keyed by (identifier, name); see MakeObjectLabelKey.
UnorderedMap<Uint64, String> objectLabels;
};
DebugState& State() {
static DebugState state;
const Uint64 contextId = MG_State::pGLContext ? MG_State::pGLContext->GetTextureContextId() : 0;
if (state.contextId != contextId) {
state.contextId = contextId;
state.groupStack.clear();
state.objectLabels.clear();
}
return state;
}
Uint64 MakeObjectLabelKey(GLenum identifier, GLuint name) {
return (static_cast<Uint64>(identifier) << 32) | static_cast<Uint64>(name);
}
void RecordDebugError(ErrorCode code, const char* caller, const String& message) {
MG_State::pGLContext->RecordError(code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, message));
}
// GL 4.6 core 20.2: only an APPLICATION or THIRD_PARTY source may be injected; the rest
// are reserved for the implementation itself.
Bool ValidateInjectedSource(GLenum source, const char* caller) {
if (source == GL_DEBUG_SOURCE_APPLICATION || source == GL_DEBUG_SOURCE_THIRD_PARTY) {
return true;
}
RecordDebugError(ErrorCode::InvalidEnum, caller,
std::format("source {} is not GL_DEBUG_SOURCE_APPLICATION or "
"GL_DEBUG_SOURCE_THIRD_PARTY.",
MG_Util::ConvertGLEnumToString(source)));
return false;
}
// A negative length means the string is NUL-terminated (GL 4.6 core 20.2), which is how
// every one of these entry points spells "just use the whole thing".
Bool ValidateDebugStringLength(GLsizei length, const GLchar* text, GLsizei limit, const char* caller,
const char* what) {
const GLsizei effective =
length < 0 ? static_cast<GLsizei>(text != nullptr ? std::strlen(text) : 0) : length;
if (effective < limit) {
return true;
}
RecordDebugError(ErrorCode::InvalidValue, caller,
std::format("{} length {} is not less than the {} limit of {}.", what, effective, what,
limit));
return false;
}
String MakeDebugString(GLsizei length, const GLchar* text) {
if (text == nullptr) return {};
return length < 0 ? String(text) : String(text, static_cast<SizeT>(length));
}
// Whether `name` currently names an object of `identifier`'s type. GL 4.6 core 20.5 makes
// labelling something that does not exist INVALID_VALUE, and every type KHR_debug lists
// has a frontend name check - so this is answered exactly rather than waved through.
// GL_DISPLAY_LIST is deliberately absent: it exists only in the compatibility profile,
// which MobileGL does not expose, so it falls to the INVALID_ENUM path below.
Bool ValidateLabelledObject(GLenum identifier, GLuint name, Bool& outIdentifierKnown) {
outIdentifierKnown = true;
auto* context = MG_State::pGLContext.get();
switch (identifier) {
case GL_BUFFER:
return context->ValidateBufferName(name);
case GL_SHADER:
return context->ValidateShaderName(name);
case GL_PROGRAM:
return context->ValidateProgramName(name);
case GL_VERTEX_ARRAY:
return context->ValidateVertexArrayName(name);
case GL_QUERY:
return IsQuery(name) == GL_TRUE;
case GL_PROGRAM_PIPELINE:
return context->ValidateProgramPipelineName(name);
case GL_TRANSFORM_FEEDBACK:
return context->ValidateTransformFeedbackName(name);
case GL_SAMPLER:
return context->ValidateSamplerName(name);
case GL_TEXTURE:
return context->ValidateTextureName(name);
case GL_RENDERBUFFER:
return context->ValidateRenderbufferName(name);
case GL_FRAMEBUFFER:
// Name 0 is the default framebuffer, which is a real, labellable object.
return name == 0 || context->ValidateFramebufferName(name);
default:
outIdentifierKnown = false;
return false;
}
}
} // namespace
GLint GetDebugGroupStackDepth() {
// GL 4.6 core 20.6: the context is created with one group already on the stack, so the
// reported depth is one more than the number of pushes the application has made.
return static_cast<GLint>(State().groupStack.size()) + 1;
}
void PushDebugGroup(GLenum source, GLuint id, GLsizei length, const GLchar* message) {
static_cast<void>(id);
if (!ValidateInjectedSource(source, __func__)) return;
if (!ValidateDebugStringLength(length, message, kMaxDebugMessageLength, __func__, "message")) return;
auto& state = State();
if (state.groupStack.size() + 1 >= kMaxDebugGroupStackDepth) {
// Not INVALID_*: KHR_debug gives the group stack its own error code.
RecordDebugError(ErrorCode::StackOverflow, __func__,
std::format("the debug group stack is already {} deep, which is its maximum.",
kMaxDebugGroupStackDepth));
return;
}
state.groupStack.push_back(MakeDebugString(length, message));
MGLOG_D("glPushDebugGroup(%s) -> depth %d", state.groupStack.back().c_str(), GetDebugGroupStackDepth());
}
void PopDebugGroup() {
auto& state = State();
if (state.groupStack.empty()) {
// The base group the context was created with may not be popped (GL 4.6 core 20.6).
RecordDebugError(ErrorCode::StackUnderflow, __func__,
"the debug group stack holds only the group the context was created with.");
return;
}
MGLOG_D("glPopDebugGroup(%s)", state.groupStack.back().c_str());
state.groupStack.pop_back();
}
void DebugMessageInsert(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length,
const GLchar* buf) {
static_cast<void>(id);
if (!ValidateInjectedSource(source, __func__)) return;
switch (type) {
case GL_DEBUG_TYPE_ERROR:
case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:
case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:
case GL_DEBUG_TYPE_PORTABILITY:
case GL_DEBUG_TYPE_PERFORMANCE:
case GL_DEBUG_TYPE_MARKER:
case GL_DEBUG_TYPE_PUSH_GROUP:
case GL_DEBUG_TYPE_POP_GROUP:
case GL_DEBUG_TYPE_OTHER:
break;
default:
RecordDebugError(ErrorCode::InvalidEnum, __func__,
std::format("type {} is not a debug message type.",
MG_Util::ConvertGLEnumToString(type)));
return;
}
switch (severity) {
case GL_DEBUG_SEVERITY_HIGH:
case GL_DEBUG_SEVERITY_MEDIUM:
case GL_DEBUG_SEVERITY_LOW:
case GL_DEBUG_SEVERITY_NOTIFICATION:
break;
default:
RecordDebugError(ErrorCode::InvalidEnum, __func__,
std::format("severity {} is not a debug message severity.",
MG_Util::ConvertGLEnumToString(severity)));
return;
}
if (!ValidateDebugStringLength(length, buf, kMaxDebugMessageLength, __func__, "message")) return;
// No callback is ever invoked and the message log is empty by construction
// (GL_MAX_DEBUG_LOGGED_MESSAGES is 1 and glGetDebugMessageLog returns nothing), so the
// application-visible effect is exactly the error checking above. The text still reaches
// MobileGL's own log, where it is worth having next to the calls it annotates - at debug
// level, so an application that inserts a message per draw costs nothing in a release build.
MGLOG_D("glDebugMessageInsert: %s", MakeDebugString(length, buf).c_str());
}
void ObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label) {
Bool identifierKnown = false;
const Bool objectExists = ValidateLabelledObject(identifier, name, identifierKnown);
if (!identifierKnown) {
RecordDebugError(ErrorCode::InvalidEnum, __func__,
std::format("identifier {} is not a labellable object type.",
MG_Util::ConvertGLEnumToString(identifier)));
return;
}
if (!objectExists) {
RecordDebugError(ErrorCode::InvalidValue, __func__,
std::format("{} {} is not the name of an existing object.",
MG_Util::ConvertGLEnumToString(identifier), name));
return;
}
if (!ValidateDebugStringLength(length, label, kMaxLabelLength, __func__, "label")) return;
auto& labels = State().objectLabels;
const Uint64 key = MakeObjectLabelKey(identifier, name);
if (label == nullptr) {
// GL 4.6 core 20.5: a NULL label removes any label the object had.
labels.erase(key);
return;
}
labels[key] = MakeDebugString(length, label);
}
void GetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) {
if (bufSize < 0) {
RecordDebugError(ErrorCode::InvalidValue, __func__, "bufSize must not be negative.");
return;
}
Bool identifierKnown = false;
const Bool objectExists = ValidateLabelledObject(identifier, name, identifierKnown);
if (!identifierKnown) {
RecordDebugError(ErrorCode::InvalidEnum, __func__,
std::format("identifier {} is not a labellable object type.",
MG_Util::ConvertGLEnumToString(identifier)));
return;
}
if (!objectExists) {
RecordDebugError(ErrorCode::InvalidValue, __func__,
std::format("{} {} is not the name of an existing object.",
MG_Util::ConvertGLEnumToString(identifier), name));
return;
}
const auto& labels = State().objectLabels;
const auto it = labels.find(MakeObjectLabelKey(identifier, name));
const String& text = it != labels.end() ? it->second : String{};
// GL 4.6 core 20.5: the returned length excludes the NUL, and an unlabelled object hands
// back an empty string with length 0 rather than an error.
SizeT copied = 0;
if (label != nullptr && bufSize > 0) {
copied = std::min(text.size(), static_cast<SizeT>(bufSize) - 1);
std::memcpy(label, text.data(), copied);
label[copied] = '\0';
}
if (length != nullptr) {
*length = static_cast<GLsizei>(copied);
}
}
} // namespace MobileGL::MG_Impl::GLImpl
+42
View File
@@ -0,0 +1,42 @@
// MobileGL - MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.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>
namespace MobileGL::MG_Impl::GLImpl {
// KHR_debug, core since GL 4.3 (GL 4.6 core 20). Applications use these to annotate a capture
// and to name their objects; Better Clouds calls all four for exactly that.
//
// MobileGL implements the STATE and the ERRORS, and deliberately does not forward the calls to
// the host driver. Two independent reasons:
//
// * glObjectLabel names a FRONTEND object. MobileGL's texture 5 is not the ES driver's
// texture 5 (and under DirectVulkan it is not a driver object at all), so forwarding the
// pair verbatim would label an unrelated object or a nonexistent one - worse than not
// labelling.
// * A debug GROUP is only meaningful if it brackets the commands the application issued
// inside it. Neither backend emits its work at the moment the GL call arrives: DirectGLES
// defers and reorders state sync and uploads around draws, and DirectVulkan is usually not
// even recording a command buffer here. A forwarded push/pop would therefore enclose the
// wrong commands, which is a misleading capture rather than a helpful one.
//
// What the application can rely on is the observable contract: the group stack depth is real
// (GL_DEBUG_GROUP_STACK_DEPTH tracks it, and over/underflow raise the errors KHR_debug
// specifies), and a label written with glObjectLabel comes back from glGetObjectLabel.
void PushDebugGroup(GLenum source, GLuint id, GLsizei length, const GLchar* message);
void PopDebugGroup();
void DebugMessageInsert(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length,
const GLchar* buf);
void ObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label);
void GetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label);
// Current depth of the debug group stack, for GL_DEBUG_GROUP_STACK_DEPTH. The base group the
// context is created with counts, so this is never below 1 (GL 4.6 core 20.6).
GLint GetDebugGroupStackDepth();
} // namespace MobileGL::MG_Impl::GLImpl
+410 -41
View File
@@ -34,8 +34,81 @@ namespace MobileGL::MG_Impl::GLImpl {
return true;
}
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
return ValidateProgramForExecution(MG_State::pGLContext->GetProgramForDraw(), functionName);
// Takes the ALREADY-RESOLVED draw program rather than looking it up: GLContext::GetProgramForDraw
// is not a plain getter (it settles the program's link and SPIR-V jobs so every version a
// backend samples during this draw describes the program it is drawing), so the draw funnel
// below resolves it exactly once and hands it to both users.
static Bool ValidateResolvedProgramForDraw(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
const char* functionName) {
// "If there is no current program object or bound program pipeline object, the results of
// a draw are UNDEFINED" - and undefined is not an error (GL 4.6 core 7.3, ES 3.1 7.3).
// The draw is dropped, silently, which is one of the shapes "undefined" is allowed to
// take; recording INVALID_OPERATION here is not, and es31cSeparateShaderObjsTests'
// StateInteraction reads exactly that error back after useProgram(0) + bindProgramPipeline(0).
// A DISPATCH is the opposite rule ("INVALID_OPERATION if there is no active program for
// the compute shader stage"), which is why this lives on the draw path and not in the
// shared ValidateProgramForExecution below.
if (!currentProgram) return false;
if (!ValidateProgramForExecution(currentProgram, functionName)) return false;
// GL 4.6 core 7.4.1, the pipeline validation rule every vertex-transferring command
// inherits: it is an INVALID_OPERATION when a tessellation control, tessellation
// evaluation or geometry stage has an executable but no program supplies an executable
// VERTEX shader. A non-separable program cannot reach this - the link rule forbids the
// shape - so in practice it catches a program pipeline assembled out of stage programs,
// which today draws happily and renders nothing.
//
// Asked of the EXECUTABLE, like the compute check below: for a pipeline the resolved
// program is the graphics composite, whose linked-shader snapshot is built out of exactly
// the pipeline's own graphics stage programs (GLContext::GetProgramForDraw), and the only
// stage compositing ever invents is a default FRAGMENT shader. A fragment-only pipeline is
// deliberately NOT rejected: the rule above names the three pre-rasterization stages, and
// nothing else here should start refusing draws GL accepts.
//
// On the DRAW path only, never in ValidateProgramForExecution itself, so a dispatch -
// which shares that helper and legitimately has no vertex stage - is untouched.
const Bool hasPreRasterizationStage = currentProgram->HasLinkedShaderStage(ShaderStage::Geometry) ||
currentProgram->HasLinkedShaderStage(ShaderStage::TessControl) ||
currentProgram->HasLinkedShaderStage(ShaderStage::TessEval);
if (hasPreRasterizationStage && !currentProgram->HasLinkedShaderStage(ShaderStage::Vertex)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"The program in use runs a geometry or tessellation stage but has no vertex shader stage."));
return false;
}
return true;
}
// gl_NumSamples has no SPIR-V built-in, so the source pipeline lowers it onto a reserved
// default-block uniform (see InjectNumSamplesBuiltinShim). This is where that uniform is paid
// for: the value is a property of the DRAW FRAMEBUFFER, not of the program, so one program
// drawn into a 4x target and then into the default framebuffer must see 4 and then 1 - which
// rules out baking it at link time.
//
// Per draw rather than on framebuffer changes because the pair (program, framebuffer) is what
// decides the value and either half can move between draws. It costs a phase-A flag read for
// every program that has no shim, and a 4-byte compare for the ones that do: the write only
// bumps the UBO content version when the number actually changes, so a run of draws into one
// framebuffer re-uploads nothing.
static void PublishDrawFramebufferSampleCount(const SharedPtr<MG_State::GLState::ProgramObject>& program) {
if (!program || !program->UsesReservedNumSamples()) return;
// GL 4.6 core 15.2.2: gl_NumSamples is the number of samples in the framebuffer, or ONE
// when the target is not multisampled - where glGetIntegerv(GL_SAMPLES) answers zero.
program->WriteReservedNumSamples(static_cast<Int>(std::max<GLint>(ResolveDrawFramebufferSampleCount(), 1)));
}
// The one funnel every drawing command passes through. Order is load-bearing: validate first
// (a rejected draw must leave state alone), then publish the sample count - which reads the
// DRAW FRAMEBUFFER binding, so it has to run after the caller's framebuffer state is settled
// and before the backend consumes the program's UBO content version.
static Bool PrepareCurrentProgramForDraw(const char* functionName) {
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
if (!ValidateResolvedProgramForDraw(currentProgram, functionName)) return false;
PublishDrawFramebufferSampleCount(currentProgram);
return true;
}
// A dispatch resolves its program through the DISPATCH accessor: with a pipeline bound
@@ -45,7 +118,11 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch();
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(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
@@ -69,6 +146,20 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TRIANGLES: return static_cast<Uint64>(count / 3);
case GL_TRIANGLE_STRIP:
case GL_TRIANGLE_FAN: return count >= 3 ? static_cast<Uint64>(count - 2) : 0;
// Adjacency primitives (GL 4.6 core table 10.1). Only a geometry stage can consume
// them, and it is the ADJACENT-free primitive count that reaches it: 4 vertices per
// line, 6 per triangle, one per step for the strips. Answering 0 here - which is what
// the default arm did - made AccountTransformFeedbackPrimitives bail before it had
// recorded anything, so an adjacency capture advanced neither the captured-vertex
// counter the scattered-capture path is bounded by nor the geometry-capture-draw flag
// that routes the transform feedback queries to the driver's own counter.
case GL_LINES_ADJACENCY: return static_cast<Uint64>(count / 4);
case GL_LINE_STRIP_ADJACENCY: return count >= 4 ? static_cast<Uint64>(count - 3) : 0;
case GL_TRIANGLES_ADJACENCY: return static_cast<Uint64>(count / 6);
case GL_TRIANGLE_STRIP_ADJACENCY: return count >= 6 ? static_cast<Uint64>((count - 4) / 2) : 0;
// GL_PATCHES is deliberately absent: the tessellator's amplification is not knowable
// on the CPU, and answering 0 is what defers GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
// to the driver's own counter, which is the only correct source for a patch capture.
default: return 0;
}
}
@@ -95,11 +186,17 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_LINES:
case GL_LINE_STRIP:
case GL_LINE_LOOP:
// An adjacency primitive delivers the same line/triangle to the geometry stage; the
// adjacent vertices are context, not part of the primitive.
case GL_LINES_ADJACENCY:
case GL_LINE_STRIP_ADJACENCY:
verticesPerPrimitive = 2;
break;
case GL_TRIANGLES:
case GL_TRIANGLE_STRIP:
case GL_TRIANGLE_FAN:
case GL_TRIANGLES_ADJACENCY:
case GL_TRIANGLE_STRIP_ADJACENCY:
verticesPerPrimitive = 3;
break;
default:
@@ -108,6 +205,12 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
if (program != nullptr) {
// A geometry stage writes what it emits, not what the draw assembled, and the
// amplification factor lives in the shader. Record that this span contained such
// a draw so the transform feedback queries keep their backend result for it.
if (program->HasLinkedShaderStage(ShaderStage::Geometry)) {
MG_State::pGLContext->AddTransformFeedbackGeometryCaptureDraw();
}
// Capacity in captured vertices = the tightest bound buffer.
Uint64 capacityVertices = ~0ull;
for (SizeT i = 0; i < program->GetTransformFeedbackBufferCount(); ++i) {
@@ -127,6 +230,11 @@ namespace MobileGL::MG_Impl::GLImpl {
}
MG_State::pGLContext->AddTransformFeedbackPrimitives(primitives);
MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive);
// Only draws that get this far are in the written counter at all. The instanced and
// indirect entry points never call this function, so a span that contains one is NOT
// fully accounted, and the queries must be able to tell: they compare this counter's
// delta against zero before standing in for the backend's own result.
MG_State::pGLContext->AddTransformFeedbackAccountedCaptureDraw();
}
// Every primitive mode a draw command accepts (GL 4.6 core table 10.1, plus
@@ -151,11 +259,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) {
if (!IsAcceptedPrimitiveMode(mode)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "mode is not an accepted primitive type."));
if (!ValidatePrimitiveModeEnum(functionName, mode)) {
return false;
}
@@ -176,13 +296,58 @@ namespace MobileGL::MG_Impl::GLImpl {
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
// 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
// converted, so it is not constrained here.
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE;
if (gsInput != GL_NONE && mode != GL_PATCHES) {
//
// "Is there a geometry stage at all" has to be asked of the STAGE, never of the input
// 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;
switch (gsInput) {
case GL_POINTS:
@@ -216,24 +381,41 @@ namespace MobileGL::MG_Impl::GLImpl {
// 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 constraint moves to the shader's output primitive type instead, so
// the draw mode itself is unconstrained here. 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).
// the draw mode itself is unconstrained here - and a TESSELLATION EVALUATION
// stage relocates it exactly the same way (GL 4.6 core 13.2.2 names both):
// 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() &&
!MG_State::pGLContext->IsTransformFeedbackPaused() &&
!(MG_State::pGLContext->GetTransformFeedbackProgram() &&
MG_State::pGLContext->GetTransformFeedbackProgram()->GetShaderIndexByStage(ShaderStage::Geometry) >= 0)) {
!MG_State::pGLContext->IsTransformFeedbackPaused() && !feedbackModeIsProgramDriven) {
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
Bool compatible = false;
switch (feedbackMode) {
case GL_POINTS:
compatible = mode == GL_POINTS;
break;
// The adjacency modes belong here too (GL 4.6 core table 13.1, ES 3.2 table 12.1).
// This arm is only reached when the program has NO geometry or tessellation
// evaluation stage, and without a geometry stage the adjacent vertices are simply
// ignored (GL 4.6 core 10.1) - the primitive assembled IS a plain line or triangle,
// so the combination is legal and must capture. Omitting them raised a spurious
// GL_INVALID_OPERATION and dropped the draw entirely, leaving the capture buffer
// with its pre-draw bytes. The geometry-stage input table above already carries the
// same four arms; this is the second table catching up with it.
case GL_LINES:
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP;
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP ||
mode == GL_LINES_ADJACENCY || mode == GL_LINE_STRIP_ADJACENCY;
break;
case GL_TRIANGLES:
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN;
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN ||
mode == GL_TRIANGLES_ADJACENCY || mode == GL_TRIANGLE_STRIP_ADJACENCY;
break;
default:
break;
@@ -303,10 +485,48 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// GL 4.6 core 10.3.9: every DrawElements-family count is a sizei and "if count is negative, an
// INVALID_VALUE error is generated". The same sentence covers instancecount and the
// MultiDraw* drawcount, so one helper serves all of them; the parameter is named for the
// caller so the message says which argument the application actually got wrong.
static Bool ValidateNonNegativeDrawArgument(const char* functionName, const char* argumentName, GLsizei value) {
if (value >= 0) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
String(argumentName) + " must be non-negative."));
return false;
}
// GL 4.6 core 10.3.9 for DrawRangeElements*: "if end < start, an INVALID_VALUE error is
// generated". Both are uints, so a caller that passes -1 for start arrives here as
// 0xFFFFFFFF and is caught by the same comparison - which is exactly what
// KHR-GL4x.draw_elements_base_vertex_tests.invalid_count_argument checks.
static Bool ValidateDrawElementsRange(const char* functionName, GLuint start, GLuint end) {
if (end >= start) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "end must not be less than start."));
return false;
}
// GL 4.6 core 10.9: inside a conditional block whose predicate did not pass, the drawing
// commands, Clear, ClearBuffer* and the compute dispatches are DISCARDED. The gate sits on the
// wrappers that ISSUE the backend call rather than at the top of each entry point, so that
// everything a real driver would still do inside the block - argument validation and the
// errors it raises - happens exactly as it does outside one, and only the command itself is
// dropped. It is deliberately not on the frontend's transform-feedback accounting either:
// that mirrors what the capture stage would have written, and a conditional block around a
// capturing draw has no test coverage in either direction.
static Bool ConditionalRenderDiscardsCommand() {
return MG_State::pGLContext->ConditionalRenderDiscardsCommands();
}
void Clear_Backend(GLbitfield mask) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.Clear(mask);
}
@@ -314,6 +534,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices);
}
@@ -322,6 +543,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
}
@@ -330,6 +552,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
basevertex);
}
@@ -338,6 +561,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
}
@@ -345,6 +569,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
}
@@ -353,6 +578,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
}
@@ -361,6 +587,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
}
@@ -368,6 +595,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
}
@@ -376,6 +604,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
maxdrawcount, stride);
}
@@ -385,6 +614,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
stride);
}
@@ -394,6 +624,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices,
basevertex);
}
@@ -403,6 +634,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices);
}
@@ -412,6 +644,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
mode, count, type, indices, instancecount, basevertex, baseinstance);
}
@@ -421,6 +654,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount,
basevertex);
}
@@ -430,6 +664,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
instancecount, baseinstance);
}
@@ -439,6 +674,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount);
}
@@ -446,6 +682,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect);
}
void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
@@ -453,6 +690,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
baseinstance);
}
@@ -461,6 +699,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount);
}
@@ -468,6 +707,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect);
}
@@ -496,6 +736,9 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
}
// GL 4.3 added both dispatches to the conditional-render set (GL 4.6 core 10.9), which is
// exactly what KHR-GL43.compute_shader.conditional-dispatching checks.
if (ConditionalRenderDiscardsCommand()) return;
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
}
@@ -547,6 +790,7 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
if (!ValidateCurrentProgramForCompute(__func__)) return;
if (ConditionalRenderDiscardsCommand()) return;
dispatchComputeIndirect(indirect);
}
@@ -572,7 +816,65 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// GL 4.6 core 11.2.2. The default tessellation levels a program with an evaluation stage and
// NO control stage tessellates at; both backends have to synthesize that control stage
// themselves (ES 3.2 and Vulkan both require one), and they compile these numbers into it, so
// there is no backend entry point to forward to - ES has none at all. INVALID_ENUM on a bad
// pname is the only error the spec lists: any float values are accepted, negatives and NaN
// included, and it is the tessellator that clamps them.
//
// This used to be a stub, which is why the two synthesizers hardcoded 1.0.
void PatchParameterfv(GLenum pname, const GLfloat* values) {
if (pname != GL_PATCH_DEFAULT_OUTER_LEVEL && pname != GL_PATCH_DEFAULT_INNER_LEVEL) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"pname must be GL_PATCH_DEFAULT_OUTER_LEVEL or GL_PATCH_DEFAULT_INNER_LEVEL."));
return;
}
if (!values) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "values pointer cannot be null"));
return;
}
if (pname == GL_PATCH_DEFAULT_OUTER_LEVEL) {
MG_State::pGLContext->SetPatchDefaultOuterLevel(
FloatVec4(values[0], values[1], values[2], values[3]));
} else {
MG_State::pGLContext->SetPatchDefaultInnerLevel(FloatVec2(values[0], values[1]));
}
}
namespace {
// GL 4.6 core 7.11.2 (and ARB_shader_image_load_store, which introduced the call): the
// barrier bitfield is INVALID_VALUE unless every bit is one of the defined ones, with
// GL_ALL_BARRIER_BITS - which is 0xFFFFFFFF, not the union of the list - accepted whole.
// Forwarding an undefined bit to the host driver let a caller that had computed its mask
// wrongly (or reused an ES-only bit) get silence instead of the error the spec promises.
constexpr GLbitfield kAllDefinedBarrierBits =
GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT | GL_UNIFORM_BARRIER_BIT |
GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_COMMAND_BARRIER_BIT |
GL_PIXEL_BUFFER_BARRIER_BIT | GL_TEXTURE_UPDATE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT |
GL_FRAMEBUFFER_BARRIER_BIT | GL_TRANSFORM_FEEDBACK_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT |
GL_SHADER_STORAGE_BARRIER_BIT | GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT | GL_QUERY_BUFFER_BARRIER_BIT;
Bool ValidateMemoryBarrierBits(const char* function, GLbitfield barriers) {
if (barriers == GL_ALL_BARRIER_BITS) return true;
if ((barriers & ~kAllDefinedBarrierBits) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function,
"barriers contains bits that are not defined barrier bits."));
return false;
}
return true;
}
} // namespace
void MemoryBarrier(GLbitfield barriers) {
if (!ValidateMemoryBarrierBits(__func__, barriers)) return;
auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
if (!memoryBarrier) {
MG_State::pGLContext->RecordError(
@@ -583,7 +885,29 @@ namespace MobileGL::MG_Impl::GLImpl {
memoryBarrier(barriers);
}
void TextureBarrier() {
// GL 4.5 core 8.26 / GL_ARB_texture_barrier: order every write the fixed-function
// framebuffer has already issued ahead of every subsequent texture fetch, so a shader may
// read texels of a texture that is also attached to the current framebuffer.
//
// Both backends serve this through their existing memory-barrier hook rather than a new
// entry point of their own: GL_FRAMEBUFFER_BARRIER_BIT is the source half (framebuffer
// writes) and GL_TEXTURE_FETCH_BARRIER_BIT the destination half (texture fetches), which
// is exactly the dependency ARB_texture_barrier defines - just expressed with the wider
// scope glMemoryBarrier gives it. That is a superset of the required ordering, never a
// subset, so it cannot under-synchronize.
auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
if (!memoryBarrier) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers."));
return;
}
memoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT);
}
void MemoryBarrierByRegion(GLbitfield barriers) {
if (!ValidateMemoryBarrierBits(__func__, barriers)) return;
auto memoryBarrierByRegion = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrierByRegion;
if (!memoryBarrierByRegion) {
MG_State::pGLContext->RecordError(
@@ -596,13 +920,15 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride);
}
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
}
@@ -680,7 +1006,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// NegativeApiErrorsTest.IndirectParameterDrawsCheckBothBuffers pins the INVALID_VALUE
// they produce for a call made with no program bound. Same precedence decision, and
// the same reason, as DispatchComputeIndirect above.
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount;
if (!multiDrawElementsIndirectCount) {
MG_State::pGLContext->RecordError(
@@ -701,7 +1027,7 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
// See MultiDrawElementsIndirectCount, including why this one goes last.
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount;
if (!multiDrawArraysIndirectCount) {
MG_State::pGLContext->RecordError(
@@ -715,20 +1041,26 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) return;
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
if (!ValidateDrawElementsRange(__func__, start, end)) return;
DrawRangeElementsBaseVertex_Backend(mode, start, end, count, type, indices, basevertex);
}
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawRangeElements_Backend(mode, start, end, count, type, indices);
}
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex,
baseinstance);
@@ -736,26 +1068,33 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) return;
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
if (!ValidateNonNegativeDrawArgument(__func__, "instancecount", instancecount)) return;
DrawElementsInstancedBaseVertex_Backend(mode, count, type, indices, instancecount, basevertex);
}
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLuint baseinstance) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance);
}
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstanced_Backend(mode, count, type, indices, instancecount);
}
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) return;
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawElementsIndirectCommandBytes)) return;
@@ -764,40 +1103,48 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
GLuint baseinstance) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance);
}
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawArraysInstanced_Backend(mode, first, count, instancecount);
}
void DrawArraysIndirect(GLenum mode, const void* indirect) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return;
DrawArraysIndirect_Backend(mode, indirect);
}
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) return;
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
AccountTransformFeedbackPrimitives(mode, count);
DrawElementsBaseVertex_Backend(mode, count, type, indices, basevertex);
}
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count);
DrawArrays_Backend(mode, first, count);
}
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (drawcount < 0) {
MG_State::pGLContext->RecordError(
@@ -810,15 +1157,30 @@ namespace MobileGL::MG_Impl::GLImpl {
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
GLsizei drawcount) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawElements_Backend(mode, count, type, indices, drawcount);
}
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
GLsizei drawcount, const GLint* basevertex) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) return;
if (!ValidateNonNegativeDrawArgument(__func__, "drawcount", drawcount)) return;
// GL 4.6 core 10.5 defines MultiDrawElementsBaseVertex as drawcount separate
// DrawElementsBaseVertex calls, so each element of the count array carries the same
// non-negative requirement the single-draw entry point applies to its own count. The
// whole call is rejected before any sub-draw is issued, which is what makes the error
// observable at all - a driver that drew the valid prefix first would leave the
// framebuffer half-written.
if (count != nullptr) {
for (GLsizei draw = 0; draw < drawcount; ++draw) {
if (!ValidateNonNegativeDrawArgument(__func__, "every element of count", count[draw])) return;
}
}
MultiDrawElementsBaseVertex_Backend(mode, count, type, indices, drawcount, basevertex);
}
@@ -827,7 +1189,8 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
if (!PrepareCurrentProgramForDraw(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count);
DrawElements_Backend(mode, count, type, indices);
@@ -1251,7 +1614,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// (GL 4.6 core 10.3.7).
static void DrawTransformFeedbackImpl(const char* functionName, GLenum mode, GLuint id, GLuint stream,
GLsizei instancecount) {
if (!ValidateCurrentProgramForExecution(functionName)) return;
if (!PrepareCurrentProgramForDraw(functionName)) return;
if (!ValidatePrimitiveModeForBackend(functionName, mode)) return;
if (instancecount < 0) {
MG_State::pGLContext->RecordError(
@@ -1274,8 +1637,13 @@ namespace MobileGL::MG_Impl::GLImpl {
std::to_string(id) + " is not a transform feedback object name."));
return;
}
// GL_MAX_VERTEX_STREAMS is 1, so stream 0 is the only one that exists.
if (stream != 0) {
// GL 4.6 core 10.3.7 bounds `stream` by GL_MAX_VERTEX_STREAMS, which this implementation
// answers as 1 - so stream 0 is the only one that exists and anything else is
// INVALID_VALUE. Read from the getter rather than written as `stream != 0` so the two can
// never drift: if vertex-stream support ever lands, this bound moves with the limit.
GLint maxVertexStreams = 1;
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
if (stream >= static_cast<GLuint>(std::max(maxVertexStreams, 1))) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
@@ -1293,6 +1661,7 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
// `stream` is provably 0 here (the bound above is 1), so this is stream 0's record.
const Uint64 vertices = MG_State::pGLContext->GetTransformFeedbackRecordedVertices(id);
if (vertices == 0) return;
const auto count = static_cast<GLsizei>(vertices);
@@ -32,8 +32,10 @@ namespace MobileGL::MG_Impl::GLImpl {
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void DispatchComputeIndirect(GLintptr indirect);
void PatchParameteri(GLenum pname, GLint value);
void PatchParameterfv(GLenum pname, const GLfloat* values);
void MemoryBarrier(GLbitfield barriers);
void MemoryBarrierByRegion(GLbitfield barriers);
void TextureBarrier();
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
@@ -20,6 +20,7 @@
#include "../Framebuffer/GL_Framebuffer.h"
#include "../VertexArray/GL_VertexArray.h"
#include "../Sync/GL_Sync.h"
#include "../Debug/GL_Debug.h"
#include <MG_State/GLState/Core.h>
#define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
@@ -159,7 +160,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ReleaseShaderCompiler) DECLARE_GL_FUNCTION_S
DECLARE_GL_FUNCTION_HEAD(void, RenderbufferStorage, GLenum target, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, RenderbufferStorage, target, internalformat, width, height)
DECLARE_GL_FUNCTION_HEAD(void, SampleCoverage, GLfloat value, GLboolean invert) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SampleCoverage, value, invert)
DECLARE_GL_FUNCTION_HEAD(void, Scissor, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Scissor, x, y, width, height)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShaderBinary, GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShaderBinary, count, shaders, binaryformat, binary, length)
DECLARE_GL_FUNCTION_HEAD(void, ShaderBinary, GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderBinary, count, shaders, binaryformat, binary, length)
DECLARE_GL_FUNCTION_HEAD(void, ShaderSource, GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderSource, shader, count, string, length)
DECLARE_GL_FUNCTION_HEAD(void, StencilFunc, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFunc, func, ref, mask)
DECLARE_GL_FUNCTION_HEAD(void, StencilFuncSeparate, GLenum face, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFuncSeparate, face, func, ref, mask)
@@ -378,27 +379,13 @@ DECLARE_GL_FUNCTION_HEAD(void, VertexBindingDivisor, GLuint bindingindex, GLuint
DECLARE_GL_FUNCTION_STUB_HEAD(void, BlendBarrier) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BlendBarrier)
DECLARE_GL_FUNCTION_HEAD(void, CopyImageSubData, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyImageSubData, srcName, srcTarget, srcLevel, srcX, srcY, srcZ, dstName, dstTarget, dstLevel, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageControl, GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint* ids, GLboolean enabled) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageControl, source, type, severity, count, ids, enabled)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageInsert, GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* buf) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageInsert, source, type, id, severity, length, buf)
DECLARE_GL_FUNCTION_HEAD(void, DebugMessageInsert, GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* buf) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DebugMessageInsert, source, type, id, severity, length, buf)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageCallback, GLDEBUGPROC callback, const void* userParam) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageCallback, callback, userParam)
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, GetDebugMessageLog, GLuint count, GLsizei bufSize, GLenum* sources, GLenum* types, GLuint* ids, GLenum* severities, GLsizei* lengths, GLchar* messageLog) DECLARE_GL_FUNCTION_STUB_END(GLuint, GetDebugMessageLog, count, bufSize, sources, types, ids, severities, lengths, messageLog)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PushDebugGroup, GLenum source, GLuint id, GLsizei length, const GLchar* message) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushDebugGroup, source, id, length, message)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PopDebugGroup) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopDebugGroup)
MOBILEGL_GL_API void glObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label) {
(void)identifier;
(void)name;
(void)length;
(void)label;
}
MOBILEGL_GL_API void glGetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) {
(void)identifier;
(void)name;
if (length) {
*length = 0;
}
if (label && bufSize > 0) {
label[0] = '\0';
}
}
DECLARE_GL_FUNCTION_HEAD(void, PushDebugGroup, GLenum source, GLuint id, GLsizei length, const GLchar* message) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PushDebugGroup, source, id, length, message)
DECLARE_GL_FUNCTION_HEAD(void, PopDebugGroup) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PopDebugGroup)
DECLARE_GL_FUNCTION_HEAD(void, ObjectLabel, GLenum identifier, GLuint name, GLsizei length, const GLchar* label) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ObjectLabel, identifier, name, length, label)
DECLARE_GL_FUNCTION_HEAD(void, GetObjectLabel, GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetObjectLabel, identifier, name, bufSize, length, label)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ObjectPtrLabel, const void* ptr, GLsizei length, const GLchar* label) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ObjectPtrLabel, ptr, length, label)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectPtrLabel, const void* ptr, GLsizei bufSize, GLsizei* length, GLchar* label) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectPtrLabel, ptr, bufSize, length, label)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPointerv, GLenum pname, void** params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetPointerv, pname, params)
@@ -424,7 +411,7 @@ DECLARE_GL_FUNCTION_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLs
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MinSampleShading, value)
DECLARE_GL_FUNCTION_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MinSampleShading, value)
DECLARE_GL_FUNCTION_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameteri, pname, value)
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIiv, GLenum target, GLenum pname, const GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIiv, target, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIuiv, GLenum target, GLenum pname, const GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIuiv, target, pname, params)
@@ -725,8 +712,8 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, LoadName, GLuint name) DECLARE_GL_FUNCTION_S
DECLARE_GL_FUNCTION_STUB_HEAD(void, PushName, GLuint name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushName, name)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PopName) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopName)
DECLARE_GL_FUNCTION_HEAD(void, ClampColor, GLenum target, GLenum clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClampColor, target, clamp)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginConditionalRender, id, mode)
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndConditionalRender, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndConditionalRender)
DECLARE_GL_FUNCTION_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginConditionalRender, id, mode)
DECLARE_GL_FUNCTION_HEAD(void, EndConditionalRender) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndConditionalRender)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI1i, GLuint index, GLint x) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI1i, index, x)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI2i, GLuint index, GLint x, GLint y) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI2i, index, x, y)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI3i, GLuint index, GLint x, GLint y, GLint z) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI3i, index, x, y, z)
@@ -936,7 +923,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveSubroutineName, GLuint program, GLe
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformSubroutinesuiv, GLenum shadertype, GLsizei count, const GLuint* indices) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformSubroutinesuiv, shadertype, count, indices)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformSubroutineuiv, GLenum shadertype, GLint location, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformSubroutineuiv, shadertype, location, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStageiv, GLuint program, GLenum shadertype, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStageiv, program, shadertype, pname, values)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PatchParameterfv, pname, values)
DECLARE_GL_FUNCTION_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameterfv, pname, values)
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedback, mode, id)
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream)
DECLARE_GL_FUNCTION_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginQueryIndexed, target, index, id)
@@ -969,24 +956,24 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL3dv, GLuint index, const GLdoub
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL4dv, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL4dv, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLPointer, index, size, type, stride, pointer)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexAttribLdv, GLuint index, GLenum pname, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexAttribLdv, index, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportArrayv, first, count, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedfv, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorArrayv, first, count, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexedv, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
DECLARE_GL_FUNCTION_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportArrayv, first, count, v)
DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h)
DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedfv, index, v)
DECLARE_GL_FUNCTION_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorArrayv, first, count, v)
DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height)
DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexedv, index, v)
DECLARE_GL_FUNCTION_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
DECLARE_GL_FUNCTION_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
DECLARE_GL_FUNCTION_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetFloati_v, target, index, data)
DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetDoublei_v, target, index, data)
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, 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, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, 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)
DECLARE_GL_FUNCTION_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInternalformati64v, GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInternalformati64v, target, internalformat, pname, count, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth)
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexImage, GLuint texture, GLint level) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexImage, texture, level)
@@ -996,7 +983,7 @@ DECLARE_GL_FUNCTION_HEAD(void, MultiDrawArraysIndirect, GLenum mode, const void*
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirect, GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawElementsIndirect, mode, type, indirect, drawcount, stride)
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocationIndex, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocationIndex, program, programInterface, name)
DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderStorageBlockBinding, program, storageBlockIndex, storageBlockBinding)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
DECLARE_GL_FUNCTION_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset)
DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags)
DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data)
@@ -1007,7 +994,7 @@ DECLARE_GL_FUNCTION_HEAD(void, BindTextures, GLuint first, GLsizei count, const
DECLARE_GL_FUNCTION_HEAD(void, BindSamplers, GLuint first, GLsizei count, const GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindSamplers, first, count, samplers)
DECLARE_GL_FUNCTION_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindImageTextures, first, count, textures)
DECLARE_GL_FUNCTION_HEAD(void, BindVertexBuffers, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizei* strides) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindVertexBuffers, first, count, buffers, offsets, strides)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClipControl, origin, depth)
DECLARE_GL_FUNCTION_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClipControl, origin, depth)
DECLARE_GL_FUNCTION_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids)
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferBase, GLuint xfb, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferBase, xfb, index, buffer)
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferRange, GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferRange, xfb, index, buffer, offset, size)
@@ -1060,9 +1047,9 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureStorage3DMultisample, GLuint texture, GLsi
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage1D, texture, level, xoffset, width, format, type, pixels)
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, type, pixels)
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height)
@@ -1120,11 +1107,11 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnConvolutionFilter, GLenum target, GLenum
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnSeparableFilter, GLenum target, GLenum format, GLenum type, GLsizei rowBufSize, void* row, GLsizei columnBufSize, void* column, void* span) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnSeparableFilter, target, format, type, rowBufSize, row, columnBufSize, column, span)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnHistogram, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnHistogram, target, reset, format, type, bufSize, values)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMinmax, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnMinmax, target, reset, format, type, bufSize, values)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBarrier, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBarrier, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, SpecializeShader, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
DECLARE_GL_FUNCTION_HEAD(void, TextureBarrier, void) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBarrier, )
DECLARE_GL_FUNCTION_HEAD(void, SpecializeShader, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawArraysIndirectCount, GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawArraysIndirectCount, mode, indirect, drawcount, maxdrawcount, stride)
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirectCount, GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawElementsIndirectCount, mode, type, indirect, drawcount, maxdrawcount, stride)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PolygonOffsetClamp, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
DECLARE_GL_FUNCTION_HEAD(void, PolygonOffsetClamp, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBoxARB, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END(void, PrimitiveBoundingBoxARB, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint64, GetTextureHandleARB, GLuint texture) DECLARE_GL_FUNCTION_STUB_END(GLuint64, GetTextureHandleARB, texture)
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint64, GetTextureSamplerHandleARB, GLuint texture, GLuint sampler) DECLARE_GL_FUNCTION_STUB_END(GLuint64, GetTextureSamplerHandleARB, texture, sampler)
@@ -1163,7 +1150,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramLocalParameterdvARB, GLenum target
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramLocalParameterfvARB, GLenum target, GLuint index, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramLocalParameterfvARB, target, index, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStringARB, GLenum target, GLenum pname, void* string) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStringARB, target, pname, string)
DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferTextureFaceARB, GLenum target, GLenum attachment, GLuint texture, GLint level, GLenum face) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferTextureFaceARB, target, attachment, texture, level, face)
DECLARE_GL_FUNCTION_STUB_HEAD(void, SpecializeShaderARB, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SpecializeShaderARB, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
DECLARE_GL_FUNCTION_HEAD(void, SpecializeShaderARB, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1i64ARB, GLint location, GLint64 x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1i64ARB, location, x)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2i64ARB, GLint location, GLint64 x, GLint64 y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2i64ARB, location, x, y)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3i64ARB, GLint location, GLint64 x, GLint64 y, GLint64 z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3i64ARB, location, x, y, z)
@@ -1848,9 +1835,9 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetBooleanIndexedvEXT, GLenum target, GLuint
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage3DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage3DEXT, texture, target, level, internalformat, width, height, depth, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage2DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage2DEXT, texture, target, level, internalformat, width, height, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage1DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage1DEXT, texture, target, level, internalformat, width, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3DEXT, texture, target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1DEXT, texture, target, level, xoffset, width, format, imageSize, bits)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImageEXT, GLuint texture, GLenum target, GLint lod, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImageEXT, texture, target, lod, img)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage3DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage3DEXT, texunit, target, level, internalformat, width, height, depth, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage2DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage2DEXT, texunit, target, level, internalformat, width, height, border, imageSize, bits)
@@ -2062,7 +2049,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPixelTransformParameterivEXT, GLenum targ
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPixelTransformParameterfvEXT, GLenum target, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetPixelTransformParameterfvEXT, target, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfEXT, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfEXT, pname, param)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfvEXT, GLenum pname, const GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfvEXT, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PolygonOffsetClampEXT, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PolygonOffsetClampEXT, factor, units, clamp)
DECLARE_GL_FUNCTION_HEAD(void, PolygonOffsetClampEXT, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
DECLARE_GL_FUNCTION_HEAD(void, ProvokingVertexEXT, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProvokingVertex, mode)
DECLARE_GL_FUNCTION_STUB_HEAD(void, RasterSamplesEXT, GLuint samples, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, RasterSamplesEXT, samples, fixedsamplelocations)
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColor3bEXT, GLbyte red, GLbyte green, GLbyte blue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColor3bEXT, red, green, blue)
@@ -2559,7 +2546,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateImageBarrierNV, GLboolean synchro
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateImagePaletteNV, GLuint viewport, GLuint first, GLsizei count, const GLenum* rates) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateImagePaletteNV, viewport, first, count, rates)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateSampleOrderNV, GLenum order) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateSampleOrderNV, order)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateSampleOrderCustomNV, GLenum rate, GLuint samples, const GLint* locations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateSampleOrderCustomNV, rate, samples, locations)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBarrierNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBarrierNV, )
DECLARE_GL_FUNCTION_HEAD(void, TextureBarrierNV, void) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBarrier, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexImage2DMultisampleCoverageNV, GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLint internalFormat, GLsizei width, GLsizei height, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexImage2DMultisampleCoverageNV, target, coverageSamples, colorSamples, internalFormat, width, height, fixedSampleLocations)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexImage3DMultisampleCoverageNV, GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLint internalFormat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexImage3DMultisampleCoverageNV, target, coverageSamples, colorSamples, internalFormat, width, height, depth, fixedSampleLocations)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureImage2DMultisampleNV, GLuint texture, GLenum target, GLsizei samples, GLint internalFormat, GLsizei width, GLsizei height, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureImage2DMultisampleNV, texture, target, samples, internalFormat, width, height, fixedSampleLocations)
@@ -13,6 +13,7 @@
#include <MG_Backend/BackendObjects.h>
#include <MG_Util/Metrics/TextureMetrics.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_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
@@ -473,6 +474,75 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// GL 4.6 core 9.2.8 conditions that depend only on the framebuffer and the attachment
// point. Shared, because glFramebufferTexture / 1D / 2D / 3D / TextureLayer are aliases of
// one another in that section and a CTS case that walks the family must not get five
// different answers - which is exactly what happened when these lived in one helper that
// only two of the five went through.
Bool ValidateFramebufferTextureAttachmentPoint(const char* functionName,
const SharedPtr<MG_State::GLState::FramebufferObject>&
framebufferObject,
FramebufferAttachmentType attachmentType) {
// "An INVALID_OPERATION error is generated if COLOR_ATTACHMENTm is used with m greater
// than or equal to MAX_COLOR_ATTACHMENTS."
if (!FramebufferImpl::ValidateColorAttachmentInRange(attachmentType, functionName)) return false;
// "An INVALID_OPERATION error is generated if zero is bound to target." MobileGL keeps
// a real FramebufferObject for framebuffer 0, so a null test can never see this - the
// object is always there, and framebuffer 0 has to be recognised by identity instead,
// the same comparison DrawBuffers_State makes. Without this an attach onto the default
// framebuffer silently REPLACED its colour attachment, permanently desynchronising it
// from what the swapchain keeps publishing.
const auto& defaultFramebufferInfo = FramebufferImpl::pDefaultFramebufferInfo;
if (!framebufferObject ||
(defaultFramebufferInfo && framebufferObject == defaultFramebufferInfo->defaultFBO)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"No framebuffer object is bound to the target; the default framebuffer's attachments "
"cannot be named."));
return false;
}
return true;
}
// The other half of 9.2.8: "level must be greater than or equal to zero", and for a
// texture with immutable storage it "must be smaller than the number of levels the texture
// has". Split from the attachment-point half because the caller only has a texture object
// once the detach (texture == 0) case is behind it.
Bool ValidateFramebufferTextureLevel(const char* functionName,
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
GLint level) {
if (level < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Texture level must be non-negative."));
return false;
}
if (!textureObject || !textureObject->IsImmutable()) {
// A mutable texture has no level bound here: a level it has not specified yet is
// not an error, it just leaves the framebuffer incomplete.
return true;
}
// GetAddressableLevelCount(), NOT GetImmutableLevels(): for a VIEW the latter is
// deliberately the ORIGINAL texture's count (GL 4.6 core 8.18 defines
// TEXTURE_IMMUTABLE_LEVELS on a view that way), which is far too large a bound - a
// two-level view onto a ten-level texture would accept level 5 and attach an image
// nothing can draw into.
const Uint levelBound = textureObject->GetAddressableLevelCount();
if (static_cast<Uint>(level) >= levelBound) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
std::format("Texture level {} is beyond the {} level(s) this texture has.", level,
levelBound)));
return false;
}
return true;
}
void AttachFramebufferTextureWithUploadTarget(const char* functionName, GLenum target, GLenum attachment,
GLuint texture, GLint level,
TextureUploadTarget textureUploadTarget, Bool layered = false) {
@@ -481,10 +551,24 @@ namespace MobileGL::MG_Impl::GLImpl {
}
if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) {
// `layered` has to travel with the split. GL_DEPTH_STENCIL_ATTACHMENT is only a
// shorthand for attaching the same image to both halves (GL 4.6 core 9.2.6), so
// whether glFramebufferTexture made it LAYERED is a property of the call, not of
// which half is being recorded - and dropping it here (the parameter defaults to
// false) recorded a non-layered depth/stencil attachment beside a layered colour
// one for every layered target. That is an inconsistent framebuffer by 9.4.1's
// own rule, and downstream it means the depth/stencil attachment covers layer 0
// alone: DirectVulkan built its view with layerCount 1 under a framebuffer
// declaring N layers (VUID-VkFramebufferCreateInfo-flags-04535), and DirectGLES
// attached one layer of it beside a layered colour target, which the driver
// answers with GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS - every draw silently
// produced nothing. This is the shape
// texture_cube_map_array.stencil_attachments_*_layered and
// geometry_shader.layered_framebuffer.stencil_support are built on.
AttachFramebufferTextureWithUploadTarget(functionName, target, GL_DEPTH_ATTACHMENT, texture, level,
textureUploadTarget);
textureUploadTarget, layered);
AttachFramebufferTextureWithUploadTarget(functionName, target, GL_STENCIL_ATTACHMENT, texture, level,
textureUploadTarget);
textureUploadTarget, layered);
return;
}
@@ -496,13 +580,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
auto& framebufferObject = bindingSlot.GetBoundObject();
if (!framebufferObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Framebuffer target is bound to no framebuffer object."));
return;
}
if (!ValidateFramebufferTextureAttachmentPoint(functionName, framebufferObject, attachmentType)) return;
if (texture == 0) {
framebufferObject->Detach(attachmentType);
@@ -517,6 +595,7 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture object {} is not valid.", texture)));
return;
}
if (!ValidateFramebufferTextureLevel(functionName, textureObject, level)) return;
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
if (expectedTextureTarget == TextureTarget::Unknown ||
@@ -617,21 +696,39 @@ namespace MobileGL::MG_Impl::GLImpl {
if (MG_Backend::pActiveBackendObject == nullptr) {
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
// (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
// The multisample TEXTURE path resolves the limit per format the same way
// (GL_Texture.cpp, GetMaxSupportedTextureSamples), and both now enforce exactly what their
// pname advertises. The integer ceiling used to be floored at GL_MAX_SAMPLES so that the
// frontend would accept a count it had advertised globally - but on Adreno and Mali the
// integer path is genuinely one sample, and accepting four only moved the failure from an
// honest INVALID_OPERATION here to a silently under-allocated renderbuffer.
// The head of the per-format renderbuffer sample list the backend probed, or 0 when nothing
// was probed for it. Same shape as GetProbedMaxTextureSamples in GL_Texture.cpp, and reads
// the same cache glGetInternalformativ(GL_RENDERBUFFER, ..., GL_SAMPLES) answers from.
static Int GetProbedMaxRenderbufferSamples(TextureInternalFormat format) {
if (MG_Backend::pActiveBackendObject == nullptr) {
return 0;
}
const SizeT targetIndex = MG_Backend::GetRenderbufferFormatCapabilityTargetIndex();
const SizeT formatIndex = static_cast<SizeT>(format);
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();
}
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own, lower
// one (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
// (GL_Texture.cpp, GetMaxTextureSamplesForFormat); renderbuffers only ever compared
// against GL_MAX_SAMPLES, so on a driver where the two differ - Adreno reports
// GL_MAX_SAMPLES 4 and GL_MAX_INTEGER_SAMPLES 1 - an integer renderbuffer accepted a
// sample count the format cannot deliver, and said GL_NO_ERROR about it.
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::max();
}
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format);
GLenum normalizedFormat = GL_RGBA;
@@ -642,10 +739,24 @@ namespace MobileGL::MG_Impl::GLImpl {
&normalizedType);
const Bool isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
// The per-format probe first, for the same reason the texture path takes it first: GL 4.6
// core 9.2.4 words the error as "samples is greater than the maximum number of samples
// supported for internalformat (see GetInternalformativ)", and
// glGetInternalformativ(GL_RENDERBUFFER, ..., GL_SAMPLES) is answered from exactly this
// list. It was never consulted here - the TODO that deferred it was written before the
// query was backed and had gone stale - so a format whose multisample probes fail inside
// a category that allows four was accepted at four, quietly allocated at one by
// ClampSamplesToBackendSupport, and then reported as four by
// glGetRenderbufferParameteriv(GL_RENDERBUFFER_SAMPLES).
const Int probedMaxSamples = GetProbedMaxRenderbufferSamples(format);
if (probedMaxSamples > 0) {
return probedMaxSamples;
}
if (!isIntegerFormat) {
return GetMaxRenderbufferSamples_State();
}
return std::max(dynamicParameters.MaxIntegerSamples, 1);
// Exactly what glGetIntegerv(GL_MAX_INTEGER_SAMPLES) reports.
return GetAdvertisedIntegerMaxSamples();
}
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
@@ -677,8 +788,10 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
// TODO: Resolve the remaining per-internalformat renderbuffer sample limits once
// glGetInternalformativ is backed; integer formats are handled below.
// Per-internalformat, from the probe list glGetInternalformativ answers with, falling back
// to the format's category pname where nothing was probed. (This carried a TODO deferring
// the per-format resolution "once glGetInternalformativ is backed"; it has been backed for
// both renderbuffers and multisample textures since, so the deferral was collected.)
const Int maxSamples = GetMaxRenderbufferSamplesForFormat_State(format);
if (samples > maxSamples) {
// GL 4.6 core 9.2.4 makes asking for more samples than the format supports
@@ -1043,13 +1156,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
auto& framebufferObject = bindingSlot.GetBoundObject();
if (!framebufferObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Framebuffer target is bound to no framebuffer object."));
return;
}
if (!ValidateFramebufferTextureAttachmentPoint(functionName, framebufferObject, attachmentType)) return;
if (texture == 0) {
framebufferObject->Detach(attachmentType);
@@ -1064,6 +1171,7 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture object {} is not valid.", texture)));
return;
}
if (!ValidateFramebufferTextureLevel(functionName, textureObject, level)) return;
if (layer < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
@@ -1186,6 +1294,13 @@ namespace MobileGL::MG_Impl::GLImpl {
"Framebuffer target is bound to no framebuffer object."));
return;
}
// glFramebufferTexture2D is by far the most-used member of the family and the only one
// that inlines its own logic instead of going through the shared helper, so the 9.2.8
// conditions have to be asked here explicitly.
if (!ValidateFramebufferTextureAttachmentPoint("FramebufferTexture2D_State", framebufferObject,
attachmentType)) {
return;
}
if (texture == 0) {
framebufferObject->Detach(attachmentType);
@@ -1200,6 +1315,7 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture object {} is not valid.", texture)));
return;
}
if (!ValidateFramebufferTextureLevel("FramebufferTexture2D_State", textureObject, level)) return;
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
if (expectedTextureTarget == TextureTarget::Unknown ||
@@ -1236,6 +1352,12 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
// The name's validity is an INVALID_VALUE condition (GL 4.6 core 9.2.8), and it has to be
// asked BEFORE the object is resolved: reporting the miss as the INVALID_OPERATION below
// pre-empted the shared helper's ValidateTextureName and answered the wrong error code for
// every texture name that was never generated.
if (!TextureImpl::ValidateTextureName(texture, true)) return;
auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
if (!textureObject) {
MG_State::pGLContext->RecordError(
@@ -1286,13 +1408,10 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture object {} is not valid.", texture)));
return;
}
if (level < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "NamedFramebufferTexture_State",
"Texture level must be non-negative."));
return;
}
// The whole level condition, not just its negative half: glNamedFramebufferTexture and
// glFramebufferTexture are equivalent in 9.2.8, so an out-of-range immutable level has to
// be rejected on both or a CTS case gets two answers for one rule.
if (!ValidateFramebufferTextureLevel("NamedFramebufferTexture_State", textureObject, level)) return;
TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown;
Bool layered = false;
@@ -2608,18 +2727,26 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void ClearBufferfi_Backend(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil);
}
void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value);
}
void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* value) {
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value);
}
void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* value) {
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value);
}
@@ -3148,15 +3275,55 @@ namespace MobileGL::MG_Impl::GLImpl {
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,
GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask,
GLenum filter) {
if (!ValidateBlitMaskAndFilter(__func__, mask, filter)) return;
BlitNamedFramebuffer_State(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
dstY1, mask, filter);
}
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter) {
if (!ValidateBlitMaskAndFilter(__func__, mask, filter)) return;
BlitFramebuffer_Backend(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
}
File diff suppressed because it is too large Load Diff
@@ -24,4 +24,25 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
GLenum GetError();
GLenum GetGraphicsResetStatus();
// The GL_MAX_SAMPLES value MobileGL advertises, i.e. the driver's value floored to the GL
// core minimum of 4. This is the RENDERBUFFER ceiling; the three per-category texture
// ceilings below have a minimum of one and are reported as probed.
GLint GetAdvertisedMaxSamples();
// Exactly what GL_MAX_COLOR_TEXTURE_SAMPLES / GL_MAX_DEPTH_TEXTURE_SAMPLES /
// GL_MAX_INTEGER_SAMPLES report: the probed backend limit floored at the GL 4.6 core minimum
// of ONE (table 23.53). Exported so the frontend's storage validation enforces exactly what
// the query promised - it used to floor both at 4 and then let the backend quietly
// under-allocate whatever the driver could not actually provide.
GLint GetAdvertisedColorTextureMaxSamples();
GLint GetAdvertisedDepthTextureMaxSamples();
GLint GetAdvertisedIntegerMaxSamples();
// What glGetIntegerv(GL_SAMPLES) answers for the CURRENT draw framebuffer: the largest sample
// count over its attachments, and 0 for a single-sample or default framebuffer (GL 4.6 core
// 9.2.3 / 22.2 - GL_SAMPLE_BUFFERS is 1 exactly when this is non-zero).
//
// Shared rather than duplicated because two callers need the identical number and disagreeing
// would be a silent bug: the query itself, and the draw path's write of the reserved
// gl_NumSamples stand-in - a shader comparing gl_NumSamples against glGetIntegerv(GL_SAMPLES)
// is exactly what the sample_variables CTS does.
GLint ResolveDrawFramebufferSampleCount();
} // namespace MobileGL::MG_Impl::GLImpl
File diff suppressed because it is too large Load Diff
@@ -13,6 +13,12 @@ namespace MobileGL::MG_Impl::GLImpl {
void AttachShader(GLuint program, GLuint shader);
void BindAttribLocation(GLuint program, GLuint index, const GLchar* name);
void CompileShader(GLuint shader);
// GL_ARB_gl_spirv, core since 4.6. The pair is a two-step operation: glShaderBinary attaches
// the module to one or more shader objects, glSpecializeShader names its entry point and
// supplies its specialization constants and is what actually compiles them.
void ShaderBinary(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length);
void SpecializeShader(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
const GLuint* pConstantIndex, const GLuint* pConstantValue);
GLuint CreateProgram(void);
GLuint CreateShader(GLenum type);
void DeleteProgram(GLuint program);
@@ -140,6 +146,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
GLint GetProgramResourceLocation(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 Uniform1d(GLint location, GLdouble v0);
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value);
@@ -192,6 +192,15 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Program {} has not been linked successfully.", program));
return;
}
// GL 4.6 core 7.4: "INVALID_OPERATION is generated if program was not linked with its
// PROGRAM_SEPARABLE status set". The LATCHED flag is the one that decides - a program
// whose live flag was cleared after a separable link is still a legal stage, and a
// program whose live flag was set after a non-separable link is not.
if (!programObject->GetLinkedSeparable()) {
RecordPipelineError(ErrorCode::InvalidOperation, __func__,
std::format("Program {} was not linked as a separable program.", program));
return;
}
}
const GLbitfield selected = stages == GL_ALL_SHADER_BITS ? kAllStageBits : stages;
@@ -19,7 +19,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
// "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL
// 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.
constexpr const char* kAtomicCounterBlockPrefix = "gl_AtomicCounterBlock";
constexpr const char* kAtomicCounterBlockPrefix = MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX;
enum class BlockKind {
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
// that to uniforms and buffer variables (EShReflectionBasicArraySuffix), but never to
// stage inputs/outputs, so those get it here.
String WithArraySuffix(const String& name, const glslang::TType* type) {
if (type == nullptr || !type->isArray() || EndsWithZeroSubscript(name)) return name;
String WithArraySuffix(const String& name, const ProgramObject::TypeFacts& type) {
if (!type.isArray || EndsWithZeroSubscript(name)) return name;
return name + "[0]";
}
// 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.
GLint ArraySizeOf(const glslang::TType* type, GLint reflectedSize) {
if (type != nullptr && type->isArray()) {
if (!type->isSizedArray()) return 0;
return type->getOuterArraySize();
}
return reflectedSize < 1 ? 1 : reflectedSize;
// `record.arraySize` is already the sized-array/reflected-size resolution; the only
// extra rule here is GL's 0 for a runtime-sized array.
GLint ArraySizeOf(const ProgramObject::ResourceReflection& record) {
if (record.type.isArray && !record.type.isSizedArray) return 0;
return record.arraySize;
}
// 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);
}
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) {
return BlockKind::GlobalUbo;
}
if (IsAtomicCounterBlockName(block.name)) return BlockKind::AtomicCounter;
const glslang::TType* type = block.getType();
if (type != nullptr && type->getQualifier().storage == glslang::EvqBuffer) return BlockKind::Storage;
if (block.type.isBuffer) return BlockKind::Storage;
return BlockKind::Uniform;
}
// std140/std430 column stride, the same vec4-rounded rule ProgramObject applies to
// uniform matrices. 0 for a non-matrix.
GLint MatrixStrideOf(const glslang::TType* type) {
if (type == nullptr || !type->isMatrix()) return 0;
const bool rowMajor = type->getQualifier().layoutMatrix == glslang::ElmRowMajor;
const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows();
GLint MatrixStrideOf(const ProgramObject::TypeFacts& type) {
if (!type.isMatrix) return 0;
const bool rowMajor = type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor);
const int strideVectorComponents = rowMajor ? type.matrixCols : type.matrixRows;
constexpr int scalarSize = 4;
const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize
: (strideVectorComponents == 2) ? 2 * scalarSize
@@ -197,9 +195,9 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
return (vectorAlignment + 15) & ~15;
}
GLint IsRowMajorOf(const glslang::TType* type) {
if (type == nullptr || !type->isMatrix()) return 0;
return type->getQualifier().layoutMatrix == glslang::ElmRowMajor ? 1 : 0;
GLint IsRowMajorOf(const ProgramObject::TypeFacts& type) {
if (!type.isMatrix) return 0;
return type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor) ? 1 : 0;
}
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
// 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.
Vector<Uint32> BuildBlockStagesFromMembers(const glslang::TProgram& reflection, Int blockCount) {
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
Vector<Uint32> BuildBlockStagesFromMembers(const ProgramObject::LinkArtifacts& reflection,
Int blockCount) {
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) {
const auto& uniform = mutableReflection.getUniform(index);
const auto& uniform = reflection.uniformReflection[index];
const Int owner = uniform.index;
if (owner < 0 || owner >= blockCount) continue;
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
// (deliberately over-approximating) block mask gets right. Storage and atomic-counter
// 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) {
String arrayBase;
Uint element = 0;
@@ -264,15 +262,15 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
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) {
const Int blockCount = const_cast<glslang::TProgram&>(reflection).getNumUniformBlocks();
const Int blockCount = static_cast<Int>(reflection.blockReflection.size());
blockKind.assign(blockCount, BlockKind::Uniform);
blockInterfaceIndex.assign(blockCount, -1);
const Vector<Uint32> stagesFromMembers = BuildBlockStagesFromMembers(reflection, blockCount);
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);
blockKind[tIndex] = kind;
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 -
// exactly the same rule GL_UNIFORM_BLOCK follows through
// GetUniformBlockBinding below).
const GLint declared = block.getBinding();
const GLint declared = block.binding;
resource.bufferBinding = declared < 0 ? 0 : declared + BlockArrayElement(block.name);
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
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
// glGetActiveUniformBlockiv already use, so an index handed out here is usable
// 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) {
// 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.name = program.GetUniformBlockName(glIndex);
resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(glIndex));
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex));
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex));
resource.name = program.GetUniformBlockName(static_cast<Uint>(blockIndex));
resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(static_cast<Uint>(blockIndex)));
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(static_cast<Uint>(blockIndex)));
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(blockIndex));
if (tIndex >= 0 && tIndex < blockCount) {
resource.stages = UniformBlockStages(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex),
resource.stages = UniformBlockStages(reflection.blockReflection[tIndex],
stagesFromMembers, tIndex);
}
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<Int>& blockInterfaceIndex) {
const Uint uniformCount = program.GetUniformCount();
for (Uint glIndex = 0; glIndex < uniformCount; ++glIndex) {
const Int tIndex = program.TProgramUniformIndex(glIndex);
const auto& refl = const_cast<glslang::TProgram&>(reflection).getUniform(tIndex);
const glslang::TType* type = refl.getType();
// Walks the TPROGRAM uniform space, not the GL one. A buffer variable is not a GL
// uniform (GL 4.6 core 7.3.1) and DoReflection therefore keeps it out of the GL
// active-uniform index space - but GL_BUFFER_VARIABLE still has to enumerate it, and
// this is the only place that does. GL uniforms keep their GL index as their
// 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 BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size()))
? blockKind[owner]
: 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.name = refl.name;
resource.type = static_cast<GLenum>(refl.glDefineType);
resource.arraySize = ArraySizeOf(type, refl.size);
resource.arraySize = ArraySizeOf(refl);
resource.stages = static_cast<Uint32>(refl.stages);
if (kind == BlockKind::Storage) {
@@ -366,11 +379,12 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
resource.atomicCounterBufferIndex = blockInterfaceIndex[owner];
resource.location = -1;
} else {
resource.blockIndex = program.GetActiveUniformBlockIndex(glIndex);
resource.offset = program.GetActiveUniformOffset(glIndex);
resource.arrayStride = program.GetActiveUniformArrayStride(glIndex);
resource.matrixStride = program.GetActiveUniformMatrixStride(glIndex);
resource.isRowMajor = program.GetActiveUniformIsRowMajor(glIndex);
const Uint glUniformIndex = static_cast<Uint>(glIndex);
resource.blockIndex = program.GetActiveUniformBlockIndex(glUniformIndex);
resource.offset = program.GetActiveUniformOffset(glUniformIndex);
resource.arrayStride = program.GetActiveUniformArrayStride(glUniformIndex);
resource.matrixStride = program.GetActiveUniformMatrixStride(glUniformIndex);
resource.isRowMajor = program.GetActiveUniformIsRowMajor(glUniformIndex);
// 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
// so glUniform* can address block members through the global UBO).
@@ -389,12 +403,16 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
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].
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) {
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
// gl_PointSize and gl_ClipDistance through the block-unwrapping reflection, and they
// are not part of its output interface.
Bool IsHiddenBlockMember(const glslang::TType* type) {
return type != nullptr && type->getBasicType() == glslang::EbtVoid;
}
Bool IsHiddenBlockMember(const ProgramObject::TypeFacts& type) { return type.isVoid; }
void BuildStageIO(ProgramObject& program, const glslang::TProgram& reflection, Model& model) {
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
const Int inputCount = mutableReflection.getNumPipeInputs();
void BuildStageIO(ProgramObject& program, const ProgramObject::LinkArtifacts& reflection, Model& model) {
const Int inputCount = static_cast<Int>(reflection.pipeInputReflection.size());
for (Int index = 0; index < inputCount; ++index) {
const auto& refl = mutableReflection.getPipeInput(index);
const glslang::TType* type = refl.getType();
const auto& refl = reflection.pipeInputReflection[index];
const auto& type = refl.type;
if (IsHiddenBlockMember(type)) continue;
Resource resource;
// 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);
resource.name = WithArraySuffix(glName, type);
resource.type = static_cast<GLenum>(refl.glDefineType);
resource.arraySize = ArraySizeOf(type, refl.size);
resource.arraySize = ArraySizeOf(refl);
resource.location = program.GetAttributeLocation(refl.name);
if (resource.location < 0) resource.location = MappedLocation(static_cast<Int>(refl.layoutLocation()));
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0;
if (resource.location < 0) resource.location = MappedLocation(refl.location);
resource.isPerPatch = type.isPatch ? 1 : 0;
resource.stages = static_cast<Uint32>(refl.stages);
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
// index of 0 where GL requires -1
// (KHR-GL43.program_interface_query.separate-programs-tess-control).
const Bool lastStageIsFragment = mutableReflection.getIntermediate(EShLangFragment) != nullptr;
const Int outputCount = mutableReflection.getNumPipeOutputs();
const Bool lastStageIsFragment = reflection.lastStageIsFragment;
const Int outputCount = static_cast<Int>(reflection.pipeOutputReflection.size());
for (Int index = 0; index < outputCount; ++index) {
const auto& refl = mutableReflection.getPipeOutput(index);
const glslang::TType* type = refl.getType();
const auto& refl = reflection.pipeOutputReflection[index];
const auto& type = refl.type;
if (IsHiddenBlockMember(type)) continue;
Resource resource;
resource.name = WithArraySuffix(refl.name, type);
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()));
if (resource.location < 0 || !lastStageIsFragment) {
// 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());
// glBindFragDataLocationIndexed wins; otherwise the shader's
// layout(index = N), which the frag-data maps never saw.
if (resource.locationIndex == 0 && type != nullptr && type->getQualifier().hasIndex()) {
resource.locationIndex = static_cast<GLint>(type->getQualifier().layoutIndex);
if (resource.locationIndex == 0 && type.hasIndex) {
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);
model.programOutputs.push_back(Move(resource));
}
@@ -511,15 +525,14 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
Model BuildModel(ProgramObject& program) {
Model model;
if (!program.GetLinkStatus()) return model;
const glslang::TProgram* reflection = program.GetReflection();
if (reflection == nullptr) return model;
const ProgramObject::LinkArtifacts& reflection = program.GetLinkReflection();
model.valid = true;
Vector<BlockKind> blockKind;
Vector<Int> blockInterfaceIndex;
BuildBlocks(program, *reflection, model, blockKind, blockInterfaceIndex);
BuildUniformsAndBufferVariables(program, *reflection, model, blockKind, blockInterfaceIndex);
BuildStageIO(program, *reflection, model);
BuildBlocks(program, reflection, model, blockKind, blockInterfaceIndex);
BuildUniformsAndBufferVariables(program, reflection, model, blockKind, blockInterfaceIndex);
BuildStageIO(program, reflection, model);
BuildXfb(program, model);
return model;
}
+293 -23
View File
@@ -31,8 +31,15 @@ namespace MobileGL::MG_Impl::GLImpl {
Bool ended = false;
Bool resultCached = false;
Uint64 cachedResult = 0;
// Transform feedback primitive counter at BeginQuery time.
// The transform feedback primitive counter matching this query's target, at
// BeginQuery time.
Uint64 counterSnapshot = 0;
// Capture-draw counters at BeginQuery time: how many capture draws the CPU
// accounting had reproduced exactly, and how many of those it could not (a
// geometry stage amplifies). Their deltas decide whether the CPU result may
// stand in for the backend's.
Uint64 accountedCaptureDrawSnapshot = 0;
Uint64 geometryCaptureDrawSnapshot = 0;
};
// Query calls may arrive from any thread (launchers migrate the context
@@ -52,6 +59,62 @@ namespace MobileGL::MG_Impl::GLImpl {
GLuint g_activePrimitivesGeneratedQueryId = 0;
// Id of the query active on GL_SAMPLES_PASSED (0 = none).
GLuint g_activeSamplesPassedQueryId = 0;
// Ids of the queries active on the GL_ARB_pipeline_statistics_query targets, one slot per
// target (0 = none). A map rather than a field per target: the eleven behave identically
// and none of them has any state beyond "which object is counting".
UnorderedMap<GLenum, GLuint> g_activePipelineStatisticsQueryIds;
// Whether MobileGL puts GL_ARB_tessellation_shader in its extension string. Read from the
// ADVERTISED list rather than from a capability bit for the same reason
// BackendSupportsTextureViews does (GL_Texture.cpp): it makes "MobileGL claims tessellation
// support" and "the tessellation-conditional API surface is open" the same fact by
// construction, so the day a backend starts advertising the string the surface below opens
// with it and no second edit is owed.
Bool AdvertisesTessellationShaderExtension() {
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) return false;
const auto& extensions = activeBackendObject->GetRendererInfo().RendererGLInfo.Extensions;
return std::find(extensions.begin(), extensions.end(), E_GL_ARB_tessellation_shader) != extensions.end();
}
// The eleven pipeline-statistics counters (GL 4.6 core table 4.3 / ARB_pipeline_statistics_query).
// A 4.6 core context ACCEPTS the nine unconditional ones at glBeginQuery - there is no query
// by which an application could learn otherwise before calling. MobileGL instruments none of
// them, and says so the way GL 4.6 core 4.2.1 provides for: GL_QUERY_COUNTER_BITS answers
// zero for these targets, which is the spec's own signal that the counter is unsupported and
// its results indeterminate. That is an honest zero, not an advertised capability - the
// alternative, GL_INVALID_ENUM on a core entry point, is both non-conformant AND less
// informative.
//
// The two TESSELLATION targets are the exception, because ARB_pipeline_statistics_query
// makes them conditional on tessellation support rather than unconditional, and the only
// thing an application (or the conformance suite) can read to decide whether an
// implementation has it is the GL_ARB_tessellation_shader string. MobileGL does not emit it
// today, so these two answer GL_INVALID_ENUM: an API surface that accepts a
// tessellation-conditional token while withholding the string that announces the condition
// is self-contradictory, and it is the contradiction the suite catches
// (KHR-GL46.pipeline_statistics_query_tests_ARB.api_coverage_unsupported_calls, whose
// support probe is gl4cPipelineStatisticsQueryTests.cpp:1166-1176). The gate is the
// advertisement itself, not a hardcoded "no", so this is one switch and not two.
Bool IsPipelineStatisticsQueryTarget(GLenum target) {
switch (target) {
case GL_VERTICES_SUBMITTED:
case GL_PRIMITIVES_SUBMITTED:
case GL_VERTEX_SHADER_INVOCATIONS:
case GL_GEOMETRY_SHADER_INVOCATIONS:
case GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED:
case GL_FRAGMENT_SHADER_INVOCATIONS:
case GL_COMPUTE_SHADER_INVOCATIONS:
case GL_CLIPPING_INPUT_PRIMITIVES:
case GL_CLIPPING_OUTPUT_PRIMITIVES:
return true;
case GL_TESS_CONTROL_SHADER_PATCHES:
case GL_TESS_EVALUATION_SHADER_INVOCATIONS:
return AdvertisesTessellationShaderExtension();
default:
return false;
}
}
Bool TimerQueryDisabled() {
return MG_Config::Features.DisableTimerQuery;
@@ -122,6 +185,46 @@ namespace MobileGL::MG_Impl::GLImpl {
g_activeTimeElapsedQueryId = 0;
}
// The CPU accounting counter a transform feedback query target reads: what the capture
// buffers took for GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, and everything the capture
// stage assembled - a paused span included - for GL_PRIMITIVES_GENERATED. One counter
// for both targets would report the clamped written count as the generated one.
Uint64 TransformFeedbackCounterForTarget(GLenum target) {
return target == GL_PRIMITIVES_GENERATED
? MG_State::pGLContext->GetTransformFeedbackGeneratedCounter()
: MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
}
// The span's CPU accounting delta. Saturating: a snapshot left above its counter (a
// context switch between Begin and End, a counter that never moved) would otherwise
// wrap to 2^64-1, which GetQueryObjectuiv hands the app as 4294967295.
Uint64 TransformFeedbackCpuResult(const QueryObject* queryObject) {
const Uint64 counter = TransformFeedbackCounterForTarget(queryObject->target);
return counter > queryObject->counterSnapshot ? counter - queryObject->counterSnapshot : 0;
}
// Whether this ended span's result should come from the CPU accounting rather than from
// the backend query it also ran. Three conditions, all necessary:
// * the backend asked for it (DirectGLES, whose ES driver counter is the unreliable
// one; DirectVulkan never sets the bit and so is untouched by any of this);
// * the target is PRIMITIVES_WRITTEN. GL_PRIMITIVES_GENERATED counts primitives
// whether or not a capture is active, and the accounting only ever sees capture
// draws, so the backend's counter is the more complete answer there;
// * the span was fully accounted: at least one capture draw reached the accounting
// (the instanced, indirect and multi-draw entry points do not call it at all, so a
// span made of those is invisible to it) and none of them amplified through a
// geometry stage, which the CPU cannot model.
Bool PrefersCpuTransformFeedbackResult(const QueryObject* queryObject) {
if (!MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting) return false;
if (queryObject->target != GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN) return false;
if (MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws() !=
queryObject->geometryCaptureDrawSnapshot) {
return false;
}
return MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws() !=
queryObject->accountedCaptureDrawSnapshot;
}
// Shared GetQueryObject* implementation. Returns false when an error
// was recorded and no value should be written back. `outValueProduced`, when given,
// additionally distinguishes "succeeded with a value" from "succeeded but the result is not
@@ -323,6 +426,9 @@ namespace MobileGL::MG_Impl::GLImpl {
}
queryObject->active = false;
g_activeSamplesPassedQueryId = 0;
} else if (IsPipelineStatisticsQueryTarget(queryObject->target)) {
queryObject->active = false;
g_activePipelineStatisticsQueryIds[queryObject->target] = 0;
} else if (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ||
queryObject->target == GL_PRIMITIVES_GENERATED) {
queryObject->active = false;
@@ -363,7 +469,9 @@ namespace MobileGL::MG_Impl::GLImpl {
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
!isPipelineStatisticsQuery) {
// GL_TIMESTAMP is not a valid BeginQuery target; the occlusion targets
// need backend support.
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
@@ -379,10 +487,12 @@ namespace MobileGL::MG_Impl::GLImpl {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
return;
}
GLuint& activeQueryId = isTransformFeedbackQuery
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId)
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
GLuint& activeQueryId = isPipelineStatisticsQuery
? g_activePipelineStatisticsQueryIds[target]
: (isTransformFeedbackQuery
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId)
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId));
if (activeQueryId != 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
"A query is already active on this target.");
@@ -401,13 +511,21 @@ namespace MobileGL::MG_Impl::GLImpl {
ResetQueryObjectLocked(queryObject); // discard any previous result
queryObject->target = target;
queryObject->active = true;
if (isTransformFeedbackQuery) {
if (isPipelineStatisticsQuery) {
// Nothing to start: the counter is uninstrumented and GL_QUERY_COUNTER_BITS says so.
// The object still becomes a real, target-latched query so every other rule about it
// (re-use with another target, double-begin, EndQuery pairing) keeps holding.
} else if (isTransformFeedbackQuery) {
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
// the CPU accounting delta stays as the fallback when the backend lacks them.
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
queryObject->backendHandle =
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
queryObject->counterSnapshot = MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target);
queryObject->accountedCaptureDrawSnapshot =
MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws();
queryObject->geometryCaptureDrawSnapshot =
MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws();
} else if (isOcclusionQuery) {
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
} else {
@@ -425,15 +543,19 @@ namespace MobileGL::MG_Impl::GLImpl {
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
!isPipelineStatisticsQuery) {
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
return;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
GLuint& activeQueryId = isTransformFeedbackQuery
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId)
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
GLuint& activeQueryId = isPipelineStatisticsQuery
? g_activePipelineStatisticsQueryIds[target]
: (isTransformFeedbackQuery
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId)
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId));
if (activeQueryId == 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target.");
return;
@@ -443,17 +565,37 @@ namespace MobileGL::MG_Impl::GLImpl {
activeQueryId = 0; // should not happen; keep state consistent
return;
}
if (isPipelineStatisticsQuery) {
// The result is a definite zero rather than an unread backend handle, so a later
// GetQueryObject* answers immediately and never waits on something that was never
// started. GL_QUERY_COUNTER_BITS = 0 is what marks that zero indeterminate.
queryObject->cachedResult = 0;
queryObject->resultCached = true;
queryObject->active = false;
queryObject->ended = true;
activeQueryId = 0;
return;
}
if (isTransformFeedbackQuery) {
if (queryObject->backendHandle) {
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
endXfbPrimitivesQuery(queryObject->backendHandle);
}
// Result comes from the GPU query at read time.
} else {
queryObject->cachedResult =
MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter() - queryObject->counterSnapshot;
}
// A backend query that is not going to be read is released here, not left to be
// collected later: the span is over, the driver object has nothing left to say.
// Ending it first is what makes that legal.
if (!queryObject->backendHandle || PrefersCpuTransformFeedbackResult(queryObject)) {
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
}
queryObject->cachedResult = TransformFeedbackCpuResult(queryObject);
queryObject->resultCached = true;
}
// Otherwise the result comes from the GPU query at read time.
queryObject->active = false;
queryObject->ended = true;
activeQueryId = 0;
@@ -505,6 +647,75 @@ namespace MobileGL::MG_Impl::GLImpl {
queryObject->ended = true;
}
void BeginConditionalRender(GLuint id, GLenum mode) {
// GL 4.6 core 10.9's eight modes. The _INVERTED half flips the sense of the predicate;
// the BY_REGION half only narrows WHERE an implementation is permitted to discard, so
// treating it as its whole-framebuffer sibling is what an implementation without region
// granularity does. The _NO_WAIT half is a permission to render rather than stall, not an
// obligation - see the resolve below.
Bool inverted = false;
switch (mode) {
case GL_QUERY_WAIT:
case GL_QUERY_NO_WAIT:
case GL_QUERY_BY_REGION_WAIT:
case GL_QUERY_BY_REGION_NO_WAIT:
inverted = false;
break;
case GL_QUERY_WAIT_INVERTED:
case GL_QUERY_NO_WAIT_INVERTED:
case GL_QUERY_BY_REGION_WAIT_INVERTED:
case GL_QUERY_BY_REGION_NO_WAIT_INVERTED:
inverted = true;
break;
default:
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "mode is not a conditional render mode.");
return;
}
if (MG_State::pGLContext->IsConditionalRenderActive()) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is already active.");
return;
}
{
const std::lock_guard<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) {
if (!params) {
return;
@@ -528,7 +739,12 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = static_cast<GLint>(g_activePrimitivesGeneratedQueryId);
break;
default:
*params = 0;
if (IsPipelineStatisticsQueryTarget(target)) {
const auto it = g_activePipelineStatisticsQueryIds.find(target);
*params = it != g_activePipelineStatisticsQueryIds.end() ? static_cast<GLint>(it->second) : 0;
} else {
*params = 0;
}
break;
}
return;
@@ -539,6 +755,14 @@ namespace MobileGL::MG_Impl::GLImpl {
// entry points / timestamp valid bits at call time, not at table
// init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always
// wins.
if (IsPipelineStatisticsQueryTarget(target)) {
// Zero: GL 4.6 core 4.2.1's way of saying the counter is not implemented and its
// results are indeterminate. The conformance suite reads exactly this and skips
// the functional half of each such target, which is the outcome an uninstrumented
// counter should produce.
*params = 0;
return;
}
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
@@ -612,14 +836,24 @@ namespace MobileGL::MG_Impl::GLImpl {
}
namespace {
Bool IsPerVertexStreamQueryTarget(GLenum target) {
return target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
}
// The indexed query entry points differ from the plain ones only in the vertex
// stream they address (GL 4.6 core 4.2.1): index must be below GL_MAX_VERTEX_STREAMS
// for the two transform feedback targets and zero for every other target. With a
// single vertex stream both bounds are 1, so a valid call is always index 0 and
// forwards to the unindexed implementation.
// for the two transform feedback targets and zero for every other target. MobileGL
// implements ONE vertex stream, so both bounds are 1 and a valid call is always index 0 -
// which is what makes the three forwards below equivalent to the unindexed entry points.
//
// THAT EQUIVALENCE IS THE WHOLE JUSTIFICATION, and it is read out of the getter rather
// than assumed: the moment GL_MAX_VERTEX_STREAMS answers more than one, index 1..3 starts
// reaching EndQueryIndexed and GetQueryIndexediv, which resolve the active query from
// per-TARGET globals and would end - or report - a query begun on a different stream.
// Raising that limit therefore means giving each active query a stream index and
// comparing it here, not just changing the number.
Bool ValidateQueryStreamIndex(const char* function, GLenum target, GLuint index) {
const Bool perStreamTarget =
target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
const Bool perStreamTarget = IsPerVertexStreamQueryTarget(target);
GLint maxVertexStreams = 1;
if (perStreamTarget) {
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
@@ -632,6 +866,7 @@ namespace MobileGL::MG_Impl::GLImpl {
: "index must be zero for this query target.");
return false;
}
} // namespace
void BeginQueryIndexed(GLenum target, GLuint index, GLuint id) {
@@ -648,4 +883,39 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
GetQueryiv(target, pname, params);
}
void DestroyAllQueryObjects() {
// Detach the registry under the lock, release outside it - same discipline
// (and the same accepted teardown race) as DestroyAllSyncObjects. Without
// this drain, every query the app left undeleted survived full library
// teardown in the process-global registry: the objects and their backend
// wrappers leaked across Destroy/Initialize cycles, stale ids kept
// answering IsQuery == GL_TRUE in the re-initialized library, and a later
// glDeleteQueries could hand the OLD backend's handle to a DIFFERENT
// backend's DeleteBackendQuery, which casts it to the wrong wrapper type.
UnorderedMap<GLuint, QueryObject*> orphans;
{
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
orphans.swap(g_liveQueryObjects);
g_activeTimeElapsedQueryId = 0;
g_activePrimitivesWrittenQueryId = 0;
g_activePrimitivesGeneratedQueryId = 0;
g_activeSamplesPassedQueryId = 0;
}
if (orphans.empty()) {
return;
}
// Backend handles must be released by the backend that created them, so
// this runs while the function table is still populated. Both backends'
// DeleteBackendQuery are generation-guarded, so a handle whose renderer
// or ES context is already gone frees only the wrapper.
const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery;
for (const auto& [_, queryObject] : orphans) {
if (deleteBackendQuery && queryObject->backendHandle) {
deleteBackendQuery(queryObject->backendHandle);
}
delete queryObject;
}
MGLOG_D("DestroyAllQueryObjects: reclaimed %zu query object(s) the app left undeleted", orphans.size());
}
} // namespace MobileGL::MG_Impl::GLImpl
+14
View File
@@ -29,4 +29,18 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
void QueryCounter(GLuint id, GLenum target);
// Conditional rendering (GL 4.6 core 10.9). Implemented here rather than beside the drawing
// entry points because the predicate is a QUERY OBJECT's result, and the object registry -
// with the lock that guards it - lives in this file.
void BeginConditionalRender(GLuint id, GLenum mode);
void EndConditionalRender();
// Destroys every still-registered query object exactly as DeleteQueries would.
// GL requires queries to die with their context; called only from full library
// teardown (DestroyImpl), where no context survives on any thread, so the
// process-global registry can be drained wholesale. Must run while the backend
// function table is still populated: each backend handle has to be released by
// the backend that created it, never by a later re-initialized one (whose
// DeleteBackendQuery would cast the wrapper to the wrong backend's type).
// Same contract as DestroyAllSyncObjects.
void DestroyAllQueryObjects();
} // namespace MobileGL::MG_Impl::GLImpl
@@ -20,28 +20,118 @@ namespace MobileGL::MG_Impl::GLImpl {
return std::clamp(static_cast<Float>(value), 0.0f, 1.0f);
}
static Bool ValidateIndexedBlendCapability(GLenum target, GLuint index, const char* functionName) {
if (target != GL_BLEND) {
// GL 4.6 core 17.3.2 and 22.1 give exactly two indexed capabilities: GL_BLEND, indexed by
// draw buffer, and GL_SCISSOR_TEST, indexed by viewport. They have DIFFERENT bounds
// (MAX_DRAW_BUFFERS vs MAX_VIEWPORTS), so the limit is picked per target rather than shared.
static Bool ValidateIndexedCapability(GLenum target, GLuint index, const char* functionName) {
GLuint limit = 0;
const char* indexName = nullptr;
switch (target) {
case GL_BLEND:
limit = MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS;
indexName = "Buffer";
break;
case GL_SCISSOR_TEST:
limit = RenderStateParameters::MAX_VIEWPORTS;
indexName = "Viewport";
break;
default:
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Only GL_BLEND is supported for indexed capability state."));
"Only GL_BLEND and GL_SCISSOR_TEST are supported for indexed "
"capability state."));
return false;
}
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
if (index >= limit) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"Buffer index " + std::to_string(index) + " is out of range. Max supported is " +
std::to_string(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS - 1) + "."));
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
String(indexName) + " index " + std::to_string(index) +
" is out of range. Max supported is " + std::to_string(limit - 1) +
"."));
return false;
}
return true;
}
// ------------------ ARB_viewport_array parameter validation ------------------
// All three families share the same two shapes, so they share the two checkers. GL 4.6 core
// 13.6.1/17.3.2: an out-of-range index is GL_INVALID_VALUE, and so is a negative width or
// height. `first + count == MAX_VIEWPORTS` is LEGAL - only strictly greater is an error,
// which KHR-GL43.viewport_array.api_errors checks explicitly in both directions.
static Bool ValidateViewportIndex(GLuint index, const char* functionName) {
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Viewport index " + std::to_string(index) +
" is out of range. Max supported is " +
std::to_string(RenderStateParameters::MAX_VIEWPORTS - 1) + "."));
return false;
}
static Bool ValidateViewportRange(GLuint first, GLsizei count, const char* functionName) {
if (count < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "count must not be negative."));
return false;
}
// Widened before adding: first is a GLuint and count a GLsizei, so `first + count` in
// 32 bits can wrap past MAX_VIEWPORTS and let an out-of-range range through.
const Uint64 last = static_cast<Uint64>(first) + static_cast<Uint64>(count);
if (last > RenderStateParameters::MAX_VIEWPORTS) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"first (" + std::to_string(first) + ") + count (" +
std::to_string(count) + ") exceeds GL_MAX_VIEWPORTS (" +
std::to_string(RenderStateParameters::MAX_VIEWPORTS) + ")."));
return false;
}
return true;
}
template <typename T>
static Bool ValidateNonNegativeExtent(T width, T height, const char* functionName) {
if (width >= T(0) && height >= T(0)) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "Width and height must be non-negative."));
return false;
}
// The array forms are all-or-nothing: one bad element rejects the whole call with a SINGLE
// GL_INVALID_VALUE and leaves every rectangle untouched. api_errors relies on both halves -
// it passes a full 16-element array with exactly one negative extent and then asserts the
// error queue holds exactly one entry.
template <typename T>
static Bool ValidateArrayExtents(GLsizei count, const T* v, const char* functionName) {
for (GLsizei i = 0; i < count; ++i) {
if (v[i * 4 + 2] >= T(0) && v[i * 4 + 3] >= T(0)) continue;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Width and height must be non-negative (element " + std::to_string(i) +
")."));
return false;
}
return true;
}
static Bool ValidateNonNullArray(const void* v, const char* functionName) {
if (v != nullptr) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "value pointer cannot be null."));
return false;
}
static Bool TryConvertBlendEquation(GLenum mode, const char* functionName,
::MobileGL::BlendEquation& outEquation) {
outEquation = MG_Util::ConvertGLEnumToBlendEquation(mode);
@@ -93,16 +183,70 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void Viewport_State(GLint x, GLint y, GLsizei width, GLsizei height) {
if (width < 0 || height < 0) {
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Viewport_State",
"Width abd height must be non-negative."));
return;
}
if (!ValidateNonNegativeExtent(width, height, "Viewport_State")) return;
MG_State::pGLContext->SetViewport(IntVec4(x, y, width, height));
}
// ------------------ ARB_viewport_array setters ------------------
void ViewportArrayv_State(GLuint first, GLsizei count, const GLfloat* v) {
if (!ValidateViewportRange(first, count, "ViewportArrayv_State")) return;
if (count == 0) return;
if (!ValidateNonNullArray(v, "ViewportArrayv_State")) return;
if (!ValidateArrayExtents(count, v, "ViewportArrayv_State")) return;
for (GLsizei i = 0; i < count; ++i) {
MG_State::pGLContext->SetViewportIndexed(first + static_cast<GLuint>(i),
FloatVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3]));
}
}
void ViewportIndexedf_State(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
if (!ValidateViewportIndex(index, "ViewportIndexedf_State")) return;
if (!ValidateNonNegativeExtent(w, h, "ViewportIndexedf_State")) return;
MG_State::pGLContext->SetViewportIndexed(index, FloatVec4(x, y, w, h));
}
void ScissorArrayv_State(GLuint first, GLsizei count, const GLint* v) {
if (!ValidateViewportRange(first, count, "ScissorArrayv_State")) return;
if (count == 0) return;
if (!ValidateNonNullArray(v, "ScissorArrayv_State")) return;
if (!ValidateArrayExtents(count, v, "ScissorArrayv_State")) return;
for (GLsizei i = 0; i < count; ++i) {
MG_State::pGLContext->SetScissorBoxIndexed(first + static_cast<GLuint>(i),
IntVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3]));
}
}
void ScissorIndexed_State(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) {
if (!ValidateViewportIndex(index, "ScissorIndexed_State")) return;
if (!ValidateNonNegativeExtent(width, height, "ScissorIndexed_State")) return;
MG_State::pGLContext->SetScissorBoxIndexed(index, IntVec4(left, bottom, width, height));
}
void DepthRangeArrayv_State(GLuint first, GLsizei count, const GLdouble* v) {
if (!ValidateViewportRange(first, count, "DepthRangeArrayv_State")) return;
if (count == 0) return;
if (!ValidateNonNullArray(v, "DepthRangeArrayv_State")) return;
for (GLsizei i = 0; i < count; ++i) {
MG_State::pGLContext->SetDepthRangeIndexed(
first + static_cast<GLuint>(i),
FloatVec2(ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 0])),
ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 1]))));
}
}
void DepthRangeIndexed_State(GLuint index, GLdouble n, GLdouble f) {
if (!ValidateViewportIndex(index, "DepthRangeIndexed_State")) return;
MG_State::pGLContext->SetDepthRangeIndexed(
index, FloatVec2(ClampUnitFloat(static_cast<GLfloat>(n)), ClampUnitFloat(static_cast<GLfloat>(f))));
}
void StencilOpSeparate_State(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
Bool applyFront = false;
Bool applyBack = false;
@@ -175,12 +319,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void Scissor_State(GLint x, GLint y, GLsizei width, GLsizei height) {
if (width < 0 || height < 0) {
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Scissor_State",
"Width abd height must be non-negative."));
return;
}
if (!ValidateNonNegativeExtent(width, height, "Scissor_State")) return;
MG_State::pGLContext->SetScissorBox(IntVec4(x, y, width, height));
}
@@ -189,10 +328,50 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_State::pGLContext->SetSampleCoverage(std::clamp(static_cast<Float>(value), 0.0f, 1.0f), invert == GL_TRUE);
}
// ARB_sample_shading / GL 4.6 core 14.3.1: "value is clamped to [0, 1] when specified", so
// there is no error to raise - a caller that asks for 2.0 gets 1.0 and GL_MIN_SAMPLE_SHADING_-
// VALUE reads back 1.0. Was a logging no-op while ARB_sample_shading was advertised, which
// let an application enable GL_SAMPLE_SHADING and then quietly get the driver's default rate.
void MinSampleShading_State(GLfloat value) {
MG_State::pGLContext->SetMinSampleShadingValue(std::clamp(static_cast<Float>(value), 0.0f, 1.0f));
}
void PolygonOffset_State(GLfloat factor, GLfloat units) {
MG_State::pGLContext->SetPolygonOffset(static_cast<Float>(factor), static_cast<Float>(units));
}
void PolygonOffsetClamp_State(GLfloat factor, GLfloat units, GLfloat clamp) {
// GL 4.6 core 14.6.5 / GL_EXT_polygon_offset_clamp. No error cases: any three floats are
// legal, and clamp = 0 is exactly glPolygonOffset. Whether the backend can APPLY the clamp
// is a separate question (see the DirectGLES/DirectVulkan forwarding); the state is
// recorded either way, because GL_POLYGON_OFFSET_CLAMP has to read back what was written.
MG_State::pGLContext->SetPolygonOffsetClamped(static_cast<Float>(factor), static_cast<Float>(units),
static_cast<Float>(clamp));
}
void ClipControl_State(GLenum origin, GLenum depth) {
// GL 4.5 core 13.5: both arguments are strict enums, and either being wrong is
// GL_INVALID_ENUM with the state left untouched.
if (origin != GL_LOWER_LEFT && origin != GL_UPPER_LEFT) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"glClipControl origin must be GL_LOWER_LEFT or GL_UPPER_LEFT; got " +
MG_Util::ConvertGLEnumToString(origin) + "."));
return;
}
if (depth != GL_NEGATIVE_ONE_TO_ONE && depth != GL_ZERO_TO_ONE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"glClipControl depth must be GL_NEGATIVE_ONE_TO_ONE or GL_ZERO_TO_ONE; got " +
MG_Util::ConvertGLEnumToString(depth) + "."));
return;
}
MG_State::pGLContext->SetClipControl(origin, depth);
}
void PolygonMode_State(GLenum face, GLenum mode) {
// GL 3.3 core: separate front/back polygon modes were removed in 3.1, so the only legal
// face is GL_FRONT_AND_BACK. GL_FRONT / GL_BACK must be rejected (some desktop drivers
@@ -336,7 +515,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
GLboolean IsEnabledi_State(GLenum target, GLuint index) {
if (!ValidateIndexedBlendCapability(target, index, "IsEnabledi_State")) {
if (!ValidateIndexedCapability(target, index, "IsEnabledi_State")) {
return GL_FALSE;
}
@@ -392,7 +571,14 @@ namespace MobileGL::MG_Impl::GLImpl {
}
GLint values[4] = {};
GetIntegeri_v(target, index, values);
*data = values[0] != 0 ? GL_TRUE : GL_FALSE;
// The ARB_viewport_array rectangles are the only multi-component indexed state that
// reaches here; writing element 0 alone would leave the caller's other three untouched.
const GLsizei components = target == GL_VIEWPORT || target == GL_SCISSOR_BOX
? 4
: (target == GL_DEPTH_RANGE ? 2 : 1);
for (GLsizei i = 0; i < components; ++i) {
data[i] = values[i] != 0 ? GL_TRUE : GL_FALSE;
}
}
GLboolean IsEnabled_State(GLenum cap) {
@@ -725,7 +911,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void Disablei_State(GLenum target, GLuint index) {
if (!ValidateIndexedBlendCapability(target, index, "Disablei_State")) {
if (!ValidateIndexedCapability(target, index, "Disablei_State")) {
return;
}
@@ -743,7 +929,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void Enablei_State(GLenum target, GLuint index) {
if (!ValidateIndexedBlendCapability(target, index, "Enablei_State")) {
if (!ValidateIndexedCapability(target, index, "Enablei_State")) {
return;
}
@@ -797,6 +983,44 @@ namespace MobileGL::MG_Impl::GLImpl {
Viewport_State(x, y, width, height);
}
void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v) {
ViewportArrayv_State(first, count, v);
}
void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
ViewportIndexedf_State(index, x, y, w, h);
}
void ViewportIndexedfv(GLuint index, const GLfloat* v) {
// The index is validated before the pointer is touched: glViewportIndexedfv(MAX, nullptr)
// must be one GL_INVALID_VALUE, not a null dereference.
if (!ValidateViewportIndex(index, "ViewportIndexedfv")) return;
if (!ValidateNonNullArray(v, "ViewportIndexedfv")) return;
ViewportIndexedf_State(index, v[0], v[1], v[2], v[3]);
}
void ScissorArrayv(GLuint first, GLsizei count, const GLint* v) {
ScissorArrayv_State(first, count, v);
}
void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) {
ScissorIndexed_State(index, left, bottom, width, height);
}
void ScissorIndexedv(GLuint index, const GLint* v) {
if (!ValidateViewportIndex(index, "ScissorIndexedv")) return;
if (!ValidateNonNullArray(v, "ScissorIndexedv")) return;
ScissorIndexed_State(index, v[0], v[1], v[2], v[3]);
}
void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v) {
DepthRangeArrayv_State(first, count, v);
}
void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f) {
DepthRangeIndexed_State(index, n, f);
}
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
StencilOpSeparate_State(face, sfail, dpfail, dppass);
}
@@ -829,10 +1053,22 @@ namespace MobileGL::MG_Impl::GLImpl {
SampleCoverage_State(value, invert);
}
void MinSampleShading(GLfloat value) {
MinSampleShading_State(value);
}
void PolygonOffset(GLfloat factor, GLfloat units) {
PolygonOffset_State(factor, units);
}
void PolygonOffsetClamp(GLfloat factor, GLfloat units, GLfloat clamp) {
PolygonOffsetClamp_State(factor, units, clamp);
}
void ClipControl(GLenum origin, GLenum depth) {
ClipControl_State(origin, depth);
}
void PolygonMode(GLenum face, GLenum mode) {
PolygonMode_State(face, mode);
}
@@ -20,6 +20,16 @@ namespace MobileGL::MG_Impl::GLImpl {
void Enablei(GLenum target, GLuint index);
void BlendFunc(GLenum sfactor, GLenum dfactor);
void Viewport(GLint x, GLint y, GLsizei width, GLsizei height);
// ARB_viewport_array (core since GL 4.1). Every one of these addresses the same 16-element
// indexed state the classic glViewport/glScissor/glDepthRange trio broadcasts to.
void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v);
void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h);
void ViewportIndexedfv(GLuint index, const GLfloat* v);
void ScissorArrayv(GLuint first, GLsizei count, const GLint* v);
void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height);
void ScissorIndexedv(GLuint index, const GLint* v);
void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v);
void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f);
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);
void StencilOp(GLenum fail, GLenum zfail, GLenum zpass);
void StencilMaskSeparate(GLenum face, GLuint mask);
@@ -28,7 +38,10 @@ namespace MobileGL::MG_Impl::GLImpl {
void StencilFunc(GLenum func, GLint ref, GLuint mask);
void Scissor(GLint x, GLint y, GLsizei width, GLsizei height);
void SampleCoverage(GLfloat value, GLboolean invert);
void MinSampleShading(GLfloat value);
void PolygonOffset(GLfloat factor, GLfloat units);
void PolygonOffsetClamp(GLfloat factor, GLfloat units, GLfloat clamp);
void ClipControl(GLenum origin, GLenum depth);
void PolygonMode(GLenum face, GLenum mode);
void PointSize(GLfloat size);
void PointParameterf(GLenum pname, GLfloat param);
+131 -39
View File
@@ -13,6 +13,7 @@
#include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Math/FixedPointConversion.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
@@ -22,6 +23,50 @@ namespace MobileGL::MG_Impl::GLImpl {
return static_cast<Float>(*(const GLint*)param);
}
// GL_TEXTURE_BORDER_COLOR is the only sampler parameter with more than one component, and it
// is also the only one whose meaning depends on WHICH entry point wrote it. Everything else
// reads exactly one component and does not care.
Bool IsVectorOnlySamplerPname(GLenum pname) {
return pname == GL_TEXTURE_BORDER_COLOR;
}
// A state query returns the value CONVERTED to the type the caller asked for (GL 4.6 core
// 2.2.2 / 6.1), never the other type's bits. These two are the sampler side of the numeric
// casts GetTexParameterfv_State/GetTexParameteriv_State already do on the texture side; the
// sampler path funnels all three spellings through one void* function, which is precisely how
// it came to write a fixed type regardless of the caller.
//
// Truncation rather than rounding for the float -> integer direction, matching the texture
// twin (GetTexParameteriv_State's static_cast<GLint> on MIN_LOD/MAX_LOD/LOD_BIAS): the two
// spellings of the same state disagreeing is the bug being fixed here, and a texture and a
// sampler queried the same way must answer the same number.
void StoreSamplerScalar(void* params, Bool isFloat, Bool isUnsignedInteger, Float value) {
if (isFloat) {
*(GLfloat*)params = value;
return;
}
// Via GLint in both integer spellings: a direct float -> GLuint cast of a negative value
// (GL_TEXTURE_MIN_LOD defaults to -1000) is undefined behaviour, while the two-step
// conversion is the well-defined modular one, and it is what the texture-side
// GetTexParameterIuiv fallback does.
const GLint asInt = static_cast<GLint>(value);
if (isUnsignedInteger) {
*(GLuint*)params = static_cast<GLuint>(asInt);
} else {
*(GLint*)params = asInt;
}
}
void StoreSamplerEnum(void* params, Bool isFloat, Bool isUnsignedInteger, GLenum value) {
if (isFloat) {
*(GLfloat*)params = static_cast<GLfloat>(value);
} else if (isUnsignedInteger) {
*(GLuint*)params = value;
} else {
*(GLint*)params = static_cast<GLint>(value);
}
}
Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isUnsignedInteger) {
if (param == nullptr) return false;
@@ -56,8 +101,15 @@ namespace MobileGL::MG_Impl::GLImpl {
}
} // namespace
// `isIntegerCommand` distinguishes the "I" spellings (glSamplerParameterIiv / Iuiv) from the
// plain ones. It only matters for GL_TEXTURE_BORDER_COLOR, and there it decides everything:
// GL 4.6 core 8.10 says the I forms store the components unmodified with an integer internal
// type, while glSamplerParameteriv converts them to floating point with equation 2.2. Routing
// both to the same setter - which is what this file used to do - meant glSamplerParameteriv
// stored raw integers (so a border of 255 became float 255.0 instead of the spec's ~1.19e-7)
// and glSamplerParameterIiv lost the fact that it was ever an integer at all.
void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat,
bool isUnsignedInteger) {
bool isUnsignedInteger, bool isIntegerCommand) {
if (param == nullptr) return;
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
@@ -112,6 +164,13 @@ namespace MobileGL::MG_Impl::GLImpl {
if (isFloat) {
const auto* values = (const GLfloat*)param;
samplerObj->SetBorderColor(FloatVec4(values[0], values[1], values[2], values[3]));
} else if (!isIntegerCommand) {
// glSamplerParameteriv: GL 4.6 core equation 2.2 into the FLOAT border colour.
const auto* values = (const GLint*)param;
samplerObj->SetBorderColor(FloatVec4(MG_Util::SignedNormalizedInt32ToFloat(values[0]),
MG_Util::SignedNormalizedInt32ToFloat(values[1]),
MG_Util::SignedNormalizedInt32ToFloat(values[2]),
MG_Util::SignedNormalizedInt32ToFloat(values[3])));
} else if (isUnsignedInteger) {
const auto* values = (const GLuint*)param;
samplerObj->SetBorderColorUI(UintVec4(values[0], values[1], values[2], values[3]));
@@ -128,7 +187,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat,
bool isUnsignedInteger) {
bool isUnsignedInteger, bool isIntegerCommand) {
if (params == nullptr) return;
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
@@ -141,47 +200,56 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!SamplerImpl::ValidateSamplerObject(sampler)) return;
using namespace MG_Util;
// Every scalar pname goes through StoreSamplerScalar/StoreSamplerEnum so the CALLER'S form
// decides the destination type. Writing a fixed type regardless - which is what these case
// labels used to do - hands back the other type's bit pattern rather than a converted value:
// glGetSamplerParameterfv(GL_TEXTURE_WRAP_S) deposited the integer 10497 into a GLfloat and
// the caller read 1.47e-41, and glGetSamplerParameteriv(GL_TEXTURE_MIN_LOD) deposited the
// IEEE bits of -1000.0f and the caller read -998637568. Sixteen (pname, entry-point) pairs
// were broken this way; only MAX_ANISOTROPY_EXT and BORDER_COLOR branched correctly, which is
// how the same bug class was already found and fixed once for a single pname.
switch (pname) {
case GL_TEXTURE_WRAP_S:
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapS());
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapS()));
break;
case GL_TEXTURE_WRAP_T:
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapT());
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapT()));
break;
case GL_TEXTURE_WRAP_R:
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapR());
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapR()));
break;
case GL_TEXTURE_MIN_FILTER:
*(GLuint*)params =
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMinFilter(), samplerObj->GetMipmapMode());
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMinFilter(),
samplerObj->GetMipmapMode()));
break;
case GL_TEXTURE_MAG_FILTER:
*(GLuint*)params =
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMagFilter(), SamplerMipmapMode::None);
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMagFilter(),
SamplerMipmapMode::None));
break;
case GL_TEXTURE_MIN_LOD:
*(GLfloat*)params = samplerObj->GetMinLod();
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMinLod());
break;
case GL_TEXTURE_MAX_LOD:
*(GLfloat*)params = samplerObj->GetMaxLod();
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMaxLod());
break;
case GL_TEXTURE_LOD_BIAS:
*(GLfloat*)params = samplerObj->GetLodBias();
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetLodBias());
break;
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
if (isFloat) {
*(GLfloat*)params = samplerObj->GetMaxAnisotropy();
} else if (isUnsignedInteger) {
*(GLuint*)params = static_cast<GLuint>(samplerObj->GetMaxAnisotropy());
} else {
*(GLint*)params = static_cast<GLint>(samplerObj->GetMaxAnisotropy());
}
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMaxAnisotropy());
break;
case GL_TEXTURE_COMPARE_MODE:
*(GLuint*)params = MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode());
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode()));
break;
case GL_TEXTURE_COMPARE_FUNC:
*(GLuint*)params = MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc());
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc()));
break;
case GL_TEXTURE_BORDER_COLOR: {
if (isFloat) {
@@ -191,6 +259,16 @@ namespace MobileGL::MG_Impl::GLImpl {
out[1] = color.y();
out[2] = color.z();
out[3] = color.w();
} else if (!isIntegerCommand) {
// glGetSamplerParameteriv: the inverse of the write side, GL 4.6 core equation 2.3.
// Exactly inverse, so a {0,1,2,4} written with glSamplerParameteriv reads back as
// {0,1,2,4}; a bare truncating cast answered {0,0,0,0}.
const auto& color = samplerObj->GetBorderColor();
auto* out = (GLint*)params;
out[0] = MG_Util::FloatToSignedNormalizedInt32(color.x());
out[1] = MG_Util::FloatToSignedNormalizedInt32(color.y());
out[2] = MG_Util::FloatToSignedNormalizedInt32(color.z());
out[3] = MG_Util::FloatToSignedNormalizedInt32(color.w());
} else if (isUnsignedInteger) {
const auto& color = samplerObj->GetBorderColorUI();
auto* out = (GLuint*)params;
@@ -293,16 +371,10 @@ namespace MobileGL::MG_Impl::GLImpl {
if (sampler == 0) {
textureUnit.SetSamplerObject(nullptr);
} else {
// GL 3.3 core 3.8.2: BindSampler on a name GenSamplers never returned - or one already
// deleted - is INVALID_OPERATION. SamplerParameter* raises INVALID_VALUE for the same
// name, which is why this cannot go through the shared SamplerImpl validator.
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler_State",
std::format("Invalid sampler name {}", sampler)));
return;
}
// GL 4.6 core 8.2: BindSampler on a name GenSamplers never returned - or one already
// deleted - is INVALID_OPERATION, and so is every other sampler entry point on such a
// name, so the shared validator answers for all of them.
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler);
if (!doesSamplerObjectCreated) {
MG_State::pGLContext->CreateSamplerObject(sampler);
@@ -356,30 +428,50 @@ namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
void GetSamplerParameteriv(GLuint sampler, GLenum pname, GLint* params) {
GetSamplerParam_State(sampler, pname, params, false, false);
GetSamplerParam_State(sampler, pname, params, false, false, false);
}
void SamplerParameterIuiv(GLuint sampler, GLenum pname, const GLuint* param) {
SetSamplerParam_State(sampler, pname, param, false, true);
SetSamplerParam_State(sampler, pname, param, false, true, true);
}
void SamplerParameterIiv(GLuint sampler, GLenum pname, const GLint* param) {
SetSamplerParam_State(sampler, pname, param, false, false);
SetSamplerParam_State(sampler, pname, param, false, false, true);
}
void SamplerParameteriv(GLuint sampler, GLenum pname, const GLint* param) {
SetSamplerParam_State(sampler, pname, param, false, false);
SetSamplerParam_State(sampler, pname, param, false, false, false);
}
void SamplerParameterfv(GLuint sampler, GLenum pname, const GLfloat* param) {
SetSamplerParam_State(sampler, pname, param, true, false);
SetSamplerParam_State(sampler, pname, param, true, false, false);
}
// GL 4.6 core 8.10: the scalar spellings take "the value of pname", so a pname with more than one
// component is INVALID_ENUM here rather than something to read four components of. Guarding at
// the entry point rather than downstream is also what stops the vector path reading twelve bytes
// past the caller's single stack scalar - taking the address of a by-value argument and handing
// it to a four-component reader is what these used to do. The texture-side twins already answer
// INVALID_ENUM for GL_TEXTURE_BORDER_COLOR (TexParameteri/f name it as unsupported outright).
void SamplerParameteri(GLuint sampler, GLenum pname, GLint param) {
if (IsVectorOnlySamplerPname(pname)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SamplerParameteri",
"pname has more than one component and needs a vector form."));
return;
}
SamplerParameteriv(sampler, pname, &param);
}
void SamplerParameterf(GLuint sampler, GLenum pname, GLfloat param) {
if (IsVectorOnlySamplerPname(pname)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SamplerParameterf",
"pname has more than one component and needs a vector form."));
return;
}
SamplerParameterfv(sampler, pname, &param);
}
@@ -388,15 +480,15 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void GetSamplerParameterIuiv(GLuint sampler, GLenum pname, GLuint* params) {
GetSamplerParam_State(sampler, pname, params, false, true);
GetSamplerParam_State(sampler, pname, params, false, true, true);
}
void GetSamplerParameterIiv(GLuint sampler, GLenum pname, GLint* params) {
GetSamplerParam_State(sampler, pname, params, false, false);
GetSamplerParam_State(sampler, pname, params, false, false, true);
}
void GetSamplerParameterfv(GLuint sampler, GLenum pname, GLfloat* params) {
GetSamplerParam_State(sampler, pname, params, true, false);
GetSamplerParam_State(sampler, pname, params, true, false, false);
}
void GenSamplers(GLsizei count, GLuint* samplers) {
@@ -12,11 +12,17 @@
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
namespace MobileGL::MG_Impl::GLImpl::SamplerImpl {
// GL 4.6 core 8.2: "An INVALID_OPERATION error is generated if sampler is not the name of a
// sampler object previously returned from a call to GenSamplers." That class is shared by every
// sampler entry point - BindSampler, SamplerParameter*, GetSamplerParameter* - so this one gate
// answers for all of them. It used to report INVALID_VALUE (the GL 3.3 wording), which forced
// BindSampler to carry a bespoke duplicate of the same check just to get the class right.
Bool ValidateSamplerName(GLuint sampler) {
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerName",
std::format("Invalid sampler name {}", sampler)));
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerName",
std::format("Invalid sampler name {}", sampler)));
return false;
}
return true;
+72 -1
View File
@@ -8,6 +8,7 @@
#include "GL_Sync.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
@@ -35,6 +36,22 @@ namespace MobileGL::MG_Impl::GLImpl {
} // namespace
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;
syncObject->condition = condition;
syncObject->flags = flags;
@@ -52,8 +69,25 @@ namespace MobileGL::MG_Impl::GLImpl {
}
GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
// GL 4.6 core 4.1.1: GL_SYNC_FLUSH_COMMANDS_BIT is the only bit this call accepts, and
// any other bit is INVALID_VALUE. Silently ignoring the stray bits used to make a caller
// that passed, say, GL_SYNC_GPU_COMMANDS_COMPLETE by mistake think it had asked for a
// flush it never got.
if ((flags & ~static_cast<GLbitfield>(GL_SYNC_FLUSH_COMMANDS_BIT)) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"flags must be zero or GL_SYNC_FLUSH_COMMANDS_BIT."));
return GL_WAIT_FAILED;
}
const auto* syncObject = FindSyncObject(sync);
if (!syncObject) {
// The spec pairs the GL_WAIT_FAILED return with a recorded INVALID_VALUE; returning
// the enum alone left glGetError() clean and the failure indistinguishable from a
// genuine wait failure on a live sync.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
return GL_WAIT_FAILED;
}
const auto backendClientWaitSync = MG_Backend::gBackendFunctionsTable.GL.ClientWaitSync;
@@ -64,8 +98,23 @@ namespace MobileGL::MG_Impl::GLImpl {
}
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);
if (!syncObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
return;
}
const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync;
@@ -96,8 +145,22 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values) {
// GL 4.6 core 4.1: a negative bufSize is INVALID_VALUE, an unnamed sync is INVALID_VALUE
// and an unrecognised pname is INVALID_ENUM. All three used to leave glGetError() clean
// and write a plausible-looking zero, which is the one failure mode a caller cannot tell
// apart from a real answer - GL_SYNC_STATUS legitimately answers GL_UNSIGNALED (0x9118),
// but a mistyped pname answered a bare 0 that no query ever returns.
if (bufSize < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must not be negative."));
return;
}
const auto* syncObject = FindSyncObject(sync);
if (!syncObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
if (length) {
*length = 0;
}
@@ -123,7 +186,15 @@ namespace MobileGL::MG_Impl::GLImpl {
value = static_cast<GLint>(syncObject->flags);
break;
default:
break;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"pname must be GL_OBJECT_TYPE, GL_SYNC_STATUS, GL_SYNC_CONDITION or "
"GL_SYNC_FLAGS."));
if (length) {
*length = 0;
}
return;
}
if (length) {
File diff suppressed because it is too large Load Diff
@@ -8,9 +8,24 @@
#pragma once
#include <Includes.h>
#include <MG_State/GLState/TextureState/TextureObject.h>
namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
// Answers a texture-image query straight out of the CPU shadow, into client memory or a bound
// PIXEL_PACK_BUFFER. This is the whole of glGetTexImage on a build with no backend readback, and
// it is also the sound fallback for a backend that has no GPU image to read: with no image,
// nothing GPU-side can ever have written the texture, so the shadow IS its content.
//
// It answers a NARROWER contract than glGetTexImage's, and refuses what it cannot do rather than
// answering wrongly. The copy is verbatim: it performs no format or type conversion, and it packs
// rows tightly, honouring only GL_PACK_SWAP_BYTES and the bitmap GL_PACK_LSB_FIRST path. A
// request whose (format, type) texel size differs from the texture's own, or a pixel-store state
// that adds row padding / a row-length override / a skip offset, is rejected with
// GL_INVALID_OPERATION (see ValidateShadowReadbackLayout, which spells out why each is unsafe).
void CopyTextureImageToClientOrPBO_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureUploadTarget textureUploadTarget, GLint level, GLenum format,
GLenum type, GLsizei bufSize, void* pixels, const char* caller);
// The sized internal formats a buffer texture accepts (GL 4.6 core table 8.16). The buffer
// clears take the same list, so it is shared rather than written out twice.
Bool IsBufferTextureInternalFormat(GLenum internalformat);
@@ -37,8 +52,13 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum format, GLenum type, const void* pixels);
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels);
void CompressedTextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format,
GLsizei imageSize, const void* data);
void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
GLsizei height, GLenum format, GLsizei imageSize, const void* data);
void CompressedTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
const void* data);
void TextureParameterf(GLuint texture, GLenum pname, GLfloat param);
void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params);
void TextureParameteri(GLuint texture, GLenum pname, GLint param);
@@ -60,6 +80,8 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetTextureParameteriv(GLuint texture, GLenum pname, GLint* params);
void GetTextureLevelParameterfv(GLuint texture, GLint level, GLenum pname, GLfloat* params);
void GetTextureLevelParameteriv(GLuint texture, GLint level, GLenum pname, GLint* params);
void TextureView(GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel,
GLuint numlevels, GLuint minlayer, GLuint numlayers);
void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width);
void TexStorage2D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height);
void TexStorage3D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
+169 -3
View File
@@ -103,6 +103,28 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true;
}
Bool ValidateCubeMapArrayShape(TextureUploadTarget target, GLsizei width, GLsizei height, GLsizei depth,
const char* caller) {
if (target != TextureUploadTarget::CubeMapArray && target != TextureUploadTarget::ProxyCubeMapArray) {
return true;
}
if (width != height) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Cube map array levels must be square (width == height)"));
return false;
}
if (depth % 6 != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Cube map array depth must be a multiple of six"));
return false;
}
return true;
}
Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height) {
if (target == TextureUploadTarget::CubeMapPositiveX || target == TextureUploadTarget::CubeMapNegativeX ||
target == TextureUploadTarget::CubeMapPositiveY || target == TextureUploadTarget::CubeMapNegativeY ||
@@ -313,9 +335,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return false;
}
// TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4).
if (format == TextureInputFormat::StencilIndex) {
return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format");
// The stencil-only transfer path arrived with GL 4.4 / ARB_texture_stencil8, and only ever
// pairs with stencil-only storage: against a depth, depth-stencil or colour internal 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)) {
@@ -619,4 +645,144 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
}
return true;
}
// GL 4.6 core table 8.21 ("Compatible internal formats for TextureView"), transcribed whole.
// Written against the raw GLenum rather than TextureInternalFormat on purpose: MobileGL's own
// enum collapses every compressed format onto uncompressed storage and drops formats it
// cannot carry, so classifying the converted value would silently widen the compatibility
// rule - GL_COMPRESSED_RG_RGTC2 and GL_RGBA8 would end up in the same class.
TextureViewClass GetTextureViewClass(GLenum internalformat) {
switch (internalformat) {
case GL_RGBA32F:
case GL_RGBA32UI:
case GL_RGBA32I:
return TextureViewClass::Bits128;
case GL_RGB32F:
case GL_RGB32UI:
case GL_RGB32I:
return TextureViewClass::Bits96;
case GL_RGBA16F:
case GL_RG32F:
case GL_RGBA16UI:
case GL_RG32UI:
case GL_RGBA16I:
case GL_RG32I:
case GL_RGBA16:
case GL_RGBA16_SNORM:
return TextureViewClass::Bits64;
case GL_RGB16:
case GL_RGB16_SNORM:
case GL_RGB16F:
case GL_RGB16UI:
case GL_RGB16I:
return TextureViewClass::Bits48;
case GL_RG16F:
case GL_R11F_G11F_B10F:
case GL_R32F:
case GL_RGB10_A2UI:
case GL_RGBA8UI:
case GL_RG16UI:
case GL_R32UI:
case GL_RGBA8I:
case GL_RG16I:
case GL_R32I:
case GL_RGB10_A2:
case GL_RGBA8:
case GL_RG16:
case GL_RGBA8_SNORM:
case GL_RG16_SNORM:
case GL_SRGB8_ALPHA8:
case GL_RGB9_E5:
return TextureViewClass::Bits32;
case GL_RGB8:
case GL_RGB8_SNORM:
case GL_SRGB8:
case GL_RGB8UI:
case GL_RGB8I:
return TextureViewClass::Bits24;
case GL_R16F:
case GL_RG8UI:
case GL_R16UI:
case GL_RG8I:
case GL_R16I:
case GL_RG8:
case GL_R16:
case GL_RG8_SNORM:
case GL_R16_SNORM:
return TextureViewClass::Bits16;
case GL_R8UI:
case GL_R8I:
case GL_R8:
case GL_R8_SNORM:
return TextureViewClass::Bits8;
case GL_COMPRESSED_RED_RGTC1:
case GL_COMPRESSED_SIGNED_RED_RGTC1:
return TextureViewClass::Rgtc1Red;
case GL_COMPRESSED_RG_RGTC2:
case GL_COMPRESSED_SIGNED_RG_RGTC2:
return TextureViewClass::Rgtc2Rg;
case GL_COMPRESSED_RGBA_BPTC_UNORM:
case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM:
return TextureViewClass::BptcUnorm;
case GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT:
case GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT:
return TextureViewClass::BptcFloat;
default:
// Every depth/stencil format, every S3TC/ETC/ASTC format and every unsized format
// reaches here. The caller must then demand an EXACT format match.
return TextureViewClass::None;
}
}
// GL 4.6 core table 8.20 ("Legal texture targets for TextureView").
Bool IsLegalTextureViewTargetPair(TextureTarget origTarget, TextureTarget viewTarget) {
switch (origTarget) {
case TextureTarget::Texture1D:
return viewTarget == TextureTarget::Texture1D || viewTarget == TextureTarget::Texture1DArray;
case TextureTarget::Texture2D:
return viewTarget == TextureTarget::Texture2D || viewTarget == TextureTarget::Texture2DArray;
case TextureTarget::Texture3D:
return viewTarget == TextureTarget::Texture3D;
case TextureTarget::TextureCubeMap:
return viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::Texture2D ||
viewTarget == TextureTarget::Texture2DArray || viewTarget == TextureTarget::TextureCubeMapArray;
case TextureTarget::TextureRectangle:
return viewTarget == TextureTarget::TextureRectangle;
case TextureTarget::Texture1DArray:
return viewTarget == TextureTarget::Texture1DArray || viewTarget == TextureTarget::Texture1D;
case TextureTarget::Texture2DArray:
return viewTarget == TextureTarget::Texture2DArray || viewTarget == TextureTarget::Texture2D ||
viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::TextureCubeMapArray;
case TextureTarget::TextureCubeMapArray:
return viewTarget == TextureTarget::TextureCubeMapArray || viewTarget == TextureTarget::Texture2DArray ||
viewTarget == TextureTarget::Texture2D || viewTarget == TextureTarget::TextureCubeMap;
case TextureTarget::Texture2DMultisample:
case TextureTarget::Texture2DMultisampleArray:
return viewTarget == TextureTarget::Texture2DMultisample ||
viewTarget == TextureTarget::Texture2DMultisampleArray;
case TextureTarget::TextureBuffer:
// The table lists no legal target for a buffer texture: its storage is a buffer
// object, and there is nothing to make a view of.
return false;
default:
return false;
}
}
Uint RequiredTextureViewLayerCount(TextureTarget viewTarget) {
switch (viewTarget) {
case TextureTarget::TextureCubeMap:
return 6;
case TextureTarget::Texture1D:
case TextureTarget::Texture2D:
case TextureTarget::Texture3D:
case TextureTarget::TextureRectangle:
case TextureTarget::Texture2DMultisample:
return 1;
default:
// 1D/2D array, cube-map array, 2D multisample array: any count (the cube-map array's
// "multiple of 6" is checked by the caller).
return 0;
}
}
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
@@ -20,6 +20,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
Bool ValidateTexturePixelDataType(TexturePixelDataType texturePixelDataType);
Bool ValidateTextureLevelNumber(Int level);
Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height);
// The two shape rules a cube-map-array level owes (GL 4.6 core 8.5): its faces are square, and
// its depth counts whole cubes. Both are GL_INVALID_VALUE. This used to be spelled inline in
// glTexStorage3D only, which is why glTexImage3D let both violations through - every entry
// point that DEFINES a cube-array level calls this now, so the two cannot drift again. A
// non-cube-array upload target answers true untouched.
Bool ValidateCubeMapArrayShape(TextureUploadTarget target, GLsizei width, GLsizei height, GLsizei depth,
const char* caller);
Bool ValidateTextureSizeRange(Int width, Int height, Int depth);
Bool ValidateTextureInternalFormat(TextureInternalFormat format);
Bool ValidateTextureBorderNumber(Int border);
@@ -79,4 +86,33 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
// GL 4.6 SS 8.6 subset rule for glCopyTexImage*: the read buffer must supply every component
// the requested internalformat asks for, but may supply more.
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat);
// ---- glTextureView (ARB_texture_view / GL 4.6 core 8.18) ----
// Table 8.21's view classes. `None` is not a class - it means the format has NO entry in the
// table, which the spec turns into a much stricter rule than "same class": such a format can
// only ever be viewed as ITSELF. Every depth, stencil and depth/stencil format lands here,
// which is why the Better Clouds D24S8 view must name GL_DEPTH24_STENCIL8 exactly.
enum class TextureViewClass {
None = 0,
Bits128,
Bits96,
Bits64,
Bits48,
Bits32,
Bits24,
Bits16,
Bits8,
Rgtc1Red,
Rgtc2Rg,
BptcUnorm,
BptcFloat,
};
TextureViewClass GetTextureViewClass(GLenum internalformat);
// Table 8.20: which <target> values glTextureView accepts for a given origtexture target.
Bool IsLegalTextureViewTargetPair(TextureTarget origTarget, TextureTarget viewTarget);
// Table 8.20 again, read the other way: how many layers <target> requires. Returns 0 for the
// targets whose layer count is unconstrained (the array targets), 6 for GL_TEXTURE_CUBE_MAP,
// and 1 for every single-layer target. GL_TEXTURE_CUBE_MAP_ARRAY is special-cased by the
// caller because its constraint is "a multiple of 6", not an exact count.
Uint RequiredTextureViewLayerCount(TextureTarget viewTarget);
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
@@ -514,10 +514,17 @@ namespace MobileGL::MG_Impl::GLImpl {
// 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.
//
// Whether the backend can feed it is detected, not assumed: DirectVulkan needs shaderFloat64,
// and DirectGLES can never have it at all. A backend without it declines here, loudly - GL error
// plus a log line naming the reason - rather than accepting state no draw could honour and
// rendering garbage. The matching startup POST row is in MG_Util/SelfTest/DriverPost.cpp.
// Whether the backend can FEED it at full precision is detected, not assumed: DirectVulkan
// needs shaderFloat64, and DirectGLES can never have it at all. What that costs is PRECISION,
// not the call and no longer the array: GL 4.6 core 10.3.2 defines no error for a well-formed
// 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,
GLuint attribindex, GLint size, GLenum type,
GLuint relativeoffset) {
@@ -528,14 +535,11 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!MG_Backend::pActiveBackendObject ||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
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 "
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs",
"backend has no double-precision vertex attribute support - the format is recorded "
"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);
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),
@@ -12,15 +12,15 @@
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Util/Converters/MGToGL/DataTypeConverter.h>
#include <MG_Util/Converters/MGToStr/DataTypeConverter.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
Uint GetMaxVertexAttribs() {
constexpr Uint capacity = static_cast<Uint>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
if (!MG_Backend::pActiveBackendObject) return capacity;
const Int backendLimit = MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs;
if (backendLimit <= 0) return capacity;
return std::min(static_cast<Uint>(backendLimit), capacity);
// Shared with reflection's limit and with gl_MaxVertexAttribs; see ResolveMaxVertexAttribs.
const Bool hasBackend = MG_Backend::pActiveBackendObject != nullptr;
const Int backendLimit =
hasBackend ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs : 0;
return static_cast<Uint>(MG_Util::ShaderTranspiler::ResolveMaxVertexAttribs(hasBackend, backendLimit));
}
Uint GetMaxVertexAttribBindings() {
+320 -4
View File
@@ -24,9 +24,14 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(MGL_ITEST_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..)
# Only meaningful where MobileGL_s exists (i.e. not Android).
if (NOT TARGET MobileGL_s)
message(STATUS "MobileGL_s is not available; skipping the integration test module")
# Desktop links the static implementation directly. Android runs the same
# executable from adb shell and links the shipping shared library instead.
if (ANDROID)
set(MGL_ITEST_MOBILEGL_TARGET MobileGL)
elseif (TARGET MobileGL_s)
set(MGL_ITEST_MOBILEGL_TARGET MobileGL_s)
else()
message(STATUS "No MobileGL library target is available; skipping the integration test module")
return()
endif()
@@ -53,33 +58,74 @@ add_executable(MobileGLIntegrationTest
Scenarios/DrawParametersScenario.cpp
Scenarios/AsyncCompileScenario.cpp
Scenarios/XfbAfterClipDistanceScenario.cpp
Scenarios/UnwrittenPositionOutputScenario.cpp
Scenarios/SampleMaskScopeScenario.cpp
Scenarios/SampledSetStalenessScenario.cpp
Scenarios/ThreeChannelAttachmentScenario.cpp
Scenarios/SnormAttachmentScenario.cpp
Scenarios/PipelineFailureScenario.cpp
Scenarios/AdvertisedLimitsScenario.cpp
Scenarios/PixelStoreSweepScenario.cpp
Scenarios/PrimitiveRestartScenario.cpp
Scenarios/FragCoordOriginScenario.cpp
Scenarios/ClearThenReadPixelsScenario.cpp
Scenarios/SampleVariablesScenario.cpp
Scenarios/DepthStencilReadbackScenario.cpp
Scenarios/DepthStencilReadbackMatrixScenario.cpp
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
Scenarios/ClipDistanceScenario.cpp
Scenarios/ViewportArrayScenario.cpp
Scenarios/SsboArrayLengthScenario.cpp
Scenarios/DoublePrecisionScenario.cpp
Scenarios/UniformInitializerScenario.cpp
Scenarios/SwizzleAccessRoutineScenario.cpp
Scenarios/IterationRPFirstReductionScenario.cpp
Scenarios/IterationRPProgram203Scenario.cpp
Scenarios/IterationRPScratchFixScenario.cpp
Scenarios/ProgramPipelineScenario.cpp
Scenarios/ImageLoadStoreSsoScenario.cpp
Scenarios/ImageTargetKindScenario.cpp
Scenarios/ImageFormatQualifierScenario.cpp
Scenarios/NonCoreImageFormatScenario.cpp
Scenarios/ImageSizeAfterRespecScenario.cpp
Scenarios/SsboDeclarationFormScenario.cpp
Scenarios/Glsl420DeclarationScenario.cpp
Scenarios/IoBlockNameCollisionScenario.cpp
Scenarios/UnlocatedIoBlockScenario.cpp
Scenarios/TessellationDrawModeScenario.cpp
Scenarios/GeometryDrawModeScenario.cpp
Scenarios/PostLinkAttachScenario.cpp
Scenarios/FormatlessImageBakeScenario.cpp
Scenarios/FragmentOutputArrayIndexScenario.cpp
Scenarios/BufferTextureScenario.cpp
Scenarios/VertexAttribBindingScenario.cpp
Scenarios/XfbCaptureBufferReuseScenario.cpp
Scenarios/XfbPrimitiveQueryScenario.cpp
Scenarios/PrimitivesGeneratedNoXfbScenario.cpp
Scenarios/XfbRepeatedCaptureScenario.cpp
Scenarios/TessellationXfbCaptureScenario.cpp
Scenarios/PointSizeDemotionScenario.cpp
Scenarios/VertexArrayEnableDisableScenario.cpp
Scenarios/CopyImageLevelRangeScenario.cpp
Scenarios/CopyImageLayeredScenario.cpp
Scenarios/CopyImagePacked16Scenario.cpp
Scenarios/TextureViewScenario.cpp
Scenarios/PackedWordReadbackScenario.cpp
Scenarios/LayeredAttachmentBarrierScenario.cpp
Scenarios/LayeredAttachmentShapeScenario.cpp
Scenarios/LayeredTextureReadbackScenario.cpp
Scenarios/AtomicCounterScenario.cpp
Scenarios/LargeArenaAdoptionScenario.cpp
Scenarios/SsboArrayDynamicIndexScenario.cpp
Scenarios/StorageBufferRegrowScenario.cpp
Scenarios/SpirvShaderBinaryScenario.cpp
Scenarios/RelinkStageSetScenario.cpp
Scenarios/GuiBatchScenario.cpp
Scenarios/UnboundImageDescriptorScenario.cpp
Scenarios/IntegerBorderColorScenario.cpp
Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp
Scenarios/RenderbufferBlendFormatScenario.cpp
Scenarios/DualSourceBlendScenario.cpp
)
target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -90,9 +136,20 @@ target_include_directories(MobileGLIntegrationTest PRIVATE
# gtest, not gtest_main: Main.cpp installs the harness banner itself.
target_link_libraries(MobileGLIntegrationTest PRIVATE
GTest::gtest
MobileGL_s
${MGL_ITEST_MOBILEGL_TARGET}
)
if (ANDROID)
find_library(MGL_ITEST_ANDROID_LIBRARY android REQUIRED)
find_library(MGL_ITEST_LOG_LIBRARY log REQUIRED)
find_library(MGL_ITEST_MEDIANDK_LIBRARY mediandk REQUIRED)
target_link_libraries(MobileGLIntegrationTest PRIVATE
${MGL_ITEST_ANDROID_LIBRARY}
${MGL_ITEST_LOG_LIBRARY}
${MGL_ITEST_MEDIANDK_LIBRARY}
)
endif()
if (MSVC)
# Same reason as MG_Test/Backend/DirectVulkan: the GLES headers declare gl*
# as dllimport on Windows, so the in-library GL entry-point definitions only
@@ -101,6 +158,10 @@ if (MSVC)
endif()
target_compile_definitions(MobileGLIntegrationTest PRIVATE -DNOMINMAX)
if (ANDROID)
return()
endif()
# --- ctest wiring --------------------------------------------------------
# A bare libEGL on a glvnd box resolves to whatever vendor comes first, which is
# usually Mesa/llvmpipe - a software rasteriser silently replacing the GPU under
@@ -221,6 +282,19 @@ endif()
set(MGL_ITEST_VULKAN_ENV ${MGL_ITEST_COMMON_ENV})
if (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_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1"
"MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1"
"MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1")
endif()
endif()
# The ENVIRONMENT test property is itself a `;`-list, and gtest_discover_tests
@@ -250,6 +324,70 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT
mgl_itest_join_environment(MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1" ${MGL_ITEST_COMMON_ENV})
# The shader-compiler configurations AsyncCompileScenario needs, and the one
# ViewportArrayScenario's negative control needs.
#
# These used to be poked into MG_Config::Features from inside the test bodies. They
# cannot be any more - on Android this module links the SHIPPING libMobileGL.so, which
# exports nothing internal - and they should not have been anyway: half of what each of
# them decides is latched before the first GL call (the compile pool and its threads;
# the advertised extension list, which a backend builds once from the configuration in
# force at its first use), so an in-process write could only ever have moved the other
# half. Every one of them is a whole-process property, and a whole-process property is
# spelled with an environment variable and a ctest entry of its own.
#
# Note the shape of every list here: it APPENDS to MGL_ITEST_COMMON_ENV /
# MGL_ITEST_VULKAN_ENV rather than standing alone. A ctest ENVIRONMENT property REPLACES
# the job environment rather than adding to it, so an entry that lists only its mode
# variable would silently lose the EGL vendor and Vulkan ICD pinning and run against
# whatever the loader found first.
mgl_itest_join_environment(MGL_ITEST_GLES_ASYNC_ON_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_ASYNC_OFF_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ASYNC_SHADER_COMPILE=0" ${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ON_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_VULKAN_ENV})
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_OFF_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=0" ${MGL_ITEST_VULKAN_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_OPTIMISTIC_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ASYNC_SHADER_COMPILE=1"
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1" ${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_VULKAN_OPTIMISTIC_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1"
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1" ${MGL_ITEST_VULKAN_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0" ${MGL_ITEST_COMMON_ENV})
# MOBILEGL_LOG_FILE_PATH alongside the pin, because the arming assertion needs somewhere to
# read the library's own report from. The strip's arming signal is a latched MGLOG_I and there
# is no other way for a test process to learn that it fired - MG_Config is not reachable from
# this module on Android, where it links the shipping library. The path is per-lane so nothing
# else appends to it, and the case only trusts the bytes written after it started.
mgl_itest_join_environment(MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/unlocated-io-blocks.log"
${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1" ${MGL_ITEST_COMMON_ENV})
# Same shape as the UnlocatedIoBlocks entry: the log path is where the reroute's latched
# MGLOG_I lands, and the arming case only trusts the bytes written after it started.
mgl_itest_join_environment(MGL_ITEST_VULKAN_PRIMGEN_REROUTE_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/primgen-query-reroute.log"
${MGL_ITEST_VULKAN_ENV})
# The point-size demotion pinned on, per backend, with a per-lane log file for the arming
# assertion - the same MOBILEGL_LOG_FILE_PATH reasoning as the UnlocatedIoBlocks lane above.
# Two lanes because the demotion runs in the SHARED phase-B chain and each backend then
# consumes it differently (Espryt respells the driver-side capture request, Magma binds the
# SPIR-V Xfb decorations to the carrier).
mgl_itest_join_environment(MGL_ITEST_GLES_POINT_SIZE_DEMOTION_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_POINT_SIZE_DEMOTION=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/point-size-demotion-gles.log"
${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_VULKAN_POINT_SIZE_DEMOTION_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_POINT_SIZE_DEMOTION=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/point-size-demotion-vulkan.log"
${MGL_ITEST_VULKAN_ENV})
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
set(MGL_ITEST_TIMEOUT 120)
@@ -314,3 +452,181 @@ gtest_discover_tests(MobileGLIntegrationTest
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}"
)
# UnlocatedIoBlockScenario with the interface-block location strip PINNED ON, for the same
# reason the depth/stencil entry above pins its emulation: without it this scenario is
# UNFALSIFIABLE on the machines this suite runs on. llvmpipe carries a located interface block
# correctly, so the driver POST that arms the strip on Mali answers "healthy" here and the
# emulation never runs - the ambient registration would be exercising the un-stripped path
# twice and calling it coverage. With the variable set, the blocks really are emitted with no
# location and the assertion is about the spelling the device gets.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.UnlocatedIoBlocks."
TEST_FILTER "UnlocatedIoBlockScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT}"
)
# AsyncCompileScenario, with asynchronous compilation PINNED ON per backend.
#
# Not a duplicate of what the two ambient registrations already run: they run whatever
# MobileGL's built-in default happens to be, and the day that default flips they would
# stop covering the asynchronous path without anything going red. These entries are the
# ones that keep the asynchronous half tested no matter what ships. They are also the
# only place ExtensionStringMatchesTheConfiguration can assert that the extension IS
# advertised - the case derives its expectation from this variable and nothing else, and
# skips where it is unset, precisely so that it is not asserting the implementation
# against itself.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.AsyncOn."
TEST_FILTER "AsyncCompileScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_ASYNC_ON_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.AsyncOn."
TEST_FILTER "AsyncCompileScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_ON_ENVIRONMENT}"
)
# The other side of the same switch: asynchronous compilation OFF, so
# GL_KHR_parallel_shader_compile must be WITHDRAWN from both spellings of the extension
# list and GL_MAX_SHADER_COMPILER_THREADS_KHR must read 0. Only that one case is
# registered here because it is the only one that has anything to say in this
# configuration - the other four exist to observe worker-built artifacts, and there are
# none - so registering the whole scenario would buy four guaranteed skips per backend.
# Together with the AsyncOn. entries above, one ctest run still covers both flag states,
# which is what the in-process forcing used to be for.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.AsyncOff."
TEST_FILTER "AsyncCompileScenario.ExtensionStringMatchesTheConfiguration"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_ASYNC_OFF_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.AsyncOff."
TEST_FILTER "AsyncCompileScenario.ExtensionStringMatchesTheConfiguration"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_OFF_ENVIRONMENT}"
)
# The optimistic-status quirk's end-to-end shape. Its own entries and not part of the
# AsyncOn. ones because the quirk is not neutral for the rest of the scenario: with it in
# force glGetShaderiv(GL_COMPILE_STATUS) deliberately answers without joining, which is
# exactly what CompletionStatusPollingThenForcedJoin asserts must NOT happen. Off by
# default and never advertised, so - unlike asynchronous compilation, which announces
# itself through the extension string - the variable is the only thing that can tell the
# case it is in force.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.OptimisticShaderStatus."
TEST_FILTER "AsyncCompileScenario.IrisShapedTwoPhaseBatchRendersCorrectly"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_OPTIMISTIC_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.OptimisticShaderStatus."
TEST_FILTER "AsyncCompileScenario.IrisShapedTwoPhaseBatchRendersCorrectly"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_VULKAN_OPTIMISTIC_ENVIRONMENT}"
)
# The negative control for the DirectGLES gl_ViewportIndex emulation, in a process that
# has it switched off. One case, because it is the only one the switch may touch: with
# the emulation off the three positive cases in the same fixture describe behaviour the
# backend does not have, so a whole-scenario registration would be three guaranteed reds.
# DirectGLES only - the flag steers nothing on DirectVulkan, which routes natively.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.NoViewportArrayEmulation."
TEST_FILTER "ViewportArrayScenario.WithoutTheEmulationEveryIndexCollapsesOntoViewportZero"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT}"
)
# PrimitivesGeneratedNoXfbScenario again, with the GL_PRIMITIVES_GENERATED statistics
# reroute PINNED ON. The ambient DirectVulkan registration runs the same cases under the
# bring-up probe's Auto verdict, so between the two entries both accounting paths answer
# the same GL questions and must produce the same numbers - the "two pools must agree"
# gate this machine can hold that the affected device cannot. The pinned entry is also
# the only one whose arming case runs: it asserts the renderer's latched MGLOG_I, so a
# silently-disarmed reroute (an inverted override mapping, a lost gate) fails here
# instead of leaving every equality case vacuously green. DirectVulkan only - the flag
# steers nothing on DirectGLES.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.PrimGenReroute."
TEST_FILTER "PrimitivesGeneratedNoXfbScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_VULKAN_PRIMGEN_REROUTE_ENVIRONMENT}"
)
# The packed16 copy scenarios again, with the 8-bit storage widening PINNED ON. The ambient
# registrations above cover the narrow storage - on every CI driver the widening's POST
# probe finds no field-order mirror, so Auto keeps the native 16-bit path - which means the
# storage every AFFECTED device will actually run would otherwise execute nowhere at all:
# no CI driver has the Mali bug that arms it. This lane is what proves the widened storage
# is client-invisible (same packed words in and out on every leg the 18 failing CTS bodies
# used, the renderbuffer one included). DirectGLES only - the flag steers nothing on
# DirectVulkan, which has always stored these formats widened.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.WidenedPacked16."
TEST_FILTER "CopyImagePacked16Scenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT}"
)
# PointSizeDemotionScenario with the demotion PINNED ON, per backend, for the reason every
# pinned lane above exists: llvmpipe and lavapipe both HOST gl_PointSize in tessellation and
# geometry stages, so the ambient registrations run these captures through the built-in and
# the demotion - the path every affected Mali device actually takes - would execute nowhere.
# The ambient runs stay the negative control: same scenario, same CPU-computed bytes, native
# path. Both backends, because the demotion is shared phase-B work with two different
# consumers (the ESSL capture respelling vs the SPIR-V Xfb carrier binding).
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.PointSizeDemotion."
TEST_FILTER "PointSizeDemotionScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_POINT_SIZE_DEMOTION_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.PointSizeDemotion."
TEST_FILTER "PointSizeDemotionScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_VULKAN_POINT_SIZE_DEMOTION_ENVIRONMENT}"
)
@@ -15,6 +15,16 @@
#include <ostream>
#include <sstream>
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#elif defined(__ANDROID__)
#include <android/hardware_buffer.h>
#include <android/native_window.h>
#include <media/NdkImage.h>
#include <media/NdkImageReader.h>
#endif
// MobileGL's own headers, in the order MobileGL/Includes.h uses them: GL/gl.h
// first, then glcorearb.h for the 3.x+ entry points. This binary links
// MobileGL_s, so every gl*/egl* below binds to MobileGL's implementation, not
@@ -32,7 +42,7 @@
// the only construction that is actually predictive here: MobileGL ABORTS
// (MOBILEGL_ASSERT -> SIGTRAP) rather than returning an error on an unusable
// platform, so nothing the parent can call in-process is allowed to be wrong.
#if !defined(_WIN32) && !defined(__APPLE__) && __has_include(<sys/wait.h>)
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && __has_include(<sys/wait.h>)
#define MGITEST_HAVE_FORK_PREFLIGHT 1
#include <csignal>
#include <ctime>
@@ -53,6 +63,83 @@ namespace MGITest {
constexpr int kSurfaceWidth = 128;
constexpr int kSurfaceHeight = 96;
#if defined(_WIN32)
HWND g_testWindow = nullptr;
HWND CreateTestWindow() {
static const wchar_t* const kClassName = L"MobileGLIntegrationTestWindow";
static bool registered = false;
if (!registered) {
WNDCLASSW windowClass{};
windowClass.lpfnWndProc = DefWindowProcW;
windowClass.hInstance = GetModuleHandleW(nullptr);
windowClass.lpszClassName = kClassName;
if (RegisterClassW(&windowClass) == 0 && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) {
return nullptr;
}
registered = true;
}
return CreateWindowExW(0, kClassName, L"MobileGL Integration Test", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, kSurfaceWidth, kSurfaceHeight, nullptr, nullptr,
GetModuleHandleW(nullptr), nullptr);
}
#elif defined(__ANDROID__)
AImageReader* g_imageReader = nullptr;
ANativeWindow* g_imageReaderWindow = nullptr;
void DrainImageReader(void*, AImageReader* reader) {
AImage* image = nullptr;
if (AImageReader_acquireNextImage(reader, &image) == AMEDIA_OK && image != nullptr) {
AImage_delete(image);
}
}
bool CreateImageReaderWindow() {
if (g_imageReaderWindow != nullptr) return true;
constexpr int kMaxImages = 4;
const media_status_t status = AImageReader_newWithUsage(
kSurfaceWidth, kSurfaceHeight, AIMAGE_FORMAT_RGBA_8888,
AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT,
kMaxImages, &g_imageReader);
if (status != AMEDIA_OK || g_imageReader == nullptr) return false;
AImageReader_ImageListener listener = {nullptr, DrainImageReader};
AImageReader_setImageListener(g_imageReader, &listener);
if (AImageReader_getWindow(g_imageReader, &g_imageReaderWindow) != AMEDIA_OK ||
g_imageReaderWindow == nullptr) {
AImageReader_setImageListener(g_imageReader, nullptr);
AImageReader_delete(g_imageReader);
g_imageReader = nullptr;
return false;
}
ANativeWindow_acquire(g_imageReaderWindow);
return true;
}
void DestroyImageReaderWindow() {
if (g_imageReaderWindow != nullptr) {
ANativeWindow_release(g_imageReaderWindow);
g_imageReaderWindow = nullptr;
}
if (g_imageReader != nullptr) {
AImageReader_setImageListener(g_imageReader, nullptr);
AImageReader_delete(g_imageReader);
g_imageReader = nullptr;
}
}
#endif
bool UseWindowSurface() {
#if defined(_WIN32)
const char* value = std::getenv("MOBILEGL_ITEST_WINDOW_SURFACE");
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
#elif defined(__ANDROID__)
return true;
#else
return false;
#endif
}
std::string EnvOr(const char* name, const char* fallback) {
const char* value = std::getenv(name);
return (value != nullptr && value[0] != '\0') ? std::string(value) : std::string(fallback);
@@ -87,10 +174,10 @@ namespace MGITest {
// callers). surfaceless is the platform with no window-system dependency at
// all; the surface this file then creates is still a pbuffer, which every
// platform supports and which the amendment to this rule requires as the
// fallback shape. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
// fallback shape on desktop. Android instead supplies an AImageReader
// ANativeWindow. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
// driver that consults them directly cannot reintroduce the dependency
// behind EGL's back. Desktop-only file: MG_IntegrationTest never builds
// for Android, so no device path is affected.
// behind EGL's back.
void EnsureHeadlessPlatform() {
#if defined(__linux__) && !defined(__ANDROID__)
static bool done = false;
@@ -134,8 +221,9 @@ namespace MGITest {
return 3;
}
const bool useWindowSurface = UseWindowSurface();
const EGLint configAttribs[] = {EGL_SURFACE_TYPE,
EGL_PBUFFER_BIT,
useWindowSurface ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT,
EGL_RED_SIZE,
8,
EGL_GREEN_SIZE,
@@ -152,7 +240,9 @@ namespace MGITest {
EGLConfig config = nullptr;
EGLint configCount = 0;
if (eglChooseConfig(display, configAttribs, &config, 1, &configCount) != EGL_TRUE || configCount < 1) {
outReason = WithEglError("eglChooseConfig found no pbuffer-capable RGBA8/D24 config");
outReason = WithEglError(useWindowSurface
? "eglChooseConfig found no window-capable RGBA8/D24 config"
: "eglChooseConfig found no pbuffer-capable RGBA8/D24 config");
return 4;
}
@@ -166,10 +256,32 @@ namespace MGITest {
return 5;
}
const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE};
EGLSurface surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
EGLSurface surface = EGL_NO_SURFACE;
if (useWindowSurface) {
#if defined(_WIN32)
if (g_testWindow == nullptr) g_testWindow = CreateTestWindow();
if (g_testWindow == nullptr) {
outReason = "failed to create the Windows integration-test window";
return 6;
}
surface = eglCreateWindowSurface(display, config, g_testWindow, nullptr);
#elif defined(__ANDROID__)
if (!CreateImageReaderWindow()) {
outReason = "failed to create the Android AImageReader integration-test window";
return 6;
}
surface = eglCreateWindowSurface(display, config, g_imageReaderWindow, nullptr);
#endif
} else {
const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE};
surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
}
if (surface == EGL_NO_SURFACE) {
outReason = WithEglError("eglCreatePbufferSurface failed");
#if defined(__ANDROID__)
DestroyImageReaderWindow();
#endif
outReason = WithEglError(useWindowSurface ? "eglCreateWindowSurface failed"
: "eglCreatePbufferSurface failed");
return 6;
}
// The step that brings the whole backend up (DirectVulkan creates its
@@ -440,7 +552,15 @@ namespace MGITest {
// before the pre-flight forks - the child must measure the same platform
// the parent will use.
EnsureHeadlessPlatform();
m_backendName = EnvOr("MOBILEGL_BACKEND_TYPE", "<unset>");
// The backend that is actually about to come up, which is what every
// `BackendName() == "DirectGLES"` gate in the scenarios means by the question.
// MG_ConfigLoader::InitBackendType defaults an unset MOBILEGL_BACKEND_TYPE to
// DirectGLES, so the same default belongs here; this used to report the literal
// "<unset>" instead. Under ctest the variable is always set by the ENVIRONMENT
// property, which is why that never showed - but run straight from a device
// shell, where nothing sets it, DirectGLES came up and every case gated on the
// NAME DirectGLES skipped as though it had not.
m_backendName = EnvOr("MOBILEGL_BACKEND_TYPE", "DirectGLES");
m_usable = BringUp();
}
@@ -491,6 +611,14 @@ namespace MGITest {
if (m_context != nullptr) eglDestroyContext(display, static_cast<EGLContext>(m_context));
if (m_surface != nullptr) eglDestroySurface(display, static_cast<EGLSurface>(m_surface));
eglTerminate(display);
#if defined(_WIN32)
if (g_testWindow != nullptr) {
DestroyWindow(g_testWindow);
g_testWindow = nullptr;
}
#elif defined(__ANDROID__)
DestroyImageReaderWindow();
#endif
m_context = nullptr;
m_surface = nullptr;
m_display = nullptr;
@@ -14,11 +14,11 @@
// inspects backend state - both bugs this module pins were invisible to
// state-level assertions and visible only in pixels.
//
// Headless by construction, following MG_Benchmark/Driver/DriverBench.c: an EGL
// context on a PBUFFER surface. No window, no window manager, no human. Unlike
// DriverBench the scenarios do draw to the DEFAULT framebuffer (that is where
// the Y-flip lives) and do call eglSwapBuffers (that is the frame boundary the
// cross-frame scenarios need to be real).
// Headless by construction: desktop uses an EGL pbuffer and Android uses an
// AImageReader-backed ANativeWindow that needs no Activity. No window manager,
// no human. Unlike DriverBench the scenarios do draw to the DEFAULT framebuffer
// (that is where the Y-flip lives) and do call eglSwapBuffers (that is the frame
// boundary the cross-frame scenarios need to be real).
//
// One process is one backend: MOBILEGL_BACKEND_TYPE is latched at
// initialization, so the CMake wiring runs this binary once per backend rather
@@ -21,12 +21,49 @@
#pragma once
#include <cctype>
#include <cstdlib>
#include <string>
#include <gtest/gtest.h>
#include "HeadlessGL.h"
namespace MGITest {
// How a MOBILEGL_* quirk variable reads in THIS process's environment.
//
// A scenario that needs a non-default configuration takes it from here and skips
// when the process it was launched into is not in that configuration, rather than
// writing MG_Config::Features itself. Two reasons, and the second one decides it:
//
// - the feature table is an internal symbol. On Android this module links against
// the SHIPPING libMobileGL.so - deliberately, so the on-device run validates the
// real artifact - and that library is built -fvisibility=hidden, so nothing
// internal is reachable from here at all.
// - a quirk poked in-process is already too late for everything latched at
// initialization: the compile pool and its threads, and the backend's advertised
// extension list, which is built once from the configuration in force at first
// use. The process-wide variable is the only spelling that covers the whole
// configuration instead of the half of it that is still mutable afterwards.
//
// The reading rule is MG_ConfigLoader's, character for character (ConfigLoader.cpp,
// QueryEnvQuirkOverride / IsTruthyValue): unset is Auto - device auto-detection or a
// built-in default, i.e. a value only the implementation knows - a truthy value is
// On, and anything else that IS set ("0", "false", "") is Off.
enum class AmbientQuirk { Auto, On, Off };
inline AmbientQuirk AmbientQuirkFromEnvironment(const char* name) {
const char* value = std::getenv(name);
if (value == nullptr) return AmbientQuirk::Auto;
std::string lowered(value);
for (char& c : lowered) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
if (lowered.empty() || lowered == "0" || lowered == "false") return AmbientQuirk::Off;
return AmbientQuirk::On;
}
class ScenarioTest : public ::testing::Test {
protected:
void SetUp() override {
+7 -1
View File
@@ -31,8 +31,14 @@ namespace {
// silently bound to a workstation's window system is a different
// run from CI's and must be visible as one in the log.
const char* eglPlatform = std::getenv("EGL_PLATFORM");
std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless, EGL_PLATFORM=%s)\n",
#if defined(__ANDROID__)
constexpr const char* surfaceKind = "AImageReader window";
#else
constexpr const char* surfaceKind = "pbuffer";
#endif
std::fprintf(stderr, " renderer: %s\n surface: %dx%d %s (headless, EGL_PLATFORM=%s)\n",
gl.RendererString().c_str(), gl.Width(), gl.Height(),
surfaceKind,
eglPlatform != nullptr ? eglPlatform : "<unset>");
} else if (MGITest::RequireGpu()) {
std::fprintf(stderr,
@@ -56,7 +56,13 @@ namespace MGITest {
const std::vector<LimitBound>& BufferLimitTable() {
static const std::vector<LimitBound> table = {
{GL_MAX_UNIFORM_BUFFER_BINDINGS, "GL_MAX_UNIFORM_BUFFER_BINDINGS", 36, 256},
// 84 is the GL 4.5 core table 23.64 minimum, and also the width of the state
// layer's indexed-binding array - the two were made to coincide when the array
// was widened from 36, which had made the clamp in GL_Getter degenerate.
{GL_MAX_UNIFORM_BUFFER_BINDINGS, "GL_MAX_UNIFORM_BUFFER_BINDINGS", 84, 256},
// 14 uniform blocks on each of the FIVE graphics stages. The sum used to count
// three, and the two tessellation stages were simply missing from it.
{GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS", 70, 256},
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", 12, 256},
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", 8, 256},
{GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", 8, 256},
@@ -133,6 +139,49 @@ namespace MGITest {
<< relation.blocksName << " = " << blocks << " exceeds " << relation.bindingsName << " = "
<< bindings << "; a shader may declare more blocks than there are binding points to bind them to";
}
// THE MIDDLE TERM, which the relation quoted above always had and this case never
// checked. It is the one that actually broke: widening the binding-point array to 84
// raised what every PER-STAGE count clamps to, while the combined value was a
// five-stage sum of 70 - so a device reporting descriptor-indexing-scale uniform
// buffers (Adreno: maxPerStageDescriptorUniformBuffers = 16777216) advertised 84
// compute uniform blocks inside a combined limit of 70. Per-stage <= combined is
// exactly the assertion that says so, and it costs one glGetIntegerv per row.
struct StageAgainstCombined {
GLenum stage;
const char* stageName;
GLenum combined;
const char* combinedName;
};
const StageAgainstCombined stageRelations[] = {
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_VERTEX_UNIFORM_BLOCKS, "GL_MAX_VERTEX_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS, "GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS",
GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS, "GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS",
GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_GEOMETRY_UNIFORM_BLOCKS, "GL_MAX_GEOMETRY_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_FRAGMENT_UNIFORM_BLOCKS, "GL_MAX_FRAGMENT_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS",
GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS"},
{GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS, "GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS",
GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS"},
};
for (const StageAgainstCombined& relation : stageRelations) {
GLint stage = -1;
GLint combined = -1;
glGetIntegerv(relation.stage, &stage);
glGetIntegerv(relation.combined, &combined);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << relation.stageName;
EXPECT_LE(stage, combined)
<< relation.stageName << " = " << stage << " exceeds " << relation.combinedName << " = "
<< combined << "; GL 4.6 table 23.64 orders MAX_*_BUFFER_BINDINGS >= MAX_COMBINED_*_BLOCKS >= "
"every per-stage count, and a single-stage program may use its whole per-stage allowance";
}
}
// KHR-GL44.multi_bind.functional_bind_buffers_range sizes each of an indexed target's
@@ -199,5 +248,135 @@ namespace MGITest {
"derived component limits are computed in";
}
// The GL 4.5 core minimums that had no case in the getter at all, or that were still
// carrying an ES/GL3.3-tier number. Every one of these answered GL_INVALID_ENUM or a
// too-small value against a context advertising 4.6, and each is the FIRST call its
// conformance case makes - so the case died before it could measure anything.
//
// The cull pair is deliberately absent: zero is a legal answer there (a backend with no
// cull-distance route MUST report it), so it is checked for answerability only, below.
TEST_F(AdvertisedLimitsScenario, EveryGL45CoreMinimumIsMet) {
const std::vector<LimitBound> table = {
{GL_MAX_VARYING_VECTORS, "GL_MAX_VARYING_VECTORS", 15, 256},
{GL_MAX_VERTEX_UNIFORM_VECTORS, "GL_MAX_VERTEX_UNIFORM_VECTORS", 256, 1 << 20},
{GL_MAX_VARYING_COMPONENTS, "GL_MAX_VARYING_COMPONENTS", 60, 1 << 20},
// GL_MAX_VERTEX_STREAMS is deliberately absent. GL 4.5 requires 4 and MobileGL
// answers 1, which is a KNOWN non-conformance rather than an oversight: raising
// the number un-gates two transform-feedback CTS cases per package across
// KHR-GL40..GL46 that then fail, because no part of the shader pipeline supports
// layout(stream = N). See the GL_MAX_VERTEX_STREAMS case in GL_Getter.cpp. Adding
// a row here would pin a number the implementation cannot back.
{GL_MAX_GEOMETRY_SHADER_INVOCATIONS, "GL_MAX_GEOMETRY_SHADER_INVOCATIONS", 32, 256},
{GL_MAX_SUBROUTINES, "GL_MAX_SUBROUTINES", 256, 1 << 20},
{GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS, "GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS", 1024, 1 << 20},
{GL_MAX_TESS_CONTROL_INPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_INPUT_COMPONENTS", 128, 1 << 16},
{GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS", 128, 1 << 16},
{GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS", 4096,
1 << 20},
{GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS, "GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS", 16, 256},
{GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS, "GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS", 1024, 1 << 20},
{GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS, "GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS", 14, 256},
{GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS, "GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS", 128, 1 << 16},
{GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS, "GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS", 128, 1 << 16},
{GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS, "GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS", 16, 256},
{GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS, "GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS", 1024,
1 << 20},
{GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS, "GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS", 14, 256},
{GL_MAX_TESS_PATCH_COMPONENTS, "GL_MAX_TESS_PATCH_COMPONENTS", 120, 1 << 16},
{GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS, "GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS",
58368, 1 << 30},
{GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS,
"GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS", 58368, 1 << 30},
};
for (const LimitBound& bound : table) {
GLint value = -424242;
glGetIntegerv(bound.pname, &value);
const unsigned int error = FirstGLError();
EXPECT_EQ(error, GLenum(GL_NO_ERROR)) << bound.name << " is not answerable: " << GLErrorName(error);
if (error != GL_NO_ERROR) continue;
EXPECT_GE(value, bound.minimum) << bound.name << " = " << value << " is below the GL 4.5 minimum "
<< bound.minimum;
EXPECT_LE(value, bound.ceiling) << bound.name << " = " << value << " exceeds the ceiling "
<< bound.ceiling;
}
// ARB_cull_distance's pair. Zero is honest on a backend with no cull-distance route,
// so only answerability and the combined-limit ordering are checked here.
GLint cull = -1;
GLint clip = -1;
GLint combined = -1;
glGetIntegerv(GL_MAX_CULL_DISTANCES, &cull);
glGetIntegerv(GL_MAX_CLIP_DISTANCES, &clip);
glGetIntegerv(GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, &combined);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the ARB_cull_distance queries must not error";
EXPECT_GE(cull, 0);
EXPECT_GE(combined, cull) << "GL 4.6 core 11.1.3.10: the combined limit is at least the cull one";
EXPECT_GE(combined, clip) << "GL 4.6 core 11.1.3.10: the combined limit is at least the clip one";
// GL_MAX_ELEMENT_INDEX is 64-bit state: the required 2^32-1 does not fit a GLint, so
// the wide query must answer it and the narrow one must saturate rather than wrap.
GLint64 elementIndex = -1;
glGetInteger64v(GL_MAX_ELEMENT_INDEX, &elementIndex);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_GE(elementIndex, static_cast<GLint64>(4294967295LL))
<< "GL 4.5 core table 23.55 sets the GL_MAX_ELEMENT_INDEX minimum at 2^32-1";
}
// ARB_viewport_array's own limits. They are advertised from three different places -
// GL_MAX_VIEWPORTS from the frontend's indexed state width, the bounds range and the
// subpixel bits from the backend caps table - and each backend fills that table from a
// different source, so all three are checked on both lanes.
//
// GL_VIEWPORT_BOUNDS_RANGE is the one that shipped wrong: GLES has no such query, the
// DirectGLES loader's glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE) therefore raised
// GL_INVALID_ENUM and left the probe's zero-initialized array in place, and MobileGL
// advertised [0, 0] - a range that admits no viewport origin at all, and the check that
// kept KHR-GL43.viewport_array.queries red on Espryt after the indexed-state work.
TEST_F(AdvertisedLimitsScenario, ViewportArrayLimitsMeetTheirGL43Floors) {
GLint maxViewports = -1;
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_GE(maxViewports, 16) << "GL 4.3 core table 23.53 sets the MAX_VIEWPORTS minimum at 16";
EXPECT_LE(maxViewports, 256) << "one viewport rectangle of indexed state is allocated per advertised "
"viewport, and the CTS sizes its arrays off this number";
GLfloat boundsRange[2] = {1.0f, -1.0f};
glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE, boundsRange);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_LE(boundsRange[0], -32768.0f)
<< "GL 4.6 core table 23.60 sets the VIEWPORT_BOUNDS_RANGE minimum at [-32768, 32767]; got ["
<< boundsRange[0] << ", " << boundsRange[1] << "]";
EXPECT_GE(boundsRange[1], 32767.0f)
<< "GL 4.6 core table 23.60 sets the VIEWPORT_BOUNDS_RANGE minimum at [-32768, 32767]; got ["
<< boundsRange[0] << ", " << boundsRange[1] << "]";
// KNOWN INFIDELITY, pinned here rather than hidden. MobileGL reports the driver's own
// VIEWPORT_SUBPIXEL_BITS (4 on llvmpipe, i.e. 1/16-pixel viewport precision), but the
// float viewport rectangle glViewportIndexedf stores is snapped to integers on its
// way to both backends (ComputeGLViewport, DirectGLES SyncRenderState). The STATE
// round trip is exact - which is all KHR-GL43.viewport_array.viewport_api checks, and
// all this cluster set out to fix - so the gap is in rasterization only: a fractional
// viewport origin rasterizes as if it had been rounded. Nothing in the suite or in
// Minecraft sets one. Only the spec floor is asserted; tightening this to EQ(0) would
// mean advertising no subpixel precision at all, which is a separate decision about a
// limit MobileGL currently passes through from the driver.
GLint subpixelBits = -1;
glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &subpixelBits);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_GE(subpixelBits, 0) << "GL 4.6 core table 23.60: VIEWPORT_SUBPIXEL_BITS has a minimum of 0, and "
"a negative value is what a sign-flipped uint32 looks like";
GLint viewportDims[2] = {-1, -1};
glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewportDims);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
GLint maxRenderbufferSize = -1;
glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maxRenderbufferSize);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
// GL 4.6 core 13.6.1: MAX_VIEWPORT_DIMS must be at least as large as the largest
// renderable surface, or a full-size framebuffer could not be fully viewported.
EXPECT_GE(viewportDims[0], maxRenderbufferSize);
EXPECT_GE(viewportDims[1], maxRenderbufferSize);
}
} // namespace
} // namespace MGITest
@@ -25,10 +25,12 @@
// be able to turn this into a red.
// (b) Forcing the join afterwards produces the right answer for every one of them:
// GL_COMPILE_STATUS true, an empty info log, and a program that links.
// (c) The extension string matches the configuration. This is the half a recorded
// trace can never cover - Iris and Sodium change their submission schedule the
// moment they see the string - so it is asserted against a real backend's real
// GL_EXTENSIONS, through both glGetString and glGetStringi.
// (c) The extension string matches the configuration - where "the configuration" is
// MOBILEGL_ASYNC_SHADER_COMPILE as this process inherited it, and NOT anything the
// implementation says about itself. This is the half a recorded trace can never
// cover - Iris and Sodium change their submission schedule the moment they see the
// string - so it is asserted against a real backend's real GL_EXTENSIONS, through
// both glGetString and glGetStringi.
// (d) glMaxShaderCompilerThreadsKHR(0) leaves nothing in flight: every subsequent
// GL_COMPLETION_STATUS_KHR reads GL_TRUE immediately, and compilation after it
// is synchronous. That is what the extension requires of a zero count.
@@ -40,6 +42,27 @@
//
// Backend selection is the module's usual one process, one backend (MOBILEGL_BACKEND_TYPE),
// so this file runs twice per ctest invocation.
//
// COMPILATION MODE IS PER PROCESS TOO. Every case here needs a particular configuration of
// MobileGL's shader compiler, and takes it from the ENVIRONMENT
// (MOBILEGL_ASYNC_SHADER_COMPILE, MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS) rather than by
// writing MG_Config::Features on the way past. Half of what those variables decide is
// latched before the first GL call - the compile pool and its threads, and the advertised
// extension list a backend builds once from the configuration in force at its first use -
// so an in-process poke could only ever have moved the other half; and on Android it could
// move nothing at all, because this module links against the shipping libMobileGL.so, which
// exports no such symbol. A case whose process is not in the configuration it needs SKIPS
// with that as its reason. CMakeLists.txt registers the extra ctest entries that put a
// process into each configuration (AsyncOn., AsyncOff., OptimisticShaderStatus.), so one
// ctest run still covers both sides of every switch. Run straight from a shell with nothing
// set - the on-device shape - the ambient configuration runs and the rest skip cleanly.
//
// WITHIN one process, "compiled on a worker" versus "compiled on this thread" is switched
// through glMaxShaderCompilerThreadsKHR, the extension's own entry point: a zero count joins
// everything outstanding and compiles inline from then on, any nonzero count lifts that
// again, and 0xFFFFFFFF asks for the implementation maximum (GL_Program.cpp,
// MaxShaderCompilerThreadsKHR_State). Doing it through the public call rather than the
// feature table means the switching is itself part of what these cases exercise.
#include <string>
#include <vector>
@@ -47,9 +70,6 @@
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#include "Config.h"
#include "MG_Util/Async/ShaderCompilePool.h"
#ifdef GLAPI
#undef GLAPI
#endif
@@ -76,8 +96,6 @@ extern "C" void glMaxShaderCompilerThreadsKHR(GLuint count);
namespace MGITest {
namespace {
using MobileGL::MG_Config::QuirkOverride;
// Same shape as the other scenarios: a two-attribute pass-through, so the only
// thing that can differ between the two compilation modes is the compilation.
constexpr const char* kVertexSource = R"(#version 330 core
@@ -139,50 +157,40 @@ void main() {
return source;
}
// MOBILEGL_ASYNC_SHADER_COMPILE decides the ambient mode; a scenario that wants
// the other one says so here and gets the ambient one back on scope exit. Forcing
// it in-process is what lets ONE ctest run compare the two modes against each
// other - the whole point of (e).
class AsyncModeScope {
public:
explicit AsyncModeScope(bool async) : m_saved(MobileGL::MG_Config::Features.AsyncShaderCompile) {
MobileGL::MG_Config::Features.AsyncShaderCompile =
async ? QuirkOverride::ForceOn : QuirkOverride::ForceOff;
// Whether this context advertises GL_KHR_parallel_shader_compile, which is exactly
// "MobileGL is configured to compile asynchronously" as an application can see it:
// the backends gate the string on AsyncShaderCompileEnabled() and on nothing else
// (BackendObject_DirectGLES.cpp / BackendObject_DirectVulkan.cpp), and the string
// is the only way MobileGL ever tells anyone. A case that needs asynchronous
// compilation checks for it the way an application would, and skips without it.
//
// The INDEXED form, because that is the one a core-profile application reads.
bool HasParallelShaderCompile() {
GLint count = 0;
glGetIntegerv(GL_NUM_EXTENSIONS, &count);
for (GLint i = 0; i < count; ++i) {
const char* name = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, GLuint(i)));
if (name != nullptr && std::string(name) == "GL_KHR_parallel_shader_compile") return true;
}
~AsyncModeScope() { MobileGL::MG_Config::Features.AsyncShaderCompile = m_saved; }
AsyncModeScope(const AsyncModeScope&) = delete;
AsyncModeScope& operator=(const AsyncModeScope&) = delete;
return false;
}
private:
const QuirkOverride m_saved;
};
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS, forced in-process for the same reason
// as AsyncModeScope: one ctest run asserts the quirk against the ambient default.
class OptimisticStatusScope {
public:
explicit OptimisticStatusScope(const QuirkOverride mode)
: m_saved(MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus) {
MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus = mode;
}
~OptimisticStatusScope() { MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus = m_saved; }
OptimisticStatusScope(const OptimisticStatusScope&) = delete;
OptimisticStatusScope& operator=(const OptimisticStatusScope&) = delete;
private:
const QuirkOverride m_saved;
};
// glMaxShaderCompilerThreadsKHR writes process-wide state; a scenario that calls
// it has to put the pool back or it changes how every scenario after it compiles.
// glMaxShaderCompilerThreadsKHR writes process-wide state; a scenario that calls it
// has to put the pool back or it changes how every scenario after it compiles.
//
// The restore is the extension's own "implementation maximum" spelling rather than a
// hand-rolled poke at the pool. glMaxShaderCompilerThreadsKHR(0xFFFFFFFF) is defined
// (GL_Program.cpp, MaxShaderCompilerThreadsKHR_State) as precisely the two steps this
// used to perform through internal entry points - concurrency := the pool's full
// thread count, then lift any suspension a zero count had armed - in the safer order,
// since it raises the budget before re-admitting work rather than after. Going through
// the public call also puts the restore path itself under test, and it is the only
// spelling available on Android, where this module links the shipping shared library
// and can reach nothing but the GL entry points.
class CompilerThreadScope {
public:
CompilerThreadScope() = default;
~CompilerThreadScope() {
MobileGL::MG_Util::Async::SetAsyncShaderCompileSuspended(false);
auto& pool = MobileGL::MG_Util::Async::ShaderCompilePool::Get();
pool.SetMaxConcurrency(pool.GetThreadCount());
}
~CompilerThreadScope() { glMaxShaderCompilerThreadsKHR(0xFFFFFFFFu); }
CompilerThreadScope(const CompilerThreadScope&) = delete;
CompilerThreadScope& operator=(const CompilerThreadScope&) = delete;
};
@@ -293,7 +301,12 @@ void main() {
// interesting for shaders that (a) proved were genuinely still outstanding.
TEST_F(AsyncCompileScenario, CompletionStatusPollingThenForcedJoin) {
if (!Ready()) return;
const AsyncModeScope async(true);
if (!HasParallelShaderCompile()) {
GTEST_SKIP() << "this process is configured to compile inline "
"(GL_KHR_parallel_shader_compile is not advertised), so no compile can be "
"outstanding; the AsyncOn. ctest entries run this case with "
"MOBILEGL_ASYNC_SHADER_COMPILE=1";
}
const CompilerThreadScope threads;
// One worker, so the queue behind it is what the poll observes.
glMaxShaderCompilerThreadsKHR(1);
@@ -341,14 +354,33 @@ void main() {
}
// ---- (c) ------------------------------------------------------------------
// The extension string, read from a real backend that really brought a driver
// up. No mode forcing here: a backend builds its advertised list once, from the
// configuration in force at its first use, so the meaningful assertion is
// against the AMBIENT configuration - which is exactly what makes this case
// worth running in both of the suite's flag states.
// The extension string, read from a real backend that really brought a driver up.
//
// The expectation comes from the ENVIRONMENT, never from the implementation. This
// case used to derive it by calling AsyncShaderCompileEnabled() - which is the same
// function the backends gate the string on, so the two halves could only ever agree
// and the case would have passed however wrong both of them were. Asserting an
// implementation against itself pins nothing.
//
// MOBILEGL_ASYNC_SHADER_COMPILE is the whole input: the process inherited it before
// any GL call, a backend builds its advertised list once from the configuration in
// force at first use, and nothing in this process can move it afterwards. So reading
// the variable IS reading the configuration, independently. With the variable unset
// the configuration in force is MobileGL's built-in default, which only the
// implementation knows - there is nothing independent left to compare against, and
// this case says so rather than inventing an expectation. The AsyncOn. and AsyncOff.
// ctest entries pin the variable to each of its two values, so one ctest run still
// asserts both the advertised and the withdrawn side.
TEST_F(AsyncCompileScenario, ExtensionStringMatchesTheConfiguration) {
if (!Ready()) return;
const bool expected = MobileGL::MG_Util::Async::AsyncShaderCompileEnabled();
const AmbientQuirk configured = AmbientQuirkFromEnvironment("MOBILEGL_ASYNC_SHADER_COMPILE");
if (configured == AmbientQuirk::Auto) {
GTEST_SKIP() << "MOBILEGL_ASYNC_SHADER_COMPILE is unset, so the configuration in force is "
"MobileGL's built-in default and the only way to learn it would be to ask "
"the implementation this case exists to check; the AsyncOn. and AsyncOff. "
"ctest entries run it with the variable pinned to each of its two values";
}
const bool expected = configured == AmbientQuirk::On;
const char* extensions = reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS));
ASSERT_NE(extensions, nullptr);
@@ -385,7 +417,12 @@ void main() {
// A zero count must leave nothing in flight and keep it that way.
TEST_F(AsyncCompileScenario, ZeroCompilerThreadsSettlesEverythingImmediately) {
if (!Ready()) return;
const AsyncModeScope async(true);
if (!HasParallelShaderCompile()) {
GTEST_SKIP() << "this process is configured to compile inline "
"(GL_KHR_parallel_shader_compile is not advertised), so a zero count has "
"nothing to settle; the AsyncOn. ctest entries run this case with "
"MOBILEGL_ASYNC_SHADER_COMPILE=1";
}
const CompilerThreadScope threads;
glMaxShaderCompilerThreadsKHR(1);
@@ -417,12 +454,28 @@ void main() {
// Compared through the DEFAULT framebuffer deliberately: that is where the
// backend's orientation and present path live, so the comparison covers the
// whole pipeline rather than the reflection tables alone.
//
// The two modes are selected through glMaxShaderCompilerThreadsKHR, the extension's
// own entry point, rather than through the feature table: a zero count joins
// everything outstanding and makes every later glCompileShader/glLinkProgram run its
// body on the calling thread, and 0xFFFFFFFF lifts that again with the pool at its
// full thread count (GL_Program.cpp, MaxShaderCompilerThreadsKHR_State; the compile
// and link paths both gate on AsyncShaderCompileActive(), which is what the zero
// count switches). So this is still one process comparing worker-built artifacts
// against inline-built ones - just asked for the way an application asks.
TEST_F(AsyncCompileScenario, AsyncAndSyncProgramsRenderIdenticalFrames) {
if (!Ready()) return;
if (!HasParallelShaderCompile()) {
GTEST_SKIP() << "this process is configured to compile inline "
"(GL_KHR_parallel_shader_compile is not advertised), so both halves would "
"be the same inline build and the comparison would be vacuous; the "
"AsyncOn. ctest entries run this case with MOBILEGL_ASYNC_SHADER_COMPILE=1";
}
const CompilerThreadScope threads;
Image asyncImage;
{
const AsyncModeScope async(true);
glMaxShaderCompilerThreadsKHR(0xFFFFFFFFu);
const GLuint program = BuildProgram();
ASSERT_NE(program, 0u);
asyncImage = DrawFrameWith(program);
@@ -431,7 +484,7 @@ void main() {
Image syncImage;
{
const AsyncModeScope async(false);
glMaxShaderCompilerThreadsKHR(0);
const GLuint program = BuildProgram();
ASSERT_NE(program, 0u);
syncImage = DrawFrameWith(program);
@@ -456,11 +509,16 @@ void main() {
// candidate) shows up here and not in the single-program case above.
TEST_F(AsyncCompileScenario, ABatchOfAsyncProgramsAllRenderCorrectly) {
if (!Ready()) return;
if (!HasParallelShaderCompile()) {
GTEST_SKIP() << "this process is configured to compile inline "
"(GL_KHR_parallel_shader_compile is not advertised), so nothing would be "
"built on a worker and there is no per-worker state to leak; the AsyncOn. "
"ctest entries run this case with MOBILEGL_ASYNC_SHADER_COMPILE=1";
}
constexpr int kPrograms = 12;
std::vector<GLuint> programs;
{
const AsyncModeScope async(true);
const CompilerThreadScope threads;
glMaxShaderCompilerThreadsKHR(1);
// Everything enqueued before anything is read: the only shape in which
@@ -489,6 +547,21 @@ void main() {
// then mis-renders - shows up here as a wrong quadrant signature.
TEST_F(AsyncCompileScenario, IrisShapedTwoPhaseBatchRendersCorrectly) {
if (!Ready()) return;
// The quirk is off by default and never advertised, so unlike the cases above
// there is no GL observable that says whether it is in force - only the variable
// that put it there. It also has to be set BEFORE this process started for the
// shape to be the real one: the optimistic answer is latched per compile, and a
// quirk switched on mid-process would only cover the compiles after it.
if (AmbientQuirkFromEnvironment("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS") != AmbientQuirk::On) {
GTEST_SKIP() << "this case is the optimistic-status quirk's end-to-end shape and needs it on "
"for the whole process; the OptimisticShaderStatus. ctest entries run it with "
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1";
}
if (!HasParallelShaderCompile()) {
GTEST_SKIP() << "the optimistic status only ever applies to a compile that is still in flight "
"(OptimisticShaderStatusActive() requires AsyncShaderCompileActive()), and "
"this process is configured to compile inline";
}
constexpr int kPrograms = 12;
// Distinct per program (so neither the source memo nor the adoption map turns
@@ -508,8 +581,6 @@ void main() {
std::vector<GLuint> programs;
{
const AsyncModeScope async(true);
const OptimisticStatusScope quirk(QuirkOverride::ForceOn);
const CompilerThreadScope threads;
glMaxShaderCompilerThreadsKHR(1);
@@ -0,0 +1,267 @@
// 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";
}
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;
}
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";
}
// A CPU glBufferSubData issued AFTER a dispatch, read back with NO further GPU work in
// between. Each backend has its own way to invert this pair, and both are pinned here.
// DirectGLES queues app SubData ranges for the draw-time staged-copy flush (the upload
// ring) instead of uploading in place, and readback of a GPU-written buffer overwrites
// the frontend shadow with the driver copy - so if the readback path forgets to flush the
// queued range first, the newer CPU write is REVERTED by the readback and offset 0 reads
// the dispatch's value instead of the reseed. DirectVulkan adopts the buffer into
// coherent GPU memory the moment the dispatch resolves its descriptor, so the SubData
// write lands in the very bytes the GPU reads - while the dispatch still sits recorded in
// the deferred frame command buffer. Unless the frontend retires that pending work before
// writing the adopted store (BufferObject::UploadSubData), the dispatch executes ON TOP
// of the reseed and offset 0 reads reseed + increments instead of the reseed. Offset 4
// pins the other direction for both: the upload must leave bytes outside its range - the
// dispatch's results - untouched.
TEST_F(AtomicCounterScenario, SubDataAfterDispatchSurvivesAnImmediateReadback) {
if (!Ready() || IsSkipped()) return;
const GLuint zero = MakeCounterBuffer(0, {0u, 0u});
MakeCounterBuffer(1, {0u});
ASSERT_EQ(FirstGLError(), 0u);
Dispatch();
const unsigned int reseed = 4242u;
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";
const std::vector<unsigned int> values = ReadCounters(zero, 2);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(values[0], reseed)
<< "offset 0 read back " << values[0] << "; the dispatch's value (" << kInvocations
<< ") means the readback ran before the queued SubData range was flushed and reverted it";
EXPECT_EQ(values[1], 2 * kInvocations)
<< "offset 4 read back " << values[1] << "; the SubData flush must leave bytes outside its "
<< "range untouched";
}
} // namespace MGITest
@@ -299,4 +299,99 @@ void main() {
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
@@ -0,0 +1,250 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClearTexImageUndefinedLevelZeroScenario.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 - glClearTexImage ON A TEXTURE WHOSE GL LEVEL 0 WAS NEVER DEFINED.
//
// KHR-GL4[456].clear_tex_image.* builds exactly one shape: fillTexture() issues ONE
// glTexImage2D(GL_TEXTURE_2D, m_texLevel, ...) - the only texImage2D in the whole format/level
// family - sets GL_TEXTURE_MAX_LEVEL to that level, clears it and reads it back with
// glGetTexImage(..., m_texLevel, ...). For m_texLevel > 0 the levels BELOW the defined one have no
// storage at all, and the split in the conformance results was on that alone: every texLevel_0 body
// passed on DirectVulkan and every texLevel != 0 body failed, across all four internal formats and
// all three entry points.
//
// The frontend understands this shape - the clear is a pure CPU-shadow write, and
// ValidateTextureImageQuery deliberately does not demand mip completeness for a readback. The
// Vulkan backend did not: VkTextureManager takes storage mip 0 as the physical image extent, so a
// texture with no level 0 got no VkImage, SyncTextureAndGetDescriptor answered nullptr, and
// VulkanRenderer::GetTextureImage took a silent early return - leaving the caller's buffer exactly
// as it found it. The conformance failures carried no <Text> at all, because nothing raised a GL
// error: the destination was simply never written, so the test compared its own zero-initialized
// buffer against the clear value.
//
// The fix this pins is the readback fallback: with NO VkImage, nothing GPU-side can ever have
// written the texture, so the CPU shadow IS its content and is the correct answer. It is gated on
// "no image exists at all" and not on "syncing was inconvenient - a blanket shadow answer would
// return stale bytes for every render-to-texture result instead.
//
// NOT covered here, and deliberately: such a texture still has no VkImage, so it remains invisible
// to SAMPLING and rendering on DirectVulkan. Backing the image from the lowest defined level is a
// separate change (it moves every GL-level-to-subresource translation in the backend); this
// scenario asserts the readback contract only, and the DirectGLES leg - which has always been able
// to define a lone level N - is the built-in control for what the answer should be.
#include <array>
#include <cstdint>
#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 conformance family's own shape: a mid-chain level of a texture that has nothing else.
constexpr GLint kDefinedLevel = 3;
constexpr GLsizei kLevelExtent = 8;
struct Texel8 {
GLubyte r = 0, g = 0, b = 0, a = 0;
bool operator==(const Texel8& other) const {
return r == other.r && g == other.g && b == other.b && a == other.a;
}
};
std::ostream& operator<<(std::ostream& os, const Texel8& c) {
return os << "rgba(" << int(c.r) << "," << int(c.g) << "," << int(c.b) << "," << int(c.a) << ")";
}
// The conformance test's clear value is a single repeated component; 5 is what it uses, and
// it is deliberately neither 0 (an unwritten destination) nor 255 (a saturated one).
constexpr Texel8 kClearValue{5, 5, 5, 5};
constexpr Texel8 kInitialValue{200, 100, 50, 255};
class ClearTexImageUndefinedLevelZeroScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
DrainErrors();
}
void TearDown() override {
if (!Ready()) return;
if (m_texture != 0) {
glBindTexture(GL_TEXTURE_2D, 0);
glDeleteTextures(1, &m_texture);
m_texture = 0;
}
DrainErrors();
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
// One level and nothing else, through glTexImage2D - deliberately NOT glTexStorage2D,
// which would define the whole chain and could not express "level 0 does not exist".
void MakeTextureWithOnlyLevel(GLint level) {
if (m_texture != 0) glDeleteTextures(1, &m_texture);
glGenTextures(1, &m_texture);
glBindTexture(GL_TEXTURE_2D, m_texture);
const std::vector<Texel8> initial(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, kInitialValue);
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, kLevelExtent, kLevelExtent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
initial.data());
// What the conformance case does: MAX_LEVEL names the one level that exists, and
// BASE_LEVEL is left at its default 0 - which is what makes level 0 undefined AND
// nominally the base level, the shape the backend could not express.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
ASSERT_EQ(FirstGLError(), 0u) << "texture setup with only level " << level;
}
std::vector<Texel8> ReadLevel(GLint level) {
std::vector<Texel8> pixels(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, Texel8{0, 0, 0, 0});
glBindTexture(GL_TEXTURE_2D, m_texture);
glGetTexImage(GL_TEXTURE_2D, level, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
EXPECT_EQ(FirstGLError(), 0u) << "glGetTexImage(level " << level << ") left a GL error behind";
return pixels;
}
void ExpectAllTexels(const char* what, const std::vector<Texel8>& pixels, Texel8 expected) {
std::size_t offenders = 0;
Texel8 firstBad{};
for (const Texel8& pixel : pixels) {
if (pixel == expected) continue;
if (offenders == 0) firstBad = pixel;
++offenders;
}
EXPECT_EQ(offenders, 0u) << what << ": got " << firstBad << " instead of " << expected << " ("
<< offenders << " of " << pixels.size() << " texels wrong)";
}
// Level 0 defined, a GAP, then `level` defined. GL keeps the intervening levels at a zero
// extent, so the backend's mip walk stops at the gap and the VkImage ends up with FEWER
// mip levels than the GL level count - which is a different shape from "no image at all"
// and is why the readback has to bound the level against the IMAGE.
void MakeTextureWithAGapBefore(GLint level) {
if (m_texture != 0) glDeleteTextures(1, &m_texture);
glGenTextures(1, &m_texture);
glBindTexture(GL_TEXTURE_2D, m_texture);
const std::vector<Texel8> base(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, kInitialValue);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kLevelExtent, kLevelExtent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
base.data());
const std::vector<Texel8> gapped(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, kInitialValue);
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, kLevelExtent, kLevelExtent, 0, GL_RGBA,
GL_UNSIGNED_BYTE, gapped.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
ASSERT_EQ(FirstGLError(), 0u) << "texture setup with a gap before level " << level;
}
GLuint m_texture = 0;
};
} // namespace
// The regression. Before the fix glGetTexImage wrote nothing at all on DirectVulkan, so the
// caller's buffer kept whatever it already held - which is why the conformance failures showed
// the test's own zero-initialized memory and carried no GL error.
TEST_F(ClearTexImageUndefinedLevelZeroScenario, ClearAndReadBackALevelWhoseLowerLevelsDoNotExist) {
if (!Ready()) GTEST_SKIP();
MakeTextureWithOnlyLevel(kDefinedLevel);
// Pre-flight: the level reads back as what was uploaded. This is what makes the assertion
// after the clear falsifiable - without it, a readback that silently wrote nothing could not
// be told from one that wrote the right answer.
ExpectAllTexels("before the clear", ReadLevel(kDefinedLevel), kInitialValue);
glClearTexImage(m_texture, kDefinedLevel, GL_RGBA, GL_UNSIGNED_BYTE, &kClearValue);
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexImage was rejected";
ExpectAllTexels("after the clear", ReadLevel(kDefinedLevel), kClearValue);
Gl().EndFrame();
}
// The same shape through glClearTexSubImage, which is a separate entry point in the conformance
// family and failed on exactly the same bodies.
TEST_F(ClearTexImageUndefinedLevelZeroScenario, ClearSubImageOfALevelWhoseLowerLevelsDoNotExist) {
if (!Ready()) GTEST_SKIP();
MakeTextureWithOnlyLevel(kDefinedLevel);
glClearTexSubImage(m_texture, kDefinedLevel, 0, 0, 0, kLevelExtent, kLevelExtent, 1, GL_RGBA,
GL_UNSIGNED_BYTE, &kClearValue);
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexSubImage was rejected";
ExpectAllTexels("after the sub-image clear", ReadLevel(kDefinedLevel), kClearValue);
Gl().EndFrame();
}
// The negative control: an ORDINARY texture, whose level 0 does exist, must keep answering from
// the GPU image rather than being diverted onto the shadow. A fallback that fired unconditionally
// would pass the two tests above and this one too - but it would also hand back stale bytes for
// anything the GPU had written, which is why the partial-clear check below matters: the readback
// has to see a region the backend cleared and a region it did not, in one image.
TEST_F(ClearTexImageUndefinedLevelZeroScenario, AnOrdinaryLevelZeroTextureStillReadsBackCorrectly) {
if (!Ready()) GTEST_SKIP();
MakeTextureWithOnlyLevel(0);
ExpectAllTexels("before the clear", ReadLevel(0), kInitialValue);
// Clear only the left half, so the answer is neither "all initial" nor "all cleared".
glClearTexSubImage(m_texture, 0, 0, 0, 0, kLevelExtent / 2, kLevelExtent, 1, GL_RGBA, GL_UNSIGNED_BYTE,
&kClearValue);
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexSubImage was rejected";
const std::vector<Texel8> pixels = ReadLevel(0);
ASSERT_EQ(pixels.size(), static_cast<std::size_t>(kLevelExtent) * kLevelExtent);
for (int y = 0; y < kLevelExtent; ++y) {
for (int x = 0; x < kLevelExtent; ++x) {
const Texel8 expected = x < kLevelExtent / 2 ? kClearValue : kInitialValue;
const Texel8 actual = pixels[static_cast<std::size_t>(y) * kLevelExtent + x];
ASSERT_EQ(actual, expected) << "at (" << x << "," << y << ")";
}
}
Gl().EndFrame();
}
// The adjacent shape the first fix did NOT cover: level 0 defined, a gap, then the level being
// read. This one DOES get a VkImage - just one with fewer mip levels than GL thinks the texture
// has - so the "no VkImage" test passes and the GL level was written straight into
// imageSubresource.mipLevel and into a VkImageMemoryBarrier's baseMipLevel. An out-of-range
// subresource is a promise the driver takes at face value; the glCopyImageSubData path two
// functions away grew the same guard after it SIGSEGV'd inside the Adreno driver.
//
// The level being read really does hold its own data (the shadow is its only copy, since nothing
// ever uploaded it), so the correct answer is the uploaded bytes - not a decline.
TEST_F(ClearTexImageUndefinedLevelZeroScenario, ReadBackALevelSeparatedFromLevelZeroByAGap) {
if (!Ready()) GTEST_SKIP();
MakeTextureWithAGapBefore(kDefinedLevel);
ExpectAllTexels("before the clear", ReadLevel(kDefinedLevel), kInitialValue);
glClearTexImage(m_texture, kDefinedLevel, GL_RGBA, GL_UNSIGNED_BYTE, &kClearValue);
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexImage was rejected";
ExpectAllTexels("after the clear", ReadLevel(kDefinedLevel), kClearValue);
// Level 0 is backed by the real image and must still read back from it, so the level bound is
// about the level and not about the texture.
ExpectAllTexels("level 0 after clearing level 3", ReadLevel(0), kInitialValue);
Gl().EndFrame();
}
} // 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);
}
// 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
// XfbAfterClipDistanceScenario leaving one on for the rest of the process.
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.
TEST_F(ClipDistanceScenario, AnEnabledClipDistanceRemovesTheNegativeHalf) {
if (!Ready()) return;
if (!BackendHostsTwoClipDistances()) {
GTEST_SKIP() << "this backend advertises no clip distances, so there is nothing to clip with";
}
HeadlessGL& gl = Gl();
const int width = gl.Width();
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.
TEST_F(ClipDistanceScenario, ADisabledClipDistanceRemovesNothing) {
if (!Ready()) return;
if (!BackendHostsTwoClipDistances()) {
GTEST_SKIP() << "this backend advertises no clip distances, so there is nothing to clip with";
}
HeadlessGL& gl = Gl();
const int width = gl.Width();
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.
TEST_F(ClipDistanceScenario, TheEnablesAreIndependentPerDistance) {
if (!Ready()) return;
if (!BackendHostsTwoClipDistances()) {
GTEST_SKIP() << "this backend advertises no clip distances, so there is nothing to clip with";
}
HeadlessGL& gl = Gl();
const int width = gl.Width();
const int height = gl.Height();
@@ -0,0 +1,375 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.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 - glCopyImageSubData PRESERVES 16-BIT PACKED WORDS ACROSS AN ARRAY MIP LEVEL.
//
// The shape is lifted verbatim from the 18 Espryt bodies of KHR-GL4x.copy_image.functional
// that survived every earlier wave: the three internal formats MobileGL can keep as 16-bit
// packed ES storage - GL_RGB5 (stored GL_RGB565), GL_RGB5_A1, GL_RGBA4 - crossed with the
// target pairs that put a GL_TEXTURE_2D_ARRAY's MIP LEVEL 1 on one side of the copy. On the
// affected Mali the mirrored *_REV field order is a property of WHOLE ALLOCATIONS (shape-
// and context-dependent; the failing 30x30x12 arrays carry it at every level, the small
// arrays of the suite's passing iterations do not), and glCopyImageSubData - a raw
// texel-block move - between a mirrored allocation and a plain one lands the fields
// reversed: src word 0x0047 arrives as 0x8C20 (its 5_5_5_1 -> 1_5_5_5_REV re-encoding),
// 0x0007 as 0x3800, byte-exact on every failing body. Uploads and readbacks of the same
// image are clean (the driver decodes its own layout consistently), which is why only the
// copy path ever crossed the two layouts and why the CTS's "source image was not modified"
// checks always passed.
//
// The array is 30x30x12 with THREE levels and the flat endpoint is 7x7 with three levels
// (7/3/1) because that is the allocation the failures pin - the CTS builds every functional
// texture with FUNCTIONAL_TEST_N_LEVELS = 3 (makeTextureComplete(0, 2)) - and any deviation
// from the measured shape might sit on the clean side of whatever allocation heuristic picks
// the driver's layout.
//
// The repair under test is the packed16 storage widening
// (PixelFormatNormalizeOptionBit::WidenPacked16Norm): where the POST probe
// (SelfTest::CopyImageMirrorsPacked16FieldOrder) measures the mirror - or
// MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE forces it - the three formats are stored as
// GL_RGB8/GL_RGBA8, leaving no 16-bit packed image for a copy to disagree about. The client
// word still round-trips exactly: the canonical shadow is already UNorm8, and an n-bit field
// encodes to UNorm8 and back losslessly for every n <= 8.
//
// This scenario runs in BOTH configurations, and both must hand back identical client words:
// * the ambient registrations take the narrow path on a clean driver (llvmpipe has no
// mirror, so Auto keeps the native 16-bit storage - the pre-existing behaviour stays
// covered);
// * the DirectGLES.WidenedPacked16. registration pins MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1,
// which is the storage every affected device will actually run - without it the repair
// is unfalsifiable off-device, because no CI driver has the bug that arms it.
// The Mali mirror itself CANNOT be reproduced here; only the on-device CTS run can show the
// widening killing the 18 bodies. What this scenario pins is that the widened storage is
// client-invisible: same words in, same words out, on every leg the failing bodies used.
//
// DirectVulkan is the control - Magma has always resolved these formats to RGBA8 - so a
// failure on both backends means the scenario is wrong, and a failure on DirectGLES alone
// means the widening (or the narrow path it replaces) is.
#include <algorithm>
#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 kBaseSize = 30; // array level 0; level 1 is 15x15
constexpr int kLevel1Size = kBaseSize / 2;
constexpr int kLayers = 12;
constexpr int kFlatSize = 7; // the plain-2D / renderbuffer endpoint, level 0
// Copies cover the whole flat endpoint and land at (8, 8) inside the 15x15 level so
// that offsets are honoured, not just texel (0, 0): 8 + 7 == 15 reaches the far edge.
constexpr int kRegion = kFlatSize;
constexpr int kArrayOffset = 8;
struct PackedFormatCase {
GLenum internalFormat; // the spelling the CTS uses
GLenum transferFormat;
GLenum transferType;
const char* name;
};
// Per-texel varying words, every field inside its width, so a swapped field order (or
// a mis-addressed row) cannot cancel out the way a uniform fill would let it.
GLushort MakeWord(GLenum type, int i) {
switch (type) {
case GL_UNSIGNED_SHORT_5_6_5: {
const int r = i % 32, g = (i * 7 + 3) % 64, b = (i * 5 + 11) % 32;
return static_cast<GLushort>((r << 11) | (g << 5) | b);
}
case GL_UNSIGNED_SHORT_4_4_4_4: {
const int r = i % 16, g = (i * 3 + 1) % 16, b = (i * 7 + 5) % 16, a = (i * 5 + 2) % 16;
return static_cast<GLushort>((r << 12) | (g << 8) | (b << 4) | a);
}
case GL_UNSIGNED_SHORT_5_5_5_1: {
const int r = i % 32, g = (i * 7 + 3) % 32, b = (i * 3 + 11) % 32, a = i % 2;
return static_cast<GLushort>((r << 11) | (g << 6) | (b << 1) | a);
}
default:
return 0;
}
}
std::vector<GLushort> MakeWords(GLenum type, int count, int seed) {
std::vector<GLushort> words(static_cast<size_t>(count));
for (int i = 0; i < count; ++i) {
words[static_cast<size_t>(i)] = MakeWord(type, i + seed);
}
return words;
}
class CopyImagePacked16Scenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
// 16-bit rows are 2-byte aligned; the default 4-byte row alignment would pad
// every odd-width row of the 15x15 level and shear the comparisons.
glPixelStorei(GL_UNPACK_ALIGNMENT, 2);
glPixelStorei(GL_PACK_ALIGNMENT, 2);
if (!CopyImageSubDataUsable()) {
GTEST_SKIP() << "glCopyImageSubData is unavailable on backend " << Gl().BackendName();
}
}
void TearDown() override {
if (!Ready()) return;
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glPixelStorei(GL_PACK_ALIGNMENT, 4);
for (const GLuint texture : m_textures) {
glDeleteTextures(1, &texture);
}
m_textures.clear();
if (m_renderbuffer != 0) {
glDeleteRenderbuffers(1, &m_renderbuffer);
m_renderbuffer = 0;
}
if (m_fbo != 0) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &m_fbo);
m_fbo = 0;
}
}
bool CopyImageSubDataUsable() {
GLuint probe[2] = {0, 0};
glGenTextures(2, probe);
for (const GLuint texture : probe) {
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 1, 1, 1);
}
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
while (glGetError() != GL_NO_ERROR) {
}
glCopyImageSubData(probe[0], GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, probe[1], GL_TEXTURE_2D_ARRAY, 0, 0, 0,
0, 1, 1, 1);
const bool usable = glGetError() == GL_NO_ERROR;
glDeleteTextures(2, probe);
return usable;
}
// The CTS's own mutable shape: glTexImage3D per level, filter NEAREST, THREE levels
// (30/15/7) with the chain clamped to them. Level 2 carries its own fill so nothing
// below can pass by reading a level that was never written.
GLuint MakeArrayTexture(const PackedFormatCase& format, const std::vector<GLushort>& level0,
const std::vector<GLushort>& level1) {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 2);
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, static_cast<GLint>(format.internalFormat), kBaseSize, kBaseSize,
kLayers, 0, format.transferFormat, format.transferType, level0.data());
glTexImage3D(GL_TEXTURE_2D_ARRAY, 1, static_cast<GLint>(format.internalFormat), kLevel1Size,
kLevel1Size, kLayers, 0, format.transferFormat, format.transferType, level1.data());
const int level2Size = kLevel1Size / 2;
const auto level2 = MakeWords(format.transferType, level2Size * level2Size * kLayers, 211);
glTexImage3D(GL_TEXTURE_2D_ARRAY, 2, static_cast<GLint>(format.internalFormat), level2Size,
level2Size, kLayers, 0, format.transferFormat, format.transferType, level2.data());
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
return texture;
}
// Three levels (7/3/1) like the CTS's plain endpoints; `texels` is level 0, the one
// every assertion reads.
GLuint MakeFlatTexture(const PackedFormatCase& format, const std::vector<GLushort>& texels) {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2);
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(format.internalFormat), kFlatSize, kFlatSize, 0,
format.transferFormat, format.transferType, texels.data());
for (int level = 1; level <= 2; ++level) {
const int size = std::max(kFlatSize >> level, 1);
const auto fill = MakeWords(format.transferType, size * size, 97 + level);
glTexImage2D(GL_TEXTURE_2D, level, static_cast<GLint>(format.internalFormat), size, size, 0,
format.transferFormat, format.transferType, fill.data());
}
glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
std::vector<GLushort> ReadTexImage(GLenum target, GLuint texture, int level,
const PackedFormatCase& format, size_t texelCount) {
std::vector<GLushort> words(texelCount, 0);
glBindTexture(target, texture);
glGetTexImage(target, level, format.transferFormat, format.transferType, words.data());
glBindTexture(target, 0);
return words;
}
// Every word of `got` inside the kRegion-square at (x0, y0) of a width-wide layer-0
// image equals the corresponding source word, and every word outside it still holds
// `fill`'s. Failures name the texel and both words, which is what turns a field-order
// regression into a one-line diagnosis.
void ExpectRegion(const std::vector<GLushort>& got, int width, int x0, int y0,
const std::vector<GLushort>& source, int sourceWidth, int sourceX0, int sourceY0,
const std::vector<GLushort>& fill, const char* what) {
for (int y = 0; y < width; ++y) {
for (int x = 0; x < width && static_cast<size_t>(y * width + x) < got.size(); ++x) {
const bool inRegion =
x >= x0 && x < x0 + kRegion && y >= y0 && y < y0 + kRegion;
const GLushort actual = got[static_cast<size_t>(y * width + x)];
const GLushort expected =
inRegion ? source[static_cast<size_t>((sourceY0 + y - y0) * sourceWidth + sourceX0 +
(x - x0))]
: fill[static_cast<size_t>(y * width + x)];
EXPECT_EQ(actual, expected)
<< what << ": texel (" << x << ", " << y << ")"
<< (inRegion ? " (copied)" : " (untouched)") << " holds 0x" << std::hex << actual
<< ", expected 0x" << expected;
if (actual != expected) return; // one texel names the defect; 224 more would bury it
}
}
}
std::vector<GLuint> m_textures;
GLuint m_renderbuffer = 0;
GLuint m_fbo = 0;
};
const PackedFormatCase kFormats[] = {
{GL_RGB5, GL_RGB, GL_UNSIGNED_SHORT_5_6_5, "rgb5"},
{GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1, "rgb5_a1"},
{GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4, "rgba4"},
};
// texture_2d (the ES image behind GL_TEXTURE_RECTANGLE too) -> the array's level 1:
// the array-as-destination direction of 12 of the 18 failing bodies.
TEST_F(CopyImagePacked16Scenario, FlatImageLandsInArrayMipLevelIntact) {
if (!Ready() || IsSkipped()) return;
for (const PackedFormatCase& format : kFormats) {
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
const auto flat = MakeWords(format.transferType, kFlatSize * kFlatSize, 131);
const GLuint array = MakeArrayTexture(format, level0, level1);
const GLuint source = MakeFlatTexture(format, flat);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
glCopyImageSubData(source, GL_TEXTURE_2D, 0, 0, 0, 0, array, GL_TEXTURE_2D_ARRAY, 1, kArrayOffset,
kArrayOffset, 0, kRegion, kRegion, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< format.name << ": glCopyImageSubData raised an error";
const auto got = ReadTexImage(GL_TEXTURE_2D_ARRAY, array, 1, format,
static_cast<size_t>(kLevel1Size) * kLevel1Size * kLayers);
ExpectRegion(got, kLevel1Size, kArrayOffset, kArrayOffset, flat, kFlatSize, 0, 0, level1,
(std::string("2d->2d_array level 1, ") + format.name).c_str());
// The source must not have moved - the CTS asserts this before it ever looks at
// the destination, and it is what pins the corruption to the copy itself.
const auto sourceAfter =
ReadTexImage(GL_TEXTURE_2D, source, 0, format, static_cast<size_t>(kFlatSize) * kFlatSize);
ExpectRegion(sourceAfter, kFlatSize, 0, 0, flat, kFlatSize, 0, 0, flat,
(std::string("source after 2d->2d_array, ") + format.name).c_str());
}
}
// The array's level 1 -> texture_2d: the array-as-source direction of the other 6
// bodies (2d_array -> 3d and 2d_array -> rectangle both read the level-1 array).
TEST_F(CopyImagePacked16Scenario, ArrayMipLevelLandsInFlatImageIntact) {
if (!Ready() || IsSkipped()) return;
for (const PackedFormatCase& format : kFormats) {
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
const auto fill = MakeWords(format.transferType, kFlatSize * kFlatSize, 131);
const GLuint array = MakeArrayTexture(format, level0, level1);
const GLuint destination = MakeFlatTexture(format, fill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
glCopyImageSubData(array, GL_TEXTURE_2D_ARRAY, 1, kArrayOffset, kArrayOffset, 0, destination,
GL_TEXTURE_2D, 0, 0, 0, 0, kRegion, kRegion, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< format.name << ": glCopyImageSubData raised an error";
const auto got = ReadTexImage(GL_TEXTURE_2D, destination, 0, format,
static_cast<size_t>(kFlatSize) * kFlatSize);
ExpectRegion(got, kFlatSize, 0, 0, level1, kLevel1Size, kArrayOffset, kArrayOffset, fill,
(std::string("2d_array level 1 -> 2d, ") + format.name).c_str());
}
}
// renderbuffer -> the array's level 1: the leg the remaining 3 bodies use, and the one
// that requires the renderbuffer's ES storage to move together with the textures' -
// glCopyImageSubData needs both endpoints in the same driver format, so a widening that
// reached textures alone would break exactly here.
TEST_F(CopyImagePacked16Scenario, RenderbufferLandsInArrayMipLevelIntact) {
if (!Ready() || IsSkipped()) return;
for (const PackedFormatCase& format : kFormats) {
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
const GLuint array = MakeArrayTexture(format, level0, level1);
if (m_renderbuffer == 0) glGenRenderbuffers(1, &m_renderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, format.internalFormat, kFlatSize, kFlatSize);
if (m_fbo == 0) glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
<< format.name << ": the renderbuffer is not attachable";
// Field values picked to encode exactly in the narrow fields AND in their
// UNorm8 expansions, so the expected word is the same whichever storage the
// configuration picked - which is the point of the whole scenario.
const int maxG = format.transferType == GL_UNSIGNED_SHORT_5_6_5 ? 63 : 31;
const int max = format.transferType == GL_UNSIGNED_SHORT_4_4_4_4 ? 15 : 31;
const int maxGreen = format.transferType == GL_UNSIGNED_SHORT_4_4_4_4 ? 15 : maxG;
const GLfloat clearColor[4] = {static_cast<GLfloat>(8 % (max + 1)) / max,
static_cast<GLfloat>(maxGreen / 2) / maxGreen,
static_cast<GLfloat>(max - 2) / max, 1.0f};
// The context is shared with every scenario in this process; a scissor left on
// would clip the clear and hand the copy undefined renderbuffer texels.
glDisable(GL_SCISSOR_TEST);
glClearBufferfv(GL_COLOR, 0, clearColor);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
glCopyImageSubData(m_renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, array, GL_TEXTURE_2D_ARRAY, 1,
kArrayOffset, kArrayOffset, 0, kRegion, kRegion, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< format.name << ": glCopyImageSubData raised an error";
GLushort clearedWord = 0;
switch (format.transferType) {
case GL_UNSIGNED_SHORT_5_6_5:
clearedWord = static_cast<GLushort>((8 << 11) | ((maxGreen / 2) << 5) | (max - 2));
break;
case GL_UNSIGNED_SHORT_5_5_5_1:
clearedWord = static_cast<GLushort>((8 << 11) | ((maxGreen / 2) << 6) | ((max - 2) << 1) | 1);
break;
case GL_UNSIGNED_SHORT_4_4_4_4:
clearedWord = static_cast<GLushort>((8 << 12) | ((maxGreen / 2) << 8) | ((max - 2) << 4) | 15);
break;
default:
break;
}
std::vector<GLushort> expectedRegion(static_cast<size_t>(kRegion) * kRegion, clearedWord);
const auto got = ReadTexImage(GL_TEXTURE_2D_ARRAY, array, 1, format,
static_cast<size_t>(kLevel1Size) * kLevel1Size * kLayers);
ExpectRegion(got, kLevel1Size, kArrayOffset, kArrayOffset, expectedRegion, kRegion, 0, 0, level1,
(std::string("renderbuffer -> 2d_array level 1, ") + format.name).c_str());
}
}
} // namespace
} // namespace MGITest
@@ -6,27 +6,32 @@
// SPDX-License-Identifier: LGPL-3.0-only
// 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
// Magma cannot build a module that declares the Float64 capability, and ESSL has no fp64 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
// float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than declining the
// shader: `double` compiles and runs everywhere, at float precision.
// Magma cannot build a module that declares the Float64 capability there, and ESSL has no fp64
// 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. On every such backend MobileGL narrows
// every 64-bit float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than
// 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
// laid out by reflecting the DEMOTED module, so glUniform*d has to store a float where the
// shader reads a float, glGetUniform*v has to read one back, and a dmat4's columns are now
// std140-padded like any other matrix's. Every one of those is a byte offset that fails
// silently - the uniform simply reads as something else - so the cases below set values
// through the API and have the SHADER report what it saw.
// Either way it is only half a contract. The other half is the API side: the global UBO is laid
// out by reflecting whichever module was produced, so glUniform*d has to store the width the
// shader reads, glGetUniform*v has to read that width back, and a matrix's columns are
// 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 through
// 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
// not, and cannot be. Every expectation here is the float value of the double that was set,
// which is the whole point.
// WHY ALMOST EVERY EXPECTATION HERE IS A FLOAT VALUE, and why that is not an accident of the
// demotion: the shader reports through a `float` SSBO, and every value chosen is exact in
// 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 <cstring>
#include <string>
#include <vector>
@@ -153,6 +158,151 @@ void main() {
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
// 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
@@ -428,12 +578,24 @@ void main() {
glUseProgram(0);
// 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
// shifted and nothing else in the API would say so.
// padding, the same component width - or a dmat4 comes back with its columns
// 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;
glGetUniformdv(m_program, scalar, &readScalar);
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";
// 0.1 is not representable in float32, so what comes back names the regime: a
// 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] = {};
glGetUniformdv(m_program, vector, readVector);
@@ -447,7 +609,8 @@ void main() {
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;
glGetUniformfv(m_program, scalar, &readFloat);
EXPECT_FLOAT_EQ(readFloat, static_cast<float>(0.1));
@@ -697,24 +860,188 @@ void main() {
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
}
TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsDeclinedOnEveryBackend) {
TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsRecordedAndItsArrayIsDroppedAtDraw) {
if (!Ready()) return;
// 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
// ones whose device lacks shaderFloat64. Declined loudly rather than accepted and
// drawn as garbage; the matching POST row says the same thing at startup.
// ones whose device lacks shaderFloat64.
//
// 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;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
while (glGetError() != GL_NO_ERROR) {}
glVertexAttribLFormat(0, 3, GL_DOUBLE, 0);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
glVertexAttribLFormat(1, 3, GL_DOUBLE, 8);
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);
glDeleteVertexArrays(1, &vao);
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 MGITest
@@ -0,0 +1,238 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DualSourceBlendScenario.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 DUAL-SOURCE BLEND DRAW HAS TO SURVIVE ON EVERY DRIVER.
//
// GL_SRC1_COLOR / GL_ONE_MINUS_SRC1_COLOR / GL_SRC1_ALPHA / GL_ONE_MINUS_SRC1_ALPHA
// (ARB_blend_func_extended, core since 3.3) need a backend capability that not every device has:
// GL_EXT_blend_func_extended on the ES driver, or the dualSrcBlend device feature on Vulkan. When
// the capability IS there both backends translate the factors properly, and that has always
// worked. When it is NOT, both backends used to THROW_EXCEPTION at draw time - and
// MG_Util/Types.h's THROW_EXCEPTION is a plain `throw`, with no catch anywhere in MG_Impl or
// MG_Backend, so the exception unwound out through the C GL ABI and killed the process. An
// application asking for a blend factor the device cannot do is a picture problem, never a reason
// to take the process down.
//
// Both are now a DECLINE: the attachment is drawn with blending off and neutral One/Zero factors,
// and the loss is logged once. So a dual-source draw has exactly two defined outcomes, and this
// scenario pins that it lands on one of them and never on a crash:
//
// capability present - src0 * src1 + dst * (1 - src1)
// capability absent - src0, written straight through
//
// What each CI lane actually reaches: lavapipe has dualSrcBlend, so the DirectVulkan lane runs the
// whole sequence and measures the blend. Mesa's GLES front end on llvmpipe has no
// GL_EXT_blend_func_extended, so the ESSL stage carrying `layout(index = 1)` never compiles and the
// program renders nothing - the DirectGLES lane therefore SKIPS on the capability probe in SetUp
// rather than measuring a picture the driver never produced. The DECLINE arm itself - the path this
// scenario exists for - is unit-tested against stubbed capabilities in
// MG_Test/Framebuffer/FramebufferTest.cpp (DualSourceBlendIsDeclinedRatherThanThrownWhenTheExtensionIsMissing),
// which is the only place it can be reached without a driver that lacks the extension.
//
// The Vulkan half has a second edge the last case covers: the dual-source VUIDs
// (VUID-VkPipelineColorBlendAttachmentState-srcColorBlendFactor-00608 and its three siblings)
// forbid a VK_BLEND_FACTOR_SRC1_* anywhere in VkPipelineColorBlendAttachmentState without the
// feature, whatever blendEnable says - so leaving the factors in place while clearing the enable
// would still be invalid pipeline state.
#include <cstdint>
#include <string>
#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 = 16;
constexpr const char* kVertexSource = R"(#version 330 core
void main()
{
switch (gl_VertexID)
{
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
}
}
)";
// Two outputs on the SAME location, indices 0 and 1: the shader-side spelling of
// dual-source output (GLSL 3.30 4.4.2, the `index` layout qualifier). No
// glBindFragDataLocationIndexed needed, which keeps the program buildable through the
// harness's compile-and-link helper.
constexpr const char* kDualSourceFragmentSource = R"(#version 330 core
uniform vec4 uSrc0;
uniform vec4 uSrc1;
layout(location = 0, index = 0) out vec4 fragColor0;
layout(location = 0, index = 1) out vec4 fragColor1;
void main()
{
fragColor0 = uSrc0;
fragColor1 = uSrc1;
}
)";
class DualSourceBlendScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glGenRenderbuffers(1, &m_renderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kExtent, kExtent);
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
std::string error;
m_program = CompileProgram(kVertexSource, kDualSourceFragmentSource, &error);
m_programError = error;
glViewport(0, 0, kExtent, kExtent);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
// Capability probe, not an assertion. A GL link that succeeded is not proof that
// the BACKEND can run the program: DirectGLES transpiles to ESSL lazily at first
// use, and GLSL ES has no `index` layout qualifier outside
// GL_EXT_blend_func_extended, so on a driver without it the stage never compiles
// and the draw renders nothing. One unblended white draw tells the two apart, and
// the cases skip rather than measure a picture the driver never produced.
if (m_program != 0) {
glDisable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ZERO);
Draw(/*src0=*/1.0f, /*src1=*/1.0f);
glFinish();
const Image probe = ReadPixels(kExtent, kExtent);
m_programRenders =
!probe.Empty() && static_cast<int>(probe.At(kExtent / 2, kExtent / 2).r) > 245;
}
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
void TearDown() override {
if (!Ready()) return;
glDisable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ZERO);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
if (m_renderbuffer != 0) glDeleteRenderbuffers(1, &m_renderbuffer);
if (m_program != 0) glDeleteProgram(m_program);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
}
void Draw(float src0, float src1) {
glUseProgram(m_program);
glUniform4f(glGetUniformLocation(m_program, "uSrc0"), src0, src0, src0, 1.0f);
glUniform4f(glGetUniformLocation(m_program, "uSrc1"), src1, src1, src1, 1.0f);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
glUseProgram(0);
}
// Both cases share this gate: nothing below can be measured on a backend that cannot
// run a dual-source fragment program at all. Returns the skip reason, empty when the
// program runs - NOT a void helper that calls GTEST_SKIP itself, because GTEST_SKIP
// expands to a `return` and would leave only the HELPER, letting the case run its
// assertions anyway and report Failed instead of Skipped.
std::string WhyTheProgramCannotRun() const {
if (m_program == 0) {
return "this driver cannot build a dual-source fragment shader: " + m_programError;
}
if (!m_programRenders) {
return "this backend links a dual-source fragment program but renders nothing with it "
"(GLSL ES has no `index` layout qualifier without GL_EXT_blend_func_extended)";
}
return {};
}
GLuint m_renderbuffer = 0;
GLuint m_fbo = 0;
GLuint m_vao = 0;
unsigned int m_program = 0;
bool m_programRenders = false;
std::string m_programError;
};
} // namespace
// The whole point of the scenario: this sequence used to be a process kill on any device
// without the capability, and it has to be a picture either way.
//
// dst is black, src0 is white and src1 is mid-grey, with SRC1_COLOR / ONE_MINUS_SRC1_COLOR.
// blended = 1.0 * 0.5 + 0.0 * 0.5 = 0.5 -> ~128
// declined = 1.0 -> 255
// Anything else means the factors were mistranslated rather than either honoured or declined.
TEST_F(DualSourceBlendScenario, DualSourceBlendDrawProducesOneOfTheTwoDefinedResults) {
if (!Ready()) GTEST_SKIP();
if (const std::string reason = WhyTheProgramCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
glDisable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ZERO);
Draw(/*src0=*/0.0f, /*src1=*/0.0f);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR);
EXPECT_EQ(FirstGLError(), 0u) << "glBlendFunc must accept the GL_SRC1_* factors - they are core since 3.3";
Draw(/*src0=*/1.0f, /*src1=*/0.5f);
glFinish();
glDisable(GL_BLEND);
EXPECT_EQ(FirstGLError(), 0u) << "the dual-source draw left a GL error behind";
const Image image = ReadPixels(kExtent, kExtent);
ASSERT_FALSE(image.Empty());
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
const int red = static_cast<int>(centre.r);
const bool blended = red > 100 && red < 160;
const bool declined = red > 245;
EXPECT_TRUE(blended || declined)
<< "got " << centre << ", which is neither the dual-source blend (~128) nor the declined "
<< "straight-through source (255) - the SRC1 factors were mistranslated";
Gl().EndFrame();
}
// The same factors with blending DISABLED. Nothing may blend, and on the Vulkan side nothing
// may reach VkPipelineColorBlendAttachmentState carrying a VK_BLEND_FACTOR_SRC1_* on a device
// without dualSrcBlend - the VUIDs bind to the struct, not to blendEnable. The picture is the
// source either way, so this case is really "no crash, no error, no surprise".
TEST_F(DualSourceBlendScenario, DualSourceFactorsWithBlendingDisabledJustWriteTheSource) {
if (!Ready()) GTEST_SKIP();
if (const std::string reason = WhyTheProgramCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
glDisable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ZERO);
Draw(/*src0=*/0.0f, /*src1=*/0.0f);
glBlendFunc(GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA);
Draw(/*src0=*/1.0f, /*src1=*/0.25f);
glFinish();
EXPECT_EQ(FirstGLError(), 0u) << "a draw with SRC1 factors and blending off left a GL error behind";
const Image image = ReadPixels(kExtent, kExtent);
ASSERT_FALSE(image.Empty());
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
EXPECT_GT(static_cast<int>(centre.r), 245)
<< "got " << centre << ": blending is disabled, so the source has to be written straight through";
Gl().EndFrame();
}
} // 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;
out vec4 o_color;
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 {
@@ -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";
}
// `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
@@ -0,0 +1,794 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/GuiBatchScenario.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 GUI QUAD DRAWN THE WAY AcceleratedRendering DRAWS ONE.
//
// The mod replaces every GUI blit with a compute pass: the application writes unit-space
// vertices into a persistently mapped SSBO, a compute shader multiplies them by a shared
// transform into a second buffer, and that second buffer is then bound as GL_ARRAY_BUFFER of a
// DSA vertex array and drawn with glDrawElementsBaseVertex through vanilla's position_tex_color
// program. Every element of that is replayed here, one axis per test, so a failure names the
// element that killed the quad rather than "the GUI is broken".
//
// The axis that mattered is MeshesBlockLeftUnbound. The mod's vertex-transform compute shader
// declares SIX storage blocks, and the last of them - `Meshes`, the cache of pre-uploaded model
// geometry - is read only when a vertex says it comes from a cached mesh. A batch of plain GUI
// blits has no cached meshes, so the mod binds nothing at that point and the shader never reads
// it. GL 4.6 core 7.8 is explicit that this is legal: a storage block with no buffer at its
// binding point simply has no store.
//
// DirectVulkan used to refuse the whole descriptor set over it, and both SetupDraw and
// DispatchCompute skip their work on that refusal - so the transform dispatch never ran, the
// output vertex buffer kept whatever was in it, and every hotbar and container-screen background
// quad came out degenerate. Items were unaffected because item geometry DOES come from cached
// meshes, which is what made the bug look like "only the backgrounds disappear".
//
// The assertions are whole-region, not centre-pixel: a quad that survives with three stale
// vertices still paints its centre.
//
// Reproduces on DirectVulkan only. DirectGLES forwards the unbound binding to the GLES driver,
// which does what GL says, so it is the control - every test here must stay green on both.
#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 const char* kTransformComputeSource = R"(#version 460 core
struct Vertex {
float x;
float y;
float z;
float u0;
float v0;
uint color;
};
struct VaryingData {
int offset;
int sharing;
int mesh;
int shouldCull;
};
struct SharingData {
mat4 transform;
mat3 normal;
};
layout(local_size_x = 128) in;
layout(binding=0, std430) restrict readonly buffer VerticesIn {
Vertex verticesIn[];
};
layout(binding=1, std430) restrict writeonly buffer VerticesOut {
Vertex verticesOut[];
};
layout(binding=2, std430) restrict readonly buffer Sharings {
SharingData sharings[];
};
layout(binding=3, std430) restrict readonly buffer VaryingsIn {
VaryingData varyingsIn[];
};
layout(binding=4, std430) restrict writeonly buffer VaryingsOut {
VaryingData varyingsOut[];
};
layout(binding=5, std430) restrict readonly buffer Meshes {
Vertex meshVertices[];
};
layout(location=0) uniform uint vertexCount;
layout(location=1) uniform uint vertexOffset;
layout(location=2) uniform uint varyingOffset;
void main() {
uint indexIn = gl_GlobalInvocationID.x;
uint vertexOut = indexIn + vertexOffset;
uint varyingOut = indexIn + varyingOffset;
if (indexIn >= vertexCount) {
return;
}
int offset = varyingsIn[indexIn] .offset;
uint reference = indexIn - offset;
int sharing = varyingsIn[reference] .sharing;
int mesh = varyingsIn[reference] .mesh;
mat4 transformMatrix;
if (sharing != -1) {
transformMatrix = sharings[sharing].transform;
} else {
transformMatrix = mat4(1.0);
}
Vertex vertexIn;
vec4 colorMesh;
if (mesh != -1) {
vertexIn = meshVertices[mesh + offset];
colorMesh = unpackUnorm4x8 (vertexIn.color);
} else {
vertexIn = verticesIn[indexIn];
colorMesh = vec4 (1.0);
}
vec4 colorIn = unpackUnorm4x8 (verticesIn[reference].color);
vec4 posOut = transformMatrix * vec4 (vertexIn.x, vertexIn.y, vertexIn.z, 1.0);
vec4 colorOut = colorMesh * colorIn;
verticesOut[vertexOut].x = posOut.x;
verticesOut[vertexOut].y = posOut.y;
verticesOut[vertexOut].z = posOut.z;
verticesOut[vertexOut].u0 = vertexIn.u0;
verticesOut[vertexOut].v0 = vertexIn.v0;
verticesOut[vertexOut].color = packUnorm4x8 (colorOut);
varyingsOut[varyingOut].offset = offset;
varyingsOut[varyingOut].shouldCull = varyingsIn[reference].shouldCull;
}
)";
// Vanilla position_tex_color, spelled the way MC ships it: #version 150, no explicit
// attribute locations (they come from glBindAttribLocation in format order) and the
// ProjMat/ModelViewMat pair the mod re-uploads through setDefaultUniforms.
constexpr const char* kBlitVertexSource = R"(#version 150
in vec3 Position;
in vec2 UV0;
in vec4 Color;
uniform mat4 ModelViewMat;
uniform mat4 ProjMat;
out vec2 texCoord0;
out vec4 vertexColor;
void main() {
gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);
texCoord0 = UV0;
vertexColor = Color;
}
)";
constexpr const char* kBlitFragmentSource = R"(#version 150
uniform sampler2D Sampler0;
in vec2 texCoord0;
in vec4 vertexColor;
out vec4 fragColor;
void main() {
fragColor = texture(Sampler0, texCoord0) * vertexColor;
}
)";
constexpr int kFboSize = 64;
constexpr int kShaderStorageRestoreRange = 9;
constexpr int kAtomicCounterRestoreRange = 1;
constexpr int kBuilders = 2;
struct GuiVertex {
float x, y, z;
float u, v;
std::uint32_t color;
};
static_assert(sizeof(GuiVertex) == 24, "POSITION_TEX_COLOR is 24 bytes");
struct VaryingData {
std::int32_t offset;
std::int32_t sharing;
std::int32_t mesh;
std::int32_t shouldCull;
};
struct IndexedBinding {
GLint buffer = 0;
GLint start = 0;
GLint size = 0;
};
// Which parts of the mod's real frame this replay reproduces. Each test flips exactly
// one on top of the baseline so a failure names the element that killed the quad.
struct Fidelity {
bool shortIndices = false; // MC's AutoStorageIndexBuffer is USHORT at small counts
bool baseVertex = false; // ...and the second builder draws at a base vertex
bool twoBuilders = false; // two render types share one output buffer
bool blendAndDepth = false; // TRANSLUCENT_TRANSPARENCY + LEQUAL_DEPTH_TEST
bool regrow = false; // MutableBuffer.doExpand replaces the GL name
bool rewriteMapEachFrame = false;
bool skipRelayout = false; // bindDrawBuffers() only re-lays-out when resized
bool leaveMeshesUnbound = false; // a batch with no server meshes never binds binding 5
};
class GuiBatchScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_transform = CompileComputeProgram(kTransformComputeSource);
ASSERT_NE(m_transform, 0u) << m_buildLog;
m_blit = CompileBlitProgram();
ASSERT_NE(m_blit, 0u) << m_buildLog;
m_target = MakeColorFbo(kFboSize, kFboSize);
ASSERT_NE(m_target.fbo, 0u);
MakeTexture();
MakeIndexBuffers();
MakeAcceleratedBuffers();
m_laidOut = false;
ASSERT_EQ(FirstGLError(), 0u) << "setup raised a GL error";
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindVertexArray(0);
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
DestroyColorFbo(m_target);
if (m_transform) glDeleteProgram(m_transform);
if (m_blit) glDeleteProgram(m_blit);
if (m_texture) glDeleteTextures(1, &m_texture);
if (!m_buffers.empty()) glDeleteBuffers((GLsizei)m_buffers.size(), m_buffers.data());
if (!m_vaos.empty()) glDeleteVertexArrays((GLsizei)m_vaos.size(), m_vaos.data());
m_buffers.clear();
m_vaos.clear();
}
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[4096] = {};
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[4096] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute program did not link: ") + log;
glDeleteProgram(program);
return 0;
}
return program;
}
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 == GL_FALSE) {
char log[4096] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("shader did not compile: ") + log;
glDeleteShader(shader);
return 0;
}
return shader;
}
// glBindAttribLocation in format order, exactly as ShaderInstance does it.
unsigned int CompileBlitProgram() {
const GLuint vs = CompileOne(GL_VERTEX_SHADER, kBlitVertexSource);
if (!vs) return 0;
const GLuint fs = CompileOne(GL_FRAGMENT_SHADER, kBlitFragmentSource);
if (!fs) return 0;
const GLuint program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glBindAttribLocation(program, 0, "Position");
glBindAttribLocation(program, 1, "UV0");
glBindAttribLocation(program, 2, "Color");
glLinkProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[4096] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("blit program did not link: ") + log;
glDeleteProgram(program);
return 0;
}
return program;
}
void MakeTexture() {
std::vector<std::uint8_t> pixels(4 * 4 * 4, 0);
for (int i = 0; i < 16; ++i) {
pixels[i * 4 + 2] = 255;
pixels[i * 4 + 3] = 255;
}
glGenTextures(1, &m_texture);
glActiveTexture(GL_TEXTURE0);
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);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
}
GLuint NewBuffer() {
GLuint b = 0;
glCreateBuffers(1, &b);
m_buffers.push_back(b);
return b;
}
GLuint NewVao() {
GLuint v = 0;
glCreateVertexArrays(1, &v);
m_vaos.push_back(v);
return v;
}
void MakeIndexBuffers() {
std::uint32_t wide[12];
std::uint16_t narrow[12];
for (int quad = 0; quad < 2; ++quad) {
const std::uint32_t base = (std::uint32_t)(quad * 4);
const std::uint32_t pattern[6] = {base, base + 1, base + 2, base + 2, base + 3, base};
for (int i = 0; i < 6; ++i) {
wide[quad * 6 + i] = pattern[i];
narrow[quad * 6 + i] = (std::uint16_t)pattern[i];
}
}
m_wideIndices = NewBuffer();
glNamedBufferStorage(m_wideIndices, sizeof(wide), wide, 0);
m_narrowIndices = NewBuffer();
glNamedBufferStorage(m_narrowIndices, sizeof(narrow), narrow, 0);
}
static void SetupAttributes() {
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, (const void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 24, (const void*)12);
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, GL_TRUE, 24, (const void*)20);
glEnableVertexAttribArray(2);
}
void WriteInputs() {
const GuiVertex unit[4] = {
{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0xFFFFFFFFu},
{0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0xFFFFFFFFu},
{1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0xFFFFFFFFu},
{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0xFFFFFFFFu},
};
for (int b = 0; b < kBuilders; ++b) {
std::memcpy(m_inVertexMap[b], unit, sizeof(unit));
VaryingData varyings[4];
for (int i = 0; i < 4; ++i) {
varyings[i].offset = i;
varyings[i].sharing = b;
varyings[i].mesh = -1;
varyings[i].shouldCull = 0;
}
std::memcpy(m_inVaryingMap[b], varyings, sizeof(varyings));
}
}
void MakeAcceleratedBuffers() {
const GLbitfield persistent = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT;
for (int b = 0; b < kBuilders; ++b) {
m_inVertices[b] = NewBuffer();
glNamedBufferStorage(m_inVertices[b], 4 * (GLsizeiptr)sizeof(GuiVertex), nullptr, persistent);
m_inVertexMap[b] =
glMapNamedBufferRange(m_inVertices[b], 0, 4 * (GLsizeiptr)sizeof(GuiVertex), persistent);
m_inVaryings[b] = NewBuffer();
glNamedBufferStorage(m_inVaryings[b], 4 * (GLsizeiptr)sizeof(VaryingData), nullptr, persistent);
m_inVaryingMap[b] =
glMapNamedBufferRange(m_inVaryings[b], 0, 4 * (GLsizeiptr)sizeof(VaryingData), persistent);
}
// Two SharingData entries: builder 0 lands left of centre, builder 1 right.
float sharing[56] = {};
const float tx[2] = {-0.9f, 0.1f};
for (int b = 0; b < 2; ++b) {
float* m = sharing + b * 28;
m[0] = 0.8f;
m[5] = 1.0f;
m[10] = 1.0f;
m[15] = 1.0f;
m[12] = tx[b];
m[13] = -0.5f;
m[16] = 1.0f;
m[20] = 1.0f;
m[24] = 1.0f;
}
m_sharings = NewBuffer();
glNamedBufferStorage(m_sharings, sizeof(sharing), nullptr, persistent);
void* r = glMapNamedBufferRange(m_sharings, 0, sizeof(sharing), persistent);
std::memcpy(r, sharing, sizeof(sharing));
m_outSize = 8 * (GLsizeiptr)sizeof(GuiVertex);
m_outVertices = NewBuffer();
glNamedBufferStorage(m_outVertices, m_outSize, nullptr, GL_DYNAMIC_STORAGE_BIT);
m_outVaryings = NewBuffer();
glNamedBufferStorage(m_outVaryings, 8 * (GLsizeiptr)sizeof(VaryingData), nullptr,
GL_DYNAMIC_STORAGE_BIT);
m_meshes = NewBuffer();
glNamedBufferStorage(m_meshes, 4 * (GLsizeiptr)sizeof(GuiVertex), nullptr, GL_DYNAMIC_STORAGE_BIT);
m_vao = NewVao();
WriteInputs();
}
std::vector<IndexedBinding> Record(GLenum b, GLenum s, GLenum z, int range) {
std::vector<IndexedBinding> saved((std::size_t)range);
for (int i = 0; i < range; ++i) {
glGetIntegeri_v(b, (GLuint)i, &saved[(std::size_t)i].buffer);
glGetIntegeri_v(s, (GLuint)i, &saved[(std::size_t)i].start);
glGetIntegeri_v(z, (GLuint)i, &saved[(std::size_t)i].size);
}
return saved;
}
void Restore(GLenum target, const std::vector<IndexedBinding>& saved) {
for (std::size_t i = 0; i < saved.size(); ++i) {
if (saved[i].start == 0 && saved[i].size == 0) {
glBindBufferBase(target, (GLuint)i, (GLuint)saved[i].buffer);
} else {
glBindBufferRange(target, (GLuint)i, (GLuint)saved[i].buffer, saved[i].start,
saved[i].size);
}
}
}
// MutableBuffer.doExpand: a NEW immutable store, copied from the old one, old one gone.
void RegrowOutputBuffer() {
const GLsizeiptr newSize = m_outSize * 2;
GLuint grown = 0;
glCreateBuffers(1, &grown);
glNamedBufferStorage(grown, newSize, nullptr, GL_DYNAMIC_STORAGE_BIT);
glCopyNamedBufferSubData(m_outVertices, grown, 0, 0, m_outSize);
glDeleteBuffers(1, &m_outVertices);
for (auto& b : m_buffers) {
if (b == m_outVertices) b = grown;
}
m_outVertices = grown;
m_outSize = newSize;
}
void Frame(const Fidelity& f, int builders) {
if (f.rewriteMapEachFrame) WriteInputs();
// --- prepareBuffers() -------------------------------------------------
const std::vector<IndexedBinding> ssbo =
Record(GL_SHADER_STORAGE_BUFFER_BINDING, GL_SHADER_STORAGE_BUFFER_START,
GL_SHADER_STORAGE_BUFFER_SIZE, kShaderStorageRestoreRange);
const std::vector<IndexedBinding> counters =
Record(GL_ATOMIC_COUNTER_BUFFER_BINDING, GL_ATOMIC_COUNTER_BUFFER_START,
GL_ATOMIC_COUNTER_BUFFER_SIZE, kAtomicCounterRestoreRange);
GLint currentProgram = 0;
glGetIntegerv(GL_CURRENT_PROGRAM, &currentProgram);
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_outVertices);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_sharings);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, m_outVaryings);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, f.leaveMeshesUnbound ? 0 : m_meshes);
glUseProgram(m_transform);
for (int b = 0; b < builders; ++b) {
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_inVertices[b]);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, m_inVaryings[b]);
glProgramUniform1ui(m_transform, glGetUniformLocation(m_transform, "vertexCount"), 4u);
glProgramUniform1ui(m_transform, glGetUniformLocation(m_transform, "vertexOffset"),
(GLuint)(4 * b));
glProgramUniform1ui(m_transform, glGetUniformLocation(m_transform, "varyingOffset"),
(GLuint)(4 * b));
glDispatchCompute(1, 1, 1);
}
glUseProgram(0);
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
glUseProgram((GLuint)currentProgram);
Restore(GL_SHADER_STORAGE_BUFFER, ssbo);
Restore(GL_ATOMIC_COUNTER_BUFFER, counters);
if (f.regrow) {
RegrowOutputBuffer();
m_laidOut = false; // isResized() forces the relayout
}
// --- drawBuffers() ----------------------------------------------------
glMemoryBarrier(GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT |
GL_COMMAND_BARRIER_BIT);
glBindVertexArray(m_vao);
if (!m_laidOut || !f.skipRelayout) {
glBindBuffer(GL_ARRAY_BUFFER, m_outVertices);
SetupAttributes();
m_laidOut = true;
}
if (f.blendAndDepth) {
glEnable(GL_BLEND);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
}
const GLenum indexType = f.shortIndices ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT;
const GLuint indexBuffer = f.shortIndices ? m_narrowIndices : m_wideIndices;
const GLsizei indexStride = f.shortIndices ? 2 : 4;
for (int b = 0; b < builders; ++b) {
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); // AutoStorageIndexBuffer.bind
glUseProgram(m_blit);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_texture);
glUniform1i(glGetUniformLocation(m_blit, "Sampler0"), 0);
UploadIdentityMatrices();
if (f.baseVertex) {
// The mod's BASEVERTEX path: every builder reads the SAME first six
// indices and offsets the vertices with a base vertex.
glDrawElementsBaseVertex(GL_TRIANGLES, 6, indexType, (const void*)0, 4 * b);
} else {
glDrawElements(GL_TRIANGLES, 6, indexType, (const void*)(intptr_t)(b * 6 * indexStride));
}
glUseProgram(0);
}
if (f.blendAndDepth) {
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
}
glBindVertexArray(0);
}
void UploadIdentityMatrices() {
static const float identity[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
glUniformMatrix4fv(glGetUniformLocation(m_blit, "ModelViewMat"), 1, GL_FALSE, identity);
glUniformMatrix4fv(glGetUniformLocation(m_blit, "ProjMat"), 1, GL_FALSE, identity);
}
void ExpectQuads(const Image& image, int builders, const char* when) {
EXPECT_TRUE(RegionIsMostly(image, 6, 26, 19, 45, "blue", 0.0,
std::string("left accelerated quad, ") + when));
if (builders > 1) {
EXPECT_TRUE(RegionIsMostly(image, 38, 58, 19, 45, "blue", 0.0,
std::string("right accelerated quad, ") + when));
}
}
void RunFrames(const Fidelity& f, int builders, int frames, const char* when) {
for (int i = 0; i < frames; ++i) {
BindFbo(m_target);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
Frame(f, builders);
ASSERT_EQ(FirstGLError(), 0u) << "frame " << i << " raised a GL error (" << when << ")";
const Image image = ReadPixels(kFboSize, kFboSize);
ExpectQuads(image, builders, (std::string(when) + ", frame " + std::to_string(i)).c_str());
Gl().EndFrame();
}
}
unsigned int m_transform = 0;
unsigned int m_blit = 0;
ColorFbo m_target{};
GLuint m_texture = 0;
GLuint m_wideIndices = 0;
GLuint m_narrowIndices = 0;
GLuint m_vao = 0;
GLuint m_inVertices[kBuilders] = {};
GLuint m_inVaryings[kBuilders] = {};
void* m_inVertexMap[kBuilders] = {};
void* m_inVaryingMap[kBuilders] = {};
GLuint m_sharings = 0;
GLuint m_outVertices = 0;
GLuint m_outVaryings = 0;
GLuint m_meshes = 0;
GLsizeiptr m_outSize = 0;
bool m_laidOut = false;
std::vector<GLuint> m_buffers;
std::vector<GLuint> m_vaos;
std::string m_buildLog;
};
} // namespace
TEST_F(GuiBatchScenario, Baseline) {
if (!Ready() || IsSkipped()) return;
RunFrames(Fidelity{}, 1, 1, "baseline");
}
TEST_F(GuiBatchScenario, ShortIndices) {
if (!Ready() || IsSkipped()) return;
Fidelity f;
f.shortIndices = true;
RunFrames(f, 1, 1, "short indices");
}
TEST_F(GuiBatchScenario, TwoBuildersWideIndices) {
if (!Ready() || IsSkipped()) return;
RunFrames(Fidelity{}, 2, 1, "two builders, wide indices");
}
TEST_F(GuiBatchScenario, TwoBuildersBaseVertex) {
if (!Ready() || IsSkipped()) return;
Fidelity f;
f.baseVertex = true;
RunFrames(f, 2, 1, "two builders, base vertex");
}
TEST_F(GuiBatchScenario, TwoBuildersShortIndicesBaseVertex) {
if (!Ready() || IsSkipped()) return;
Fidelity f;
f.baseVertex = true;
f.shortIndices = true;
RunFrames(f, 2, 1, "two builders, short indices, base vertex");
}
TEST_F(GuiBatchScenario, BlendAndDepth) {
if (!Ready() || IsSkipped()) return;
Fidelity f;
f.baseVertex = true;
f.shortIndices = true;
f.blendAndDepth = true;
RunFrames(f, 2, 1, "blend and depth");
}
TEST_F(GuiBatchScenario, ThreeFramesWithoutRelayout) {
if (!Ready() || IsSkipped()) return;
Fidelity f;
f.baseVertex = true;
f.shortIndices = true;
f.blendAndDepth = true;
f.skipRelayout = true;
f.rewriteMapEachFrame = true;
RunFrames(f, 2, 3, "three frames without relayout");
}
TEST_F(GuiBatchScenario, MeshesBlockLeftUnbound) {
if (!Ready() || IsSkipped()) return;
Fidelity f;
f.baseVertex = true;
f.shortIndices = true;
f.blendAndDepth = true;
f.leaveMeshesUnbound = true;
RunFrames(f, 2, 1, "Meshes block left unbound");
}
TEST_F(GuiBatchScenario, FullFidelityWithRegrowth) {
if (!Ready() || IsSkipped()) return;
Fidelity f;
f.baseVertex = true;
f.shortIndices = true;
f.blendAndDepth = true;
f.skipRelayout = true;
f.rewriteMapEachFrame = true;
RunFrames(f, 2, 1, "full fidelity, pre-growth");
f.regrow = true;
RunFrames(f, 2, 1, "full fidelity, growth frame");
f.regrow = false;
RunFrames(f, 2, 2, "full fidelity, post-growth");
}
namespace {
// Atomic counter blocks resolve through the SAME descriptor path as shader storage
// blocks (glslang rewrites every atomic_uint into a synthesized storage block), so an
// unbound GL_ATOMIC_COUNTER_BUFFER point loses the dispatch for exactly the same reason
// an unbound SSBO did. The mod reaches this with its INDIRECT draw method, whose culling
// shaders carry a counter the BASEVERTEX default never binds.
//
// The counter is INCREMENTED, not merely declared: an unreferenced one is optimised out
// before it ever reaches a descriptor, so a shader that only declares it proves nothing.
constexpr const char* kCounterComputeSource = R"(#version 430 core
layout(local_size_x = 1) in;
layout(binding = 0, offset = 0) uniform atomic_uint g_unbound;
layout(std430, binding = 0) buffer Output { uint g_data[]; };
void main() {
atomicCounterIncrement(g_unbound);
g_data[gl_GlobalInvocationID.x] = gl_GlobalInvocationID.x + 1u;
}
)";
class UnboundCounterBlockScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
GLint counters = 0;
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTERS, &counters);
if (counters < 1) {
GTEST_SKIP() << "GL_MAX_COMPUTE_ATOMIC_COUNTERS is " << counters;
}
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &kCounterComputeSource, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
ASSERT_NE(compiled, GL_FALSE);
m_program = glCreateProgram();
glAttachShader(m_program, shader);
glLinkProgram(m_program);
glDeleteShader(shader);
GLint linked = 0;
glGetProgramiv(m_program, GL_LINK_STATUS, &linked);
ASSERT_NE(linked, GL_FALSE);
glGenBuffers(1, &m_buffer);
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
if (m_buffer) glDeleteBuffers(1, &m_buffer);
if (m_program) glDeleteProgram(m_program);
}
unsigned int m_program = 0;
GLuint m_buffer = 0;
};
} // namespace
TEST_F(UnboundCounterBlockScenario, ADeclaredButUnboundCounterDoesNotLoseTheDispatch) {
if (!Ready() || IsSkipped()) return;
constexpr int kElements = 4;
const std::vector<unsigned int> zeros((std::size_t)kElements, 0u);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_buffer);
glBufferData(GL_SHADER_STORAGE_BUFFER, (GLsizeiptr)(zeros.size() * sizeof(unsigned int)), zeros.data(),
GL_DYNAMIC_COPY);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_buffer);
// Nothing is bound at GL_ATOMIC_COUNTER_BUFFER point 0 on purpose.
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, 0);
ASSERT_EQ(FirstGLError(), 0u);
glUseProgram(m_program);
glDispatchCompute(kElements, 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch raised a GL error";
std::vector<unsigned int> values((std::size_t)kElements, 0xDEADBEEFu);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_buffer);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, (GLsizeiptr)(values.size() * sizeof(unsigned int)),
values.data());
for (int i = 0; i < kElements; ++i) {
EXPECT_EQ(values[(std::size_t)i], (unsigned int)(i + 1))
<< "element " << i << " came back as " << values[(std::size_t)i]
<< "; zero everywhere means the whole dispatch was dropped over the unbound counter block";
}
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
}
} // namespace MGITest
@@ -127,14 +127,23 @@ void main()
// 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
// 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
// the array into one declaration per element and rewriting every use of it.
// spelling in a single declaration.
//
// Scoped rather than disabled, exactly as ProgramPipelineScenario scopes its
// storage-block rebinding cases: the defect is per-backend and the frontend
// mechanism these cases exist for - per-element units surviving the trip to the
// pipeline composite - is fully exercised on Magma.
bool PerElementImageUnitsAreHonoured() const { return Gl().BackendName() == "DirectVulkan"; }
// RemapImageArrayElementUnits repairs it by SPLITTING the array into one scalar
// image uniform per element, each carrying its own binding, which costs exactly the
// four image uniforms the application declared. (It used to WIDEN the array to cover
// the whole span instead, which cost seven for those four elements and had to be
// 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
// rather than fail.
@@ -164,7 +173,7 @@ void main()
if (!Ready()) return;
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
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();
@@ -283,8 +292,12 @@ void main()
TEST_F(ImageLoadStoreSsoScenario, AnImageArrayAlongsideAnotherDescriptorKeepsBothBindings) {
if (!Ready()) return;
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
if (!PerElementImageUnitsAreHonoured()) {
GTEST_SKIP() << "non-consecutive per-element image units cannot be baked into ESSL";
// The defect this guards is the SPIR-V descriptor remap, which only Magma has; the units
// 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();
@@ -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 GLuint kFilledValue = 7u;
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.
struct TargetKind {
@@ -129,6 +132,25 @@ namespace MGITest {
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 {
protected:
void TearDown() override {
@@ -374,6 +396,119 @@ namespace MGITest {
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_textures;
std::vector<GLuint> m_buffers;
@@ -435,6 +570,54 @@ namespace MGITest {
#undef MGL_DEFINE_LOAD_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 -------------------------------------------
//
// The conformance case's actual shape. The single-kind cases above cannot see a defect that
@@ -0,0 +1,391 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IntegerBorderColorScenario.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 - AN INTEGER GL_TEXTURE_BORDER_COLOR REACHES AN isampler2D AS AN INTEGER.
//
// KHR-GL46.texture_border_clamp.Texture2D{R32I,R32UI} (and the 2DArray/3D siblings) set the border
// colour with glSamplerParameterIiv/Iuiv, sample outside the texture through an integer sampler and
// expect the value back. MobileGL returned 1132396544 on Espryt - which is 0x437F0000, the IEEE-754
// bits of 255.0f, i.e. the float border-colour register read through an integer sampler - and 0 on
// Magma, where the border fell through to VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK.
//
// Two independent halves, and this scenario covers both because it goes through the frontend:
//
// * the STATE had no record of which entry point wrote the border colour. All three
// representations are kept numerically in step, so the value alone cannot say whether the
// application called glTexParameterfv or glTexParameterIiv.
// * each backend then had exactly one border-colour call site: glTexParameterfv /
// glSamplerParameterfv on DirectGLES, and a snap-to-one-of-four-predefined-values on
// DirectVulkan that never emitted the VK_BORDER_COLOR_INT_* family at all.
//
// The border value is deliberately outside every predefined VkBorderColor and outside anything a
// float register could round-trip: (255, -1, 7, 3) is neither transparent black, nor opaque black,
// nor opaque white, so on DirectVulkan it can only be delivered through VK_EXT_custom_border_color.
// That makes the scenario a real test of the extension path on lavapipe rather than a palette hit.
//
// Both an integer image view and an integer border colour are involved, which is the other half of
// the Vulkan rule: VK_BORDER_COLOR_FLOAT_* on an integer image view is undefined behaviour
// regardless of the value, so even a border of (0,0,0,1) has to resolve to INT_OPAQUE_BLACK.
// InsideTexelsAreUnaffected is what keeps that from being asserted vacuously.
#include <array>
#include <cstdint>
#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 kOutputWidth = 8;
constexpr int kOutputHeight = 8;
// The texture's own texel, and the border. Neither is a Vulkan palette entry, and the border
// is deliberately not derivable from the texel.
constexpr std::int32_t kInsideTexel[4] = {11, 22, 33, 44};
constexpr std::int32_t kBorderColor[4] = {255, -1, 7, 3};
constexpr const char* kVertexSource = R"(#version 330 core
void main()
{
switch (gl_VertexID)
{
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
}
}
)";
// One channel per draw, so a failure names the component that is wrong. The coordinate is a
// uniform rather than a literal so the same program serves the border sample and the inside
// sample and nothing can be constant-folded differently between them.
std::string FragmentSource(int channel) {
static const char* kChannels[4] = {"x", "y", "z", "w"};
return std::string("#version 330 core\n\nuniform isampler2D smp;\nuniform vec2 uCoord;\n\n"
"out int out_color;\n\nvoid main()\n{\n out_color = texture(smp, uCoord).") +
kChannels[channel] + ";\n}\n";
}
class IntegerBorderColorScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
// 2x2 RGBA32I. Integer textures are not filterable, so NEAREST is mandatory.
const std::int32_t texels[4][4] = {{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]},
{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]},
{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]},
{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]}};
glGenTextures(1, &m_sourceTexture);
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32I, 2, 2);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 2, GL_RGBA_INTEGER, GL_INT, texels);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
ASSERT_EQ(FirstGLError(), 0u) << "source texture setup left a GL error behind";
// 8x8 R32I render target: an integer readback, so nothing is normalized on the way
// out and a wrong value is reported as the number it actually was.
glGenTextures(1, &m_outputTexture);
glBindTexture(GL_TEXTURE_2D, m_outputTexture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32I, kOutputWidth, kOutputHeight);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_outputTexture, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
glGenVertexArrays(1, &m_vao);
ASSERT_EQ(FirstGLError(), 0u) << "output framebuffer setup left a GL error behind";
}
void TearDown() override {
if (!Ready()) return;
if (m_sampler != 0) {
glBindSampler(0, 0);
glDeleteSamplers(1, &m_sampler);
m_sampler = 0;
}
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
if (m_outputTexture != 0) glDeleteTextures(1, &m_outputTexture);
if (m_sourceTexture != 0) glDeleteTextures(1, &m_sourceTexture);
if (m_narrowTexture != 0) glDeleteTextures(1, &m_narrowTexture);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
// Samples `coord` through the integer sampler and returns every texel the draw wrote.
std::vector<std::int32_t> RenderChannel(int channel, float coordX, float coordY) {
const std::string fragment = FragmentSource(channel);
std::string error;
const unsigned int program = CompileProgram(kVertexSource, fragment.c_str(), &error);
if (program == 0) {
ADD_FAILURE() << "channel " << channel << ": program did not build: " << error;
return {};
}
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glViewport(0, 0, kOutputWidth, kOutputHeight);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
// A clear value nothing under test can produce, so an undrawn target is not mistaken
// for a correct one.
const GLint clearValue[4] = {-559038737, 0, 0, 0};
glClearBufferiv(GL_COLOR, 0, clearValue);
glUseProgram(program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
glUniform1i(glGetUniformLocation(program, "smp"), 0);
glUniform2f(glGetUniformLocation(program, "uCoord"), coordX, coordY);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
std::vector<std::int32_t> texels(static_cast<std::size_t>(kOutputWidth) * kOutputHeight, 0);
glReadPixels(0, 0, kOutputWidth, kOutputHeight, GL_RED_INTEGER, GL_INT, texels.data());
glUseProgram(0);
glDeleteProgram(program);
return texels;
}
void ExpectAllTexels(const char* what, int channel, std::int32_t expected,
const std::vector<std::int32_t>& texels) {
if (texels.empty()) return;
std::size_t offenders = 0;
std::int32_t firstBad = 0;
for (const std::int32_t texel : texels) {
if (texel == expected) continue;
if (offenders == 0) firstBad = texel;
++offenders;
}
EXPECT_EQ(offenders, 0u) << what << " component " << channel << " returned " << firstBad
<< " instead of " << expected << " (" << offenders << " of " << texels.size()
<< " texels wrong)";
}
// Every component of the border, in one place, so both the texture-object and the
// sampler-object case assert exactly the same thing.
void ExpectBorderIsDelivered(const char* what) {
for (int channel = 0; channel < 4; ++channel) {
// (-0.5, -0.5) is a full texture width outside the image on both axes, so
// CLAMP_TO_BORDER can only answer with the border colour.
const std::vector<std::int32_t> texels = RenderChannel(channel, -0.5f, -0.5f);
EXPECT_EQ(FirstGLError(), 0u) << what << ": the border draw left a GL error behind";
ExpectAllTexels(what, channel, kBorderColor[channel], texels);
}
}
// A narrow-format source built on demand, for the clamp cases. Returns the texture, which
// the caller owns until TearDown deletes it through m_narrowTexture.
void MakeNarrowSource(GLenum internalFormat, GLenum clientFormat, const void* texels,
const GLint* border, bool borderIsUnsigned) {
glGenTextures(1, &m_narrowTexture);
glBindTexture(GL_TEXTURE_2D, m_narrowTexture);
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, 2, 2);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 2, clientFormat,
internalFormat == GL_R8UI ? GL_UNSIGNED_BYTE : GL_BYTE, texels);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
if (borderIsUnsigned) {
const GLuint asUnsigned[4] = {static_cast<GLuint>(border[0]), static_cast<GLuint>(border[1]),
static_cast<GLuint>(border[2]), static_cast<GLuint>(border[3])};
glTexParameterIuiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, asUnsigned);
} else {
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border);
}
ASSERT_EQ(FirstGLError(), 0u) << "narrow source setup left a GL error behind";
}
// The narrow sources are single-channel, so only component 0 carries anything, and the
// sampler declaration has to match the format's signedness.
std::vector<std::int32_t> RenderNarrowBorder(bool isUnsignedSampler) {
const std::string fragment =
std::string("#version 330 core\n\nuniform ") + (isUnsignedSampler ? "usampler2D" : "isampler2D") +
" smp;\nuniform vec2 uCoord;\n\nout int out_color;\n\nvoid main()\n{\n"
" out_color = int(texture(smp, uCoord).x);\n}\n";
std::string error;
const unsigned int program = CompileProgram(kVertexSource, fragment.c_str(), &error);
if (program == 0) {
ADD_FAILURE() << "narrow-border program did not build: " << error;
return {};
}
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glViewport(0, 0, kOutputWidth, kOutputHeight);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
const GLint clearValue[4] = {-559038737, 0, 0, 0};
glClearBufferiv(GL_COLOR, 0, clearValue);
glUseProgram(program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_narrowTexture);
glUniform1i(glGetUniformLocation(program, "smp"), 0);
glUniform2f(glGetUniformLocation(program, "uCoord"), -0.5f, -0.5f);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
std::vector<std::int32_t> texels(static_cast<std::size_t>(kOutputWidth) * kOutputHeight, 0);
glReadPixels(0, 0, kOutputWidth, kOutputHeight, GL_RED_INTEGER, GL_INT, texels.data());
glUseProgram(0);
glDeleteProgram(program);
return texels;
}
GLuint m_sourceTexture = 0;
GLuint m_outputTexture = 0;
GLuint m_fbo = 0;
GLuint m_vao = 0;
GLuint m_sampler = 0;
GLuint m_narrowTexture = 0;
};
} // namespace
// The floor, and the control that keeps the two tests below from passing vacuously: an INSIDE
// sample has to fetch the texture's own texel. If this fails the sampler, the shader or the
// integer readback is broken and nothing about the border colour has been measured.
TEST_F(IntegerBorderColorScenario, InsideTexelsAreUnaffectedByTheBorderColour) {
if (!Ready()) GTEST_SKIP();
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, kBorderColor);
ASSERT_EQ(FirstGLError(), 0u) << "glTexParameterIiv(GL_TEXTURE_BORDER_COLOR) was rejected";
for (int channel = 0; channel < 4; ++channel) {
const std::vector<std::int32_t> texels = RenderChannel(channel, 0.5f, 0.5f);
EXPECT_EQ(FirstGLError(), 0u) << "the inside draw left a GL error behind";
ExpectAllTexels("inside sample", channel, kInsideTexel[channel], texels);
}
Gl().EndFrame();
}
// The regression, texture-object spelling. glTexParameterIiv is the entry point the frontend
// already accepted and then flattened into the same FloatVec4 every other spelling wrote.
TEST_F(IntegerBorderColorScenario, TexParameterIivBorderColourSurvivesToAnIntegerSampler) {
if (!Ready()) GTEST_SKIP();
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, kBorderColor);
ASSERT_EQ(FirstGLError(), 0u) << "glTexParameterIiv(GL_TEXTURE_BORDER_COLOR) was rejected";
ExpectBorderIsDelivered("glTexParameterIiv");
Gl().EndFrame();
}
// The regression, sampler-object spelling - which is the one the conformance cases actually use,
// and a separate code path in both backends (BackendSamplerObject::Sync on DirectGLES, and the
// sampler cache key on DirectVulkan, where a border colour that is not part of the key would
// alias two samplers that differ only in it).
TEST_F(IntegerBorderColorScenario, SamplerParameterIivBorderColourSurvivesToAnIntegerSampler) {
if (!Ready()) GTEST_SKIP();
glGenSamplers(1, &m_sampler);
ASSERT_NE(m_sampler, 0u);
glSamplerParameteri(m_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glSamplerParameteri(m_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glSamplerParameterIiv(m_sampler, GL_TEXTURE_BORDER_COLOR, kBorderColor);
ASSERT_EQ(FirstGLError(), 0u) << "sampler-object setup was rejected";
// The texture object carries a DIFFERENT border colour, so a pass here cannot come from the
// texture's own state leaking through: GL 4.6 core 8.10 says a bound sampler object's state
// wins over the texture's for every sampling parameter.
const std::int32_t decoyBorder[4] = {0, 0, 0, 0};
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, decoyBorder);
glBindSampler(0, m_sampler);
ASSERT_EQ(FirstGLError(), 0u) << "binding the sampler object was rejected";
ExpectBorderIsDelivered("glSamplerParameterIiv");
glBindSampler(0, 0);
Gl().EndFrame();
}
// GL 4.6 core 8.14.2: "For floating-point and integer formats, border values are clamped to the
// representable range of the format." A border of 300 on a GL_R8I texture is 127, not 300 - and
// VK_BORDER_COLOR_INT_CUSTOM_EXT delivers whatever it is handed, with format VK_FORMAT_UNDEFINED
// there is nothing for the driver to clamp against, so the clamp has to happen before the value
// leaves MobileGL. DirectGLES gets it right for free (the ES driver knows the texture format),
// which is what makes this a cross-backend divergence and not only a spec one.
TEST_F(IntegerBorderColorScenario, ASignedIntegerBorderIsClampedToTheFormatsRepresentableRange) {
if (!Ready()) GTEST_SKIP();
const std::int8_t texels[4] = {1, 1, 1, 1};
const GLint border[4] = {300, 0, 0, 1};
MakeNarrowSource(GL_R8I, GL_RED_INTEGER, texels, border, /*borderIsUnsigned=*/false);
const std::vector<std::int32_t> sampled = RenderNarrowBorder(/*isUnsignedSampler=*/false);
EXPECT_EQ(FirstGLError(), 0u) << "the clamped-border draw left a GL error behind";
ExpectAllTexels("R8I border 300", 0, 127, sampled);
Gl().EndFrame();
}
// The reciprocal half, and the one that decides how the two integer forms relate: -1 written
// through glTexParameterIiv against an UNSIGNED format. GL 4.6 core 8.10 stores an "I"-form
// border unmodified with an integer internal data type and defines no sign conversion between
// the two integer forms, so the stored bits are reinterpreted in the sampled format's own
// signedness: 0xFFFFFFFF, clamped to the format's maximum of 255.
//
// That is the DRIVER's answer, established by running this case rather than by reading the spec:
// clamping to 0 is an equally defensible reading of the same paragraph, and DirectVulkan can be
// made to produce either - but DirectGLES forwards the value to the ES driver verbatim and cannot
// deviate, so choosing 0 would mean the same program sampling 0 on Magma and 255 on Espryt. The
// whole point of carrying the border colour's form is to stop that class of divergence, so the
// backends agree on the driver's answer.
//
// The clamp itself is still doing the work: without it the value reaches the driver as
// 0xFFFFFFFF against a format whose maximum is 255, with format VK_FORMAT_UNDEFINED and so
// nothing for the driver to clamp against.
TEST_F(IntegerBorderColorScenario, ANegativeBorderOnAnUnsignedFormatClampsToTheFormatsMaximum) {
if (!Ready()) GTEST_SKIP();
const std::uint8_t texels[4] = {1, 1, 1, 1};
const GLint border[4] = {-1, 0, 0, 1};
MakeNarrowSource(GL_R8UI, GL_RED_INTEGER, texels, border, /*borderIsUnsigned=*/false);
const std::vector<std::int32_t> sampled = RenderNarrowBorder(/*isUnsignedSampler=*/true);
EXPECT_EQ(FirstGLError(), 0u) << "the clamped-border draw left a GL error behind";
ExpectAllTexels("R8UI border -1", 0, 255, sampled);
Gl().EndFrame();
}
// The same clamp from the unambiguous side: a value written through the UNSIGNED form that is
// simply too large for the format. No sign reinterpretation is involved, so both backends and
// the spec agree that 5000 on a GL_R8UI texture is 255.
TEST_F(IntegerBorderColorScenario, AnOversizedUnsignedBorderIsClampedToTheFormatsMaximum) {
if (!Ready()) GTEST_SKIP();
const std::uint8_t texels[4] = {1, 1, 1, 1};
const GLint border[4] = {5000, 0, 0, 1};
MakeNarrowSource(GL_R8UI, GL_RED_INTEGER, texels, border, /*borderIsUnsigned=*/true);
const std::vector<std::int32_t> sampled = RenderNarrowBorder(/*isUnsignedSampler=*/true);
EXPECT_EQ(FirstGLError(), 0u) << "the clamped-border draw left a GL error behind";
ExpectAllTexels("R8UI border 5000", 0, 255, sampled);
Gl().EndFrame();
}
} // namespace MGITest

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