Compare commits

..
876 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
Swung0x48 cee17025a0 [Test] (MG_Test): pin the plain mutable-texture copy every new validator rule is a new way to reject 2026-08-13 03:17:27 -04:00
Swung0x48 9642ae4d20 [Fix, Test] (MG_Impl/Texture, MG_Backend, MG_Test): copy_image compatibility is texel-block size, and neither backend was told which slice to copy 2026-08-13 03:17:27 -04:00
swung0x48 595d140036 Merge branch "feat/cts-copyimage-magma-layers" into dev 2026-08-13 03:11:21 -04:00
swung0x48 b62d1f2078 [Fix, Test] (MG_Backend/DirectVulkan, MG_IntegrationTest): map a layered CopyImageSubData onto the axis each endpoint keeps its slices on, instead of copying slice 0 and calling it done 2026-08-13 02:49:57 -04:00
swung0x48 5e82ff968a Merge branch "feat/cts-rgb9e5-raw-transfer" into dev 2026-08-13 02:47:18 -04:00
swung0x48 6a80a82dd3 [Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_Backend/DirectVulkan, MG_Test): a packed texel whose client type already spells its storage word must cross glGetTexImage unencoded, or the RGB9_E5 shared exponent gets canonicalized 2026-08-13 02:41:37 -04:00
swung0x48 f9182a5ca3 Merge branch "fix/cts-copyimage-level-validation" into dev 2026-08-13 02:38:43 -04:00
swung0x48 f37b511fca [Fix, Test] (MG_Impl/GLImpl, MG_Backend/DirectVulkan, MG_Backend/DirectGLES, MG_Test, MG_IntegrationTest): a glCopyImageSubData to a mipmap level the texture never had is INVALID_VALUE, not a subresource handed to the driver 2026-08-13 02:28:55 -04:00
swung0x48 38027d21f8 Merge branch "feat/log-level-semantics" into dev 2026-08-13 02:19:26 -04:00
Swung0x48 dd2a62228f [Fix, Feat, Test] (MG_Util, MG_Backend, MG_Impl, MG_State): correct the log severity ordering and unwind the diagnostics it silenced 2026-08-13 02:02:49 -04:00
swung0x48 373aa44dd7 Merge branch "fix/cts-tess-ssbo-crash" into dev 2026-08-13 01:38:58 -04:00
swung0x48 96646df12e [Fix] (MG_Backend/DirectVulkan): review round - failure diagnostics take MGLOG_E, the once-per-patch-size success note takes MGLOG_D, and the per-draw refusal stays latched 2026-08-13 01:34:57 -04:00
swung0x48 f20b20e643 [Fix, Feat, Test] (MG_Backend/DirectVulkan, MG_Test): synthesize the pass-through tessellation control stage GL gives an evaluation-only program, and refuse the half-tessellated pipeline Mali dereferences null inside 2026-08-13 01:28:35 -04:00
swung0x48 2587814970 Merge branch "feat/cts-image-format-qualifier" into dev 2026-08-12 19:23:51 -04:00
swung0x48 43398e33e8 [Fix, Test] (MG_Util, MG_IntegrationTest): review round - anchor a clone only behind a real definition, decline the atomic-image shape, and say what the software lanes cannot falsify 2026-08-12 19:12:39 -04:00
swung0x48 b7557d6615 [Feat, Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_IntegrationTest): bake the bound image format into the ESSL a format-less image declaration needs 2026-08-12 18:56:31 -04:00
swung0x48 bce34d7fac Merge branch "feat/cts-followup-fp64-ds-imgbuf" into dev 2026-08-12 17:56:13 -04:00
swung0x48 f91857266f [Fix, Test] (MG_Backend/DirectGLES, MG_Test): the tessellation stages never got the flat qualifier an ES linker compares across both sides of every integer interface 2026-08-12 17:50:31 -04:00
swung0x48 49cb1be0fd [Fix, Test] (MG_Backend/DirectGLES, MG_IntegrationTest): a shader writing a buffer texture through an image unit left the CPU shadow stale, so every map and readback after it saw the old bytes 2026-08-12 17:50:31 -04:00
swung0x48 a51c68bb2c [Fix, Test] (MG_Util, MG_IntegrationTest): a 64-bit float constant was read as 32 bits, so every comparison against a round double became an epsilon test against zero 2026-08-12 17:50:25 -04:00
swung0x48 1c723a6cfc Merge branch "feat/cts-image-targets" into dev 2026-08-12 16:52:34 -04:00
swung0x48 44805bfa07 [Fix, Test] (MG_Util, MG_Backend/DirectVulkan, MG_IntegrationTest): adversarial review - the rewritten 1D-array image collided with a module's own 2D-array one and left an invalid duplicate type; pin the component order, keep the subject kinds in a truncated matrix 2026-08-12 16:49:59 -04:00
swung0x48 257fcbfd0b [Fix, Test] (MG_Util, MG_Backend/DirectVulkan, MG_IntegrationTest): review round - one module parse for the shaders with no 1D-array image, per-kind values the combined case can name, and the invariants two shared buffers rest on 2026-08-12 16:43:29 -04:00
swung0x48 2b46a3db96 [Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_Backend/DirectVulkan): the two image target kinds a compute dispatch could not read - 1D-array on ES, imageBuffer on Vulkan 2026-08-12 16:11:10 -04:00
swung0x48 0b36621069 Merge branch "feat/cts-diag-wave" into dev 2026-08-12 15:33:43 -04:00
swung0x48 9ee2e0a1db [Fix] (MG_Backend/DirectGLES): re-push the texture parameters a regenerated driver texture has lost, instead of trusting caches that outlived it 2026-08-12 15:22:39 -04:00
swung0x48 6b6623ae72 [Fix, Test] (MG_Backend, MG_Impl, MG_State): advertise the uniform-block and stencil-texturing strings a 4.0 context hides behind, forward DEPTH_STENCIL_TEXTURE_MODE to both backends, and stop a reserved transform feedback name passing for an object 2026-08-12 15:12:01 -04:00
swung0x48 a6e029734b [Test, Doc] (MG_IntegrationTest, MG_Backend/DirectGLES): review wave - stop the skip paths leaking their GL objects, normalise the clip enables a neighbouring scenario leaves behind, and state what the software lanes cannot falsify 2026-08-12 13:57:32 -04:00
swung0x48 e7d6bfddac [Fix, Test] (MG_Backend/DirectGLES, MG_State, MG_Util): publish the real default framebuffer depth/stencil format, forward the clip distance enables, and stop trusting a stale format probe for non-2D depth attachments 2026-08-12 13:34:39 -04:00
swung0x48 f2c879528f Merge branch "feat/es-ds-readback" into dev 2026-08-12 12:12:49 -04:00
Swung0x48 eaba4ac1dc [Fix, Docs] (MG_Backend/DirectGLES): drain the attachment-format probe, and document the depth/stencil readback emulation switch 2026-08-12 12:09:45 -04:00
Swung0x48 ed6578954e [Feat, Fix, Test] (MG_Backend/DirectGLES, MG_IntegrationTest): emulate ES depth/stencil readback by shader sampling, and stop the replicate blit leaking its borrowed texture unit 2026-08-12 11:38:34 -04:00
swung0x48 e005c8b6cb [Test] (tools/trace_replay): drop the minecraft-26.2-main-menu fixture 2026-08-12 11:07:38 -04:00
swung0x48 535b5e3095 Merge branch "feat/cts-xfb-respec-compressed" into dev 2026-08-12 10:35:09 -04:00
swung0x48 1b05a84928 [Fix, Test] (MG_State, MG_Backend, MG_Impl, MG_Test): review wave - hand the mapping back instead of renewing it, retire an immutable ES store on respecify, and tag glTexStorage2D levels too 2026-08-12 10:32:39 -04:00
swung0x48 7ccb762936 [Fix, Test] (MG_Impl, MG_Test): a persistently mapped unpack buffer is a legal source for the compressed uploads too 2026-08-12 09:24:57 -04:00
swung0x48 442e7eec1c [Docs] (MG_IntegrationTest): the capture-buffer note described a bug that is fixed and pinned elsewhere now 2026-08-12 09:22:17 -04:00
swung0x48 2787d15706 [Fix] (MG_Impl): the compressed sub-image bounds check added two application-supplied ints 2026-08-12 09:19:50 -04:00
swung0x48 74ce58a6c7 [Fix] (MG_Backend/DirectGLES): a declined map renewal left the respecify pointing at immutable storage 2026-08-12 09:17:54 -04:00
swung0x48 bb122ebd4f [Test] (MG_IntegrationTest): the DSA enable-disable-attributes case, reassembled where it can be stepped through 2026-08-12 08:56:13 -04:00
swung0x48 9b0ed5b3af [Fix, Feat, Test] (MG_Impl, MG_Test): a specific compressed internalformat now tags the level it defines, and the compressed sub-image entry points stopped being stubs 2026-08-12 08:52:01 -04:00
swung0x48 0e31c1481b [Fix, Test] (MG_State, MG_Backend/DirectVulkan, MG_Backend/DirectGLES, MG_IntegrationTest): a respecified capture buffer left the transform feedback writing one store and the readback reading another 2026-08-12 08:42:45 -04:00
swung0x48 811f32760e [CI] (test): gate the retrace pipeline on the integration lane - build-retrace and trace-cases now need it 2026-08-12 08:41:19 -04:00
swung0x48 01381a0404 Merge branch "feat/cts-baseinstance-dsa-bufstorage" into dev 2026-08-12 08:23:57 -04:00
swung0x48 0f02b0fdb1 [Fix] (MG_Util): the POST base-instance row described an emulation that no longer exists 2026-08-12 08:22:26 -04:00
swung0x48 3be02abf47 [Fix] (MG_Impl): multisample renderbuffers ignored the integer formats own sample limit 2026-08-12 08:17:11 -04:00
swung0x48 f91d6b676c [Fix] (MG_Impl): the per-stage atomic-counter-buffer limits answered GL_INVALID_ENUM instead of their zero 2026-08-12 08:12:32 -04:00
swung0x48 1ebf191f94 [Fix, Test] (MG_Backend/DirectGLES, MG_Util): baseInstance reached the shader but never the vertex fetch 2026-08-12 08:12:27 -04:00
swung0x48 ccad803023 Merge branch "feat/cts-vab-current-attrib" into dev 2026-08-12 07:40:47 -04:00
swung0x48 21159caf31 [Fix, Test] (MG_Util, MG_Backend/DirectGLES): transform feedback captured nothing at all from an interface block 2026-08-12 07:38:50 -04:00
swung0x48 ea4819a21d [Fix, Test] (MG_State, MG_IntegrationTest): an array vertex input occupies one location per element, not one location in total 2026-08-12 07:36:12 -04:00
swung0x48 6b882b3ccf Merge branch "feat/fp64-demote-to-fp32" into dev 2026-08-12 06:42:23 -04:00
swung0x48 96bd36c50b [Docs] (README): the MOBILEGL_ADVERTISE_FP64 switch 2026-08-12 06:41:54 -04:00
swung0x48 46fbd837b3 [Fix, Test] (MG_State, MG_Impl, MG_IntegrationTest): a double uniform initializer no longer reads zero 2026-08-12 06:41:54 -04:00
swung0x48 796a57a115 [Feat, Test] (MG_Backend, MG_Util, MG_Impl): report the fp64 tier at startup, decline 64-bit vertex formats everywhere, advertise GL_ARB_gpu_shader_fp64 only on request 2026-08-12 06:25:35 -04:00
swung0x48 62a2dae5ba [Fix, Test] (MG_Impl, MG_State, MG_IntegrationTest): glUniform*d stores what the demoted shader reads 2026-08-12 06:20:11 -04:00
swung0x48 532836c058 [Feat, Test] (MG_Util): demote every 64-bit float in a shader to 32 bits, with the block layout re-derived 2026-08-12 06:11:50 -04:00
swung0x48 2fced2241b [Fix, Test] (MG_Util, MG_State, MG_Backend): the GL 4.3 vertex binding model - array vertex inputs, zero binding strides, instance divisors, and formats ES refuses 2026-08-12 05:36:00 -04:00
swung0x48 21b5fc2d92 Merge branch 'feat/cts-draw-parameters' into dev 2026-08-12 04:24:54 -04:00
swung0x48 1f753ab5fa [Test] (MG_IntegrationTest): the draw-parameter builtins, read back out of the shader the draw produced 2026-08-12 04:18:08 -04:00
swung0x48 3ed9501be5 [Fix, Feat, Test] (MG_Backend/DirectGLES, MG_Test): the draw-parameter builtins never reached the draws that carry them, and glMultiDrawArraysIndirectCount had no backend at all 2026-08-12 04:18:08 -04:00
swung0x48 7311251f30 [Fix] (MG_Util, MG_Backend/DirectVulkan): GL reads gl_BaseVertex as zero on a non-indexed draw where Vulkan's builtin hands over firstVertex 2026-08-12 04:18:07 -04:00
swung0x48 7625cf450d [Fix, Test] (MG_Impl, MG_IntegrationTest): a block count may not exceed its binding points, and an atomic-counter buffer size must survive division by them 2026-08-12 02:38:04 -04:00
swung0x48 450eb209b6 [Fix, Test] (MG_State, MG_Impl): the program interface of a separable program is its own first and last stage, not vertex and fragment 2026-08-12 02:38:04 -04:00
swung0x48 8c5c39b3c3 [Feat, Test] (MG_Impl): glBindTextures and glBindImageTextures were no-op stubs while GL_ARB_multi_bind was advertised 2026-08-12 02:38:03 -04:00
swung0x48 64a0ea397c [Perf] (CI): rewrite one rolling ccache entry per job on the default branch instead of saving one per run 2026-08-12 01:56:17 -04:00
swung0x48 205d837942 [Fix, Test] (MG_Backend/DirectGLES, MG_Util, CI): keep an explicit LOD constant under a new avoid flag - folding the bias uniform into it kills the ANGLE llvmpipe JIT 2026-08-12 01:42:55 -04:00
swung0x48 b5e9339c66 [Fix] (MG_Util): the unsupported-buffer-textures log tells a too-old driver apart from one whose entry point failed to resolve 2026-08-12 01:24:41 -04:00
swung0x48 e310e3e9ff [Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_Test): resolve buffer textures through the entry point the tier ships, not the ES 3.2 core name; bound the OES retarget to an exact extension name 2026-08-12 01:24:40 -04:00
swung0x48 a0bf4a83bc [Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_Test, MG_IntegrationTest): detect buffer-texture support, emit the directive the driver advertises, and name the capability when it is missing 2026-08-12 01:24:40 -04:00
swung0x48 97facf777b [Fix] (MG_Backend/DirectVulkan): act on a failed binding remap, bound the storage-block array count, and correct the decline rationale review found describing the reverted mechanism 2026-08-12 01:09:56 -04:00
swung0x48 5cfbb716c0 Merge remote-tracking branch 'origin/dev' into feat/sampler-array-descriptors 2026-08-12 00:58:20 -04:00
swung0x48 64c3411d70 [Perf] (CI): restore unchanged trace fixtures from the Actions cache keyed on their Git LFS pointer oid, downloading only on a miss 2026-08-12 00:49:47 -04:00
swung0x48 f3a0d9e0a3 [Refactor] (CI): move the trace fixture LFS pointer and verification helpers into a shared library 2026-08-12 00:49:47 -04:00
swung0x48 068786e812 [Fix, Test] (MG_Backend/DirectVulkan, MG_IntegrationTest): a declined descriptor must refuse the draw, not just leave the layout - a multi-dimensional sampler array otherwise faulted in the shader 2026-08-12 00:45:36 -04:00
swung0x48 8e7cc62c24 [Test] (MG_Benchmark): a standing UnorderedMap observatory - the container's own shapes, measured through the alias so a future swap re-runs the same suite 2026-08-12 00:40:57 -04:00
swung0x48 d2a36d65a3 [Fix, Test] (MG_Backend/DirectVulkan, MG_IntegrationTest): a sampler uniform array is one binding with many descriptors too - Magma wrote only element zero, and declines the multi-dimensional shape loudly 2026-08-12 00:31:37 -04:00
swung0x48 a020de76e3 Merge remote-tracking branch 'origin/dev' into feat/sampler-array-descriptors 2026-08-12 00:18:51 -04:00
swung0x48 574634adfa [Fix, Test] (MG_Backend/DirectVulkan, MG_IntegrationTest): let the binding remap accept the descriptor arrays that now have per-element paths, and reserve image-info scratch for them 2026-08-12 00:14:18 -04:00
swung0x48 e71d715e1a [Fix, Test] (MG_Backend/DirectVulkan, MG_Util, MG_Test): VkProgramObject move dropped stageSpirvDigests - a robin-hood swap would pair one program's modules with another's digests; sanity tests assert key identity, not iteration counts 2026-08-12 00:11:56 -04:00
swung0x48 7b946fd527 [Fix] (MG_Backend/DirectVulkan, MG_Util): RenderPassEntry needs move assignment - robin-hood probing swaps entries, so mapped types must be swappable 2026-08-12 00:11:55 -04:00
swung0x48 8f3ce5f5b7 [Fix] (MG_Backend/DirectGLES, MG_State, Docs): CopyImageSubData held a registry reference across a re-entrant sync; retarget container rationales at the new erase contract 2026-08-12 00:11:54 -04:00
swung0x48 21ec744ef2 [Refactor] (MG_Util, MG_Test): retire FastSTL for ska::flat_hash_map, the table MobileGlues settled on 2026-08-12 00:11:54 -04:00
swung0x48 faa7b17da3 [Test] (tools/trace_replay): drop the minecraft-26.2-in-world fixture 2026-08-12 00:03:03 -04:00
swung0x48 f6849fc0b3 [Fix, Test] (MG_Backend/DirectVulkan, MG_IntegrationTest): an image uniform array is one binding with many descriptors, not one - Magma wrote only element zero and left the rest undefined 2026-08-11 23:49:31 -04:00
swung0x48 4d1d4f6225 [Fix] (DirectGLES): report a rejected shader and a failed link at MGLOG_I - MGLOG_E is compiled out of every INFO build, which is why the retrace artifact could not say which shader the driver refused 2026-08-11 23:39:48 -04:00
swung0x48 cef81df73f [Fix, Test] (ShaderTranspiler, DirectGLES): fold or lower dynamically indexed fragment outputs before ESSL emission - GLSL ES requires constant integral indices, so the OIT coefficient shader linked nothing on ANGLE and every translucent draw was a silent no-op 2026-08-11 23:39:03 -04:00
swung0x48 c4e6ea1f23 [Docs] (tools/trace_replay): strip investigation logs from the README - usage and procedures only 2026-08-11 23:25:20 -04:00
swung0x48 b4e07ce651 [Fix, Test] (MG_State, MG_Backend/DirectGLES, MG_Test): close three holes review found in the composite uniform mirror - unrecorded stage programs, byte-identical writes, and Espryt's baked image units 2026-08-11 23:19:22 -04:00
swung0x48 ff324057ad [Fix, Perf, Test] (MG_State, MG_Impl/GLImpl, MG_Test, MG_IntegrationTest): a pipeline composite mirrors only the uniforms a stage was written to, and survives a sampler or block rebinding 2026-08-11 23:11:13 -04:00
swung0x48 ef4c6dbe0a Merge branch 'feat/cts-420pack-wave' into dev 2026-08-11 23:07:45 -04:00
swung0x48 19fc7346c5 [Fix] (MG_Backend/DirectVulkan): a re-entrant cache lookup dangled the reference glShaderStorageBlockBinding then wrote through 2026-08-11 23:05:21 -04:00
swung0x48 bcb0e894ef [Fix, Test] (MG_State, MG_Backend/DirectGLES, MG_Util, MG_IntegrationTest, MG_Test): array layout(binding=N) elements bind consecutively, a read+write image reaches ESSL legally, compute local_size comes from the linked intermediate, and glShaderStorageBlockBinding is baked into the generated source 2026-08-11 23:05:21 -04:00
swung0x48 794c10e56c [Fix] (3rdparty/glslang): bump the fork to the invariant-placement and atomic-counter-lowering patches 2026-08-11 23:05:20 -04:00
swung0x48 28390667d7 [Docs] (tools/trace_replay): sundial-lite's sampler state is identical on both stacks - the crash is a driver defect, not a dangling binding 2026-08-11 23:04:25 -04:00
swung0x48 90dd9bec77 [Docs] (tools/trace_replay): sundial-lite crashes in llvmpipe's JIT on a NULL per-texture-unit descriptor 2026-08-11 22:54:50 -04:00
swung0x48 7994ca31d3 [Docs] (tools/trace_replay): sundial-lite has no output-array indexing - separate cause from improved-transparency, and it reproduces locally 2026-08-11 22:48:51 -04:00
swung0x48 5705e05156 [Docs] (tools/trace_replay): sundial-lite kills the emulator via a host llvmpipe segfault, not OOM 2026-08-11 22:38:14 -04:00
swung0x48 ab62f81545 [Docs] (tools/trace_replay): the local farm ANGLE is not the CI ANGLE - re-attribute the improved-transparency causes 2026-08-11 22:32:14 -04:00
swung0x48 f6cf04d6d7 [Docs] (tools/trace_replay): name the two generated-ESSL defects that empty the improved-transparency lane on ANGLE 2026-08-11 22:25:57 -04:00
swung0x48 38eb9589f9 [Test] (tools/trace_replay): dump every live framebuffer attachment at a call boundary 2026-08-11 22:25:57 -04:00
swung0x48 99ebf67a3d [Fix] (CMake): the Android API guard holds a floor of 26, not an exact pin - higher platforms may configure, the minSdk-26 gradle build enforces API usage 2026-08-11 22:08:54 -04:00
swung0x48 2292e99476 Merge remote-tracking branch 'origin/dev' into dev 2026-08-11 21:59:25 -04:00
swung0x48 4c5afecc71 Merge branch 'fix/drop-android-api-pin' into dev 2026-08-11 21:59:05 -04:00
swung0x48 1c5744f2be [Fix] (MobileGL, Build): drop the __ANDROID_API__ source pin; hold API 26 in CMake instead 2026-08-11 21:58:30 -04:00
swung0x48 22859b0958 [Docs] (tools/trace_replay): localise the improved-transparency ANGLE divergence to the OIT composite 2026-08-11 21:55:40 -04:00
swung0x48 7aa958fbc9 [Docs] (tools/trace_replay): reproduce the Android DirectGLES lane on Linux, and pin improved-transparency below MobileGL 2026-08-11 21:43:56 -04:00
swung0x48 a4bd4e04a1 Merge branch 'fix/ssbo-rc13' into dev 2026-08-11 21:11:38 -04:00
swung0x48 09459edb6b [Fix] (CI, android-plugin): an ANGLE window surface lost before init finishes is infrastructure, not a trace failure 2026-08-11 21:00:31 -04:00
swung0x48 8af6ebc174 [Fix] (MG_State): seed uniform initializers through the phase-A gate, not off the raw artifact block 2026-08-11 20:59:25 -04:00
swung0x48 577cd8c670 [Fix] (3rdparty/glslang): bump the fork to the uniform-initializer preservation patch 2026-08-11 20:56:59 -04:00
swung0x48 5267243404 [Test] (MG_IntegrationTest): pin declared uniform initializers end to end, on both backends 2026-08-11 20:56:07 -04:00
swung0x48 33c2715912 [Fix] (MG_State, MG_Util): a default-block uniform starts at its declared initializer instead of zero 2026-08-11 20:54:14 -04:00
swung0x48 3068cdadf8 [Test] (CI): keep the emulator log of the attempt that lost the emulator, and record host memory pressure 2026-08-11 20:51:54 -04:00
swung0x48 6dd0201bf2 [Fix, Test] (MG_Impl/GLImpl): an indexed buffer SIZE query reports the range that was asked for, not what the buffer currently holds 2026-08-11 20:43:28 -04:00
swung0x48 05bef7118b [Fix, Test] (MG_State): a pipeline draw uses the block bindings its stage programs were given, not the ones their shaders declared 2026-08-11 20:23:06 -04:00
swung0x48 94e75fef79 [Fix, Test] (MG_State, MG_Impl): glIsProgramPipeline answers for the first bind, not for the materialization every pipeline command now does 2026-08-11 20:10:18 -04:00
swung0x48 43bcd03dca [Fix] (MG_Backend/DirectVulkan, MG_Impl): a malformed graphics pipeline shape skips the draw instead of faulting inside the driver 2026-08-11 20:10:18 -04:00
swung0x48 2ce0595fab [Fix, Test] (MG_State, MG_Impl, MG_Backend): a program pipeline's compute stage is dispatched on its own, and the graphics composite draws its stage programs' uniform values 2026-08-11 20:10:17 -04:00
swung0x48 ba9af18033 [Test] (MG_IntegrationTest): park the two program-pipeline cases whose mechanisms are still open 2026-08-11 20:10:17 -04:00
swung0x48 f7d63f88fa [Fix, Test] (MG_State, MG_Impl): a reserved program-pipeline name takes state from UseProgramStages and its siblings instead of rejecting them 2026-08-11 20:10:16 -04:00
swung0x48 6cf5a7744e [Test] (MG_IntegrationTest): separable programs drawn through a program pipeline object 2026-08-11 20:10:16 -04:00
swung0x48 ca3d24f5ea [Fix] (CI, android-plugin): keep a failed Android retrace's result artifact, and echo the logs a failed replay already wrote 2026-08-11 19:30:38 -04:00
swung0x48 cc34d34706 [Fix, Test] (MG_Backend/DirectGLES): hand every compiled shader's lifetime to its program - glDeleteShader was never called, so each program build leaked its driver shader objects 2026-08-11 11:11:28 -04:00
swung0x48 757b31592d [Fix, Test] (MG_Backend/DirectVulkan): a multisample resolve that must also change orientation resolves through a pooled scratch image, then blits 2026-08-11 10:49:58 -04:00
swung0x48 dbae4eda10 [Fix, Test] (MG_Backend/DirectVulkan): depth blits to or from the default framebuffer convert their rect out of GL's bottom-origin space, like the colour blit does 2026-08-11 10:24:59 -04:00
swung0x48 fa5ff5d168 [Test] (MG_IntegrationTest): every texture access routine must read the same texel out of a usampler2DArray 2026-08-11 10:21:38 -04:00
swung0x48 994ae372f8 [Fix, Test] (MG_Backend/DirectVulkan): an SSBO block instance array is one binding of N descriptors, so bind every element from its own GL binding point 2026-08-11 10:11:23 -04:00
swung0x48 7ba012adf9 [Test] (MG_IntegrationTest): SSBO runtime-array length across preambles, block arrays and bound ranges 2026-08-11 10:11:23 -04:00
swung0x48 b1fdffd767 [Fix, Test] (MG_Backend/DirectVulkan): read the default framebuffer's depth and stencil back instead of leaving the caller's buffer untouched 2026-08-11 10:11:22 -04:00
swung0x48 18c17ae5ca [Fix, Test] (MG_Backend/DirectVulkan): a blit into the default framebuffer must execute the clear parked before it, not leave it for the readback 2026-08-11 10:08:25 -04:00
swung0x48 16c010985f [Fix, Test] (MG_Impl): multi-bind name rejection is per element, and only transform feedback constrains the range size to a multiple of four 2026-08-11 09:17:51 -04:00
swung0x48 6f64ec0f51 [Fix, Test] (MG_Impl): validate indirect-dispatch and indirect-count arguments before the backend-availability check 2026-08-11 09:12:49 -04:00
swung0x48 5d47698349 [Fix, Test] (MG_Backend/DirectVulkan): materialize the default framebuffer's pending clear before a readback, alpha included 2026-08-11 09:09:31 -04:00
swung0x48 7b593e39ef [Fix, Test] (MG_Impl, MG_State): negative-path GL errors for multi_bind, indirect_parameters, texture_storage, compute dispatch/link and buffer-range alignment; indexed getters answer the full pname table 2026-08-11 09:02:22 -04:00
swung0x48 d83b4dbbb5 [Fix, Test] (MG_State, MG_Impl): ARB_vertex_attrib_binding state model - spec stride default, legacy stride/pointer shadows, divisor re-binds, core-profile VAO-0 rejection 2026-08-11 08:49:37 -04:00
swung0x48 964a7fcc92 [Fix, Test] (MG_State, MG_Backend/DirectVulkan): resolve transform-feedback captures that name a member of an output interface block 2026-08-11 08:39:39 -04:00
swung0x48 5a7bd9942d [Fix] (MG_Backend/DirectVulkan): print VkShaderModule as a 64-bit value - the const void* cast is ill-formed on 32-bit ABIs where the handle is a plain uint64_t 2026-08-11 08:19:31 -04:00
swung0x48 21a43bf6a4 [Fix, Test] (MG_Backend/DirectVulkan): re-land the gl_FragCoord default-framebuffer origin fix - the suspected slowdown was a mismeasurement, paired timings are within 1% 2026-08-11 07:54:24 -04:00
swung0x48 5b6dec2d81 [Revert] (MG_Backend/DirectVulkan): back out the gl_FragCoord default-framebuffer origin fix - correct, but it costs DirectVulkan a large order-dependent slowdown that is not yet root-caused 2026-08-11 06:30:30 -04:00
swung0x48 543c29bf86 [Fix, Test] (MG_Backend/DirectVulkan): gl_FragCoord on the default framebuffer reports GL's window origin - the stored row is not the window row once the viewport rect is converted 2026-08-11 05:25:32 -04:00
swung0x48 ef562ee9b5 [Fix, Test] (MG_Backend/DirectGLES): backend framebuffer, renderbuffer and sampler twins release their driver ids - a framebuffer per readback leaked the driver into stale pixels 2026-08-11 03:54:05 -04:00
swung0x48 fa0f6693d0 [Fix, Test] (MG_State, MG_Impl): reflection-backed glGetProgramiv queries answer zero instead of dereferencing a null TProgram 2026-08-11 02:59:10 -04:00
swung0x48 a6c362c6ce [Fix, Test] (MG_Backend/DirectVulkan): convert every default-framebuffer rectangle between GL and display Y origins - viewport, scissor, ReadPixels offset, rect-capable readback remap, blit source 2026-08-11 02:51:22 -04:00
swung0x48 921504eccf [Fix, Test] (MG_Backend/DirectVulkan, MG_Util): clamp Vulkan-derived GL buffer limits and saturate the uint32 to Int casts 2026-08-11 02:38:51 -04:00
swung0x48 7c5fc03b26 [Fix, Test] (MG_Backend/DirectVulkan): never bind or cache a null pipeline, and name the modules a failed vkCreateGraphicsPipelines rejected 2026-08-11 02:33:32 -04:00
swung0x48 ed29e63543 [Fix, Test] (MG_Impl): glGetProgramResourceiv reports a written length on every exit path 2026-08-11 02:27:26 -04:00
swung0x48 5c8a9c41d6 [Fix, Test] (MG_Impl, MG_State): GL entry points record errors instead of throwing through the C ABI - CopyTexImage superset rule, TEXTURE_BUFFER level queries, indexed cap toggles 2026-08-11 02:24:41 -04:00
swung0x48 efa0345c36 [Fix] (MG_Backend/DirectVulkan): offscreen surfaces never touch a window system - delete the hidden-Xlib fallback, make surface and device-enumeration failures loud, guard zero devices 2026-08-11 00:58:24 -04:00
swung0x48 ce0f18969c [Fix] (MG_Util): dlopen the versioned libEGL.so.1 first - the unversioned name is a dev-package symlink absent on runtime-only systems, and its silent failure nulled every EGL entry point 2026-08-11 00:58:23 -04:00
swung0x48 7ce0966e7d [Fix, Test] (MG_IntegrationTest): the harness is surfaceless by construction - never binds a window system, hardware demand split out of REQUIRE_GPU, pre-flight crashes keep their cores 2026-08-11 00:58:22 -04:00
swung0x48 1c6ca2753f [Test] (CI): wire the integration-gpu lane into the Test workflow - lavapipe ICD pinned at configure time, REQUIRE_GPU armed, failure-only core-dump artifacts in every native lane 2026-08-11 00:58:21 -04:00
swung0x48 61b0532865 [Fix] (MG_Backend/DirectVulkan): replay client-memory multi-draw through the single-draw path - the batched shared index view cannot express per-draw client pointers and dropped the whole batch 2026-08-10 21:58:06 -04:00
swung0x48 f5b8a505ed [Fix] (MG_Backend/DirectVulkan): keep the transient arena's outgrown buffers alive until frame rewind - the transient drain freed them while frame-serial memos still bound them 2026-08-10 21:47:28 -04:00
swung0x48 8371365db5 [Fix] (MG_Backend/DirectVulkan): include the texture enum converter the WARN-level log line needs - VulkanRenderer.cpp did not compile below INFO log level 2026-08-10 21:47:27 -04:00
swung0x48 b219992ee3 [Feat, Perf, Test] (MG_Util, MG_Backend, MG_Test): test-lane SPIR-V validation latch; entry-local Private rewrite, in-chain rect lowering and uniform-Location strip 2026-08-10 13:00:20 -04:00
swung0x48 94233ef928 [Fix] (MG_Backend/DirectVulkan): drop draws and dispatches when a program has no optimized SPIR-V - a phase-B failure left raw glslang words in GetGeneratedSpirv 2026-08-10 13:00:11 -04:00
swung0x48 0827d7a539 [Fix] (MobileGL): define the numeric log levels in Defines.h - the MOBILEGL_ASSERT gate compared undefined tokens in TUs that never include Log.h 2026-08-10 13:00:10 -04:00
swung0x48 5248b8b746 [Test] (MG_Test): replay a real Iris shader pair through the whole async frontend, both phases and both quirk states 2026-08-10 10:06:26 -04:00
swung0x48 d868e1c476 [Test] (MG_Util, MG_State): remove the TEMP stage-timing probes - the async-compile measurement campaign is done 2026-08-10 09:56:05 -04:00
swung0x48 c3412ca394 [Fix, Test] (MG_Util): give inactive vertex inputs a Location - undecorated inputs are invalid SPIR-V and Adreno rejects the whole pipeline 2026-08-10 09:27:01 -04:00
swung0x48 5ccaff37af [Fix] (MG_State): revert the Cut A-prime reorder - device dumps prove the ordering is not the pipeline failure, and record the third measurement 2026-08-10 08:58:38 -04:00
swung0x48 71e29f9d58 [Fix] (MG_State): keep GlslangToSpv ahead of buildReflection - reflection-first drops a vertex input's Location decoration and Adreno rejects the pipeline 2026-08-10 08:33:47 -04:00
swung0x48 8ad07c222c [Test] (MG_Test): cover the cancelled-SPIR-V program, the buffered-write valve and drop, the delete-does-not-block bound and the zero-thread join 2026-08-10 07:31:58 -04:00
swung0x48 5722094d6f [Docs] (MG_State): the early AST drop frees only the re-parsed shaders - the compile node co-owns the rest 2026-08-10 07:31:58 -04:00
swung0x48 4831387cf0 [Fix] (MG_State, MG_Backend/DirectVulkan): a program whose SPIR-V job was cancelled faulted on the first glUniform - bounds-check the shadow lookup 2026-08-10 07:31:57 -04:00
swung0x48 d03b72267a [Perf] (MG_State): drop the parsed ASTs as soon as the SPIR-V job has generated its modules 2026-08-10 06:37:49 -04:00
swung0x48 847ec74f48 [Test] (MG_Test): AsyncSpirvPhaseTest - phase-A completeness, buffered-uniform replay, version bumps and the cancel matrix 2026-08-10 06:12:39 -04:00
swung0x48 e02e5caa17 [Fix] (MG_State, MG_Backend/DirectGLES): join both link phases on the draw and drain paths, and refuse to bind a program whose SPIR-V never arrived 2026-08-10 06:08:30 -04:00
swung0x48 1958934594 [Feat] (MG_State, MG_Impl): buffer non-opaque glUniform writes across the SPIR-V window and replay them at its publish 2026-08-10 06:05:37 -04:00
swung0x48 dec0c5eaff [Refactor, Test] (MG_State, MG_Util): split the link into ProgramLinkTask (query surface) and a chained ProgramSpirvTask behind its own join gate 2026-08-10 06:02:52 -04:00
swung0x48 b6a44cd1e2 [Refactor] (MG_State): run reflection and link validation before SPIR-V generation - the ordering constraint retested byte-identical 2026-08-10 05:50:20 -04:00
swung0x48 85f45d0e44 [Test] (MG_Util, MG_State): TEMP spirv-null plumbing probe for the allocator-pathology discriminator 2026-08-10 05:42:23 -04:00
swung0x48 404236d337 [Test] (MG_Util, MG_State): TEMP stage-timing probes for the async-compile campaign - remove before merge 2026-08-10 05:16:24 -04:00
swung0x48 6ea948779e [Feat, Test] (MG_Impl, MG_State, MG_Util): opt-in MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS - shader compile status/log answer without joining, latched per compile 2026-08-10 02:06:17 -04:00
BZLZHH d8d7530011 [Fix, Test] (MG_Backend/DirectGLES, MG_Util, MG_Impl): widen three-channel render targets wherever the driver refuses them
Complementary Reimagined would not load through Espryt on Mali: Iris got
GL_FRAMEBUFFER_UNSUPPORTED building its composite framebuffer, because
colortex1 is RGB8_SNORM and colortex2 is RGB16F - three-channel formats
that no real ES driver can render to (EXT_render_snorm covers R/RG/RGBA
only, and the float extensions exclude the RGB forms). The frontend's
probe cache diagnosed this correctly and then had nothing to offer: the
NoThreeChannelRenderTarget widening machinery existed but was gated to
multisample targets alone. llvmpipe turns out to refuse most of the same
attachments - CI retrace stayed green only because a replay never
branches on glCheckFramebufferStatus - so this was never a desktop-vs-
device split, just an unlit path.

The widening now applies to every color-attachable image, renderbuffers
included, riding the driver-probe branch so the native format is still
tried first and substituted only on refusal. One ThreeChannelWidening
table owns the widened (internalformat, format, type) triple per source
format - the previous per-case branches disagreed with each other and
could emit an unuploadable (RGBA16F, GL_RGB, GL_BYTE) combination or
widen into another three-channel format the driver refuses just the
same. Uploads repack three-component client data to four with the
format's own one in the alpha channel (127 is not 1 for RGB8I - the
integer arms carry integer ones); readback drops the synthetic alpha,
derived from the actual image being read, not the bound framebuffer,
so glGetTexImage through a scratch FBO cannot be confused by an
unrelated widened attachment.

Stored alpha on a widened attachment is now an invariant 1.0 rather
than an accident: the color-mask sync clears the alpha bit per draw
buffer (glColorMaski for MRT mixes), and clears route through
glClearBufferfv with alpha substituted on widened slots only -
scissored clears inherit the discipline for free, integer color
buffers keep their explicit integer-clear path, and glGet still
answers the application's own mask. GL_DST_ALPHA blending, blits and
readback therefore all see 1.0 without further interception.

DriverPost grows the rows this bug earned: EXT_color_buffer_float
detection (previously unreferenced anywhere) with a FAIL row when
absent, the missing EXT_render_snorm row, and a three-channel-
attachment row that reports one representative per widening class -
graded so a half-float-only driver warns about the 32-bit float gap
instead of being declared unsupported.

Gates: 606/606 unit at default and with the async kill switch; full
retrace, both backends - the complementary fixtures now run with the
widening ACTIVE on llvmpipe and pass with a slightly better SSIM than
before; ext caselist DirectGLES holds 3914/4867 with zero set drift
while 54 cases move from NotSupported to genuinely passing; on the
Mali-G77 device, Complementary Reimagined builds its pipeline and
renders in-world through Espryt (md5-verified build), BSL still green.
A new ThreeChannelAttachmentScenario pins the frontend answer -
COMPLETE where it used to say UNSUPPORTED - on the real driver.
2026-08-09 15:28:34 -04:00
BZLZHH 0f394fa46f [Test] (tools/trace_replay): pin the Iris glyph-death bug with a BSL pause-menu fixture
minecraft-1.21.4-fabric-iris-bsl-esc-menu-854: Minecraft 1.21.4 Fabric +
Sodium + Iris + BSL 10.1.3 through Espryt on Mali-G77, paused at the ESC
menu over the BSL-blurred world, captured at 854x480 through the FCL
trace-capture skill. The trimmed trace keeps the whole session preamble
deliberately: the 107669b3 bug class is triggered while Iris BUILDS the
pipeline (a by-name texture call swapping the active unit's binding under
the sync memo), and the pause menu afterwards is where its damage is
legible - every glyph alpha-discards against the zeroed lightmap while
the button frames survive.

Ablation-proven as a net before landing: against a build with both layers
of 107669b3 reverted the case fails at ssim 0.949284 and the diff image
is text and only text - menu title, all button labels, the tutorial
toast; against the fixed library it matches the golden exactly, and
Magma passes the same golden at 0.998402 with no alternate needed.
Replays in about a minute per backend on llvmpipe/lavapipe (the 1.3M-call
preamble; well under the timeout and smaller than several existing
fixtures). Existing fixtures spot-checked green.
2026-08-09 12:49:10 -04:00
BZLZHH 107669b3db [Fix, Test] (MG_Impl, MG_Backend/DirectGLES): DSA by-name texture calls corrupted borrowed-slot memo pairings - process-wide glyph death under Iris
Field report: on Espryt with a BSL Iris pipeline built, every glyph in the
game died - HUD, menu labels, even the vanilla title screen after leaving
the world - while sprites kept rendering. Captured on-device (FCL apitrace
rig), reproduced headlessly on llvmpipe, and pinned with a three-way replay:
the same trace renders full text on raw Mesa desktop GL and on Magma, so
the stream was intact and the execution was Espryt's.

MECHANISM. WithTemporarilyBoundNamedTexture implements the by-name (DSA)
texture entry points by binding the named texture onto the active unit's
real slot, running the bound-texture code, and restoring - without moving
the texture bind generation on either edge. DirectGLES's per-draw texture
sync memo keys on that generation and BORROWS the slot pointer, so a memo
built for texture A kept passing every key while a by-name call had
texture B sitting in the slot: A's backend twin was driven with B's
frontend object, and SyncMipmapsToBackend re-specified A's storage with
B's shape. In the trace, a by-name upload to a BSL 2048x2048 map while
the 16x16 lightmap was bound re-specified the lightmap's GL texture
2048x2048-NULL and back 16x16-NULL. The lightmap exists only as render
output - no glTexSubImage2D ever touches it - so it stayed zero forever,
and rendertype_text (vertexColor = Color * texelFetch(lightmap, ...)),
alpha-discards every glyph. Background quads never sample the lightmap,
which is why only text died.

FIX, class-level, two layers:
- Frontend (shared, closes the same hole for DirectVulkan's generation-
  keyed memos): the temporary bind and the restore each bump the texture
  bind generation (only when the slot actually changed), and the restore
  is an RAII scope guard so a throwing body can no longer leak the
  temporary binding - a second latent bug of the same class. Deliberately
  a generation bump and not a touched-unit note: the high-water mark must
  not chase by-name calls, and a completed bind/restore pair leaves the
  content epoch unchanged, so the cost is an owner-compare re-walk, not a
  memo rebuild.
- DirectGLES defense in depth: both borrowed-pair memos
  (g_unitTextureSyncList, g_fboTextureSyncList) record which frontend
  texture each backend twin was paired with and re-check it before any
  replay (last in the key conjunction, behind the context-id compare). A
  stale pairing now costs a list rebuild instead of silent cross-texture
  storage corruption.

Tests, both red with their own layer reverted:
TextureTest.NamedTextureCallKeepsUnitBindingAccountingCoherent (the
accounting contract) and DirectGLESTextureSync.UnitMemoRefusesToDriveA-
TwinFromAnotherTexture (the corrupting sequence shape against a mock GLES
table, asserting the resident texture's storage is never re-specified).
595/595 unit at default and with the async kill switch. Replay evidence:
the captured BSL ESC-menu trace renders all text through Espryt post-fix,
byte-comparable to the Mesa-direct and Magma replays; the no-shaderpack
control is unchanged. A trace fixture wiring this scene into CI follows
in a separate commit.
2026-08-09 11:28:24 -04:00
BZLZHH 3e0460e472 [Feat, Fix] (MG_Impl, MG_State, MG_Backend): program interface queries from frontend reflection; Espryt state-shadow reset
Wave 2 of the advertised-extension conformance campaign.

PROGRAM INTERFACE QUERIES (the load-bearing piece). glGetProgramInterfaceiv
and the five glGetProgramResource* entry points were answered by the
BACKENDS - Espryt asked the real driver about SPIRV-Cross-generated ESSL
whose namespace is not the GL one (default-block uniforms live in
MGL_GLOBAL_UBO there), and Magma kept a second, partial reflection that
hardcoded types and diverged from the frontend. Both are now deleted; a new
frontend resource-model layer (ProgramInterface.{h,cpp}) answers every
interface - uniforms, uniform blocks, atomic-counter buffers (recovered
from glslang's synthesized gl_AtomicCounterBlock_<binding> lowering),
buffer variables, shader-storage blocks (classified by TType storage
qualifier since glslang reflects them as uniform blocks), program inputs/
outputs (built-ins' layoutLocationEnd sentinel mapped to -1), and
transform-feedback varyings including the gl_NextBuffer/gl_SkipComponentsN
pseudo-varyings - from the glslang reflection the frontend already trusts
for glGetActiveUniform. Name/index round-tripping, the "[0]" array
spelling, and the GL 4.6 table 7.2 prop/error matrix live in the new layer
only; GetActiveUniform*/GetActiveAttrib* are untouched.

glShaderStorageBlockBinding now takes the interface-layer index (the one
GetProgramResourceIndex returns, with a range check it never had), records
the binding on the program keyed by block NAME - the one coordinate all
three index spaces agree on - and delegates by name across the backend
boundary. Both backends reseed the recorded bindings on their own program
rebuilds, so an unrelated resync can no longer silently revert a rebound
block, and GL_BUFFER_BINDING reports the live binding, not the declared
one. The Espryt delegate applies only to an already-synced twin and can no
longer trigger SyncToBackend from a getter; the sync path's GL query
out-params are initialized and clamped (a load-dependent stack-garbage
Vector size crash caught by the gate, reproduced 3/50 pre-fix, 100/100
post-fix under saturating load).

ESPRYT RENDER-STATE SHADOW RESET (rides along because it shares
DirectGLES.cpp): the render-state shadow is file-static and survives
MobileGL context switches, so GL_FRAMEBUFFER_SRGB (and the whole synced-cap
class) leaked between contexts - the cross-test leakage class the CTS maps
have carried for a week. MakeCurrent now invalidates the shadow like it
already invalidates the program/FBO/buffer caches, and the resync resolves
a never-set scissor box to the current surface instead of pushing the
(0,0,0,0) sentinel verbatim (which scissored everything away - caught by
the retrace gate, bisected to the exact field via a bitmask probe, and
fixed by resolving like the viewport path rather than reverting).

Frontend riders exposed by the layer: glGetUniformLocation resolves
arrays-of-arrays element addressing ("a[2][1]"); transform-feedback capture
accepts element-addressed varying names ("b[1]") and snapshots the request
verbatim for the interface (Magma's decorate pass logs loudly that element
capture is unimplemented there - follow-up).

KNOWN GAPS, documented in code and tests: the 6 subroutines-* cases
(glslang refuses subroutine for SPIR-V; wave 3), the 5 separate-programs-*
cases (glslang's pipe-I/O reflection cannot see a separable non-vertex
stage's own inputs; needs stage-aware output validation first), and
uniform-block-types' per-instance stage masks (not derivable from the
reflection).

Gate: 593/593 unit at default and kill-switch, x10 each, plus the SSB race
case 100/100 under 20-way CPU load; ext caselist Espryt 77.87% -> 80.42%,
Magma 77.58% -> 79.39% (+212 fixed, 0 newly broken); program_interface_query
2/43 -> 31/43 unique on Espryt, 9/43 -> 31/43 on Magma, backends now
byte-identical; KHR-GL45.direct_state_access 370/371 + 371/371 with the 4
sRGB leak victims recovered in cross-test ordering; KHR-GL33 held at
9884/9886; full 39x2 CI retrace with zero wave-attributable failures (the
3 failing newly-added fixtures are bit-identical on the pristine baseline).
2026-08-09 08:12:30 -04:00
BZLZHH 33ff177bb2 [Fix, Test] (MG_Impl, MG_State): advertised-extension conformance wave 1 - uniforms, validators, getters
First wave of the advertised-extension CTS campaign (targeted caselist: the
glcts groups of every extension both backends advertise, 4867 cases across the
KHR-GL41..46 namespaces). All frontend, shared by both backends:

- Non-square float matrix uniforms actually upload: glUniformMatrix{2x3,3x2,
  2x4,4x2,3x4,4x3}fv and the six glProgramUniformMatrix* twins were
  validate-only no-ops; they now write column-at-a-time at the global UBO's
  16-byte std140 column stride, honouring transpose. glUniformMatrix2fv had
  the sibling bug - mat2 written as 4 contiguous floats put column 1 at byte
  8 instead of 16. The readback path only ever un-padded mat3, so
  glGetUniformfv is fixed for mat2, mat3x2 (previously mis-gathered) and
  every non-square shape, with the bounds check widened to the padded span.
- glBindBufferRange validates offset/size at last: size <= 0, offset < 0,
  SSBO and UBO offset alignment, transform-feedback offset AND size
  multiples of 4 - all before any state write (a negative offset used to
  reach Range1D unchecked). glBindBuffersRange inherits per element, with
  the ARB_multi_bind up-front [first, first+count) checks added to the
  BindBuffersBase/Range and BindSamplers prologues.
- BufferSubData's second, wrong mapped-overlap test deleted (it rejected
  every write at or after a mapped range's start, mapped or not); the state
  layer's assert relaxed to the same half-open intersection the frontend
  checks. BufferStorage error precedence fixed: no-bound-buffer now beats
  bad-size/flags.
- glSamplerParameteri accepts the full GL_NEVER..GL_ALWAYS compare-func
  range (NEVER/LESS/EQUAL were rejected by a wrong lower bound).
  glBindSampler's unit gate uses GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS instead
  of the frontend array capacity, shared with glBindSamplers by construction.
- Getters: GL_MAX_SHADER_STORAGE_BLOCK_SIZE in glGetIntegerv; atomic-counter
  buffer limits; all 11 per-unit GL_TEXTURE_BINDING_* plus GL_SAMPLER_BINDING
  in glGetIntegeri_v; GL_VERTEX_ATTRIB_BINDING/_RELATIVE_OFFSET across the
  vertex-attrib query family; glGetFloati_v/glGetDoublei_v implemented (were
  stubs); KHR_debug limits raised to spec floors.
- glCreateShader records INVALID_ENUM for an unknown type (it previously
  handed out a usable name with no error at all); glCreateShaderProgramv
  validates count up front. glDispatchCompute/Indirect validate work-group
  counts, offset alignment and indirect-buffer presence.
- glVertexAttribIFormat & friends take a positive integer-type whitelist -
  GL_FLOAT/GL_HALF_FLOAT/GL_DOUBLE/GL_FIXED no longer slip through as
  integer attributes.

Gate (headless Mesa, default config = async on): 570/570 unit at default and
with the kill switch; ext caselist Espryt 76.29% -> 77.87% (+81 fixed, 6
crashes -> 0, the whole list now runs in one glcts process), Magma 75.94% ->
77.58% (+80 fixed, 0 newly broken); KHR-GL33 full mustpass lost nothing
(9884/9886, the 2 known Mesa-drift failures); retrace smoke clean (the
bsl-GLES miss is the documented golden drift, bit-identical on the pristine
baseline). The 4 DirectGLES direct_state_access.renderbuffers_storage* cases
that turned red are a PRE-EXISTING GL_FRAMEBUFFER_SRGB cross-test leak,
A/B-proven on an unpatched 2e6fc1ff build - wave 1 removed the two accidental
maskers (a crash partition and a failing case whose error path reset the
state). Fixing the leak itself is queued.
2026-08-09 04:39:22 -04:00
BZLZHH 2e6fc1ffc0 [Feat] (MG_Util, MG_Test): enable asynchronous shader compilation by default (P1 stage 7)
kAsyncShaderCompileDefault flips to true, which also advertises
GL_KHR_parallel_shader_compile by default on both backends. Unset
MOBILEGL_ASYNC_SHADER_COMPILE now resolves to ON; =0 remains the complete kill
switch (reverts the threading and withdraws the extension together).

The gate behind the flip (headless Mesa - llvmpipe for Espryt, lavapipe for
Magma - at c6299f75): GL30/31/32/33/40 mustpass plus the
KHR-GL46.parallel_shader_compile group, both backends, async=1 with the
extension advertised. 58,344 case-runs, 8 failures - and every one of the 8
also fails standalone at async=0, in the full async=0 suite arms with an
identical per-case failure set, and under the pre-P1-stage-6 library. Zero
async-attributable deltas; the 8 are Mesa-upgrade drift (4 unique signatures:
Espryt GL40 transform_feedback.draw_xfb{,_feedbackk}_test, Magma
texture_size_promotion.functional + packed_pixels rgb9_e5_format_red on
GL32/33/40), recorded for separate follow-up.

Validation under the flipped default (no env var): 553/553 unit at the default
AND with the kill switch; parallel_shader_compile 3/3 on both backends proving
the default advertises; 44+44 integration scenarios; 71/72 CI trace-replay
fixtures (the one failure is the pre-existing create-indirect lavapipe crash,
identical under the pre-fix library). The lifecycle test's contract updates
with the default: AsyncIsOnByDefaultAndTheOverrideDecidesEitherWay.

Not covered by this gate and deliberately left open: SSO/DSA suites (GL41+,
separate follow-up per review), and real-driver confirmation on NVIDIA/Mali -
the Mali-G77 on-device sweep runs separately as a report-only pass.
2026-08-09 02:04:59 -04:00
BZLZHH c6299f754f [Fix] (MG_Backend/DirectVulkan, MG_State): key per-object memos on lifetime ids, not heap addresses
A destroyed VertexArrayObject's heap address is handed straight back by the
next allocation of its size, and so is a destroyed BufferObject's. DirectVulkan
keyed its per-VAO draw memo on the VAO POINTER and folded the bound buffer's
ADDRESS into the content hash that validates the memoised bindings, so a
delete/recreate pair under a byte-identical attribute layout reproduced both
the key and its validating hash at once. The successor VAO then inherited the
dead one's resolved bindings and the draw fetched from a destroyed VkBuffer.
Both stated defences failed together, because both reduce to the content hash
and the hash's buffer-identity component was itself a recycled address.

VertexArrayObject and BufferObject now carry a globally-unique, never-reused
GetLifetimeId() - the same contract as ProgramObject's, minted from an atomic
starting at 1 so a zero-initialised slot can never name a live object.
VaoDrawMemo matches on (address, lifetime id) and stores the id on recycle,
SetupDrawSnapshot's "the VAO did not move" test compares the id alongside the
config version, and VertexInputStateFactory::ComputeHash hashes the bound
buffer's id instead of its pointer (0 for client memory).

Proven: the use-after-free reproduces at 100% incidence headless on lavapipe,
including a SEGV whose backtrace is the driver dereferencing a destroyed vertex
buffer inside lvp_queue_submit, and it is gone with the fix. New coverage -
MG_Test/State/ObjectLifetimeIdTest (deterministic, GPU-free, no context: it
waits for the real allocator to repeat an address and asserts the id differs,
and skips loudly rather than passing quietly if it never gets the chance), and
MG_IntegrationTest XfbAfterClipDistanceScenario, registered for DirectGLES,
DirectVulkan, and a third DirectVulkan run with async shader compilation pinned
on because that is a second allocation pattern. Gates: 553/553 unit green at
async=0 and async=1; the scenario 5/5 headless at both flag states; 71/72 CI
trace-replay fixtures over both backends, the one failure a pre-existing
lavapipe crash proven not a regression (identical SIGSEGV at the identical
call number under the pre-fix library).

Pending NVIDIA/X11 confirmation: the KHR-GL{32,40} transform_feedback failures
that opened this investigation never reproduced on lavapipe - the -2/-101
pre-fill signature appears in zero pre-fix runs there - so whether this clears
them is UNPROVEN and must be re-measured on the NVIDIA rig against a freshly
re-run pre-fix baseline. The residual suspect is deliberately untouched here:
m_xfbCounterSlotByObject keys its counter slot on the raw GL transform-feedback
name, so a recycled name whose generation check happens to pass would RESUME
instead of BEGIN. That path was never exercised on lavapipe and is neither
confirmed nor exonerated.
2026-08-08 23:56:53 -04:00
BZLZHH dcf918b9ee [Perf] (MG_State): adopt in-flight compile jobs across shader objects (P1 stage 6)
~21% of a shaderpack's glCompileShader calls hand different shader objects
byte-identical source; the P0b cache only helps after one finishes, so under
async two workers would run the whole pipeline twice. Now the GL thread
consults a per-context (stage, hash, length, envFingerprint) -> weak-node
map at enqueue and ADOPTS the in-flight (or completed) node instead of
posting a duplicate - a hit is honored only after a full byte comparison
(the hash never decides), a cancel-requested or settled-cancelled node is
never adopted, and no worker ever waits.

Sharing a node makes the unconditional cancel wrong, so release is now
adopter-counted: a plain GL-thread Int (every mutation site is a GL entry
point; the single-threadedness argument and the terminal-early-out that
keeps the count exact are in the header), and the cancel fires only at
count zero AND with no pending link pinning the node (the stage-4
MarkLinkReferenced precedence). Adoption also re-points the object's source
at the node's snapshot so the layer-1 memo's pointer compare stays armed -
without that, an adopter's next glCompileShader would re-enqueue the very
duplicate this stage removes. Both guards are negative-control-proven: each
removed guard fails exactly its own tests. Count discipline was proven with
a temporary hard-abort on underflow/leak across the full suite and retrace
corpus - zero hits.

18 new tests (13 GL-surface incl. shared-node re-source/delete/orphan-sweep
isolation, shared failure logs, 48-over-6 stress with a deterministic
adoption count, flag-off and KHR-suspended zero-adoption guards; 5 direct
map cases incl. fingerprint mismatch and cancelled/expired pruning).
Gates: 538/538 unit both flag states, async suites x5 no flakes, NVIDIA
DirectGLES retrace identical sets both states. Timing: 2-worker
(Android-shaped) 1-3% faster consistently on complementary and BSL;
4-worker unchanged - the win this stage exists for lands where CPU is
scarce.
2026-08-08 20:17:51 -04:00
BZLZHH d98f72447d [Feat, Test] (MG_Backend/DirectVulkan, MG_Test): Magma advertisement + the parallel-compile test net
Same gated push as the Espryt commit. The tests ride here because they
exercise both backends' advertisement paths and every piece of the
extension: ParallelShaderCompileTest (13 unit cases - the held-job proof
that GL_FALSE is observable and a second poll still shows outstanding work,
program equivalent, untouched objects read TRUE, always-TRUE with async
off, unknown pnames still INVALID_ENUM, zero-count join+inline for compiles
AND links, nonzero restores while Initialize() does not, clamping and
0xFFFFFFFF and KHR/ARB sharing one state, the getter vs the budget, the
string tracking configuration through glGetString AND glGetStringi) and
AsyncCompileScenario (5 real-GPU cases per the design: 64-compile polling,
forced-join correctness, string/thread-count checks against a live driver,
zero-count synchronous settlement, and async-vs-sync frames rendered
byte-identical with quadrant signatures so two identically-wrong images
cannot pass). Verified 5/5 on NVIDIA in the full 2x2 backend x flag matrix
with MOBILEGL_ITEST_REQUIRE_GPU=1.
2026-08-08 13:17:58 -04:00
BZLZHH f15cb8900f [Feat] (MG_Backend/DirectGLES): advertise GL_KHR_parallel_shader_compile when async is enabled
Gated on the configuration flag on purpose: the string is the one change a
retrace can never cover (Iris/Sodium pipeline their submissions differently
once they see it), so the kill switch has to withdraw the app-visible
behaviour along with the threading.
2026-08-08 13:17:58 -04:00
BZLZHH bd0def6133 [Feat] (MG_Impl, MG_State, MG_Util): the GL_KHR_parallel_shader_compile surface (P1 stage 5)
GL_COMPLETION_STATUS_KHR in both object getters, reading the non-joining
node-direct state - the one query that must never block is asserted never
to reach a join gate. glMaxShaderCompilerThreadsKHR/ARB share one
implementation: a zero count suspends async FIRST and then joins every
outstanding compile and link this context owns (suspend-before-join is the
only order whose post-condition is 'nothing in flight'), a nonzero count
restores; the suspension is a process latch the extension controls, kept
distinct from the configuration flag that gates the ADVERTISEMENT - an app
that turned threading off has not made the extension disappear.
GL_MAX_SHADER_COMPILER_THREADS_KHR reports the thread count. DriverPost
gains the MobileGL-side async row (PASS/INFO naming the env knob) and an
informational host-driver row backed by a new GLES capability probe.

The extension string itself lands per backend in the two follow-up
commits, keeping this one green stand-alone.
2026-08-08 13:17:58 -04:00
BZLZHH 6f8b7fbc40 [Feat] (MG_State, MG_Util): async program linking on the job graph (P1 stage 4)
glLinkProgram with the flag on snapshots its inputs in a GL-thread prologue
(stage-sorted shaders with their compile nodes taken without joining, env,
explicit locations/fragdata/xfb, draw-buffer count), then runs the whole
link body - glslang link/mapIO, SPIR-V, reflection, routing tables - as a
ProgramLinkTask that auto-posts when its last compile dependency settles
(+1-guarded countdown; no worker ever waits on another job). The publish is
one move of the LinkArtifacts block at the join, with the second version
bump so nothing memoized during the pending window survives.

The consume-once TShader claim moved onto the shared compile node as a CAS:
two link jobs racing for one shader resolve to winner-takes-the-parse,
loser re-parses the preprocessed source against the node's own env -
identical SPIR-V pinned by test for 2 and for 12 sharing programs.

Two deliberate corrections to the design's cancel matrix, both test-proven:
attach/detach do NOT cancel a pending link (the snapshot isolates it, and
glCreateShaderProgramv's link-then-detach would otherwise discard its own
result before anyone read it); and a compile node a pending link depends on
is pinned against the orphan-name sweep - the ordinary LWJGL teardown
compile/attach/link/detach/delete used to cancel the dependency and turn a
must-pass link into GL_FALSE.

Continuations are now throw-contained per-item (a stage-3 leftover made
load-bearing by the first real continuation), and the review's deadlock
find is fixed: the dispatch loop no longer cancels a node while holding the
pool mutex, since that cancel can run OnDepSettled -> Post -> same mutex.

Explicit joins: the draw path (GetProgramForDraw, both the pipeline stage
loop and the plain-UseProgram half) and the composite-link site; destroy
paths cancel-not-join; COMPLETION_STATUS readers stay non-joining.

Gates: 506/506 unit both flag states; AsyncCompile/AsyncLink/AsyncTeardown
suites x10 repeats clean both states (teardown with 128 jobs in flight,
then re-Initialize); full NVIDIA DirectGLES retrace flag on twice - result
sets identical to flag off, zero new deltas. Compile-phase prefix-diff,
flag on vs off: complementary-reimagined 5.21s -> 2.16s, BSL 1.72s ->
0.90s - past the design's final acceptance targets before the KHR
extension is even advertised. Default remains OFF until stage 5+7.
2026-08-08 11:58:38 -04:00
BZLZHH e5fb57f7eb [Feat] (MG_State, MG_Util): async shader compilation behind the default-off flag (P1 stage 3)
glCompileShader with MOBILEGL_ASYNC_SHADER_COMPILE=1 snapshots its inputs on
the GL thread (source SharedPtr, CompileEnv, cache handle) and runs the whole
pure pipeline - preprocess, validators, extractors, glslang parse - as a
ShaderCompileTask on the worker pool, returning immediately. Every read of
compile-produced state joins through the single Compiled() gate; links stay
synchronous this stage and join their attached shaders at the top of the
body. Flag off, the path is the same code run inline.

Mechanics: the job node owns all its inputs (no back-pointer, no lifetime
tie to the shader object), so re-sourcing or deleting a pending shader is
cancel-and-drop, never a wait; glslang worker hygiene is a TLS-allocator
scope guard plus GL-thread builtin prewarm (gated on the flag, latch reset
on Destroy so re-initialization re-warms); worker-side diagnostics defer
through the job and replay on the GL thread at the join, enforced by
IsPoolThread asserts in RecordError and an empty-deferred-errors tripwire.
A body that throws publishes a COMPLETE failed compile (status false, real
info log) rather than an abandoned node, and never memoizes away the retry;
a failed enqueue (OOM) cancels the node instead of stranding the joiner -
including inside the dispatch loop, where the in-flight slot is repaid.
The pool StopAndDrains from an atexit sentinel too: workers still inside
glslang parse while exit() ran static destructors was a real 2-in-5 SIGSEGV,
reproduced and fixed (15/15 clean after).

Backend-internal shader objects (default FS, DirectVulkan blit/mipmap) are
cache-less and always compile inline - compile-and-read-in-one-breath needs
no round trip.

Gates: unit suite 488/488 with the flag off AND on (x5); AsyncCompileTest
(12 e2e cases: pending re-source/delete/recompile, byte-identical failure
logs across modes, 48-compile cache stress) x10 repeats clean both modes;
full NVIDIA DirectGLES retrace identical result sets flag off/on (zero new
deltas); compile-phase timing flat as designed (links still serial - the
parallel win arrives with stage 4's async link + stage 5's
KHR_parallel_shader_compile).
2026-08-08 10:33:50 -04:00
BZLZHH c93e5fa409 [Refactor] (MG_State, MG_Util): join-by-construction link/compile artifacts (P1 stage 2)
Still fully synchronous - EnsureLinkJoined()/EnsureCompileJoined() are empty
inline no-ops (verified to fold away at every one of the ~1200 call sites;
this project builds without LTO) - but every read of link- or compile-produced
state now goes through a private accessor the compiler enforces, so when
stage 4 moves the bodies onto pool workers, 'which reads must join' is a
type-system fact instead of a 400-line audit.

- ProgramObject: the 31 fields ResetLinkArtifacts clears plus the 5 link
  outputs it forgot (infoLog, linkedFragData{Location,Index}, the geometry
  strip-capture pair) move into a nested LinkArtifacts behind Artifacts().
  ResetLinkArtifacts is now a worker-safe pure clear; the link-observable
  version bumps (backendState/link/uboContent) move to a GL-thread-only
  BumpLinkObservableVersions() called once from Link()'s prologue and from
  glProgramBinary's mandated failure - the link body never writes them, so
  a stage-4 worker cannot lose an invalidation against the draw path.
- ShaderObject: compile artifacts (TShader, preprocessed source, side-channel
  maps, status/log, consume-once flag) behind Compiled(); the P0b layer-1
  memo trio deliberately stays outside as the future non-joining
  COMPLETION_STATUS_KHR fast path.
- CompileEnv (new): a GL-thread snapshot of everything the compile pipeline
  used to read live from the backend mid-parse - compute limits (the
  GetIntegeri_v reach-back is gone from the worker path), advertised
  extensions, device quirks, TBuiltInResource inputs. Captured lazily per
  backend activation; the consume-once re-parse now runs against the same
  env as the original parse.
- The GL-thread prologue / worker-body boundary is marked in Link() where
  the stage sort ends; everything below is a pure function of the snapshot.

Public getter signatures unchanged - MG_Impl and both backends compile
untouched. Unit 476/476, Program suites 117/117, DirectGLES retrace 38/39 on
llvmpipe (the one failure is the known pre-existing non-CI iterationrp case;
the NVIDIA userspace driver was updated out from under the running kernel
module mid-session, so GLX there is down until a reboot).
2026-08-08 07:12:37 -04:00
BZLZHH 8191075133 [Feat] (MG_Util): the async-compile pool skeleton behind a default-off flag (P1 stage 1)
Standalone Asio (submodule, asio-1-38-2 @ 8806a680, ASIO_STANDALONE +
ASIO_NO_DEPRECATED, header-only - no linked artifact) and the job machinery
the async shader pipeline will run on: JobNode (state machine with deferred
errors, continuations firing exactly once, dependency counters, cancel
semantics split into request vs outcome) and ShaderCompilePool
(asio::thread_pool behind a pimpl so no header leaks asio; big-core count
via cpufreq at >=85% of peak clamped to [1,4]; lazily constructed, so with
the flag off no worker thread ever exists; StopAndDrain leads DestroyImpl).

MOBILEGL_ASYNC_SHADER_COMPILE / _THREADS config knobs, default OFF. Nothing
in the GL pipeline references the pool yet - grep-verified; the full
DirectGLES retrace and compile benches are byte- and time-identical. 25
threaded unit tests, clean across 20x gtest_repeat.
2026-08-08 05:28:51 -04:00
BZLZHH d6caed7822 [Fix] (MG_Util, MG_State): five latent frontend bugs the async work made load-bearing
- SpvcSession's move constructor and move assignment dropped the parsed
  metadata, so a moved-to session silently reported empty reflection.
- ParseComputeLocalSize used std::stoull, whose std::out_of_range escaped
  glCompileShader on an oversized local_size literal; now std::from_chars
  saturating to UINT_MAX, pinned by a regression test that reproduced the
  escaping exception.
- The compute local_size std::regex was rebuilt on every compile; hoisted.
- LinkProgram dumped every shader's full source through MGLOG_D per link.
- glslang::FinalizeProcess ran before the GL context tore down, leaving the
  context's live TShaders pointing at freed builtin symbol tables.
2026-08-08 05:28:28 -04:00
BZLZHH 9152e88734 [Perf] (MG_State): dedupe shader compiles by source hash
Iris-style packs hand MobileGL the same source text repeatedly: probed across
three shaderpack traces, 28-32% of all glCompileShader work was redundant -
~9% same-object recompiles with byte-identical source, ~21% distinct shader
objects sharing identical source (the same common GLSL chunk glued into many
program stages). Two layers, both keyed by XXH64 + length with a full byte
compare on every hit (correctness never rides on the hash):

- Per-object: a successful (or failed) compile remembers its source hash;
  glShaderSource with byte-identical text keeps the compiled state and
  glCompileShader on unchanged source returns immediately. Deterministic
  (stage, source) pipeline makes the memo observationally identical to
  recompiling; the consume-once TakeShaderForLink re-parse path is untouched.

- Cross-object: a per-context bounded cache (ProgramState-owned, declared to
  outlive every shader object) shares the preprocessed source, both explicit
  side-channel maps, and the validation verdicts between objects with equal
  source; only the glslang parse stays per-object. Single-GL-thread today;
  flagged for a mutex when compiles go async (P1).

Interleaved A/B on the iterationrp trace (the recompile-heavy pack):
5.65s -> 5.46s median total replay, every round faster; BSL/complementary
stay flat (their duplicate sources are the small common shaders, so calls
drop but wall time is parse-bound on unique sources). Full DirectGLES
retrace, 445-test unit suite, and dedupe-semantics tests (no-op recompile,
invalidation on new source, failed-compile memo, cache bounds) all green.
2026-08-08 04:18:32 -04:00
BZLZHH 2406e2d219 [Perf, Fix] (MG_Util): preprocessing cleanups - dead scanners, quote-mask bug, one version inspection per compile
Three scoped changes to ShaderSourceProcessor, none altering any transform's
output (pinned by a byte-stability test across the legacy-shader anchor path):

- Delete BlankBlockComments and RemoveDefineForIdentifier - dead since their
  callers left; the former's newline-terminated quote handling moves into
  MaskCommentsAndQuotedText (below) together with its rationale comment.

- Fix MaskCommentsAndQuotedText treating a quote as running past end-of-line.
  GLSL has no multi-line literals, but a stray apostrophe in a directive or
  comment tail ("#pragma message can't") blanked the REST OF THE FILE for
  every masked consumer - the tokenizer, the version inspection, and the P0a
  explicit-location/binding extractors silently lost everything after it.

- Inspect the shader language once per PreprocessShaderSource run instead of
  up to five times: NormalizeVersionDirective now takes the already-computed
  ShaderLanguageInfo, and the two after-version injections share one
  AfterVersionAnchor instead of re-running a full masked sweep each
  (FindAfterVersionDirective -> InspectShaderLanguage) to find the same spot.

Compile-phase timings hold (BSL 1.848s, complementary-reimagined ~5.7s);
retraces and the 435-test unit suite unchanged.
2026-08-08 03:43:56 -04:00
BZLZHH b228f813c0 [Perf] (MG_Util): replace the builtin-shadowing string scans with one tokenize and a SPIR-V OpName pass
RenameBuiltinShadowingFunction probed the whole source ten times per compile
(5 names x mask + scan, each a full-text pass) and still had two blind spots:
a 5-name list and single-line-definition-only detection. On Complementary-scale
packs (4.5MB of sources) that was ~68% of the compile phase.

The rename is now split by FAILURE LAYER, both halves sharing one name table
header so they cannot drift:

- A SPIR-V OpName pass in SanitizeAndOptimizeBinary covers the full ESSL 3.20
  builtin set (~146 names). Renaming a function id is safe by construction:
  builtin calls are GLSL.std.450 instructions and can never resolve to a user
  OpFunction, overloads are distinct ids (a helper overload delegating to the
  real builtin keeps working), dead preprocessor branches never reach SPIR-V,
  and macro-expanded definitions are covered. ESSL 3.x is the only consumer
  that forbids the redefinitions, and this pass runs before its transpile.

- A lexical pass covers only the 5 names whose exact-signature redefinitions
  glslang's relaxed parse rejects outright (never producing SPIR-V for the
  backstop): the historical fma/max3/min3/round/tanh. One TokenizeCode pass;
  definition detection requires brace depth 0, a type-identifier previous
  token that is neither a statement keyword nor a directive tail, and skips
  files whose token-level braces do not balance (preprocessor-asymmetric
  arms) - over-detection is unrecoverable, so every ambiguity falls through
  to the backstop.

Measured on the compile phase (prefix-diff, 3-run medians, Espryt/NVIDIA):
complementary-reimagined 20.0s -> 5.5s, BSL 2.14s -> 1.85s. bliss (the pack
that ships from-scratch fma/tanh helpers) stays at SSIM 0.999962.

Tests: end-to-end ESSL assertions for the multiline-definition and
new-overload shapes, the three adversarial-review reproductions (statement-
keyword call under asymmetric braces, dead-#if compat shim, overload
delegating to the shadowed builtin), and a source-level assertion pinning
the lexical half specifically.
2026-08-08 03:11:38 -04:00
BZLZHH 0d0527192a [Perf] (MG_State, MG_Util): compile shaders with a single relaxed parse
glCompileShader used to parse every source twice: once under the GL client
(reflection only) and once under the relaxed Vulkan client (SPIR-V + the
plain-uniform global UBO), with GenerateBinary re-preprocessing, re-parsing
and re-linking every attached shader on every glLinkProgram. The GL-client
pass is gone: Compile() performs the one link-compatible relaxed parse and
the linked TProgram serves reflection and codegen both. Measured on the BSL
shaderpack compile phase: Espryt 2.80s -> 2.14s, Magma 3.78s -> 3.07s.

What the relaxed parse cannot provide is restored explicitly:
- explicit layout(location/binding) qualifiers on default-block uniforms and
  samplers are extracted lexically at Compile() (the relaxed parse strips
  them) and merged per link with cross-stage conflict checks;
- uniforms the relaxed parse sweeps into MGL_GLOBAL_UBO but no stage reads
  are filtered from the GL reflection surface through GL<->TProgram index
  translation maps (dead uniforms stay inactive, the synthesized block stays
  hidden, builtins reflect under their GL spellings);
- SPIR-V is generated BEFORE buildReflection touches the program (its
  live-variable analysis perturbs GlslangToSpv output - generated modules
  stay bit-identical to the old pipeline's), while the glUniform*-to-scratch
  routing tables are built strictly AFTER reflection, whose results size and
  key them;
- a TShader feeds exactly one link (mapIO mutates the intermediate); relinks
  and multi-program attachments re-parse the stored preprocessed source.

Validated: DirectGLES retrace suite green (two pre-existing local-driver
failures unchanged old vs new), KHR-GL30 877/878 on Espryt/NVIDIA (the one
failure pre-exists this change), unit tests green, per-module SPIR-V hashes
identical across a full DirectVulkan replay.
2026-08-08 01:25:54 -04:00
BZLZHH 81bcbd6c14 [Fix] (MG_State): allocate program and shader names from one shared name space
GL 3.3 core 2.11 puts program and shader names in one name space: a shader
name passed where a program is expected must fail with INVALID_OPERATION,
and vice versa. Two independent IndexGenerators handed out colliding names
(shader 2 and program 2 could coexist), so CheckProgramNameValidity resolved
a shader handle to an unrelated linked program and the error checks in
KHR-GL30.get_uniform_tests.get_uniform were silently swallowed - the case
only ever passed because the collided program happened to reject the queried
location. One shared generator keeps the names disjoint; the per-kind object
tables are unchanged.
2026-08-08 01:25:37 -04:00
BZLZHH 867fe3e0ef [Feat] (MG_Util, MG_IntegrationTest): POST rows for the Espryt multi-draw tier, and the scenario that pins it
Three DriverPost rows per the POST rule, since the ladder took on two
new driver dependencies: glDrawElementsBaseVertex (WARN when absent -
every base-vertex draw then costs a CPU index rewrite and an upload) and
compute shaders (INFO - the default tiers never use them). The third
names the tier that will actually run, with the full set the driver
supports, resolved by the same function the backend calls so the two can
not drift. The existing "Multi-draw base vertex" row stopped saying the
fallback is a per-draw loop, which is no longer the whole truth.

Scenario D asserts the one contract every tier shares: a multi-draw
paints exactly what the unrolled single draws paint. The reference side
is a loop of glDrawElementsBaseVertex and never enters the emulation, so
a tier cannot make itself look right by breaking both sides alike, and a
blank-frame pair is rejected outright - drawing nothing is the failure
mode this path actually has.

Nine cases, chosen for the shapes the Minecraft retraces contain none
of: narrow index types, a base vertex past the index type's range,
primitive restart inside a strip on two index types, client-memory index
arrays, and a batch with zero-count sub-draws (whose prefix sums the
flattening tier's binary search has to skip). Each of the six tiers
passes all nine on NVIDIA, and ext/auto/compute also pass on Mesa where
the ext tier is reachable.

The suite is falsifiable, not merely green: rewriting the rebase the way
MobileGlues does it - truncate to the source width, no restart
passthrough - fails exactly three cases on the drawelements tier (both
restart cases and the out-of-range base vertex) and leaves basevertex,
which rewrites nothing, passing. That control is also what turned up the
restart hole in the flattening tier fixed in the previous commit.
2026-08-07 08:16:08 -04:00
BZLZHH 0ec487c993 [Feat] (MG_Backend): port MobileGlues' multi-draw emulation to DirectGLES as a tier ladder
ES has neither glMultiDrawElements nor glMultiDrawElementsBaseVertex, so
both are emulated. DirectGLES had two ways of doing it - one
glMultiDrawElementsBaseVertexEXT where the driver has the extension
interaction, otherwise a per-draw loop. This adds the five MobileGlues
uses (gl/multidraw.cpp), so the ladder is now: one
glMultiDrawElementsBaseVertexEXT; one glMultiDrawElementsIndirectEXT
over a synthesized command buffer; one glDrawElementsIndirect per
command over that same buffer; the base-vertex replay; plain
glDrawElements over a CPU-rewritten index stream, for drivers with no
base-vertex draw at all; and a compute shader that flattens the whole
batch into one rebased index buffer drawn by a single glDrawElements.
They live in their own translation unit that owns the entry point
outright, preparation included - the compute tier has to dispatch BEFORE
PrepareForDraw, or it would have to unpick the program, storage-block
and index bindings the preparation just made, and a dispatch inside an
open transform-feedback span is not legal at all.

The auto ladder is ext -> basevertex -> multiindirect -> indirect ->
drawelements, which is NOT MobileGlues' order (it puts the indirect
tiers first). Measured on mc_sodium_multidraw, ns/op, median of three:
NVIDIA ES 3.2 basevertex 2500 vs multiindirect 5700 and indirect 5800;
Mesa llvmpipe ext 19300, basevertex 25200, multiindirect 27600,
drawelements 28700, indirect 31000. Ring-allocating the command staging
instead of respecifying per batch was tried first and moved the indirect
tiers by less than noise, so the cost is the indirect draw path itself,
not the upload; only a real multi-draw entry point beats replaying the
sub-draws. auto therefore resolves to basevertex on this box - byte for
byte the behaviour that shipped - and the new tiers are what a driver
with the ext interaction, or without base vertex at all, now gets.
compute is never chosen by auto (nor by MobileGlues'): it rewrites the
primitive stream rather than replaying it, and it measured slowest here.

Four places this deliberately does not follow MobileGlues, each a
correctness bug there. A rewritten stream is emitted as GL_UNSIGNED_INT
whatever came in, because GL adds baseVertex at full precision and
folding it into ushort indices wraps. The restart sentinel is carried
across a rebase unrebased, or an enabled primitive restart is lost. The
flattening tier declines strip/loop/fan modes, any sub-draw whose count
is not a whole number of primitives, and any batch at all while
primitive restart is enabled - a restart ends a primitive, so leftover
vertices would find a third vertex in the next sub-draw and become a
triangle GL never draws. And the indirect tiers decline client-memory
index arrays, which have no buffer to address.

gl_DrawID gets better rather than worse: the unrolled tiers now feed
each sub-draw its index (the spec's value, where the old loop left the
uniform untouched), and a program that actually reads it demotes the
batched tiers, which can only hold one value for the whole batch. The
per-batch cost is nil for the programs that do not read it.

Verified: the five DirectGLES retraces are byte-identical (md5) across
all six tiers on NVIDIA and on Mesa, each tier proven to have really
executed rather than silently demoted, via a per-tier announcement in
the log. Unit suite 421/421. The full retrace suite's five failures all
reproduce unchanged on a stashed tree, so none are new.
2026-08-07 08:15:54 -04:00
BZLZHH 23b880c8be [Feat] (MG_Config, MG_Util): a tier knob and two capability flags for Espryt multi-draw
MOBILEGL_ESPRYT_MULTIDRAW_MODE=ext|multiindirect|indirect|basevertex|
drawelements|compute|auto names the DirectGLES emulation tier for
glMultiDrawElements(BaseVertex). Same contract as the Magma knob: a
preference, not a demand, clamped at resolution time to what the driver
actually has, and invalid values keep auto. Nothing reads it yet.

The two capability flags the ladder selects on are new because neither
existed in the shape the choice needs. SupportsDrawElementsBaseVertex is
the weaker sibling of SupportsMultiDrawElementsBaseVertex - ES 3.2 core
or EXT/OES_draw_elements_base_vertex, with no GL_EXT_multi_draw_arrays
requirement - and it decides whether a batch can replay its sub-draws
with their own base vertices or has to fold them into rewritten indices.
SupportsComputeShader is ES 3.1 core plus the dispatch, barrier and
shader-object entry points. Both keep the house rule the multi-draw
flags already follow: the extension/version check is what proves
support, the resolved pointer only confirms it, because
eglGetProcAddress may hand back a live-looking stub for a function the
context does not implement.
2026-08-07 08:15:31 -04:00
BZLZHH ebc5bff9b1 [Fix, Feat] (MG_Backend): make DirectVulkan multi-draw actually draw, then pick its best tier
The bug: DirectVulkan.cpp::MultiDrawElements had its entire body
commented out - plain glMultiDrawElements on Magma recorded NOTHING,
no error, no pixels (readback shows the deferred clear never even
materialized). It now shares the tuned base-vertex implementation, and
both plain entries are pixel-proven by a 4-sub-draw harness.

The feature: every CPU-side multi-draw form dispatches through three
tiers after round-9's contiguous-run merge (restructured to merge into
a span BEFORE dispatch, so every tier consumes the shrunken array):
  1. VK_EXT_multi_draw: one vkCmdDrawMulti(Indexed)EXT, chunked by
     maxMultiDrawCount; per-draw vertexOffset rides in the struct. The
     extension is requested only when enumerated and its feature bit
     confirmed, entry points via vkGetDeviceProcAddr, demoted if
     missing.
  2. multiDrawIndirect: the param span uploads DIRECTLY as a transient
     INDIRECT-usage buffer - DrawIndexedCmdParam is layout-identical
     to VkDrawIndexedIndirectCommand and DrawCmdParam's head is a
     legal 24-byte-stride VkDrawIndirectCommand, both static_asserted,
     so no repacking - then one vkCmdDraw(Indexed)Indirect per
     maxDrawIndirectCount chunk. firstInstance!=0 additionally
     requires drawIndirectFirstInstance or the batch drops a tier.
  3. The byte-identical unroll.
gl_DrawID: tiers 1-2 are spec-correct (0,1,2,3 across a probe's
sub-draws); the unroll tier keeps the pre-existing always-0 contract.
The default tiers strictly improve DrawID correctness.

Adversarially verified: the five real DirectVulkan retrace images are
BIT-IDENTICAL (md5) across auto/ext/indirect/unroll; zero validation
VUIDs on every tier; a simulated no-EXT device resolves to indirect
and renders the same bytes; the known-red create-indirect fixture
crashes at the identical call before and after (not worse, not fixed).
Unit suite 423/423 on the rebased tree, retrace subset 10/10. Bench:
mc_sodium_multidraw's contiguous shape merges 32->1 before dispatch,
so no bench delta - the tiers' beneficiaries are non-contiguous real
streams (the sodium RETRACE pushes ~58-sub-draw batches, in=out
243101 with zero merges) and mobile drivers. A reproducible +3-4%
code-layout drift on mc_use_program (zero shared code, I-cache
displacement from +400 lines) stays under the action gate and is
booked here rather than hidden.
2026-08-07 06:41:21 -04:00
BZLZHH 231d5c90e4 [Feat] (MG_Config, MG_Util): a preference knob and POST rows for Magma's multi-draw tiers
MOBILEGL_MAGMA_MULTIDRAW_MODE=ext|indirect|unroll|auto selects the
DirectVulkan multi-draw dispatch tier, clamped to what the device
supports with one INFO line when it falls back; auto (and unset) picks
the best supported tier. Invalid values keep auto. Magma-only: the
variable has no effect on DirectGLES. Note for the escape hatch:
mode=unroll also forces the GL indirect multi-draw paths onto their
per-command loop, where gl_DrawID reads 0 for every sub-draw -
Flywheel-style content that keys on flw_drawId renders accordingly.

Three DriverPost rows per the POST rule: VK_EXT_multi_draw
(PASS/INFO), the multiDrawIndirect feature (WARN downgraded to INFO -
there is always a fallback tier), and the resolved dispatch tier with
the full chain. drawIndirectFirstInstance gains a row too, since the
indirect tier's legality check now relies on it.
2026-08-07 06:41:21 -04:00
BZLZHH d5f5e6405b [Perf] (MG_Backend): batch DirectGLES multi-draw base-vertex where the driver really has it
When SupportsMultiDrawElementsBaseVertex is true, glMultiDrawElements-
BaseVertex issues one glMultiDrawElementsBaseVertexEXT instead of a
per-draw loop; the fallback loop is byte-identical otherwise.

The local NVIDIA ES driver lacks GL_EXT_multi_draw_arrays, so the
batch cannot engage here and no local win is claimed (counter-proven:
batched=0 / fallback=264329 across a sodium retrace). On Mesa llvmpipe,
which implements the full interaction, the batch engages (batched=4566,
~58 sub-draws per call) and is pixel-identical to a forced-fallback
control (same SSIM to the last digit). The beneficiaries are mobile
drivers advertising the interaction - the Sodium chunk path collapses
32 driver entries into one - and the DriverPost row shows which side
any device falls on. A/B on both backends: every case inside the 5%
bar. Unit suite 423/423, retrace subset 10/10.
2026-08-07 06:00:54 -04:00
BZLZHH ac3a83b207 [Fix] (MG_Util, MG_Test): never take a non-null eglGetProcAddress result as support
On GLVND Linux eglGetProcAddress returns a non-NULL trampoline for ANY
name - including a fabricated one - so pointer-nullness can never
signal driver support. The three EXT multi-draw entry points were
registered as required (spurious error logs on drivers without them)
and their pointers were trusted; the NVIDIA ES driver hands back a
stub for glMultiDrawElementsBaseVertexEXT that SILENTLY DROPS draws,
which once made a "77% faster" multi-draw batch that rendered nothing.

The three entries are optional now, and two extension-derived
capability flags follow the established Supports* pattern - each is an
extension-string check AND a resolved pointer, so a flag alone is
sufficient at a call site:
  SupportsMultiDrawIndirect: GL_EXT_multi_draw_indirect + both entry
  points resolved.
  SupportsMultiDrawElementsBaseVertex: (GL_EXT or
  GL_OES_draw_elements_base_vertex) + GL_EXT_multi_draw_arrays + the
  entry point resolved. The multi_draw_arrays conjunct is the registry
  fact the stub exploited: glMultiDrawElementsBaseVertexEXT exists only
  in interaction with GL_EXT_multi_draw_arrays, and this NVIDIA driver
  advertises everything else EXCEPT that one - so the entry point is
  genuinely unsupported while eglGetProcAddress still "resolves" it.

Two DriverPost rows report both capabilities (INFO when absent - a
fallback always exists). Unit tests pin the stub shape, the exact
NVIDIA shape, the supported shape and extension-without-pointer.

Proven load-bearing: forcing the old pointer-only condition on the
NVIDIA ES driver reproduces the silent drop exactly (sodium retrace
SSIM 1.000000 -> 0.329522, no crash, no GL error); with the gate the
same run is a literal 1.000000. Unit suite 423/423 (two new tests),
retrace subset 10/10, integration suite 52/52.
2026-08-07 06:00:54 -04:00
BZLZHH 335f2decbd [Perf] (MG_Backend): stop DirectVulkan re-proving sampler sets and re-walking render passes
Two per-draw costs from the round-10 profiles. A per-program sampled-set
epoch inside UniformManager skips the per-binding descriptor proof walk
when no texture or sampler API ran since that program's previous draw -
the mc_sampler_churn/mc_tex_param pattern. The pass-switch path stops
re-deriving render-pass state that its own value hash already pins.

Load-gated 6-round order-alternating A/B (medians): magma tex_param
-13.3%, pass_switch -10.9%, state_toggle -5.4%; espryt untouched and
unmoved. The two matrix flags (sodium +7.5%, tex_stream +6.2%) reversed
under 10-pair isolated alternating re-runs (-5.5% and +2.5%) - the same
position-bias artifact every previous round's flags showed. Unit tests
421/421; retrace subset and the 52-entry integration suite pass.

Landing note: this diff was authored by a round-10 agent whose session
died before adjudication; the A/B data survived (r10bmag_ab_raw.csv)
and the flags were adjudicated before landing. Its relink also exposed
the pre-existing exit-teardown SIGSEGV fixed in the previous commit.
2026-08-07 04:13:55 -04:00
BZLZHH fb1ad96c04 [Fix] (MG_Backend): stop DirectGLES twin destructors calling a dead driver at exit
The static twin registries destroy their backend objects from
__run_exit_handlers, and a twin destructor then jumps through
g_GLESFuncs into a driver library that exit() may already have torn
down - a latent SIGSEGV that DriverBench has been dumping core with on
every exit, and that any relink shuffling static destructor order can
hand to the trace-replay binary (a byte-perfect replay then "fails with
status Segmentation fault").

A process-teardown flag now short-circuits the program, VAO and texture
twin destructors: past exit() the driver reclaims every GPU object
anyway, so the skip is a deliberate leak of nothing. The flag is set by
a std::atexit handler registered lazily on first registry use - by then
every static everywhere has finished constructing, so the handler runs
BEFORE any static destructor. A registry-destructor hook was tried
first and is wrong: tests and cache resets destroy temporary registry
instances mid-run, which latched the flag while the process was alive
(caught by DirectGLESBackendTexture.DestructorDeletesIdAndScrubsBindingCache).

421/421 unit tests, the retrace subset exits cleanly on both backends,
and the 52-entry integration suite passes.
2026-08-07 04:13:55 -04:00
BZLZHH 313b75a7c0 [Test] (MG_IntegrationTest): pin the two shipped memo bugs with rendered pixels
Both d7976326 bugs passed every unit test while corrupting real frames -
state-level assertions cannot see them. This module renders and reads
back.

A headless EGL-pbuffer harness (no window, no GLFW) linking MobileGL_s
directly, registered once per backend under the ctest label
integration-gpu, behind the default-OFF option
MOBILEGL_BUILD_INTEGRATION_TEST. The platform pre-flight runs the ENTIRE
bring-up in a forked child first - MobileGL aborts rather than returning
errors on an unusable platform, and the child dying on any signal turns
into a clean GTEST_SKIP instead of taking the test binary down.
MOBILEGL_ITEST_REQUIRE_GPU makes the label falsifiable: with it set, an
unusable harness (or a context that lands on a software rasterizer) is a
FAILURE - without it, a CI runner whose driver pinning silently broke
reports the same green as one that rendered every frame. Configure-time
detection pins the EGL vendor and Vulkan ICD jsons, preferring hardware
vendors and never selecting llvmpipe/lavapipe.

Scenarios assert on glReadPixels with whole-region pixel counts (a
2x2 quadrant pattern whose signature distinguishes all eight square
symmetries; every region predicate reports the first offending pixel):
- OrientationScenario: default -> FBO -> default, pinning the
  transform-flags memo key. Keying GetBaseTransformFlagsRaw on the
  pre-transform alone fails exactly 3 entries.
- StreamedArenaScenario: an untouched streamed vertex buffer must
  survive transient-arena recycling. Re-enabling only the cross-frame
  vertex revalidation fails exactly this entry.
- CrossFrameBufferScenario + ResidentIndexScenario: cross-frame
  mutation matrix (SubData, map/unmap, persistent+flush, coherent
  persistent, orphan, CopyBufferSubData; vertex and index) plus six
  adversarial resident-EBO constructions. Instrumentation showed the
  cross-frame EBO memo cannot be made to serve wrong bytes from GL
  level on this stack (89 entries, 81 accepts, zero divergent slices) -
  these cases are freshness tripwires, documented as such in-file; the
  EBO half of d7976326 remains unpinned by a failing test.

At the buggy commit 72ee7c43 the suite fails 4 entries (3 orientation +
1 streamed-arena); at d7976326 all 52 pass, 5 consecutive runs, zero
flakes, and the default build is bit-for-bit unaffected (unit suite
unchanged). Adversarially verified twice, including hostile-platform
sweeps (26 configurations, all clean skips) and hand-edits of each
production hole in isolation.
2026-08-07 03:30:18 -04:00
BZLZHH d7976326fa [Fix] (MG_Backend): two DirectVulkan draw memos trusted more than they proved
Two correctness holes from the round-7/8 fast-path work, found by
bisecting the retrace matrix after corruption reports on device.

Cross-frame slice trust: the vertex-binding and EBO memos skipped the
acquire - the frame's content-sync point - whenever their recorded
slice epochs still matched, trusting the BumpSliceEpoch inventory to
cover every way a buffer's GPU copy can go stale. At least one mutation
path escapes it: journeymap and common-mods retraces shipped visibly
corrupted, and Sodium on an Adreno device rendered random triangles
from stale vertex data. A memo recorded in an earlier frame now
declines, so the first draw of each (VAO, frame) re-runs the full
acquire; the same-frame paths (layout memo, factory-chase elimination,
one-compare rescue) are untouched. The cross-frame idea can return once
the bump-site inventory is proven complete against exactly these traces.

Transform-flags memo key: GetShaderTransformFlags reads the swapchain
pre-transform AND whether the bound draw framebuffer is the default one
- only a presenting pass gets the Y-flip/rotation bits. The memo
declared it pure in the pre-transform, so after any render-to-texture
pass the next default-framebuffer pass inherited the FBO's unflipped
flags: 1.17-main-menu retraced as a perfectly rendered, perfectly
upside-down frame (SSIM 0.052, deterministic), and cloud passes
flickered on device. The memo now keys on (preTransform, isDefaultFbo).

DirectVulkan retraces for 1.17-main-menu, journeymap, common-mods,
sodium and xaero-world-map all pass on lavapipe; unit tests 421/421.
2026-08-06 21:55:41 -04:00
BZLZHH 72ee7c439c [Perf] (MG_Backend): merge DirectVulkan's contiguous sub-draws, remember four programs
61% of mc_sodium_multidraw's steady-state CPU sat inside the driver
encoding one vkCmdDrawIndexed per sub-draw. MultiDrawElements now
collapses contiguous runs: merge only when the topology is a list
(POINTS/LINES/TRIANGLES), the accumulated count sits on a primitive
boundary, primitive restart is off, baseVertex/instanceCount/
firstInstance are identical and firstIndex is adjacent, with a
count-overflow guard - the bench's 132x32 sub-draws become 132x1.
Dangling-index discard semantics for list topologies are what the GL
spec already mandates per draw. No new Vulkan feature, so no DriverPost
gate; VK_EXT_multi_draw stays a gated follow-up.

The draw fast path's single SetupDraw snapshot died on every program
ping-pong (use_program's A/B pattern sent every other draw down the
full path, CollectSampledTextures alone 6.2% self). A 4-entry
program-keyed snapshot table (MRU by program lifetime id, per-entry
sampled-set copies, per-entry invalidation on decline or full-path
start, all entries still cleared at command-buffer boundary, pipeline
age-out and swapchain recreate) keeps all cycling programs hot.

Load-gated 6-round order-alternating A/B, sha1-fingerprinted pair:
sodium_multidraw -41.3%, use_program -23.9%, pass_switch -12.7%,
tex_param -3.8%, vanilla -2.1%; the one flag (tex_stream +5.4%)
reversed to -0.5% across 10 isolated alternating pairs. Espryt
untouched and unmoved. Unit tests 421/421.
2026-08-06 20:17:19 -04:00
BZLZHH cdea275227 [Perf] (MG_Backend): stage only the rects DirectVulkan actually dirtied
Consume MipmapStorage's new dirty-rect list: pack each rect tightly
into the staging block and issue ONE vkCmdCopyBufferToImage with N
regions instead of staging the whole union box. Offsets are computed
identically in the pack and copy loops; disjoint rects mean no
overlapping copy destinations; the combined depth-stencil and
RGB-expand/depth-convert paths keep their single-box route (gated to
the color-aspect, no-conversion case).

54% of mc_tex_stream's steady-state CPU was the one shadow->staging
memmove of the union box; staged bytes drop to 4.8% (~2MB -> ~95KB per
frame) and the case improves ~-49% (5945 -> 3048 ns/op, ~2.2x native
to ~1.2x). Zero validation-layer findings on the 95-region copy. Unit
tests 421/421.
2026-08-06 20:17:19 -04:00
BZLZHH 6a02c5fea0 [Perf] (MG_Backend): upload only the rects DirectGLES actually dirtied
Consume MipmapStorage's new dirty-rect list: when a level offers a
profitable rect list, the sync path issues one glTexSubImage2D/3D per
rect under a single UNPACK_ROW_LENGTH set/reset instead of one call
covering the union box. Striding is the exact scheme the single-box
path already uses (UNPACK_ALIGNMENT pinned to 1 by
ScopedDefaultUnpackState, so every bpp is stride-exact); levels
without a profitable list take the old path unchanged.

On the atlas-streaming case this trades one ~2MB upload for ~95 small
ones totalling ~95KB - roughly a wash in driver-call overhead on
desktop NVIDIA GL (mc_tex_stream ~-3%), a clear byte-volume win for
tiled/mobile GLES where the driver shadow-copies every upload. Unit
tests 421/421.
2026-08-06 20:17:19 -04:00
BZLZHH 7db5b35a3e [Perf] (MG_State): remember every dirty rect, not just their union
A Minecraft frame updates ~95 scattered 16x16 sprites in a 1024x512
atlas; MipmapStorage's single union dirty box turned ~95KB of changed
texels into a ~2MB upload on every backend. The storage now keeps a
bounded (96-slot) list of pairwise-disjoint dirty rects BEHIND the
untouched union box: rects cascade-merge on touch or overlap, overflow
folds the pair with minimum enlargement and re-cascades, whole-level
dirties and respecifies just clear the list (empty list = "union box
tells all"). GetDirtyRects hands the list out only when it has 2+
rects, fits the caller's capacity, and its summed area is under 75% of
the union box - fewer driver calls beat equal bytes - so consumers can
never stage more than the union box did.

The list is maintained inside the same four mutation funnels every
texel writer already goes through (MarkDirty, MarkDirtyRegion,
AllocateLevel, TruncateToLevelCount - callers enumerated at the
declaration), so list and union box cannot disagree. Backends OPT IN:
the union-box API and its update order are byte-identical, and an
unmodified backend keeps rendering exactly as before.

96 slots is measured, not guessed: on the bench's 95-sprite lattice a
16-slot list collapses to >93% of the union box, 96 slots reach 4.8%
(~2MB -> ~95KB staged per frame). Verified by a 2859-check fuzz run
against a reference dirty bitmap (union exactness, full coverage,
disjointness, bounds, profitability). Unit tests 421/421.
2026-08-06 20:17:19 -04:00
BZLZHH 990e518e33 [Perf] (MG_Backend): give DirectVulkan's draw memo a table that fits in cache lines
The per-VAO resolved-bindings map probe was ~45% of
UploadAndBindVertexBuffers' self time, and the aux-memo pointer chase
was the single hottest instruction left in TrySetupDrawFastPath. Both
die together: a fixed 2048-slot two-probe 64B-aligned VaoDrawMemo table
embeds the VAO key, content-hash-validated layout facts and the
bindings payload reordered hot-to-cold. Layout facts hold exactly while
the slot's content hash equals the live VAO's own config-guarded hash;
a recycled VAO address either misses or reproduces a byte-identical
config, for which the facts are correct by construction. Bindings keep
their full per-draw revalidation; recycled slots zero their frame
serials so half-filled entries can never match.

ComputePipelineStateHash, the depth/stencil probe and the
primitive-restart probe now take one bulk GetRenderStateParameters()
fetch instead of ~17 cross-TU accessor calls (verified pure field
reads, identical bit packing). The EBO slice memo gained the same
manager-wide epoch one-compare rescue the vertex half uses.
GetShaderTransformFlags is memoized on pre-transform. Sodium's
MultiDrawElementsBaseVertex hoists GetGLTypeSize out of the
per-sub-draw loop, replaces the division with a shift, and skips
unsupported index types loudly instead of dividing by zero.

Also verified: a GL_BLEND toggle recompiles nothing in steady state -
the glslang frames in earlier state_toggle profiles were startup
contamination.

Quiet-box load-gated 6-round A/B: sodium_multidraw -8.0%, tex_param
-4.1%, use_program -3.3%; steady-state vanilla_draw CPU -20% ns/op at
4096 frames (the 80-frame matrix compresses CPU wins under GPU boost
clocks; profiles confirm UploadAndBindVertexBuffers 6.3% -> 4.4%
including the table probe, and the aux cold-line load gone). The one
matrix flag (pass_switch +7.5%) reversed to -3.2% in 10-pair isolated
re-runs. Unit tests 421/421.
2026-08-06 14:07:49 -04:00
BZLZHH 25a8f51db5 [Perf] (MG_Backend): make DirectGLES program switches remember their own bindings
mc_use_program cycles programs whose texture bindings never change, yet
every switch re-walked the units. Six fixes, one theme: a switch back to
a known program should find its own state waiting.

Per-program 4-entry resolved-texture-binding memo (round-robin, shadow
memcmp on hit) skips the unit walk when a program returns with its
bindings intact. The whole sampler-uniform pass in
BindCurrentProgramWithResources is memoized per program twin behind
(context, unitBindingsEpoch, samplingGeneration, backendStateVersion,
textureContextGeneration) plus a per-sampled-unit sampler-shadow row
compare, invalidated on relink/backend rebuild; the
BindCurrentUnitSamplers walk sits behind the same keys. Every unit
assignment, sampler-parameter change and bind path was verified to bump
one of those inputs.

UboRingAllocate's common path is now a generation check, a power-of-two
mask, an overrun check and a head bump - the duplicate availability
probe, frame-mark retirement and divisions moved to the wrap slow path.
The per-context framebuffer binding slots (the frontend getter
linear-scans per call) are cached as direct pointers - slots are
by-value members of GLContext, so the pointers are stable by
construction - feeding SyncCurrentFBO, SyncNeccessaryTextures and the
broadcast memo; BindCurrentFBO's per-draw registry hash Find became a
TwinLookupMemo probe. The VAO config-version cold-line load is hoisted
to the top of PrepareForDraw to overlap its miss.

Quiet-box load-gated 6-round order-alternating A/B, all nine cases,
both backends: use_program -27.7%, vanilla_draw -16.2%, ubo_range
-13.4%, pass_switch -11.1%, sampler_churn -10.7%, state_toggle -9.2%,
sodium_multidraw -5.3%, rest flat. No regression on either backend
(magma's one matrix flag disproved by isolated re-runs against
byte-identical DirectVulkan sources). Unit tests 421/421.
2026-08-06 14:07:49 -04:00
BZLZHH 8f2b766b56 [Perf] (MG_Backend): let DirectVulkan trust across frames what it proved once
The draw fast path still paid for its own proofs: the hottest single
load (20% of TrySetupDrawFastPath) was chasing the cold
VertexInputStateFactory heap entry just to answer "same vertex-input
layout?". That answer now comes from the frontend VAO's config-guarded
aux memo (layout hash + attribute masks), and a VAO-cycling stream with
a stable layout skips the pre-flight AND pipeline re-resolution
entirely. The VkProgramObject* is memoized on the snapshot behind a new
ProgramFactory cache-structure epoch (bumped on every insert/erase; use
is re-stamped so the idle sweep can never evict a live entry). A
render-state version move no longer forces the full path: the pipeline
value hash is refreshed in place and the 8-entry memo probed directly
(the GL_BLEND-toggle case).

The resolved-vertex-bindings memo now revalidates all-resident unmapped
entries ACROSS frames via per-binding slice epochs - minted from a
process-lifetime counter so a recycled address can never revalidate,
with every mutation path funnelled through BumpSliceEpoch - while
stamping each resource's GPU-use serial exactly as the skipped acquire
would, preserving the busy-tracking that glBufferSubData's
host-write-vs-staged-copy choice depends on. Resident index buffers get
the same treatment through an EBO slice memo.

The six-part dynamic-state tail (viewport/scissor/blend constants/depth
bias/line width/stencil) is gated behind one render-state-parameters
version + pass-geometry compare per command buffer. GetSlice is inlined;
SampledBindingsUnchanged walks only the program's declared bindings.

Quiet-box 6-round order-alternating A/B (on top of the frontend
VAO-bind commit): vanilla_draw -20.8% (790 -> 626 ns/op, 3.4x native to
2.5x), sampler_churn -28.2%, ubo_range -8.4%, state_toggle -3.8%;
tex_param's matrix flag (+10%) was adjudicated by an isolated
alternating re-run at +1.0% - position bias, not regression. Unit tests
421/421.
2026-08-06 13:18:38 -04:00
BZLZHH b9d8ad0421 [Perf] (MG_Backend): give DirectGLES one epoch that says no buffer moved
Four draw-path costs, one theme: re-proving what nothing invalidated.

A manager-wide buffer-mutation epoch (atomic; bumped with release AFTER
every mutation lands: all six BufferBackendOps via tracking wrappers,
every backend-initiated writeback - XFB readback/scatter, the five
pack-PBO readbacks - registry registration changes, and backend context
destruction; the full site inventory lives in a comment at the accessor)
lets the per-VAO resolved-buffers memo stamp the epoch after one
all-clean probe pass and skip every IsBufferDrawClean probe while it
holds. The IBO keeps its bound-object identity compare - only the probe
is elided. Non-bumping paths are enumerated with why they are safe:
GPU-authoritative writes are ignored by the probe, persistent-mapped
resources are clean by construction, and draws on non-persistent maps
are frontend-rejected GL errors.

GetProgramForDraw is hoisted to one call per PrepareForDraw and handed
to the four consumers that each re-derived it. The enabled-draw-buffers
walk feeding the fragColor broadcast count is memoized on the
(FBO, slot version, object version) trio. The UBO-binding loop probes
IsBufferDrawClean before falling back to EnsureBufferResource.

The texture chain captures (context, maxTouchedUnit, samplingGeneration,
unitBindingsEpoch) once per draw - shared by SyncNeccessaryTextures and
BindCurrentTextures, halving the epoch computations - and an aggregate
gate that is the exact conjunction of the three Sync*ToBackend
early-outs skips the per-texture cross-TU calls.

The t_egl* thread_local verification pair became owner-thread-guarded
atomics reset by MakeCurrent/ReleaseCurrent, removing __tls_get_addr
from the draw loop.

Quiet-box 6-round order-alternating A/B (with the frontend VAO-bind
commit): all NINE Espryt cases improved - sampler_churn -11.0%,
state_toggle -9.3%, ubo_range -9.1%, vanilla_draw -5.4%, pass_switch
-3.5%, the rest -1% to -2.5%. Unit tests 421/421.
2026-08-06 13:16:44 -04:00
BZLZHH f8069c0624 [Perf] (MG_State): stop paying two atomic refcounts for every glBindVertexArray
perf annotate put 94% of VertexArrayState::Bind's 10.5% self time on the
two lock-prefixed shared_ptr refcount RMWs each bind performs. The bound
VAO is now stored as a slot index into m_vertexArrays - no SharedPtr
copy, no atomics on the bind path. The lifetime invariant (the bound
object is kept alive by its slot; any cold path that clobbers a bound
slot - delete-while-bound including slot 0, create-over-bound-slot -
detaches the old object into m_boundDetached so GetBoundVertexArray
keeps answering with it) is enforced in MarkVertexArrayForDeletion /
CreateVertexArrayObject rather than assumed, and documented at the
change. Out-of-range binds and null slots keep their exact old
semantics.

VertexArrayObject also gains two opaque config-version-guarded backend
aux memo words, letting a backend answer "same vertex-input layout?"
from the frontend object instead of chasing its own cold cache entry.

After this change the frontend Bind drops out of the DirectVulkan draw
profile entirely (11.5% -> 0.5%). Measured jointly with the two backend
rounds that land on top: quiet-box 6-round order-alternating A/B,
all nine cases, no case worse than noise on either backend. Unit tests
421/421.
2026-08-06 13:16:02 -04:00
BZLZHH d0aae85da2 [Perf] (MG_Backend): let DirectVulkan's draw fast path survive a VAO swap
TrySetupDrawFastPath declined on its VAO pointer check for every draw of
a 512-VAO cycle - the Blaze3D chunk-render shape - so the fast path was
dead exactly where it mattered: full SetupDraw, per-draw
ResolveSamplerDescriptor, SyncTextureAndGetDescriptor and render-pass
re-fetch, for draws whose only change was the VAO.

Three fixes. A moved VAO now re-runs only the vertex-input pre-flight
and re-resolves the pipeline instead of declining to the full path. That
resolution probes the value-keyed pipeline memo directly off a cached
pipeline-state hash and snapshot render-pass hash, skipping
GetOrCreateRenderPass and its GetPendingRenderbufferClear probes per
draw; a stale cached hash can only miss, never false-hit. And when the
sampler-descriptor hint holds and the program's single dynamic UBO
re-resolves to the same VkBuffer and range - only the dynamic offset
moved, the per-draw glUniform case - the descriptor walk collapses to
one offset recompute and a vkCmdBindDescriptorSets of the same recorded
set with new pDynamicOffsets. The rebind memo is invalidated at
BeginFrame, layout destruction and override walks; the program lifetime
id never repeats, and per-frame descriptor sets are never rewritten
within their frame.

mc_vanilla_draw -36.9% (1260 -> 795 ns/op, 4.6x native to 3.4x),
sodium_multidraw -18.0%, state_toggle -14.9%, sampler_churn -7.8%,
use_program -7.7%, ubo_range -7.3%, tex_param -7.3%. All nine cases on
both backends, interleaved A/B; no attributable regression. Unit tests
421/421.
2026-08-06 12:00:31 -04:00
BZLZHH b904658b10 [Perf] (MG_Backend): stop DirectGLES re-resolving the same VAO's buffers and twins every draw
Four per-draw costs, all lookups that re-answer the same question.

SyncNeccessaryBuffers walked all 32 attribute slots cold and ran
EnsureBufferResource per buffer on every draw. The backend VAO twin now
hosts a resolved-draw-buffers memo: the deduped enabled-attribute buffers
and the index buffer resolve once per VAO config version, and each hit
re-validates every entry with IsBufferDrawClean - a shadow probe mirroring
every no-op branch of EnsureBufferResource (resource identity, context
generation, pending ops, change serial) - falling back to the full path
for just the dirty entries. The IBO entry is checked against the live
bound object each draw, so slot-version wrap cannot false-hit.

The registry hash Finds that resolve state objects to their backend twins
ran several times per draw. TwinLookupMemo - a direct-mapped,
Fibonacci-hashed table (4096 VAO / 256 program slots) with weak-ptr owner
equality against address reuse - answers them in one probe; collisions
fall back to the registry. A live entry's twin is never replaced once
set, so owner equality proves the raw pointer.

SyncCurrentVertexAttributeValues' pending-mask memo was a function-static
single entry that missed every draw once the app cycled VAOs; it now
lives on the twin. CurrentXfb()'s per-draw FastSTL map lookup became a
cached pointer invalidated at every map mutation (open addressing moves
values on any insert/erase/clear).

mc_vanilla_draw -12.6% (3.4x native to 3.0x), ubo_range -9.8%,
sampler_churn -7.5%, pass_switch -6.5%, sodium_multidraw -6.0%,
state_toggle -4.6%. All nine cases measured on both backends, interleaved
A/B; no case regressed. Unit tests 421/421.
2026-08-06 12:00:31 -04:00
BZLZHH 4b3fd11462 [Perf] (MG_Backend): key DirectVulkan's pipeline memo on state values, not a version that never repeats
Two per-draw churn costs, one cause each.

A blend toggle switched pipelines through a memo keyed on a monotonic
pipeline-state version - which never repeats, so flipping GL_BLEND off and back
on produced a "new" key both times, forced the full SetupDraw and rebuilt the
whole pipeline payload for a pipeline the cache already held. The memo now keys
on a value hash of the pipeline-relevant fixed-function state, recomputed only
when the state version moved, and the consecutive-draw fast path re-resolves
just the pipeline through it when nothing but render state changed. Blaze3D
brackets every batch with exactly this toggle; mc_state_toggle drops 36%
(6629 -> 4230 ns/op, 4.8x native to 3.7x).

The sampler-churn cost had the same shape as the Espryt side fixed separately:
glBindSampler bumps the frontend texture-bind generation even when it re-binds
the sampler the unit already holds, so the per-draw fast path died every draw.
The fast path now proves each binding's descriptor inputs unchanged - texture
and sampler lifetime ids, parameter and content sums, the sampling-resolution
generation, image epochs and exact layouts - and reuses the binding's cached
VkDescriptorImageInfo instead of re-running the resolve chain.
mc_sampler_churn drops 30% (1597 -> 1125), and the proof machinery pays for
itself on the uniform-range case too (-17%).

mc_tex_param stays where it is on this backend deliberately: profiling shows its
remaining cost is frontend validation with zero backend work, unreachable from
Renderer/.

All nine cases measured on both backends, interleaved A/B, no case worse than
noise. Unit tests 421/421.
2026-08-06 11:21:26 -04:00
BZLZHH 9be5d95440 [Perf] (MG_Backend): give DirectGLES unit bindings an epoch the sampler churn cannot fake
The texture-binding memos added earlier keyed on the frontend texture-bind
generation, and 26.2-style unit switching defeats them: glBindSampler bumps the
generation even when it re-binds the sampler the unit already carries, so a
frame that cycles active units re-ran the full two-pass, eleven-slot alias
resolution and the unbind walks on every draw. mc_sampler_churn sat at 1674
ns/op against the native driver's 239 - the worst multiplier left on this
backend - with about half the time in two virtual calls per binding slot.

The units now carry an epoch: a snapshot of each touched unit's slot objects and
sampler object, compared by weak_ptr OWNERSHIP rather than raw pointer - a held
weak_ptr pins its control block, so a freed-and-recycled object can never
owner-equal its predecessor, which is the ABA hole a pointer key would have and
the reason version keying was rejected (WithTemporarilyBoundNamedTexture bumps
slot versions without touching the bind generation). The
(context id, bind generation, high-water mark) triple gates the snapshot walk to
at most once per draw; the epoch moves only when a binding really changed. Both
per-draw memos key on the epoch plus the sampling-resolution generation, which
carries what the epoch cannot see: a default texture's image appearing, and
every completeness input. Two smaller memos ride along: the per-unit
sampler-registry lookup (owner-keyed, misses never cached - the backend object
may be created later in the same draw), and the pending-vertex-attribute mask,
whose first version scanned all 32 slots and put +10% on the VAO-cycling case
before being restricted to the program's active locations.

ns per op, DriverBench on a GTX 1660 SUPER, isolated A/B, all nine cases on both
backends: mc_sampler_churn 1673 -> 732, mc_use_program 4513 -> 4279,
mc_state_toggle 2365 -> 2247, everything else within noise and nothing worse.
7.0x native to 3.1x on the churn case.

Unit tests 421/421.
2026-08-06 11:20:52 -04:00
BZLZHH d49d79a64b [Perf] (MG_Backend): pool DirectVulkan's upload staging and batch its submits
Every dirty texture bought itself a fresh staging buffer (vmaCreateBuffer +
vmaMapMemory), a fresh command buffer, a fresh fence, and its own vkQueueSubmit.
A perf profile of the sprite-animation case put 41% of the whole run in the
kernel on the resulting ioctl traffic; the reclaim list already avoided waiting
on the fences, so the cost was the allocation and submission machinery itself,
paid per texture per frame.

Staging now comes from a pool of persistently-mapped blocks (1 MiB minimum,
exact-size beyond that, bump-allocated, 32 MiB idle cap), and uploads record
into one shared batch command buffer from a dedicated command pool, going out as
one submit with one pooled fence per flush. Fences, command buffers and blocks
all recycle through the existing fence-list reclaim instead of being destroyed.
Flush points: before every frame command buffer submission (which is what
preserves the old ordering argument - the batch reaches the queue strictly
before anything that could sample its images), on the glFlush finite-time path,
when a batch would outgrow its staging bound, and eagerly at 128 KiB, which
measured faster because the GPU overlaps the copy with the rest of the frame's
CPU recording. The mid-frame upload-draw-upload-again sequence detects itself
through the batch image list and flushes first, reproducing the old two-submit
granularity exactly; a deferred image release flushes any open batch that still
references the image, because drain proofs only cover submitted work.

ns per op, DriverBench on a GTX 1660 SUPER: mc_tex_stream 9405 -> 5373 (2.3x
the native driver, from 3.9x), atlas_sprite -57%, lightmap -89%, chunk_upload
-10%; draw-path cases unchanged. The suite's sampler-churn number reads a few
percent worse right after the now-much-faster upload case, which was chased to
schedutil downclocking during the newly-blocking-free frames - isolated and
frequency-pinned runs measure parity; noted here so the next person does not
re-chase it.

Unit tests 421/421; Vulkan validation layer clean across draw and upload cases.
2026-08-06 10:36:08 -04:00
BZLZHH f5761ea1f3 [Perf] (MG_Backend): diff only the render-state span that moved, and gate the per-draw walks
Four per-draw costs in DirectGLES, all of the same species: work re-done for an
answer that had not changed.

SyncRenderState was guarded by a single version compare, so one blend toggle -
the way Blaze3D brackets every batch - re-diffed the whole ~40-field render
state block and copied the full struct back into the shadow, every draw. The
parameter struct is now split into three contiguous byte spans, each gated by a
memcmp against the backend shadow; a per-draw blend flip touches only the blend
span. The shadow is byte-cloned after each sync so the span compares stay exact,
padding included. Blocks whose inputs live outside the parameter struct (the
surface-size viewport fallback, the sRGB context capability) stay ungated, and
the dual-source-blend hard-fail still fires every draw because a throwing sync
never stamps the shadow.

SyncMipmapsToBackend gained a first-level clean gate on (context id,
sampling-resolution generation, content version, params version) that skips the
IsComplete walk and the eight-field shape probe outright; every shape mutation
funnels through BumpShapeVersion, which is what makes the gate sound.
SyncToBackend for vertex arrays compares one aggregate config version instead of
three stamps per attribute slot. And SyncNeccessaryTextures memoises the
draw-framebuffer attachment list, keyed the same way the framebuffer sync memo
already is, instead of re-walking attachments per draw.

ns per draw, DriverBench on a GTX 1660 SUPER, isolated A/B: mc_state_toggle
3151 -> 2397, mc_ubo_range 792 -> 579, mc_vanilla_draw 1111 -> 881,
mc_sampler_churn 2019 -> 1676, mc_use_program 5132 -> 4356; every one of the
nine cases improved. Against the native driver Espryt now stands at 3.6x on the
plain draw path, 2.8x on the per-draw uniform-range path and 2.1x on the blend
toggle, from 8.7x / 9.1x / 7.2x when this effort began.

Unit tests 421/421.
2026-08-06 10:35:41 -04:00
BZLZHH b3f774d2c0 [Fix] (CI): name the EGL vendor library the benchmark job runs on
The benchmark job is the only one that brings a real GL context up - DriverBench
dlopens libEGL.so.1 and renders through it - but its apt list only asks for
libegl1, which is glvnd's dispatch layer and nothing more. The vendor library
behind it, libegl-mesa0, has been arriving as a Recommends of libegl1 rather
than because anything asked for it. That is too quiet a dependency for the one
job whose whole purpose is running a driver: a base image change, or
--no-install-recommends turning up anywhere upstream, would leave eglInitialize
with no vendor to dispatch to and fail the job for a reason nothing in the
workflow explains. Name it, next to libgl1-mesa-dri, which is listed for
exactly the same reason.

Verified with a full headless ctest -C Release -L benchmark - no $DISPLAY, no
$EGL_PLATFORM, mesa as the only EGL vendor: SanityBench, ProgramBench,
BufferBench and DriverBench all pass.
2026-08-06 09:44:04 -04:00
BZLZHH d524330032 [Test] (MG_Benchmark, MG_Util): model four more Minecraft frame patterns in the driver bench
The captured traces contain per-frame patterns the bench did not exercise, and
first measurements show two of them are now the worst remaining multipliers -
which is exactly what the missing cases were hiding.

mc_pass_switch: the 26.2 snapshot switches render targets 132 times a frame and
re-declares draw buffers 198 times. Render-target churn is where a Vulkan
backend pays for render-pass breaks and where a tiler pays most on device, and
no case measured it. mc_state_toggle: Blaze3D brackets batches with blend
toggles - 46 enable/disable pairs and 28 blend-func changes per vanilla frame.
mc_tex_param: 26.2 re-sets texture parameters 612 times a frame, almost always
to the value already in place, so this measures redundant-parameter filtering.
mc_use_program: Sodium switches programs 62 times a frame with a mat4 upload on
each, roughly one switch per multi-draw.

All four live in the shared case file at the measured per-frame rates, so the
desktop harness, the on-device harness and the POST screen's Run Bench report
comparable numbers. First desktop measurements (ns/op, native / Espryt / Magma):
pass_switch 8877 / 18502 / 13896, state_toggle 1182 / 8526 / 8305,
tex_param 42 / 102 / 197, use_program 2182 / 10648 / 5096. The state-toggle
multiplier - 7x on both backends - is the largest newly exposed gap and the next
optimization target.

Unit tests 421/421; the Android JNI translation unit compiles against the
extended case set.
2026-08-06 09:39:02 -04:00
BZLZHH f2d210b12d [Perf] (MG_Backend): memoise DirectVulkan's per-draw vertex binding resolution
Every draw re-resolved its whole vertex binding array: for each enabled binding,
look up the buffer, acquire a slice from the buffer manager, apply the binding's
base offset, fill the VkBuffer and offset arrays, bind. In the Minecraft-shaped
benchmark the same few hundred vertex array objects cycle for the whole run and
each one's answer is stable, so UploadAndBindVertexBuffers was the single largest
cost in the backend at 7.9% of the render thread, with AcquireResidentSlice
another 3.8% underneath it.

The resolved array is now kept per vertex array object and revalidated instead of
rebuilt. Validation is two-tier. The vertex array's own configuration version
already invalidates its backend vertex-input state, so a changed attribute,
format, buffer or base offset yields a different state object - the memo compares
both that object's address and its hash, which mixes the bound buffers and the
whole layout. What that does not cover is the slice moving underneath an
unchanged configuration, so the buffer manager now carries a monotonic epoch that
every writer of slice-deciding state bumps: resident storage creation, respecify,
sub-data, flush of a mapped range, the promotion and demotion between streamed
and resident storage, each fresh arena allocation, and bulk release. The counter
is manager-wide and never reset, so a resource created at a recycled address
cannot reproduce a value some memo still holds.

The miss path was the thing to get right, because the previous attempt in this
area regressed the texture-upload and sampler-churn cases by 60-85%: it added a
verification pass that re-ran the resolution work it was trying to skip, so every
miss paid for it twice. Here a miss is one pointer-keyed lookup and a few stores,
and nothing else runs that the full path would not have run anyway.

ns per draw, DriverBench on a GTX 1660 SUPER: mc_ubo_range 924 -> 767,
mc_vanilla_draw 1346 -> 1227, mc_sampler_churn 1397 -> 1279,
mc_sodium_multidraw 3365 -> 3266. Magma is now 4.1x the native driver on the
per-draw uniform-range case, from 5.4x when this round started. No case
regressed on either backend.

Unit tests 421/421.
2026-08-06 09:24:08 -04:00
BZLZHH fd40960f70 [Perf] (MG_Backend): revive DirectGLES's dead framebuffer-sync guard, and stop probing twice
SyncCurrentFBO has an early-out that compares three memos, and it could never
fire. One of the three, g_fboBindVersions, was only ever stamped by
ForceBindCurrentFBO - which runs from glBlitFramebuffer and the DSA
glClearNamedFramebuffer* paths and nowhere else. An application that touches
neither leaves that memo at 0 while the binding slot's version is at least 1 from
its first glBindFramebuffer, so the first term mismatched forever and the guard
was dead code rather than merely too coarse. Every draw therefore re-walked all
40-odd attachment slots and rebuilt the 8-slot snorm/unorm clamp mask for a
framebuffer that had not changed since the previous draw.

SyncCurrentFBO now stamps all three memos itself, through one helper, on every
path that leaves the target synced - including the default-framebuffer
"nothing to do" path, which previously returned without stamping anything. The
memo is renamed to say what it now records (a sync, not a bind). Instrumenting a
throwaway build put it at 539998 hits against 2 misses, the misses being the
first bind of each target; it was 0 hits before.

Skipping the sync also skips the Bind() inside it, so all eleven call sites were
checked: every one issues its own bind afterwards (PrepareForDraw and the
glClearBuffer* paths bind Draw, ReadPixels and the CopyTexSubImage paths bind
Read, BlitFramebuffer binds both, GetTexImage uses its own scoped binder). The
global snorm/unorm clamp masks written inside the sync stay correct because they
can only be stale if a different framebuffer was synced as Draw in between, which
moves the pointer or slot version and forces the re-sync that rewrites them.
InvalidateFramebufferBindingCache now also clears these memos: both its callers
mean the ES context may have been reset, and a live early-out must not survive
that.

Two smaller items in the same pass. StateBackendObjectRegistry kept the backend
twin and its liveness weak_ptr in two maps, so every lookup cost two hash probes
and the draw path does ten to twenty of them; they are one map with one entry
type now, one probe. The weak_ptr check itself is load-bearing and stays -
glDeleteVertexArrays followed by glGenVertexArrays recycles heap addresses
readily. And SyncNeccessaryBuffers ran the full EnsureBufferResource check once
per enabled vertex attribute, which on an interleaved Minecraft-shaped VAO means
four to eight times over the same VBO; it is deduplicated per distinct buffer now.

ns per draw, DriverBench on a GTX 1660 SUPER, A/B against a build differing only
by this diff: mc_vanilla_draw 1403 -> 1113, mc_ubo_range 983 -> 797,
mc_sampler_churn 2309 -> 2003, mc_sodium_multidraw 3232 -> 3023. Against the
native driver Espryt is now 4.3x on both the plain draw and the per-draw
uniform-range case, from 8.7x and 9.1x at the start of this work.

Unit tests 421/421. Also replayed all 38 locally-available DirectGLES trace
fixtures against a baseline library: every one produced bit-identical ssim and
mismatched-pixel counts, including the improved-transparency OIT trace whose
scratch clear framebuffer is exactly the draw-buffer hazard the code comments
warn about.
2026-08-06 09:23:47 -04:00
BZLZHH 49aab57f03 [Perf] (MG_Backend, MG_State): stop re-resolving texture unit bindings on every draw
DirectGLES re-derived the whole texture binding state for every draw: for each
touched unit, two alias-resolution passes over all binding slots, then a third
walk to unbind native targets nothing claimed, then the sampler. With the
Minecraft-shaped bench that was 13.2% of the render thread in BindCurrentTextures
alone, plus 4.6% in SyncNeccessaryTextures deciding which textures to consider.
The answer is identical across a whole terrain batch.

The resolution is now memoised, and what makes replaying it as a no-op legitimate
is that the memo does not merely trust a key: it compares the backend's own bound
texture shadow against the one resolution left behind. Every path that binds a
texture behind this function's back already maintains that shadow - the scratch
bind an upload does on the temp unit, CopyTexSubImage2D and GenerateMipmap
binding on the active unit, the glBindTextures fast path, the scrub a backend
texture performs when it is destroyed or respecified - so a memcmp catches all of
them without having to enumerate them. On top of that the key covers the texture
bind generation, the program that arbitrates aliased targets (pointer, lifetime
id, backend state version, link status), and the ES context generation.

Two invalidation sources had no signal at all and needed one. Mipmap completeness
decides whether a texture is bound in the first place, and it moves with texture
shape and with the effective sampler's filter - so a sampling-resolution
generation now moves with both, routed through single choke points
(TextureObjectBase::BumpShapeVersion, SamplerObject::BumpVersion) so a future
bump site cannot forget it. A texture context id was needed because both
generations restart at zero in a new GLContext, which can land on the old heap
address.

This also closes a pre-existing hole rather than working around it:
glDeleteSamplers unbinds the sampler from every unit straight through
TextureUnit::SetSamplerObject, bypassing the touch bookkeeping, so that setter now
bumps the bind generation on a real change. The sampler bind step itself stays
outside the memo and runs every draw - the program's raw-depth-fetch substitution
rewrites unit samplers immediately afterwards, so a memo there could never hit.

ns per draw, DriverBench on a GTX 1660 SUPER (native / Espryt):
mc_vanilla_draw 253 / 2037->1315, mc_ubo_range 202 / 1684->955,
mc_sodium_multidraw 739 / 3939->3150. Espryt goes from 8.3x to 4.7x the native
driver on the per-draw uniform-range case. Magma is unaffected (the MG_State
additions are counter bumps), and no case regressed.

Unit tests 421/421.
2026-08-06 07:41:10 -04:00
BZLZHH 62dea3bea4 [Perf] (MG_State): answer texture sampling completeness from a memo
Every draw asks, for every bound texture, whether it is mipmap-complete for the
filter in use, and the answer was recomputed from scratch each time: walk the
level chain, read each level's texel size, verify each is half the previous.
With the Minecraft-shaped bench that walk plus the GetTexelSize calls under it
measured about 8% of the render thread on both backends.

The answer depends only on the texture's shape - internal format, stored level
set, level sizes, level range - and never on its texel content, which is the
thing that actually changes between draws. A shape version now moves on exactly
those four mutations (SetInternalFormat, SetBaseLevel/SetMaxLevel, and the
AllocateStorage/TruncateMipmapLevels pair on both mipmap storage classes), and
the completeness answer is memoised against it, one slot for the mipmapped
question and one for the plain one. An upload leaves the memo standing, which is
the whole point; anything that could change the answer invalidates it.

ns per draw, DriverBench on a GTX 1660 SUPER (native / Espryt / Magma):
mc_vanilla_draw 257 / 2201->2037 / 1550->1346, mc_ubo_range 203 / 1832->1684 /
1089->934, mc_sampler_churn 272 / 2349->2325 / 1533->1396. Texture-upload cases
are unchanged, as expected - they were never asking this question in a loop.

Unit tests 421/421.
2026-08-06 06:42:43 -04:00
BZLZHH 57aeeec053 [Perf] (MG_State, MG_Impl, MG_Backend): stop paying per draw and per upload for work already known
A per-draw CPU profile of a real Minecraft frame (perf on the render thread,
which sits at 100% of one core on both backends) said the deficit is translation
overhead, not the GPU, and named where it goes. This removes the largest items
it found, on both backends and in the shared frontend they both feed.

The single biggest one was not translation at all: IsBackendContextCurrentOnThisThread
called eglGetCurrentContext on every invocation, and glvnd answers that with a
getpid() fork check - a real syscall. The predicate sits two and three deep in
every draw (the deferred-release drain, the global-UBO ring availability check,
and the ring allocation), so it accounted for 16.3% of the render thread. EGL is
still the ground truth, but re-verifying it once per thread per frame catches an
external migration at the next frame boundary rather than the next call, which
recovers the same bookkeeping.

Texture uploads now carry a dirty region instead of a per-level flag. Minecraft
animates atlas sprites with 16x16 glTexSubImage2D calls into a 1024x512 atlas
and respecifies the lightmap every frame; a per-level flag turned each of those
into a full-level re-upload - about 3.6 MB a frame of texels nobody changed.
MipmapStorage accumulates the written box, Espryt uploads it with
UNPACK_ROW_LENGTH striding into the level shadow, and Magma stages just that box.
The box is a union, not a range list: repeated writes to one level widen it and
it degrades to exactly the old whole-level upload, which is the honest worst case.

glBufferData(NULL) is the orphaning idiom, and the backend was answering it by
uploading the stale CPU shadow - turning a rename the driver does for free into
a full synchronized upload. BufferObject now records that a NULL respecify leaves
the store undefined, and the upload is skipped until content is actually written.

The rest are smaller and of a kind: the deferred-release queue is probed without
taking its mutex, the UBO ring waits on the frame fence that frees the space it
needs instead of draining the whole pipeline with glFinish at the size cap, VAO
binds go through a shadow so a draw's second bind of the same object does not
reach the driver, the per-draw clean-texture probe short-circuits on the content
version before rebuilding shape info, glUniform drops byte-identical writes
(which otherwise dirty the whole UBO for the next draw), re-binding the texture
or VAO a slot already holds no longer bumps the generation counters a backend
fast path is keyed on, and the texture validators stopped taking shared_ptr by
value.

On Magma: descriptor-set reuse keeps four entries instead of one, because draws
alternating between two programs - the chunk/entity ping-pong - thrashed a single
slot into a full re-allocate and re-write every draw; a DynamicDraw buffer whose
contents survive two frame boundaries is promoted to resident storage instead of
being re-copied into the per-frame arena forever; and sampled-read barriers name
only the shader stages whose device feature is enabled, which also removes a
latent VUID violation (ALL_GRAPHICS names geometry and tessellation stages a
device need not have).

Measured with the Minecraft rig (render distance 32, p50 fps, same machine,
single sample each): vanilla 1.21.1 Espryt 10.8 -> 36.3 and Magma 31.3 -> 44.6;
26.2 snapshot Magma 114.5 -> 210.5. Fabric+Sodium moved inside noise on Magma
(854 -> 766) with the native baseline itself moving 838 -> 1031 between the two
sessions, so treat that cell as unresolved rather than a regression measured.
Unit tests 421/421. The CTS A/B was not run: these numbers and the test suite are
the whole of the evidence, and a conformance regression would not have been
caught here.
2026-08-06 06:24:37 -04:00
BZLZHH 9c0144d24a [Test] (MG_Benchmark, MG_Util, MG_Backend, android-plugin): run the driver benchmark on a phone
The Minecraft-shaped driver benchmark could only be run from a desktop shell
against a desktop driver, which is the wrong machine: MobileGL exists to run on
mobile GPUs, and nothing said what its translation costs there. This puts the
same cases on an Android device, both in the plugin's POST screen and from a
shell, and adds the native-driver baseline they have to be read against.

The cases move into DriverBenchCases.inc so both harnesses run byte-identical
bodies - the desktop program resolving entry points from one EGL provider, and
DriverBenchJni.cpp calling MobileGL's frontend in-process. The JNI file binds
every gl*/egl* name to MG_Impl by macro rather than by linkage: this library
legitimately has the platform libEGL and libGLESv3 in its own lookup scope, and
a benchmark that quietly measured the device driver instead of the translation
layer would have looked like very good news.

Frames are now closed with a fence wait instead of glFinish. MobileGL implements
glFinish and glFlush as no-ops, so the old loop timed submit-plus-GPU on a native
driver and submit-only on a MobileGL backend, and the two numbers did not
describe the same work.

To measure a device's own driver the cases needed to be expressible in GLES:
ESSL 3.20 twins of the four shaders (chosen at runtime from GL_VERSION, since
MobileGL is deliberately still fed desktop GLSL - translating it is the thing
under test), a multi-draw hook that loops DrawElementsBaseVertex where the
multi-draw entry point does not exist, and an EGL bootstrap that falls back from
desktop GL to GLES 3. The binary cross-compiles for arm64 unchanged.

BenchService hosts each run in its own process and exits afterwards. That is not
caution: the backend is latched from MOBILEGL_BACKEND_TYPE at initialization, so
Espryt and Magma can never share a process, and Espryt's teardown terminates the
process-default EGL display, which would take the POST activity's own EGL
objects with it.

Running it found that Magma could not create a windowless context on Mali at
all - CreateInstance required VK_EXT_headless_surface, which no mobile driver
here exposes, and aborted the process. The Xlib path already probes and falls
back to a hidden window for the same reason on NVIDIA; Android now probes too
and hands the WSI an AImageReader's ANativeWindow, a real producer surface
attached to no display whose images are never acquired. DriverPost reports the
extension's absence as a WARN so the fallback is visible rather than silent.

Measured on a Mali-G77 MC9 (native / Espryt / Magma, ns per operation):
5495 chunk draws 14397 / 36934 / 33763, the 26.2 per-draw uniform-range pattern
13710 / 31205 / 21252, sodium-style multi-draw 256956 / 238389 / 209527. The
translation costs about 2.4x per draw here against 5-9x on the desktop, because
the mobile driver's own per-call cost dwarfs it - and both backends beat the
native driver on multi-draw, which it has to emulate.

Desktop unit tests 421/421; the POST screen and both Run Bench buttons verified
on the device.
2026-08-06 06:13:34 -04:00
BZLZHH 1e45958e01 [Test] (MG_Benchmark): measure the driver work a real Minecraft frame asks for
The benchmark tree had nothing that exercised a driver: SanityBench times
std::vector, and the Buffer/Program benches call into MobileGL_s directly, so
neither can say what a backend costs against the native driver. This adds a
headless EGL client that can, and shapes its cases from measured traces rather
than guesses.

DriverBench dlopens exactly one EGL provider - the system libEGL.so.1, or a
libMobileGL.so with MOBILEGL_BACKEND_TYPE selecting Espryt or Magma - so the
same binary measures all three stacks with no LD_LIBRARY_PATH shadowing, which
matters because MobileGL's own loader has to keep finding the real driver
underneath. It renders into its own renderbuffer FBO on a 64x64 pbuffer and
paces frames with glFinish, so it needs no window and no compositor.

The six mc_* cases replay the per-frame call mix of 30-second render-distance-32
captures of three Minecraft versions, at the rates those captures measured:
vanilla 1.21.1 issues 5495 glDrawElements per frame, each preceded by its own
glBindVertexArray and glUniform3fv; Fabric+Sodium collapses the same scene into
132 glMultiDrawElementsBaseVertex; the 26.2 snapshot issues 3401
glDrawElementsBaseVertex, each preceded by glBindBufferRange + glBindBuffer.
The texture case wraps every 16x16 atlas upload in the four glPixelStorei and
two glTexParameteri calls Blaze3D re-sets around it, because that wrapper is a
large part of what an upload costs a translation layer. One bench frame
therefore costs what one real frame of that version costs, and ns_per_op is
directly comparable across renderers.

run_driver_bench.sh pins __EGL_VENDOR_LIBRARY_FILENAMES and VK_ICD_FILENAMES.
Without that, eglGetDisplay(EGL_DEFAULT_DISPLAY) on this glvnd system resolves
to Mesa llvmpipe and the "native" numbers silently describe a software
rasteriser - the first run of this bench reported 11 us per draw before the
pin, versus 250 ns on the real GPU.

Verified against the NVIDIA 610.43.03 driver, Espryt and Magma on a GTX 1660
SUPER; the CMake target builds and runs from a clean configure.
2026-08-06 03:57:52 -04:00
BZLZHH 6e6f5268fb [Fix] (MG_Backend): let a default-visual X11 window match an alpha-free config
ChooseConfigForSurface prefilters candidate configs with eglChooseConfig
requiring EGL_ALPHA_SIZE 8, then tries to match the window's X visual. On
NVIDIA's X11 EGL every alpha-8 config lives on the 32-bit ARGB visual, and the
default depth-24 TrueColor visual only appears on alpha-0 configs - so for any
window created with the default visual the match loop scanned a list that
could not contain its visual, fell through to a 32-bit-visual config, and
eglCreateWindowSurface failed with EGL_BAD_CONFIG.

Keep the alpha-8 list as the first tier and add an alpha-relaxed second tier
used only for the visual match; the sizeless fallbacks below still run on the
alpha-8 list. Mesa is unaffected (its default-visual configs carry alpha), and
a destination-alpha-free default framebuffer is exactly what native GLX hands
out on these visuals anyway.

Found by running Minecraft through the new GLXImpl on Espryt: NVIDIA EGL also
needs EGL_PLATFORM=x11 under a Wayland session or eglGetDisplay itself returns
no display, which is a launcher-environment concern, not a library one.
2026-08-05 23:12:12 -04:00
BZLZHH 08f98ad9ce [Feat] (MG_Impl): implement GLX 1.4 on the EGL layer so GLFW apps run on Linux
Desktop Linux GL apps (GLFW/LWJGL, glxgears, anything X11) create contexts
through GLX, and MobileGL only spoke EGL - the two exported glX symbols were
proc-address stubs that could resolve GL entry points but never produce a
context. GLXImpl is the missing sibling of WGLImpl/CGLImpl: the same
window-system-binding pattern, calling the internal MG_Impl::EGLImpl namespace
directly.

The surface covers exactly what GLFW 3.4 resolves via dlsym plus the legacy
visual API: FBConfig enumeration mirrors the two EGLState configs (stencil-8
first so stencil-wanting choosers land on it), glXGetVisualFromFBConfig answers
with the screen's default visual (falling back to any 24-bit TrueColor one),
and glXCreateContextAttribsARB maps the ARB attribs onto EGL context attribs
the way WGL's Ext_CreateContextAttribsARB does - profile mask only emitted for
3.2+ or an explicit profile request, since that bit is what keys MobileGL's
relaxed-semantics compatibility mode. Legacy glXCreateContext/CreateNewContext
hand out 3.3 compatibility contexts, matching wglCreateContext.

Drawables follow the WGL HWND model: the GLXWindow is the X window itself, the
EGL window surface is created lazily on first MakeCurrent and cached per XID,
and the GLX layer owns size discovery per the platform-layer contract - it
pushes changes through EGLImpl::ResizePlatformWindowSurface, polling
XGetGeometry on MakeCurrent and on swaps throttled to 250ms so a fast-swapping
app is not paying a server round trip per frame. libX11 is dlopen'd at runtime
like everywhere else in the tree; Xlib.h is already in every TU via the vulkan
include, so XVisualInfo gets an ABI mirror struct (Xutil.h needs the Bool and
Status macros that Includes.h deliberately pops) and the caller's XFree pairs
with our malloc.

glXGetProcAddress now resolves glX names from the export table before falling
through to the shared GL resolver, which previously returned nullptr for every
glX extension entry point - GLFW requires glXCreateContextAttribsARB and
glXSwapIntervalEXT to arrive that way.

Verified with a smoke test replaying GLFW's exact call sequence (dlsym-only
resolution, manual FBConfig filtering, 3.2 core forward-compatible context,
glXCreateWindow, 60 swapped frames, clean glGetError) on both backends against
the real NVIDIA driver, then with Minecraft 1.21.1, 1.21.4+Fabric+Sodium and
26.2-snapshot-6 reaching in-world rendering on both Espryt and Magma.
2026-08-05 23:08:29 -04:00
BZLZHH d39a706d57 [Perf] (MG_Backend): stop paying for descriptor slots and mip barriers nobody asked for
Five independent bits of per-draw and per-operation waste in the DirectVulkan
backend, all removing work whose answer was already known.

The per-draw descriptor walk iterated all 256 slots of bindingKinds to find the
one to eight bindings a real GL program declares, because that vector is sized to
the binding cap rather than to the program. Reflection now records the bindings it
actually assigned, and the draw path iterates that. It is built at the end of
ReflectLayout, not where bindingKinds is sized - at that point the vector is only
zero-initialised and the kinds are assigned further down, so a list built there
would be empty. It has to stay ascending: Vulkan consumes pDynamicOffsets in
binding order and the writer pushes them in iteration order, so an unordered list
would silently mis-pair dynamic offsets with their uniform blocks.

Descriptor pools were sized maxSets * the 256-binding cap, declaring 81,920
descriptors per pool and 245,760 across the frames in flight, for sets that hold
what shader reflection found. Sized from eight now; an outlier program is absorbed
by the VK_ERROR_OUT_OF_POOL_MEMORY path that already exists, which works because
pool sizes are aggregate budgets rather than per-set limits.

TrackLiveResource swept the whole live-buffer vector on every insert once it
passed 256 entries, and when the buffers are all live the sweep removes nothing
and the vector grows by one - so creating N live buffers cost about N^2/2
expired() checks. It sweeps on a doubling watermark now, with the same
reclamation semantics.

GenerateMipmap transitioned each destination level individually inside its loop,
but every generated level starts in the same layout and the loop only moves a
level out of TRANSFER_DST after writing it, so the whole range can be prepared in
one barrier - 3(N-1)+1 barrier commands become 2(N-1)+2. Each level is still
transitioned to TRANSFER_SRC before it is read, so the dependency between
consecutive levels is unchanged.

WaitForFrameSerial drained the entire graphics queue, as its own comment admitted.
Every submission records the frame serial it was made under, so it now waits on
the first fence at or past the requested serial. The narrow path deliberately does
not call NotifyDeviceIdle(): that claims every submission has retired, which is
only true after a real drain, so it stays on the fallback.

Verified with an 8213-case A/B (textures, buffers, queries, mipmaps, uniforms and
the whole direct_state_access suite): the Espryt failure list is identical, the
Magma failure list differs by one case, and both crash sets are unchanged on
Magma. That one case, buffer_storage.map_persistent_draw, does not reproduce in
isolation - running the buffer_storage group alone gives byte-identical results on
both builds (the same three failures, not including it), and it reports
NotSupported when run on its own. It is the same ordering-dependent behaviour this
suite shows elsewhere, and the three Espryt crash-set differences are the known
copy_image cluster moving chunk position. Flagging rather than hiding it.

direct_state_access stays at Espryt 370/371 and Magma 371/371; unit tests 421/421.
2026-08-05 15:19:37 -04:00
BZLZHH f3d52faad4 [Perf] (MG_State, MG_Backend): stop glViewport from evicting a cached VkPipeline
RenderState kept one version counter for all render state, and DirectVulkan read
it in three places: the pipeline memo key, the SetupDrawSnapshot fast-path guard,
and that guard's store. So glViewport, glScissor, glBlendColor, glStencilMask,
glClearColor, glPolygonOffset, glLineWidth and the point-size family - none of
which can alter a VkPipeline, all of which an application changes between draws -
knocked the next draw off both fast paths and made it rebuild a pipeline lookup
that was already correct.

The counter is now split. m_version still moves on every state change, because
the draw snapshot really does depend on all of it. m_pipelineStateVersion moves
only for the state a backend bakes into a pipeline object, and it is what the
three DirectVulkan sites read.

The exclusion list is the eight VkDynamicState entries PipelineFactory declares
plus the state that is not pipeline state at all (the clear values, hints, the
point-size family, clamp read colour, the primitive restart index). glStencilFunc
is the one setter that had to be split rather than classified: Func is in the
pipeline payload but Ref and ValueMask are dynamic state, so it bumps the
pipeline version only when Func actually changes.

Capabilities are deliberately NOT in the exclusion list even though several look
like dynamic state: GL_FRAMEBUFFER_SRGB feeds the render-pass hash, depth and
stencil test feed drawUsesDepthStencil, and scissor test, blend, cull face,
polygon offset fill, primitive restart, colour logic op and rasterizer discard
all feed the pipeline payload.

Two smaller draw-path wins ride along, both removing work whose answer was
already in hand. UploadAndBindVertexStreams searched all 32 VAO attribute slots
for the SharedPtr matching a binding's buffer key, once per binding per draw -
but VertexInputStateFactory writes bindingBufferKeys[b] and
bindingAttributeLocations[b] from the same loop iteration, one binding per
attribute with no merging, so the attribute at that location IS the buffer, by
construction. UploadAndBindIndexBuffer round-tripped the element-array buffer's
raw pointer back through the GL name table on every indexed draw, costing a map
lookup and an atomic refcount pair, when the binding slot's SharedPtr was already
in scope forty lines above - where a comment says exactly that about the vertex
path.

Behaviour-neutral by construction and verified as such: a 13355-case subset of
GL30-GL45 covering viewport, scissor, blend, stencil, depth, polygon offset,
clear, multisample, cull, logic op, line width and point state, plus the whole
direct_state_access suite, is identical before and after on both backends - in
the failure list and in the crashed-case set. direct_state_access stays at
Espryt 370/371 and Magma 371/371.
2026-08-05 12:49:54 -04:00
BZLZHH ba81ee114e [Feat] (MG_Backend, MG_Impl, MG_Util): attach one layer of any layered texture on DirectVulkan
Whether a backend can attach a single layer of a texture to a framebuffer was one
Bool, so it could only give the most conservative answer any target needed.
DirectVulkan therefore declined every layer of every target and
direct_state_access.framebuffers_texture_layer_attachment failed with 542
messages across four targets.

The three ways a GL layer maps onto Vulkan are independent capabilities, so the
flag becomes a per-TextureTarget mask. A 2D or 2D multisample array layer IS a
VkImage array layer and needed nothing but the gate opened. A cube map array is
one 2D image with arrayLayers = 6 * cubeCount and CUBE_COMPATIBLE, which is a
shape VkTextureManager simply did not have - it is declined softly when the depth
is not a whole number of cubes or the level is not square, because that function's
Bool return exists for unrepresentable shapes and asserting there would abort on
ordinary input, GL_PROXY_TEXTURE_CUBE_MAP_ARRAY above all. A 3D texture's layer is
a z slice, which needs a 2D-array-compatible image and a per-slice clear, because
vkCmdClearColorImage cannot address a subset of a 3D image's slices - a render
pass whose only content is its LOAD_OP_CLEAR can, since its attachment is a 2D
view over that one slice.

VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is asked for per format and withdrawn per
format, mirroring the MUTABLE_FORMAT pattern already in this file: the capability
is per format+usage, so a single global probe answers a different question than
the one the frontend goes on to ask. Losing it costs per-slice attachment for that
format; failing creation would lose the texture.

Three things found on the way that are not the headline:

glFramebufferTextureLayer, the non-DSA twin, had no gate at all and additionally
refused cube map arrays that GL 4.5 requires it to accept. GL 4.6 core 9.2.8 makes
the two entry points equivalent, so they now decline in the same places - leaving
one ungated is what let an unrepresentable attachment reach the renderer.

ComputeFullMipLevelCount takes max(x, y, z), and for every array shape z is the
layer count rather than a mip-able axis, so a 4x4 array with 192 layers asked for
six mip levels on an image whose legal maximum is three
(VUID-VkImageCreateInfo-mipLevels-00958). Only the image's own extent can bound
it. lavapipe had been letting that through.

A layered GL clear queues layerCount = depth, which is illegal for a
VK_IMAGE_TYPE_3D image (VUID-vkCmdClearColorImage-baseArrayLayer-01472 pins it to
0/1, read as the whole mip level) and the old code passed it straight through.

Takes framebuffers_texture_layer_attachment green on DirectVulkan, so the whole
direct_state_access suite is 371/371 there; Espryt stays 370/371, the remaining
case being the fp64 one it declines by design.

Known and deliberately not fixed here, with a FIXME at the site:
KHR-GL44/45/46.geometry_shader.layered_framebuffer.clear_call_support now fails on
DirectVulkan - a layered clear of a 3D texture reads back zeros. Those cases exist
only in the GL44+ lists, above the 4.0 this backend reports. An A/B of a 6935-case
subset (cube map array, texture storage, framebuffer, 3D, the full DSA suite and
the GL33 texture group) is otherwise clean on both backends: 16 cases fixed and
none broken on Espryt, 15 fixed and those 2 broken on Magma, and zero difference
anywhere at GL 4.0 or below. The FIXME records which causes were already ruled out
by bisection so the next reader does not repeat them.
2026-08-05 12:07:04 -04:00
BZLZHH c8c7b19579 [Feat] (MG_Backend, MG_Util): give DirectVulkan GL's provoking vertex
Vulkan's built-in convention is "provoking vertex first"; GL's default is
LAST_VERTEX_CONVENTION, and GL derives both flat shading and the transform
feedback vertex order from it. DirectVulkan had no way to say so, which is why
direct_state_access.queries_functional failed on a value with nothing in its log
- the primitives came back counted against a strip recorded in the wrong vertex
order.

VK_EXT_provoking_vertex is now enabled when present, and the mode is a hashed
field of the pipeline payload rather than dynamic state, because it is baked into
VkPipelineRasterizationStateCreateInfo: two draws differing only in it must not
collide on one cached VkPipeline, or whichever mode built first would stick for
the rest of the frame. The pNext is chained only when the mode is not Vulkan's
default, so a device without the extension produces a byte-identical
VkGraphicsPipelineCreateInfo to before.

Two carve-outs, both measured rather than reasoned:

A geometry shader already emits its triangles in GL's vertex order, so asking for
LAST rotates them a second time and transform_feedback.geometry reads back the
wrong vertices. The mode is one pipeline bit and the input-assembler path wants
the opposite, so the two cannot both be satisfied: a program that runs a geometry
shader and captures transform feedback keeps Vulkan's own convention. That test
is read off the program's own shader list, not
programObj.rasterizationProducerStage - the latter is filled by the clip-fixup
analysis, which does not run for every program and reads Unknown for exactly the
programs this guard exists to catch. Both halves are link-time facts folded into
programObj.hash, so no pipeline memo can hand back one built for the other mode;
keying on IsTransformFeedbackActive() instead would be a live bug, since neither
memo key moves on glBeginTransformFeedback.

transformFeedbackPreservesProvokingVertex is deliberately not requested. It buys
nothing here - the capture order queries_functional needs comes from
provokingVertexLast alone - and leaving it off keeps
VUID-VkGraphicsPipelineCreateInfo-topology-04884 disarmed, so a TRIANGLE_FAN
pipeline may take LAST on any device.

The blit pipeline routes through the same selector: it has no flat varying and no
capture, but on a device without provokingVertexModePerPipeline a blit left on
FIRST inside a render pass whose draws are LAST is an illegal mix.

Per the POST rule the new extension gets rows for provokingVertexLast and for the
two properties that change what MobileGL can promise.

Fixes queries_functional on Magma (370/371). An A/B over a 976-case transform
feedback / geometry shader / layered rendering subset of GL30-GL45 is otherwise
identical on both backends and additionally takes 14 geometry_shader rendering
and layered_rendering cases from failing to passing on Magma.
2026-08-05 10:04:30 -04:00
BZLZHH 0e7692251d [Feat] (MG_State, MG_Impl, MG_Util): store a compressed texture image and hand it back
glCompressedTexImage2D rejected every internalformat with GL_INVALID_ENUM, so
direct_state_access.textures_get_image threw at its first compressed call and
reported InternalError with nothing in the log at all - the uncompressed half of
the case had already passed.

The compressed bytes are now kept verbatim, in a side-channel beside the texel
shadow rather than in place of it. That placement is the load-bearing decision:
both backends pair MapMipmapData with GetMipmapByteSize while sizing their copy
regions from GetMipmapTexelSize, and DirectGLES additionally divides the byte
size by the texel count to recover bytes-per-texel, so putting 16 bytes where a
4x4 RGBA8 extent says 64 would be an out-of-bounds read on both. The texel
storage therefore stays uncompressed and correctly sized - the image samples as
zeros, which is the same deviation the RGTC/BPTC/ETC2 arms of
ConvertGLEnumToTextureInternalFormat already document - while
glGetCompressedTexImage returns the image *as stored*, which GL 4.6 core 8.11
requires and which no re-encode could satisfy byte for byte. Nothing ever hands
the compressed bytes to GLES or Vulkan, so the shadow is authoritative rather
than potentially stale, which is why the readback never asks a backend.

The accepted set is exactly the RGTC/BPTC/ETC2-EAC formats core GL requires, and
it is deliberately the same set ConvertGLEnumToTextureInternalFormat can back
with uncompressed storage, so the upload can never accept a format whose texel
shadow it cannot allocate. imageSize is checked against the block arithmetic,
which is also what keeps the copy in bounds.

Three things the shape depends on. AllocateStorage clears the compressed tag, so
a glTexImage2D or glTexStorage2D over the level un-compresses it - without that,
textures_compressed_subimage would flip branches and start asking for data
MobileGL cannot produce. GL_TEXTURE_COMPRESSED and
GL_TEXTURE_COMPRESSED_IMAGE_SIZE are answered per level rather than per texture,
because a compressed internalformat handed to glTexImage2D resolves to
uncompressed storage and must keep reading as uncompressed. And
GL_TEXTURE_INTERNAL_FORMAT now reports the compressed token for such a level, or
it would claim GL_RGBA8 while GL_TEXTURE_COMPRESSED said true.

Still rejected on purpose: glCompressedTexImage1D/3D and every
glCompressedTexSubImage*, which caps the blast radius.

Fixes textures_get_image on both backends (Espryt 370/371, Magma 369/371). A/B
over a 1210-case compressed/texture-storage/texture-view/buffer-storage subset of
KHR-GL45 is identical before and after on both backends but for
get_texture_sub_image.errors_test, which stops throwing and fails on a value
instead.
2026-08-05 09:40:29 -04:00
BZLZHH 34f09291da [Feat] (MG_State, MG_Backend, MG_Util): feed a 64-bit vertex attribute on DirectVulkan
glVertexAttribLFormat validated its arguments and then refused unconditionally
with "64-bit vertex attributes are not supported", so
direct_state_access.vertex_arrays_attribute_format failed every GL_DOUBLE
subcase on both backends - the format never landed, the draw fetched whatever
the attribute held before, and the captured values came back as reinterpreted
garbage.

The attribute is now real state. IsLong is its own bit rather than being
inferred from Float64, because glVertexAttribFormat(GL_DOUBLE) also reads
doubles - it just asks for them converted to float - so the type alone cannot
tell the two apart. It participates in the format comparison, so an L-format
call over a plain one still bumps the version, and glVertexAttribPointer clears
it inside the mutation block so the clear and the bump stay atomic.
GL_VERTEX_ATTRIB_ARRAY_LONG stops being hardcoded false, and the pname is now
accepted by the attribute queries at all.

Support is detected, never assumed. SupportsFloat64VertexAttributes comes from
VkPhysicalDeviceFeatures::shaderFloat64 on DirectVulkan and is false on
DirectGLES - not a driver question there and never will be, since ES has no
GL_DOUBLE vertex format and ESSL has no fp64 type to consume one with. A backend
without it declines in the entry point, with the GL error and a log line naming
the reason, rather than accepting state no draw could honour. Both cases get a
DriverPost row so the loss is named at startup instead of at draw setup.

On DirectVulkan the attribute deliberately does not use VK_FORMAT_R64*_SFLOAT:
those are optional and lavapipe advertises zero features for all four of them.
It is fetched as its 32-bit word pair (R32G32_UINT / R32G32B32A32_UINT) and
bitcast back to double in the shader by a new SPIR-V pass, which is bit-exact
and needs no format capability at all. The pass re-declares the input as uvec2 /
uvec4, demotes the original variable to a Private global and seeds it once at
the top of the entry point, so every existing load keeps its id and its double
type and no other instruction is rewritten. Both halves branch on nothing but
"is this attribute long", so they cannot disagree - and if the pass ever fails,
the assertion fires rather than letting a UINT format sit under a double input.
The pointer types are all created before any variable that names them and the
demoted variable is moved after them, since the types-and-variables section may
not forward-reference a type.

dvec3/dvec4 are declined rather than fetched wrong: six or eight uint32
components have no single VkFormat, and GL spreads such an input over two
attribute locations, which the location-per-index model here does not express.

Fixes vertex_arrays_attribute_format on Magma (369/371). On Espryt it stays
failing, now as a detected and explained decline rather than a blanket refusal.
2026-08-05 08:49:23 -04:00
BZLZHH 3b65e646e1 [Fix] (MG_Backend): give every colour attachment its own backend slot on DirectGLES
ES only accepts glDrawBuffers bufs[s] == GL_COLOR_ATTACHMENTs, so a desktop
glDrawBuffer(GL_COLOR_ATTACHMENT3) cannot be expressed directly and DirectGLES
compacts: it physically relocates the draw buffer's image onto backend point 0 so
ES's output-0-to-attachment-0 rule lands on the right image. The clears were
therefore always correct. The read side was not.

GetBackendAttachmentType derived the attachment-to-point map by searching the
draw-buffer array and falling back to the identity point for anything it did not
find. That derivation is not injective against the compaction: after clearing
attachments 0..7 one at a time, every one of them has been relocated onto point 0
in turn, so a later glReadBuffer(GL_COLOR_ATTACHMENT0) - not a draw buffer any
more - takes the identity fallback to point 0 and reads attachment 7's image.
Hence the single mismatch, 0.875 where 0 was expected: 7/8 is attachment 7's clear
colour.

The map is now stored state rather than a re-derivation, and kept a permutation:
a draw buffer takes the point ES forces on it, everything else keeps its identity
point when that point survived, and an attachment evicted from its identity point
is parked on the lowest free one so it stays addressable for glReadBuffer and
blits. With identity draw buffers nothing moves and not one extra GL call is
issued, which is what keeps ordinary rendering untouched.

Two things the permutation depends on. The attachment loop now detaches a colour
point whose frontend owner is empty - SyncAttachmentObject only ever attaches, so
without this a point handed to an empty attachment would still hold the previous
owner's image and hand it back. And QueryReadColorAttachmentInternalFormat asked
GL_COLOR_ATTACHMENT0 for the format it sizes the multisample-resolve scratch
renderbuffer from; it now asks the point the read buffer actually names, since
that is only CA0 when the map happens to be identity.

Fixes framebuffers_read_draw_buffer on Espryt. A 5677-case readback and
framebuffer subset of GL30-33 stays at zero failures on both backends.
2026-08-05 08:24:01 -04:00
BZLZHH 25b9370815 [Fix] (MG_Backend): stop a renderbuffer blit reading a freed image layout
VkRenderPassManager kept m_renderbufferResources on FastSTL's open-addressing
UnorderedMap while BlitFramebuffer caches a raw pointer into one of its elements -
ResolveColorBlitBinding stores &rbResource->layout - and then calls
MaterializePendingClearForRenderbuffer, which looks that same resource up again.
FastSTL's operator[] runs its load-factor check before find_key and reallocates
the whole bucket array when occupancy crosses it, so even a plain lookup relocates
every element; erase only tombstones and never lowers the occupancy, so the
doubling keeps firing. After a relocation the cached pointer names freed storage
still holding the pre-clear VK_IMAGE_LAYOUT_UNDEFINED, BlitFramebuffer takes its
"source image layout is undefined" early return, and the blit is silently dropped
- glReadPixels then returns the zero-filled fresh allocation.

That is why the failures looked arbitrary: which iteration breaks is pure
arithmetic on the table's occupancy, and the observed set (GL_R8 at k=0,1,3,7,
GL_R16 at k=6, GL_RG16 at k=4) is exactly the doubling ladder. Padding the map
with unrelated live renderbuffers moves the failures to the positions the model
predicts and every previously failing format then passes, so nothing else hides
behind it.

Reordering the materialize ahead of the resolves - the fix ReadPixels got, see the
note at its call site - does not cover this, because BlitFramebuffer resolves two
bindings and the second resolve still runs after the first pointer is taken. The
depth blit, GetOrCreateRenderPass's depthRenderbufferResource and
ReadDepthStencilPixels cache the same kind of pointer, so the invariant belongs in
the container rather than in a per-call-site ordering rule. m_textureResources was
already node-based for exactly this reason; this is the map that was left behind.

Fixes renderbuffers_storage_multisample on DirectVulkan.
2026-08-05 08:24:01 -04:00
BZLZHH 4ce808b9f2 [Feat] (MG_State, MG_Impl, MG_Backend): let a bound program pipeline actually draw
The pipeline object bookkeeping landed already - names, stage slots, queries -
but nothing consumed it. Every draw asked the context for the current program,
got null because a pipeline is used with program zero, and drew nothing;
glCreateShaderProgramv was still a stub returning zero, so
direct_state_access.program_pipelines_functional could not even build its stage
programs and reported InternalError on both backends.

glCreateShaderProgramv is written as the exact call sequence the spec defines it
to be, with one deviation that matters: the link goes straight to
ProgramObject::Link(false) rather than through LinkProgram, because LinkProgram
injects a default fragment shader into a program that has none - correct for a
whole program, wrong for a separable vertex-stage one whose fragment stage comes
from the pipeline. glDetachShader defers removal to the next link, so the program
keeps the shader object it was built from while correctly no longer reporting it
attached. GL_PROGRAM_SEPARABLE joins glProgramParameteri and glGetProgramiv.

Everything downstream of a draw - both backends, the uniform plumbing, the draw
validation - is written against one linked program, so rather than teach all of
it about stages, the pipeline is flattened: GetProgramForDraw() composites the
stage programs' shaders into a single hidden program object and caches it against
a signature of each stage program's lifetime id and link generation, so it is
rebuilt exactly when a stage or a stage's link changes. The composite carries no
GL name - it must not answer glIsProgram, and it must not consume a name the
application could be handed.

Uniform entry points get their own resolver rather than sharing that one:
glUniform* addresses the pipeline's active program, not the composited draw
program. GL_CURRENT_PROGRAM still reads the program in use, which is zero here.

Fixes program_pipelines_functional on both backends.
2026-08-05 07:20:42 -04:00
BZLZHH 5545d31c37 [Feat] (MG_Backend, MG_Util): give a cube map array real storage on DirectGLES
TextureCubeMapArray was missing from every storage and upload switch in the
DirectGLES texture sync, so a cube map array reached the driver with no storage
at all - and from the glFramebufferTextureLayer branch, so attaching one of its
layers fell through to glFramebufferTexture2D and raised INVALID_ENUM. Every
GL_TEXTURE_CUBE_MAP_ARRAY colour check in
direct_state_access.framebuffers_texture_layer_attachment read nothing.

ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly like a 2D
array whose depth is six times the cube count, so each switch gains the case
beside Texture2DArray and nothing else changes. 1D arrays join the layer branch
for the same reason - their backend image is a 2D array.

Per the POST rule the new GLES dependency gets a capability
(SupportsTextureCubeMapArray, ES 3.2 core or EXT/OES_texture_cube_map_array) and
a DriverPost row saying what a user loses without it.

Takes framebuffers_texture_layer_attachment from failing to passing on Espryt. It
still fails on DirectVulkan, which declines a layered attachment outright.
2026-08-05 06:58:00 -04:00
BZLZHH 588ddba722 [Fix] (MG_Backend): scale a depth blit, keep going after one declines, and mip a 1D texture
Three DirectVulkan gaps found together.

glBlitFramebuffer's depth/stencil path refused any blit whose source and
destination extents differ, because vkCmdCopyImage cannot resize. vkCmdBlitImage
can, and VK_FILTER_NEAREST is the only filter Vulkan allows for depth/stencil
anyway - which is what the GL front end already requires. A same-size pair keeps
the cheaper copy.

Worse, that refusal and four others were `return`, not `continue`, so a
depth/stencil aspect this backend could not handle abandoned the whole function -
including the colour blit that only starts after the aspect loop. The CTS's
scaling blits therefore lost their colour as well, which is why
direct_state_access.framebuffers_blit failed all three of its checks rather than
one.

VulkanRenderer::GenerateMipmap declined GL_TEXTURE_1D. It needed nothing else:
the blit loop derives every offset from the storage extent, and a 1D texture's is
{width, 1, 1}, which is exactly the y and z offsets a 1D image requires.

Also: IsTimerQueryResultReady now asks the query pool before the frame serial.
The pool polls with VK_QUERY_RESULT_WITH_AVAILABILITY_BIT and is the authority;
the frame serial only advances at Present and neither completion notifier will
mark the current serial done, so a timestamp written and fence-waited inside one
GL frame could never be read back within it.

Takes framebuffers_blit and textures_generate_mipmaps from failing to passing on
DirectVulkan. queries_functional still fails there on a value.
2026-08-05 06:50:13 -04:00
BZLZHH 62301b1061 [Fix] (MG_State): let a double-typed varying be captured by transform feedback
ResolveXfbSymbolType accepted only float, int and uint, and its caller reports
anything it rejects as "Transform feedback varying 'x' is not an output of the
vertex stage" - which is a misleading thing to say about a varying that is right
there in the shader, just declared `double`. Program linkage failed outright.

Doubles are now resolved to the GL_DOUBLE* types, in vector and matrix form, and
the per-element size is computed from an 8-byte component rather than a hardcoded
4 (GL 4.6 core 11.1.2.1), so the byte-based limit checks charge a double what GL
says it costs.

direct_state_access.vertex_arrays_attribute_format stops throwing on both
backends and fails on the captured values instead: the capture layout still owes
the 8-byte alignment doubles require, and neither backend feeds a 64-bit vertex
attribute yet - DirectGLES cannot at all, ESSL having no double.
2026-08-05 06:43:41 -04:00
BZLZHH f3a846d336 [Docs] (README): carry the 4.2 short-term target into the status note
The compatibility section already said 4.2; the status note at the top of the
README still said 3.3, so the two disagreed depending on how far a reader got.
2026-08-05 06:10:37 -04:00
BZLZHH 9cdc82fbdd [Fix] (MG_Backend): actually bind the sampler object DirectGLES just synced
BindCurrentTextures' program-driven path synced a bound sampler object's
parameters to its backend object and then never put it on the texture unit, so
every sampler object was inert and the driver kept sampling with the texture's
own parameters - direct_state_access.samplers_functional read black where the
sampler's NEAREST filtering should have given red.

The bind alone is a regression, and the CTS says so loudly: a sampler left on a
unit by an earlier draw keeps being applied, and a multisample texture takes no
sampler object at all, so the next draw against one is rejected and all 27
textures_storage_multisample_3d_* cases fail. The sibling path in the same
function had an empty else branch where the unbind belonged; it now unbinds,
making the two symmetric.

Takes samplers_functional from failing to passing on Espryt, with no other case
moving in either direction.
2026-08-05 06:08:59 -04:00
BZLZHH 4a9d20c49f [Fix] (MG_Backend): resolve a framebuffer attachment's layer in the Vulkan blit bindings
ResolveAttachmentBaseArrayLayer answered zero for everything but a cube map face,
so every blit, copy and glReadPixels against a layered attachment read layer zero
whatever was attached. It reads the attachment's layer now.

A 3D texture needs the other half of the distinction: its image has arrayLayers
== 1 and the GL layer is a z slice, which VkBufferImageCopy will not take as a
base array layer. BlitImageBinding carries it separately as depthOffset, and the
readback copy region uses it as the image offset's z.

Takes textures_copy from failing to passing on DirectVulkan, which is what
glCopyTextureSubImage3D needs to see the slice the CTS attached rather than
slice zero.
2026-08-05 05:56:09 -04:00
BZLZHH 394d1ce748 [Feat] (MG_Impl, MG_Util): copy into 1D and 3D textures, and accept the BPTC and ETC2 enums
Two unrelated texture gaps.

glCopyTextureSubImage1D and 3D validated their arguments and then did nothing:
CopyTexSubImage1D_State and CopyTexSubImage3D_State were empty TODOs and no
backend exposes anything but a 2D blit. But a texture's contents live in its CPU
storage - the backends sync from it - so the copy does not need a blit at all.
CopyReadFramebufferIntoMipmapRegion reads the region out of the read framebuffer
through the existing ReadPixels path, in the destination's own canonical client
layout so the bytes need no second conversion, and writes them straight into the
level. GL 4.6 core 8.6 says the copy ignores pixel-store state and any bound pack
buffer, which the borrowed readback does not, so both are neutralised for the
duration and restored after. A cube map destination addresses its faces as
separate upload targets, so its zoffset picks the target rather than a slice.

ConvertGLEnumToTextureInternalFormat had arms for the six generic compressed
formats and the four RGTC ones, all resolving to uncompressed storage, but none
for BPTC or ETC2/EAC - so glTexImage2D with one of those fourteen enums answered
INVALID_ENUM, which was never a legal reply for formats core GL has required
since 4.2 and 4.3. They follow the same deviation for the same reason: nothing in
this stack can compress them, and uncompressed storage is the trade the RGTC
formats already take.

Takes textures_compressed_subimage from failing to passing on both backends and
textures_copy on Espryt. textures_copy still fails on Magma, where the readback
of a layered attachment does not yet resolve the attached layer.
2026-08-05 05:50:32 -04:00
BZLZHH 300b458132 [Feat] (MG_State, MG_Impl): give program pipelines their object and their state
Every program pipeline entry point was an export stub, and the stub macro's
`return (type)1` made glIsProgramPipeline answer GL_TRUE for anything - including
the names glGenProgramPipelines had never written. All four
direct_state_access.program_pipelines cases failed.

ProgramPipelineObject holds what GL 4.6 core 7.4 says a pipeline is: a program
reference per shader stage, the active program glProgramUniform* addresses, a
validate status and an info log. Its validate status starts false, unlike
ProgramObject's, because a pipeline that has never been validated must report
GL_VALIDATE_STATUS as 0.

The name rules follow the shape queries and transform feedbacks already use, and
which the CTS checks first: glGenProgramPipelines only RESERVES a name and
glIsProgramPipeline answers GL_FALSE for it; the object appears on first bind, or
immediately from glCreateProgramPipelines. Map membership is object existence -
a pipeline, unlike a transform feedback, has no stateful default object zero, so
no everBound flag is needed.

glGet(GL_PROGRAM_PIPELINE_BINDING) reports the real binding now instead of a
hardcoded zero whose comment said the entry points were stubbed.

This is the state half only. program_pipelines_functional needs mixed-stage
rendering - a vertex-only and a fragment-only program drawn together - and stays
failing; glCreateShaderProgramv is deliberately left stubbed until that lands, so
nothing can half-work in between.

Takes program_pipelines_creation, _defaults and _errors from failing to passing
on both backends.
2026-08-05 05:43:13 -04:00
BZLZHH 1f1a331a44 [Feat] (MG_Impl, MG_State): implement the framebuffer parameter getters and setters
glFramebufferParameteri, glGetFramebufferParameteriv and their two by-name
siblings were all export stubs - the GL_ARB_framebuffer_no_attachments entry
points. The stub raises no error and writes nothing, so
direct_state_access.framebuffers_get_parameter_errors saw GL_NO_ERROR for all
three conditions it checks.

FramebufferObject gains the five DEFAULT_* parameters as real state, initialised
to GL 4.6 core table 23.24 and bumping the object version on a write like the
read buffer does. The getter answers those plus the six derived names -
GL_SAMPLES and GL_SAMPLE_BUFFERS from the attachments' sample counts,
GL_IMPLEMENTATION_COLOR_READ_FORMAT/_TYPE from the read buffer's internal format,
GL_DOUBLEBUFFER true only for the window-system framebuffer, GL_STEREO false
because stereo surfaces are not exposed - which is what glGetIntegerv already
reports for the bound framebuffer.

The pname rules live in ValidateFramebufferParameterPname, and their ORDER is
load-bearing: a name outside the table is INVALID_ENUM, and only a name that IS
in the table but that the default framebuffer cannot answer is INVALID_OPERATION.
Testing the framebuffer kind first would answer INVALID_ENUM for
GL_FRAMEBUFFER_DEFAULT_WIDTH on framebuffer zero, which is exactly the third
thing the case checks. The by-name forms take zero as the default framebuffer,
like the other DSA framebuffer entry points.

Rendering to a framebuffer with no attachments is deliberately NOT enabled by
this: CheckCompleteness still reports INCOMPLETE_MISSING_ATTACHMENT, because no
backend can rasterize one. The state is real and the queries are honest; the
draw path is a separate piece of work.

Takes framebuffers_get_parameter_errors from failing to passing on both backends,
with framebuffers_get_parameters - which passed only because both getters were
stubs leaving the CTS's zero-initialised comparands untouched - still passing.
2026-08-05 05:33:31 -04:00
BZLZHH 817091641c [Fix] (MG_Impl): give a cube map the storage and the layered attachment it asks for
direct_state_access.framebuffers_texture_attachment threw on both backends, and
three separate things were wrong on the way to a cube map framebuffer.

glTexStorage1D/2D/3D validated their target by converting it to a single
TextureUploadTarget. GL_TEXTURE_CUBE_MAP has no single upload target - it
allocates all six faces - so the conversion produced Unknown and a legal
glTexStorage2D(GL_TEXTURE_CUBE_MAP, ...) was rejected with INVALID_ENUM, which is
where the case threw. The accepted set for these entry points is the dimension's
storage targets, which IsTextureStorageTargetForDimension already spells out, so
that is what they check now.

TextureStorage2D then allocated only the primary upload target, leaving a cube
map with one face out of six - cube-incomplete, so every framebuffer it was
attached to answered GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. It allocates every
upload target the object has; for every other 2D target that is the same single
target as before.

ResolveRepresentableFramebufferTextureUploadTarget declined every layered target
but 2D array, so glNamedFramebufferTexture on a cube map reported "not
represented by the current framebuffer attachment model". Cube maps, cube map
arrays, 1D arrays, 2D multisample arrays and 3D textures are all the same shape
as the 2D array that already worked - glFramebufferTexture binds the whole
texture and the attachment records a representative upload target - so they are
all handled now. DirectGLES routes a layered attachment to glFramebufferTexture,
which is exactly this.

Takes framebuffers_texture_attachment from failing to passing on both backends.
2026-08-05 05:24:21 -04:00
BZLZHH e64c7c7e65 [Fix] (MG_Backend): never back a multisample texture with a one-sample Vulkan image
Every one of the sixty direct_state_access.textures_storage_multisample_2d_* and
_3d_* cases failed on DirectVulkan, for every internal format, with no GL error
anywhere - a pure data mismatch.

The CTS asks for glTextureStorage2DMultisample(tex, samples = 1, ...), which is
legal GL, and MobileGL carried the 1 faithfully through to
VkImageCreateInfo::samples = VK_SAMPLE_COUNT_1_BIT. It then binds that image to
the auxiliary program's sampler2DMS, whose SPIR-V is OpTypeImage with MS = 1.
VUID-RuntimeSpirv-samples-08726 forbids exactly that pairing: an MS access must
come from an image created with more than one sample. The texelFetch therefore
read undefined data - which is why it looked format-independent and raised
nothing.

GL only promises "at least the requested number of samples", so a multisample
texture is now floored at two. GL_TEXTURE_SAMPLES still reports what the
application asked for; that is read off the texture object, not off the image.
The device-capability round below it is bounded at two for the same reason -
letting it land back on one sample would recreate the violation silently for any
format whose only supported count is one.

Takes all 60 textures_storage_multisample_* cases from failing to passing on
DirectVulkan, which goes from 296/371 to 356/371. DirectGLES is untouched.
2026-08-05 04:46:15 -04:00
BZLZHH dd60ff39ce [Feat] (MG_State, MG_Impl, MG_Backend, MG_Util): make the border colour real sampler state
glGetSamplerParameterfv(sampler, GL_TEXTURE_BORDER_COLOR) raised INVALID_ENUM,
because MobileGL kept the border colour on the texture object and
GetSamplerParam_State had no case for it at all. That is the first thing
direct_state_access.samplers_defaults asks, so the case threw before reaching
any of the defaults it was written to check.

GL 4.6 core table 23.18 lists TEXTURE_BORDER_COLOR as sampler state, so it moves
to SamplerParameters and TextureObjectBase reaches it through the SamplerObject
it already owns - one source of truth, and a sampler object bound over a texture
now supplies its own border colour, which is what GL says should happen. The
texture params version still moves on a write, because the DirectGLES texture
sync memoises on it. glSamplerParameter{fv,Iiv,Iuiv} and their getters read and
write all four components in whichever representation the caller used, and the
three representations are kept in step so any getter has an answer. The bogus
[0,1] and [0,255] range checks are gone: GL clamps a border colour when a
fixed-point format is sampled, it does not reject it.

DirectVulkan's ResolveVkBorderColor now reads the sampler rather than the
texture. DirectGLES gained a glSamplerParameterfv in its sampler sync, and both
that and the pre-existing glTexParameterfv are gated on a new
SupportsTextureBorderClamp capability - ES 3.2 core, or EXT/OES_texture_border_clamp
before it - since without the extension every such call is INVALID_ENUM on the
driver. DriverPost gains the matching row per the POST rule, saying what a user
actually loses when it is missing.

Takes direct_state_access.samplers_defaults from failing to passing on both
backends.
2026-08-05 04:45:55 -04:00
BZLZHH 96ad7ca0cc [Fix] (MG_Impl): asking a renderbuffer for more samples than it has is INVALID_OPERATION
ValidateRenderbufferStorageSamples_State answered INVALID_VALUE for a sample
count above GL_MAX_SAMPLES. GL 4.6 core 9.2.4 reserves INVALID_VALUE for a
negative count: a count that is well formed but larger than the format can
deliver is INVALID_OPERATION, because the argument is fine and the format is
what cannot honour it.

Takes direct_state_access.renderbuffers_storage_multisample_errors from failing
to passing on both backends.
2026-08-05 04:13:59 -04:00
BZLZHH e80a23eae6 [Fix] (MG_Backend): read a multi-slice glGetTexImage off the GPU instead of the CPU shadow
DirectGLES served every multi-slice glGetTexImage from the CPU shadow copy, on
the grounds that its scratch FBO can only expose one layer at a time. But the
shadow only holds what was uploaded, so any slice that was rendered to rather
than written by glTexSubImage came back stale - and a layered framebuffer
produces exactly that.

The scratch FBO can expose one layer at a time repeatedly. The read now attaches
each layer in turn and takes the slice off the GPU, walking the destination over
GL_PACK_SKIP_IMAGES / GL_PACK_IMAGE_HEIGHT itself so each per-slice call packs a
plain 2D image with the same layout StoreWideRowsToClient computes for the whole
stack. The shadow stays as the fallback for the formats a colour attachment
cannot represent at all, and for any slice whose attachment comes back
incomplete.

Takes all 27 remaining direct_state_access.textures_storage_multisample_3d_*
cases from failing to passing on Espryt - they render into a
TEXTURE_2D_MULTISAMPLE_ARRAY one layer per colour attachment and then read the
whole array back. DirectVulkan is untouched.
2026-08-05 03:55:30 -04:00
BZLZHH 088f263495 [Feat] (MG_Impl): answer the two query parameters the getters were missing
GetQueryObjectValue implemented GL_QUERY_RESULT_AVAILABLE and GL_QUERY_RESULT and
rejected everything else, so direct_state_access.queries_functional threw on its
very first probe - GL_QUERY_TARGET - and never reached any of the checks it was
written for.

GL_QUERY_TARGET is state the object has carried all along; it just had no case.
GL_QUERY_RESULT_NO_WAIT is GL_QUERY_RESULT with the backend asked not to block,
and it brings a wrinkle the shared getter could not express: when the result has
not landed, GL_ARB_query_buffer_object leaves the destination untouched rather
than writing a placeholder. GetQueryObjectValue now reports "succeeded but
produced no value" through an optional out-parameter, and all five callers - the
four buffer forms and the four client-memory forms - skip the write on it.

The switch is deliberately widened by exactly these two names: its default
INVALID_ENUM is what the GL33 and GL40 query error cases rely on.

queries_functional passes on Espryt. On Magma it stops throwing and fails on a
value instead, which is a separate problem in the query results themselves.
2026-08-05 03:48:14 -04:00
BZLZHH 3b3b6e5b8b [Fix] (MG_Backend): read back the stencil half, and clear an sRGB target to the value asked for
Two reasons a framebuffer's contents came back wrong, both on the read/clear
side rather than the write side.

Stencil, on both backends. The CTS reads stencil with glReadPixels(GL_STENCIL_INDEX,
GL_INT), which is as legal as the unsigned widths, and neither backend accepted
it: DirectGLES's ReadPixelsStencilViaNative rejected every signed type, after
which the call fell through to a native ES read the driver refuses and nothing
was written at all, so the caller kept its zeros; DirectVulkan's pack switch had
no GL_INT case, and of the cases it did have only GL_UNSIGNED_INT sourced the
stencil plane - GL_FLOAT and GL_UNSIGNED_SHORT emitted a depth value, which is
meaningless for a stencil-only image. Both now take the signed and float widths,
and DirectVulkan decides "this is a stencil read" once rather than per type.
DirectGLES also gains the GL_FLOAT_32_UNSIGNED_INT_24_8_REV fallback a
DEPTH32F_STENCIL8 attachment needs, which rejects the 24_8 packed type.

sRGB, on DirectVulkan. Every other write path goes through the UNORM twin view
while GL_FRAMEBUFFER_SRGB is off, storing the raw value GL asked for, but a
deferred clear is materialised with vkCmdClearColorImage - which names the image,
so the driver applied the sRGB transfer function and a clear to 0.25 landed at
0.537. PreCompensateSrgbClearColor hands it the linear colour whose encoding is
the requested value instead. It is a no-op for non-sRGB destinations, for integer
clear encodings, and when GL_FRAMEBUFFER_SRGB is on and GL really does want the
encode.

Takes renderbuffers_storage from failing to passing on both backends, plus
renderbuffers_storage_multisample and framebuffers_blit on Espryt.
2026-08-05 03:41:23 -04:00
BZLZHH 9eda2147b1 [Fix] (MG_Impl, MG_Backend): let the backend that can honour a layered attachment have it
NamedFramebufferTextureLayer declined every attachment but layer zero, on both
backends. That was right for DirectVulkan, which maps a GL layer onto a Vulkan
array layer with no notion of a 3D depth slice, but wrong for DirectGLES:
SyncAttachmentObject already routes a layered upload target to
glFramebufferTextureLayer with the attachment's layer passed straight through,
and array storage already carries the real layer count into glTexStorage3D. The
one backend that could render to the layer was being told it could not.

The decision now lives in a DynamicBackendParameters flag, so it is the backend
that answers rather than the entry point guessing. DirectGLES sets it when the
driver resolved glFramebufferTextureLayer; DirectVulkan leaves it false until
VkRenderPassManager tells a depth slice from an array layer.

framebuffers_texture_layer_attachment's colour checks now pass on Espryt for 3D,
2D array and 2D multisample array textures - the case still fails there on cube
map arrays, which DirectGLES gives no storage at all, and on the depth and
stencil halves. No case changes on DirectVulkan, which keeps the old behaviour.
2026-08-05 03:40:59 -04:00
BZLZHH a63699cde6 [Fix] (MG_Impl, MG_Backend): reject incomplete cube maps in mipmap generation instead of crashing on them
Both direct_state_access.textures_generate_mipmap* cases crashed DirectVulkan.
Two causes, neither of them a broken invariant:

glGenerateMipmap and glGenerateTextureMipmap never checked cube completeness, so
an incomplete cube map went straight to the backend, which asserts that the
texture it is handed is complete. GL 4.6 core 8.14.4 makes that call
INVALID_OPERATION - there is no consistent set of faces to filter down - and both
entry points now say so through a shared check.

VulkanRenderer::GenerateMipmap asserted that the target was one of the four it
implements. 1D, 1D array and cube map array are legal GL and the front end passes
them through, so meeting one is a gap in this backend's coverage; it now logs and
declines, leaving the generated levels unwritten rather than aborting.

textures_generate_mipmap_errors passes on both backends now. textures_generate_mipmaps
stops crashing but still fails: DirectVulkan does not generate the 1D mip chain
the case checks - the frontend's storage allocation gives the levels the right
sizes, which is why the case passes when run on its own, but not the descending
content the full-run state leaves it looking for.
2026-08-05 02:55:54 -04:00
BZLZHH 765aaec6dc [Fix] (MG_Impl, MG_Backend): stop the new layer attachment from reaching backends that cannot back it
Implementing NamedFramebufferTextureLayer made layered attachments reachable for
the first time, and direct_state_access.framebuffers_texture_layer_attachment
went from Fail to Crash on DirectVulkan. Two separate gaps sat behind it, both
of them asserted on rather than reported:

- The renderer resolves an attachment's GL layer straight onto a Vulkan array
  layer. A 3D texture's z-slice therefore lands outside its image, which has one
  array layer by construction, and the array texture objects are still the
  one-image stubs in TextureObjectStubs.h, so their image has a single layer
  whatever GL believes. MaterializePendingClearForTexture tripped over a clear
  whose layer span was outside the image it was given.
- A cube map array has no image shape in VkTextureManager at all, so
  SyncTextureAndGetDescriptor returns null for it.

NamedFramebufferTextureLayer now answers the full error set for every target and
layer - which is what took the two error cases green - and then declines to
attach anything but layer zero of a non-cube-array texture, through the same
RecordUnsupportedFramebufferTextureAttachmentError the by-target entry point
already uses. Layer zero of the other targets is the plain first-slice
attachment glFramebufferTextureLayer already backs, so it still goes through.

SyncTextureResource's assertion on an unsupported texture shape is also gone: it
is a gap in this backend's coverage, not a broken invariant, and the code below
it already handles the failure by declining the sync. It logs a warning instead.

framebuffers_texture_layer_attachment goes back to Fail on DirectVulkan rather
than Crash; no case changes in either direction beyond that.
2026-08-05 02:47:27 -04:00
BZLZHH bcd669bd25 [Feat] (MG_Impl): complete the by-name framebuffer attachment and buffer-selection entry points
Four direct_state_access framebuffer cases failed on one shared cause and three
local ones.

The shared cause: every DSA framebuffer entry point resolved its name through
GetNamedFramebufferObject_State, which rejects zero outright. But zero names the
default framebuffer to these functions, so glGetNamedFramebufferAttachmentParameteriv,
glNamedFramebufferDrawBuffer(s) and glNamedFramebufferReadBuffer answered
INVALID_VALUE for every default-framebuffer query the CTS makes. They now resolve
zero to the default framebuffer object and tell the two kinds apart explicitly,
which is what the accepted-name rules key off anyway.

Attachment queries: the accepted attachment names differ between the default
framebuffer (FRONT/BACK variants, DEPTH, STENCIL) and a framebuffer object
(COLOR_ATTACHMENTi, DEPTH/STENCIL/DEPTH_STENCIL_ATTACHMENT), and a name outside
the relevant list is INVALID_ENUM. Both getters share ResolveAttachmentQueryName
for that, so the by-target form no longer aliases GL_FRONT onto a framebuffer
object's colour attachment 0. The TEXTURE_* parameters are also rejected with
INVALID_ENUM when the attached object is a renderbuffer.

Buffer selection: naming a buffer that belongs to the other kind of framebuffer
is INVALID_OPERATION, not INVALID_ENUM - the enum is accepted, the framebuffer
just has no such buffer. glDrawBuffers additionally rejects the multi-buffer
names (FRONT, LEFT, RIGHT, FRONT_AND_BACK) with INVALID_ENUM on both kinds,
takes BACK only when n is one, and glReadBuffer treats the multi-buffer names as
accepted-but-unselectable. Both colour-attachment range checks now go through
ValidateColorAttachmentInRange instead of comparing against MAX_DRAW_BUFFERS with
an off-by-one.

NamedFramebufferTextureLayer was a stub that reported "not represented by the
current framebuffer attachment model" for every call, even though the attachment
model stores a layer and the by-target glFramebufferTextureLayer already uses it.
It is implemented against the same model, with the per-target layer limits and
the INVALID_OPERATION-for-a-bad-name rule that separates it from
NamedFramebufferTexture. NamedFramebufferTexture itself gained the two checks it
lacked: colour attachment range, and a negative level.

Takes framebuffers_get_attachment_parameters, framebuffers_get_attachment_parameter_errors,
framebuffers_texture_attachment_errors and framebuffers_draw_read_buffers_errors
from failing to passing on both backends.
2026-08-05 02:34:12 -04:00
Claude f3405d1d53 [Fix] (CI): narrow the trace fixture Git LFS fallback to the files mirrors lost
The fetch script tries git.hit.moe, then the repo.miawa.cn mirror, and only
then Git LFS, but it bailed out of the mirror loop on the first file no
mirror could serve and then pulled the whole case from GitHub. A case whose
mirrors served every file but one paid GitHub's LFS bandwidth for all of
them.

Collect the files that survived every mirror and every retry instead, and
scope the LFS fallback to just those, matching what the local macOS retrace
helper already does.
2026-08-05 05:59:25 +00:00
BZLZHH 81604d5596 [Feat] (MG_Impl, MG_Test): validate the direct-state-access texture copies
CopyTextureSubImage1D and 3D were do-nothing stubs and the 2D form checked only
its effective target, so all 28 conditions in
direct_state_access.textures_copy_errors went unreported: level and region
bounds, and every read-framebuffer precondition.

The read-framebuffer half lands in FramebufferImpl as ValidateReadFramebufferForCopy -
incomplete read framebuffer (INVALID_FRAMEBUFFER_OPERATION), a read buffer that
names no attachment, and a multisampled read buffer (both INVALID_OPERATION). It
decides multisampledness by attachment kind rather than by sample count alone,
because a TEXTURE_2D_MULTISAMPLE attachment sets SAMPLE_BUFFERS even when its
sample count is one - which is exactly what the CTS attaches, and what a
renderbuffer-only check would have missed.

The texture half is ValidateCopyTextureSubImage, shared by all three forms; 1D
and 3D also get the effective-target rule their form specifies.

NOTE: the copy itself is still not implemented for 1D and 3D - CopyTexSubImage1D_State
and CopyTexSubImage3D_State remain TODOs and no backend exposes anything but a
2D blit - so direct_state_access.textures_copy stays red. Only the errors are
complete, which is what un-stubbing these two entry points buys; both carry a
comment saying so.

CopyTextureSubImage2DUsesNamedObjectAndRestoresBinding had been passing a
storage-less texture and no read framebuffer, which the new validation correctly
rejects. It now sets up a legal copy, so it still measures the by-name plumbing
it was written for.

Takes direct_state_access.textures_copy_errors from failing to passing on both
backends.
2026-08-05 01:54:39 -04:00
BZLZHH 31ea6aa5a3 [Feat] (MG_Impl): give the by-name texture image queries their error set
glGetTextureImage resolved a texture by name and went straight to the read,
skipping every object-level rule glGetTexImage enforces through
GetTexImage_State - and on DirectVulkan it skipped the level checks in
CopyTextureImageToClientOrPBO_State as well, because that backend answers
GetTextureImage itself. Fifteen of the sixteen conditions in
direct_state_access.textures_image_query_errors went unreported.

The object-level half of that error set now lives in ValidateTextureImageQuery
and both entry points run it. Three rules are new rather than merely relocated:

- Multisample and buffer textures are not in the accepted target list; neither
  has a single image to return.
- The destination-size checks (bufSize, and the span written into a bound pixel
  pack buffer) move ahead of the read. They existed, but downstream of it, where
  any early bail-out - an unmapped level, a pack step that declines the format -
  swallowed them. Both measure the tightly packed span summed over the object's
  faces, which is the least a query can produce, so nothing that would have fit
  is rejected.
- IsDepthLikeInternalFormat had no case for StencilIndex8, so a colour client
  format read back against a stencil-only texture looked like a matching pair.

glGetCompressedTextureImage was a do-nothing stub. It validates the name and the
level, then reports INVALID_OPERATION: no format MobileGL can hold is
compressed, and answering GL_NO_ERROR without writing would hand the caller
stale memory - the same reasoning GetCompressedTexImage_State already follows.

Takes direct_state_access.textures_image_query_errors from failing to passing on
both backends.
2026-08-05 01:46:10 -04:00
BZLZHH 7d6f6603c1 [Feat] (MG_Impl): enforce the unpack-buffer rules on texture sub-image uploads
TexSubImage1D/2D/3D_State each carried a TODO for the three INVALID_OPERATION
conditions GL 4.6 core 8.5 attaches to sourcing an upload from a bound
PIXEL_UNPACK_BUFFER: the store being mapped, an offset that is not a multiple of
the size of one datum of `type`, and reads that would run past the end of the
store. None of them was checked, so every such call was quietly accepted.

ValidatePixelUnpackBufferSource now covers all three and returns true when no
unpack buffer is bound, so the callers can run it unconditionally. Persistent
mappings stay legal sources, matching what ReadPixels already does on the pack
side. The overrun check measures the tightly packed span, which is the smallest
the unpack can read - pixel store parameters only ever widen it - so it cannot
reject an upload that would have fit.

TextureSubImage2D needed the call of its own: unlike its 1D and 3D siblings it
does not route through TexSubImage2D_State.

Takes direct_state_access.textures_subimage_errors from failing to passing on
both backends.
2026-08-05 01:32:37 -04:00
BZLZHH 39c17c0b1b [Fix] (MG_Impl): validate the float texture parameter setter and the compressed size query
Two independent gaps in the texture parameter paths, both reported by
direct_state_access:

TexParameterf_State never ran ValidateTextureParameterForTarget. The integer
setter reaches it through TextureParameterObject_State and the scalar float
setter through TextureParameterObjectf_State, but glTexParameterfv and
glTextureParameterfv funnel every non-vector pname straight into
TexParameterf_State - so in float form MobileGL accepted sampler state on a
multisample texture, a mipmapping min filter or a REPEAT wrap on a rectangle
texture, and a negative TEXTURE_BASE_LEVEL/TEXTURE_MAX_LEVEL, all of which the
integer form rejected. It now validates first, passing the same
anisotropy-exempt param the by-object float setter uses so the anisotropy range
check is not run twice.

GL_TEXTURE_COMPRESSED_IMAGE_SIZE answered 0 for every texture. GL 4.6 core 8.11
makes the query INVALID_OPERATION on an image whose internal format is
uncompressed and on any proxy target. TextureInternalFormat has no compressed
enumerator, so that is every texture MobileGL can hold today; the condition is
still written against an IsCompressedTextureFormat predicate so both level
getters answer consistently once compressed formats land, and
GL_TEXTURE_COMPRESSED now reads from the same predicate instead of a hardcoded
false.

Takes textures_parameter_setup_errors and textures_level_parameter_errors from
failing to passing on both backends.
2026-08-05 01:24:29 -04:00
BZLZHH 88138b48ec [Test] (MG_Test): follow the backends to an advertised GL 4.0
Both AdvertisesVoxyRequiredRenderingExtensions cases pinned TargetGLVersion at
3.3, which was the reported version until V_OpenGL40 joined the advertised
extension lists. The version assertion is incidental to what these cases are
for - Voxy needs the individual ARB extensions, not a version - so it just
tracks the new report instead of holding the old one.
2026-08-05 01:24:05 -04:00
BZLZHH c114ce750b [Feat] (MG_Backend): advertise OpenGL 4.0 on both backends
Both backends stopped their advertised version list at V_OpenGL33, so an
application - or the CTS - asking what MobileGL supports was told 3.3 even
though the 4.0 entry points and the KHR-GL40 suite already pass on both.
Adding V_OpenGL40 lets that work be reached through the ordinary version query
instead of only through the individual ARB extension strings.
2026-08-05 13:19:41 +08:00
BZLZHH 1ca2d3c0fe [Docs] (README): move the short-term target to OpenGL 4.2
The 3.3 line is done - GL30 through GL33 conform on both backends - and the
work in flight (GL40, direct state access) is already past it, so the stated
short-term target now reads 4.2 and MG_State/MG_Impl are focused there.
Performance work joins the focus list alongside the two backends.
2026-08-05 01:15:26 -04:00
BZLZHH 58c17f85a5 [Fix] (MG_State, MG_Backend): start TEXTURE_COMPARE_FUNC at LEQUAL
SamplerParameters defaulted compareFunc to ALWAYS, but GL 4.6 core table 23.18
and GLES 3.2 table 21.16 both say the initial value is LEQUAL - for sampler
objects and for the sampler state a texture object carries alike. Every freshly
created texture and sampler therefore answered GL_ALWAYS to
glGetTextureParameteriv(GL_TEXTURE_COMPARE_FUNC).

The Vulkan backend had been papering over it: ResolveCompareFunc substituted
LESS_EQUAL whenever a depth texture was sampled in compare mode and the func
still read ALWAYS, which fixed the rendering but also made an explicitly
requested GL_ALWAYS unreachable. With the default corrected that special case is
both unnecessary and wrong, so it is gone and the compare op is taken straight
from the sampler.

Takes direct_state_access.textures_defaults from failing to passing on both
backends.
2026-08-05 01:13:04 -04:00
BZLZHH 4873da6844 [Fix] (MG_Impl): accept COLOR when invalidating the default framebuffer
The validation added with the invalidation entry points took the default framebuffer's
buffers to be only FRONT_LEFT, FRONT_RIGHT, BACK_LEFT, BACK_RIGHT, DEPTH and STENCIL, so a
call naming COLOR came back INVALID_ENUM. The by-name forms spell the colour buffer the way
glClearNamedFramebuffer does - COLOR, DEPTH, STENCIL - while the target forms use the
individual left/right tokens, and both spellings arrive at the same validation, so both sets
belong there (GL 4.6 core 17.4.4).

Caught by framebuffers_invalidate_data and framebuffers_invalidate_subdata, which had been
passing while the entry points were stubs doing nothing at all. Those two plus
invalidate_data_and_subdata_errors now pass together on both backends.
2026-08-05 00:51:12 -04:00
BZLZHH efeb24ff9b [Fix] (MG_Impl, MG_State): answer the texture parameters the getters were missing
glGetTexParameter and its by-name form rejected several parameters GL 4.6 core table 8.20
lists, with INVALID_ENUM as if the application had made them up. GL_DEPTH_STENCIL_TEXTURE_MODE
was the worst of them: the float setter accepted it, validated it and then threw the value
away, the integer setter did not accept it at all, and neither getter could report it - so
the mode could be set and never read back, and setting it through glTextureParameteri was an
error.

It is real state now, defaulting to DEPTH_COMPONENT, set by both setters and readable from
both getters. GL_TEXTURE_LOD_BIAS was in the same position: settable, not gettable.

The by-name getters reach the target-based ones through a temporary binding rather than the
per-object path, so both had to learn these; the per-object path gained the swizzle
components, the target, the image format compatibility type and the texture-view parameters
at the same time, since they were missing there for the same reason.

direct_state_access.textures_get_set_parameter passes on both backends, and textures_defaults
stops raising an internal error and reports an ordinary failure it can be diagnosed from.
2026-08-05 00:49:12 -04:00
BZLZHH 18c1a4d586 [Feat] (MG_Impl): implement the query getters that write into a buffer object
glGetQueryBufferObjectiv and its three siblings were stubs. They are the ordinary query
getters with the destination changed from client memory to a buffer object, so everything
about the query itself - the name, whether it is still active, the parameter - is already
answered by the shared GetQueryObjectValue, including the errors it raises.

What was left is the destination: a negative offset is INVALID_VALUE, a name that is not a
buffer object is INVALID_OPERATION, and so is a write that would run past the end of the
buffer. The four differ only in the width they store, so they share one template.

direct_state_access.queries_errors passes on both backends, putting the group at 4 of 5.
queries_functional now reaches further into the test and ends in an unrelated InternalError
rather than a plain failure.
2026-08-05 00:36:53 -04:00
BZLZHH 66ac3486e1 [Feat] (MG_Impl): validate the framebuffer invalidation entry points
glInvalidateFramebuffer, glInvalidateSubFramebuffer and their two by-name forms were all
stubs, so every call - including the malformed ones - returned quietly with no error.

These four only grant permission to throw the named attachments' contents away, and keeping
them satisfies "the contents become undefined", so the frontend validates the call and
leaves the contents alone. Actually discarding is a bandwidth optimisation that would need a
backend dependency; it can be added later without changing what any of these promise.

The validation is where the real content is. Which tokens name an attachment depends on
which framebuffer is affected: the default framebuffer has buffers (FRONT_LEFT and company)
and a framebuffer object has attachment points, so a token from the wrong set is
INVALID_ENUM. A COLOR_ATTACHMENTm past GL_MAX_COLOR_ATTACHMENTS is different in kind - a
well-formed enum naming a point that does not exist - and is INVALID_OPERATION, which the
existing colour-attachment range validator already expresses. Negative counts and negative
sub-region extents are INVALID_VALUE.

direct_state_access.invalidate_data_and_subdata_errors passes on both backends.
2026-08-05 00:31:46 -04:00
BZLZHH 764a44f589 [Fix] (MG_Impl): stop treating an empty buffer mapping access mask as a bad enum
glMapBufferRange and glMapNamedBufferRange rejected an access of zero with INVALID_ENUM.
Zero is a perfectly well-formed bitfield value - it contains no invalid flags - and what it
violates is the separate rule that a mapping has to ask for read or write access, which GL
reports as INVALID_OPERATION. Both callers already checked that rule immediately after, so
the validator was reporting the wrong error for a case its callers were about to handle
correctly.

direct_state_access.buffers_errors passes, which puts the whole buffers group at 4 of 4 on
both backends.
2026-08-05 00:13:51 -04:00
BZLZHH d96acb7972 [Fix] (MG_Impl): let a buffer clear name any format the spec allows
glClearBufferData and friends accepted exactly two argument triples - R8UI with
UNSIGNED_BYTE and R32UI with UNSIGNED_INT, both through RED_INTEGER - and raised
INVALID_ENUM for everything else. That is most of the entry point missing rather than a
narrow gap: GL takes any of the sized formats in the buffer-texture table, which is what an
application clearing an RGBA8 or R32F buffer uses.

The wrong error also hid the checks behind it. A test clearing a mapped buffer, or one
passing a misaligned offset, never reached those rules because the format tuple was rejected
first, so INVALID_ENUM came back where INVALID_OPERATION or INVALID_VALUE was due - the
validation was there and correct all along, just unreachable.

internalformat now goes through the same table the buffer textures use (shared rather than
written out twice, since it is the same list for the same reason), and format and type
through the ordinary pixel format converters. The element size comes from the internal
format, which is what offset and size have to be multiples of. Note that a bad format or
type here is INVALID_VALUE, not INVALID_ENUM (GL 4.6 core 6.3) - the odd one out among the
enum arguments, and what the conformance tests check for.

The pattern is still replicated verbatim, which is correct while the client layout matches
the internal format - every real caller, and every conformance case. When they differ it now
says so instead of quietly writing a differently-sized pattern.

direct_state_access.buffers_clear and buffers_functional pass on both backends;
buffers_errors is down to one unrelated complaint about glMapNamedBufferRange.
2026-08-05 00:09:11 -04:00
BZLZHH bd710078fc [Feat] (MG_Impl): implement glGetNamedBufferSubData
The by-name read was a stub, so it left the caller's buffer untouched and a test comparing
it against a reference saw whatever that memory already held. Its by-target sibling
glGetBufferSubData was already implemented, so this is that function with the buffer
resolved by name instead of through a binding: the same non-negative offset and size check,
the same bound-by-the-buffer's-size check, the same refusal to read a buffer mapped without
GL_MAP_PERSISTENT_BIT, and the same SyncGpuWrites before the download so a GPU-side write
that has not landed yet is not missed.

Resolving by name reports INVALID_OPERATION for a name that is not a buffer, which the
by-target form expresses as "target is bound to no buffer object" instead.

direct_state_access.buffers_get_named_buffer_subdata passes on both backends.
2026-08-05 00:01:20 -04:00
BZLZHH 95a7b17d45 [Fix] (DirectVulkan): clear an integer colour buffer with an integer value
glClearBufferiv and glClearBufferuiv flattened their values into the payload's float vector,
and every clear was later written into VkClearColorValue::float32. Vulkan reads that union
according to the destination image's format rather than converting between its members, so
an R8I attachment cleared to -16 received the bit pattern of -16.0f. On top of that,
QueueRenderbufferClear copied only the float vector into the pending clear, so even the
flattened value was dropped and the attachment kept reading zero - which is what the
conformance tests actually observed.

The payload now records which of the three entry points supplied the colour and keeps the
value in that form, and one helper builds the union member the encoding calls for. GL's rule
that a format with no alpha channel reads as one has to be applied in the value's own type,
so the "does this format lack alpha" question is now asked separately from the substitution
and the helper applies it to whichever member is live. glClear is left on the float path
explicitly: ClearFramebufferPayload has no other form.

Takes every integer renderbuffer format in direct_state_access.renderbuffers_storage from
failing to passing on Magma - 115 reported mismatches down to 20, the rest being the stencil
formats Espryt fails too and SRGB8_ALPHA8 - and makes framebuffers_clear pass on both
backends.
2026-08-04 23:55:43 -04:00
BZLZHH 19932f9e49 [Feat] (MG_Impl, MG_Backend): implement the integer direct state access framebuffer clears
glClearNamedFramebufferiv and glClearNamedFramebufferuiv were stubs, so a clear through
them was silently dropped and the attachment kept whatever it held. Their float siblings
were already implemented, which is what made the gap look like a rendering bug rather than
a missing entry point.

Which buffers they accept is narrower than glClearNamedFramebufferfv and differs between
the two: signed values clear COLOR or STENCIL, unsigned only COLOR (GL 4.6 core 17.4.3.1).
Only the colour buffer is indexed, so a stencil clear naming any drawbuffer other than 0 is
INVALID_VALUE rather than merely ignored, and anything else is INVALID_ENUM. Resolving the
framebuffer by name goes through the same helper the float forms use, which is what reports
INVALID_OPERATION for a name that is neither zero nor an existing framebuffer.

Both backends express them the way they already express the float forms: DirectGLES binds
the named framebuffer and forwards to glClearBuffer*, Magma queues the payload against the
named framebuffer rather than the bound one.

direct_state_access.framebuffers_clear_errors passes on both backends, and
framebuffers_clear passes on Espryt. Magma still fails that one, for a separate reason on
the materialization side rather than in these entry points.
2026-08-04 23:41:28 -04:00
BZLZHH 1011d9fea1 [Test] (MG_Test): follow the query-name and incomplete-texture rules the CTS pinned down
Two unit tests asserted behaviour the conformance tests had since contradicted, so they
were testing MobileGL's old answer rather than GL's.

QueryTest expected glIsQuery to report a name straight out of glGenQueries as a query
object. It is not one: GenQueries reserves names, and they "acquire query state only when
they are first used by calling BeginQuery" (GL 4.6 core 4.2.1). The test now checks that a
reserved name reads FALSE, that BeginQuery is what turns it into an object, and that a
sibling name left untouched stays FALSE. A companion case covers the direct state access
half, where glCreateQueries does create the object outright - which is the whole reason the
two entry points both exist.

The DirectGLES binding test built its texture with glGenTextures and glBindTexture and
nothing else, then expected BindCurrentTextures to bind it natively. A texture with no
image is incomplete and samples as (0, 0, 0, 1), which DirectGLES expresses by leaving the
native target unbound, so the setup no longer produced the binding the test then went on to
clear. It now gives the texture a format and a 1x1 level 0 - one level is the entire mip
chain at that size, so it is complete under any filter - and asserts that directly, so a
future completeness change fails on the setup line instead of on the assertion three calls
later.
2026-08-04 23:13:02 -04:00
BZLZHH b5565ae503 [Docs] (tools/cts): refresh the DSA reference tables after the multisample storage fix 2026-08-05 02:50:22 +00:00
BZLZHH b06ad3f877 [Fix] (MG_Impl): make multisample texture storage immutable, and validate it by name
glTexStorage2DMultisample and glTexStorage3DMultisample forwarded straight to the
glTexImage*Multisample allocation and stopped there. The allocation is indeed the same;
what the storage forms add is that it is final - TEXTURE_IMMUTABLE_FORMAT becomes TRUE and
any later call on that texture is INVALID_OPERATION (GL 4.6 core 8.19). MobileGL left the
texture mutable forever, so it reported TEXTURE_IMMUTABLE_FORMAT as FALSE and accepted
being respecified any number of times, silently discarding storage a test or an
application had already rendered into.

The by-name forms had no validation of their own either. The target forms get their target
checked when the binding is resolved; reached by name there is no binding, so
glTextureStorage2DMultisample took any texture, any extent and any sample count. It now
rejects a target that belongs to the other entry point (INVALID_OPERATION), extents
outside 1..GL_MAX_TEXTURE_SIZE and a depth past GL_MAX_ARRAY_TEXTURE_LAYERS
(INVALID_VALUE), and a sample count above GL_MAX_SAMPLES (INVALID_OPERATION) - measured
against the limit the getter reports rather than the backend parameter it is derived from,
since the frontend raises that number.

glTextureStorage1D/2D/3D gained the same treatment: a target belonging to a different one
of the three is INVALID_OPERATION, a zero extent is INVALID_VALUE (immutable storage
describes a real image, unlike glTexImage*D where an empty level is legal), and a level
count longer than the level-zero size admits is INVALID_OPERATION. Which dimensions take
part in that mip chain is per target: a 1D array keeps its layer count in height, so its
height does not halve.

Takes direct_state_access.textures_storage_multisample_2d_* from 0 to 30 of 30 on Espryt,
and the whole group from 74.93% to 82.48%. Magma still fails them for a separate reason.
2026-08-05 02:49:45 +00:00
BZLZHH 3311e6034a [Fix] (MG_Impl): report a buffer texture as the wrong object, not the wrong token
glGetTextureParameter* resolve the texture by name and then hand the work to the
target-based getter, which validates the target it was given. For a buffer texture that
is GL_TEXTURE_BUFFER, and the target form correctly calls that an unaccepted token -
INVALID_ENUM.

By name there is no token to blame. The application named an object that carries none of
the sampler or level state the query reports, which is INVALID_OPERATION (GL 4.6 core
8.11). The four by-name getters check the resolved object before delegating, so the error
describes what the caller actually got wrong.

Fixes direct_state_access.textures_parameter_errors on both backends, taking the group to
74.93% on Espryt and 73.32% on Magma.
2026-08-05 02:36:31 +00:00
BZLZHH 027c1bd4ab [Docs] (tools/cts): add the desktop Linux CTS skill
The Android and Windows paths each have a skill; the desktop Linux one had only
a runner script and a README section, so it was the least discoverable of the
three despite being the one to reach for while iterating - it needs no device
and no GPU, and a single test group takes seconds rather than hours.

Records what the other two skills cannot: that the toolchain has to be GCC 13+
or Clang 20+ (Clang 18 reports __cpp_concepts as 201907L, which switches
libstdc++'s <expected> off and breaks the shader transpiler), that
EGL_PLATFORM=surfaceless is mandatory for DirectGLES and why the symptom points
at the wrong call, and which of this environment's results are MobileGL's own
versus artefacts of software rendering.

Also states the rule the other skills only imply: report Espryt and Magma
separately. They fail different cases, and one combined number hides which
backend a change moved.
2026-08-05 02:31:46 +00:00
BZLZHH da52cc3906 [Docs] (tools/cts): refresh the DSA reference table for the fixes in this branch 2026-08-05 02:29:42 +00:00
BZLZHH ebe4fe133f [Fix] (MG_Impl): apply the buffer texture's own format and range rules
glTextureBuffer and glTextureBufferRange took any internal format the texture enum
converter recognised. A buffer texture accepts a much shorter list than a sampled or a
renderable texture does (GL 4.6 core table 8.16), and it cannot be inferred from either,
so a format like GL_RGB8 was accepted and produced a texture nothing could read.

Two error codes were wrong as well. A texture whose effective target is not
GL_TEXTURE_BUFFER is the wrong object rather than the wrong token, so it is
INVALID_OPERATION. And the range form never checked its range against the buffer it was
attaching, so a size past the end of the buffer was accepted and left the texture
addressing memory the buffer does not own.

Fixes direct_state_access.textures_buffer_errors and textures_buffer_range_errors on both
backends.
2026-08-05 02:29:25 +00:00
BZLZHH 534ec65dda [Fix] (MG_Util): ask the ES driver for the texture buffer offset alignment
The DirectGLES capability probe queried GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT with a bare
glGetIntegerv while every other query in the same function goes through glesFuncs. A bare
call resolves to MobileGL's own exported entry point, which answers that pname out of the
capability table this code is in the middle of filling in, so the value read back was the
default it started from and the driver's real alignment never arrived.

The backend therefore advertised an alignment of 1. An application that trusts that -
which is the only thing it can do - passes glTextureBufferRange an offset the ES driver
cannot honour, and the driver produces a texture that reads as zeros with no error
anywhere. The alignment llvmpipe actually wants is 16.

Takes direct_state_access.textures_buffer_* from 3 to 30 of 30 on DirectGLES, and the
whole DSA group from 66.85% to 74.12%. DirectVulkan was unaffected: its alignment comes
from a Vulkan device limit and was already right.
2026-08-05 02:26:50 +00:00
BZLZHH 35ad1ae7fc [Docs] (tools/cts): document the desktop Linux CTS path and the DSA baseline
run_cts_local.py and the mobilegl-desktop VK-GL-CTS target were both in the tree
with nothing describing how to reach them, so the only documented ways to run the
suite needed either an Android device or a Windows box with a GPU. The desktop
Linux path needs neither: lavapipe gives DirectVulkan a headless surface and
Mesa's surfaceless EGL gives DirectGLES a context, so a single test group can be
measured in seconds while working on it.

Records the two things that cost time to find. EGL_PLATFORM=surfaceless is
mandatory for DirectGLES - without a /dev/dri node Mesa fails eglInitialize on
the default display, and MobileGL surfaces that as EGL_BAD_ALLOC from
eglCreatePbufferSurface, which points at the wrong call entirely. And
DirectVulkan's default-framebuffer readback returns zeros here exactly as it does
on Adreno, so that defect is MobileGL's and reproducible without a phone.

The direct_state_access reference table is the measured baseline for the fixes in
this branch, so a later change has something to be compared against.
2026-08-05 02:22:09 +00:00
BZLZHH 6152ee933f [Fix] (MG_Impl): bound a colour attachment and a vertex binding range by the limit
GL_COLOR_ATTACHMENTn is a token for every n up to 31, but only the first
GL_MAX_COLOR_ATTACHMENTS of them name an attachment point of a framebuffer object. The
enum conversion accepted the whole token range, so attaching a renderbuffer or a texture
to a colour attachment past the limit silently succeeded instead of reporting
INVALID_OPERATION, and the attachment landed in a slot nothing else would ever look at.

glBindVertexBuffers and glVertexArrayVertexBuffers take a range of binding points rather
than one index. A range running past the last binding point is INVALID_OPERATION, which
the per-binding validation could not report: it saw one index at a time and reported the
INVALID_VALUE that a single out-of-range index earns. The range is checked up front now,
before any binding point is touched, so a rejected call also leaves none of them changed.

Takes direct_state_access.vertex_arrays_* to 18 of 19 and fixes
direct_state_access.framebuffers_renderbuffer_attachment_errors on both backends.
2026-08-05 02:21:13 +00:00
BZLZHH dac02ca044 [Feat] (MG_Impl, MG_State): implement the direct state access transform feedback API
glCreateTransformFeedbacks, glTransformFeedbackBufferBase, glTransformFeedbackBufferRange
and the three glGetTransformFeedback* queries were all stubs, so a transform feedback
object could only be configured and inspected by binding it first - the exact thing
direct state access exists to avoid. The queries were the worse half: they returned
nothing and raised no error, so an application could not tell that it had learned
nothing.

glCreateTransformFeedbacks creates the objects outright. glGenTransformFeedbacks only
reserves names, and a reserved name becomes an object when it is first bound
(GL 4.6 core 13.2.1); the DSA form has no bind step to create them from.

The queries and the buffer bindings read and write a named object's state. That state
lives in two places: the context keeps one live copy of the capture bindings and the
active/paused flags for whichever object is bound, and every other object's copy sits in
its saved state until a bind swaps it in. The by-name accessors added to the context
resolve that, so a query for the bound object reads the live copy rather than a stale
save.

GL_TRANSFORM_FEEDBACK_BUFFER_START and _SIZE are answered as zero unless the binding was
made by the range form, matching what the buffer object binding points already do.

Takes direct_state_access.xfb_* from 0 to 4 of 5 on both backends; xfb_functional still
fails on the capture itself, which is a separate defect.
2026-08-05 02:16:48 +00:00
BZLZHH 42fd02d82f [Fix] (MG_Impl, MG_State): give the vertex buffer binding points a real state view
The binding-point half of ARB_vertex_attrib_binding was implemented, but nothing
outside it could see the result. glGetIntegerv answered GL_MAX_VERTEX_ATTRIB_BINDINGS,
GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET and GL_MAX_VERTEX_ATTRIB_STRIDE with a hardcoded
0 and a comment saying the entry points were stubs, which they no longer are. An
application that sizes its loops off those limits therefore saw none, and every
"bindingindex must be less than MAX_VERTEX_ATTRIB_BINDINGS" check silently accepted
everything because the limit it validated against was not the one it reported.

The indexed getters answer GL_VERTEX_BINDING_{BUFFER,DIVISOR,OFFSET,STRIDE} from the
bound vertex array now, and the non-indexed getter reports them as indexed-only rather
than returning a fabricated 0.

glVertexAttribPointer is defined in terms of the binding model: it also points the
attribute at its own binding point and gives that point the buffer, the pointer as the
offset and the effective (never zero) stride. MobileGL resolved the pointer form
straight into the flat attribute view and left the binding point untouched, so
GL_VERTEX_BINDING_OFFSET read back 0 for every attribute set up the classic way. The
flat view keeps the raw stride, because GL_VERTEX_ATTRIB_ARRAY_STRIDE reports that
argument verbatim, so the binding point is recorded alongside it rather than resolved
from it. glVertexAttribDivisor likewise now moves the binding point's divisor.

The by-name entry points reject vertex array 0. MobileGL keeps a real object at index 0
for the compatibility paths, so the name validation used to let the default vertex array
through a direct-state-access call that has no such thing.

glVertexAttribFormat and friends validated with the pointer-only subset, which reports
GL_BGRA as an out-of-range size instead of applying the BGRA rules, and never saw
relativeoffset at all. They share the full format validation now, which also grew the
GL_UNSIGNED_INT_10F_11F_11F_REV rules - that type has no DataType of its own, so it has
to be recognised before the conversion turns it into Unknown and reports the wrong error.

glVertexAttribLFormat and glVertexArrayAttribLFormat were stubs. They validate their
arguments now and then report that 64-bit vertex attributes are unsupported, which is
honest; silently accepting a format that can never be used is not.

Takes direct_state_access.vertex_arrays_* from 12 to 17 of 19 on both backends.
2026-08-05 02:16:32 +00:00
BZLZHH 6359b0002b [Feat] (MG_Impl, MG_State): implement the DSA vertex array queries
glGetVertexArrayiv, glGetVertexArrayIndexediv and glGetVertexArrayIndexed64iv
were stubs, so nothing could read a vertex array's state without binding it
first -- the exact thing direct state access exists to avoid.

They read the state the vertex array already holds. Two accessors were needed for
that: the relative offset and the binding points, which are the binding-point
view the flat per-attribute state was resolved from and cannot be reconstructed
from the resolved form.

Note the index means different things by entry point: for the 32-bit indexed
query it is an attribute, but GL_VERTEX_BINDING_OFFSET names a vertex buffer
binding point directly (GL 4.6 core 10.3.1). GL_VERTEX_ATTRIB_ARRAY_LONG is
answered GL_FALSE throughout, which is honest while 64-bit vertex attributes are
unsupported.

Takes direct_state_access.vertex_arrays_* from 8 to 12 of 19 on Espryt.
GL_VERTEX_BINDING_OFFSET still reads back 0: the query is right but the offset is
not reaching the binding point, which is a separate defect further up.
2026-08-04 21:16:59 -04:00
BZLZHH c186f5f255 [Feat] (MG_Impl): implement glCreateQueries and stop treating a reserved name as a query
glGenQueries only reserves names; a name becomes a query object when it is first
used with BeginQuery or QueryCounter (GL 4.6 core 4.2.1). MobileGL created the
live object eagerly at glGenQueries time and glIsQuery reported every reserved
name as an object, with a comment noting the shortcut.

The registry already distinguished the two states -- a target of 0 means the name
has never been used -- so glIsQuery now consults it, and a name that came from
glCreateQueries carries a flag saying it is an object regardless.

glCreateQueries itself was a stub. It creates the objects outright with their
target already fixed, which is the whole point of the DSA form: there is no
binding step to infer the target from later.
2026-08-04 21:07:18 -04:00
BZLZHH 3d97f6fa8f [Fix] (DirectVulkan): decline a draw with no usable fallback instead of aborting
GetFallbackTexture asserted that the target was 2D or rectangle, so a sampler
whose texture could not be resolved took the process down whenever it was any
other kind. A multisample sampler reaches exactly that path: its texture is
reported incomplete, the resolve falls back, and the assert fires. Sixty
direct_state_access multisample cases died that way, and because the abort kills
the whole process the harness lost the rest of its chunk with them -- one run
needed 63 invocations to get through the suite instead of 3.

The fallback is a single-sampled 2D image, so it genuinely cannot stand in for a
multisample sampler: that descriptor demands a multisample view, and binding this
one is invalid usage rather than a degraded picture. So report that no fallback
exists and let the caller decline the draw. An unbound or incomplete sampler is
an application-level mistake with a defined GL meaning; it is never a reason to
abort.

The cases still fail -- multisample textures are not yet complete enough to
sample -- but they fail as one reported case each.
2026-08-04 21:00:13 -04:00
BZLZHH bb582203d9 [Feat] (MG_Impl, MG_State, MG_Util): attach a buffer texture to a range of its buffer
glTexBufferRange, glTextureBuffer and glTextureBufferRange were all stubs, so a
buffer texture could only ever be attached through glTexBuffer -- by binding, and
always to the whole buffer.

Give the buffer texture the window it is supposed to address. The non-range forms
record it as offset 0 with a whole-buffer sentinel rather than the size the buffer
happens to have, so a later respecify keeps being followed instead of freezing the
texture at yesterday's size. All four entry points now share one attach path,
differing only in how they name the texture: by binding for the target forms, by
name for the DSA ones.

Both backends honour the window: DirectVulkan offsets and clamps the buffer view,
DirectGLES uses glTexBufferRange when the texture names a sub-range and keeps
plain glTexBuffer for the whole-buffer case, which also works on a driver without
the range entry point.

GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT reported 0 with a comment explaining that the
range entry points were stubbed. It now reports what the device actually requires
-- minTexelBufferOffsetAlignment on Vulkan, the driver's own value on GLES -- and
the range entry points enforce it. Zero was never a legal answer; the minimum is
1, and an application that trusted it would have built unaligned offsets.
2026-08-04 20:53:53 -04:00
BZLZHH f39e6eb82d [Feat] (MG_Impl): implement glReadnPixels
It was exported as a stub: it logged a warning and returned, leaving the caller's
buffer untouched. Anything reading back through it saw whatever the destination
already held, which for a freshly allocated vector is zeros -- so every
direct_state_access texture test comparing a readback against reference data
failed without a GL error to explain it.

glReadnPixels is glReadPixels with a bound on how much it may write (GL 4.6 core
18.2.8, originally GL_ARB_robustness) and is identical in every other respect, so
it validates and reads through exactly the same path once the destination is
known to be big enough.

Sizing the read honours the GL_PACK_* state: rows are padded to GL_PACK_ALIGNMENT
and laid out GL_PACK_ROW_LENGTH wide, with the skip parameters offsetting the
first texel. The last row is deliberately not padded -- nothing follows it to
align -- which is what makes a tightly-sized destination legal.
2026-08-04 20:33:14 -04:00
BZLZHH cb2ba71feb [Feat] (DirectVulkan): run the tessellation stages
The backend already turned a tessellation control/evaluation shader into the
right VkShaderStage, but nothing downstream knew what to do with it: GL_PATCHES
had no topology, so it fell through to the triangle-list default, and the
pipeline carried no tessellation state at all. A GL_PATCHES draw therefore ran
the vertex and fragment stages over raw triangles.

Map GL_PATCHES to VK_PRIMITIVE_TOPOLOGY_PATCH_LIST, carry GL_PATCH_VERTICES into
the pipeline as patchControlPoints (part of the key, since two patch sizes are
two pipelines), attach VkPipelineTessellationStateCreateInfo for a patch topology
only, and enable the tessellationShader device feature.

POST reports the feature, because without it a program with a tessellation stage
cannot build a pipeline at all and GL_PATCHES draws render nothing.
2026-08-04 20:03:24 -04:00
BZLZHH 6ea7ccdf64 [Feat] (DirectVulkan): support an arbitrary primitive restart index
Vulkan restarts only on the fixed all-ones value of the index type, so
GL_PRIMITIVE_RESTART with a glPrimitiveRestartIndex of anything else used to
hard-fail the draw. GL_PRIMITIVE_RESTART_FIXED_INDEX already matches Vulkan and
is untouched.

Rewrite the indices into a transient copy instead, substituting the fixed value
for the application's. An index that already equals the fixed value would then be
indistinguishable from a restart, so it is nudged down by one: it can only be a
real index, since the application's restart index is a different number, and the
vertex it names is outside any well-defined draw -- whereas leaving it alone would
tear the primitive in two.

The element array buffer is rewritten whole rather than only the drawn range,
because an indirect draw's firstIndex lives in GPU memory and cannot be adjusted
from here; every element therefore keeps its position.
2026-08-04 20:00:17 -04:00
BZLZHH 14605723f0 [Fix] (DirectVulkan): flag a transform feedback capture as a GPU write
A capture is a GPU write like any shader's, so a later CPU read of the buffer has
to wait for it. Only shader storage buffers were flagged, so mapping or reading
back a capture buffer could observe whatever the queue had retired so far.

Nothing needs copying -- the capture writes land in coherent host-visible storage
already -- but coherence only says the writes are visible once they have
happened, which is exactly what MarkGpuWritten arranges through the readback op.
2026-08-04 19:56:28 -04:00
BZLZHH a680611c9f [Fix] (DirectVulkan): never stream a buffer whose storage the application holds
AcquirePersistentMap promises the storage it creates is never recreated, because
the frontend adopts it in place of the shadow and hands out pointers into it.
AcquireStreamedSlice broke that promise: its downgrade path releases the resident
storage unconditionally to avoid keeping a second stale copy, so binding such a
buffer as a vertex or index source freed the memory the application was still
pointing at.

It also fed that draw the wrong bytes. The streaming copy is uploaded from the
shadow, and a persistently mapped buffer can hold bytes the shadow never saw -- a
transform feedback capture writes straight into the resident storage. The next
capture into the same buffer then landed in freshly recreated storage while the
application kept reading the original, which is how the ping-pong in
transform_feedback.draw_xfb_feedbackk_test stalled after its first doubling.

Route a persistently mapped resource to the resident path instead, where its
single piece of storage is bound directly.
2026-08-04 19:56:28 -04:00
BZLZHH 93224ca406 [Fix] (DirectVulkan): make transform feedback writes visible to what reads them
GL makes transform feedback results visible to every later command on their own,
with no glMemoryBarrier in between -- unlike shader storage writes. An
application replaying a capture with glDrawTransformFeedback is therefore
entitled to the captured bytes without asking for them, so the barrier the Vulkan
memory model requires has to come from here.

It cannot be recorded where the write happens: the capturing draw runs inside a
render pass that declares no self-dependency. Flag it there instead and emit the
barrier at the next point that could read the buffer -- the following draw's
setup, or a readback -- ending the render pass first, the same shape
glMemoryBarrier already uses.

The destination covers every way a captured buffer comes back: replayed as vertex
attributes or indices, read through a uniform or storage binding, sourced as an
indirect command, copied out, or mapped.
2026-08-04 19:47:39 -04:00
BZLZHH fbed4485b7 [Fix] (DirectVulkan): key the program cache on the transform feedback capture layout
The program cache is content-hash-shared across GL program names, so its key has
to cover everything that changes the modules it stores. The capture layout did
not: XfbCaptureDecoratePass bakes XfbBuffer/XfbStride/Offset into the SPIR-V from
the frontend's layout, none of which is in the SPIR-V being hashed.

Two programs with identical shaders and different glTransformFeedbackVaryings
therefore shared one entry, and the first one linked decided how both captured.
That is precisely what changing the buffer mode does -- the same varyings
recorded with GL_SEPARATE_ATTRIBS instead of GL_INTERLEAVED_ATTRIBS -- so the
separate-attribs pass of transform_feedback.draw_xfb_test replayed a capture that
was still interleaved into buffer 0.

Hash the captured varyings' names, buffer indices and offsets plus the per-buffer
strides, and only for a capturing compile, so no other program changes key.
2026-08-04 19:47:39 -04:00
BZLZHH 90ae0f048c [Fix] (MG_Impl): answer the GL_UNIFORM program interface from the frontend reflection
The GL_UNIFORM interface queries and glGetActiveUniform(s)iv describe the same
set of resources in two spellings, but they were reading it from two different
places: the latter from the frontend reflection, the former forwarded straight
to the backend program.

The backend program is not a source of truth for this. It does not exist at all
for a program whose types its shading language cannot express -- a
double-precision uniform has no ESSL form, so the program never links there --
and the interface queries then described a program with no uniforms, which is
how gpu_shader_fp64.fp64.state_query failed.

Route GL_ACTIVE_RESOURCES / GL_MAX_NAME_LENGTH, the resource index, the resource
name and the resource properties for GL_UNIFORM through the same reflection that
already answers glGetActiveUniformsiv, so the two spellings can no longer
disagree and neither depends on the backend having linked. The props that
reflection does not model (GL_ATOMIC_COUNTER_BUFFER_INDEX and the
GL_REFERENCED_BY_* stage bits) still come from the backend, looked up by the
uniform's name so the two index spaces do not have to agree.

GL_MAX_NAME_LENGTH counts the terminator; the stored maximum does not, as every
other caller of GetUniformMaxLength() already accounted for.
2026-08-04 19:36:49 -04:00
BZLZHH cff959b2e8 [Feat] (DirectVulkan, MG_Util): honour a glVertexAttribDivisor other than 1
Vulkan's VK_VERTEX_INPUT_RATE_INSTANCE advances an attribute once per instance and has
no way to say anything else, so every non-zero divisor collapsed to 1: an attribute the
application asked to change every three instances changed every one, and
KHR-GL40.draw_indirect.basic-drawArrays-instancing and its elements sibling drew the
wrong colours from instance one onward.

VK_EXT_vertex_attribute_divisor is exactly this state, so it is enabled when the device
has it and the per-binding divisors ride into the pipeline through
VkPipelineVertexInputDivisorStateCreateInfoEXT. Only divisors other than 1 are listed -
1 is what the plain input rate already means - and they join the layout hash, so two
layouts that differ only in a divisor no longer share a pipeline.

POST reports the feature either way, because without it the failure is silent and looks
like a shader bug: the attribute is fetched, just from the wrong instance. The GLES side
gains the two checks this session's other work made load-bearing for the same reason -
glPatchParameteri (without it GL_PATCH_VERTICES stays at the driver's 3 and a patch draw
of any other size renders nothing) and the transform feedback object entry points
(without them a second object cannot open a capture while the first is paused).

KHR-GL40.draw_indirect on Magma: 70/70 but for the arbitrary primitive-restart index,
which Vulkan cannot express at all.
2026-08-04 19:25:32 -04:00
BZLZHH a50b2c422b [Fix] (DirectVulkan): submit a generated mip chain before a later upload can overtake it
Texture uploads go out on a command buffer of their own the moment they happen, while
glGenerateMipmap records its blit chain into the frame's command buffer, which is not
submitted until the frame ends. So a glTexSubImage2D into a level that was just
generated reached the GPU FIRST and the blits then wrote over it.

KHR-GL40.texture_gather.base-level does exactly that - generates the chain, then writes
the texels it is going to sample into level 1 and points TEXTURE_BASE_LEVEL at it - and
read back the generated content instead of what it had written. The image view, the mip
range and the upload itself were all correct; only their order on the GPU was not.

This is the same hazard the mip-chain-growth recreate above already flushes for, from
the other side: there the recorded work had to reach the GPU before an out-of-band copy
read the image, here before an out-of-band copy writes it. Submitting at the end of the
generation orders every upload that can follow.
2026-08-04 19:18:03 -04:00
BZLZHH 9dcda82d71 [Fix] (DirectVulkan): advertise GL_ARB_get_program_binary on Magma too
The extension and its three entry points are frontend state - no binary format is
exposed on either backend - but only DirectGLES listed it, so on Magma dEQP's loader
still left glProgramParameteri null and KHR-GL40.api.coverage called straight through
the null pointer. The entry point is not core before GL 4.1; this is what exposes it.
2026-08-04 19:10:40 -04:00
BZLZHH 28d0af6f04 [Feat] (MG_Util, DirectGLES, DirectVulkan): normalize rectangle coordinates in the module
Neither target API has GL_TEXTURE_RECTANGLE: ESSL has no rectangle sampler, and
Vulkan's SPIR-V environment does not allow Dim::Rect. Both emulate it on a plain 2D
texture, and the two differ in exactly one way - a rectangle lookup addresses texels
where a 2D one addresses [0,1].

That one difference now lives in one SPIR-V pass, so neither backend has to know about
it: every lookup taking normalized coordinates gets its coordinate divided by the size
the texture reports, and the image type is then rewritten to 2D. Magma had no rectangle
handling at all - it fed Dim::Rect straight to Vulkan, which read the texel coordinates
as normalized and sampled the edge, so all fifteen KHR-GL40.texture_gather.*-2drect
cases came back holding the clear colour.

This replaces the ESSL text rewrite that did the same divide for DirectGLES only. Doing
it in the module instead is both shorter and stricter: the pass resolves an operation's
image type through the sampled-image and pointer wrappers rather than matching a
sampler name in generated source, so it cannot be fooled by an expression where it
expected an identifier, and it needs no help from the frontend reflection to know which
samplers were rectangles.

Still declined, as before: the Dref *sample* forms, whose coordinate carries the compare
value in its last component, and the projective ones, where the divide would have to
happen after the perspective divide. texelFetch is deliberately untouched - integer
texel coordinates mean the same thing on both targets.

KHR-GL40.texture_gather: Magma 66 failures -> 2, Espryt stays at 75/75.
2026-08-04 13:25:40 -04:00
BZLZHH 44ee6b66b3 [Fix] (MG_State, DirectVulkan): apply the incomplete-texture rule on Magma too
The completeness rule itself is GL's, not a backend's, so it now reads as one question
both backends ask - SamplesAsIncompleteTexture(texture, effective sampler) - and each
answers in whatever way it already expresses "nothing is bound at this sampler".
DirectGLES leaves the native target unbound; Magma has a fallback texture for exactly
that case and now routes an incomplete texture to it.

The fallback's texel had never been written, so it read whatever its freshly allocated
storage held. GL is specific here: an incomplete texture - and a sampler with nothing
bound - reads (0, 0, 0, 1). It says so now, which is what makes
KHR-GL40.texture_gather.incomplete-texture-last-comp (it gathers the alpha) meaningful
rather than accidentally right.
2026-08-04 13:14:36 -04:00
BZLZHH 28c3cfc1d6 [Fix] (DirectVulkan, MG_State): make a shader-written storage buffer readable on Magma
Reading a buffer a compute shader wrote gave zeros: the frontend shadow that MapBuffer
resolves against is only maintained by uploads, and Magma had no path back. Every
KHR-GL40.texture_gather case ends by dispatching a compute shader into an SSBO and
comparing the mapped result, so 66 of 75 failed on it.

Magma needs no readback: EnsureGpuResidentStorage - the same host-visible coherent
adoption the transform feedback capture already uses - makes the shadow BE the memory
the shader writes, so binding a buffer as a shader storage buffer now adopts it. What
coherence does not give is ordering: the writes are visible once they have happened,
and the CPU was reading before the dispatch had retired. The readback op therefore
submits the recorded work and waits.

That exposed a mistake in the frontend flag this rides on: MarkGpuWritten skipped
GPU-resident buffers, reasoning there was no shadow to refresh. True, but the wait is
still needed - "reconcile with the GPU write" is not always "copy it back", and which
of the two it is belongs to the backend. The flag now only says a write is outstanding;
DirectGLES's readback still skips its persistent-mapped buffers when copying.

KHR-GL40.texture_gather on Magma: 66 failures -> 19 (the rest are rectangle textures,
mipmap completeness and tessellation, all still to do). Espryt stays at 75/75.
2026-08-04 11:55:17 -04:00
BZLZHH 38e04eefae [Fix] (DirectVulkan): size the indirect draw command by GL's struct, not the renderer's
The indirect draw paths bounded their read out of GL_DRAW_INDIRECT_BUFFER - and took
their default stride - from `sizeof(DrawCmdParam)`, this renderer's own draw-parameter
struct. That is not the command GL defines: DrawCmdParam carries two extra members for
bounding vertex-stream conversion and is 24 bytes, where GL's DrawArraysIndirectCommand
is four uint32.

So every glDrawArraysIndirect against a tightly-sized indirect buffer - which is what an
application writes, and what the CTS writes - failed the range check and drew nothing.
It went unnoticed on the elements side only by coincidence: DrawIndexedCmdParam happens
to be exactly the 20 bytes of DrawElementsIndirectCommand.

Both sizes are now named constants of GL's own layout.

KHR-GL40.draw_indirect on Magma: 21 failures -> 3.
2026-08-04 11:47:35 -04:00
BZLZHH fd29cb914e [Fix] (DirectVulkan, MG_State): give each transform feedback object its own capture counters
The frontend half of ARB_transform_feedback2 landed for both backends, but Magma's
capture was still written for the one implicit span GL 3.3 has:

- A paused span kept capturing. VK_EXT_transform_feedback's counter buffers already
  make consecutive draws append, so pausing is simply "do not wrap this draw" - the
  counters keep their values and the next resumed draw carries on where the last
  captured one stopped.
- Those counter buffers were context-wide. Transform feedback objects can each hold an
  open, paused span at the same time - KHR-GL40.transform_feedback.draw_xfb_test keeps
  three - and they were all appending through one set of four slots. Each object now
  gets its own group, handed out on first use; past sixteen objects they share group 0,
  which only matters for concurrently-paused spans.
- The generation that identifies a span is what a backend keys its append state on, so
  it is now part of the per-object state the frontend saves and restores. Without that,
  resuming an object that was paused before another one began looked like a new span
  and restarted its counters at zero.

GL_PRIMITIVES_GENERATED needed one more thing. It counts what the last vertex
processing stage emitted whether or not anything is being captured, but
VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT only counts what the capture saw - so a
draw made while the span was paused is invisible to it. The frontend now tallies those
draws, and the Vulkan query adds the delta at result time. The correction lives in the
backend that needs it: an ES driver's GL_PRIMITIVES_GENERATED counts them by itself, and
adding it there too would double them.

transform_feedback* on Magma: 4 failures -> 3. Espryt stays at 38/38.
2026-08-04 11:42:44 -04:00
BZLZHH 38497174c8 [Feat] (MG_Impl): implement the double-precision uniform state
glUniform*d, glUniformMatrix*dv, their glProgramUniform twins and glGetUniformdv were
all stubs - 35 entry points - so a GL 4.0 program's double uniforms could be declared
and located but never set or read. Worse, glGetUniformfv on one did reach the storage:
the generic getter memcpy'd the uniform's declared size into the caller's buffer, so a
4-byte float pointer received 8 bytes. That overrun is what took the process down in
KHR-GL40.gpu_shader_fp64.fp64.state_query.

The upload path is already templated on the component type, so the vector forms are
wiring. A matrix is not: the column stride the linker used for a double matrix is not
the 16 bytes a float one gets. It is not guessed - the slot the uniform was given is
exactly `columns` columns wide, so dividing states the stride the rest of the pipeline
already agreed on, for both the upload and the readback.

The four getters now convert instead of reinterpreting when the uniform holds doubles,
following GL 4.6 core 7.6: round to nearest for the integer queries, and clamp into the
queried type's range so a negative double read through glGetUniformuiv is 0 rather than
its two's complement.

The case still fails one step further on, where it queries the same uniforms through
GL_ARB_program_interface_query: those calls are answered by the backend program, and an
fp64 shader has none - ESSL has no doubles, so it never links. Answering them from the
frontend reflection is a separate change.
2026-08-04 11:06:15 -04:00
BZLZHH b95fcb7bca [Feat] (MG_State, MG_Impl, DirectGLES): implement glPatchParameteri
GL_PATCH_VERTICES decides how many vertices one tessellation patch consumes, and
glPatchParameteri was a stub - so the value stayed at the driver's default of 3 no
matter what the application asked for. KHR-GL40.texture_gather.gather-tesselation-shader
sets it to 1 and then draws a single patch: with the request dropped the draw had too
few vertices for one patch, produced nothing at all, and the case read back the clear
colour.

The value is context state on both sides and ES 3.2 spells the entry point exactly the
same way, so it is stored in the render state (where glGetIntegerv(GL_PATCH_VERTICES)
now finds it) and forwarded. Validation needs the real bound, so GL_MAX_PATCH_VERTICES
and GL_MAX_TESS_GEN_LEVEL are probed off the host driver alongside the other limits and
answered from there too; the defaults are the GL 4.0 core minimums.

KHR-GL40.texture_gather is now 75/75.
2026-08-04 10:58:14 -04:00
BZLZHH 5fce287de5 [Fix] (DirectGLES): generate a three-channel float mip chain on the CPU
glGenerateMipmap requires the level-0 format to be colour-renderable, and ES has no
colour-renderable three-channel float format at all - so an ES driver rejects
GL_RGB16F and GL_RGB32F where every desktop driver accepts them, and the error was
forwarded to the application. KHR-GL40.texture_gather.plain-gather-float-2d-rgb and
its offset- sibling build their texture that way and fail on the leftover error alone.

The blit-based emulation already used for GL_R11F_G11F_B10F is no help: it renders
level n from level n-1, so it needs exactly the renderability that is missing. But a
format the driver cannot render into is a format nothing can have rendered into
either, which makes the frontend's own copy of the texels authoritative for precisely
these formats. So the chain is box-filtered there and the levels are marked dirty; the
backend sync that follows uploads them like any other texture data.

Deliberately narrow: only the two formats whose texels are a plain float array, and
only when they are what the texture actually holds. Every other format keeps the
driver's behaviour, error included.
2026-08-04 10:54:17 -04:00
BZLZHH ae6949e459 [Fix] (MG_State, DirectGLES): sample a mipmap-incomplete texture as black
A minification filter that reads the mip chain requires every level from the base down
to hold exactly half the previous one's size; a texture that does not is incomplete and
every lookup on it returns (0, 0, 0, 1) (GL 4.6 core 8.17). Nothing checked it.

The ES driver cannot catch this on MobileGL's behalf, which is why it has to be a
frontend rule here: the backend texture is immutable storage allocated from the level
set as it stood, so a level the application later redefined at a different size never
reaches the driver at all, and the ES texture stays complete. That is exactly what
KHR-GL40.texture_gather.incomplete-texture does - it redefines level 1 of a complete
chain as 1x1 - and it read the original contents back.

The check runs where the sampling bindings are established, and an incomplete texture
simply leaves its native target unbound: an unbound ES target samples as (0, 0, 0, 1),
which is the answer GL asks for, with no scratch texture to keep around.

An array texture's layer count is not one of the dimensions that halves, so the
comparison only shrinks the components that belong to the image itself - getting that
wrong turned eight *-2darray cases black.
2026-08-04 10:49:51 -04:00
BZLZHH 5437947240 [Feat] (DirectGLES, MG_Util): normalize the coordinates of a rectangle lookup
A rectangle texture is emulated on an ES 2D texture, and LowerRectImagesForEssl
rewrites the image type in the SPIR-V to match. That is exact only where the lookup
addresses texels directly, which is why the pass declined any module containing a
lookup that takes normalized coordinates - the whole KHR-GL40.texture_gather 2drect
set among them.

The missing half is one divide: a rectangle lookup's coordinate is in texels and the
2D lookup it becomes wants [0,1], so the coordinate has to be divided by the texture's
size. It goes in on the ESSL the transpiler produces, next to the LOD-bias emulation
that already rewrites lookup arguments there, and reads the size back with
textureSize() rather than plumbing a uniform down - the emulated texture is a real ES
2D texture, so the shader can ask it directly.

Only the forms whose argument 1 is the bare coordinate are rewritten - texture,
textureOffset and the three textureGather flavours, which covers the Dref gathers too
because those carry the compare value in a separate argument. texelFetch is
deliberately left alone: its coordinates are integer texels on both targets. The
SPIR-V pass keeps declining everything else, so a projective lookup or a Dref sample
(where the compare value rides in coord.z) still refuses the module instead of
producing something subtly wrong.

Which samplers were declared rectangle is no longer visible in the transpiled source -
they are plain sampler2D by then - so the names come from the frontend program's
reflection.
2026-08-04 10:38:01 -04:00
BZLZHH f38dbf018d [Fix] (MG_State): give a rectangle texture its own initial sampler state
Every texture object started from the shared defaults, which are the 2D ones:
TEXTURE_MIN_FILTER of NEAREST_MIPMAP_LINEAR and TEXTURE_WRAP_S/T of REPEAT. A
rectangle texture has no mip chain at all, so GL gives it a different initial state -
LINEAR and CLAMP_TO_EDGE (GL 4.6 core table 23.15) - and a mipmapped minification
filter is not even a legal value to set on one.

With the 2D default in place a rectangle texture was mipmap-incomplete the moment it
was created, and an application that (correctly) never touches the filters read
(0, 0, 0, 1) out of every lookup. That is what the eleven
KHR-GL40.texture_gather.*-2drect cases saw: they set only the wrap modes, because the
filters are already what a rectangle texture needs.
2026-08-04 10:37:46 -04:00
BZLZHH 2dcc15bb0e [Feat] (MG_Impl, DirectGLES): advertise GL_ARB_get_program_binary with no binary format
glProgramParameteri is not core before GL 4.1, so in the 4.0 context the CTS runs it
only exists through GL_ARB_get_program_binary or GL_ARB_separate_shader_objects.
MobileGL advertised neither, so dEQP's loader left the entry point null - and
KHR-GL40.api.coverage, which registers glProgramParameteri from GL 3.2 upwards, called
straight through the null pointer and took the process down.

GL_NUM_PROGRAM_BINARY_FORMATS was already 0, and the extension explicitly allows an
implementation to support no binary format at all; that is the honest state of things
here, since a MobileGL program is a glslang link plus a per-backend translation with no
serialised form. So the extension is advertised for what it really provides:
glProgramParameteri stores GL_PROGRAM_BINARY_RETRIEVABLE_HINT (reported back by
glGetProgramiv alongside a GL_PROGRAM_BINARY_LENGTH of zero), glGetProgramBinary is the
INVALID_OPERATION the spec requires when that length is zero, and glProgramBinary
rejects every format with INVALID_ENUM and leaves the program's LINK_STATUS false.

Applications that ask for a binary get the documented "no formats" answer and fall
back, which is what they already had to do - only now they can ask.
2026-08-04 10:27:23 -04:00
BZLZHH ff76af9df7 [Fix] (MG_State, MG_Impl): a transform feedback name is only an object once it is bound
glIsTransformFeedback answered GL_TRUE for any name glGenTransformFeedbacks had handed
out. A generated name is reserved but does not denote an object until the first
glBindTransformFeedback (GL 4.6 core 13.2.1) - the same rule the other object types
follow - and KHR-GL40.api.coverage checks exactly the window in between.

The two questions are now asked separately: whether a name may be bound or deleted
(reserved, which is what the delete and bind paths need) and whether it is an object
(reserved and bound at least once).
2026-08-04 10:27:23 -04:00
BZLZHH 76f37a18e6 [Feat] (MG_State, MG_Impl, DirectGLES): transform feedback objects, pause/resume and the special capture names
GL 4.0 folds ARB_transform_feedback2 and _3 into core, and neither existed:
glGenTransformFeedbacks, glBindTransformFeedback, glDeleteTransformFeedbacks,
glIsTransformFeedback, glPause/ResumeTransformFeedback, the whole
glDrawTransformFeedback family and glBegin/EndQueryIndexed were all stubs, and
gl_NextBuffer / gl_SkipComponents1..4 failed the link as "not an output of the vertex
stage". Seven KHR-GL40.transform_feedback* cases failed on it, three of them by
leaving a capture open at deinit and taking the process down.

Objects. The capture state and the indexed GL_TRANSFORM_FEEDBACK_BUFFER bindings are
object state, but the context keeps one live copy of both, which is what every
existing reader - each backend's per-draw sync, the drawing and getter paths - is
written against. Rather than teach all of them about objects, a bind saves the live
copy into the outgoing object and restores the incoming one's. Object 0 is the
default object and needs no seeding; operator[] materialises the rest on first touch.

Pause. A paused span captures nothing, and three rules key off that: a draw is exempt
from the capture primitive-mode match, it feeds PRIMITIVES_GENERATED but not
TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, and glUseProgram is allowed again (that last
one was already refused for an active capture, correctly for GL 3.3, which has no
pause).

glDrawTransformFeedback replays the vertices the object captured in its last completed
span, recorded at End. "Has a completed span" is tracked separately from that count,
because a completed empty span draws nothing while an object that never ended one is
INVALID_OPERATION. Drawing from the object whose capture is currently open is
deliberately allowed - feeding a result straight into the next span is the point of
KHR-GL40.transform_feedback.draw_xfb_feedbackk_test.

DirectGLES gets a real driver object per frontend object. That is the only reason the
default one would not do: several objects can be paused at once, and a paused span
lives inside the driver's object. The deferred driver-side Begin (still needed - ES
wants the program current and the buffers bound) now also has to be held back while
the span is paused, or a pause taken before the first draw would open the span on that
draw and subject it to the primitive-mode rule it is exempt from.

Special names. gl_NextBuffer and gl_SkipComponents<n> are consumed during varying
resolution and never become varyings of their own, so they only move where the
following ones land - and stay out of the name list the backend declares on its own
driver. ES cannot express the resulting layout at all: it packs every captured varying
into one gap-free record. So when the layout has holes or spans several buffers,
DirectGLES captures into a scratch buffer bound in place of the application's, and
End distributes the records to the offsets GL asked for. Only the bytes a varying
occupies are written, which is exactly what makes the holes keep the contents the
application left there - the property KHR-GL40.transform_feedback3.skip_components
checks.

glBegin/EndQueryIndexed and glGetQueryIndexediv differ from the plain forms only in the
vertex stream they address, so they validate the index and forward. GL_MAX_VERTEX_STREAMS
stays at 1: multi-stream capture needs ARB_gpu_shader5 stream qualifiers that no ES
driver implements, and the CTS cases that need more than one stream check the limit and
skip.

KHR-GL40.transform_feedback, transform_feedback2 and transform_feedback3: 38/38.
2026-08-04 10:18:53 -04:00
BZLZHH 8d1a734c22 [Fix] (MG_State, MG_Impl): reject a draw mode the geometry stage cannot accept
A geometry shader declares the primitive type it consumes, and a draw may only present
a mode that decomposes into it - points for `points`, the three triangle modes for
`triangles`, and so on (GL 4.6 core 11.3.1). Anything else is GL_INVALID_OPERATION.
Nothing checked it, so KHR-GL40.draw_indirect.negative-gshIncompatible-arrays and
-elements drew points through a `layout(triangles) in` shader and got no error.

The program object had no notion of the geometry input primitive at all: glslang knows
it right after the link, so it is read off the geometry intermediate and kept as the
GL enum (this is also what GL_GEOMETRY_INPUT_TYPE would report). Resolved on every
link rather than only when transform feedback captures the stage, since every draw
consults it, and cleared with the rest of the link artifacts.

The check sits on the shared pre-draw gate next to the transform feedback primitive
rule, which is the same shape of constraint. GL_PATCHES is deliberately exempt: it is
the tessellation pipeline's input and has already become the tessellator's output
primitive by the time the geometry stage sees it.

draw_indirect is now at 70/70.
2026-08-04 09:55:36 -04:00
BZLZHH 7d215028fb [Fix] (MG_Impl): validate the draw mode and the indirect draw's command source
Two classes of draw-time error were never raised, which the KHR-GL40.draw_indirect
negative-* cases check one by one:

- `mode` was passed through unexamined, so glDrawArraysIndirect(GL_FLOAT, ...) reached
  the backend instead of raising GL_INVALID_ENUM. The check belongs on the shared
  pre-draw gate, so it now covers every draw entry point rather than just the indirect
  pair. Nothing that used to render stops rendering: a mode the frontend now rejects is
  a mode the backend driver was rejecting anyway, silently.
- The indirect commands read their arguments out of the buffer bound to
  GL_DRAW_INDIRECT_BUFFER, and all three of that source's preconditions were unchecked
  (GL 4.6 core 10.3.10): a 4-byte-aligned offset, a bound buffer at all, and enough room
  left in it for the whole 16- or 20-byte command. glDrawElementsIndirect also never
  validated its index type, which is the same accepted set as the rest of the
  DrawElements family.

Takes the group from 24 failures to 2 - both of the remaining ones are the geometry
shader input-primitive compatibility rule, which needs reflection the program object
does not keep yet.
2026-08-04 09:52:40 -04:00
BZLZHH 00534d8bbc [Fix] (MG_Impl): report the draw-indirect binding and the buffer access state
Three pieces of queryable buffer state were missing, all of them read by the
KHR-GL40.draw_indirect basic-binding-* and basic-buffer-* cases:

- GL_DRAW_INDIRECT_BUFFER_BINDING had no case in glGetIntegerv, so it raised
  GL_INVALID_ENUM and left the caller's variable untouched (the test read back its own
  -9999 sentinel). GL_DISPATCH_INDIRECT_BUFFER_BINDING right next to it was already
  handled; this is the same two lines against BufferTarget::DrawIndirect. Because
  glGetBooleanv/glGetFloatv/glGetDoublev all widen from the integer path, one case
  fixes all four getters.
- GL_BUFFER_ACCESS answered 0 for an unmapped buffer. Its initial value is
  GL_READ_WRITE and glUnmapBuffer restores it (GL 4.6 core table 6.2); 0 is not a legal
  value of that state at all, and the test threw on the unrecognised enum.
- GL_BUFFER_ACCESS_FLAGS was not implemented, so it fell through to the invalid-pname
  arm. It is the MapBufferRange bitfield verbatim, which the mapping access flags
  already hold in normalised form - glMapBuffer's access enum is converted on the way
  in - so it converts straight back out, and reads zero while unmapped.
2026-08-04 09:52:40 -04:00
BZLZHH d81a6a0998 [Fix] (MG_Impl): silently ignore program and shader name zero on delete
glDeleteProgram and glDeleteShader are the two entry points in the program/shader name
space where 0 is not "a name GL never handed out" but an explicit no-op: "if program is
zero, it is silently ignored" (GL 4.6 core 7.3, and 7.1 for shaders). Both went through
the shared name validator instead and recorded GL_INVALID_VALUE.

Only tests that never got as far as creating a program noticed, because they still run
their cleanup path: the five KHR-GL40.texture_gather.*-cube-array cases bail out of
Init with "GL_ARB_texture_cube_map_array not supported", then Cleanup deletes its
zero-initialised handles and the leftover error fails the case after the fact - the
downstream-error-misattribution shape. Every array-taking delete already skipped 0.
2026-08-04 09:52:40 -04:00
BZLZHH 9bf23d7ffd [Fix] (MG_State, DirectGLES): read a shader-written storage buffer back before mapping it
Buffer contents live in a CPU shadow that every read - MapBuffer, MapBufferRange,
GetBufferSubData, CopyBufferSubData - resolves against, and backend transfer ops only
ever push the shadow outwards. Two paths already knew the GPU can write a buffer on
its own and mirrored the result back by hand (ReadPixels into a pixel-pack buffer,
the transform feedback capture at EndTransformFeedback); a shader storage buffer
written by a draw or a dispatch had no such path at all, so the map handed the
application the bytes from before the dispatch.

Nothing exercised it until now because GL 3.3 has no compute stage. Every
KHR-GL40.texture_gather case ends by dispatching a compute shader that writes its
sampled texel into an SSBO and comparing the mapped result, and all 71 read back the
zero-filled shadow.

Adds the missing direction as a backend op: BufferObject::MarkGpuWritten flags a
buffer the GPU may have moved ahead of the shadow, SyncGpuWrites pulls it back at
every read point, and DirectGLES implements the readback with a plain read map of the
ES buffer. The flag is raised where the storage-buffer points are bound for the
upcoming draw or dispatch, which is the last moment the set of exposed buffers is
known, and cleared by the readback - so a buffer nothing writes costs one bool test
per map. Backends that cannot read their storage back leave the op null and keep
today's behaviour; a GPU-resident (coherent persistent) buffer needs nothing, since
its reads already resolve against the memory the shader wrote.

Drops the texture_gather failures from 71/75 to 25/75 with no crashes left.
2026-08-04 09:40:53 -04:00
BZLZHH 41e45f7d48 [Fix] (MG_Impl): let an indexed buffer bind reach the generic binding point too
BindBufferBase and BindBufferRange bind the buffer to the indexed point AND to the
generic binding point of the same target (GL 4.6 core 6.1.1); only the indexed half
was implemented. Applications lean on the second half constantly, because it is what
makes the set-up idiom work:

    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo);
    glBufferData(GL_SHADER_STORAGE_BUFFER, size, nullptr, GL_DYNAMIC_DRAW);

With the generic point left at 0 the glBufferData raised GL_INVALID_OPERATION and
the buffer kept its zero size, so the later glMapBufferRange over it failed the
offset+length bound and returned nullptr. The whole KHR-GL40.texture_gather group
verifies its result through exactly that sequence and dereferences the map's return
value without checking it, so 51 of its 75 cases took the process down with a
SIGSEGV inside the test.

Unbinding propagates the same way: buffer 0 clears both points.
2026-08-04 09:40:28 -04:00
BZLZHH 3dc6a1b6db [Fix] (MG_Impl, DirectGLES): answer the texture-gather offset limit queries
glGetIntegerv(GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET) and its GL_MAX_ counterpart fell
through to the default arm of the getter and raised GL_INVALID_ENUM, leaving the
caller's variable untouched - KHR-GL40.texture_gather.api-enums read back the
uninitialised 32764 that happened to be on its stack and failed on the error alone.

Both are core state from GL 4.0 (table 23.53) and from ES 3.1 (table 20.40), so the
value is simply the host driver's, probed alongside the other limits in
FillInGLESCapabilities and carried to the getter through DynamicBackendParameters.
The probe result is widened to the -8/+7 core minimums rather than trusted blindly:
a driver that leaves the out-parameter alone (no ES 3.1, or an enum it ignores) would
otherwise hand us a range narrower than GL 4.0 requires MobileGL to advertise, and
the shaders the CTS builds assume the guaranteed range regardless.
2026-08-04 09:40:15 -04:00
BZLZHH 598c5497b0 [Fix] (DirectVulkan): submit pending work before growing a texture's mip chain
Sizing backings by their defined mip level count gave every level-0-only texture a
single-level image, and left growing it to the recreate-and-preserve path: the new
image is created and the old contents are carried over by a vkCmdCopyImage that
PreserveTextureContentsOnRecreate submits on its own command buffer and waits on
straight away.

Whatever the frame has already recorded into the old image has not been submitted
yet at that point, so that copy reads the texture as it stood before this frame's
writes. GenerateMipmap then descends the whole chain from a stale level 0, and the
composite pass that samples it renders a washed-out frame -
minecraft-1.21.4-fabric-iris-iterationt-in-world (Iris's mipmapped colour target,
the one texture in the trace that grows 1 -> 10 levels) came back at ssim 0.5699
against a 0.99 threshold.

This is the hazard the storage-usage upgrade already flushes for before its own
preserve-copy; growing the mip chain is simply the second trigger of that same
recreate, and it was added without the same ordering guarantee. Flush there too,
gated on a texture whose live image really does carry a short chain, so the submit
happens once per texture and only when a recreate is actually coming.

Keeps the single-level backing and its memory saving; ssim goes back to 0.9992.
2026-08-02 13:09:48 -04:00
BZLZHH 86c00bdf18 [Test] (MG_Test): catch the unit tests up with three deliberate behaviour changes
ctest -L unit had been failing 13 of its 418 cases, all of them tests left asserting
what the code did before a commit that changed it on purpose:

- "restore target GL version to 3.3" put the advertised target back after the
  experimental 4.6 run, but the two Voxy sanity tests still demanded 4.6. The
  extensions they really care about are all still advertised, so assert 3.3 and drop
  the now-meaningless AtExperimentalCTSVersion from their names.
- "support rectangle textures where the emulation is exact" made every desktop-only
  target supported - rectangle included, stored as a plain 2D - while the texture
  test still expected rectangle to be rejected.
- "keep declared modern GLSL versions strict" changed two things at once: a
  normalized legacy directive now carries a marker on its line, so the ten tests
  matching "#version 330 core\n" whole no longer match; and a version the
  application declared itself is no longer raised to 460, so the sources declaring
  330/400 keep their own number and only MobileGL's own normalization is retargeted.

Test expectations follow, rather than the implementation being bent back: each of
the three changes is the intended behaviour and is argued for where it was made. The
retry test now drives the 460 escalation from a legacy "#version 130" source, which
is the only thing that is still rescued, and gained a case pinning the other half of
that contract - an application-declared "#version 330" stays at 330.

418/418 unit tests pass.
2026-08-02 09:04:16 -04:00
BZLZHH 2f2f95498f [Fix] (MG_State): detach a deleted texture from the framebuffer that is bound
GL 3.3 core 4.4.2: deleting a texture whose image is attached to the framebuffer
currently bound acts as if FramebufferTexture* had been called with texture zero for
every attachment point it occupied there. Framebuffers that are not bound keep the
orphaned attachment, so only the bound ones are touched.

MobileGL unbound a deleted texture from every texture unit and image binding but
left framebuffer attachments alone, so the framebuffer went on holding the dead
texture alive as its attachment and reads through it returned that texture's
contents rather than those of whatever the application put in its place - and since
the deleted name usually comes straight back out of the next glGenTextures, the two
are indistinguishable from the outside.
2026-08-02 07:25:07 -04:00
BZLZHH 7105c2ebdc [Fix] (DirectGLES): never skip a framebuffer bind on a stale version snapshot
BindCurrentFBO returned early when the framebuffer binding slot's version matched
g_fboBindVersions - but nothing on that path ever writes that entry. Only
ForceBindCurrentFBO stamps it, so the comparison was against an arbitrarily old
snapshot, and any later slot version that happened to land on the same 16-bit value
read as "already bound". The driver was then left on whatever framebuffer it had
last been given.

That is how KHR-GL32.packed_pixels.varied_rectangle.rg8i_format_rg_integer read its
gradient back out of the previous subtest's framebuffer, seeing 18 where 127 was
expected. It only shows up after a few thousand cases have gone by - long enough for
the counter to come back around - which is why it reproduced exactly under one
caselist and not at all in isolation.

Drop the fast path. Skipping redundant work is BindFramebufferId's job: it shadows
the driver's own draw and read bindings and drops the glBindFramebuffer when the
target already holds that id, which is where the cost actually is. What is left here
is one registry lookup.

Takes GL32 to 100% conformance; GL30, GL31 and GL33 stay at 100%.
2026-08-02 07:24:59 -04:00
BZLZHH 13bab780f2 [Fix] (DirectGLES): gate the replicate blit's stencil pass on ES 3.1
Reading the stencil half of a packed depth/stencil texture goes through
GL_DEPTH_STENCIL_TEXTURE_MODE, which is ES 3.1 state. On an older driver the pname
would raise GL_INVALID_ENUM and the shader would go on sampling depth bits as if
they were stencil, so decline the emulation instead.
2026-08-02 04:55:05 -04:00
BZLZHH 027310f993 [Fix] (MG_Impl): ask whether a colour format is renderable per target
The framebuffer-completeness check scanned every row of the backend's
format-capability cache and called the format renderable if any target said so. That
was already loose, and it broke outright once DirectGLES started widening
three-channel formats so they stay renderable as multisample storage: the caveat
capability recorded for the multisample target made GL_RGB8_SNORM look renderable
everywhere, so an ordinary 2D GL_RGB8_SNORM texture attachment reported
GL_FRAMEBUFFER_COMPLETE while the driver's own framebuffer was
INCOMPLETE_ATTACHMENT.

KHR-GL3x.packed_pixels stopped skipping those formats and read a framebuffer that
could not be read, so all 18 of its rgb8_snorm cases got back an untouched buffer.

Pass the row the attachment actually lives in - the texture's target, or the
renderbuffer row - and consult only that one; a format is still asked about in
general when the caller has no target.
2026-08-02 04:54:48 -04:00
BZLZHH c741a938bc [Feat] (DirectGLES): emulate a depth/stencil blit into a multisample framebuffer
Desktop GL replicates the source sample into every destination sample when the read
framebuffer is single-sampled and the draw framebuffer is not. ES forbids the call
outright - "an INVALID_OPERATION error is generated if SAMPLE_BUFFERS for the draw
framebuffer is greater than zero" - so the blit did nothing at all, and every one of
KHR-GL3x.packed_depth_stencil.blit's replicate iterations verified a destination
that still held its clear values.

Emulate it by drawing a full-screen triangle into the multisample framebuffer: every
pixel is fully covered, so every sample of it receives the same value, which is
precisely the replicate rule. The source rectangle is first copied into a scratch
texture of its own format (both sides single-sampled, which ES does allow), then
depth is written through gl_FragDepth and stencil - which has no shader output on ES
- one bit plane at a time with REPLACE and a discard for the pixels whose source bit
is clear.

The draw runs inside the caller's framebuffer, so every piece of pipeline state it
touches is read back and restored, including the per-draw-buffer colour masks the
non-indexed glColorMask does not cover: the sync layer's shadow of the driver state
has to stay true across this.

Colour replicate is not emulated (it would need a sampler variant per component
type); it now says so instead of failing silently.
2026-08-02 04:34:25 -04:00
BZLZHH 01d0f01d13 [Fix] (DirectGLES): report the alpha added by the multisample widening as ONE
A three-channel format widened to four for a multisample target gains an alpha
channel the application never asked for, and it holds whatever the draw that filled
the texture happened to write there. GL says a format without alpha reads back as
1.0, so KHR-GL33.texture_swizzle - which fills such a texture by rendering
vec4(r, g, b, 0.0) and then swizzles red from alpha - read 0 where it expected the
maximum.

Fold ONE into the texture's swizzle for exactly those textures, composed with the
swizzle the application set, so the promotion stays invisible.
2026-08-02 04:25:08 -04:00
BZLZHH e45f7ae5d4 [Fix] (DirectGLES, MG_Util): keep 16-bit SNORM precision through the widening
GL_RGB16_SNORM widened to GL_RGBA16F to stay renderable as multisample storage, and
a half float's 11-bit mantissa cannot hold a 16-bit signed-normalized channel:
KHR-GL33.texture_swizzle's blue channel came back several units of 32767 away from
the value the reference computes, well outside its one-unit tolerance.

GL_EXT_render_snorm makes the signed-normalized formats colour-renderable on ES, so
widen to GL_RGBA16_SNORM instead wherever it and EXT_texture_norm16 are both
present, and only fall back to the half float otherwise. Threaded through as its own
normalize option so the capability probe and the runtime pick the same format, the
way every other driver-dependent substitution here is decided.
2026-08-02 04:24:23 -04:00
BZLZHH a687873d32 [Fix] (DirectGLES): probe format capabilities on the target ES stores them on
1D, 1D-array and rectangle textures are emulated on ES 2D and 2D-array targets, but
the capability probe kept asking the driver about the desktop-only target itself.
glTexImage2D(GL_TEXTURE_1D, ...) is not something an ES driver has ever accepted, so
those rows of the cache stayed empty - and an empty row reads as "nothing is known",
not as "the format needs help", so no fallback format was ever selected for them.

GL_DEPTH_COMPONENT32 on a 1D texture therefore went to the driver unchanged instead
of as GL_DEPTH_COMPONENT24, and the texture ended up with no storage
(KHR-GL33.texture_swizzle format_idx_65 on both 1D targets read the wrong value for
every pixel).

Probe the ES target the texture will actually live on, while still recording the
capabilities against the target the frontend asked for.
2026-08-02 04:18:20 -04:00
BZLZHH 43a43c1180 [Fix] (DirectGLES, MG_Util): raw framebuffer writes while GL_FRAMEBUFFER_SRGB is off
GLES core always encodes a fragment written into an sRGB colour attachment, and
offers no switch to stop it. Desktop GL has one, GL_FRAMEBUFFER_SRGB, and it starts
out disabled - so a GL application that never touches it expects its writes to land
raw. The frontend models exactly that (the capability reads as disabled and
DirectVulkan attaches the UNORM twin to honour it), but DirectGLES was passing the
draw straight to a driver that encodes anyway.

The value therefore came back one conversion short of the reference wherever it was
written and then read again: rendering into an sRGB texture and fetching it in a
shader decodes once but had encoded twice, which is how
KHR-GL32.texture_size_promotion read 0.0142 for GL_SRGB8_ALPHA8 where 0.00111 was
expected.

Detect GL_EXT_sRGB_write_control and sync GL_FRAMEBUFFER_SRGB from the frontend
capability alongside the other enables, starting from the driver's enabled state so
the first sync always pushes the disable down.
2026-08-02 04:10:13 -04:00
BZLZHH 65dbfa6f26 [Fix] (DirectGLES, MG_Util): widen three-channel formats for multisample textures
GLES has no colour-renderable three-channel format beyond RGB8, so
glTexStorage2DMultisample rejects GL_RGB16 (and the SNORM variants) with
GL_INVALID_ENUM and the texture is left with no storage at all - every draw into it
then hit GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT and every read came back zero.

The existing fallback machinery could not help: it picks one replacement format per
requested format, from the driver's capabilities, and never re-checks that
replacement against the target it is going to be used with. GL_RGB16's fallback is
GL_RGB32F, which is a perfectly legal ES texture format and a perfectly illegal
multisample storage format, and with EXT_texture_norm16 present no fallback was
selected at all.

Add PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget, applied only to
multisample targets, mapping GL_RGB16 to GL_RGBA32F and the three-channel SNORM
formats to GL_RGBA16F. Widening the channel count is safe precisely there and
nowhere else: a multisample texture can never be uploaded to, only rendered into, so
no transfer path has to expand three-channel client data, and the alpha a draw
writes for a three-channel source is already the 1.0 the frontend format implies.

The capability probe recomputes its fallback per target for the same reason, so the
probed format and the format the texture is actually created with stay in agreement.
2026-08-02 04:10:01 -04:00
BZLZHH 6de38c666c [Feat] (DirectGLES): support rectangle textures where the emulation is exact
ES has no rectangle target and no rectangle sampler, so DirectGLES declared
GL_TEXTURE_RECTANGLE unsupported outright: the texture was never synced or bound,
and SPIRV-Cross refused the shader ("Rectangle textures are not supported on
OpenGL ES") which left the whole program unlinkable.

A rectangle texture is a single-level, clamped 2D texture whose only real
difference is that its lookups take non-normalized coordinates. Where every use
takes *integer* texel coordinates - texelFetch, textureSize - that difference
does not exist at all, and the two are the same thing. So:

- A new SPIR-V pass rewrites Dim::Rect image types to Dim::2D before
  transpiling, and restates the rectangle capabilities as Shader. It declines
  any module containing a normalized-coordinate lookup rather than emitting
  something subtly wrong; SPIRV-Cross then rejects that module exactly as
  before, so nothing that used to work changes and nothing new renders wrongly.
- The target maps to GL_TEXTURE_2D for storage, uploads and binding, alongside
  the existing 1D and 1D-array emulation.

Fixes KHR-GL31.texture_size_promotion.functional outright, which takes GL31 to
100% conformance. GL32/GL33 advance past their rectangle cases to a separate
GL_RGB16 multisample issue. No regressions across texture_swizzle, shaders30,
texture_lod_*, framebuffer_blit, packed_depth_stencil, transform_feedback,
clip_distance or draw_buffers; DirectVulkan re-verified unaffected.
2026-08-02 01:56:22 -04:00
BZLZHH 9dff24f3e1 [Fix] (DirectGLES): never leave a stale program bound when the new one is broken
When a shader stage fails to transpile or compile, SyncToBackend logs it and
carries on, so the program is linked without that stage - or does not link at
all. Use() then issued glUseProgram for it, which is an INVALID_OPERATION for an
unlinked program and, crucially, leaves the *previous* program current. The draw
went ahead and rendered with an entirely unrelated shader.

That is how KHR-GL3x.texture_size_promotion's GL_TEXTURE_RECTANGLE cases
produced 1.0 for a red channel: SPIRV-Cross refuses sampler2DRect for ESSL
("Rectangle textures are not supported on OpenGL ES"), so every rectangle
program was broken, and the draws kept running the previous case's 1D-array
alpha shader - whose alpha is 1.0. Wrong pixels from a shader the app never
bound are far worse to debug than a blank result.

The program now records whether the last sync produced something usable, and
Use() binds 0 rather than the broken program, making the draw a visible no-op.
The redundancy cache tracks whatever was actually bound, so it stays correct
across the switch.

Does not fix the rectangle cases themselves - those need a SPIR-V pass lowering
Dim::Rect to Dim::2D before SPIRV-Cross runs (plus the coordinate divide for
non-texelFetch lookups), alongside mapping the target to GL_TEXTURE_2D.
2026-08-02 01:40:53 -04:00
BZLZHH 5a400e0297 Merge branch 'dev' of github.com:MobileGL-Dev/MobileGL into dev 2026-08-02 12:21:25 +08:00
BZLZHH b6a2bf08d4 [Fix] (DirectGLES): resolve an aliased texture unit by the sampler's type
Desktop GL_TEXTURE_1D/1D_ARRAY are emulated on ES GL_TEXTURE_2D/2D_ARRAY, so one
native binding serves two of a unit's frontend slots. An earlier fix settled the
real-versus-default case; two REAL textures can collide just as easily, and there
the slot iteration order decided it. KHR-GL3x.texture_size_promotion keeps its 1D
source texture and its 2D destination texture bound to the same unit, so the
shader sampled the render target it was drawing into instead of the source.

GL resolves this from the shader's sampler type, so ask the program: the
frontend's uniform reflection still carries the original GLSL type, which maps
straight back to the target the lookup means. Only consulted when a collision
actually happens, so an ordinary unit costs nothing, and the first binding
placed stands when the program gives no answer rather than being overwritten by
whichever slot happens to come last.

Also adds the read-colour clamp that goes with it: GL clamps a glReadPixels from
a fixed-point colour buffer to [0,1] (GL_CLAMP_READ_COLOR defaults to
GL_FIXED_ONLY), which ES has no equivalent for at all - a GL_R16_SNORM target
holding -0.125 read back unclamped. Applied to the wide rows before they are
repacked, for float, half, short and byte reads alike, and deliberately NOT for
glGetTexImage, which reaches the same helper through a scratch framebuffer but
is not subject to read-colour clamping.

texture_size_promotion now clears every 1D case (it stops at the first failure
and has moved on to GL_TEXTURE_RECTANGLE, which DirectGLES does not emulate at
all yet), and KHR-GL33.texture_swizzle's GL_DEPTH_COMPONENT32 1D cases pass.
DirectVulkan re-verified unchanged.
2026-08-01 16:21:28 -04:00
BZLZHH 6b2a2b5e00 [Fix] (MG_Util): emulate GL_DEPTH_COMPONENT32 with the 24-bit sized format
GL_DEPTH_COMPONENT32 has no ES equivalent. The previous commit routed it to
GL_DEPTH_COMPONENT32F, which gives the attachment storage but changes the
encoding: the transfer type has to become GL_FLOAT for ES to accept the store,
and the upload path hands over the caller's fixed-point GL_UNSIGNED_INT bytes
unchanged, so the texels came out as garbage.

GL_DEPTH_COMPONENT24 is the nearest sized ES format that keeps the same
fixed-point encoding, so GL_UNSIGNED_INT still describes the data and no
conversion is needed. Fixes KHR-GL33.texture_swizzle's GL_DEPTH_COMPONENT32
cases on the 2D and 2D-array targets; framebuffer_blit's GL_DEPTH_COMPONENT32
config still passes, since the depth values it compares are exactly
representable in 24 bits.

(The 1D and 1D-array targets still fail, but for the separate desktop-1D-on-ES
emulation reason that also holds back texture_size_promotion.)
2026-08-01 14:59:19 -04:00
BZLZHH 512c857f18 [Fix] (DirectGLES): emulate a format-converting multisample resolve blit
ES rejects any blit out of a multisample read framebuffer whose format differs
from the draw framebuffer's. Desktop GL only requires identical formats when
BOTH framebuffers are multisampled - a multisample resolve into a single-sample
target is allowed to convert on the way out, and KHR-GL3x.framebuffer_blit
resolves an R8 multisample texture straight into the RGBA8 default framebuffer.
The forwarded blit failed with GL_INVALID_OPERATION, and since the driver's
error never reaches the frontend error queue the caller saw a successful call
that had written nothing.

Retried in two steps when the first blit fails and the read framebuffer really
is the multisampled one: resolve into a scratch renderbuffer of the source's own
format, then run the caller's blit from there - single-sample on both sides,
which is exactly where ES does allow the conversion. The scratch buffer is
cached and grown on demand, keyed on the source format and dropped with its ES
context. Only reached on the failure path, so an ordinary blit is untouched.

KHR-GL3{0,1,2,3}.framebuffer_blit is now 3/3 on all four versions; DirectVulkan
(lavapipe) re-verified at 3/3 as well.
2026-08-01 14:37:58 -04:00
BZLZHH 9f3cac6691 [Fix] (DirectGLES): report distinct depth/stencil framebuffers as unsupported
GL only requires framebuffers whose depth and stencil attachments refer to the
same image; anything else may be answered GL_FRAMEBUFFER_UNSUPPORTED, and both
backends' real targets do exactly that - DirectVulkan cannot form two separate
attachments at all, and the ES drivers behind DirectGLES return UNSUPPORTED for
a separate depth renderbuffer plus stencil renderbuffer.

The frontend already knew how to detect the configuration, but only consulted it
for DirectVulkan. On DirectGLES it answered GL_FRAMEBUFFER_COMPLETE for a
framebuffer the driver had rejected, so every clear and draw against it was
silently dropped and the results read back as zeros - which is what
KHR-GL3x.packed_depth_stencil.verify_mixed_attachments saw. (That test
explicitly tolerates GL_FRAMEBUFFER_UNSUPPORTED; what it cannot survive is being
told the framebuffer works.)

Turned into a backend capability rather than a backend-type check, probed once
at init from a scratch framebuffer the same way the format-capability cache is,
so a driver that does support the configuration keeps using it. Defaults to
supported, leaving any backend that does not set it on the permissive path.

Fixes KHR-GL3{2,3}.packed_depth_stencil.verify_mixed_attachments for both
formats; DirectVulkan re-verified unchanged at 23/25 pass + 2 not-supported.
2026-08-01 14:05:28 -04:00
BZLZHH 4c7332d5e6 [Fix] (DirectGLES): broadcast legacy gl_FragColor to every draw buffer
Legacy GLSL's gl_FragColor goes to every enabled draw buffer (GL 4.6 15.2.3),
but ShaderSourceProcessor lowers it to a single mg_FragColor output, which only
ever reaches draw buffer 0. Everything past the first attachment kept its
pre-draw contents.

Replicated across the enabled draw buffers with copies at the end of main.
Gated on the count so the ordinary single-target shader is byte-for-byte what it
was: the pass is a no-op below two draw buffers, and the count comes from the
frontend draw framebuffer at program-sync time (not from the backend framebuffer
sync, which only runs later in PrepareForDraw - a program compiled against a
stale count would not be relinked until the draw after the one that needed it).
It joins the snorm/unorm clamp masks as framebuffer state the shader is compiled
against, with the same relink-on-change check.

Also advertises GL_ARB_explicit_attrib_location and GL_ARB_texture_multisample,
which DirectGLES implements for every version it advertises but only listed for
DirectVulkan. Both are core from GL 3.2/3.3 on, so an app targeting 3.0/3.1
reaches them only through the extension string - without the former the CTS
picks an entirely different draw_buffers shader, and without the latter
KHR-GL31.texture_size_promotion.functional crashed outright.

KHR-GL3{0,1,2,3}.draw_buffers.draw_buffers_1 now passes on all four versions,
and texture_size_promotion.functional on GL31 downgrades from a crash to a
(still open) comparison failure.
2026-08-01 13:59:11 -04:00
BZLZHH 1c5f6c0986 [Chore] (tools/cts): pick a run config the suite does not contradict
Two harness settings were producing failures that say nothing about the backend:

- dEQP's FboRenderContext picks the first entry of its own depth/stencil format
  list, GL_DEPTH32F_STENCIL8, when the config leaves the bit counts DONT_CARE.
  framebuffer_blit meanwhile hardcodes GL_DEPTH24_STENCIL8 for its own buffers
  as soon as it detects an FBO surface, and then blits depth between the two -
  which the spec forbids for mismatched formats, so a conformant driver has no
  choice but to fail it. Default to --deqp-gl-config-name=rgba8888d24s8 so the
  wrapper framebuffer and the test agree.

- --deqp-watchdog aborts the whole process when one case exceeds a hardcoded 30
  seconds (framework/common/tcuApp.hpp). That is not a hang on a CPU rasterizer:
  several texture_swizzle cases take ~17s each standalone and cross the limit
  once the process is warm, which came back as ten spurious Timeouts. dEQP's own
  default is off, and --chunk-timeout is what actually rescues a genuinely
  wedged case, so default it off too and leave it selectable.
2026-08-01 13:48:58 -04:00
BZLZHH 8b75628dec [Fix] (DirectGLES): depth/stencil clear value and readback gaps
Three separate holes, all of them silent, that KHR-GL3x.framebuffer_blit walks
straight into because it clears and reads back depth and stencil directly:

- glClearStencil was frontend-only. The value was recorded in render state and
  never synced, so the real driver kept its default of 0 and every
  glClear(GL_STENCIL_BUFFER_BIT) wrote zeros. glClearColor and glClearDepthf
  were already synced right next to it.

- Stencil readback assumed GL_STENCIL_INDEX works. It is not part of core ES
  (it needs GL_NV_read_stencil) and a driver without it rejects the read
  outright, which left the caller's buffer untouched. Where the attachment is a
  combined depth-stencil buffer the packed GL_DEPTH_STENCIL read carries the
  same bytes in its low octet, so that is now the fallback; the widening to
  GL_UNSIGNED_SHORT/INT moved into the same helper, since even a byte-for-byte
  read needs it.

- Depth readback always went through GL_UNSIGNED_INT. A floating-point depth
  attachment (GL_DEPTH_COMPONENT32F, GL_DEPTH32F_STENCIL8 - the latter is what
  dEQP's own fbo-surface-type wrapper framebuffer picks) rejects that with
  GL_INVALID_OPERATION and only reads back as GL_FLOAT. Try both.

And one format gap behind the same test: GL_DEPTH_COMPONENT32 has no ES
equivalent and was being normalized to the *unsized* GL_DEPTH_COMPONENT base
format, which is not a legal glTexStorage/glRenderbufferStorage internal format
there - the attachment ended up with no storage and the framebuffer read back as
incomplete. GL_DEPTH_COMPONENT32F is the sized ES format that keeps the
requested 32-bit depth footprint; the transfer type follows it to GL_FLOAT.

Takes KHR-GL3x.framebuffer_blit from 0/3 to 2/3 (the remaining
multisampled_to_singlesampled_blit_color_config_test is a separate
single-channel MSAA resolve issue). Note that scissor_blit additionally needs
the suite to run with a depth/stencil config the test agrees with
(--deqp-gl-config-name=rgba8888d24s8): under FBO surfaces the test hardcodes
GL_DEPTH24_STENCIL8 for its own buffers while dEQP's wrapper framebuffer
defaults to GL_DEPTH32F_STENCIL8, and blitting depth between mismatched formats
is a spec error that any conformant driver has to report.
2026-08-01 13:24:30 -04:00
BZLZHH 8269a1786f [Feat] (DirectGLES): emulate GL_TEXTURE_LOD_BIAS in the transpiled ESSL
ES has no per-texture or per-sampler LOD bias at all - GL_TEXTURE_LOD_BIAS is
desktop only, and Vulkan spells it VkSamplerCreateInfo::mipLodBias, which is why
DirectVulkan already honours it. DirectGLES stored the value in sampler state
and then dropped it, so every lookup sampled at the unbiased level of detail.

The bias now reaches the shader as a uniform: a new SPIRV-Cross post-pass
declares one `uniform highp float mg_lodBias_<sampler>;` per mip-capable sampler
and folds it into the level of detail of every lookup that has somewhere to put
it - appended as the bias argument, added to an existing bias, or added to an
explicit textureLod level (Vulkan applies mipLodBias to explicit-LOD fetches too,
and the CTS reference expects the same). texelFetch/textureGather have no bias
by definition, textureGrad offers no argument to fold one into, and the
array-shadow lookups have no bias overload in GLSL at all, so all of those are
left alone. Draws push the bound texture's (or the bound sampler object's, which
overrides it as in GL) value into the uniform, and only when it changed - a
shader whose samplers all have a zero bias issues no extra call at all.

Fixes KHR-GL3{0,2,3}.texture_lod_bias.texture_lod_bias_all.
2026-08-01 13:24:12 -04:00
BZLZHH 3019c68945 [Fix] (MG_Util): glBindBufferBase must not freeze the buffer's size
BindBufferBase_State stored Range1D(0, bufferObject->GetSize()) as the binding
point's range, so the range reflected whatever size the buffer happened to have
at bind time. Binding an empty buffer and giving it storage afterwards is
ordinary application code - glGenBuffers / glBindBufferBase / glBufferData is
exactly the order KHR-GL3{0,2,3}.clip_distance.coverage uses - and the binding
then stayed frozen at [0, 0).

Every backend consumer reads GetRange() as the range the binding actually
covers, so the stale window meant the capture buffer was bound with
glBindBufferRange(..., 0, 0) instead of glBindBufferBase, transform feedback
captured nothing, and the test read back its pre-draw zeros. The same stale
range also under-counted the CPU-side transform feedback capacity accounting.

GL resolves a whole-buffer binding against the object's size at every use;
only glBindBufferRange pins a fixed window, and the binding point already
tracked which of the two it was for the glGetIntegeri_v START/SIZE queries.
GetRange() now resolves the non-explicit case dynamically.

Fixes KHR-GL3{0,2,3}.clip_distance.coverage on Espryt; transform_feedback stays
21/21 on all four versions, and DirectVulkan (lavapipe) re-verified unaffected.
2026-08-01 12:30:46 -04:00
BZLZHH 800142c104 [Fix] (DirectGLES): stop the default texture clobbering an aliased real binding
Desktop GL_TEXTURE_1D/1D_ARRAY have no ES equivalent and are emulated on
GL_TEXTURE_2D/2D_ARRAY, so one native binding serves two frontend slots of the
same texture unit. BindCurrentTextures walked the slots in enum order and let
the last one win, which is wrong as soon as one of an aliased pair holds a real
texture and the other holds the unit's default (name 0) object: the default
would be bound over the real texture and the shader sampled an empty texture,
which GL resolves to opaque black.

The default is only skipped while it has never been given an image, so this
needed nothing more than some earlier test in the same glcts process defining
one on texture name 0 - after which every later case that sampled a 1D texture
returned black. That is the mechanism behind a whole family of failures that
only reproduced when another case ran first: texture_lod_basic.lod_selection,
packed_pixels.varied_rectangle.rgba4_format_bgra, shaders.arrays.{return,
unnamed_parameter}.float_vertex and clip_distance.functional all pass in the
full-suite ordering now.

Resolved with a second pass, mirroring the intent the unbind half of the
function already had ("a default alias must not clear a real binding"): real
textures are placed first, then defaults fill only the native targets nothing
else claimed.
2026-08-01 12:07:23 -04:00
BZLZHH e9382f5329 [Fix] (DirectGLES): exact transform feedback primitive queries
Two leftovers from the capture passthrough, both only observable with a
geometry shader in the pipeline:

- GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED fell back
  to the frontend's CPU accounting, which counts the primitives the draw call
  assembles and so cannot see a geometry stage's amplification. Both are core ES
  query targets (GL_PRIMITIVES_GENERATED from 3.2 on, gated accordingly so an
  older driver doesn't get a stray GL_INVALID_ENUM), so they now go straight to
  the driver's own counters. Generalized the occlusion-query handle's isOcclusion
  flag into the glBeginQuery target it already had to remember for glEndQuery,
  which is what tells the result read to use the core 32-bit getter.

- FixupGsStripCaptureOrder rewrites captured strip triangles from Vulkan's
  (i, i+1, i+2) order into GL's (i+1, i, i+2). A driver-side capture already
  emits GL order, so the rewrite corrupted it - KHR-GL33.transform_feedback
  .geometry read back the odd triangle rotated one vertex. Skipped when the
  backend owns the capture span.

KHR-GL3{0,1,2,3}.transform_feedback is now 21/21 on Espryt; DirectVulkan
(lavapipe) re-verified at 21/21 for the shared frontend change.
2026-08-01 11:57:18 -04:00
BZLZHH df7d1edeca [Feat] (DirectGLES): implement transform feedback capture
Transform feedback was frontend-only on DirectGLES: glBeginTransformFeedback
just flipped MobileGL's own capture state and the real ES driver was never told
to capture anything, so every capture buffer read back as whatever it held
before the draw (zeros for a fresh glBufferData(NULL)). DirectVulkan drives its
capture from its own draw recording, so the shared GLFunctionsTable had no
entries for the span at all.

Capture now runs on the real driver:

- The backend program declares the capture set with glTransformFeedbackVaryings
  before it links. SPIRV-Cross keeps user output names verbatim in the
  transpiled ESSL, so the frontend's requested names carry over unchanged.
- New GLFunctionsTable Begin/EndTransformFeedback entries hand the span
  boundaries to the backend (null for DirectVulkan, which is unaffected).
- The driver-side begin is deferred to the first draw of the span: ES needs the
  capturing program current and the capture buffers bound, and both only become
  true once PrepareForDraw has run. A span that never draws never touches the
  driver, which is what the GL semantics amount to anyway.
- The end mirrors the captured ranges back into the frontend buffer shadows -
  the GPU wrote them behind the frontend's back, so MapBuffer/GetBufferSubData
  would otherwise still return the pre-draw bytes.

Takes KHR-GL32.transform_feedback from 13/21 to 19/21; the two remaining
failures are the geometry-amplified primitive queries, which still go through
the frontend's CPU accounting.
2026-08-01 11:51:19 -04:00
BZLZHH 8025745fa3 [Chore] (Version): bump version to 26.8 for android plugin 2026-08-01 23:46:58 +08:00
BZLZHH 4a533a215a [Chore] (MG_Backend): fix compiling error. 2026-08-01 23:21:35 +08:00
BZLZHH 0a5d7ceb6c [Feat] (DirectGLES): implement GL_ANY_SAMPLES_PASSED occlusion queries
DirectGLES never registered BeginOcclusionQuery/EndOcclusionQuery, so
the frontend rejected the occlusion query targets entirely; the CTS
tests that use them (e.g. packed_depth_stencil.verify_partial/mixed_
attachments) left a stray GL_INVALID_ENUM that a later, unrelated
glGetError() check would report as its own failure
("Uploading buffer data failed" at gluDrawUtil.cpp:363).

Occlusion queries are core ES3 (glGenQueries/glBeginQuery(GL_ANY_
SAMPLES_PASSED, ...)/glEndQuery/glGetQueryObjectuiv), unlike the timer
queries which need GL_EXT_disjoint_timer_query, so they're wired up
unconditionally (independent of MOBILEGL_DISABLE_TIMERQUERY) using the
same handle-based GetQueryResult64/DeleteBackendQuery plumbing already
shared with timer queries. GetQueryResult64 now reads the 0/1 result
through the core 32-bit glGetQueryObjectuiv getter for occlusion
handles instead of the timer-only 64-bit GL_EXT_disjoint_timer_query
getter, since a driver can fully support core occlusion queries while
lacking that extension entirely.
2026-08-01 11:20:38 -04:00
BZLZHH ac81185968 [Fix] (DirectGLES): implement GL_DEPTH_STENCIL readback for ReadPixels/GetTexImage
Neither ReadPixels nor GetTexImage recognized format=GL_DEPTH_STENCIL
(type GL_UNSIGNED_INT_24_8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV): it
matched none of the native-passthrough gates nor the color-channel
conversion mapping, so both silently no-op'd (logging a compiled-out
MGLOG_E) and left the caller's buffer untouched. Real GLES/GL drivers
already implement this readback natively, so widen the native-pair
gates to include it.

GetTexImage additionally always attached the source texture to its
scratch FBO as GL_COLOR_ATTACHMENT0, which a depth-stencil texture
cannot be (framebuffer-incomplete) - route it through the existing
EnsureDepthAttachment2D(..., withStencil=true) path instead and skip
the color-only glReadBuffer call for that format.

Fixes KHR-GL3{2,3}.packed_depth_stencil.verify_read_pixels,
verify_get_tex_image, and verify_copy_tex_image (which depends on
GetTexImage internally) for both depth24_stencil8 and
depth32f_stencil8.
2026-08-01 11:14:33 -04:00
BZLZHH 9e861f3f7a [Test] (tools/cts): extend the fbo-harness waiver to the Espryt renderer
Same test-methodology artifact as Magma (dEQP's fbo-surface-type
wrapper FBO being mistaken for the true default framebuffer by
ApiCoverageTestCase's ReadBuffer coverage sub-test) - the waiver's
renderer_list only matched "Magma*", so KHR-GL3{0,1,2}.api.coverage
still reported Fail under the DirectGLES (Espryt) backend. Add
"Espryt*" to the same waiver entry.
2026-08-01 10:48:11 -04:00
BZLZHH da6f75dbd1 [Fix] (DirectGLES): cap advertised GL_MAX_SAMPLE_MASK_WORDS to 1
MobileGL's sample-mask state is a single 32-bit word (RenderState::
SampleMaskValue) and SampleMaski_State() hard-rejects any maskNumber
other than 0. DirectGLES forwarded the real underlying driver's
GL_MAX_SAMPLE_MASK_WORDS unmodified (NVIDIA's GLES driver reports 2),
so dEQP's per-test-case gluStateReset - which always calls
glSampleMaski up to that reported word count - hit GL_INVALID_VALUE on
word 1 after every single case and aborted the whole glcts process.
Each restart only got through one more case before repeating, which
run_cts_local.py recorded as a wall of per-case crashes (63 in
packed_pixels.rectangle alone) and tripped its "many empty chunks"
abort heuristic partway through the GL32 suite. 1 is the spec-required
minimum and is what MobileGL actually implements, so cap to it instead
of forwarding the raw driver limit.
2026-08-01 09:59:26 -04:00
BZLZHH 951da362f7 [Fix] (submodules): point include/FastSTL at the fork's already-pushed fix
Our local-only commit f8567f6 for the erase(iterator) bug was never
pushed to MobileGL-Dev/FastSTL and broke CI's submodule checkout
("not our ref"). The fork's own main branch already carries an
equivalent fix (022211c, same root cause) plus a perf improvement on
erase(key) (34f55f9), so switch to that instead of pushing a redundant
duplicate that would diverge from it.
2026-08-01 09:25:01 -04:00
BZLZHH 4c929b9b3f Merge branch 'dev' of github.com:MobileGL-Dev/MobileGL into dev 2026-08-01 21:03:14 +08:00
BZLZHH 8d5072543a [Test] (tools/cts): waive api.coverage's fbo-surface-type wrapper-FBO artifact
KHR-GL3{0,1,2,3}.api.coverage's ReadBuffer coverage sub-test captures
GL_READ_BUFFER while dEQP's own fbo-surface-type wrapper FBO is bound
(a real, non-zero-named FBO, not framebuffer 0), then later deletes an
unrelated FBO of its own. Per the GL spec, deleting a bound FBO
implicitly rebinds framebuffer target 0 - the true default framebuffer
this time, not the wrapper - and restoring the captured
GL_COLOR_ATTACHMENTn value against it correctly raises GL_INVALID_ENUM
(only FRONT/BACK-style tokens are valid there). This is a spec-correct
response to a --deqp-surface-type=fbo-only test-methodology artifact,
not a MobileGL conformance defect, and cannot occur on a real
window/pbuffer-backed run where framebuffer 0 is genuinely bound
throughout. Add a waiver (dEQP's own mechanism for exactly this kind of
known non-defect) instead of weakening the (correct) validation, and
wire --waiver-file through run_cts_local.py.
2026-08-01 08:53:35 -04:00
BZLZHH 5de2b9e3e9 [Chore] (Version): bump version to 26.8 2026-08-01 20:35:31 +08:00
BZLZHH 9192d156d1 [Fix] (DirectVulkan): ReadPixels materializes pending clears before resolving the blit binding
ResolveColorBlitBinding cached a RenderbufferResource*/TextureResource*
(trackedLayout) before the pending-clear materialization step ran. For an
attachment that had never been part of any render pass yet (e.g. a
GL_NONE draw buffer slot read back via an explicit glReadBuffer), the
materialize call was the first thing to touch its resource, and creating
that entry in the UnorderedMap (FastSTL, open-addressing) can rehash and
invalidate every previously-taken pointer into the map - including the
one just cached. The read then saw a stale VK_IMAGE_LAYOUT_UNDEFINED and
silently bailed (via a compiled-out MGLOG_E in release builds), leaving
the client buffer untouched. Reordering so the clear is materialized
first, then the binding resolved, guarantees the pointer reflects the
final resource state. Fixes KHR-GL3{0,1,2,3}.draw_buffers.draw_buffers_1.
2026-08-01 08:09:49 -04:00
BZLZHH 27cdfbc0ca [Fix] (DirectVulkan): advertise GL_ARB_explicit_attrib_location unconditionally
layout(location=N) out qualifiers are fully supported (glslang parses them,
SPIR-V expresses them natively), but the extension string was never
advertised. KHR-GL3{0,1,2}.draw_buffers.draw_buffers_1 builds its MRT
fragment shader with per-attachment layout(location=i) outputs only when
GL_ARB_explicit_attrib_location is reported or the context is >=3.3; below
that it fell back to a single non-indexed `out vec4`, which only ever
targets location 0, leaving every draw buffer past slot 0 unwritten.
2026-08-01 08:09:35 -04:00
BZLZHH 4567c3b468 [Fix] (DirectVulkan): advertise GL_ARB_texture_multisample unconditionally
GL_ARB_texture_multisample was implemented (glTexImage2D/3DMultisample,
GL_TEXTURE_2D_MULTISAMPLE) but never listed in BuildAdvertisedExtensions.
dEQP's GL 3.1 context loader only binds non-core-until-3.2 entry points
when the extension string is present, so glTexImage2DMultisample stayed
a null function pointer and KHR-GL31.texture_size_promotion.functional
crashed on the null call. GL 3.2+ contexts treat it as core and were
unaffected.
2026-08-01 08:09:20 -04:00
BZLZHH d18c6a1bae [Fix] (DirectVulkan): Xlib surface fallback for ICDs without VK_EXT_headless_surface
Real drivers (NVIDIA proprietary Linux) don't implement VK_EXT_headless_surface,
which the pbuffer path required unconditionally, hard-aborting at CreateInstance.
CreateInstance now detects instance-extension support and requests
VK_KHR_xlib_surface instead when headless is unavailable; CreateSurface creates
an unmapped Xlib window purely to obtain a VkSurfaceKHR, then proceeds through
the existing swapchain path unchanged. Shutdown destroys the window it owns.
Lavapipe and other headless-capable ICDs are unaffected.
2026-08-01 06:38:30 -04:00
BZLZHH dd745d7547 [Fix] (MG_State, MG_Impl): GL-order capture for geometry triangle strips
Vulkan transform feedback captures odd strip triangles as (i, i+2, i+1)
while GL table 10.1 decomposes them as (i+1, i, i+2). When the capture
stage is a triangle-strip geometry shader whose EmitVertex/EndPrimitive
sequence is statically knowable (no emission under control flow), link
time extracts the per-invocation strip lengths from the glslang AST, and
EndTransformFeedback rotates each odd triangle's captured vertex records
into GL order in place (bounded by the binding ranges' whole-triangle
capacity; raw input primitives tracked per Begin/End).
KHR-GL33.transform_feedback.geometry passes - the family is 21/21.
2026-08-01 03:17:30 -04:00
BZLZHH 4407be89cd [Fix] (MG_Impl): count multisample texture attachments in GL_SAMPLE_BUFFERS
The draw-framebuffer sample resolver only looked at renderbuffer
attachments, so framebuffers with multisample texture attachments
reported GL_SAMPLE_BUFFERS == 0 and callers took single-sampled paths
(the CTS blit helpers read multisampled attachments based on it).
2026-08-01 02:50:29 -04:00
BZLZHH 0c1a433af6 [Fix] (DirectVulkan): multisample resolve blits; per-buffer blit skip; RGBA-widened renderbuffers
Color blits from a multisampled source now use vkCmdResolveImage (both
blit resolvers carry the image sample count); a buffer named in the blit
mask but absent from either framebuffer skips just that buffer instead
of cancelling the whole blit (GL 4.6 18.3.1); and three-channel color
renderbuffers widen to their RGBA twin exactly like textures, so
renderbuffer<->texture blits of the same GL format see one VkFormat.
framebuffer_blit.multisampled_to_singlesampled_blit_color_config_test
passes - the whole framebuffer_blit family is green.
2026-08-01 02:46:16 -04:00
BZLZHH a2f3efe22c [Feat] (DirectVulkan): combined and scissored blits; cross-format depth-stencil blits
BlitFramebuffer now serves any GL_COLOR/DEPTH/STENCIL mask combination:
the depth/stencil aspects run as per-aspect image copies before the color
path, renderbuffer attachments materialize their pending clears like
texture ones, and the scissor test clips blit writes (destination rect
intersected, source shrunk proportionally). Depth copies between images
of different depth formats (a D24S8 renderbuffer into a
DEPTH_COMPONENT24 texture riding the D32_SFLOAT fallback) round-trip
through the host with a per-texel re-encode; stencil aspects pass
through raw since every packed format encodes S8. scissor_blit and
packed_depth_stencil.blit.* now pass.
2026-08-01 02:19:07 -04:00
BZLZHH b9a15aed61 [Feat] (DirectVulkan, MG_Impl): GPU transform feedback primitive queries
The TF primitive queries now ride VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT
pools when the device reports transformFeedbackQueries: each captured draw
is wrapped in a slot (shared between both GL targets when active
together), and results sum the (written, needed) pairs -
GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN from the first,
GL_PRIMITIVES_GENERATED from the second. This is exact through geometry
shaders, so KHR-GL33.transform_feedback.query_geometry_* pass; the CPU
accounting delta remains the fallback for backends without the feature.
2026-08-01 02:02:35 -04:00
BZLZHH f748a06632 [Fix] (3rdparty): glslang evaluates defined() expanded from macros
Pulls the glslang change that downgrades the 'defined in macro
expansion' diagnostic to a portability warning with normal evaluation.
KHR-GL33.shaders.preprocessor.conditional_inclusion.basic_2_* pass; the
whole preprocessor family (482 cases, including every negative
invalid_defined_* case) stays green.
2026-08-01 01:54:48 -04:00
BZLZHH 4532cae175 [Feat] (MG_Impl, MG_Util): STENCIL_INDEX8 renderbuffers; report distinct D/S renderbuffers unsupported
GL_STENCIL_INDEX8 becomes a first-class internal format (VK_FORMAT_S8_UINT
backing, metrics, classifiers, converters), so glRenderbufferStorage
accepts it instead of leaving GL_INVALID_ENUM behind. Framebuffer
completeness now also mirrors the renderer's gate for renderbuffers:
distinct depth/stencil renderbuffer attachments (or a renderbuffer
paired with a texture) report GL_FRAMEBUFFER_UNSUPPORTED - the spec only
requires the same-image case - instead of passing completeness and then
failing at draw/clear (verify_mixed_attachments.* now passes).
2026-08-01 01:50:34 -04:00
BZLZHH 107b56d603 [Feat] (DirectVulkan, MG_Impl): occlusion queries via Vulkan query pools
GL_SAMPLES_PASSED / GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) now work: every
app draw between Begin/EndQuery is wrapped in a slot of a host-reset
occlusion query pool (precise counts when occlusionQueryPrecise is
granted), and the result flush ends any active render pass before
submitting, waits, sums the slots and recycles them. ANY_* targets
report the boolean form; GL_QUERY_COUNTER_BITS and GL_CURRENT_QUERY
answer for the occlusion targets, and deleting an active query releases
its slot. Draw-time depth/stencil state also honors attachment absence:
a framebuffer without a depth (stencil) attachment behaves as if that
test always passes, even when a packed depth-stencil image is attached
through only one half (verify_partial_attachments.*).
2026-08-01 01:50:34 -04:00
BZLZHH 22b749dd37 [Fix] (MG_Util, DirectVulkan): canonical depth shadows with upload conversion
Depth textures previously raw-copied whatever the client handed over
into the Vulkan image, so any client format other than the image's exact
texel layout uploaded garbage (float DEPTH_COMPONENT data read as
16-bit words, GL_TEXTURE_1D/2D alike).

The shadow now has a defined canonical layout - unorm16 for
DEPTH_COMPONENT16, a full-scale unorm32 word for the 24/32-bit fixed
depths, float for DEPTH_COMPONENT32F - produced by the pixel-store
unpack converter (new DepthComponent channel mapping + UNorm32
component). GL_DEPTH_COMPONENT client data may also fill packed
depth-stencil internals (stencil half zero). The Vulkan uploader
converts shadow words to the image texel layout per aspect, and
X8_D24_UNORM falls back to D32_SFLOAT where optimal tiling lacks
support (lavapipe). texture_size_promotion.functional and
packed_depth_stencil.verify_copy_tex_image.* now pass.
2026-08-01 01:15:58 -04:00
BZLZHH f0c0211767 [Fix] (DirectVulkan): raw sRGB attachment writes while FRAMEBUFFER_SRGB is off
GL renders into sRGB color attachments RAW when GL_FRAMEBUFFER_SRGB is
disabled (the core-profile default), but Vulkan sRGB attachments always
encode on write - one decode went missing whenever a rendered-into sRGB
texture was sampled again (multisampled sRGB targets in
texture_size_promotion and texture_swizzle idx27/28 ms cases).

Attachment views (textures and renderbuffers) now reinterpret sRGB
images through their UNORM twin while the capability is off, switching
back when enabled: images get MUTABLE_FORMAT, the attachment-view cache
keys the view format, renderbuffers carry a second view, and the render
pass hash includes the capability state. Sampled views keep decoding.
The VkTextureManager.cpp half of this rides with the next commit.
2026-08-01 01:15:57 -04:00
BZLZHH 9ebbb76df1 [Fix] (DirectVulkan): clamp fixed-point ReadPixels to [0,1]
glReadPixels final conversion honors GL_CLAMP_READ_COLOR (default
GL_FIXED_ONLY): fixed-point normalized color buffers clamp to [0,1] on
read - visible for SNORM attachments, whose negative values previously
leaked through (texture_size_promotion SNORM cases). True float formats
stay unclamped unless the mode is GL_TRUE; GetTexImage is unaffected.
2026-08-01 01:15:57 -04:00
BZLZHH 95547ab9ce [Fix] (MG_Util): map GL adjacency primitives to their Vulkan topologies
GL_LINES_ADJACENCY / GL_LINE_STRIP_ADJACENCY / GL_TRIANGLES_ADJACENCY /
GL_TRIANGLE_STRIP_ADJACENCY fell through to the TRIANGLE_LIST default,
so adjacency draws assembled garbage. They now map to the matching
*_WITH_ADJACENCY topologies (adjacency vertices are discarded by Vulkan
when no geometry shader is active, matching GL semantics);
KHR-GL33.primitive_restart.restart_mode passes.
2026-08-01 00:30:01 -04:00
BZLZHH 8eaf2d0069 [Fix] (MG_Impl): create sampler objects at Gen; guard combined-format CopyTexImage
glGenSamplers creates the sampler objects themselves (unlike texture and
buffer names), so glIsSampler must answer GL_TRUE before any bind - the
names now get their state vectors at Gen time (KHR-GL33.api.coverage).

glCopyTexImage2D with a combined DEPTH_STENCIL internalformat now
requires both halves in the read framebuffer and reports
GL_INVALID_OPERATION when only the depth or only the stencil attachment
point is populated (packed_depth_stencil.validate_errors.*).
2026-08-01 00:27:05 -04:00
BZLZHH 54a8609c64 [Feat] (DirectVulkan): depth-stencil GetTexImage
The depth-stencil ReadPixels core (per-aspect copies + CPU repack) is
now shared, and glGetTexImage serves GL_DEPTH_COMPONENT /
GL_DEPTH_STENCIL / GL_STENCIL_INDEX queries of depth textures with it
instead of rejecting every non-color aspect
(packed_depth_stencil.verify_get_tex_image.* now passes).
2026-07-31 18:45:52 -04:00
BZLZHH 1fb0eb0737 [Fix] (MG_Util): GL_FLOAT_32_UNSIGNED_INT_24_8_REV is 8 bytes per pixel
The packed-type size table listed the D32F+S8 client format at 4 bytes,
so every GL_DEPTH32F_STENCIL8 upload copied only half its client data -
the top half of such textures stayed zero (packed_depth_stencil
verify_read_pixels/clear_buffer.depth32f_stencil8 now pass).
2026-07-31 18:43:30 -04:00
BZLZHH 282dd69230 [Feat] (DirectVulkan, MG_Impl): depth-stencil ReadPixels and combined-attachment queries
glReadPixels now serves GL_DEPTH_COMPONENT, GL_DEPTH_STENCIL and
GL_STENCIL_INDEX from the read framebuffer's depth/stencil attachment:
per-aspect vkCmdCopyImageToBuffer copies (4-byte-aligned stencil region)
with CPU repacking into GL_FLOAT / GL_UNSIGNED_SHORT / GL_UNSIGNED_INT /
GL_UNSIGNED_INT_24_8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV /
GL_UNSIGNED_BYTE layouts, honoring pack state and pixel-pack buffers.

GL_DEPTH_STENCIL_ATTACHMENT parameter queries follow the spec's combined
rules: differing depth/stencil attachment images (or a lone half) fail
with GL_INVALID_OPERATION, as does GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE
on the combined name. packed_depth_stencil.verify_parameters.* and
verify_read_pixels.depth24_stencil8 now pass.
2026-07-31 18:41:08 -04:00
BZLZHH e6ebe7078d [Fix] (MG_Util, MG_State): reject reserved GLSL identifiers glslang accepts
glslang parses "packed" and "row_major" as plain identifiers outside a
layout(...) list and accepts the reserved image*Shadow names outright.
A comment/preprocessor-aware pre-scan in the compile path now fails such
shaders with a proper info log, while layout(packed)/layout(row_major)
qualifier lists stay legal (uniform_block family still passes).
KHR-GL31/32/33.CommonBugs.CommonBug_ReservedNames now pass.
2026-07-31 18:27:11 -04:00
BZLZHH 30a91023f6 [Chore] (tools/cts): pin local runner surface size to 256x256
Without an explicit size dEQP's FboRenderContext sizes the wrapper FBO
to GL_MAX_RENDERBUFFER_SIZE (16384^2 here) and size-derived test
allocations explode - the multisampled depth blit config test alone
needs a 4 GiB depth texture on such a surface.
2026-07-31 18:21:28 -04:00
BZLZHH 47dd8cdc05 [Feat] (MG_Impl): answer format-derived framebuffer attachment queries
glGetFramebufferAttachmentParameteriv (and the DSA variant) now answer
GL_FRAMEBUFFER_ATTACHMENT_RED/GREEN/BLUE/ALPHA/DEPTH/STENCIL_SIZE,
COMPONENT_TYPE and COLOR_ENCODING from the attached image's internal
format, and accept the default-framebuffer attachment names (GL_DEPTH,
GL_STENCIL, GL_FRONT/GL_BACK variants). Querying them with no image
attached reports GL_INVALID_OPERATION per spec instead of
GL_INVALID_ENUM.
2026-07-31 18:21:28 -04:00
BZLZHH 8b36a15fb3 [Fix] (DirectVulkan): repair VK_VERIFY varargs and soften image-creation OOM
VK_VERIFY appended the caller's context format string to the base format
while the context ARGUMENTS expanded before the base arguments, so any
failing VK_VERIFY with context args formatted every conversion from the
wrong slot - the %s for VkResultToString dereferenced an integer arg and
crashed inside the logger. The context line is now its own log call
(XXHASH_VERIFY had the same defect).

vmaCreateImage failure in SyncTextureResource is now a soft failure like
the unsupported-sample-count path: a driver may pass the
vkGetPhysicalDeviceImageFormatProperties pre-check yet still refuse
creation (a 4-sample 16K depth texture on lavapipe is 4 GiB), and a GL
implementation must not abort on that.
2026-07-31 18:21:28 -04:00
BZLZHH 07fa84fb8d [Fix] (MG_State, MG_Impl): defer deletion of the program in use
glDeleteProgram on the current program now only flags it: the name (and
every glGetProgram* query) stays valid until the program stops being
current, at which point UseProgram frees the slot and releases orphaned
attached shaders. Previously the name died immediately, so a second
glDeleteProgram - as issued by common CTS utility teardown - recorded
GL_INVALID_VALUE that poisoned the next test iteration's build
(KHR-GL33.clip_distance.functional now passes its build phase).

glIsProgram/glIsShader piggyback on the same rule: a flagged name is
still a program/shader while it stays GL-visible, which resolves the
long-standing FIXMEs there.
2026-07-31 18:21:14 -04:00
BZLZHH a03817b4ee [Fix] (FastSTL): bump submodule for erase(iterator) out-of-bounds fix
Pulls the FastSTL fix for erase() iterator advancement: erase loops
(pending-clear GC, render-pass eviction, frame-transient drains) no
longer skip elements or walk past the bucket array. Root cause of the
order-dependent CTS batch segfaults (texture_lod_bias_all,
clip_distance.functional after ReadPixels, batch-order aborts).
2026-07-31 18:21:05 -04:00
BZLZHH 641bc0cdd9 [Feat] (MG_Impl): transform feedback primitive queries
glBeginQuery/glEndQuery now accept GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
and GL_PRIMITIVES_GENERATED. The result comes from CPU-side accounting:
every captured draw adds the primitives it assembles, clamped by the
capture buffers' remaining capacity in whole primitives (a full buffer
stops recording, which is exactly what PRIMITIVES_WRITTEN reports), with
the captured-vertex cursor resetting on glBeginTransformFeedback.

Draws without a geometry stage write exactly what they assemble, so this
is precise for them (KHR-GL33.transform_feedback.query_vertex_* now pass);
geometry amplification is not modelled yet and the query_geometry_*
variants still fail.
2026-07-31 17:40:42 -04:00
BZLZHH c069890ac7 [Feat] (DirectVulkan): GPU transform feedback capture via VK_EXT_transform_feedback
Second stage of GL 3.0 transform feedback: captured draws now write real
data.

- Device setup enables the VK_EXT_transform_feedback feature when present
  and loads the bind/begin/end entry points.
- Captured draws compile an XfbCapture program variant whose last
  vertex-processing stage gets XfbBuffer/XfbStride/Offset decorations from
  the program's resolved varyings (a new spirv-opt pass). A captured
  gl_Position is mirrored into a dedicated output written before every
  OpReturn - or before every OpEmitVertex in a geometry stage - ahead of
  the position fixup, so the captured value is the shader's own pre-remap
  position.
- DrawArrays/DrawElements wrap the draw in Begin/EndTransformFeedbackEXT;
  a small counter buffer resumes the append position across draws within
  one glBeginTransformFeedback (fresh Begin starts at the bound offsets).
- Capture targets are promoted to persistently-mapped host-coherent GPU
  storage (persistent-map storage now also carries the transform feedback
  usage), so MapBuffer/GetBufferSubData read the captured bytes after the
  fence wait glEndTransformFeedback now performs.
- Draw-mode/feedback-mode validation defers to the geometry shader's
  output primitive when one is present, and glGetBooleanv reports
  GL_TRANSFORM_FEEDBACK_ACTIVE/PAUSED so dEQP's per-case state reset can
  unwind an active capture.

KHR-GL33: transform_feedback capture_vertex_*/capture_geometry_*/
discard_*/draw_xfb and clip_distance.coverage now pass; queries
(PRIMITIVES_WRITTEN) and gl_ClipDistance capture remain.
2026-07-31 17:31:41 -04:00
BZLZHH 48dd1c5956 [Feat] (MG_State, MG_Impl): transform feedback state, validation and reflection
First stage of GL 3.0 transform feedback: glTransformFeedbackVaryings /
glGetTransformFeedbackVarying / glBeginTransformFeedback /
glEndTransformFeedback were unimplemented stubs. This adds

- per-program capture state: requested varyings apply on the next link and
  resolve against the last vertex-processing stage's linker objects (with
  gl_Position/gl_PointSize handled as builtins), failing the link on
  unknown or duplicate names or exceeded interleaved/separate limits, with
  offsets and strides computed per GL rules;
- context Begin/End state with the GL 3.3 error semantics: invalid
  primitive modes, redundant Begin/End, missing program or capture-buffer
  bindings, primitive-mode compatibility at draw time, and the
  while-active prohibitions on rebinding capture buffers, switching
  programs, and relinking the captured program;
- GetProgramiv TRANSFORM_FEEDBACK_* queries and a 4-slot bound on indexed
  GL_TRANSFORM_FEEDBACK_BUFFER binding points.

KHR-GL33.transform_feedback api_errors/linking_errors/get_xfb_varying now
pass; GPU-side capture is the remaining stage.
2026-07-31 17:08:37 -04:00
BZLZHH a389477f78 [Fix] (MG_Impl): report INVALID_OPERATION for shader names in program APIs
Program entry points answered GL_INVALID_VALUE whenever the name did not
resolve to a program, including names that exist but belong to a shader
object. Programs and shaders share one name space, so the spec (and
KHR-GL33.get_uniform_tests.get_uniform) requires GL_INVALID_OPERATION for
the shader-name case and GL_INVALID_VALUE only for names GL never handed
out, matching the interface-query helper's existing behavior.
2026-07-31 16:47:45 -04:00
BZLZHH 94a8f1e3f3 [Fix] (ShaderTranspiler): keep declared modern GLSL versions strict
Normalization rewrote every desktop core #version below 400 to 330 (and
400+ to 460), and a failed parse was retried at 460. Together these erased
the declared version's rules: KHR-GL33 negative-compile cases (reserved
names, parenthesized layout-qualifier values in a declared-420 shader,
GLSL 4.5 mix() overloads at 330, precise in struct members) all compiled.

Explicitly declared core versions >= 330 now keep their number, and the
460 retry only fires for sources whose directive carries the normalizer's
own legacy marker - i.e. shaders that declared 110-150 (or nothing), which
is the shader-pack compatibility case the retry exists for. Replaces the
narrower arrays-of-arrays special case.
2026-07-31 16:43:27 -04:00
BZLZHH 45d506545e [Fix] (MG_Util, DirectVulkan): tolerate storage-less attachments in component-size queries
GetComponentSizesForInternalFormat asserted on TextureInternalFormat::Unknown,
which framebuffer-parameter queries legitimately reach for attachments that
have no storage yet (KHR-GL33.packed_depth_stencil.validate_errors.initial_state
aborted there). Answer with all-zero sizes and keep a warning for genuinely
unhandled formats. Also include the image dimensions in the texture
vmaCreateImage failure report.
2026-07-31 16:35:03 -04:00
BZLZHH d9d63c9496 [Fix] (MG_Impl): answer ARB_transform_feedback3 limit queries
The GL CTS queries GL_MAX_TRANSFORM_FEEDBACK_BUFFERS and
GL_MAX_VERTEX_STREAMS before checking whether the extension is advertised
and requires no GL error (desktop drivers all accept these enums). Answer
with the separate-attrib capacity and a single vertex stream; the
transform_feedback3 tests then report NotSupported instead of failing on
GL_INVALID_ENUM.
2026-07-31 16:25:46 -04:00
BZLZHH 92140405c1 [Feat] (DirectVulkan): emulate GL_LINE_LOOP with closed indexed line strips
Vulkan has no LINE_LOOP topology and the frontend used to reject the mode
with GL_INVALID_OPERATION, which is itself non-conformant (several KHR-GL33
transform_feedback tests draw line loops and expect no error). DrawArrays,
DrawElements and DrawElementsBaseVertex now rewrite the draw into an
indexed GL_LINE_STRIP whose synthesized uint32 index list revisits the
first vertex, delivered through the client-memory index path (a new
forceClientMemory flag keeps a bound element-array buffer from hijacking
the synthesized pointer). Entry points without the rewrite degrade to an
open line strip instead of a triangle list.
2026-07-31 16:25:46 -04:00
BZLZHH b9ecfef0b6 [Fix] (DirectVulkan): handle renderbuffer attachments in color blit clears
BlitFramebuffer's color path asserted that the read framebuffer's source
attachment is a texture; a renderbuffer source (packed_depth_stencil.blit
color checks) aborted the process. Materialize pending clears through the
renderbuffer path for both source and destination, as ReadPixels already
does.
2026-07-31 16:15:50 -04:00
BZLZHH fba26ea169 [Fix] (DirectVulkan): round renderbuffer MSAA requests to supported counts
glRenderbufferStorageMultisample accepts any sample count up to MAX_SAMPLES
(including non-powers-of-two like 3) and promises at-least allocation, but
the renderbuffer path required an exact Vulkan sample-count match and failed
on devices like llvmpipe that expose 1x/4x only. Round the request up to a
power of two and then to the nearest count the device supports for the
format, cached per format so per-draw resolution does not re-query the
physical device.

Un-crashes KHR-GL33.packed_depth_stencil.blit.* (2x/3x MSAA renderbuffers).
2026-07-31 16:15:50 -04:00
BZLZHH 93a3b55907 [Feat] (DirectVulkan): upload combined depth-stencil texture data
UploadDirtyMipLevels used to skip D24S8/D32FS8 textures outright, leaving
glTexImage-supplied depth-stencil data unuploaded (KHR-GL33
texture_repeat_mode depth24_stencil8 and texture_swizzle depth-stencil
cases all sampled zeros). De-interleave the shadow's GL wire format into a
depth plane (X8_D24 word / float) and a stencil byte plane and record one
copy per aspect, with cross-conversion when the device backs the texture
with the other depth-stencil format.

Depth32FStencil8's shadow byte size also claimed 16 bytes/texel while the
stored wire format (GL_FLOAT_32_UNSIGNED_INT_24_8_REV) is 8; that mismatch
truncated every upload of it.

Also route a multisample-texture sample-count request through the device's
supported counts (round up, GL promises at-least semantics).
2026-07-31 16:15:50 -04:00
BZLZHH d1487bedf0 [Fix] (DirectVulkan): address array layers in mip upload copies
UploadDirtyMipLevels encoded a texture's GL depth into VkBufferImageCopy
imageExtent.depth with layerCount = 1. For array textures the layers live in
the image's arrayLayers, and extent.depth > 1 is invalid for 2D images - in
practice every layer past the first never received its data.

Route the third dimension into layerCount for 1D/2D/cube array images and
keep imageExtent.depth for genuine 3D images.

Fixes the KHR-GL33.pixelstoragemodes.teximage3d.* failures (110 cases) on
lavapipe.
2026-07-31 16:01:41 -04:00
BZLZHH 1bb736c57e [Fix] (ShaderTranspiler): keep arrays-of-arrays illegal below GLSL 430
The legacy-shader retry that retargets a failed parse to #version 460 also
re-legalized multidimensional arrays, which every desktop driver rejects
below 430 and KHR-GL33.shaders.arrays.invalid.* requires to fail. Skip the
retry when the original failure is glslang's arrays-of-arrays error; other
legacy rescues (e.g. layout(binding=...)) keep working.

KHR-GL33.shaders.arrays.invalid.multidimensional_array* now report the
required compile failure (4 cases).
2026-07-31 15:51:40 -04:00
BZLZHH eb76686c1e [Fix] (DirectVulkan): stop replaying consumed renderbuffer clears mid-pass
A cached RenderPassEntry bakes renderbuffer clear payloads inline into its
pendingClearAttachments, and that list outlives the clear's consumption at
pass begin (loadOp CLEAR). Every subsequent draw that reused the entry while
its pass was still active replayed the stale clear through
vkCmdClearAttachments, wiping the color and depth of everything drawn so far
in the pass.

Texture-keyed clears already re-checked the clear manager before clearing;
do the same for inline renderbuffer payloads: only clear while the
renderbuffer clear is still actually pending, and take the live payload so a
newer glClear's values win.

On lavapipe this takes KHR-GL33.shaders.fragdepth.* from 0/18 to 18/18; the
same defect hit any renderbuffer-FBO case with several draws per pass.
2026-07-31 15:51:40 -04:00
BZLZHH 1a04fb8c0c [Fix] (DirectVulkan): support client-memory indices in DrawElements
With no GL_ELEMENT_ARRAY_BUFFER bound, the IndexBufferView byte offset is a
raw client pointer (desktop drivers accept client-memory indices and the GL
CTS relies on this even in core contexts). UploadAndBindIndexBuffer used to
assert-crash the process there; it now snapshots the client index data into
a transient per-frame slice and binds that, matching how client-memory
vertex attributes are already streamed.

Fixes the process abort in KHR-GL33.transform_feedback.capture_* and every
other mustpass case that draws with client-side index arrays.
2026-07-31 15:10:01 -04:00
BZLZHH 306790ee7c [Feat] (tools/cts): add crash-resuming local-host glcts runner
Local counterpart of run_cts.py for desktop Linux runs: re-invokes glcts
with the not-yet-measured cases after a crash, quarantines timed-out cases
with the dEQP watchdog enabled, and records crashed/hung/unrun lists so a
partial run cannot read as a complete one.
2026-07-31 14:52:38 -04:00
BZLZHH 170ccda3e7 [Feat] (tools/cts): port dEQP MobileGL platform to desktop Linux
Guard the AImageReader window path behind __ANDROID__ and add a
mobilegl-desktop DEQP target so glcts can run against libMobileGL.so on a
Linux host via pbuffer surfaces (VK_EXT_headless_surface).
2026-07-31 14:50:18 -04:00
BZLZHH e86a9bbec5 [Chore] (MG_Backend): restore target GL version to 3.3 2026-07-31 16:45:19 +08:00
swung0x48 c5569e71b3 [Feat] (tools/cts): automate Windows WGL conformance runs 2026-07-30 23:00:13 -04:00
swung0x48 7e8c32a063 [Feat] (MG_Backend, MG_Impl): expose experimental GL 4.6 CTS limits 2026-07-30 23:00:13 -04:00
swung0x48 77bd03d962 [Chore]: remove unnecessary doc 2026-07-30 21:56:17 -04:00
swung0x48 37111ae992 [Perf] (DirectVulkan): snapshot-gated consecutive-draw fast path skips SetupDraw re-resolution 2026-07-30 09:40:54 -04:00
swung0x48 6b0c2a15ab [Perf] (DirectVulkan): reuse unchanged global-UBO slices and skip identical descriptor binds 2026-07-30 08:18:21 -04:00
swung0x48 e9ffd99313 [Perf] (DirectVulkan): bake the attribute location mask and memoize the explicit-LOD eligibility probe 2026-07-30 08:18:20 -04:00
swung0x48 7c01ddea0c [Perf] (DirectVulkan): drop per-draw weak-ptr locks, re-resolves and rebuilt masks from the sampled-texture and vertex paths 2026-07-30 08:01:07 -04:00
swung0x48 2d4d6e9cfb [Perf] (DirectVulkan): skip pending-clear probes through a lock-free empty check 2026-07-30 07:02:03 -04:00
swung0x48 76b8957b99 [Perf] (DirectVulkan): memoize sampled-texture resources across draws 2026-07-30 07:02:02 -04:00
swung0x48 ec685b9fa7 [Perf] (DirectVulkan): memoize resolved vertex-input state on the VAO and dedupe vertex/index binds 2026-07-30 07:02:01 -04:00
swung0x48 a12068df52 [Perf] (DirectVulkan): reuse pipelines across per-chunk buffers and skip redundant pipeline binds 2026-07-30 05:01:46 -04:00
swung0x48 0b344792cc [Fix] (DirectVulkan): stop fence-waiting out-of-band texture uploads; reclaim transients asynchronously 2026-07-30 05:01:46 -04:00
swung0x48 9fa32bdad0 [Fix] (DirectVulkan): declare only the used colour attachment span per subpass
- every render pass declared colorAttachmentCount=8 (the full GL draw-buffer
  slot span) with trailing VK_ATTACHMENT_UNUSED references, and Adreno
  configures its per-pixel render-backend/export path from the DECLARED
  count - so every fragment of every pass paid an 8-render-target export
  cost; this was the bulk of the 1.5x per-pixel gap against
  MobileGlues+ANGLE on the same Qualcomm driver (their subpasses declare
  exactly the used span)
- measured on Adreno 650 / MC 26.2 / 1440x3044: total GPU frame time
  11.9 -> 7.5 ms (-37%, now below ANGLE's 7.87 ms), the single-quad
  swapchain blit pass alone 1.26 -> 0.40 ms, steady in-world FPS 82.8 -> 123
  under the standard cooled-start protocol, matching the
  MobileGlues+ANGLE+system-Vulkan benchmark of 123.8
- trailing UNUSED references are popped before the subpass is built (the
  entry's colorAttachmentCount and every pipeline's colour-blend span follow
  it); interior GL_NONE holes keep their slots so fragment-output locations
  still line up
- the pipeline-side fragmentOutputMask check downgrades from assert to a
  debug log: an output at a location past the trimmed span is discarded,
  which is GL's defined behaviour for a draw buffer set to GL_NONE
2026-07-30 02:45:39 -04:00
swung0x48 a4980f2b56 [Fix] (DirectVulkan): skip redundant per-draw dynamic-state commands
- viewport, scissor, blend constants, depth bias, line width and the six
  stencil parameters were re-emitted unconditionally for EVERY draw (~1500
  vkCmdSet* per frame in MC 26.2, where ANGLE emits a handful), costing CPU
  record time and GPU command-processor work for values that almost never
  change between draws
- a recording-scoped shadow now drops any vkCmdSet* whose values match what
  the command buffer already holds; valid because every PipelineFactory
  pipeline declares the same eight dynamic states, so set values persist
  across those binds
- the shadow resets at every command-buffer (re)begin (dynamic state does
  not survive the boundary) and after binding the blit or depth-mipmap
  pipelines, whose narrower dynamic sets make the untouched states undefined
  and whose raw viewport/scissor writes bypass the shadow
2026-07-30 02:45:07 -04:00
swung0x48 8ca20e28ca [Fix] (DirectVulkan): size texture backings by their defined mip level count
- every non-MSAA texture was allocated with a full mip chain regardless of
  how many levels the GL texture actually defines, so MC's 3044x1440 main
  colour and depth render targets each carried 12 levels where ANGLE
  allocates one; a level-0-only texture now gets a single-level backing and
  upgrades to the full chain exactly once when a second level is first
  defined, through the existing preserve-copy recreation path
- saves a third of the memory of every mip-less texture and keeps
  single-level render targets off the multi-mip image layout entirely, which
  also removes the surface the Adreno 650 implicit-LOD overread workaround
  (ForceExplicitLod0SamplePass) exists to defend
- measured perf-neutral on Adreno 650 / MC 26.2 (the driver keeps full UBWC
  on multi-mip render targets), so this is a memory/robustness fix, not a
  speed one
2026-07-30 02:43:32 -04:00
swung0x48 c353a2055f [Feat] (DirectVulkan): pre-pass command stream for reorderable out-of-pass work
- a draw whose sampled texture needs out-of-pass work (deferred clear
  materialization or a sampled-layout transition) used to end the active
  render pass - a full-target store+reload on a tiler - even when the only
  ordering the work needs is 'before this draw'; MC 26.2 clears an overlay
  texture every frame and samples it mid-pass, splitting the main scene pass
  once per frame for nothing
- every frame slot now carries a second primary command buffer, submitted
  strictly AHEAD of the frame command buffer in the same vkQueueSubmit; when
  the open recording has not referenced the image yet (tracked via a
  recording-generation stamp on the texture resource, advanced on every
  frame-command-buffer begin and stamped at every recorded reference:
  attachments at BeginRenderPass/attachment-write, sampled reads per draw,
  layout transitions), the clear/transition is recorded there and the active
  pass stays open - ANGLE's outside-render-pass command stream, restricted
  to the provably reorderable case
- mid-frame flushes and readback submits close and carry the pre stream with
  the frame buffer (it must never be submitted later than the recording it
  was paired with), retiring both under the same submit index; dropped
  recordings (present suspension, swapchain recreation) abandon it
- MaterializePendingClearForTexture's no-active-render-pass assert now
  applies only to the frame command buffer, since the pre stream records
  while a pass is open on the frame buffer by design
2026-07-30 02:43:13 -04:00
swung0x48 421c20984e [Fix] (DirectVulkan): stop loading and carrying dead default-framebuffer content
- EGL swap semantics make the presented colour buffer's content undefined at
  its next acquire (EGL_BUFFER_DESTROYED, the implementation default) and
  every ancillary depth/stencil buffer's content undefined after ANY swap,
  yet the default-FBO render pass reloaded both with LOAD_OP_LOAD every
  frame; SwapchainObject now tracks per-image content validity (defined when
  a pass stores into the attachment, invalidated at present) and the
  render-pass manager turns an undefined attachment's tile load into
  LOAD_OP_DONT_CARE with initialLayout=UNDEFINED, keyed into both hashes so
  the cached LOAD variants cannot be hit by mistake
- the default framebuffer's depth attachment is now attached ON DEMAND: a
  draw with depth test and stencil test both disabled (GL: a disabled test
  neither reads nor writes its buffer), and no pending depth/stencil clear,
  resolves to a depth-less pass flavour, dropping the D24S8 tile load AND
  store outright - MC 26.2 renders its GUI into its own FBO and only ever
  blits colour to the default framebuffer, so its swapchain pass carried a
  full-screen depth round-trip for nothing
- the flavour only escalates: an active depth-full pass absorbs depth-less
  draws unchanged, while a depth-using draw against a depth-less pass
  resolves to an incompatible entry and splits, its depth loading DONT_CARE
  (the content was undefined all along); the depth-less flavour is folded
  into ComputeHash and the per-draw fast-path memo so the two flavours can
  never alias
2026-07-30 02:40:47 -04:00
swung0x48 fc4cd980f2 [Fix] (DirectVulkan): bound image mutability so Adreno keeps UBWC compression
- Every storage-capable colour texture was created MUTABLE_FORMAT, and Adreno
  gives up bandwidth compression on an image that may be viewed as any format in
  its compatibility class. MC's main render target therefore ran uncompressed;
  in a fill-bound scene that is the whole frame budget. Measured on Adreno 650,
  MC 26.2, same scene and camera, device cooled to 38-40C before each run:
  65.3 -> 80.9 fps (+23.9%), GPU busy ~93% in both.
- VK_KHR_image_format_list (enabled when present) fixes it without giving up
  mutability: VkImageFormatListCreateInfo names the exact formats a view may
  use, so the driver can keep the image compressed. The set must be exhaustive
  or the result is undefined - for sampled views it is exactly what
  ResolveSampledImageViewFormat can return over the three numeric domains.
- glBindImageTexture may name any compatible format, which cannot be enumerated
  ahead of time, so a texture bound to an image unit gets no format list. That
  is what VK_IMAGE_USAGE_STORAGE_BIT becoming on-demand is for: it makes
  "unmarked" mean "will never receive an arbitrary-format storage view", which
  is what makes the list sound. Removing STORAGE is worth nothing on its own
  (65.4 fps, measured) - only the mutability bound pays.
- MarkStorageImageTexture runs over every collected image-unit texture before
  the probe loop in PrepareStorageImageTextures, because that loop stops at the
  first texture needing work and would leave the rest unmarked. The mark makes
  NeedsStorageImagePreparation report true, which is what ends the render pass,
  so the recreate lands outside it.
- storageUsageResolved separates "not upgraded yet" from "this format can never
  carry STORAGE", so a format whose optimalTilingFeatures lack STORAGE_IMAGE
  cannot ask for a recreate that will never happen. SyncTexture's cross-draw
  early-out also has to break on a pending upgrade or the recreate never runs.
- An upgrade recreates the image and carries its contents forward through
  PreserveTextureContentsOnRecreate, which submits its own command buffer and
  waits. Whatever the frame already recorded into the old image is still
  unsubmitted, so that copy would read pre-frame content and this frame's
  rendering into the texture would be lost - exactly the render-target-then-
  image-unit case. PrepareStorageImageTextures now flushes first; it takes the
  FrameData rather than a command buffer because the flush retires the current
  one, and drops the sampled-descriptor-set memo that described it.
2026-07-29 07:07:50 -04:00
swung0x48 992d16267c [Fix] (DirectVulkan): rewrite implicit-LOD fragment samples to explicit LOD 0 when every bound sampler is pinned to a single mip level - Adreno 650 (driver 512.502) reads outside a full-screen colour render target's allocation on its implicit-LOD sampling path and faults the GPU, which killed MC 26.2 on its own blit shader (texture(InSampler, texCoord)) between frames 344-421 on every run; this is the same driver defect the default-framebuffer blit shader already works around with textureLod, but an application's shader cannot be edited, so ForceExplicitLod0SamplePass converts OpImageSample*ImplicitLod to the explicit form at the SPIR-V level under a new CompileOptionBit that is only requested when the rewrite provably cannot move a texel (every sampler binding on a single-level view, no anisotropy, and either a LOD clamp that already pins lambda at 0 or min and mag filters that agree - an explicit LOD 0 always takes the magnification side of the min/mag decision); a single-level view now also clamps its sampler to mipmapMode NEAREST with maxLod min(maxLod, 0.25) rather than 0, since collapsing the clamp would make every fragment magnify and quietly retire the min filter; and the program's backend hash memo grows from one slot to four so a program resolved under two compile-flag sets in the same frame stops re-hashing every stage's SPIR-V once per draw 2026-07-29 03:26:57 -04:00
swung0x48 0ea9e6de5f [Fix] (DirectVulkan): follow surface resizes instead of rebuilding the swapchain on VK_SUBOPTIMAL_KHR - a per-frame surface-capabilities comparison (ANGLE's model) is now the only thing that schedules a rebuild, so a launcher-side resolution change reaches the swapchain and the compositor scales the smaller image up to the view, while a driver that merely reports the surface as suboptimal can no longer rebuild every frame (each rebuild destroys every pipeline, resets the render-pass manager and reallocates the default framebuffer, which showed as flicker, then corruption, then a crash); the comparison runs in SURFACE space against the extent the live swapchain was created from, since comparing against the swapchain's own quarter-turn-swapped extent reports a difference on every rotated frame 2026-07-28 21:06:57 -04:00
swung0x48 241ed377b4 [Fix] (macOS): harden Cocoa context setup and isolate embedded glslang 2026-07-28 11:54:50 -04:00
swung0x48 bf312a4b67 [Fix] (DirectVulkan): explicit-LOD blit sampling and present-path hardening - the default-framebuffer blit shader now samples with textureLod 0 (a blit reads exactly the selected level; Adreno 650's implicit-LOD path reads past a single-mip UBWC render target's allocation despite maxLod=0, page-faulting the GPU on MC 26.2's second startup frame once the neighbouring startup staging memory is returned - the invalidated context then failed the next Present submit with EDEADLK/DEVICE_LOST), TransitionToPresent appends the present barrier into the frame's open recording instead of silently dropping it whenever anything was recorded (frames without a default-FBO render pass presented images stuck in their acquired layout), VK_SUBOPTIMAL_KHR acquires are treated as the success they are (image acquired, semaphore signal armed - the early return skipped the fence reset and consumed-flag clear, and callers re-acquired on the same binary semaphore; rebuilds now defer to after the signal is consumed), and validation builds report through VK_EXT_debug_report when VK_EXT_debug_utils is absent instead of aborting instance creation 2026-07-28 06:00:20 -04:00
swung0x48 56b31a9587 [Fix] (FastSTL): bump submodule for the erase(iterator) double-advance fix and add erase-while-iterating regression tests - the old semantics skipped one live element per erase and ran past end() when erasing the highest occupied bucket, sending the new mass pipeline-cache eviction sweeps off the bucket array (device crash on first eviction during world load: garbage handles fed to vkDestroyPipeline) 2026-07-27 23:50:25 -04:00
swung0x48 8a0a8a0274 [Fix] (DirectVulkan): harden the leak-fix round after adversarial review - pipeline memo now drops at every command-buffer boundary (a flush-loop-memoized pipeline could age out and be destroyed while its submission was in flight), mid-frame drains no longer rewind the arena or advance the cache-aging clocks in presenting apps (gated to every 8th drain since the last Present, so readback/fence-heavy frames neither churn conversions nor shrink the 1024-boundary retire window), render-pass eviction notifies the pipeline cache once per sweep batch instead of once per dying pass, descriptor pools use FREE_DESCRIPTOR_SET_BIT so a destroyed layout's cached sets are freed back and credited instead of abandoning pool slots (the live-layout age sweep that could orphan slots is removed - layout destruction is the sole purge path), and renderbuffer respecify parks the old backing for aged destruction instead of destroying it while possibly in flight 2026-07-27 22:51:26 -04:00
swung0x48 d076c29146 [Fix] (DirectVulkan): bound the vertex-input and sampler caches and sweep undeleted GL syncs - both caches age out entries idle >1024 frame boundaries (animated LOD bias no longer mints a VkSampler per float value, buffer/VAO churn no longer grows the vertex-input map for the whole session), and library teardown drains the live-sync registry exactly as glDeleteSync would since GL requires syncs to die with their context 2026-07-27 22:16:16 -04:00
swung0x48 930a607bdf [Fix] (DirectVulkan): make texture/renderbuffer GC reach every dead resource - name-deleted textures register via weak_from_this so first-sync-after-delete can no longer orphan a TextureResource, an orphan sweep makes GC authoritative over the resource map, dead-texture pruning moves to a frame-boundary gate (64 frames) so churn through clears/readbacks reclaims without draws, and dead renderbuffers age past frames-in-flight before their VkImage/view is destroyed instead of leaking until shutdown (or being freed while in flight) 2026-07-27 22:16:15 -04:00
swung0x48 34685b4bb0 [Fix] (DirectVulkan): age-based eviction for the content-addressed cache family - ProgramFactory entries (shader modules/layouts), PipelineFactory graphics pipelines, compute pipelines and per-layout descriptor-set tracking now retire after ~1024 idle frame boundaries (render-pass-manager sweep precedent), render-pass eviction purges pipelines hashed on the dying handle (closes a handle-recycling stale-pipeline hazard), and the program reflection cache is lifetime-id-keyed and cleared at EGL teardown - shader/program churn no longer grows Vulkan objects without bound 2026-07-27 22:05:25 -04:00
swung0x48 c540fb88ee [Fix] (DirectVulkan): drain frame transients on present-less paths - readback waits, suspended presentation, blocking sync waits and flush completion polls now run Present's per-frame drains (deferred buffer/texture releases, transient arena rewind, descriptor cursors, retired command buffers, conversion caches) whenever every submission is provably complete, so offscreen/minimized workloads stay bounded; never blocks, frames-in-flight overlap untouched 2026-07-27 21:45:21 -04:00
swung0x48 6ae3245a0d [Test] (CTS): raise the no-output abort threshold - consecutive instant-crash cases are real progress once device liveness is confirmed 2026-07-26 19:23:05 -04:00
swung0x48 7e048fc2bf [Fix] (DirectVulkan): map RGB10_A2(UI) to A2B10G10R10 - GL 2_10_10_10_REV puts R in bits 0-9 so the A2R10G10B10 mapping silently swapped R/B on upload; also decode both 1010102 variants in readback 2026-07-26 19:13:46 -04:00
swung0x48 83cdfd6bdd [Fix] (DirectVulkan): GetTexImage reads all 3D slices/array layers with PACK_IMAGE_HEIGHT/SKIP_IMAGES semantics, and sRGB readback returns raw sRGB-encoded bytes instead of linearizing 2026-07-26 18:30:43 -04:00
swung0x48 1c76f886cf [Fix] (DirectVulkan): back legacy low-bit formats (RGB565/RGB5A1/RGBA4/R3G3B2/RGB4/RGBA2/RGB10/12) with their UNorm8/16 canonical shadow layouts and add capability fallbacks - they mapped to VK_FORMAT_UNDEFINED and crashed or wedged the GPU on upload; also admit 2DMSArray/CubeMap/3D color attachment targets in the render pass 2026-07-26 18:30:42 -04:00
swung0x48 a2e109beff [Fix] (DirectVulkan): general (format,type) readback conversion - hoist the CTS-verified StoreWideRowsToClient into shared ReadbackImpl and decode any color VkFormat to wide RGBA rows; readback previously supported only RGB/BGR/RGBA/BGRA x UNSIGNED_BYTE/FLOAT and silently returned zeros for everything else 2026-07-26 18:30:41 -04:00
swung0x48 63f0756644 [Fix] (DirectVulkan): support UBO instance arrays as arrayed descriptors - uniform Block{...}b[N] reflected as one binding with descriptorCount=N, per-element GL block mapping, per-element buffer infos and dynamic offsets; non-UBO descriptor arrays now fail program creation cleanly instead of continuing corrupt 2026-07-26 18:30:41 -04:00
swung0x48 450215d12c [Fix] (DirectVulkan): implement color renderbuffer attachments - render pass/pipeline/blit/copy/readback/clear paths treated color renderbuffers as absent (writes masked to VK_ATTACHMENT_UNUSED, glClear dropped, readback zeros) 2026-07-26 18:30:40 -04:00
swung0x48 3a9e520170 [Test] (CTS): isolate the DirectVulkan renderbuffer-FBO readback defect so the rest of KHR-GL33 can be measured 2026-07-26 18:30:39 -04:00
swung0x48 d2996ba1cf [Test] (CTS): run VK-GL-CTS KHR-GL33 against MobileGL on Android via a standalone glcts binary 2026-07-26 18:30:39 -04:00
swung0x48 c8632dfefe [Test] (piglit-android): add on-device piglit harness for MobileGL - patched waffle (WAFFLE_EGL_LIBRARY/WAFFLE_GL_LIBRARY overrides so waffle drives libMobileGL.so directly, AImageReader-backed windows for DirectVulkan since Android ICDs lack VK_EXT_headless_surface, WAFFLE_FORCE_GL_CONTEXT_VERSION to upgrade piglit's low compat context requests to 3.3 core, meson cross fixes) and patched piglit (Android platform support, EGL support decoupled from the X11-dependent EGL tests, and a dispatch-init fix: the waffle resolvers were never installed because gl_fw is NULL during framework construction, so gl* silently bound to the system driver via the DT_NEEDED libEGL's eglGetProcAddress), plus the adb chunked runner with PIGLIT-result parsing, a results comparator, cross-file examples, and the piglit-on-android skill 2026-07-26 11:57:11 -04:00
swung0x48 b8a8a660e1 [Refactor] (Lifecycle): own MobileGL's lifecycle from the EGL layer instead of ELF static ctor/dtor - the first EGL/WGL entry point lazily initializes via a thread-safe, re-init-capable EnsureInitialized (AutoInit is gone), the last eglTerminate with no initialized display and nothing current tears the whole library down deterministically inside the EGL lifecycle, and the global singletons move to leak-at-exit heap storage so process exit runs no backend destructors at all (AutoDestroy and the Windows DllMain abandon hook are gone); fixes the exit-time SIGABRT from undefined static-destruction order - the DirectGLES buffer-pool mutex abort on Android clean exits, and the pre-existing macOS QueryTest/ProgramTest 'Subprocess aborted' gtest failures now pass (ctest 411/411) 2026-07-26 10:59:14 -04:00
swung0x48 7ab83861ca [Fix] (DirectVulkan): suspend presentation while the window is zero-area - a minimized window's out-of-date swapchain used to keep Present submitting on a signaled fence and presenting never-acquired images (adversarial review); also drop logging from the process-detach abandon path 2026-07-26 07:59:40 -04:00
swung0x48 eaeba556a3 [Test] (WGL): add manual Windows smoke tests - hand-rolled WGL bootstrap and GLFW-driven variant covering the zero-area helper-window path 2026-07-26 07:21:36 -04:00
swung0x48 72fa1221a5 [Fix] (DirectVulkan): survive zero-area windows at renderer init - skip the eager first acquire when RecreateSwapchain's minimize guard left no swapchain (GLFW's hidden helper window), and let Present bring the swapchain up once the window has real size 2026-07-26 07:16:11 -04:00
swung0x48 c4254c4bbd [Feat] (WGL): add Windows host layer - drop-in opengl32.dll with WGL over EGLImpl, Win32 window backend plumbing for both backends, ANGLE loader path, and leak-at-exit process teardown 2026-07-26 06:46:29 -04:00
swung0x48 199164c2e0 [Perf] (DirectGLES): route the per-upload GL_PIXEL_UNPACK_BUFFER unbinds through the unpack binding cache - the six texture-upload sites re-issued glBindBuffer(UNPACK, 0) on every upload; with the resting-0 shadow they now no-op after the first 2026-07-21 19:54:55 -04:00
swung0x48 e87063e90c [Fix] (DirectGLES): route default-framebuffer binds through the FBO-binding shadow - the raw glBindFramebuffer(0) in BindCurrentFBO/SyncAndBindFramebufferObject left the shadow claiming the previous user FBO, false-skipping its next re-bind and letting scoped guards restore a stale binding (caught by adversarial review); also scrub buffer-binding shadows when VAO client-attribute staging buffers are deleted, include cube-map arrays in the pack-image-params gate, and make the delete-recording test hook assertion-unwind safe 2026-07-21 19:54:54 -04:00
swung0x48 122da27249 [Fix] (DirectGLES): overhaul readback/copy/blit driver-state handling with shadow-backed RAII guards - pixel-PACK/UNPACK PBO binding caches resting at 0 (the old bind-then-query 'restore' left the user PBO bound forever, capturing later client-memory readbacks), a PACK pixel-store shadow replacing per-readback glGetIntegerv syncs, a driver FBO-binding shadow behind all scoped binders (per-instance prev slots, nest-safe), scratch-FBO attachment shadows that detach cross-aspect residue exactly when present (depth CopyTex* attachments used to wedge later GetTexImage color reads and vice versa), scissor guards around emulation blits (app scissor clipped depth copies), stale-driver-error drains before single-shot glGetError consumers (fallbacks silently dropped readbacks / GenerateMipmap raised phantom app errors in production builds), ClearBufferiv missing BindCurrentFBO(Draw), cube-face glBindTexture INVALID_ENUM cache poisoning, per-slice GL_PACK_IMAGE_HEIGHT/SKIP_IMAGES semantics on 3D GetTexImage, backend texture ids deleted on wrapper destruction with cache/scratch-FBO scrubs (ids used to leak for the context lifetime and dangling cache pointers could false-skip binds), and context-death/MakeCurrent invalidation for all new shadows; regression tests drive the shadows against a recording mock GLES table 2026-07-21 19:54:54 -04:00
swung0x48 bc2d698b3e [Fix] (DirectGLES): apply the read buffer when one FBO is bound as both draw and read
- SyncCurrentFBO skips the READ-target pass when the same GL FBO is bound as
  both draw and read (the common GL_FRAMEBUFFER case), but the read buffer
  (glReadBuffer) is only applied inside SyncToBackend's READ path — so the skip
  silently dropped every glReadBuffer change, leaving the backend read buffer
  stuck at COLOR_ATTACHMENT0.
- Extract the read-buffer application into BackendFramebufferObject::
  SyncReadBufferToBackend and invoke it from the skip branch (target == Read)
  as well as from SyncToBackend, so reads always target the right attachment.
- Bind the backend FBO as READ inside the helper before glReadBuffer, since the
  skip path only bound it as DRAW.
- Fixes KHR-GL3x.draw_buffers.draw_buffers_1 (reading COLOR_ATTACHMENT1 while
  the FBO stays GL_FRAMEBUFFER-bound returned attachment 0's value); the render
  was already correct, only the readback resolved the wrong attachment.
2026-07-21 12:18:55 -04:00
swung0x48 3049c4b82b [Fix] (ShaderTranspiler): stop blanking block comments in the source handed to glslang
- BlankBlockComments replaced comment chars with spaces but preserved interior newlines, so a block comment spanning a newline inside a #define truncated the macro body (VALUE became empty)
- glslang has a conformant preprocessor and collapses a block comment to one space across newlines, so the delivered source now keeps comments intact and lets glslang handle them
- Fixes KHR-GL3x.shaders.preprocessor multiline_comment_define / redefine_object_multiline_comment / function_redefinition_3 (6 cases, both devices)
- FilterUnsupportedGpuShaderInt64 relied on the blanking to skip commented-out #extension lines; it now masks comments locally (MaskCommentsAndQuotedText) like the sibling passes, collecting edits and applying them back-to-front
- conditional_inclusion.basic_2 (defined() via macro expansion) stays failing by design: glslang rejects it as UB and working around it would mean re-running preprocessing MobileGL defers to glslang
2026-07-21 05:27:01 -04:00
swung0x48 2b3850b76b [Fix] (DirectGLES): upload RGB565/RGB5_A1 shadow data as packed 16-bit types
- The 8-bit unorm shadow was uploaded as GL_UNSIGNED_BYTE, leaving the 8->5/6-bit requantization to the driver
- That rounding direction is implementation-defined: Adreno rounds to nearest (lossless round trip), Mali floors
- On Mali mid-range texels drifted one 5-bit step down, failing all 20 KHR-GL33.pixelstoragemodes.teximage3d rgb565/rgb5a1 cases (eps 1/32); layers with exact values (0.125/0.25/1.0-ish) passed, matching the observed 0,1,7-valid pattern
- PreparePackedNormUpload repacks shadow rows to GL_UNSIGNED_SHORT_5_6_5 / 5_5_5_1 with round-to-nearest, which exactly recovers the original 5/6-bit values (the shadow expansion is injective), so the driver stores them verbatim
- Idempotent across each region's level loop (glType is shared); RGBA4 exempt since its 8-bit expansion (v*17) is exact under either rounding
- Wired at all four SyncMipmapsToBackend upload regions (append-mipmaps, immutable TexSubImage, mutable full, dirty-level update)
2026-07-21 04:30:46 -04:00
swung0x48 2a0ae743a0 [Fix] (DirectGLES): glFinish before the glGetTexImage temp-FBO readback
- Mali (tile-based) does not resolve a texture's render into memory when it is read back through a different (temp) FBO than the one it was rendered with
- The cross-FBO glReadPixels raced the deferred tile resolve and returned pre-render clear contents
- Distinct render targets read back byte-identical, so KHR-GLxx.glsl_noperspective failed on Mali-G715 (all four programs read as the clear colour)
- glGetTexImage is already a CPU/GPU sync point so the extra drain is negligible; Adreno resolves eagerly and was unaffected
2026-07-21 03:34:33 -04:00
swung0x48 6839219c10 [Fix] (GLImpl): report GL_NO_ERROR from glGetGraphicsResetStatus
- The generic export stub returned (GLenum)1; dEQP reads any non-zero status as a lost device
- It is polled after every case (gl3cTestPackages.cpp:121) and sets QP_TEST_RESULT_DEVICE_LOST
- Under the default --deqp-terminate-on-device-lost=enable that tears the whole CTS run down
- MobileGL tracks no GPU resets, so GL_NO_ERROR ("no reset detected") is the honest, spec-correct answer
- Routed through GLImpl::GetGraphicsResetStatus like every other entry point, no inline body in Definitions.cpp
2026-07-21 02:40:15 -04:00
swung0x48 52ddb440ca [Feat] (SelfTest/DriverPost): add a noperspective correctness check to the GLES POST - render a strong-perspective quad and read the centre texel to verify the varying interpolates screen-linear (not perspective-correct), carried through the native GL_NV_shader_noperspective_interpolation path when present or MobileGL's exact gl_Position.w/gl_FragCoord.w emulation when absent. PASS = native and correct; WARN = emulated and correct (the fallback path shipping packs hit on such devices); FAIL = interpolation wrong/perspective-correct, or the program will not build. The ESSL header matches the device version because noperspective is rejected at #version 300 es on some drivers even with the extension enabled 2026-07-21 02:09:17 -04:00
swung0x48 79feeffd25 [Feat] (ShaderTranspiler, DirectGLES): emulate noperspective on GLES devices lacking GL_NV_shader_noperspective_interpolation - EmulateNoPerspectivePass pre-multiplies each NoPerspective output by gl_Position.w in the vertex stage and recovers each input via gl_FragCoord.w in the fragment stage (exact screen-linear L = P(a*w)*gl_FragCoord.w, handling whole-variable and component/access-chain reads, scalar and vector varyings), forces highp on emulated varyings, and strips what it cannot emulate; replaces the smooth-strip fallback so no NV extension is ever required. Restricts the vertex pre-multiply to the entry function so a non-inlined helper cannot double-scale 2026-07-20 23:48:48 -04:00
swung0x48 202037b5a3 [Fix] (DirectVulkan): shrink the blended depth-write quirk to MIN/MAX extremum blends only - a fixture-wide trace sweep showed the additive ONE+ONE arm never fires on the 26.3 OIT chain (its accumulation passes disable depth writes themselves) and only hit unrelated additive glow content; also fail reflection toward the gl_FragDepth exemption and zero phantom default-FBO blend slots so stale indexed state cannot trigger the strip 2026-07-20 23:39:28 -04:00
swung0x48 bce9c48c8e [Feat] (ShaderTranspiler, DirectGLES): support noperspective conformantly instead of stripping it - let the qualifier reach glslang as the core SPIR-V NoPerspective decoration (native on DirectVulkan; SPIRV-Cross emits ESSL noperspective + GL_NV_shader_noperspective_interpolation on DirectGLES), and for GLES devices lacking that extension add StripNoPerspectivePass to drop the decoration and fall back to smooth; the old naked substring erase discarded the interpolation shader packs need and mangled identifiers containing the word 2026-07-20 22:59:43 -04:00
swung0x48 b6a7807a3a [Fix] (MG_Util/ShaderTranspiler): reject malformed #version directives instead of legalizing them - an unrecognized version number (329/331), a bad profile keyword, a float or trailing token used to be rewritten to "#version 330 core" (or rescued to 460 by the retry); now InspectShaderLanguage marks such directives invalid so NormalizeVersionDirective and RetargetLegacyVersionDirectiveTo460 leave them for glslang to reject, while every valid version still normalizes as before 2026-07-20 22:03:36 -04:00
swung0x48 48ba622387 [Fix] (MG_Util/ShaderTranspiler): keep #line directives instead of deleting them, dropping only the GLSL-illegal quoted filename and the ones that precede #version, so __LINE__ and compiler diagnostics follow the application's own numbering 2026-07-20 21:06:38 -04:00
swung0x48 05260d1262 [Fix] (MG_Util/ShaderTranspiler): blank block comments lexically instead of erasing them - a '//*** banner ***' line opened a comment the old scanner never closed, so it deleted the rest of the shader, and a commented-out builtin definition renamed every genuine call to a name nothing defines 2026-07-20 21:06:37 -04:00
swung0x48 6eb5ff51c5 [Fix] (MG_Impl/GLImpl): reject the RGTC internal formats on 3D texture targets - RGTC compresses 4x4 blocks of a 2D image and has no 3D form, and the check must run on the raw enum because RGTC now resolves to plain R8/RG8 storage 2026-07-20 21:05:57 -04:00
swung0x48 e526f8e8ac [Fix] (MG_Util/Converters): resolve the GL_COMPRESSED_* internal formats to the uncompressed storage that backs them instead of rejecting them as unknown - GL prescribes this base-format fallback for the six generic formats, and RGTC stores uncompressed because ES exposes no compressor 2026-07-20 21:05:57 -04:00
swung0x48 e724e88eec [Fix] (MG_State, MG_Impl/GLImpl): allocating a mipmap level no longer truncates the chain above it - AllocateLevel now only grows and the callers that genuinely redefine the whole level set (glTexStorage*, mip regeneration, multisample storage, level-0 respecification) drop the tail explicitly 2026-07-20 21:02:54 -04:00
swung0x48 3b175fb88a [Fix] (MG_Impl/GLImpl): a multisample sample count above the format's maximum is INVALID_OPERATION, not INVALID_VALUE - matching both the spec and the native Adreno driver 2026-07-20 21:02:53 -04:00
swung0x48 b5a4e7075a [Fix] (MG_Impl/GLImpl): record a GL error from the unimplemented compressed texture entry points instead of throwing - a C++ exception unwinding through the C GL ABI hard-crashes any caller, and glGetCompressedTexImage reported success while writing nothing 2026-07-20 21:02:53 -04:00
swung0x48 520c2b6750 [Fix] (MG_Backend/DirectGLES): sync GL_TEXTURE_SWIZZLE_* on multisample targets - the early return meant to skip the sampler-only parameters dropped every swizzle write, which the frontend already treats as legal on those targets 2026-07-20 21:02:52 -04:00
swung0x48 57cc652b1d [Fix] (MG_Impl/GLImpl): glIsTransformFeedback reports GL_FALSE instead of claiming every name it is handed is a live transform feedback object 2026-07-20 21:02:52 -04:00
swung0x48 65ea54da9e [Refactor] (ShaderTranspiler, DirectVulkan): replace the hand-rolled SPIR-V word walkers with a DecoratePositionInvariantPass and SPIRV-Reflect-based InstanceIndex detection 2026-07-20 08:35:00 -04:00
swung0x48 c81dd04f08 [Test] (CI, trace_replay): force the blended depth-write quirk on the Linux DirectVulkan OIT retrace lane and tighten that case's SSIM threshold to 0.995 2026-07-20 07:39:25 -04:00
swung0x48 c158bfa584 [Fix] (DirectVulkan): narrow the blended depth-write quirk to order-independent accumulation blends, exempting sorted-transparency, gl_FragDepth writers and fully masked attachments 2026-07-20 07:39:25 -04:00
swung0x48 f9f455144c [Refactor] (MG_Config, DirectVulkan): route the blended depth-write quirk through the FeaturesTable as MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE instead of an ad-hoc getenv 2026-07-20 04:37:23 -04:00
swung0x48 64e4840de2 [Fix] (DirectVulkan): fall back to immutable images when the mutable probe fails, gate robustBufferAccess behind MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS, decode R32/RG32/R16-class readback formats, and warn when shaderStorageImage*WithoutFormat is unavailable 2026-07-20 02:36:26 -04:00
swung0x48 bf7b5755cc [Refactor] (ShaderTranspiler, MG_Backend, MG_Util): gate the subgroup prefix-scan rewrite behind a generic device-quirk registry with GPU vendor detection and MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN override, warn on template mismatch, and block ARB/NV subgroup spellings 2026-07-20 02:36:26 -04:00
swung0x48 293f64b3c2 [Fix] (MG_Impl, DirectGLES): correct ARB_clear_texture error codes, reject cube maps in CopyTextureSubImage2D, advertise the extension on Espryt, and pin the error contracts with tests 2026-07-20 02:36:25 -04:00
swung0x48 f0cc07c937 [Perf] (DirectVulkan): bound vertex-stream conversion by the draw's real fetch range, reuse cached prefixes, pin cached source buffers, and stop repacking client arrays for pointer alignment 2026-07-20 02:36:25 -04:00
swung0x48 4658536652 [Perf] (DirectVulkan): keep the render pass alive for steady-state storage-image draws and skip storage-image collection for programs without them 2026-07-20 02:36:24 -04:00
swung0x48 04b4627c65 [Fix] (DirectVulkan): pass depth/stencil formats through sampled-view resolution so D24S8/D32FS8 samplers stop dropping draws, and memoize per-binding view-format resolution 2026-07-20 02:36:24 -04:00
swung0x48 68e13705c8 [Fix] (MG_Backend/DirectVulkan, ShaderTranspiler, MG_Test, TraceReplay): make iterationRP retrace pass on 64-lane Vulkan devices with subgroup-width emulation and format-aware readback 2026-07-20 02:36:23 -04:00
swung0x48 e5388c0e7e [Fix] (MG_Backend, MG_Impl, ShaderTranspiler, MG_Test): support iterationRP custom images and storage format reinterpretation 2026-07-20 02:35:30 -04:00
swung0x48 8bc4808b1a [Test] (TraceReplay): match the iterationRP fixture name published on the mirror 2026-07-19 23:09:29 -04:00
swung0x48 5d8a5387e2 [Test] (TraceReplay): try the hit.moe mirror before miawa and Git LFS 2026-07-19 22:29:55 -04:00
swung0x48 aa2184e47a [Test] (TraceReplay): register iterationRP in-world fixture (non-CI, fixture files pending LFS) 2026-07-19 21:39:06 -04:00
swung0x48 9152a4a4bc [Test] (TraceReplay): enable improved-transparency fixture in CI and drop unneeded coherent_as_flush 2026-07-19 07:35:34 -04:00
swung0x48 1963b427db [Refactor] (TraceReplay): reorganize trace skills into uniform packages with bundled scripts 2026-07-19 07:00:56 -04:00
swung0x48 f3def150e7 [Fix] (DirectVulkan): suppress blended depth writes on Qualcomm and mark gl_Position invariant to fix MC 26.3 OIT cloud flicker 2026-07-19 05:47:23 -04:00
565 changed files with 195044 additions and 8134 deletions
+51 -60
View File
@@ -1,6 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=trace-fixture-lib.sh
. "${script_dir}/trace-fixture-lib.sh"
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
echo "usage: $0 <trace-case> [fixture-dir]" >&2
exit 2
@@ -9,7 +13,23 @@ fi
case_name="$1"
fixture_dir="${2:-tools/trace_replay/fixtures}"
python_bin="${PYTHON:-python3}"
mirror_base="${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-https://repo.miawa.cn/mgl/tools/trace_replay/fixtures}"
# Fixture mirrors, tried in order before falling back to Git LFS. Override the
# whole list with MOBILEGL_TRACE_FIXTURE_MIRROR_BASES (whitespace separated);
# MOBILEGL_TRACE_FIXTURE_MIRROR_BASE still works and is tried first.
default_mirror_bases=(
"https://git.hit.moe/swung0x48/MobileGL/media/branch/dev/tools/trace_replay/fixtures"
"https://repo.miawa.cn/mgl/tools/trace_replay/fixtures"
)
if [ -n "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASES:-}" ]; then
read -r -a mirror_bases <<< "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASES}"
else
mirror_bases=("${default_mirror_bases[@]}")
fi
if [ -n "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-}" ]; then
mirror_bases=("${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE}" "${mirror_bases[@]}")
fi
# Optional bearer token for mirrors that require authentication (private Gitea).
mirror_token="${MOBILEGL_TRACE_FIXTURE_MIRROR_TOKEN:-}"
download_attempts="${MOBILEGL_TRACE_FIXTURE_DOWNLOAD_ATTEMPTS:-5}"
retry_delay="${MOBILEGL_TRACE_FIXTURE_RETRY_DELAY:-2}"
@@ -30,7 +50,8 @@ fixture_list="$("${python_bin}" tools/trace_replay/trace_cases.py \
--format fixture-files \
--case "${case_name}" \
--fixture-root "${fixture_dir}")"
mapfile -t files <<< "${fixture_list}"
# Strip CR so the script also works when python emits CRLF (Git Bash on Windows).
mapfile -t files < <(printf '%s\n' "${fixture_list}" | tr -d '\r')
include="$(IFS=,; echo "${files[*]}")"
if [ "${case_name}" = "OpenRA" ]; then
@@ -45,57 +66,6 @@ if [ "${case_name}" = "OpenRA" ]; then
exit 0
fi
get_lfs_metadata() {
local file="$1"
local pointer
local expected_oid
local expected_size
if ! pointer="$(git show "HEAD:${file}" 2>/dev/null)"; then
echo "failed to read tracked fixture metadata: ${file}" >&2
return 1
fi
if ! grep -q '^version https://git-lfs.github.com/spec/v1$' <<< "${pointer}"; then
echo "tracked fixture is not a Git LFS pointer: ${file}" >&2
return 1
fi
expected_oid="$(awk '$1 == "oid" && $2 ~ /^sha256:/ { sub(/^sha256:/, "", $2); print $2 }' <<< "${pointer}")"
expected_size="$(awk '$1 == "size" { print $2 }' <<< "${pointer}")"
if ! [[ "${expected_oid}" =~ ^[0-9a-f]{64}$ ]] || ! [[ "${expected_size}" =~ ^[0-9]+$ ]]; then
echo "invalid Git LFS pointer metadata: ${file}" >&2
return 1
fi
printf '%s %s\n' "${expected_oid}" "${expected_size}"
}
verify_fixture_file() {
local downloaded_file="$1"
local display_name="$2"
local expected_oid="$3"
local expected_size="$4"
local actual_oid
local actual_size
if [ ! -f "${downloaded_file}" ]; then
echo "fixture file is missing: ${display_name}" >&2
return 1
fi
actual_size="$(wc -c < "${downloaded_file}" | tr -d '[:space:]')"
if [ "${actual_size}" != "${expected_size}" ]; then
echo "fixture size mismatch for ${display_name}: expected ${expected_size}, got ${actual_size}" >&2
return 1
fi
actual_oid="$(sha256sum "${downloaded_file}" | awk '{ print $1 }')"
if [ "${actual_oid}" != "${expected_oid}" ]; then
echo "fixture SHA-256 mismatch for ${display_name}: expected ${expected_oid}, got ${actual_oid}" >&2
return 1
fi
}
fetch_file_from_mirror() {
local file="$1"
local url="$2"
@@ -106,6 +76,7 @@ fetch_file_from_mirror() {
local attempt
local partial_size
local curl_status
local curl_auth
metadata="$(get_lfs_metadata "${file}")" || return 1
read -r expected_oid expected_size <<< "${metadata}"
@@ -136,7 +107,11 @@ fetch_file_from_mirror() {
echo "Starting mirror download for ${file} (attempt ${attempt}/${download_attempts})"
fi
if curl -L --fail --show-error --continue-at - --output "${tmp_file}" "${url}"; then
curl_auth=()
if [ -n "${mirror_token}" ]; then
curl_auth=(--header "Authorization: token ${mirror_token}")
fi
if curl -L --fail --show-error --continue-at - "${curl_auth[@]}" --output "${tmp_file}" "${url}"; then
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
mv "${tmp_file}" "${file}"
return 0
@@ -179,26 +154,42 @@ fetch_file_from_mirror() {
return 1
}
# Files no mirror could serve, even after retrying every mirror. Only these fall
# back to Git LFS, so a mirror that served the rest of the case still spares
# GitHub the bandwidth for those files.
mirror_failures=()
fetch_from_mirror() {
mkdir -p "${fixture_dir}"
for file in "${files[@]}"; do
local name
local url
local base
local fetched=0
name="$(basename "${file}")"
url="${mirror_base%/}/${name}"
echo "Fetching trace fixture from mirror: ${url}"
if ! fetch_file_from_mirror "${file}" "${url}"; then
return 1
for base in "${mirror_bases[@]}"; do
url="${base%/}/${name}"
echo "Fetching trace fixture from mirror: ${url}"
if fetch_file_from_mirror "${file}" "${url}"; then
fetched=1
break
fi
echo "Mirror did not serve ${name}; trying the next mirror" >&2
done
if [ "${fetched}" -ne 1 ]; then
mirror_failures+=("${file}")
fi
done
[ "${#mirror_failures[@]}" -eq 0 ]
}
if fetch_from_mirror; then
echo "Fetched trace fixture files for ${case_name} from mirror: ${include}"
else
echo "Mirror fetch failed for ${case_name}; falling back to Git LFS: ${include}"
fallback_include="$(IFS=,; echo "${mirror_failures[*]}")"
echo "All mirrors failed for ${#mirror_failures[@]} of ${#files[@]} file(s) of ${case_name}; falling back to Git LFS: ${fallback_include}"
git lfs install --local
git lfs pull --include="${include}" --exclude=""
git lfs pull --include="${fallback_include}" --exclude=""
fi
for file in "${files[@]}"; do
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
# Cache-side helper for trace fixtures.
#
# key <case> [fixture-dir] derive the actions/cache key and path list
# verify <case> [fixture-dir] check restored fixtures against their pointers
# reset <case> [fixture-dir] drop restored fixtures, leaving the pointers
#
# The cache key is content-addressed on the Git LFS pointer oids tracked at
# HEAD, which are readable from a plain checkout without smudging. Fixture
# content therefore maps 1:1 onto a key: unchanged content hits, changed
# content is a new key and thus a miss, and the download path handles it. The
# key deliberately carries no restore-keys prefix in the workflow - a fixture
# that does not match the pointer exactly must never be restored.
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=trace-fixture-lib.sh
. "${script_dir}/trace-fixture-lib.sh"
# Bump when the key derivation changes in a way that must invalidate old
# entries; the content digest alone would not notice a format change.
key_schema="v1"
if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then
echo "usage: $0 <key|verify|reset> <trace-case> [fixture-dir]" >&2
exit 2
fi
command_name="$1"
case_name="$2"
fixture_dir="${3:-tools/trace_replay/fixtures}"
python_bin="${PYTHON:-python3}"
if ! command -v "${python_bin}" >/dev/null 2>&1 && command -v python >/dev/null 2>&1; then
python_bin=python
fi
mapfile -t files < <(trace_fixture_files "${case_name}" "${fixture_dir}" "${python_bin}")
if [ "${#files[@]}" -eq 0 ]; then
echo "no fixture files declared for trace case: ${case_name}" >&2
exit 1
fi
# Writes "name=value" to $GITHUB_OUTPUT when running under Actions, and to
# stdout otherwise so the script stays runnable (and testable) off-CI.
emit_output() {
local name="$1"
local value="$2"
if [ -n "${GITHUB_OUTPUT:-}" ]; then
if [[ "${value}" == *$'\n'* ]]; then
local delimiter="ghadelim_$(date +%s%N)_$$"
{
printf '%s<<%s\n' "${name}" "${delimiter}"
printf '%s\n' "${value}"
printf '%s\n' "${delimiter}"
} >> "${GITHUB_OUTPUT}"
else
printf '%s=%s\n' "${name}" "${value}" >> "${GITHUB_OUTPUT}"
fi
fi
printf '%s=%s\n' "${name}" "${value}"
}
sanitize_case() {
printf '%s' "$1" | sed 's/[^A-Za-z0-9._-]/_/g'
}
case "${command_name}" in
key)
manifest=""
for file in "${files[@]}"; do
# A case whose fixtures are committed directly rather than through Git LFS
# (OpenRA) has no pointer oid to key on, and nothing to download either.
# Report it as uncacheable so the workflow skips the cache entirely.
if ! metadata="$(get_lfs_metadata "${file}" 2>/dev/null)"; then
echo "trace case ${case_name} is not stored in Git LFS; skipping fixture cache" >&2
emit_output "cacheable" "false"
emit_output "key" ""
exit 0
fi
read -r expected_oid expected_size <<< "${metadata}"
manifest+="$(basename "${file}") ${expected_oid} ${expected_size}"$'\n'
done
digest="$(printf '%s' "${manifest}" | sha256sum | awk '{ print substr($1, 1, 16) }')"
safe_case="$(sanitize_case "${case_name}")"
emit_output "cacheable" "true"
emit_output "key" "trace-fixture-${key_schema}-${safe_case}-${digest}"
emit_output "paths" "$(printf '%s\n' "${files[@]}")"
;;
verify)
for file in "${files[@]}"; do
metadata="$(get_lfs_metadata "${file}")"
read -r expected_oid expected_size <<< "${metadata}"
verify_fixture_file "${file}" "${file}" "${expected_oid}" "${expected_size}"
done
echo "Verified ${#files[@]} fixture file(s) for ${case_name} against the tracked Git LFS pointers."
;;
reset)
# Put the working tree back to the pointer files a fresh checkout would
# have, so that a rejected cache entry falls through to exactly the same
# download path a cache miss takes.
for file in "${files[@]}"; do
rm -f "${file}" "${file}.tmp"
done
git checkout -- "${files[@]}"
echo "Reset ${#files[@]} fixture file(s) for ${case_name} to their tracked Git LFS pointers."
;;
*)
echo "unknown command: ${command_name}" >&2
exit 2
;;
esac
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# Shared helpers for trace-fixture handling: reading the in-tree Git LFS pointer
# metadata and verifying a fixture file against it. Sourced by
# fetch-trace-fixture-lfs.sh (verify after download) and by
# trace-fixture-cache.sh (cache key derivation and verify after cache restore),
# so both paths agree on what a valid fixture is.
# Reads the Git LFS pointer tracked at HEAD for a fixture path and prints
# "<oid> <size>". Fails if the tracked blob is not a well-formed LFS pointer.
get_lfs_metadata() {
local file="$1"
local pointer
local expected_oid
local expected_size
if ! pointer="$(git show "HEAD:${file}" 2>/dev/null)"; then
echo "failed to read tracked fixture metadata: ${file}" >&2
return 1
fi
if ! grep -q '^version https://git-lfs.github.com/spec/v1$' <<< "${pointer}"; then
echo "tracked fixture is not a Git LFS pointer: ${file}" >&2
return 1
fi
expected_oid="$(awk '$1 == "oid" && $2 ~ /^sha256:/ { sub(/^sha256:/, "", $2); print $2 }' <<< "${pointer}")"
expected_size="$(awk '$1 == "size" { print $2 }' <<< "${pointer}")"
if ! [[ "${expected_oid}" =~ ^[0-9a-f]{64}$ ]] || ! [[ "${expected_size}" =~ ^[0-9]+$ ]]; then
echo "invalid Git LFS pointer metadata: ${file}" >&2
return 1
fi
printf '%s %s\n' "${expected_oid}" "${expected_size}"
}
# Checks an on-disk fixture against the size and SHA-256 from its LFS pointer.
verify_fixture_file() {
local downloaded_file="$1"
local display_name="$2"
local expected_oid="$3"
local expected_size="$4"
local actual_oid
local actual_size
if [ ! -f "${downloaded_file}" ]; then
echo "fixture file is missing: ${display_name}" >&2
return 1
fi
actual_size="$(wc -c < "${downloaded_file}" | tr -d '[:space:]')"
if [ "${actual_size}" != "${expected_size}" ]; then
echo "fixture size mismatch for ${display_name}: expected ${expected_size}, got ${actual_size}" >&2
return 1
fi
actual_oid="$(sha256sum "${downloaded_file}" | awk '{ print $1 }')"
if [ "${actual_oid}" != "${expected_oid}" ]; then
echo "fixture SHA-256 mismatch for ${display_name}: expected ${expected_oid}, got ${actual_oid}" >&2
return 1
fi
}
# Prints the fixture file paths of a trace case, one per line. Strips CR so the
# result is usable when python emits CRLF (Git Bash on Windows).
trace_fixture_files() {
local case_name="$1"
local fixture_dir="$2"
local python_bin="${3:-python3}"
"${python_bin}" tools/trace_replay/trace_cases.py \
--format fixture-files \
--case "${case_name}" \
--fixture-root "${fixture_dir}" | tr -d '\r'
}
+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
+117 -17
View File
@@ -11,6 +11,9 @@ on:
jobs:
build:
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
env:
CCACHE_BASEDIR: ${{ github.workspace }}
CCACHE_COMPRESS: "true"
@@ -41,12 +44,11 @@ jobs:
gradle-version: 8.10.2
- name: Restore ccache
uses: actions/cache@v5
uses: actions/cache/restore@v5
with:
path: .ccache
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-${{ github.run_id }}
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
restore-keys: |
${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-
${{ runner.os }}-apk-${{ github.job }}-ccache-
- name: Install ccache
@@ -125,6 +127,28 @@ jobs:
if: always()
run: ccache --show-stats
# Rewrite one rolling entry per job on the default branch. The upload stays
# cumulative - it carries every object restored at the top of this run plus
# the few TUs that actually changed - but Actions cache keys are immutable,
# so the superseded blob has to be released before the same key can be
# re-uploaded. Running after the build means a failed build leaves the
# existing entry untouched. The other trigger branches restore this entry
# rather than each writing a ~4 GB one of their own.
- name: Release superseded ccache entry
if: github.ref_name == github.event.repository.default_branch
env:
GH_TOKEN: ${{ github.token }}
CACHE_KEY: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
run: gh cache delete "${CACHE_KEY}" || true
- name: Save ccache
if: github.ref_name == github.event.repository.default_branch
continue-on-error: true
uses: actions/cache/save@v5
with:
path: .ccache
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
- name: Verify APK metadata and packaging
run: |
AAPT2="$(find "$ANDROID_HOME/build-tools" -name aapt2 -type f | sort -V | tail -n 1)"
@@ -185,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:
@@ -201,9 +225,41 @@ jobs:
- name: Checkout repo
uses: actions/checkout@v6
- name: Derive trace fixture cache key
id: fixture-key
run: bash .github/scripts/trace-fixture-cache.sh key '${{ matrix.case }}'
- name: Restore trace fixture cache
id: fixture-cache
if: steps.fixture-key.outputs.cacheable == 'true'
uses: actions/cache/restore@v5
with:
path: ${{ steps.fixture-key.outputs.paths }}
key: ${{ steps.fixture-key.outputs.key }}
- name: Verify restored trace fixture
id: fixture-verify
if: steps.fixture-cache.outputs.cache-hit == 'true'
run: |
if bash .github/scripts/trace-fixture-cache.sh verify '${{ matrix.case }}'; then
echo "ok=true" >> "$GITHUB_OUTPUT"
else
echo "ok=false" >> "$GITHUB_OUTPUT"
echo "::warning::Cached fixture for ${{ matrix.case }} failed verification; falling back to the download path"
bash .github/scripts/trace-fixture-cache.sh reset '${{ matrix.case }}'
fi
- name: Fetch trace fixture
if: steps.fixture-verify.outputs.ok != 'true'
run: bash .github/scripts/fetch-trace-fixture-lfs.sh '${{ matrix.case }}'
- name: Save trace fixture cache
if: steps.fixture-key.outputs.cacheable == 'true' && steps.fixture-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ${{ steps.fixture-key.outputs.paths }}
key: ${{ steps.fixture-key.outputs.key }}
- name: Stage trace fixture
run: |
safe_case="$(printf '%s' '${{ matrix.case }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
@@ -281,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
@@ -367,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}"
@@ -379,6 +432,9 @@ jobs:
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.name }}" = "minecraft-1.21.4-fabric-iris-bliss-in-world" ]; then
extra_retrace_args+=(--avoid-angle-llvmpipe-sampler-mipmap-min-filter)
fi
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.avoid_angle_llvmpipe_explicit_lod_bias || false }}" = "true" ]; then
extra_retrace_args+=(--avoid-angle-llvmpipe-explicit-lod-bias)
fi
if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then
extra_retrace_args+=(--coherent-as-flush)
fi
@@ -411,6 +467,24 @@ jobs:
run_retrace || retrace_status=$?
if [ "${retrace_status}" -eq 75 ]; then
echo "::warning::Android emulator infrastructure failed; restarting it and retrying this retrace once."
# Surface-lost is retried rather than failed, so it would otherwise
# be invisible. Report it per job - a healthy run prints nothing and
# a rate spike shows up as a row per affected case.
reason_file="android-retrace-result/infrastructure-failure-reason.txt"
surface_lost_retries=0
if [ -f "${reason_file}" ]; then
surface_lost_retries="$(grep -c 'angle-surface-lost' "${reason_file}" || true)"
fi
if [ "${surface_lost_retries}" -gt 0 ]; then
echo "surface-lost retries: ${surface_lost_retries} (${{ matrix.backend.name }}, ${{ matrix.case.name }})" \
>> "${GITHUB_STEP_SUMMARY}"
fi
# The restart truncates EMULATOR_LOG, and the attempt that lost the
# emulator is the one worth reading - the retry usually only shows
# the wreckage. Keep the first attempt's log before it is clobbered.
if [ -f "${EMULATOR_LOG}" ]; then
cp "${EMULATOR_LOG}" "${EMULATOR_LOG}.first-attempt" || true
fi
sh android-plugin/run-avd-ci.sh stop \
--avd-name "${AVD_NAME}" \
--emulator-log "${EMULATOR_LOG}" \
@@ -450,6 +524,13 @@ jobs:
if [ -f "${EMULATOR_LOG}" ]; then
cp "${EMULATOR_LOG}" android-retrace-result/diagnostics/emulator.log
fi
if [ -f "${EMULATOR_LOG}.first-attempt" ]; then
cp "${EMULATOR_LOG}.first-attempt" android-retrace-result/diagnostics/emulator-first-attempt.log
fi
# A vanished emulator looks identical whether the host OOM killer took
# qemu or the renderer faulted. These two say which.
free -h > android-retrace-result/diagnostics/host-memory.txt 2>&1 || true
sudo dmesg -T 2>/dev/null | tail -300 > android-retrace-result/diagnostics/host-dmesg.txt || true
- name: Stop Emulator
if: always()
@@ -531,22 +612,41 @@ jobs:
)
if ((${#failed_cases[@]})); then
echo "Retaining fixtures for failed retrace case(s):"
echo "Retaining fixtures and results for failed retrace case(s):"
printf ' %s\n' "${!failed_cases[@]}"
else
echo "All retrace jobs succeeded; no fixtures need to be retained."
echo "All retrace jobs succeeded; nothing needs to be retained."
fi
deleted=0
retained=0
while IFS=$'\t' read -r artifact_id artifact_name; do
keep=0
if [[ "${artifact_name}" == MobileGL-trace-fixture-* ]]; then
case_name="${artifact_name#MobileGL-trace-fixture-}"
if [[ -v "failed_cases[${case_name}]" ]]; then
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
((retained += 1))
continue
keep=1
fi
elif [[ "${artifact_name}" == MobileGL-android-retrace-result-* ]]; then
# The result artifact carries mobilegl.log, retrace.log, logcat,
# the emulator log and the actual/diff images - the only record of
# why a retrace failed. Its name ends in -<backend>-<case>, so a
# suffix match on the case name keeps both backends' results for a
# case that failed on either of them, which is what a comparison
# needs. The match is anchored at the end, so a case name that is a
# prefix of a longer one does not retain the longer one's results.
for case_name in "${!failed_cases[@]}"; do
if [[ "${artifact_name}" == *-"${case_name}" ]]; then
keep=1
break
fi
done
fi
if ((keep)); then
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
((retained += 1))
continue
fi
echo "Deleting ${artifact_name} (${artifact_id})"
+235 -15
View File
@@ -1,4 +1,4 @@
name: Test
name: Test
on:
push:
@@ -11,6 +11,9 @@ on:
jobs:
build-linux:
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
env:
BUILD_DIR: build-linux
CCACHE_BASEDIR: ${{ github.workspace }}
@@ -34,12 +37,11 @@ jobs:
uses: lukka/get-cmake@v4.3.3
- name: Restore ccache
uses: actions/cache@v5
uses: actions/cache/restore@v5
with:
path: .ccache
key: ${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-${{ github.run_id }}
key: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
restore-keys: |
${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-
${{ runner.os }}-test-${{ github.job }}-ccache-
- name: Prepare Vulkan SDK
@@ -83,6 +85,8 @@ jobs:
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
-DMOBILEGL_BUILD_TEST=ON \
-DMOBILEGL_BUILD_BENCHMARK=ON \
-DMOBILEGL_BUILD_INTEGRATION_TEST=ON \
-DMOBILEGL_ITEST_VK_ICD=/usr/share/vulkan/icd.d/lvp_icd.json \
-DMOBILEGL_BUILD_TRACE_REPLAY=OFF \
-DBENCHMARK_DOWNLOAD_DEPENDENCIES=ON \
-DBENCHMARK_ENABLE_TESTING=OFF \
@@ -95,6 +99,28 @@ jobs:
if: always()
run: ccache --show-stats
# Rewrite one rolling entry per job on the default branch. The upload stays
# cumulative - it carries every object restored at the top of this run plus
# the few TUs that actually changed - but Actions cache keys are immutable,
# so the superseded blob has to be released before the same key can be
# re-uploaded. Running after the build means a failed build leaves the
# existing entry untouched. The other trigger branches restore this entry
# rather than each writing one of their own.
- name: Release superseded ccache entry
if: github.ref_name == github.event.repository.default_branch
env:
GH_TOKEN: ${{ github.token }}
CACHE_KEY: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
run: gh cache delete "${CACHE_KEY}" || true
- name: Save ccache
if: github.ref_name == github.event.repository.default_branch
continue-on-error: true
uses: actions/cache/save@v5
with:
path: .ccache
key: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
- name: Package Linux runtime
run: |
mkdir -p ci-artifacts
@@ -110,6 +136,7 @@ jobs:
"${BUILD_DIR}/CTestTestfile.cmake" \
"${BUILD_DIR}/MobileGL/MG_Test" \
"${BUILD_DIR}/MobileGL/MG_Benchmark" \
"${BUILD_DIR}/MobileGL/MG_IntegrationTest" \
"${SHARED_LIBS[@]}"
- name: Upload Linux runtime
@@ -159,12 +186,115 @@ jobs:
- name: Test
working-directory: build-linux
run: |
ulimit -c unlimited
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
ctest -V -L unit --no-tests=error
else
ctest --output-on-failure -L unit --no-tests=error
fi
- name: Upload core dumps
if: failure()
uses: actions/upload-artifact@v7
with:
name: unit-core-dumps
path: /tmp/core.*
if-no-files-found: ignore
integration:
runs-on: ubuntu-latest
needs: build-linux
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Get CMake
uses: lukka/get-cmake@v4.3.3
- name: Install runtime dependencies
# Same set as the benchmark job, for the same reason: the scenarios bring
# up real headless EGL (llvmpipe) and Vulkan (lavapipe) contexts, and
# libegl-mesa0 - the EGL vendor library behind glvnd's libegl1 dispatch -
# only arrives as a Recommends.
run: |
sudo apt-get update
sudo apt-get install -y libvulkan1 libegl1 libegl-mesa0 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
- name: Download Linux runtime
uses: actions/download-artifact@v8
with:
name: mobilegl-linux-runtime
path: .
- name: Unpack Linux runtime
run: tar -xzf mobilegl-linux-runtime.tgz
- name: Normalize CTest command paths
run: |
python - <<'PY'
from pathlib import Path
import re
for path in Path('build-linux').rglob('CTestTestfile.cmake'):
text = path.read_text()
text = re.sub(r'"[^"]*/cmake-[^"]*/bin/cmake"', '"cmake"', text)
path.write_text(text)
PY
- name: Integration scenarios
working-directory: build-linux
# REQUIRE_GPU makes a driverless runner FAIL instead of skipping every
# scenario - an all-skip run is otherwise indistinguishable from a pass,
# which is how a five-month-old draw-dropping bug survived unseen until
# this lane existed.
#
# The lavapipe ICD pin lives in the build-linux configure
# (-DMOBILEGL_ITEST_VK_ICD), NOT here: the configure bakes it into each
# test's ctest ENVIRONMENT property, and a property entry OVERRIDES the
# job environment - a VK_ICD_FILENAMES exported here would be silently
# ignored while looking like it works. This lane runs on lavapipe
# deterministically, not on whichever of the eight Mesa ICDs a GPU-less
# runner enumerates first.
#
# Cores are armed so that any crash - the harness pre-flight child's
# included - leaves /tmp/core.*, which the failure-only step below ships
# as an artifact. Analyzing a downloaded core against the runtime
# artifact's binary in an ubuntu-24.04 userspace reproduces the exact
# 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
if: failure()
uses: actions/upload-artifact@v7
with:
name: integration-core-dumps
path: /tmp/core.*
if-no-files-found: ignore
benchmark:
runs-on: ubuntu-latest
needs: build-linux
@@ -177,9 +307,13 @@ jobs:
uses: lukka/get-cmake@v4.3.3
- name: Install runtime dependencies
# libegl-mesa0 is the EGL vendor library itself: DriverBench brings up a
# real GL context, and libegl1 is only glvnd's dispatch. It normally
# arrives as a Recommends of libegl1, which is too quiet a dependency for
# the one job that needs a working driver.
run: |
sudo apt-get update
sudo apt-get install -y libvulkan1 libegl1 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
sudo apt-get install -y libvulkan1 libegl1 libegl-mesa0 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
- name: Download Linux runtime
uses: actions/download-artifact@v8
@@ -204,7 +338,18 @@ jobs:
- name: Benchmark
working-directory: build-linux
run: ctest -V -C Release -L benchmark --no-tests=error
run: |
ulimit -c unlimited
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
ctest -V -C Release -L benchmark --no-tests=error
- name: Upload core dumps
if: failure()
uses: actions/upload-artifact@v7
with:
name: benchmark-core-dumps
path: /tmp/core.*
if-no-files-found: ignore
build-retrace:
runs-on: ubuntu-latest
@@ -212,6 +357,10 @@ jobs:
- build-linux
- test
- benchmark
- integration
permissions:
actions: write
contents: read
env:
BUILD_DIR: build-retrace
CCACHE_BASEDIR: ${{ github.workspace }}
@@ -236,12 +385,11 @@ jobs:
uses: lukka/get-cmake@v4.3.3
- name: Restore ccache
uses: actions/cache@v5
uses: actions/cache/restore@v5
with:
path: .ccache
key: ${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-${{ github.run_id }}
key: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
restore-keys: |
${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-
${{ runner.os }}-test-${{ github.job }}-ccache-
- name: Prepare Vulkan SDK
@@ -307,6 +455,21 @@ jobs:
if: always()
run: ccache --show-stats
- name: Release superseded ccache entry
if: github.ref_name == github.event.repository.default_branch
env:
GH_TOKEN: ${{ github.token }}
CACHE_KEY: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
run: gh cache delete "${CACHE_KEY}" || true
- name: Save ccache
if: github.ref_name == github.event.repository.default_branch
continue-on-error: true
uses: actions/cache/save@v5
with:
path: .ccache
key: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
- name: Normalize CTest command paths
run: |
python - <<'PY'
@@ -339,7 +502,9 @@ jobs:
needs:
- test
- benchmark
- integration
outputs:
matrix: ${{ steps.trace-cases.outputs.matrix }}
names: ${{ steps.trace-cases.outputs.names }}
steps:
- name: Checkout repo
@@ -347,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 }})
@@ -362,9 +529,41 @@ jobs:
- name: Checkout repo
uses: actions/checkout@v6
- name: Derive trace fixture cache key
id: fixture-key
run: bash .github/scripts/trace-fixture-cache.sh key '${{ matrix.case }}'
- name: Restore trace fixture cache
id: fixture-cache
if: steps.fixture-key.outputs.cacheable == 'true'
uses: actions/cache/restore@v5
with:
path: ${{ steps.fixture-key.outputs.paths }}
key: ${{ steps.fixture-key.outputs.key }}
- name: Verify restored trace fixture
id: fixture-verify
if: steps.fixture-cache.outputs.cache-hit == 'true'
run: |
if bash .github/scripts/trace-fixture-cache.sh verify '${{ matrix.case }}'; then
echo "ok=true" >> "$GITHUB_OUTPUT"
else
echo "ok=false" >> "$GITHUB_OUTPUT"
echo "::warning::Cached fixture for ${{ matrix.case }} failed verification; falling back to the download path"
bash .github/scripts/trace-fixture-cache.sh reset '${{ matrix.case }}'
fi
- name: Fetch trace fixture
if: steps.fixture-verify.outputs.ok != 'true'
run: bash .github/scripts/fetch-trace-fixture-lfs.sh '${{ matrix.case }}'
- name: Save trace fixture cache
if: steps.fixture-key.outputs.cacheable == 'true' && steps.fixture-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ${{ steps.fixture-key.outputs.paths }}
key: ${{ steps.fixture-key.outputs.key }}
- name: Stage trace fixture
run: |
safe_case="$(printf '%s' '${{ matrix.case }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
@@ -394,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
@@ -452,11 +647,36 @@ jobs:
- name: Retrace and validate
working-directory: build-retrace/tools/trace_replay
run: |
ulimit -c unlimited
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
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
# lavapipe. The Android AVD lane deliberately leaves it off, keeping the
# unstripped path covered for the same trace.
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
&& [ '${{ matrix.case }}' = 'improved-transparency-minecraft-26.3' ]; then
export MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE=1
fi
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
- name: Upload core dumps
if: failure()
uses: actions/upload-artifact@v7
with:
name: retrace-core-dumps-${{ matrix.backend }}-${{ matrix.case }}
path: /tmp/core.*
if-no-files-found: ignore
- name: Upload actual image
if: always()
uses: actions/upload-artifact@v7
+3
View File
@@ -25,3 +25,6 @@ MobileGL/MG*/cmake-build*
/android-plugin/app/src/trace/jniLibs
/android-plugin/local.properties
tools/trace_replay/work/
__pycache__/
*.py[cod]
/.gradle
+6 -3
View File
@@ -7,9 +7,6 @@
[submodule "3rdparty/SPIRV-Cross"]
path = 3rdparty/SPIRV-Cross
url = https://github.com/KhronosGroup/SPIRV-Cross.git
[submodule "include/FastSTL"]
path = include/FastSTL
url = https://github.com/MobileGL-Dev/FastSTL.git
[submodule "3rdparty/tracy"]
path = 3rdparty/tracy
url = https://github.com/wolfpld/tracy.git
@@ -31,3 +28,9 @@
[submodule "3rdparty/apitrace"]
path = 3rdparty/apitrace
url = https://github.com/MobileGL-Dev/apitrace.git
[submodule "3rdparty/asio"]
path = 3rdparty/asio
url = https://github.com/chriskohlhoff/asio.git
[submodule "include/ska"]
path = include/ska
url = https://github.com/MobileGL-Dev/flat_hash_map.git
Vendored Submodule
+1
Submodule 3rdparty/asio added at 8806a6803c
+204 -2
View File
@@ -4,6 +4,11 @@ project("MobileGL")
option(MOBILEGL_BUILD_TEST "Build MobileGL tests" ON )
option(MOBILEGL_BUILD_BENCHMARK "Build MobileGL benchmarks" ON )
# Headless end-to-end GPU scenarios (MobileGL/MG_IntegrationTest). They need a
# real GPU/ICD to do anything, so they are off by default for CI; every scenario
# skips cleanly where there is none. Registered under the `integration-gpu`
# ctest label so a run can select or exclude them.
option(MOBILEGL_BUILD_INTEGRATION_TEST "Build MobileGL headless GPU integration tests" OFF)
option(MOBILEGL_FORCE_RELEASE_OPT "Enable Release optimization flags in Debug build" ON )
option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling" OFF)
option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF)
@@ -15,9 +20,86 @@ set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to
if (ANDROID)
set(MOBILEGL_BUILD_TEST OFF CACHE BOOL "Build MobileGL tests" FORCE)
set(MOBILEGL_BUILD_BENCHMARK OFF CACHE BOOL "Build MobileGL benchmarks" FORCE)
# ------- Android API level policy: minimum 26, decided here and only here -------
# MobileGL ships against API 26: the codebase must not use any API introduced
# after 26. That usage constraint is enforced where it is real - the shipping
# gradle build compiles at minSdk 26, where a newer API is simply undeclared
# and fails to compile. Configuring at a HIGHER level is therefore allowed
# (nothing in the tree may rely on it), but a LOWER level would change the
# libc contract underneath the shipped library and is refused.
#
# This has to live at configure time because the level cannot be corrected
# from a source header. A `#define __ANDROID_API__ 26` in a common header
# only rewrites the macro for the bionic headers that happen to be included
# after it; any libc++ header pulled in earlier has already latched its
# feature macros at the real configure-time level. libc++ and bionic then
# disagree about which symbols exist - libc++ calls e.g.
# pthread_cond_clockwait while bionic, re-read at the lowered level, has
# hidden its declaration. MobileGL/Defines.h carried exactly that pin from
# the first commit until it was removed; this guard is what replaces it.
#
# Read the level back from the compiler target triple first. Its trailing
# number (aarch64-none-linux-android26) is precisely what clang turns into
# __ANDROID_API__, so it cannot disagree with the compile itself, and it is
# already past every NDK normalisation step - codename aliases, "latest",
# and per-ABI minimum pull-ups. ANDROID_PLATFORM_LEVEL is the fallback for
# generators/languages where the triple variable is not populated.
#
# Note CMAKE_SYSTEM_VERSION is deliberately NOT consulted: it holds the API
# level only under the NDK's newer toolchain path, and is a meaningless 1
# when ANDROID_USE_LEGACY_TOOLCHAIN_FILE is on (which is what AGP has been
# defaulting to). Reading it would fail every legacy-mode build.
set(MOBILEGL_ANDROID_API_LEVEL 26)
set(_mobilegl_android_api "")
foreach (_mobilegl_api_triple "${CMAKE_CXX_COMPILER_TARGET}"
"${CMAKE_C_COMPILER_TARGET}")
if (NOT _mobilegl_android_api AND
_mobilegl_api_triple MATCHES "-android([0-9]+)$")
set(_mobilegl_android_api "${CMAKE_MATCH_1}")
endif()
endforeach()
foreach (_mobilegl_api_var ANDROID_PLATFORM_LEVEL ANDROID_NATIVE_API_LEVEL
ANDROID_PLATFORM)
if (NOT _mobilegl_android_api AND ${_mobilegl_api_var})
string(REGEX REPLACE "^android-" ""
_mobilegl_android_api "${${_mobilegl_api_var}}")
endif()
endforeach()
if (NOT _mobilegl_android_api MATCHES "^[0-9]+$")
message(FATAL_ERROR
"MobileGL: could not determine the Android API level (got "
"\"${_mobilegl_android_api}\"). Configure with the NDK toolchain "
"file and -DANDROID_PLATFORM=android-${MOBILEGL_ANDROID_API_LEVEL}.")
elseif (_mobilegl_android_api LESS MOBILEGL_ANDROID_API_LEVEL)
message(FATAL_ERROR
"MobileGL requires at least Android API ${MOBILEGL_ANDROID_API_LEVEL}, "
"but this build resolved to API ${_mobilegl_android_api}.\n"
"Configure with -DANDROID_PLATFORM=android-${MOBILEGL_ANDROID_API_LEVEL} "
"(gradle builds get this from minSdk ${MOBILEGL_ANDROID_API_LEVEL}, so "
"check that minSdk instead of adding an override).")
elseif (_mobilegl_android_api GREATER MOBILEGL_ANDROID_API_LEVEL)
message(STATUS
"MobileGL: configuring at Android API ${_mobilegl_android_api} "
"(> shipping minimum ${MOBILEGL_ANDROID_API_LEVEL}). Allowed, but the "
"tree must not use post-${MOBILEGL_ANDROID_API_LEVEL} APIs - the "
"minSdk-${MOBILEGL_ANDROID_API_LEVEL} gradle build is the enforcing "
"compile.")
endif()
message(STATUS "MobileGL: Android API level ${_mobilegl_android_api}")
unset(_mobilegl_android_api)
unset(_mobilegl_api_var)
unset(_mobilegl_api_triple)
endif()
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT)
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO" OFF)
if ((NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT) AND MOBILEGL_ENABLE_LTO)
# Check if ThinLTO or LTO is suppported
include(CheckIPOSupported)
include(CheckCCompilerFlag)
@@ -100,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)
@@ -147,6 +230,9 @@ set(SOURCE_FILES
MobileGL/MG_Util/Debug/Log.cpp
MobileGL/MG_Util/Async/JobNode.cpp
MobileGL/MG_Util/Async/ShaderCompilePool.cpp
MobileGL/MG_Util/Math/VectorTypes.cpp
MobileGL/MG_Util/Metrics/TextureMetrics.cpp
@@ -180,27 +266,63 @@ set(SOURCE_FILES
MobileGL/MG_Util/Classifiers/TextureEnumClassifier.cpp
MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp
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
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp
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
MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
@@ -214,7 +336,10 @@ set(SOURCE_FILES
MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp
MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp
MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp
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
@@ -236,6 +361,7 @@ set(SOURCE_FILES
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
MobileGL/MG_Backend/DirectGLES/Utils.cpp
MobileGL/MG_Backend/DirectGLES/Managers.cpp
MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp
@@ -271,10 +397,17 @@ 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
MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderCompileAdoptionMap.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp
MobileGL/MG_State/GLState/RenderState/RenderState.cpp
MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp
@@ -297,9 +430,22 @@ endif()
if (ANDROID)
list(APPEND SOURCE_FILES
MobileGL/MG_Util/SelfTest/DriverPostJni.cpp
MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp
)
endif()
if (WIN32)
list(APPEND SOURCE_FILES
MobileGL/MG_Impl/WGLImpl/WGLImpl.cpp
MobileGL/MG_Impl/WGLImpl/Exporting/Definitions.cpp
)
endif()
# The shader-compile pool runs standalone Asio on real threads. This host's glibc (>= 2.34)
# merged pthread into libc, so it links without asking, but the NDK and musl are not
# guaranteed to be as forgiving - ask for it explicitly rather than rely on the accident.
find_package(Threads REQUIRED)
set(MOBILEGL_LINK_LIBRARIES
glslang::glslang
spirv-cross-c
@@ -309,12 +455,17 @@ set(MOBILEGL_LINK_LIBRARIES
GPUOpen::VulkanMemoryAllocator
Vulkan::UtilityHeaders
spirv-reflect-static
Threads::Threads
)
set(MOBILEGL_COMPILE_DEF
-DVMA_STATIC_VULKAN_FUNCTIONS=0
-DVMA_DYNAMIC_VULKAN_FUNCTIONS=1
-DVMA_VULKAN_VERSION=1001000
# Header-only Asio, no Boost, no deprecated interfaces. Set on the definition list
# rather than per-target so the shared library and the _s static target agree.
-DASIO_STANDALONE
-DASIO_NO_DEPRECATED
)
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
@@ -326,12 +477,24 @@ set(MOBILEGL_INCLUDE_DIR
${spirv-tools_SOURCE_DIR}/include
${spirv-tools_BINARY_DIR}
${SPIRV-Headers_SOURCE_DIR}/include
# 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/include
)
add_library(${CMAKE_PROJECT_NAME} SHARED
add_library(${CMAKE_PROJECT_NAME} SHARED
${SOURCE_FILES}
)
if (WIN32)
# The wgl* entry points are exported via .def (see the comment in wgl.def);
# only the shared library links it.
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
MobileGL/MG_Impl/WGLImpl/Exporting/wgl.def
)
endif()
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
C_VISIBILITY_PRESET default
@@ -374,6 +537,18 @@ if(UNIX AND NOT APPLE AND NOT ANDROID)
endforeach()
endif()
if(WIN32)
# Drop-in for the classic GL loader path: a copy named opengl32.dll placed
# next to a host executable is what LoadLibrary("opengl32.dll") and gdi32's
# pixel-format forwarding will resolve.
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:${CMAKE_PROJECT_NAME}>"
"$<TARGET_FILE_DIR:${CMAKE_PROJECT_NAME}>/opengl32.dll"
COMMENT "Creating opengl32.dll drop-in copy"
)
endif()
if(NOT ANDROID)
add_library(${CMAKE_PROJECT_NAME}_s STATIC
${SOURCE_FILES}
@@ -425,8 +600,21 @@ if (ANDROID)
endif()
if (APPLE AND NOT MOBILEGL_IOS)
# MobileGL statically embeds glslang, SPIRV-Tools, and SPIRV-Cross. When
# this dylib is injected with DYLD_INSERT_LIBRARIES, exporting those C++
# symbols interposes incompatible copies embedded by host libraries such
# as shaderc. Keep only the public GL/EGL/CGL loader surface globally
# visible; GetProcAddress can still return pointers to hidden internals.
set(MOBILEGL_MACOS_EXPORTED_SYMBOLS
"${CMAKE_CURRENT_SOURCE_DIR}/MobileGL/MG_Impl/DyldInterpose/ExportedSymbols.txt")
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
"LINKER:-exported_symbols_list,${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
set_property(TARGET ${CMAKE_PROJECT_NAME} APPEND PROPERTY
LINK_DEPENDS "${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
"-framework Cocoa"
"-framework CoreVideo"
"-framework QuartzCore"
"-framework Foundation"
"-framework OpenGL"
@@ -434,6 +622,7 @@ if (APPLE AND NOT MOBILEGL_IOS)
if(TARGET ${CMAKE_PROJECT_NAME}_s)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
"-framework Cocoa"
"-framework CoreVideo"
"-framework QuartzCore"
"-framework Foundation"
"-framework OpenGL"
@@ -489,6 +678,12 @@ if (NOT ANDROID)
add_subdirectory(MobileGL/MG_Test)
endif()
# After MG_Test so googletest is already available when the unit tests are
# built; the module fetches its own copy when they are not.
if (MOBILEGL_BUILD_INTEGRATION_TEST)
add_subdirectory(MobileGL/MG_IntegrationTest)
endif()
if (MOBILEGL_BUILD_BENCHMARK)
add_subdirectory(MobileGL/MG_Benchmark)
endif()
@@ -497,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()
+258 -9
View File
@@ -14,12 +14,48 @@ namespace MobileGL::MG_Config {
inline const String ProjectName = "MobileGL";
inline const String CoreName = "MobileGL Core";
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
inline const Version CoreVersion = {26, 7, 0, "-dev", VersionType::Development};
inline const Version CoreVersion = {26, 8, 0, "-dev", VersionType::Development};
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
inline const Uint64 CacheVersion = 0;
extern BackendType ActiveBackendType;
// Tri-state override for device-specific quirks: Auto lets the detected device decide,
// ForceOn/ForceOff bypass the detection in either direction. ForceOn only bypasses the
// device gate - each quirk keeps its structural safety checks.
enum class QuirkOverride : Uint8 {
Auto = 0,
ForceOn,
ForceOff,
};
// Preferred DirectVulkan dispatch tier for the glMultiDraw* families. A preference,
// never a demand: the renderer clamps it to what the device supports at device
// creation, falling down the chain ext -> indirect -> unroll with one log line.
enum class MultiDrawMode : Uint8 {
Auto = 0, // unset: best supported tier
Ext, // VK_EXT_multi_draw: one vkCmdDrawMultiEXT / vkCmdDrawMultiIndexedEXT
Indirect, // multiDrawIndirect feature: one vkCmdDraw*Indirect over a transient command array
Unroll, // one vkCmdDraw* per sub-draw
};
// Preferred DirectGLES emulation tier for glMultiDrawElements(BaseVertex). GLES has no
// such entry point in core, so every tier below is an emulation; they differ only in
// which driver capability they lean on and how many driver calls a batch costs. Like
// the Magma knob this is a preference, clamped at resolution time to what the ES
// driver actually supports, with one log line when it falls back.
enum class GLESMultiDrawMode : Uint8 {
Auto = 0, // unset: best supported tier
Ext, // one glMultiDrawElementsBaseVertexEXT
MultiIndirect, // one glMultiDrawElementsIndirectEXT over a scratch command buffer
Indirect, // one glDrawElementsIndirect per sub-draw over that same buffer
BaseVertex, // one glDrawElementsBaseVertex per sub-draw
DrawElements, // baseVertex folded into a scratch index buffer on the CPU, then plain
// glDrawElements per sub-draw (for drivers with no base-vertex draw at all)
Compute, // a compute shader flattens every sub-draw into one rebased index buffer,
// drawn by a single glDrawElements
};
// Feature toggles parsed once from environment variables in MG_ConfigLoader::Init()
// (ConfigLoader.cpp), before the accepted-env map is destroyed. All Bool fields share
// one truthy rule: the variable is set, non-empty, not "0", and not "false"
@@ -33,21 +69,102 @@ namespace MobileGL::MG_Config {
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
// promises 64-bit precision, and that is the one thing the narrowing cannot deliver.
// Off by default so an application that checks the string before using doubles keeps
// its float path; on for measuring what the conformance suite makes of the demoted
// precision. See the DemoteFloat64Pass header and the "fp64" POST row.
Bool AdvertiseFp64 = false;
// MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
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;
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 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
@@ -57,16 +174,148 @@ 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,
// Adreno does not), which means the emulation is dead code on exactly the stack the
// headless suite runs on. This forces it live so the scenarios and the CTS can
// exercise the path, and gives the device an A/B lever over the same choice.
Bool EsprytForceDepthStencilReadbackEmulation = false;
// MOBILEGL_RELAXED_SEMANTICS: relax strict core-profile rules (e.g. VAO-0 draws,
// texture-name reuse after delete) even on contexts that explicitly requested a core
// profile. Without it, relaxed semantics still apply to every context that did not
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
// version request.
Bool RelaxedSemantics = false;
// 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
// cross-pipeline vertex position invariance. Sorted-transparency "over" blends,
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
// 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 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.
MultiDrawMode MagmaMultiDrawMode = MultiDrawMode::Auto;
// MOBILEGL_ESPRYT_MULTIDRAW_MODE: preferred DirectGLES glMultiDrawElements emulation
// tier ("ext" | "multiindirect" | "indirect" | "basevertex" | "drawelements" |
// "compute", see GLESMultiDrawMode). Clamped to driver support; unset picks the best
// supported tier, which never includes "compute" - see the note on its resolution.
GLESMultiDrawMode EsprytMultiDrawMode = GLESMultiDrawMode::Auto;
// MOBILEGL_ASYNC_SHADER_COMPILE: overrides asynchronous shader compilation. Unset
// keeps the built-in default (MG_Util::Async::kAsyncShaderCompileDefault); falsy
// forces every glCompileShader/glLinkProgram to run synchronously on the calling
// thread AND withdraws GL_KHR_parallel_shader_compile, so the single switch reverts
// both the threading and the application-visible behaviour change.
QuirkOverride AsyncShaderCompile = QuirkOverride::Auto;
// MOBILEGL_ASYNC_SHADER_COMPILE_THREADS: shader-compile worker count. 0 (unset) means
// auto, which is min(4, big cores); an explicit value is honoured as given.
Uint32 AsyncShaderCompileThreads = 0;
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS: while a compile job is still in flight,
// glGetShaderiv(GL_COMPILE_STATUS) answers GL_TRUE and the shader info log reads
// empty, WITHOUT joining the job (latched per compile - see
// ShaderObject::TakeOptimisticCompileAnswer). A deliberate, bounded spec violation:
// a real failure still fails the program link with the compile log quoted. It
// exists for applications that compile hundreds of shaders serially and read the
// status right after each glCompileShader - Iris's shader-pack load - where those
// per-shader joins are what serializes the batch on its main path (Iris's gbuffer
// phase issues no program-level query between programs; program-level LINK_STATUS
// and the program info log still join truthfully, so paths that check each link
// 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
+89 -5
View File
@@ -86,6 +86,58 @@ namespace MobileGL::MG_ConfigLoader {
return it != acceptedEnvVariablesMap->end() && IsTruthyValue(it->second);
}
// Quirk overrides are tri-state: an unset variable keeps device auto-detection, a truthy
// value forces the quirk on, anything else set ("0", "false", "") forces it off.
inline MG_Config::QuirkOverride QueryEnvQuirkOverride(const String& key) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return MG_Config::QuirkOverride::Auto;
}
return IsTruthyValue(it->second) ? MG_Config::QuirkOverride::ForceOn
: MG_Config::QuirkOverride::ForceOff;
}
// Multi-draw mode is a named-value preference: unset keeps Auto (best supported tier),
// a recognized name selects that tier as the ceiling, anything else warns and keeps Auto.
inline MG_Config::MultiDrawMode QueryEnvMultiDrawMode(const String& key) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return MG_Config::MultiDrawMode::Auto;
}
String lowered = it->second;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (lowered == "ext") return MG_Config::MultiDrawMode::Ext;
if (lowered == "indirect") return MG_Config::MultiDrawMode::Indirect;
if (lowered == "unroll") return MG_Config::MultiDrawMode::Unroll;
if (lowered.empty() || lowered == "auto") return MG_Config::MultiDrawMode::Auto;
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected ext|indirect|unroll|auto, using auto",
key.c_str(), it->second.c_str());
return MG_Config::MultiDrawMode::Auto;
}
// Same contract as QueryEnvMultiDrawMode, over the DirectGLES tier names.
inline MG_Config::GLESMultiDrawMode QueryEnvGLESMultiDrawMode(const String& key) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return MG_Config::GLESMultiDrawMode::Auto;
}
String lowered = it->second;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (lowered == "ext") return MG_Config::GLESMultiDrawMode::Ext;
if (lowered == "multiindirect") return MG_Config::GLESMultiDrawMode::MultiIndirect;
if (lowered == "indirect") return MG_Config::GLESMultiDrawMode::Indirect;
if (lowered == "basevertex") return MG_Config::GLESMultiDrawMode::BaseVertex;
if (lowered == "drawelements") return MG_Config::GLESMultiDrawMode::DrawElements;
if (lowered == "compute") return MG_Config::GLESMultiDrawMode::Compute;
if (lowered.empty() || lowered == "auto") return MG_Config::GLESMultiDrawMode::Auto;
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected "
"ext|multiindirect|indirect|basevertex|drawelements|compute|auto, using auto",
key.c_str(), it->second.c_str());
return MG_Config::GLESMultiDrawMode::Auto;
}
inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
@@ -110,19 +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.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.MagmaDisableBlendedDepthWriteQuirk =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
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() {
+38 -4
View File
@@ -9,10 +9,20 @@
#pragma once
// ============== Platform-specific definitions and macros ============== //
#ifdef __ANDROID__
#undef __ANDROID_API__
#define __ANDROID_API__ 26 // force Android API level to 26 for compatibility
#endif
// No __ANDROID_API__ pin here on purpose. The effective API level is owned by
// the build system (gradle minSdk 26 -> -DANDROID_PLATFORM=android-26, enforced
// by the configure-time guard in CMakeLists.txt), not by a macro.
//
// History: this used to `#define __ANDROID_API__ 26` to *raise* the level back
// when the build configured something lower, so that pthread_getname_np (which
// bionic guards with __INTRODUCED_IN(26)) would be declared. Once a later
// change added an `#undef` in front of it, the same line started *lowering* the
// level whenever the build configured higher than 26 - and that is an
// include-order split-brain, not a compatibility knob: a TU that includes any
// libc++ header before Includes.h latches libc++'s feature macros at the
// configure-time level, and only the bionic headers pulled in afterwards see
// the lowered value. The two halves then disagree (e.g. libc++ believes
// pthread_cond_clockwait exists while bionic has since hidden its declaration).
#ifdef _WIN32
#ifndef NOMINMAX
@@ -34,8 +44,26 @@
#define MOBILEGL_EGL_API MOBILEGL_API
#define MOBILEGL_CGL_API MOBILEGL_API
#define MOBILEGL_NSOPENGL_API MOBILEGL_API
#define MOBILEGL_WGL_API MOBILEGL_API
// ====================== MobileGL configurations ======================= //
// The numeric log levels live here, not only in Log.h: MOBILEGL_ASSERT below compares
// MOBILEGL_LOG_ACTIVE_LEVEL against MOBILEGL_LOG_LEVEL_DEBUG, and in a translation unit
// that includes Defines.h without Log.h both tokens would silently evaluate to 0 in the
// preprocessor conditional - enabling the assert in exactly the INFO-level builds it is
// documented to be compiled out of. Log.h redefines them identically, which is legal.
//
// Severity order, ascending: DEBUG < INFO < WARN < ERROR < FATAL. MOBILEGL_LOG_ACTIVE_LEVEL
// names the lowest severity compiled in, so the production default INFO keeps I/W/E/F and
// drops only D. Any edit here must be mirrored in Log.h.
#ifndef MOBILEGL_LOG_LEVEL_DEBUG
#define MOBILEGL_LOG_LEVEL_DEBUG 0
#define MOBILEGL_LOG_LEVEL_INFO 1
#define MOBILEGL_LOG_LEVEL_WARN 2
#define MOBILEGL_LOG_LEVEL_ERROR 3
#define MOBILEGL_LOG_LEVEL_FATAL 4
#endif
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL
#define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_INFO
#endif
@@ -67,6 +95,12 @@
#endif
// =============================== Utils ================================ //
// Asserts are live in exactly the builds where MGLOG_D is live, i.e. DEBUG builds only;
// an INFO build (the production default) compiles them out. DEBUG is the lowest severity
// in the ordering above, so "ACTIVE <= DEBUG" is true only for ACTIVE == DEBUG - the same
// gate MGLOG_D uses in Log.h. That equivalence is what makes this gate survive the
// 2026-08-13 renumbering unchanged; the contract is and stays
// "INFO builds: asserts OFF; DEBUG builds: asserts ON".
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
#define MOBILEGL_ASSERT(condition, ...) \
do { \
+7 -1
View File
@@ -14,7 +14,13 @@ namespace MobileGL {
} // namespace MG_Config
namespace MG_Backend {
UniquePtr<BackendObject> pActiveBackendObject;
// Leak-at-exit storage: the UniquePtr itself lives on the heap and is
// never destroyed by the runtime, so process exit runs no backend
// destructors (static destruction order across TUs is undefined).
// Deterministic teardown happens inside the EGL lifecycle instead:
// the last eglTerminate calls MobileGL::Destroy(), which .reset()s
// these singletons while the process is still healthy.
UniquePtr<BackendObject>& pActiveBackendObject = *new UniquePtr<BackendObject>();
GlobalBackendFunctionsTable gBackendFunctionsTable;
} // namespace MG_Backend
} // namespace MobileGL
+2 -2
View File
@@ -49,8 +49,8 @@
#include <stacktrace>
#endif
// Include FastSTL
#include <FastSTL/UnorderedMap.h>
// Include ska::flat_hash_map
#include <ska/flat_hash_map.hpp>
// Include xxHash
#include <xxhash.h>
+94 -34
View File
@@ -9,14 +9,30 @@
#include "Init.h"
#include "Config.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Backend/DirectVulkan/DirectVulkan.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/EGLState/Core.h>
#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>
namespace MobileGL {
namespace {
Bool g_isInitialized = false;
std::atomic<Bool> g_isInitialized = false;
thread_local Bool tl_initializing = false;
std::mutex& InitMutex() {
static std::mutex mutex;
return mutex;
}
void DestroyImpl(Bool logLifecycle) {
if (!g_isInitialized) {
@@ -26,12 +42,46 @@ namespace MobileGL {
if (logLifecycle) {
MGLOG_I("MobileGL closing...");
}
glslang::FinalizeProcess();
// First, before anything else is torn down. In-flight compile/link jobs own
// their own inputs and are safe against everything below EXCEPT glslang's
// process globals and the TShader/TProgram objects hanging off pGLContext,
// both of which this function is about to destroy. This is the one
// cancellation path in the whole design that waits.
MG_Util::Async::ShaderCompilePool::Get().StopAndDrain();
// GL syncs die with their contexts, and every context is gone by the
// time full teardown runs: drain the live-sync registry while the
// backend function table can still release the backend handles (and
// 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();
MG_Impl::GLImpl::TextureImpl::pProxyTextureManager.reset();
MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo.reset();
// Must run AFTER pGLContext.reset(). FinalizeProcess -> ShFinalize deletes
// glslang's process-wide pool allocator and every cached built-in symbol table,
// while the TShader/TProgram objects owned by the shader and program objects
// still reference levels adopted from those tables. Finalizing first left live
// glslang objects pointing at freed memory for the rest of the teardown.
glslang::FinalizeProcess();
// Immediately after, and never apart from it: FinalizeProcess just deleted the
// 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) {
@@ -59,45 +109,55 @@ namespace MobileGL {
MG_Impl::Init();
MGLOG_D("MG_Impl initialized");
glslang::InitializeProcess();
// On the GL thread, before any worker can exist. glslang builds its built-in symbol
// tables lazily under a process-wide lock held for the whole build, so without this
// the first concurrent compiles of a shaderpack all serialize behind the very first
// parse and asynchronous compilation looks like it is doing nothing.
//
// Gated on the flag, because the problem it solves only exists when there are
// workers: with compilation synchronous, nothing ever contends for that lock and the
// three throwaway parses buy nothing - they just add to every eglInitialize. Read the
// flag here rather than inside PrewarmBuiltins so ShaderCompiler keeps no dependency
// on the async subsystem (ProgramUtilTest compiles that file without it).
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
MG_Util::ShaderTranspiler::ShaderCompiler::PrewarmBuiltins();
}
MGLOG_D("glslang initialized");
g_isInitialized = true;
MGLOG_I("MobileGL initialized");
}
void EnsureInitialized() {
if (g_isInitialized.load(std::memory_order_acquire)) {
return;
}
// Re-entrant call while this thread is already inside Initialize()
// (e.g. an init step routing back through a public entry point).
if (tl_initializing) {
return;
}
const std::lock_guard<std::mutex> lock(InitMutex());
if (g_isInitialized.load(std::memory_order_acquire)) {
return;
}
tl_initializing = true;
Initialize();
tl_initializing = false;
}
void Destroy() {
DestroyImpl(true);
}
#if defined(__linux__) || defined(__APPLE__)
__attribute__((constructor)) static void AutoInit() {
Initialize();
}
__attribute__((destructor)) static void AutoDestroy() {
if (MG_Config::Features.TraceSkipAutodestroy) {
return;
}
#if defined(__APPLE__)
// macOS injected dylibs can run destructors after logging/backend static state is already torn down.
return;
#else
DestroyImpl(false);
#endif
}
#endif
#ifdef _WIN32
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
Initialize();
break;
case DLL_PROCESS_DETACH:
Destroy();
break;
}
return TRUE;
}
#endif
// MobileGL's lifecycle is owned entirely by the host-API layers
// (EGL/WGL/CGL): initialization happens lazily on the first entry point
// via EnsureInitialized(), and full teardown happens deterministically
// when the last EGL display is terminated with nothing current (EGLImpl
// calls Destroy()). There is intentionally no backend-initializing static
// constructor, no static destructor, and no DllMain: the global singletons
// use leak-at-exit storage (see GlobalObjects.cpp), so a process that exits
// without eglTerminate simply leaks them to the OS instead of running
// backend destructors during static teardown. macOS has a lightweight
// dyld constructor that installs NSOpenGL dispatch hooks only; full backend
// initialization still enters here from the first hooked CGL context.
} // namespace MobileGL
+7
View File
@@ -11,6 +11,13 @@
namespace MobileGL {
void Initialize();
// Thread-safe, idempotent, and re-entrant wrapper around Initialize().
// Host layers (EGL/WGL/CGL entry points) call this lazily on first use so
// full backend initialization never depends on ELF/DLL static constructors,
// and so a fresh init can follow a full Destroy() (e.g. after the last
// eglTerminate). The macOS dyld bootstrap installs only lightweight
// NSOpenGL method hooks.
void EnsureInitialized();
void Destroy();
namespace MG_Util::Debug {
+241 -12
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,
@@ -145,6 +159,10 @@ namespace MobileGL {
GLenum buffer, GLint drawbuffer, const GLfloat* value);
void (*ClearNamedFramebufferfi)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void (*ClearNamedFramebufferiv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLint* value);
void (*ClearNamedFramebufferuiv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLuint* value);
void (*BlitFramebuffer)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
void (*BlitNamedFramebuffer)(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
@@ -156,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);
@@ -177,15 +195,18 @@ namespace MobileGL {
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
void (*GetProgramInterfaceiv)(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
GLuint (*GetProgramResourceIndex)(GLuint program, GLenum programInterface, const GLchar* name);
void (*GetProgramResourceName)(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
GLsizei* length, GLchar* name);
void (*GetProgramResourceiv)(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
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 (*ShaderStorageBlockBinding)(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
// The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) is NOT
// a backend query: it describes the program the application wrote, in the
// application's namespace, which neither backend program is in. It is answered
// entirely by MG_Impl/GLImpl/Program/ProgramInterface from the frontend reflection.
// Takes the block's GL NAME, not glShaderStorageBlockBinding's index. The index
// the application passes is the frontend interface-query enumeration's, and no
// backend shares that index space: DirectVulkan enumerates SPIR-V descriptor
// bindings and DirectGLES asks a real driver about SPIRV-Cross-generated ESSL.
// The name is the one coordinate all three agree on, so the frontend resolves the
// index against its own enumeration and each backend maps the name to its own.
void (*ShaderStorageBlockBinding)(GLuint program, const GLchar* storageBlockName,
GLuint storageBlockBinding);
// GL fence sync objects. All entries are optional (may be null); the
// frontend then falls back to always-signaled sync semantics.
// FenceSync may itself return null when the backend cannot create a
@@ -220,6 +241,39 @@ namespace MobileGL {
// and leave the query readable later.
Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
void (*DeleteBackendQuery)(BackendQueryHandle query);
// GL_SAMPLES_PASSED occlusion queries (optional; null = unsupported,
// the frontend then rejects the target). Results/deletion flow through
// GetQueryResult64 / DeleteBackendQuery like timer queries.
BackendQueryHandle (*BeginOcclusionQuery)();
void (*EndOcclusionQuery)(BackendQueryHandle query);
// Transform feedback primitive queries backed by real GPU query pools
// (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
// called while the frontend capture state is still active, so the backend
// can still see the capture program and buffer bindings.
// GL_PATCH_VERTICES; ES 3.2 spells it the same way.
void (*PatchParameteri)(GLenum pname, GLint value);
void (*BeginTransformFeedback)(GLenum primitiveMode);
void (*EndTransformFeedback)();
// ARB_transform_feedback2. A backend that leaves these null keeps the single
// implicit capture span the frontend has always modelled; the frontend state
// (paused flag, per-object bindings) is tracked either way.
void (*PauseTransformFeedback)();
void (*ResumeTransformFeedback)();
void (*BindTransformFeedback)(GLuint name);
void (*DeleteTransformFeedback)(GLuint name);
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
};
struct GlobalBackendFunctionsTable {
@@ -230,8 +284,29 @@ namespace MobileGL {
void (*SetSwapInterval)(Int interval);
};
// Coarse GPU vendor identity for gating device-specific quirks. Detected from the
// Vulkan physical-device vendorID or the GLES GL_VENDOR/GL_RENDERER strings; stays
// Unknown when detection is inconclusive, in which case auto-gated quirks stay off.
enum class GpuVendorKind : Uint8 {
Unknown = 0,
Qualcomm,
Arm,
Nvidia,
Amd,
Intel,
ImgTec,
// Software rasterizers (llvmpipe/lavapipe, SwiftShader).
Software,
};
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;
@@ -257,6 +332,13 @@ namespace MobileGL {
Int MaxIntegerSamples = 1;
Int MaxSamples = 1;
Int MaxSampleMaskWords = 1;
// Tessellation limits; defaults are the GL 4.0 core minimums.
Int MaxPatchVertices = 32;
Int MaxTessGenLevel = 64;
// GL_MIN/MAX_PROGRAM_TEXTURE_GATHER_OFFSET. Defaults are the GL 4.0 core
// minimums, which every ES 3.1 driver also guarantees.
Int MinProgramTextureGatherOffset = -8;
Int MaxProgramTextureGatherOffset = 7;
Int MaxTextureImageUnits = 32;
Int MaxVertexTextureImageUnits = 32;
Int MaxComputeTextureImageUnits = 32;
@@ -264,10 +346,28 @@ 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;
Int MaxTextureBufferSize = 65536;
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
Int TextureBufferOffsetAlignment = 1;
Int MaxUniformBufferBindings = 24;
Int MaxUniformBlockSize = 16384;
Int MaxImageUnits = 8;
@@ -278,26 +378,155 @@ 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;
Float ViewportBoundsRangeMax = 0.0f;
Int ViewportSubpixelBits = 0;
// GL 4.x fragment-interpolation offset limits. These defaults are the
// core minimums and are replaced by live GLES/Vulkan device limits.
Float MinFragmentInterpolationOffset = -0.5f;
// For four fractional bits the greatest required legal offset is
// 0.5 - 2^-4 = 0.4375 (GL 4.6 table 23.70).
Float MaxFragmentInterpolationOffset = 0.4375f;
Int FragmentInterpolationOffsetBits = 4;
Bool SupportsWideLines = false;
// Whether a framebuffer whose depth and stencil attachments are distinct
// images can be rendered to. GL only requires support when both refer to the
// same image and lets an implementation answer GL_FRAMEBUFFER_UNSUPPORTED
// otherwise, which is what DirectVulkan (one combined attachment) and the
// real ES drivers behind DirectGLES both do. Defaults to true so a backend
// that never sets it keeps the permissive behaviour.
Bool SupportsDistinctDepthStencilAttachments = true;
// Whether attaching a single layer of a 3D or array texture to a framebuffer actually
// renders to that layer. DirectGLES hands the layer straight to
// glFramebufferTextureLayer, so it does; DirectVulkan maps a GL layer onto a Vulkan
// array layer with no notion of a 3D depth slice, so it does not yet. Defaults to false
// so a backend that never sets it gets the conservative answer.
// Which layered texture targets this backend can attach ONE layer of to a framebuffer
// and then really clear, render and read back that layer. Bit (1u << TextureTarget) is
// set for each supported target. Deliberately per target rather than one flag: the three
// ways a GL layer maps onto Vulkan are independent capabilities. A 2D or 2D multisample
// array layer IS a VkImage array layer and needs nothing extra; a 3D texture's layer is
// a z slice, which needs a 2D-array-compatible image and a per-slice clear that
// vkCmdClearColorImage cannot express; a cube map array needs an image shape and the
// imageCubeArray feature before it can be attached at any layer at all. Defaults to 0 so
// a backend that never sets it gets the conservative answer.
Uint32 PerLayerFramebufferAttachmentTargets = 0;
static constexpr Uint32 PerLayerFramebufferAttachmentBit(TextureTarget target) {
return (static_cast<Int>(target) >= 0 &&
static_cast<Int>(target) < static_cast<Int>(TextureTarget::TextureTargetCount))
? (1u << static_cast<Uint32>(target))
: 0u;
}
Bool SupportsPerLayerFramebufferAttachment(TextureTarget target) const {
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;
Uint32 SubgroupSupportedFeatures = 0;
Bool SubgroupQuadOperationsInAllStages = false;
GpuVendorKind GpuVendor = GpuVendorKind::Unknown;
};
enum class WindowBackend {
Android,
X11,
MetalLayer,
// TODO: Wayland, Windows, etc.
Win32, // Handle is an HWND
// TODO: Wayland, etc.
WindowBackendCount,
Unknown = -1
};
+1 -1
View File
@@ -13,6 +13,6 @@
#include "DirectVulkan/BackendObject_DirectVulkan.h"
namespace MobileGL::MG_Backend {
extern UniquePtr<BackendObject> pActiveBackendObject;
extern UniquePtr<BackendObject>& pActiveBackendObject;
extern GlobalBackendFunctionsTable gBackendFunctionsTable;
} // namespace MobileGL::MG_Backend
File diff suppressed because it is too large Load Diff
@@ -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
+85 -11
View File
@@ -40,6 +40,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride);
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount,
GLsizei stride);
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex);
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices);
@@ -59,6 +61,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLuint* value);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter);
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
@@ -70,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);
@@ -88,15 +94,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length,
GLchar* name);
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
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 ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
Bool InitWindowSurface(NativeWindowType window);
Bool InitPbufferSurface(EGLint width, EGLint height);
Bool MakeCurrent();
@@ -121,6 +119,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
// capability read needs no current ES context, and it stays false until
// the ES capabilities have been filled in.
Bool AreTimerQueriesSupported();
// True when the host ES driver can back a GL_TEXTURE_BUFFER at all - ES 3.2 core, or
// EXT/OES_texture_buffer, with glTexBuffer resolved. Desktop GL has had buffer textures as
// core since 3.1, so the frontend advertises them unconditionally and an app may call
// glTexBuffer whenever it likes; this is the only thing standing between that call and a
// null entry point. False also means every shader declaring a samplerBuffer is
// uncompilable on this driver, which the program build reports by name.
Bool AreBufferTexturesSupported();
// Human-readable name of the buffer-texture tier for diagnostics and the driver POST:
// "core (ES 3.2)", "GL_EXT_texture_buffer", "GL_OES_texture_buffer" or "unsupported".
const char* GetBufferTextureTierName();
// glTexBuffer / glTexBufferRange through whichever spelling this driver's buffer-texture
// support actually ships: the unsuffixed names are ES 3.2 core, while an EXT/OES driver
// exports glTexBuffer{,Range}EXT / OES. Callers must have checked
// AreBufferTexturesSupported() first. CallTexBufferRange reports whether it could honour
// the range - no tier is required to expose the range form, and the whole-buffer form is
// the documented fallback.
void CallTexBuffer(GLenum target, GLenum internalFormat, GLuint buffer);
Bool CallTexBufferRange(GLenum target, GLenum internalFormat, GLuint buffer, GLintptr offset, GLsizeiptr size);
// GL timer-query objects, backed by GL_EXT_disjoint_timer_query. The
// creators return null (the frontend then falls back to an immediately
// available zero result) when the calling thread does not own the ES
@@ -130,6 +146,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendQueryHandle BeginTimeElapsedQuery();
void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp();
// GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) occlusion queries: core ES3, independent of
// GL_EXT_disjoint_timer_query and of MOBILEGL_DISABLE_TIMERQUERY. Results/deletion
// flow through GetQueryResult64/DeleteBackendQuery like the timer queries above.
BackendQueryHandle BeginOcclusionQuery();
void EndOcclusionQuery(BackendQueryHandle query);
// GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED, also core ES
// (GL_PRIMITIVES_GENERATED from ES 3.2 on). Null when the target is unavailable, in
// which case the frontend falls back to counting primitives from the draw calls.
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
void EndXfbPrimitivesQuery(BackendQueryHandle query);
Bool IsQueryResultAvailable(BackendQueryHandle query);
// Returns true when a final value landed in *outNanoseconds (a zero for
// null or stale-generation handles IS final: the frontend may cache it
@@ -146,6 +172,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// A buffer retired during frame N is safe to recycle once CompletedFrameSerial() >= N.
Uint64 CurrentFrameSerial();
Uint64 CompletedFrameSerial();
// Block (up to timeoutNs) until the given frame serial provably retired on the
// GPU, using the per-frame fence ring. False when no usable fence covers the
// serial (fence-less context, foreign thread, or the slot was recycled);
// completion state is untouched in that case.
Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs);
// Applies (or defers until the window surface exists) the app-requested
// eglSwapInterval on the native EGL surface.
void SetSwapInterval(Int interval);
@@ -154,6 +185,49 @@ namespace MobileGL::MG_Backend::DirectGLES {
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
void DestroyEGLContext();
// Transform feedback capture spans, performed by the real ES driver. The
// capture set is declared on the backend program at link time; the driver-side
// begin is deferred to the first draw of the span (ES needs the capturing
// program current and the capture buffers bound), and the end also mirrors the
// captured bytes back into the frontend buffer shadows.
void PatchParameteri(GLenum pname, GLint value);
namespace XfbImpl {
Bool AreTransformFeedbacksSupported();
// True while a capture span is open on the current transform feedback object
// (frontend Begin seen and not paused), whether or not the deferred driver-side
// Begin has been issued yet. Draw paths that would restructure the primitive
// stream, or that need to dispatch compute mid-draw, decline while it is set.
Bool IsCaptureSpanOpen();
void BeginTransformFeedback(GLenum primitiveMode);
void EndTransformFeedback();
void PauseTransformFeedback();
void ResumeTransformFeedback();
void BindTransformFeedback(GLuint name);
void DeleteTransformFeedback(GLuint name);
void OnBackendContextDestroyed();
} // namespace XfbImpl
namespace RenderStateImpl {
// Pushes the frontend's render-state block to the ES driver, diffed against what was
// last pushed.
//
// `forColorClear` names the CALLER, and the only thing it changes is the colour write
// mask handed to the driver. A draw into a colour attachment the backend widened from
// three channels to four gets that buffer's alpha channel masked OFF, so nothing can
// move the stored alpha away from the 1.0 the application's three-channel format
// implies (see FramebufferImpl::g_alphaWidenedDrawBufferMask). A CLEAR is how that 1.0
// gets there in the first place, so it must be allowed to write alpha - hence the flag
// rather than an unconditional doctoring. It is part of the sync memo, so a clear
// followed by a draw re-pushes the mask instead of early-outing on an unchanged
// frontend version.
//
// The application's own colour mask is never modified: glGet(GL_COLOR_WRITEMASK)
// answers from the frontend state, which this function only reads.
void SyncRenderState(Bool forColorClear = false);
void InvalidateSyncedRenderState();
} // namespace RenderStateImpl
extern MG_External::EGLFunctionsTable g_EGLFuncs;
extern MG_External::GLESFunctionsTable g_GLESFuncs;
extern MG_External::GLESCapabilities g_GLESCapabilities;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,972 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/MultiDraw.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 "MultiDraw.h"
#include "Managers.h"
#include <MG_State/GLState/Core.h>
#include <cstring>
#include <limits>
namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
using MG_Config::GLESMultiDrawMode;
namespace {
// ---------------------------------------------------------------------------
// Batch shape
// ---------------------------------------------------------------------------
SizeT IndexTypeSize(GLenum type) {
switch (type) {
case GL_UNSIGNED_BYTE: return 1;
case GL_UNSIGNED_SHORT: return 2;
case GL_UNSIGNED_INT: return 4;
default: return 0;
}
}
// 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) {
if (ResolveRestartSubstitution(type) != RestartSubstitutionKind::None) {
return MG_State::pGLContext->GetPrimitiveRestartIndex();
}
return MG_Util::FixedRestartIndexForGLType(type);
}
Bool RestartActive() {
return MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
}
// Vertices per primitive for the modes whose sub-draws may be concatenated into a
// single draw without changing the primitive stream. Zero for strip/loop/fan modes
// (concatenation would weld one sub-draw's last primitive to the next sub-draw's
// first) and for GL_PATCHES, whose primitive size is dynamic tessellation state.
Uint32 ConcatenablePrimitiveSize(GLenum mode) {
switch (mode) {
case GL_POINTS: return 1;
case GL_LINES: return 2;
case GL_TRIANGLES: return 3;
case GL_LINES_ADJACENCY: return 4;
case GL_TRIANGLES_ADJACENCY: return 6;
default: return 0;
}
}
// Beyond this an emulated batch would ask for a scratch allocation measured in
// hundreds of megabytes (and the scratch ring never shrinks again); decline and let
// a per-sub-draw tier handle it instead of trying and failing inside the driver.
constexpr SizeT kMaxFlattenedIndices = SizeT{1} << 24;
// The flattening dispatch is one invocation per output index. ES 3.1 only
// guarantees 65535 work groups per dimension, and exceeding it makes
// glDispatchCompute an INVALID_VALUE no-op - which would leave the draw reading an
// uninitialised index buffer rather than failing visibly. Cap the tier there
// instead of querying: 4.19M indices is far past any real multi-draw batch, and
// beyond it the per-sub-draw tiers are the better answer anyway.
constexpr SizeT kComputeWorkGroupSize = 64;
constexpr SizeT kMaxComputeWorkGroups = 65535;
constexpr SizeT kMaxComputeFlattenedIndices = kMaxComputeWorkGroups * kComputeWorkGroupSize;
Uint BoundDrawIndirectBufferId() {
const auto& indirect =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (!indirect) return 0;
const auto* resource = BufferImpl::EnsureBufferResource(indirect);
return resource ? resource->id : 0;
}
const SharedPtr<MG_State::GLState::BufferObject>& BoundIndexBuffer() {
static const SharedPtr<MG_State::GLState::BufferObject> none;
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
if (!vao) return none;
return vao->GetIndexBufferBindingSlot().GetBoundObject();
}
// The GL name PrepareForDraw left on GL_ELEMENT_ARRAY_BUFFER, i.e. what a tier
// that swaps in a scratch index buffer has to put back. Restoring the exact name
// matters beyond tidiness: the VAO twin memoises that it already synced this
// index binding and will not re-issue it on the next draw.
Uint BoundIndexBufferId() {
const auto& ibo = BoundIndexBuffer();
if (!ibo) return 0;
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
return resource ? resource->id : 0;
}
// ---------------------------------------------------------------------------
// Scratch GL objects
//
// All of them belong to the ES context and are abandoned (not deleted) when it
// dies, exactly like XfbImpl's scatter buffer: the names are the dead context's
// to reclaim, and deleting them would target whatever the successor context
// handed out for the same name.
// ---------------------------------------------------------------------------
struct ScratchBuffer {
Uint id = 0;
SizeT capacity = 0;
SizeT cursor = 0; // ring buffers only: next free byte
};
ScratchBuffer g_indirectCommands; // synthesized DrawElementsIndirectCommand array
ScratchBuffer g_rebasedIndices; // CPU-rebased index stream
ScratchBuffer g_drawInfo; // compute tier: per-sub-draw descriptors
ScratchBuffer g_flattenedIndices; // compute tier: flattened index stream
Uint g_computeProgram = 0;
Bool g_computeProgramFailed = false;
GLint g_uElementSize = -1;
GLint g_uDrawCount = -1;
GLint g_uTotalIndices = -1;
// Reused staging, so a steady stream of batches allocates nothing.
Vector<DrawElementsIndirectCommand> g_commandStaging;
Vector<Uint32> g_indexStaging;
Vector<Uint32> g_drawInfoStaging;
Vector<GLint> g_zeroBaseVertices;
// Everything below stages through GL_ARRAY_BUFFER, the manager-wide staging target
// (BufferImpl::TempBufferTarget); binding it disturbs no VAO state.
Bool EnsureScratchName(ScratchBuffer& buffer) {
if (buffer.id != 0) return true;
GLuint id = 0;
g_GLESFuncs.glGenBuffers(1, &id);
if (id == 0) return false;
buffer.id = id;
buffer.capacity = 0;
buffer.cursor = 0;
return true;
}
// Whole-buffer upload, for the two buffers that are read from offset 0 because they
// are bound as storage blocks. Respecifies rather than sub-updates: glBufferData
// orphans the previous store, so the upload never waits on a dispatch still reading
// the old contents out of the same name.
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data) {
if (bytes == 0) return true;
if (!EnsureScratchName(buffer)) return false;
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
// Grow in powers of two so a batch that creeps up in size stops respecifying.
SizeT capacity = buffer.capacity == 0 ? bytes : buffer.capacity;
while (capacity < bytes) capacity *= 2;
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(capacity), nullptr,
GL_STREAM_DRAW);
buffer.capacity = capacity;
buffer.cursor = 0;
if (data) {
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
}
return true;
}
// Ring upload, for the buffers whose consumers can address a byte offset (indirect
// commands and rewritten index streams). Respecifying per batch is what an
// orphan-every-time scheme costs, and on a desktop-class driver that allocation
// dominated the tiers that use these buffers - a multi-draw of 32 sub-draws stages
// 640 bytes and paid for a fresh store to hold them. Bump-allocating instead means
// one respecify per wrap; every byte between two wraps is written exactly once, so
// nothing in flight is overwritten, and the wrap itself orphans.
constexpr SizeT kRingAlignment = 16; // >= 4, so both command and uint32-index offsets stay legal
constexpr SizeT kMinRingBytes = 1u << 16;
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data, SizeT& outOffset) {
outOffset = 0;
if (bytes == 0) return true;
if (!EnsureScratchName(buffer)) return false;
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
const SizeT aligned = (bytes + kRingAlignment - 1) & ~(kRingAlignment - 1);
if (buffer.capacity < aligned) {
SizeT capacity = buffer.capacity == 0 ? kMinRingBytes : buffer.capacity;
while (capacity < aligned) capacity *= 2;
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(capacity), nullptr,
GL_STREAM_DRAW);
buffer.capacity = capacity;
buffer.cursor = 0;
} else if (buffer.cursor + aligned > buffer.capacity) {
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(buffer.capacity),
nullptr, GL_STREAM_DRAW);
buffer.cursor = 0;
}
outOffset = buffer.cursor;
if (data) {
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, static_cast<GLintptr>(outOffset),
static_cast<GLsizeiptr>(bytes), data);
}
buffer.cursor += aligned;
return true;
}
// ---------------------------------------------------------------------------
// Tier resolution
// ---------------------------------------------------------------------------
// Best-first, and measured rather than assumed. MobileGlues orders its own Auto
// multiindirect -> indirect -> basevertex; on both ES drivers available here that
// is backwards, because staging a command buffer per batch costs more than the
// driver entries it saves. mc_sodium_multidraw (132 batches x 32 sub-draws),
// ns/op, median of three:
//
// NVIDIA ES 3.2 Mesa llvmpipe ES 3.2
// ext n/a 19300
// basevertex 2500 25200
// multiindirect 5700 27600
// drawelements 5600 28700
// indirect 5800 31000
//
// Ring-allocating the command staging (instead of respecifying per batch) was
// tried first and moved the indirect tiers by less than noise, so the cost is the
// indirect draw path itself, not the upload. Only "ext" - a real multi-draw entry
// point rather than an indirect one - actually beats replaying the sub-draws.
//
// The compute tier is deliberately absent from the ladder: it rewrites the
// primitive stream rather than replaying it, and it measured slowest of all here,
// so it stays opt-in behind the env knob (the same call MobileGlues makes - its
// Auto never selects Compute either).
constexpr GLESMultiDrawMode kAutoLadder[] = {
GLESMultiDrawMode::Ext, GLESMultiDrawMode::BaseVertex, GLESMultiDrawMode::MultiIndirect,
GLESMultiDrawMode::Indirect, GLESMultiDrawMode::DrawElements,
};
Bool SupportsTier(GLESMultiDrawMode tier) {
return IsTierSupported(g_GLESCapabilities, g_GLESFuncs, tier);
}
GLESMultiDrawMode g_resolvedTier = GLESMultiDrawMode::Auto;
Bool g_tierResolved = false;
String g_tierResolution;
void ResolveTierOnce() {
if (g_tierResolved) return;
g_tierResolved = true;
g_resolvedTier =
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
&g_tierResolution);
MGLOG_D("DirectGLES multi-draw: %s", g_tierResolution.c_str());
}
// Which tiers have already announced themselves, one bit per GLESMultiDrawMode.
// The resolution line above says which tier was CHOSEN; this says which one a
// batch actually went through, and the two differ whenever a batch's shape
// demotes it. Worth a line each: a multi-draw path that resolves to a tier and
// then quietly runs a different one is exactly how "the batch drew nothing"
// hides.
Uint32 g_announcedTiers = 0;
void NoteTierExecuted(GLESMultiDrawMode tier) {
const Uint32 bit = 1u << static_cast<Uint32>(tier);
if (g_announcedTiers & bit) return;
g_announcedTiers |= bit;
MGLOG_D("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
}
// The tier this particular batch can actually take. A tier is demoted here when
// the batch's own shape - not the driver - rules it out; the compute tier keeps
// 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 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
// own index (the spec's value); nothing else observes the difference. The
// emulated gl_BaseVertex is one uniform for the same reason, so a batch whose
// sub-draws carry their own base vertices unrolls too - even the Ext tier,
// which hands the driver the whole basevertex array, can only leave ONE value
// in the uniform the shader reads.
const Bool batched = tier == GLESMultiDrawMode::Ext || tier == GLESMultiDrawMode::MultiIndirect ||
tier == GLESMultiDrawMode::Compute;
if (batched && (programReadsDrawID || perSubDrawBaseVertex)) {
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
: GLESMultiDrawMode::DrawElements;
}
// The indirect tiers describe each sub-draw as an element offset into the
// bound element array buffer. A client-memory index array has no such buffer,
// and indirect draws are not defined without one.
if (!hasIndexBuffer &&
(tier == GLESMultiDrawMode::MultiIndirect || tier == GLESMultiDrawMode::Indirect)) {
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
: GLESMultiDrawMode::DrawElements;
}
return tier;
}
// ---------------------------------------------------------------------------
// Index rewriting, shared by the two tiers that fold base vertices into indices
// ---------------------------------------------------------------------------
// Both of those tiers emit GL_UNSIGNED_INT regardless of the source type. Keeping
// the source width would be wrong, not merely tight: GL adds baseVertex to the
// index at full precision, so a GL_UNSIGNED_SHORT index plus a base vertex past
// 65535 addresses a vertex the source type cannot spell. Widening also gives the
// rewritten stream a restart sentinel (0xFFFFFFFF) that survives the rebase.
void RebaseIndices(const Uint8* source, SizeT sourceIndexCount, SizeT indexSize, Int32 baseVertex,
Bool restartActive, Uint32 restartSentinel, Uint32* out) {
const Uint32 baseVertexBits = static_cast<Uint32>(baseVertex);
for (SizeT i = 0; i < sourceIndexCount; ++i) {
Uint32 value = 0;
switch (indexSize) {
case 1: value = source[i]; break;
case 2: {
Uint16 narrow = 0;
std::memcpy(&narrow, source + i * 2, sizeof(narrow));
value = narrow;
break;
}
default: std::memcpy(&value, source + i * 4, sizeof(value)); break;
}
// Unsigned wraparound is the defined behaviour for a negative base vertex.
out[i] = (restartActive && value == restartSentinel) ? 0xFFFFFFFFu : value + baseVertexBits;
}
}
// CPU-readable bytes of one sub-draw's indices, from the frontend shadow of the
// bound index buffer or straight from the client array. Null when the sub-draw
// would read outside the buffer.
const Uint8* ResolveSubDrawIndices(const SharedPtr<MG_State::GLState::BufferObject>& indexBuffer,
const Uint8* indexBufferBytes, SizeT indexBufferSize, const void* indices,
SizeT indexCount, SizeT indexSize) {
if (!indexBuffer) {
return static_cast<const Uint8*>(indices);
}
if (!indexBufferBytes) return nullptr;
const SizeT byteOffset = reinterpret_cast<SizeT>(indices);
const SizeT byteEnd = byteOffset + indexCount * indexSize;
if (byteEnd > indexBufferSize || byteEnd < byteOffset) return nullptr;
return indexBufferBytes + byteOffset;
}
// ---------------------------------------------------------------------------
// Tier: Ext - one glMultiDrawElementsBaseVertexEXT
// ---------------------------------------------------------------------------
Bool RunExt(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, GLsizei drawcount,
const GLint* basevertex) {
if (!SupportsTier(GLESMultiDrawMode::Ext)) return false;
const GLint* baseVertices = basevertex;
if (!baseVertices) {
// glMultiDrawElements: every base vertex is 0, but the entry point still
// wants an array. One permanently-zero vector serves every such batch.
if (g_zeroBaseVertices.size() < static_cast<SizeT>(drawcount)) {
g_zeroBaseVertices.resize(static_cast<SizeT>(drawcount), 0);
}
baseVertices = g_zeroBaseVertices.data();
}
g_GLESFuncs.glMultiDrawElementsBaseVertexEXT(mode, count, type, indices, drawcount, baseVertices);
NoteTierExecuted(GLESMultiDrawMode::Ext);
return true;
}
// ---------------------------------------------------------------------------
// Tiers: MultiIndirect / Indirect - synthesized indirect commands
// ---------------------------------------------------------------------------
Bool RunIndirect(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID,
Bool feedBaseVertex) {
if (!SupportsTier(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect)) return false;
const SizeT indexSize = IndexTypeSize(type);
if (indexSize == 0) return false;
// Indirect commands address indices as an element offset into the bound element
// array buffer, and an indirect draw is not defined without one.
const auto& indexBuffer = BoundIndexBuffer();
if (!indexBuffer) return false;
g_commandStaging.resize(static_cast<SizeT>(drawcount));
for (GLsizei i = 0; i < drawcount; ++i) {
const SizeT byteOffset = reinterpret_cast<SizeT>(indices[i]);
// firstIndex counts elements, so an offset that is not a whole number of
// them cannot be expressed as a command at all.
if (byteOffset % indexSize != 0) return false;
auto& command = g_commandStaging[static_cast<SizeT>(i)];
command.count = count[i] > 0 ? static_cast<Uint32>(count[i]) : 0u;
command.instanceCount = 1;
command.firstIndex = static_cast<Uint32>(byteOffset / indexSize);
command.baseVertex = basevertex ? basevertex[i] : 0;
command.baseInstance = 0;
}
const SizeT commandBytes = g_commandStaging.size() * sizeof(DrawElementsIndirectCommand);
SizeT commandBase = 0;
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(), commandBase)) {
return false;
}
// Every synthesized command carries baseInstance 0. Say so through the direct
// path, which also clears the indirect-params word index a preceding real
// indirect draw may have left pointing into its own command buffer.
SetCurrentBaseInstance(0);
const Uint previousIndirectBinding = BoundDrawIndirectBufferId();
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id);
if (batched) {
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);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
});
}
if (feedDrawID) SetCurrentDrawID(0);
if (feedBaseVertex) SetCurrentBaseVertex(0);
}
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, previousIndirectBinding);
NoteTierExecuted(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect);
return true;
}
// ---------------------------------------------------------------------------
// Tier: BaseVertex - the per-sub-draw replay
// ---------------------------------------------------------------------------
Bool RunBaseVertexLoop(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID, Bool feedBaseVertex) {
if (!SupportsTier(GLESMultiDrawMode::BaseVertex)) return false;
for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] <= 0) continue;
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
if (feedBaseVertex) SetCurrentBaseVertex(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);
NoteTierExecuted(GLESMultiDrawMode::BaseVertex);
return true;
}
// ---------------------------------------------------------------------------
// Tier: DrawElements - base vertices folded into a scratch index stream
// ---------------------------------------------------------------------------
Bool RunRebasedDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID,
Bool feedBaseVertex) {
const SizeT indexSize = IndexTypeSize(type);
if (indexSize == 0) return false;
SizeT total = 0;
for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] > 0) total += static_cast<SizeT>(count[i]);
}
if (total == 0) return true;
if (total > kMaxFlattenedIndices) return false;
const auto& indexBuffer = BoundIndexBuffer();
const Uint8* indexBufferBytes = nullptr;
SizeT indexBufferSize = 0;
if (indexBuffer) {
// The shadow is the source of truth for CPU reads, but a persistent map or
// a shader write may have moved past it since the last sync.
indexBuffer->SyncPersistentMappedRange();
indexBuffer->SyncGpuWrites();
indexBufferBytes = indexBuffer->MappedData();
indexBufferSize = indexBuffer->GetSize();
}
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) {
if (count[i] <= 0) continue;
const SizeT subDrawCount = static_cast<SizeT>(count[i]);
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
subDrawCount, indexSize);
if (!source) {
MGLOG_E_ONCE("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
"buffer; skipping the batch",
i);
return false;
}
RebaseIndices(source, subDrawCount, indexSize, basevertex ? basevertex[i] : 0, restartActive,
restartSentinel, g_indexStaging.data() + cursor);
cursor += subDrawCount;
}
SizeT indexBase = 0;
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(), indexBase)) {
return false;
}
const Uint previousIndexBinding = BoundIndexBufferId();
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, g_rebasedIndices.id);
cursor = 0;
for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] <= 0) continue;
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
// The base vertex is folded into the rewritten index stream here, so the
// 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);
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);
if (feedBaseVertex) SetCurrentBaseVertex(0);
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
NoteTierExecuted(GLESMultiDrawMode::DrawElements);
return true;
}
// ---------------------------------------------------------------------------
// Tier: Compute - the whole batch flattened into one rebased index stream
// ---------------------------------------------------------------------------
// One index per invocation. The sub-draw an output slot belongs to is found by
// binary search over the inclusive prefix sums of the sub-draw counts, which is
// why the descriptors are sorted by construction. Sub-draws with a zero count
// repeat the previous prefix sum and are therefore skipped by the search.
//
// Three storage blocks, not the five the shape suggests: ES 3.1 only guarantees
// four per compute stage, so the per-sub-draw descriptors share one buffer.
constexpr const char* kFlattenComputeSource = R"(#version 310 es
layout(local_size_x = 64) in;
uniform uint uElementSize;
uniform uint uDrawCount;
uniform uint uTotalIndices;
layout(std430, binding = 0) readonly buffer SourceIndices { uint sourceWords[]; };
layout(std430, binding = 1) readonly buffer DrawInfo { uint drawInfo[]; };
layout(std430, binding = 2) writeonly buffer FlatIndices { uint flatIndices[]; };
uint ReadSourceIndex(uint element) {
if (uElementSize == 4u) {
return sourceWords[element];
}
if (uElementSize == 2u) {
uint word = sourceWords[element >> 1u];
return (word >> ((element & 1u) * 16u)) & 0xFFFFu;
}
uint word = sourceWords[element >> 2u];
return (word >> ((element & 3u) * 8u)) & 0xFFu;
}
void main() {
uint outIndex = gl_GlobalInvocationID.x;
if (outIndex >= uTotalIndices) {
return;
}
uint low = 0u;
uint high = uDrawCount - 1u;
while (low < high) {
uint mid = low + (high - low) / 2u;
if (drawInfo[mid * 3u + 2u] > outIndex) {
high = mid;
} else {
low = mid + 1u;
}
}
uint localIndex = outIndex - (low == 0u ? 0u : drawInfo[(low - 1u) * 3u + 2u]);
// Unsigned wraparound is the defined behaviour for a negative base vertex. No
// restart sentinel handling: the tier declines outright while restart is enabled.
flatIndices[outIndex] = ReadSourceIndex(localIndex + drawInfo[low * 3u]) + drawInfo[low * 3u + 1u];
}
)";
struct FlattenedStream {
Uint bufferId = 0;
SizeT indexCount = 0;
};
Bool EnsureComputeProgram() {
if (g_computeProgram != 0) return true;
if (g_computeProgramFailed) return false;
g_computeProgramFailed = true; // cleared again only on a complete success
const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER);
if (shader == 0) {
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
return false;
}
const char* source = kFlattenComputeSource;
g_GLESFuncs.glShaderSource(shader, 1, &source, nullptr);
g_GLESFuncs.glCompileShader(shader);
GLint status = GL_FALSE;
g_GLESFuncs.glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status != GL_TRUE) {
char log[1024] = {};
g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
g_GLESFuncs.glDeleteShader(shader);
return false;
}
const GLuint program = g_GLESFuncs.glCreateProgram();
if (program == 0) {
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateProgram failed");
g_GLESFuncs.glDeleteShader(shader);
return false;
}
g_GLESFuncs.glAttachShader(program, shader);
g_GLESFuncs.glLinkProgram(program);
g_GLESFuncs.glDeleteShader(shader);
g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status != GL_TRUE) {
char log[1024] = {};
g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
g_GLESFuncs.glDeleteProgram(program);
return false;
}
g_computeProgram = program;
g_uElementSize = g_GLESFuncs.glGetUniformLocation(program, "uElementSize");
g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount");
g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices");
g_computeProgramFailed = false;
MGLOG_D("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
return true;
}
// Builds the flattened stream, or leaves `out` empty when this batch's shape rules
// the tier out. Runs BEFORE PrepareForDraw - see the call site - so it may leave
// the compute program current and the first storage points unbound; the
// preparation that follows re-establishes both.
void FlattenWithCompute(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex, FlattenedStream& out) {
if (!SupportsTier(GLESMultiDrawMode::Compute)) return;
const SizeT indexSize = IndexTypeSize(type);
if (indexSize == 0) return;
// Merging sub-draws into a single draw only reproduces the original primitive
// stream for list-shaped modes: a strip, loop or fan would gain primitives
// spanning the seam between two sub-draws.
const Uint32 primitiveSize = ConcatenablePrimitiveSize(mode);
if (primitiveSize == 0) return;
// Primitive restart defeats the whole-multiple-of-a-primitive argument below,
// even for a list mode. A restart ends the current primitive, so a sub-draw of
// six GL_TRIANGLES indices with a restart after the third emits ONE triangle
// and drops the two leftover vertices - and once concatenated those leftovers
// find a third vertex in the next sub-draw and become a triangle that GL never
// draws. Splicing separator sentinels into the flattened stream could fix it,
// at the cost of a per-sub-draw offset the prefix-sum layout does not carry;
// declining is the honest trade for a tier that is already opt-in.
if (RestartActive()) return;
// The shader reads the source indices as a storage buffer, so there has to be
// a real buffer to read - a client-memory index array has none.
const auto& indexBuffer = BoundIndexBuffer();
if (!indexBuffer) return;
// A dispatch inside an open capture span is not legal, and the span would also
// observe one merged draw rather than the batch it asked for.
if (XfbImpl::IsCaptureSpanOpen()) return;
auto* sourceResource = BufferImpl::EnsureBufferResource(indexBuffer);
if (!sourceResource || sourceResource->id == 0) return;
const SizeT sourceSize = indexBuffer->GetSize();
// std430 addresses the source as uint[]; a tail shorter than a word is not
// reachable, so a narrow index type needs a word-multiple buffer.
if (indexSize < 4 && (sourceSize % 4) != 0) return;
g_drawInfoStaging.resize(3 * static_cast<SizeT>(drawcount));
SizeT total = 0;
for (GLsizei i = 0; i < drawcount; ++i) {
const SizeT subDrawCount = count[i] > 0 ? static_cast<SizeT>(count[i]) : 0;
// GL drops a trailing partial primitive per sub-draw; concatenation would
// instead splice it onto the next sub-draw's first vertices.
if (subDrawCount % primitiveSize != 0) return;
const SizeT byteOffset = reinterpret_cast<SizeT>(indices[i]);
if (byteOffset % indexSize != 0) return;
if (subDrawCount != 0) {
const SizeT byteEnd = byteOffset + subDrawCount * indexSize;
if (byteEnd > sourceSize || byteEnd < byteOffset) return;
}
total += subDrawCount;
if (total > kMaxComputeFlattenedIndices) return;
const SizeT slot = 3 * static_cast<SizeT>(i);
g_drawInfoStaging[slot] = static_cast<Uint32>(byteOffset / indexSize);
g_drawInfoStaging[slot + 1] = static_cast<Uint32>(basevertex ? basevertex[i] : 0);
g_drawInfoStaging[slot + 2] = static_cast<Uint32>(total);
}
if (total == 0) return; // nothing to draw; the ordinary tiers no-op just as well
if (!EnsureComputeProgram()) return;
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data())) {
return;
}
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr)) return;
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, sourceResource->id);
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 1, g_drawInfo.id);
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 2, g_flattenedIndices.id);
g_GLESFuncs.glUseProgram(g_computeProgram);
PrgramImpl::g_lastUsedBackendProgramId = g_computeProgram;
if (g_uElementSize >= 0) g_GLESFuncs.glUniform1ui(g_uElementSize, static_cast<GLuint>(indexSize));
if (g_uDrawCount >= 0) g_GLESFuncs.glUniform1ui(g_uDrawCount, static_cast<GLuint>(drawcount));
if (g_uTotalIndices >= 0) g_GLESFuncs.glUniform1ui(g_uTotalIndices, static_cast<GLuint>(total));
g_GLESFuncs.glDispatchCompute(
static_cast<GLuint>((total + kComputeWorkGroupSize - 1) / kComputeWorkGroupSize), 1, 1);
g_GLESFuncs.glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT);
// Hand the storage points back to their GL default. PrepareForDraw re-syncs
// only the points the app has actually touched, so leaving a scratch buffer on
// an untouched point would keep it visible to the next shader that declares one.
for (Uint point = 0; point < 3; ++point) {
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, point, 0);
}
NoteTierExecuted(GLESMultiDrawMode::Compute);
out.bufferId = g_flattenedIndices.id;
out.indexCount = total;
}
} // namespace
// -------------------------------------------------------------------------------
// Public surface
// -------------------------------------------------------------------------------
Bool IsTierSupported(const MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& funcs,
GLESMultiDrawMode tier) {
const Bool esAtLeast31 =
caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1);
switch (tier) {
case GLESMultiDrawMode::Ext:
return caps.SupportsMultiDrawElementsBaseVertex;
case GLESMultiDrawMode::MultiIndirect:
return caps.SupportsMultiDrawIndirect && esAtLeast31 && funcs.glDrawElementsIndirect != nullptr;
case GLESMultiDrawMode::Indirect:
return esAtLeast31 && funcs.glDrawElementsIndirect != nullptr;
case GLESMultiDrawMode::BaseVertex:
return caps.SupportsDrawElementsBaseVertex;
case GLESMultiDrawMode::DrawElements:
// Plain glDrawElements over a rewritten index stream: ES 2 core, so this is
// the floor every other tier can fall back to.
return true;
case GLESMultiDrawMode::Compute:
// Three storage blocks, which is inside the four ES 3.1 guarantees per stage.
return caps.SupportsComputeShader && caps.MaxComputeShaderStorageBlocks >= 3 &&
funcs.glBindBufferBase != nullptr;
case GLESMultiDrawMode::Auto:
break;
}
return false;
}
GLESMultiDrawMode ResolveTier(const MG_External::GLESCapabilities& caps,
const MG_External::GLESFunctionsTable& funcs, GLESMultiDrawMode requested,
String* explanation) {
const auto bestAuto = [&]() {
for (const GLESMultiDrawMode tier : kAutoLadder) {
if (IsTierSupported(caps, funcs, tier)) return tier;
}
return GLESMultiDrawMode::DrawElements;
};
GLESMultiDrawMode resolved = GLESMultiDrawMode::DrawElements;
String line;
if (requested == GLESMultiDrawMode::Auto) {
resolved = bestAuto();
line = String("auto -> ") + TierName(resolved);
} else if (IsTierSupported(caps, funcs, requested)) {
resolved = requested;
line = String("MOBILEGL_ESPRYT_MULTIDRAW_MODE=") + TierName(requested) + " -> " + TierName(resolved);
} else {
resolved = bestAuto();
line = String("MOBILEGL_ESPRYT_MULTIDRAW_MODE=") + TierName(requested) +
" requested but unsupported by this driver -> " + TierName(resolved);
}
if (explanation) {
String supported;
for (const GLESMultiDrawMode tier : kAutoLadder) {
if (!IsTierSupported(caps, funcs, tier)) continue;
if (!supported.empty()) supported += ", ";
supported += TierName(tier);
}
if (IsTierSupported(caps, funcs, GLESMultiDrawMode::Compute)) {
supported += supported.empty() ? "compute (opt-in)" : ", compute (opt-in)";
}
*explanation = line + " (driver supports: " + supported + ")";
}
return resolved;
}
const char* TierName(GLESMultiDrawMode tier) {
switch (tier) {
case GLESMultiDrawMode::Auto: return "auto";
case GLESMultiDrawMode::Ext: return "ext";
case GLESMultiDrawMode::MultiIndirect: return "multiindirect";
case GLESMultiDrawMode::Indirect: return "indirect";
case GLESMultiDrawMode::BaseVertex: return "basevertex";
case GLESMultiDrawMode::DrawElements: return "drawelements";
case GLESMultiDrawMode::Compute: return "compute";
}
return "unknown";
}
GLESMultiDrawMode ResolvedTier() {
ResolveTierOnce();
return g_resolvedTier;
}
String DescribeTierResolution() {
ResolveTierOnce();
return g_tierResolution;
}
void OnBackendContextDestroyed() {
g_indirectCommands = {};
g_rebasedIndices = {};
g_drawInfo = {};
g_flattenedIndices = {};
g_computeProgram = 0;
g_computeProgramFailed = false;
g_uElementSize = -1;
g_uDrawCount = -1;
g_uTotalIndices = -1;
}
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex) {
if (drawcount <= 0 || !count || !indices) return;
// 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;
// The compute tier dispatches BEFORE the draw state is established: doing it
// afterwards would mean unpicking the program, SSBO and index bindings
// PrepareForDraw just made, and a dispatch inside an open transform feedback
// span is not legal at all. On success it hands back a flattened index stream.
// A batch whose sub-draws carry their own base vertices cannot be flattened either
// when the program reads gl_BaseVertex: one draw call leaves one uniform value.
// Asked conservatively because this decision precedes PrepareForDraw - see
// CurrentProgramMayNeedPerSubDrawBuiltins. Flattening is the irreversible half:
// once the batch is one draw the values are gone, whereas declining to flatten only
// costs the unrolled tier.
FlattenedStream flattened;
if (ResolvedTier() == GLESMultiDrawMode::Compute &&
!CurrentProgramMayNeedPerSubDrawBuiltins(basevertex != nullptr)) {
FlattenWithCompute(mode, count, type, indices, drawcount, basevertex, flattened);
}
PrepareForDraw(DrawSyncBit::IndexBuffer);
if (flattened.indexCount != 0) {
const Uint previousIndexBinding = BoundIndexBufferId();
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
});
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
return;
}
// Now that PrepareForDraw has synced the program, both questions have real answers;
// 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, arbitraryRestart);
Bool drawn = false;
switch (tier) {
case GLESMultiDrawMode::Ext:
drawn = RunExt(mode, count, type, indices, drawcount, basevertex);
break;
case GLESMultiDrawMode::MultiIndirect:
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID,
feedBaseVertex);
break;
case GLESMultiDrawMode::Indirect:
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID,
feedBaseVertex);
break;
case GLESMultiDrawMode::BaseVertex:
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
break;
case GLESMultiDrawMode::DrawElements:
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
feedBaseVertex);
break;
case GLESMultiDrawMode::Compute:
// Its pre-pass ran above; reaching here means it declined this batch's shape.
break;
case GLESMultiDrawMode::Auto:
break; // resolution never yields Auto
}
// 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 - 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) {
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
feedBaseVertex);
}
if (!drawn) {
MGLOG_E_ONCE("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
"the batch was dropped",
drawcount, mode, type);
}
}
} // namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl
@@ -0,0 +1,64 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/MultiDraw.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include <Config.h>
#include "DirectGLES.h"
// Emulation of the desktop glMultiDrawElements / glMultiDrawElementsBaseVertex entry
// points on OpenGL ES, which has neither in core.
//
// Every strategy below is an emulation; they differ only in which driver capability
// they lean on and in how many driver entries a batch of N sub-draws costs. The design
// follows MobileGlues (MobileGL-Dev/MobileGlues, gl/multidraw.cpp) tier for tier, plus
// the native GL_EXT_multi_draw_arrays interaction that MobileGL already had:
//
// Ext one glMultiDrawElementsBaseVertexEXT 1 driver entry
// MultiIndirect one glMultiDrawElementsIndirectEXT 1 driver entry + 1 upload
// Indirect N x glDrawElementsIndirect N + 1 upload
// BaseVertex N x glDrawElementsBaseVertex N
// DrawElements N x glDrawElements over CPU-rebased indices N + 1 upload
// Compute 1 x glDrawElements over a GPU-flattened, 1 dispatch + 1 entry
// rebased index stream
//
// Which one runs is resolved once per ES context from the driver's capabilities,
// capped by MOBILEGL_ESPRYT_MULTIDRAW_MODE, and can additionally be demoted per batch
// when the batch's own shape rules a tier out (see ResolveTierForBatch in the .cpp).
namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// The tier this ES context resolved to, computed on first use and stable after.
MG_Config::GLESMultiDrawMode ResolvedTier();
// "multiindirect", "compute", ... - stable identifiers, also used by the POST row.
const char* TierName(MG_Config::GLESMultiDrawMode tier);
// One line naming the resolved tier, the tiers the driver can support, and the env
// clamp if one applied. For DriverPost and the startup log.
String DescribeTierResolution();
// The resolution itself, as a pure function of a capability set: the backend feeds
// it the live ES context's capabilities, DriverPost feeds it the ones it probed
// standalone, and both therefore report the same tier. `explanation`, when non-null,
// receives the "requested -> resolved (driver supports: ...)" line.
MG_Config::GLESMultiDrawMode ResolveTier(const MG_External::GLESCapabilities& caps,
const MG_External::GLESFunctionsTable& funcs,
MG_Config::GLESMultiDrawMode requested, String* explanation);
// Whether one tier is runnable on the given capability set, for per-row POST output.
Bool IsTierSupported(const MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& funcs,
MG_Config::GLESMultiDrawMode tier);
// Runs `drawcount` indexed sub-draws as one glMultiDrawElements(BaseVertex) call
// would. `basevertex` is null for the plain glMultiDrawElements entry point (every
// base vertex is 0). Owns the whole draw, preparation included: callers must not
// have run PrepareForDraw, because the compute tier has to dispatch before the
// draw state is established.
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex);
// The ES context is gone: every scratch buffer and the compute program belonged to
// it, so drop the names without deleting them (the dead context reclaims them).
void OnBackendContextDestroyed();
} // namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl
File diff suppressed because it is too large Load Diff
+437
View File
@@ -9,6 +9,8 @@
#pragma once
#include <Includes.h>
#include <MG_State/GLState/Core.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Texture/TextureFormatProcessor.h>
namespace MobileGL::MG_Backend::DirectGLES {
namespace DebugImpl {
@@ -34,13 +36,148 @@ namespace MobileGL::MG_Backend::DirectGLES {
} // namespace VertexArrayImpl
namespace TextureImpl {
// Whether images on this format-capability target can back a colour attachment, and so
// need a colour-renderable storage format even when the frontend asked for a
// three-channel one ES never renders to. Shared by the capability probe (which passes the
// capabilities it has just queried, before the globals are published) and by the
// allocation path (which reads the active backend's), so the format the cache was probed
// with is always the format the image is created with.
Bool TargetRequiresRenderableFormat(SizeT targetIndex);
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);
void GenerateRenderbufferFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType);
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);
// True when the format the image is actually created with has an alpha channel the
// frontend format does not (the three-channel colour-renderable widening). GL reads such
// a channel back as 1.0, so any swizzle source of ALPHA has to be answered with ONE and
// any readback of the image has to overwrite the alpha the draw happened to leave there.
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
@@ -88,6 +225,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
// bytes, dst receives width * GetReadbackDstPixelSize(mapping, type) bytes.
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
const ReadbackChannelMapping& mapping, GLenum type);
// Stores wide RGBA(_INTEGER) rows into the client pointer or the bound PACK pixel buffer,
// honoring the client-side PACK pixel-store parameters (row length, alignment, skips,
// swap-bytes, and - when applyPackImageParams - image height/skip images). Shared by the
// DirectGLES and DirectVulkan readback conversion paths.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams);
// Stores packed 32-bit source words verbatim, with the same destination addressing, PACK
// parameters and pixel-pack-buffer handling as StoreWideRowsToClient. For the sources whose
// storage word already IS the client word (MG_Util::IsRawPackedPixelTransfer): routing those
// through the wide float intermediate re-encodes them, and the RGB9_E5 encoder canonicalizes
// the shared exponent, so glGetTexImage would answer with different bits than were stored.
// `srcWords` holds sliceHeight * sliceCount tightly stacked rows of `width` 32-bit words.
// False when `type` is not a 4-byte packed type.
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
GLenum type, void* pixels, Bool applyPackImageParams);
} // namespace ReadbackImpl
namespace PrgramImpl {
@@ -96,7 +251,289 @@ namespace MobileGL::MG_Backend::DirectGLES {
String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
Uint32 unormOutputMask);
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
// Legacy GLSL's gl_FragColor is broadcast to every enabled draw buffer (GL 4.6
// 15.2.3), but ShaderSourceProcessor lowers it to the single output mg_FragColor,
// which only ever reaches draw buffer 0. Replicates it across `drawBufferCount`
// outputs and copies the value into them at the end of main. A no-op for
// drawBufferCount <= 1, i.e. for everything but a framebuffer that actually
// enables several draw buffers, so the ordinary single-target shader is untouched.
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
// SPIRV-Cross emits `#extension GL_EXT_texture_buffer : require` for every buffer-texture
// sampler when it targets ESSL below 320, and offers no way to ask for the OES spelling.
// On a driver that advertises only GL_OES_texture_buffer that directive is a compile
// error, so the name is retargeted in the emitted source. A no-op on every other tier:
// ES 3.2 needs no directive at all and an EXT driver already has the right one.
String RetargetTextureBufferExtension(String glslCode,
MG_External::GLESCapabilities::TextureBufferTier tier);
// Adds `#extension GL_NV_image_formats : require` when the shader carries an image
// format qualifier GLSL ES has no core spelling for. SPIRV-Cross prints the format and
// asks for nothing, so the request has to be made here. `needed` is the caller's answer,
// because only it knows which formats are in play AND whether the driver advertises the
// 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
// format in, but SPIRV-Cross throws rather than printing the formats it calls
// desktop-only when it targets ESSL - r8ui among them, which is what the stencil half of
// KHR-GL4x.packed_depth_stencil.stencil_texturing binds - and a throw loses the whole
// stage. So those formats stay out of the module and are spelled here instead, on the
// emitted text, where nothing can refuse them.
//
// Declarations that already carry a format are left exactly as they are, whoever wrote
// it. Must run before RemoveLayoutBinding, which is where an image's layout qualifier
// 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 (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).
// SPIRV-Cross emits NEITHER for an image the shader both reads and writes: it
// speculatively decorates every storage image NonWritable+NonReadable
// (fixup_image_load_store_access), then OpImageRead clears NonReadable and
// OpImageWrite clears NonWritable, and to_qualifiers_glsl only prints `readonly`
// from NonWritable and `writeonly` from NonReadable. Desktop GLSL is happy with the
// 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, 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, `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
// shader that used to compile into a link failure. ES only guarantees 4 fragment image
// uniforms, so a shader with more than half the limit in read+write images is the case
// to watch.
//
// 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. 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_";
// ES has no per-texture/sampler LOD bias at all (GL_TEXTURE_LOD_BIAS is desktop
// only; Vulkan spells it VkSamplerCreateInfo::mipLodBias), so it has to reach the
// shader as a uniform and be folded into every lookup's level of detail. Declares
// one `uniform highp float mg_lodBias_<sampler>;` per mip-capable sampler and adds
// it to the bias / explicit-LOD argument of every lookup that takes one. Draws push
// the bound texture's (or sampler object's) value into it; a shader whose samplers
// all have a zero bias is therefore unaffected. Returns the source unchanged when
// there is nothing to rewrite.
//
// 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_ESPRYT_AVOID_EXPLICIT_LOD_BIAS).
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false);
} // namespace PrgramImpl
namespace Utils {
@@ -9,15 +9,19 @@
#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"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Texture/TextureFormatProcessor.h"
#include "MG_Util/Async/ShaderCompilePool.h"
#include <Config.h>
#include <cmath>
#include <cstdlib>
#include <cstring>
@@ -40,13 +44,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool IsLayeredTarget(TextureTarget target) {
return target == TextureTarget::Texture3D || target == TextureTarget::Texture1DArray ||
target == TextureTarget::Texture2DArray || target == TextureTarget::TextureCubeMap ||
target == TextureTarget::TextureCubeMapArray ||
target == TextureTarget::Texture2DMultisampleArray;
target == TextureTarget::TextureCubeMapArray || target == TextureTarget::Texture2DMultisampleArray;
}
Bool IsMultisampleTarget(TextureTarget target) {
return target == TextureTarget::Texture2DMultisample ||
target == TextureTarget::Texture2DMultisampleArray;
return target == TextureTarget::Texture2DMultisample || target == TextureTarget::Texture2DMultisampleArray;
}
Bool IsTextureBufferTarget(TextureTarget target) {
@@ -58,8 +60,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLenum normalizedInternalFormat = glFormat;
GLenum imageFormat = GL_RGBA;
GLenum imageType = GL_UNSIGNED_BYTE;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
glFormat, PixelFormatNormalizeOptionBit::None, &normalizedInternalFormat, &imageFormat, &imageType);
MG_Util::TextureFormatProcessor::NormalizePixelFormat(glFormat, PixelFormatNormalizeOptionBit::None,
&normalizedInternalFormat, &imageFormat, &imageType);
return imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER || imageFormat == GL_RGB_INTEGER ||
imageFormat == GL_RGBA_INTEGER;
}
@@ -80,8 +82,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return caps;
}
FormatCapabilityFlags BuildVulkanCaps(TextureInternalFormat logicalFormat,
TextureTarget target,
FormatCapabilityFlags BuildVulkanCaps(TextureInternalFormat logicalFormat, TextureTarget target,
VkFormatFeatureFlags features) {
FormatCapabilityFlags caps;
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat);
@@ -100,8 +101,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool sampled = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0;
const Bool linearFilter = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0;
const Bool colorRenderable = (features & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
const Bool depthStencilRenderable =
(features & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0;
const Bool depthStencilRenderable = (features & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0;
const Bool renderable = (isDepth || isStencil) ? depthStencilRenderable : colorRenderable;
if (sampled || renderable) {
@@ -140,6 +140,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case TextureInternalFormat::RGB:
case TextureInternalFormat::RGB8:
return TextureInternalFormat::RGBA8;
// Legacy low-bit-depth formats with no (or rarely supported) native Vulkan
// encoding; a wider normalized fallback keeps at least the required precision.
case TextureInternalFormat::R3G3B2:
case TextureInternalFormat::RGB4:
case TextureInternalFormat::RGB5:
case TextureInternalFormat::RGBA2:
case TextureInternalFormat::RGBA4:
case TextureInternalFormat::RGB5A1:
return TextureInternalFormat::RGBA8;
case TextureInternalFormat::RGB10:
return TextureInternalFormat::RGB10A2;
case TextureInternalFormat::RGB12:
case TextureInternalFormat::RGBA12:
return TextureInternalFormat::RGBA16;
case TextureInternalFormat::SRGB8:
return TextureInternalFormat::SRGB8Alpha8;
case TextureInternalFormat::RGB8Snorm:
@@ -184,21 +198,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool HasNewCaveatFormatCaps(FormatCapabilityFlags nativeCaps, FormatCapabilityFlags fallbackCaps) {
for (FormatCapability capability : kReportedFormatCapabilities) {
if (HasFormatCapability(fallbackCaps, capability) &&
!HasFormatCapability(nativeCaps, capability)) {
if (HasFormatCapability(fallbackCaps, capability) && !HasFormatCapability(nativeCaps, capability)) {
return true;
}
}
return false;
}
void LogVulkanFormatCaveat(TextureInternalFormat logicalFormat,
SizeT targetIndex,
void LogVulkanFormatCaveat(TextureInternalFormat logicalFormat, SizeT targetIndex,
TextureInternalFormat fallbackFormat) {
MGLOG_D("Caveat: %s %s not fully supported. Reason: native Vulkan format is not fully supported. Fallback: %s",
GetFormatCapabilityTargetName(targetIndex).c_str(),
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
MG_Util::ConvertTextureInternalFormatToString(fallbackFormat).c_str());
MGLOG_D(
"Caveat: %s %s not fully supported. Reason: native Vulkan format is not fully supported. Fallback: %s",
GetFormatCapabilityTargetName(targetIndex).c_str(),
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
MG_Util::ConvertTextureInternalFormatToString(fallbackFormat).c_str());
}
Vector<Int> BuildSampleCounts(Int maxSamples) {
@@ -242,15 +255,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
const auto target = static_cast<TextureTarget>(targetIndex);
const VkFormatFeatureFlags nativeFeatures =
IsTextureBufferTarget(target) ? nativeProperties.bufferFeatures
: nativeProperties.optimalTilingFeatures;
const VkFormatFeatureFlags nativeFeatures = IsTextureBufferTarget(target)
? nativeProperties.bufferFeatures
: nativeProperties.optimalTilingFeatures;
FormatCapabilityFlags nativeCaps = BuildVulkanCaps(logicalFormat, target, nativeFeatures);
cache.FullCaps[targetIndex][formatIndex] |= nativeCaps;
const VkFormatFeatureFlags fallbackFeatures =
IsTextureBufferTarget(target) ? fallbackProperties.bufferFeatures
: fallbackProperties.optimalTilingFeatures;
const VkFormatFeatureFlags fallbackFeatures = IsTextureBufferTarget(target)
? fallbackProperties.bufferFeatures
: fallbackProperties.optimalTilingFeatures;
FormatCapabilityFlags fallbackCaps = BuildVulkanCaps(logicalFormat, target, fallbackFeatures);
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
cache.CaveatCaps[targetIndex][formatIndex] |= fallbackCaps;
@@ -285,9 +298,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
cache.FullCaps[renderbufferTargetIndex][formatIndex] |= renderbufferCaps;
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
FormatCapabilityFlags fallbackRenderbufferCaps =
BuildVulkanCaps(logicalFormat, TextureTarget::Texture2D,
fallbackProperties.optimalTilingFeatures);
FormatCapabilityFlags fallbackRenderbufferCaps = BuildVulkanCaps(
logicalFormat, TextureTarget::Texture2D, fallbackProperties.optimalTilingFeatures);
fallbackRenderbufferCaps &= FormatCapability::Creatable;
if ((fallbackProperties.optimalTilingFeatures &
(VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)) !=
@@ -296,8 +308,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
fallbackRenderbufferCaps |= FormatCapability::MultisampleRenderbuffer;
}
cache.CaveatCaps[renderbufferTargetIndex][formatIndex] |= fallbackRenderbufferCaps;
if (fallbackLogicalFormat &&
HasNewCaveatFormatCaps(renderbufferCaps, fallbackRenderbufferCaps)) {
if (fallbackLogicalFormat && HasNewCaveatFormatCaps(renderbufferCaps, fallbackRenderbufferCaps)) {
LogVulkanFormatCaveat(logicalFormat, renderbufferTargetIndex, *fallbackLogicalFormat);
}
}
@@ -314,14 +325,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void PopulateFormatCapabilities(VkPhysicalDevice physicalDevice,
PFN_vkGetPhysicalDeviceFormatProperties getFormatProperties,
const MG_External::VulkanCapabilities& capabilities,
FormatCapabilityCache& cache) {
const MG_External::VulkanCapabilities& capabilities, FormatCapabilityCache& cache) {
PopulateFormatCapabilitiesImpl(physicalDevice, getFormatProperties, capabilities, cache);
}
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() = default;
BackendObject_DirectVulkan::BackendObject_DirectVulkan(): m_rendererInfo{GetRendererIdentity()} {}
BackendObject_DirectVulkan::BackendObject_DirectVulkan() : m_rendererInfo{GetRendererIdentity()} {}
Bool BackendObject_DirectVulkan::InitWindowSurface() {
if (!m_windowHandle.Handle) {
@@ -375,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());
@@ -395,10 +408,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_E("DirectVulkan backend not initialized");
return false;
}
if (!handle.Handle || (handle.Backend != WindowBackend::Android &&
handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer)) {
MGLOG_E("DirectVulkan backend only supports Android, X11, and CAMetalLayer native windows");
if (!handle.Handle || (handle.Backend != WindowBackend::Android && handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer && handle.Backend != WindowBackend::Win32)) {
MGLOG_E("DirectVulkan backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
return false;
}
@@ -454,6 +466,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// treat them as signaled/available with zero results from here on.
BumpRendererGeneration();
pVulkanRenderer.reset();
// The reflection cache is file-scope, not renderer-owned; without this the
// deleted programs' reflection strings survive full context teardown.
ClearProgramResourceCaches();
BackendObject::ReleaseEGLResources();
}
@@ -463,6 +478,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// treat them as signaled/available with zero results from here on.
BumpRendererGeneration();
pVulkanRenderer.reset();
// The reflection cache is file-scope, not renderer-owned; without this the
// deleted programs' reflection strings survive full context teardown.
ClearProgramResourceCaches();
}
const RendererInfo& BackendObject_DirectVulkan::GetRendererInfo() const {
@@ -482,35 +500,158 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.RendererName = "Magma",
.BackendName = "Direct (Vulkan)",
.ExtraVendor = Nullopt,
.RendererGLInfo =
{
.TargetGLVersion = {3, 3, 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),
.IsCompatibilityProfile = false
},
.RendererGLInfo = {.TargetGLVersion = {4, 6, 0},
.TargetGLSLVersion = {4, 6, 0},
// 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) {
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
V_OpenGL33, 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_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_clear_texture, E_GL_ARB_direct_state_access,
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug,
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size};
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
Bool anisotropicFilteringSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool cubeMapArraySupported) {
Vector<GLExtension> extensions = {
// 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_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,
E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
E_GL_ARB_explicit_attrib_location,
// Core since GL 3.1 and implemented for every version advertised here. The string
// matters because applications gate the ENTRY POINTS on it rather than on the
// version: a caller that finds the extension missing never resolves
// glGetUniformBlockIndex / glUniformBlockBinding, and one that then uses uniform
// blocks anyway calls through a null pointer.
E_GL_ARB_uniform_buffer_object,
// 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};
// 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
// device's: the compiler threads belong to MobileGL's shader pool and
// glCompileShader/glLinkProgram are serviced entirely inside the frontend, so there
// is no device feature to condition this on.
//
// Gated on the async flag deliberately, and this is the whole reason the gate
// exists. Advertising the string is the one part of asynchronous compilation that a
// recorded trace can never cover: Iris and Sodium change their SUBMISSION SCHEDULE
// the moment they see it - they enqueue whole pipeline batches and poll
// GL_COMPLETION_STATUS_KHR instead of compiling one program at a time - so
// MOBILEGL_ASYNC_SHADER_COMPILE=0 has to withdraw the application-visible behaviour
// change as well as the threading, or the kill switch would only be half a switch.
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), 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);
}
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension string);
// only advertised when the device actually supports timestamp queries and the
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
@@ -524,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;
}
@@ -570,6 +723,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.ClearBufferiv = ClearBufferiv;
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
@@ -587,12 +742,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
funcsTable.GL.GetProgramiv = GetProgramiv;
funcsTable.GL.GetProgramInterfaceiv = GetProgramInterfaceiv;
funcsTable.GL.GetProgramResourceIndex = GetProgramResourceIndex;
funcsTable.GL.GetProgramResourceName = GetProgramResourceName;
funcsTable.GL.GetProgramResourceiv = GetProgramResourceiv;
funcsTable.GL.GetProgramResourceLocation = GetProgramResourceLocation;
funcsTable.GL.GetProgramResourceLocationIndex = GetProgramResourceLocationIndex;
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
funcsTable.GL.FenceSync = FenceSync;
funcsTable.GL.ClientWaitSync = ClientWaitSync;
@@ -613,6 +762,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
}
// Occlusion queries share the handle-based result/delete entries, which must
// exist even when timer queries are disabled.
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTableInitialized = true;
}
return funcsTable;
@@ -627,6 +785,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_vulkanCaps = capabilities;
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MG_State::pGLContext) {
MG_State::pGLContext->InvalidateCompileEnv();
}
MutableFormatCapabilities().Clear();
}
@@ -637,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() {
@@ -687,13 +856,14 @@ 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)
// rather than a maximum the sampler manager will never apply.
m_dynamicParameters.MaxTextureMaxAnisotropy =
(pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported()) ? m_vulkanCaps.MaxSamplerAnisotropy
: 1.0f;
: 1.0f;
m_dynamicParameters.SmoothLineWidthRangeMin = m_vulkanCaps.SmoothLineWidthRangeMin;
m_dynamicParameters.SmoothLineWidthRangeMax = m_vulkanCaps.SmoothLineWidthRangeMax;
m_dynamicParameters.SmoothLineWidthGranularity = m_vulkanCaps.SmoothLineWidthGranularity;
@@ -714,8 +884,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.MaxIntegerSamples = m_vulkanCaps.MaxIntegerSamples;
m_dynamicParameters.MaxSamples = m_vulkanCaps.MaxSamples;
m_dynamicParameters.MaxSampleMaskWords = m_vulkanCaps.MaxSampleMaskWords;
const Int maxSupportedTextureUnits =
static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
const Int maxSupportedTextureUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
// GL_MAX_TEXTURE_IMAGE_UNITS is a *per-stage* sampler limit. Adreno/Qualcomm report a huge
// maxPerStageDescriptorSampledImages (descriptor-indexing scale), so clamping it only to our
// combined array capacity (192) still advertises 192 per stage. Host code treats this value as
@@ -725,8 +894,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// limits while keeping the combined limit at our texture-unit array capacity.
constexpr Int maxPerStageTextureUnits =
static_cast<Int>(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS);
m_dynamicParameters.MaxTextureImageUnits =
std::min(m_vulkanCaps.MaxTextureImageUnits, maxPerStageTextureUnits);
m_dynamicParameters.MaxTextureImageUnits = std::min(m_vulkanCaps.MaxTextureImageUnits, maxPerStageTextureUnits);
m_dynamicParameters.MaxVertexTextureImageUnits =
std::min(m_vulkanCaps.MaxVertexTextureImageUnits, maxPerStageTextureUnits);
m_dynamicParameters.MaxComputeTextureImageUnits =
@@ -735,19 +903,84 @@ namespace MobileGL::MG_Backend::DirectVulkan {
std::min(m_vulkanCaps.MaxCombinedTextureImageUnits, maxSupportedTextureUnits);
// Never advertise more attributes than the state layer can store: the current-value array and
// the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
m_dynamicParameters.MaxVertexAttribs =
std::min(m_vulkanCaps.MaxVertexAttribs,
static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_vulkanCaps.MaxComputeShaderStorageBlocks;
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_vulkanCaps.MaxCombinedShaderStorageBlocks;
m_dynamicParameters.MaxComputeUniformBlocks = m_vulkanCaps.MaxComputeUniformBlocks;
m_dynamicParameters.MaxVertexAttribs = std::min(
m_vulkanCaps.MaxVertexAttribs, static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
// Vulkan descriptor limits are not GL limits, and a GL application reads an advertised
// limit as an amount it may actually USE. Adreno answers the per-stage/per-set descriptor
// queries at descriptor-indexing scale - the same driver whose
// GL_MAX_SHADER_STORAGE_BLOCK_SIZE is clamped from 2147483647 further down - so
// KHR-GL44.multi_bind.dispatch_bind_buffers_base read GL_MAX_COMPUTE_UNIFORM_BLOCKS,
// created that many buffers and spliced that many UBO declarations into a single compute
// shader: ~14 s of allocation, then death on std::bad_alloc. Its sibling
// dispatch_bind_buffers_range hard-codes 4 buffers and passes, which is the clean
// discriminator. Every ceiling below is far above what any desktop driver advertises for
// these (84-96 for the binding families) and far below a descriptor-indexing count, so it
// can only lower a limit that was never usable in the first place. The zero floor is not
// decoration: a driver reporting UINT32_MAX used to arrive here as -1.
const auto clampLimit = [](const char* name, Int reported, Int ceiling) {
const Int clamped = std::min(std::max(reported, 0), ceiling);
if (clamped != reported) {
MGLOG_I("DirectVulkan: clamped %s from %d to %d", name, reported, clamped);
}
return clamped;
};
// GL 4.6 required minimums, for the record: MAX_COMPUTE_UNIFORM_BLOCKS 12,
// MAX_COMPUTE/COMBINED_SHADER_STORAGE_BLOCKS 8, MAX_SHADER_STORAGE_BUFFER_BINDINGS 8,
// MAX_UNIFORM_BUFFER_BINDINGS 84, MAX_TEXTURE_BUFFER_SIZE 65536.
constexpr Int kMaxAdvertisedBufferBlocks = 256;
constexpr Int kMaxAdvertisedTextureBufferSize = 1 << 27; // texels; what desktop GL reports
m_dynamicParameters.MaxComputeShaderStorageBlocks =
clampLimit("GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", m_vulkanCaps.MaxComputeShaderStorageBlocks,
kMaxAdvertisedBufferBlocks);
m_dynamicParameters.MaxCombinedShaderStorageBlocks =
clampLimit("GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", m_vulkanCaps.MaxCombinedShaderStorageBlocks,
kMaxAdvertisedBufferBlocks);
m_dynamicParameters.MaxComputeUniformBlocks =
clampLimit("GL_MAX_COMPUTE_UNIFORM_BLOCKS", m_vulkanCaps.MaxComputeUniformBlocks,
kMaxAdvertisedBufferBlocks);
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings;
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
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;
m_dynamicParameters.MaxUniformBufferBindings = clampLimit(
"GL_MAX_UNIFORM_BUFFER_BINDINGS", m_vulkanCaps.MaxUniformBufferBindings, kMaxAdvertisedBufferBlocks);
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
m_dynamicParameters.MaxImageUnits =
std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
m_dynamicParameters.MaxImageUnits = std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
const Int maxPerStageImageUniforms =
std::min(m_dynamicParameters.MaxImageUnits, m_dynamicParameters.MaxCombinedImageUniforms);
@@ -764,25 +997,156 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_vulkanCaps.SupportsFragmentStoresAndAtomics ? maxPerStageImageUniforms : 0;
m_dynamicParameters.MaxComputeImageUniforms =
std::min(std::max(m_vulkanCaps.MaxComputeImageUniforms, 0), maxPerStageImageUniforms);
const Int maxSupportedDrawBuffers =
static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
m_dynamicParameters.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;
m_dynamicParameters.ViewportBoundsRangeMax = m_vulkanCaps.ViewportBoundsRangeMax;
m_dynamicParameters.ViewportSubpixelBits = m_vulkanCaps.ViewportSubpixelBits;
m_dynamicParameters.MinFragmentInterpolationOffset =
std::isfinite(m_vulkanCaps.MinFragmentInterpolationOffset) &&
m_vulkanCaps.MinFragmentInterpolationOffset <= -0.5f
? m_vulkanCaps.MinFragmentInterpolationOffset
: -0.5f;
m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
if (m_vulkanCaps.FragmentInterpolationOffsetBits >= 4 &&
std::isfinite(m_vulkanCaps.MaxFragmentInterpolationOffset)) {
const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -m_vulkanCaps.FragmentInterpolationOffsetBits);
if (m_vulkanCaps.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
m_dynamicParameters.MaxFragmentInterpolationOffset = m_vulkanCaps.MaxFragmentInterpolationOffset;
m_dynamicParameters.FragmentInterpolationOffsetBits = m_vulkanCaps.FragmentInterpolationOffsetBits;
}
}
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
// A 2D or 2D multisample array texture is a VK_IMAGE_TYPE_2D image whose GL depth IS its
// arrayLayers, so a GL layer is a Vulkan array layer with nothing to translate.
// ResolveAttachmentBaseArrayLayer already passes the attachment's layer through. The other
// layered targets are declared separately as their own machinery lands.
{
using DynParams = MG_Backend::DynamicBackendParameters;
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DArray) |
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DMultisampleArray);
// A cube map array is one 2D image with arrayLayers = 6 * cubeCount, so a GL layer is a
// Vulkan array layer here too - but the image cannot be created without imageCubeArray.
// A 3D texture's GL layer is a z slice, which only a 2D view over a 2D-array-compatible
// image can name. Optimistic: a format that refuses the flag is caught at image creation
// and declines the slice view there, which the clear path handles as a soft miss.
if (m_vulkanCaps.Supports2DArrayCompatible3DImages) {
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture3D);
}
if (m_vulkanCaps.SupportsImageCubeArray) {
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
}
}
// 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.
//
// 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.
//
// 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);
if (m_vulkanCaps.SupportsShaderSubgroup) {
m_dynamicParameters.SubgroupSize = m_vulkanCaps.SubgroupSize;
m_dynamicParameters.SubgroupSupportedStages = mapShaderStages(m_vulkanCaps.SubgroupSupportedStages);
m_dynamicParameters.SubgroupSupportedFeatures = mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
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;
@@ -791,8 +1155,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (m_dynamicParameters.MaxShaderStorageBlockSize != m_vulkanCaps.MaxShaderStorageBlockSize) {
MGLOG_I("DirectVulkan: clamped GL_MAX_SHADER_STORAGE_BLOCK_SIZE from %zu to %zu",
m_vulkanCaps.MaxShaderStorageBlockSize,
m_dynamicParameters.MaxShaderStorageBlockSize);
m_vulkanCaps.MaxShaderStorageBlockSize, m_dynamicParameters.MaxShaderStorageBlockSize);
}
switch (m_vulkanCaps.VendorId) {
case 0x5143u: // VK_VENDOR_ID: Qualcomm
m_dynamicParameters.GpuVendor = GpuVendorKind::Qualcomm;
break;
case 0x13B5u: // ARM
m_dynamicParameters.GpuVendor = GpuVendorKind::Arm;
break;
case 0x10DEu: // NVIDIA
m_dynamicParameters.GpuVendor = GpuVendorKind::Nvidia;
break;
case 0x1002u: // AMD
m_dynamicParameters.GpuVendor = GpuVendorKind::Amd;
break;
case 0x8086u: // Intel
m_dynamicParameters.GpuVendor = GpuVendorKind::Intel;
break;
case 0x1010u: // Imagination
m_dynamicParameters.GpuVendor = GpuVendorKind::ImgTec;
break;
case 0x10005u: // Mesa software (lavapipe)
case 0x1AE0u: // Google (SwiftShader)
m_dynamicParameters.GpuVendor = GpuVendorKind::Software;
break;
default:
m_dynamicParameters.GpuVendor = GpuVendorKind::Unknown;
break;
}
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -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
File diff suppressed because it is too large Load Diff
+18 -12
View File
@@ -12,7 +12,7 @@
#include "Renderer/VulkanRenderer.h"
namespace MobileGL::MG_Backend::DirectVulkan {
extern UniquePtr<VulkanRenderer> pVulkanRenderer;
extern UniquePtr<VulkanRenderer>& pVulkanRenderer;
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
@@ -23,12 +23,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 GetRendererGeneration();
void BumpRendererGeneration();
// Drops every cached program-resource reflection entry (CPU-side strings/vectors
// only, no Vulkan handles). Called at EGL teardown next to the renderer reset;
// safe because GL calls are serialized in this codebase, and any still-live
// program rebuilds its entry from the retained generated SPIR-V on demand.
void ClearProgramResourceCaches();
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLuint* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, GLfloat depth, GLint stencil);
void Clear(GLbitfield mask);
@@ -72,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);
@@ -87,15 +97,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
GLsizei* length, GLchar* name);
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
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 ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
@@ -117,6 +119,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// only while a live renderer exists whose device can actually time.
Bool IsTimerQuerySupported();
BackendQueryHandle BeginTimeElapsedQuery();
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
void EndXfbPrimitivesQuery(BackendQueryHandle query);
BackendQueryHandle BeginOcclusionQuery();
void EndOcclusionQuery(BackendQueryHandle query);
void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp();
Bool IsQueryResultAvailable(BackendQueryHandle query);
@@ -111,6 +111,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (buffer.IsValid()) {
// Outgrown, not dead: every BufferSlice handed out from this frame's arena so far
// still names it, and those slices stay in service until the frame slot is rewound
// (VkBufferResource::transientSlice, the converted-vertex-stream cache, the draw
// memos). The release therefore has to survive every mid-frame reclaim and land on
// the next ResetFrame of this slot - see VkBufferManager::CollectAllDeferredReleases.
m_deferredReleases[frameIndex].push_back(std::move(buffer));
}
@@ -16,18 +16,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device = device;
m_commandPool = commandPool;
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = frameCount;
allocInfo.commandBufferCount = frameCount * 2;
VkResult result = vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data());
if (result != VK_SUCCESS) {
return result;
}
for (Uint32 i = 0; i < frameCount; ++i) {
m_frames[i].commandBuffer = commandBuffers[i];
m_frames[i].preCommandBuffer = commandBuffers[frameCount + i];
}
VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
@@ -47,9 +48,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void FrameContext::Destroy(VkDevice device, VkCommandPool commandPool) {
const Uint32 frameCount = static_cast<Uint32>(m_frames.size());
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
for (Uint32 i = 0; i < frameCount; ++i) {
commandBuffers[i] = m_frames[i].commandBuffer;
commandBuffers[frameCount + i] = m_frames[i].preCommandBuffer;
}
for (Uint32 i = 0; i < frameCount; ++i) {
@@ -60,7 +62,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto& frame : m_frames) {
FreeRetiredCommandBuffers(frame);
}
vkFreeCommandBuffers(device, commandPool, frameCount, commandBuffers.data());
vkFreeCommandBuffers(device, commandPool, frameCount * 2, commandBuffers.data());
}
m_frames.clear();
currentFrameIndex = 0;
@@ -87,6 +89,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
currentFrameIndex = (currentFrameIndex + 1) % static_cast<Uint32>(m_frames.size());
GetCurrent().isCommandRecording = false;
GetCurrent().hasCommandBufferRecorded = false;
GetCurrent().isPreCommandRecording = false;
GetCurrent().hasPreCommandBufferRecorded = false;
}
VkCommandBuffer& FrameContext::BeginCommandRecording(VkCommandBufferUsageFlags flags,
@@ -118,6 +122,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frame.hasCommandBufferRecorded = true;
}
VkCommandBuffer FrameContext::BeginPreCommandRecording() {
auto& frame = GetCurrent();
if (frame.isPreCommandRecording) {
return frame.preCommandBuffer;
}
MOBILEGL_ASSERT(!frame.hasPreCommandBufferRecorded,
"BeginPreCommandRecording: a recorded pre stream is still awaiting submission");
VK_VERIFY(vkResetCommandBuffer(frame.preCommandBuffer, 0), "BeginPreCommandRecording, vkResetCommandBuffer");
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
VK_VERIFY(vkBeginCommandBuffer(frame.preCommandBuffer, &beginInfo),
"BeginPreCommandRecording, vkBeginCommandBuffer");
frame.isPreCommandRecording = true;
return frame.preCommandBuffer;
}
void FrameContext::EndPreCommandRecordingIfOpen() {
auto& frame = GetCurrent();
if (!frame.isPreCommandRecording) {
return;
}
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "EndPreCommandRecordingIfOpen, vkEndCommandBuffer");
frame.isPreCommandRecording = false;
frame.hasPreCommandBufferRecorded = true;
}
void FrameContext::AbandonPreCommandRecording() {
auto& frame = GetCurrent();
if (frame.isPreCommandRecording) {
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "AbandonPreCommandRecording, vkEndCommandBuffer");
}
frame.isPreCommandRecording = false;
frame.hasPreCommandBufferRecorded = false;
}
VkResult FrameContext::InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount) {
DestroySwapchainSemaphores(device);
if (swapchainImageCount == 0) {
@@ -150,12 +189,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool FrameContext::TransitionToPresent(VkImage image, VkImageLayout oldLayout, VkImageLayout presentLayout) {
auto& frame = GetCurrent();
if (frame.hasCommandBufferRecorded || frame.isCommandRecording || oldLayout == presentLayout ||
oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
if (oldLayout == presentLayout || oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
return false;
}
auto& commandBuffer = BeginCommandRecording();
// The barrier belongs in the frame's own recording. Bailing out because
// something was already recorded (the previous behaviour) dropped the
// transition entirely for every frame that never ran a default-framebuffer
// render pass - the only other thing that carries the image to
// PRESENT_SRC_KHR, via that pass's finalLayout - so the swapchain image was
// handed to the WSI still in the layout it was acquired in.
// A closed-but-unsubmitted buffer can only come from a submit that already
// failed (SubmitPendingCommandBuffer leaves the flag set on error), and
// appending to it is illegal while reopening would reset the frame's own
// commands away. The device is gone on that path anyway - stay silent-safe
// rather than trade a lost device for a barrier into a closed buffer.
if (frame.hasCommandBufferRecorded) {
MGLOG_E_ONCE("TransitionToPresent: command buffer already closed; skipping the present barrier");
return false;
}
// Reopening a recording here would vkResetCommandBuffer this frame's own
// commands away, so append to the open one and let the caller close it.
const Bool openedRecording = !frame.isCommandRecording;
VkCommandBuffer commandBuffer = openedRecording ? BeginCommandRecording() : frame.commandBuffer;
VkImageMemoryBarrier presentBarrier{};
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
@@ -174,7 +231,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
nullptr, 0, nullptr, 1, &presentBarrier);
EndCommandRecording();
if (openedRecording) {
EndCommandRecording();
}
return true;
}
@@ -182,17 +241,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 swapchainImageIndex) const {
const auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording, "GetSubmitInfo called while command buffer recording is still active");
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
"GetSubmitInfo called while the pre-pass stream is still recording");
AssertValidSwapchainImageIndex(swapchainImageIndex);
SubmitInfoPacket packet{};
packet.waitSemaphore = frame.imageAvailableSemaphore;
packet.signalSemaphore = m_swapchainImageRenderFinishedSemaphores[swapchainImageIndex];
packet.commandBuffer = frame.commandBuffer;
Uint32 commandBufferCount = 0;
// The pre-pass stream executes strictly before the frame's commands.
if (frame.hasPreCommandBufferRecorded) {
packet.commandBuffers[commandBufferCount++] = frame.preCommandBuffer;
}
if (shouldSubmitCommandBuffer) {
packet.commandBuffers[commandBufferCount++] = frame.commandBuffer;
}
packet.submitInfo.waitSemaphoreCount = frame.imageAvailableSemaphoreConsumed ? 0U : 1U;
packet.submitInfo.pWaitSemaphores = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitSemaphore;
packet.submitInfo.pWaitDstStageMask = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitDstStageMask;
packet.submitInfo.commandBufferCount = shouldSubmitCommandBuffer ? 1U : 0U;
packet.submitInfo.pCommandBuffers = shouldSubmitCommandBuffer ? &packet.commandBuffer : nullptr;
packet.submitInfo.commandBufferCount = commandBufferCount;
packet.submitInfo.pCommandBuffers = commandBufferCount > 0 ? packet.commandBuffers : nullptr;
packet.submitInfo.signalSemaphoreCount = 1;
packet.submitInfo.pSignalSemaphores = &packet.signalSemaphore;
return packet;
@@ -227,12 +296,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence,
&outImageIndex);
if (result != VK_SUCCESS) {
// VK_SUBOPTIMAL_KHR is a success code: an image *was* acquired and
// imageAvailableSemaphore *will* be signaled. Bailing out on it skipped both
// the consumed-flag reset (leaving a stale "already consumed", so the next
// submit never waited on the pending signal) and the fence reset (leaving
// the slot's fence signaled for the next submit to reuse). Only a genuine
// failure - VK_ERROR_OUT_OF_DATE_KHR and friends, where nothing is acquired
// and nothing is signaled - skips the bookkeeping.
if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
return result;
}
frame.imageAvailableSemaphoreConsumed = false;
return vkResetFences(device, 1, &frame.imageInFlightFence);
const VkResult resetResult = vkResetFences(device, 1, &frame.imageInFlightFence);
// Hand the acquire's own code back so the caller can schedule a rebuild.
return resetResult == VK_SUCCESS ? result : resetResult;
}
Uint32 FrameContext::GetCurrentFrameIndex() const {
@@ -247,12 +325,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_recordingObserver = observer;
}
VkResult FrameContext::RetireCurrentCommandBuffer() {
VkResult FrameContext::RetireCurrentCommandBuffer(Bool retirePreCommandBuffer) {
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE,
"RetireCurrentCommandBuffer requires an initialized FrameContext");
auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording,
"RetireCurrentCommandBuffer called while the command buffer is still recording");
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
"RetireCurrentCommandBuffer called while the pre-pass stream is still recording");
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
@@ -260,11 +340,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
VkCommandBuffer replacement = VK_NULL_HANDLE;
const VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
if (result != VK_SUCCESS) {
return result;
}
frame.retiredCommandBuffers.push_back(frame.commandBuffer);
if (retirePreCommandBuffer) {
VkCommandBuffer preReplacement = VK_NULL_HANDLE;
result = vkAllocateCommandBuffers(m_device, &allocInfo, &preReplacement);
if (result != VK_SUCCESS) {
vkFreeCommandBuffers(m_device, m_commandPool, 1, &replacement);
return result;
}
frame.retiredCommandBuffers.push_back({frame.preCommandBuffer, frame.lastSubmitIndex});
frame.preCommandBuffer = preReplacement;
}
// lastSubmitIndex was just written by the renderer for the submission
// that carried this command buffer.
frame.retiredCommandBuffers.push_back({frame.commandBuffer, frame.lastSubmitIndex});
frame.commandBuffer = replacement;
return VK_SUCCESS;
}
@@ -274,12 +366,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return;
}
if (m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE) {
vkFreeCommandBuffers(m_device, m_commandPool, static_cast<Uint32>(frame.retiredCommandBuffers.size()),
frame.retiredCommandBuffers.data());
for (const auto& retired : frame.retiredCommandBuffers) {
vkFreeCommandBuffers(m_device, m_commandPool, 1, &retired.commandBuffer);
}
}
frame.retiredCommandBuffers.clear();
}
void FrameContext::FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex) {
if (m_device == VK_NULL_HANDLE || m_commandPool == VK_NULL_HANDLE) {
return;
}
for (auto& frame : m_frames) {
// Retired buffers are appended in submit order, so the completed
// ones form a prefix.
SizeT completedCount = 0;
while (completedCount < frame.retiredCommandBuffers.size() &&
frame.retiredCommandBuffers[completedCount].submitIndex <= completedSubmitIndex) {
vkFreeCommandBuffers(m_device, m_commandPool, 1,
&frame.retiredCommandBuffers[completedCount].commandBuffer);
++completedCount;
}
if (completedCount > 0) {
frame.retiredCommandBuffers.erase(frame.retiredCommandBuffers.begin(),
frame.retiredCommandBuffers.begin() + completedCount);
}
}
}
void FrameContext::FreeAllRetiredCommandBuffers() {
for (auto& frame : m_frames) {
FreeRetiredCommandBuffers(frame);
}
}
void FrameContext::AssertValidFrameIndex(Uint32 frameIndex) const {
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "FrameContext index out of range");
}
@@ -29,7 +29,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
VkSemaphore signalSemaphore = VK_NULL_HANDLE;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
// [0] = pre-pass command buffer (when recorded), then the frame
// command buffer; submitInfo.pCommandBuffers points here.
VkCommandBuffer commandBuffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO};
};
@@ -40,17 +42,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPresentInfoKHR presentInfo{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
};
// A command buffer submitted mid-frame (FlushPendingCommands), tagged
// with the submit-tracker index it was submitted under so it can be
// freed as soon as that submission is observed complete - without
// waiting for the slot's fence to be waited again (present-less flush
// loops never wait it).
struct RetiredCommandBuffer {
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
Uint64 submitIndex = 0;
};
struct FrameData {
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
// Pre-pass work stream: out-of-pass commands (deferred clear
// materialization, sampled-layout transitions) for resources the
// frame's recording has not touched yet. Submitted immediately
// BEFORE commandBuffer in the same vkQueueSubmit, so recording
// into it never has to split the frame's active render pass.
VkCommandBuffer preCommandBuffer = VK_NULL_HANDLE;
VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE;
VkFence imageInFlightFence = VK_NULL_HANDLE;
Bool isCommandRecording = false;
Bool hasCommandBufferRecorded = false;
Bool isPreCommandRecording = false;
Bool hasPreCommandBufferRecorded = false;
Bool imageAvailableSemaphoreConsumed = false;
// Command buffers submitted mid-frame (FlushPendingCommands) whose
// execution is only known complete once this slot's fence has been
// waited again; freed at that point.
Vector<VkCommandBuffer> retiredCommandBuffers;
// Command buffers submitted mid-frame (FlushPendingCommands),
// appended in submit order; freed once their submission is known
// complete (fence wait or completion poll).
Vector<RetiredCommandBuffer> retiredCommandBuffers;
// Submit-tracker index of this slot's most recent queue submission
// (written by the renderer at submit time).
Uint64 lastSubmitIndex = 0;
@@ -67,6 +87,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkCommandBuffer& BeginCommandRecording(VkCommandBufferUsageFlags flags = 0,
const VkCommandBufferInheritanceInfo* pInheritanceInfo = nullptr);
void EndCommandRecording();
// Lazily opens the pre-pass work stream (see FrameData::preCommandBuffer).
VkCommandBuffer BeginPreCommandRecording();
// Closes the pre stream if open, marking it for submission ahead of the
// frame command buffer. Safe to call when it never opened.
void EndPreCommandRecordingIfOpen();
// Drops an in-progress or recorded-but-unsubmitted pre stream (dropped
// frame recordings, swapchain recreation).
void AbandonPreCommandRecording();
VkResult InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount);
void DestroySwapchainSemaphores(VkDevice device);
Bool TransitionToPresent(VkImage image, VkImageLayout oldLayout,
@@ -79,8 +107,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Parks the current (already ended and submitted) command buffer on the
// slot's retired list and installs a freshly allocated one, so recording
// can restart while the submitted buffer is still executing. Retired
// buffers are freed after the slot's fence is next waited.
VkResult RetireCurrentCommandBuffer();
// buffers are freed after the slot's fence is next waited, or as soon
// as their submission is observed complete.
VkResult RetireCurrentCommandBuffer(Bool retirePreCommandBuffer = false);
// Frees every retired command buffer whose tagged submission index is
// known complete. Driven by the renderer's submit tracker on completion
// events (fence waits and non-blocking polls), so present-less flush
// loops reclaim their buffers without any extra wait.
void FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex);
// Frees every slot's retired command buffers. Only valid when the
// caller has proven every queue submission complete.
void FreeAllRetiredCommandBuffers();
Uint32 GetCurrentFrameIndex() const;
Uint32 GetFrameCount() const;
@@ -8,6 +8,8 @@
#include "PipelineFactory.h"
#include <algorithm>
namespace MobileGL::MG_Backend::DirectVulkan {
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
switch (topology) {
@@ -108,6 +110,81 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"vkCreatePipelineCache");
}
// Must be called once, before any pipeline is created: the flag is not part of the
// pipeline hash, so flipping it mid-life would serve cached pipelines built under the
// old value.
void PipelineFactory::SetSuppressBlendedDepthWrite(Bool enabled) {
s_suppressBlendedDepthWrite = enabled;
}
Bool PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride,
Uint32 vendorId) {
static constexpr Uint32 kVendorIdQualcomm = 0x5143;
switch (quirkOverride) {
case MG_Config::QuirkOverride::ForceOn:
return true;
case MG_Config::QuirkOverride::ForceOff:
return false;
case MG_Config::QuirkOverride::Auto:
default:
return vendorId == kVendorIdQualcomm;
}
}
namespace {
// MIN/MAX extremum blending: the signature of a depth-bounds accumulation pass
// (MC 26.3 OIT writes vec4(-linD, linD, deviceZ, 0) under GL_MAX while writing
// depth for its equality chain). MIN/MAX ignore blend factors per the Vulkan spec.
//
// Deliberately the ONLY shape stripped. A quirk should touch as little unrelated
// content as possible, and a trace sweep of every fixture showed the wider
// alternatives all cost more than they fix:
// - additive ONE+ONE with a depth write matched zero draws of the 26.3 chain
// (its transmittance/accumulate passes disable depth writes themselves) - the
// only real content it caught was harmless additive glow effects (Create);
// - sorted-transparency "over" blends (SRC_ALPHA-style) are order-dependent,
// drawn once per surface, and rely on their depth writes for occlusion;
// - separate-alpha accumulation over an over-blending color channel has no
// known pairing with a depth-equality chain (color channel only, see tests).
// If a future workload pairs another blend shape with an equality chain, widen
// this with that evidence in hand rather than pre-emptively.
Bool IsAccumulationBlend(const VkPipelineColorBlendAttachmentState& attachment) {
return attachment.colorBlendOp == VK_BLEND_OP_MIN ||
attachment.colorBlendOp == VK_BLEND_OP_MAX;
}
} // namespace
Bool PipelineFactory::ShouldSuppressDepthWrite(const PipelineCreatePayload& payload) {
if (!payload.depthWriteEnable) {
return false;
}
// A shader that assigns gl_FragDepth supplies depth itself rather than taking the
// pipeline's interpolated Z, so a driver that varies the vertex position math
// between pipelines cannot desynchronize it. (A gl_FragDepth = gl_FragCoord.z
// passthrough is the exception that stays exposed; no known content pairs one with
// an equality chain, and 26.3's composite is a genuine computed-depth writer.)
if (payload.fragmentReplacesDepth) {
return false;
}
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
const VkPipelineColorBlendAttachmentState& attachment = payload.colorBlendAttachments[i];
if (attachment.blendEnable != VK_TRUE) {
continue;
}
// All color writes masked: blending is moot (depth-prepass pattern that left
// GL_BLEND enabled); stripping the depth write would delete the whole prepass.
if (attachment.colorWriteMask == 0) {
continue;
}
// Any attachment qualifies, not just attachment 0: the 26.3 transmittance pass
// accumulates into a 2-target MRT and must stay stripped.
if (IsAccumulationBlend(attachment)) {
return true;
}
}
return false;
}
PipelineFactory::~PipelineFactory() {
DestroyAll();
if (m_pipelineCache != VK_NULL_HANDLE) {
@@ -124,13 +201,22 @@ 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)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
@@ -152,6 +238,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
if (payload.colorAttachmentCount > 0) {
XXHASH_VERIFY(XXH64_update(
m_hashState,
@@ -165,23 +253,125 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const HashType hash = ComputeHash(payload);
auto it = m_cache.find(hash);
if (it != m_cache.end()) {
return it->second;
it->second.lastUsedFrame = m_frameCounter;
return it->second.pipeline;
}
VkPipeline pipeline = CreatePipeline(payload);
m_cache.emplace(hash, pipeline);
// A failed creation must never be memoized. Caching VK_NULL_HANDLE served the null back for
// the rest of the process, so one transient driver rejection turned every later draw with
// the same state into a vkCmdBindPipeline(VK_NULL_HANDLE) - the SIGSEGV behind 9 of the 15
// CTS process deaths. Retrying costs one failed vkCreateGraphicsPipelines per draw, which
// is the correct price for a broken pipeline and is bounded by the draw itself being
// skipped.
if (pipeline == VK_NULL_HANDLE) {
// Unlatched, like the CreatePipeline report it accompanies: a pipeline MobileGL
// assembled and the driver refused is a broken invariant, not an expected failure,
// so it stays loud for as long as it is reachable. Raised from MGLOG_I once the
// Log.h ordering fix made MGLOG_E live in INFO builds.
MGLOG_E("PipelineFactory::GetOrCreatePipeline: creation failed for hash=0x%llx "
"programHash=0x%llx; not caching the failure",
static_cast<unsigned long long>(hash),
static_cast<unsigned long long>(payload.programHash));
return VK_NULL_HANDLE;
}
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass,
m_frameCounter});
return pipeline;
}
void PipelineFactory::DestroyAll() {
for (auto& pair : m_cache) {
if (pair.second != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, pair.second, nullptr);
if (pair.second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, pair.second.pipeline, nullptr);
}
}
m_cache.clear();
}
Uint32 PipelineFactory::OnFrameBoundary() {
++m_frameCounter;
// Sweep cadence and retire age mirror VkRenderPassManager::OnPresent: an entry
// idle for more than kRetireAgeFrames frame boundaries cannot be referenced by
// any in-flight command buffer (frames-in-flight <= MOBILEGL_MAGMA_FRAMESINFLIGHT),
// so immediate vkDestroyPipeline is safe. The caller must drop its "last
// pipeline" memo when this returns non-zero: the memo can return a cached
// handle without touching this cache, so an evicted pipeline may still be
// memoized (present-less flush loops never reset the memo per frame).
constexpr Uint64 kSweepInterval = 256;
constexpr Uint64 kRetireAgeFrames = 1024;
if ((m_frameCounter % kSweepInterval) != 0) {
return 0;
}
Uint32 evicted = 0;
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
if (it->second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
}
it = m_cache.erase(it);
++evicted;
} else {
++it;
}
}
if (evicted > 0) {
MGLOG_D("PipelineFactory::OnFrameBoundary: evicted %u idle pipelines (%zu remain)", evicted,
m_cache.size());
}
return evicted;
}
Uint32 PipelineFactory::EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses) {
if (renderPasses.empty() || m_cache.empty()) {
return 0;
}
// Sorted-batch membership test keeps a mass eviction (shader-pack switch,
// dimension exit) at one O(cache * log batch) scan instead of one full scan
// per dying pass.
Vector<VkRenderPass> sortedPasses = renderPasses;
std::sort(sortedPasses.begin(), sortedPasses.end());
Uint32 evicted = 0;
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (std::binary_search(sortedPasses.begin(), sortedPasses.end(), it->second.renderPass)) {
if (it->second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
}
it = m_cache.erase(it);
++evicted;
} else {
++it;
}
}
if (evicted > 0) {
MGLOG_D("PipelineFactory::EvictByRenderPasses: evicted %u pipelines for %zu destroyed render passes",
evicted, sortedPasses.size());
}
return evicted;
}
Uint32 PipelineFactory::EvictByProgramHash(HashType programHash) {
Uint32 evicted = 0;
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (it->second.programHash == programHash) {
if (it->second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
}
it = m_cache.erase(it);
++evicted;
} else {
++it;
}
}
if (evicted > 0) {
MGLOG_D("PipelineFactory::EvictByProgramHash: evicted %u pipelines for program hash 0x%llx",
evicted, static_cast<unsigned long long>(programHash));
}
return evicted;
}
VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
@@ -216,9 +406,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ia.topology = payload.topology;
ia.primitiveRestartEnable = payload.primitiveRestartEnable ? VK_TRUE : VK_FALSE;
// Only a patch topology has a tessellation stage to configure; leaving the pointer null
// otherwise is what the spec expects.
VkPipelineTessellationStateCreateInfo tessellation{VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO};
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;
@@ -227,9 +426,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
raster.depthBiasEnable = payload.depthBiasEnable ? VK_TRUE : VK_FALSE;
raster.rasterizerDiscardEnable = payload.rasterizerDiscardEnable ? VK_TRUE : VK_FALSE;
raster.lineWidth = 1.0f;
// Only chain the struct when the mode is not Vulkan's implicit default: a device without
// VK_EXT_provoking_vertex enabled must never see this pNext entry, and the renderer's
// selector already collapses to FIRST in exactly that case - so a device without the
// extension produces a byte-identical VkGraphicsPipelineCreateInfo to before.
VkPipelineRasterizationProvokingVertexStateCreateInfoEXT provokingVertexState{
VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_PROVOKING_VERTEX_STATE_CREATE_INFO_EXT};
if (payload.provokingVertexMode != VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) {
provokingVertexState.provokingVertexMode = payload.provokingVertexMode;
provokingVertexState.pNext = raster.pNext;
raster.pNext = &provokingVertexState;
}
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;
@@ -258,17 +476,77 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
colorAttachments[i] = payload.colorBlendAttachments[i];
}
// Suppress depth writes on accumulation-blended pipelines when the active driver
// cannot keep vertex positions invariant across the pipelines of a multi-pass
// depth-equality chain (see SetSuppressBlendedDepthWrite). The decision is narrowed
// in ShouldSuppressDepthWrite: sorted-transparency "over" blends (vanilla MC water),
// gl_FragDepth writers, and masked-out attachments keep their depth writes.
// This bakes the decision into the pipeline, which only works because depth write is
// static state here - adding VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE to kDynamicStates
// would let the record-time value override it and silently disable the quirk.
if (s_suppressBlendedDepthWrite && ShouldSuppressDepthWrite(payload)) {
depthStencil.depthWriteEnable = VK_FALSE;
}
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE;
blend.logicOp = payload.logicOp;
blend.attachmentCount = payload.colorAttachmentCount;
blend.pAttachments = colorAttachments.empty() ? nullptr : colorAttachments.data();
// A GL program may have a tessellation EVALUATION stage and no CONTROL stage: GL 4.6 core
// 11.2.2 gives it a fixed-function pass-through instead. Vulkan has no such stage, and
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 requires both tessellation stages or
// neither - so the renderer synthesizes the pass-through GL describes and hands it in
// here (see ProgramFactory::GetOrCreatePassthroughTessControlStage).
//
// The refusal below is what keeps the half-tessellated shape away from the driver when
// there is no synthesized stage to add - because Mali does not reject it, it dereferences
// null INSIDE vkCreateGraphicsPipelines and takes the process down (SIGSEGV, fault addr
// 0x34, on Mali-G715/r54p2 and Mali-G925/r49p1 alike; Adreno and lavapipe merely render
// wrong). Returning VK_NULL_HANDLE routes this through the same path a driver rejection
// takes: the draw is skipped, nothing is memoised, and the process survives.
const Vector<VkPipelineShaderStageCreateInfo>* effectiveStages = payload.stages;
Vector<VkPipelineShaderStageCreateInfo> stagesWithPassthrough;
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) {
stagesWithPassthrough = *payload.stages;
stagesWithPassthrough.push_back(payload.passthroughTessControlStage);
effectiveStages = &stagesWithPassthrough;
}
{
VkShaderStageFlags stagesPresent = 0;
for (const auto& stageInfo : *effectiveStages) {
stagesPresent |= stageInfo.stage;
}
const Bool hasTessControl = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT) != 0;
const Bool hasTessEval = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT) != 0;
if (hasTessControl != hasTessEval) {
// Latched, and the latch is the point: a failed creation is deliberately never
// memoised (see GetOrCreatePipeline), so a program in this state re-enters here
// once per draw, every frame - and a refusal diagnostic that repeats per draw is
// noise, not a diagnostic. One line names the program; the draws it explains are
// all the same draw.
static Bool s_warnedHalfTessellatedPipeline = false;
if (!s_warnedHalfTessellatedPipeline) {
s_warnedHalfTessellatedPipeline = true;
MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with %s tessellation stage and "
"no %s stage (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). programHash=0x%llx "
"patchControlPoints=%u. Its draws are skipped; logged once.",
hasTessEval ? "an evaluation" : "a control",
hasTessEval ? "control" : "evaluation",
static_cast<unsigned long long>(payload.programHash),
payload.patchControlPoints);
}
return VK_NULL_HANDLE;
}
}
VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
gpi.stageCount = static_cast<Uint32>(payload.stages->size());
gpi.pStages = payload.stages->data();
gpi.stageCount = static_cast<Uint32>(effectiveStages->size());
gpi.pStages = effectiveStages->data();
gpi.pVertexInputState = payload.vertexInputState;
gpi.pInputAssemblyState = &ia;
gpi.pTessellationState =
payload.topology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST ? &tessellation : nullptr;
gpi.pViewportState = &vpci;
gpi.pRasterizationState = &raster;
gpi.pMultisampleState = &ms;
@@ -281,6 +559,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline pipeline = VK_NULL_HANDLE;
const VkResult result = vkCreateGraphicsPipelines(m_device, m_pipelineCache, 1, &gpi, nullptr, &pipeline);
// Loud, at MGLOG_F, and deliberately NOT latched. vkCreateGraphicsPipelines refusing a
// pipeline MobileGL assembled is a should-never-happen state, and the driver's own
// answer is VK_ERROR_UNKNOWN - no information at all - so this dump is the entire
// diagnosis. It is not an expected failure mode, so the one-shot rule that quiets W/E
// does not apply: while this is reachable it should keep saying so on every draw.
// GetOrCreatePipeline deliberately does not cache the failure, which is what makes that
// repetition happen; if the repetition ever needs to stop, fix the pipeline, not the log.
if (result != VK_SUCCESS) {
MGLOG_F("PipelineFactory::CreatePipeline failed: result=%s (%d) programHash=0x%llx vertexInputHash=0x%llx stageCount=%u topology=%s(%d) colorAttachmentCount=%u samples=%s(%d) subpass=%u",
VkResultToString(result),
@@ -311,6 +596,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_F("PipelineFactory::CreatePipeline vertex input: bindingCount=%u attributeCount=%u",
payload.vertexInputState->vertexBindingDescriptionCount,
payload.vertexInputState->vertexAttributeDescriptionCount);
// The driver's own answer is VK_ERROR_UNKNOWN, i.e. no information at all, so the only
// way to work out WHICH shader it choked on (the open sampler-array-in-struct
// investigation) is to name the modules. MGLOG_I, not _D: this is part of a
// should-never-happen report and must survive in the INFO-level builds that CTS
// actually runs against, alongside the MGLOG_F lines above.
if (payload.stageSpirvDigests) {
for (SizeT i = 0; i < payload.stageSpirvDigests->size(); ++i) {
const auto& digest = (*payload.stageSpirvDigests)[i];
MGLOG_I("PipelineFactory::CreatePipeline spirv[%zu]: stage=0x%x words=%u bytes=%zu "
"hash=0x%llx",
i, digest.stage, digest.wordCount,
static_cast<SizeT>(digest.wordCount) * sizeof(Uint32),
static_cast<unsigned long long>(digest.hash));
}
} else {
MGLOG_I("PipelineFactory::CreatePipeline: no SPIR-V digests attached to the payload");
}
if (payload.stages) {
for (SizeT i = 0; i < payload.stages->size(); ++i) {
const auto& stage = (*payload.stages)[i];
// VkShaderModule is a non-dispatchable handle: a pointer on 64-bit but a
// plain uint64_t on 32-bit ABIs, where a cast to const void* is ill-formed
// (broke the armeabi-v7a build). Print it as the 64-bit value it is.
MGLOG_I("PipelineFactory::CreatePipeline stage[%zu]: stage=0x%x module=0x%llx entry=%s "
"specialization=%d",
i, static_cast<Uint32>(stage.stage),
static_cast<unsigned long long>(reinterpret_cast<Uint64>(stage.module)),
stage.pName ? stage.pName : "(null)", stage.pSpecializationInfo ? 1 : 0);
}
}
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
const auto& attachment = payload.colorBlendAttachments[i];
MGLOG_F("PipelineFactory::CreatePipeline colorAttachment[%u]: blend=%d colorWriteMask=0x%x srcColor=%d dstColor=%d colorOp=%d srcAlpha=%d dstAlpha=%d alphaOp=%d",
@@ -14,6 +14,16 @@
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
// Enough of a fingerprint to identify the exact module the driver rejected without keeping the
// SPIR-V alive for every program in the cache: a driver that answers VK_ERROR_UNKNOWN tells us
// nothing, so the log has to carry the shader's identity itself. Diagnostic only - never part
// of any pipeline or program hash.
struct ShaderStageSpirvDigest {
Uint32 stage = 0; // VkShaderStageFlagBits
Uint32 wordCount = 0;
Uint64 hash = 0;
};
class PipelineFactory {
public:
using HashType = Uint64;
@@ -27,12 +37,55 @@ 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;
// GL's provoking vertex, baked into the pipeline (VK_EXT_provoking_vertex). It selects
// which vertex a flat varying takes AND the vertex order transform feedback records for
// strips/fans, so it is part of the pipeline's identity, not dynamic state. Defaults to
// Vulkan's own convention, which is what a device without the extension gets.
VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
Bool depthTestEnable = false;
Bool depthWriteEnable = false;
Bool depthBiasEnable = false;
@@ -49,9 +102,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkStencilOp backStencilPassOp = VK_STENCIL_OP_KEEP;
VkStencilOp backStencilDepthFailOp = VK_STENCIL_OP_KEEP;
VkCompareOp backStencilCompareOp = VK_COMPARE_OP_ALWAYS;
// The fragment module writes gl_FragDepth (SPIR-V DepthReplacing); exempts the
// pipeline from the blended depth-write quirk (see ShouldSuppressDepthWrite).
Bool fragmentReplacesDepth = false;
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
// The tessellation control stage this renderer synthesized for a program that has
// an evaluation stage and none of its own (GL 4.6 core 11.2.2 gives such a program a
// fixed-function pass-through; Vulkan has no such thing and
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 forbids the half-tessellated
// pipeline outright). Appended to `stages` at creation. A null module means the
// renderer could not build one, and CreatePipeline refuses the pipeline - the same
// refusal it applies when `stages` itself is half-tessellated.
//
// 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.
const Vector<ShaderStageSpirvDigest>* stageSpirvDigests = nullptr;
};
explicit PipelineFactory(VkDevice device, const VulkanRendererConfig& config);
@@ -62,13 +132,69 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
void DestroyAll();
// Frame boundary hook: ages the pipeline cache and destroys long-unused entries
// (their command buffers retired many frames ago), mirroring
// VkRenderPassManager::OnPresent's sweep. Returns the number of pipelines
// destroyed so the caller can drop any memoized VkPipeline handle.
Uint32 OnFrameBoundary();
// Destroys every cached pipeline hashed on one of `renderPasses`. Only safe
// when the caller guarantees GPU idleness for them - the render-pass manager
// calls this (via the renderer) for passes its own >1024-boundary-idle sweep
// just evicted, and a pipeline hashed on those handles is only ever bound by
// draws that also hit the render-pass entries. Also closes the handle-recycling
// hazard: a recycled VkRenderPass value must never serve a stale pipeline.
// Batched: one cache scan regardless of how many passes died in the sweep.
// Returns the number destroyed (callers invalidate memos when non-zero).
Uint32 EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses);
// Destroys every cached pipeline built from the program with content hash
// `programHash`. Called from the ProgramFactory eviction path, which proves the
// same >1024-boundary idleness (the program's pipelines are only bound by draws
// that stamp its factory entry). Returns the number destroyed.
Uint32 EvictByProgramHash(HashType programHash);
// Driver quirk: suppress depth writes on accumulation-blended pipelines. Multi-pass
// depth-equality rendering (a blended prepass writes depth that later passes re-test
// with an equality-inclusive compare on the re-rasterized geometry) requires
// cross-pipeline position invariance that some mobile compilers do not provide, even
// with the SPIR-V Invariant decoration; whole primitives then drop out of the later
// passes. Only MIN/MAX extremum blends are stripped - the signature of such a
// chain's depth-bounds pass (MC 26.3 OIT), and per a fixture-wide trace sweep the
// only depth-writing shape the chain actually uses - so every other blend
// (sorted-transparency "over" like vanilla MC water, additive glows, ...) keeps
// its depth writes. Set at renderer initialization based on the active driver.
static void SetSuppressBlendedDepthWrite(Bool enabled);
static Bool IsSuppressBlendedDepthWriteEnabled() { return s_suppressBlendedDepthWrite; }
// Device gate for the quirk: ForceOn/ForceOff bypass detection, Auto enables it on
// the known-affected vendor (Qualcomm).
static Bool ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride,
Uint32 vendorId);
// Pure per-pipeline strip decision (exempts gl_FragDepth writers, masked-out and
// non-accumulation blends); combined with the device flag in CreatePipeline. Static
// and payload-only so tests can pin the contract without a VkDevice.
static Bool ShouldSuppressDepthWrite(const PipelineCreatePayload& payload);
private:
struct PipelineCacheEntry {
VkPipeline pipeline = VK_NULL_HANDLE;
// The hashed inputs the eviction paths key on: programHash ties the entry to
// its ProgramFactory entry, renderPass records the exact handle the hash
// folded in (the hash is one-way, so targeted eviction needs them verbatim).
HashType programHash = 0;
VkRenderPass renderPass = VK_NULL_HANDLE;
// Frame-boundary counter value of the last GetOrCreatePipeline hit; drives
// cache eviction (see OnFrameBoundary).
Uint64 lastUsedFrame = 0;
};
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
VkDevice m_device = VK_NULL_HANDLE;
const VulkanRendererConfig& m_config;
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
UnorderedMap<HashType, VkPipeline> m_cache;
UnorderedMap<HashType, PipelineCacheEntry> m_cache;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameCounter = 0;
static inline XXH64_state_t* m_hashState = XXH64_createState();
static inline Bool s_suppressBlendedDepthWrite = false;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -9,6 +9,7 @@
#pragma once
#include "../VkIncludes.h"
#include "PipelineFactory.h"
#include "MG_State/GLState/ProgramState/ProgramObject.h"
#include "MG_State/GLState/ProgramState/ShaderObject.h"
#include "MG_State/GLState/TextureState/TextureEnum.h"
@@ -32,7 +33,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
CombinedImageSampler,
UniformTexelBuffer,
StorageBuffer,
StorageImage
StorageImage,
// GLSL `imageBuffer` - a buffer texture reached through an IMAGE unit rather than a
// texture unit. Vulkan spells it VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, which is a
// VkBufferView like UniformTexelBuffer and not a VkImageView like StorageImage: it is
// the one image uniform whose descriptor is a buffer. Appended, never inserted -
// DescriptorKeyHash mixes the enumerator's value.
StorageTexelBuffer
};
enum class CompileOptionBit : Uint {
@@ -42,31 +49,129 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SurfaceRotate90 = 1 << 2,
SurfaceRotate180 = 1 << 3,
SurfaceRotate270 = 1 << 4,
// Rewrites the fragment stage's implicit-LOD image samples to explicit LOD 0.
// Only ever set for a draw whose every sampler binding is clamped to a single mip
// level, which makes the two forms produce identical texels (the implicit lambda is
// clamped into [minLod, maxLod] = [0, 0] regardless of derivatives or bias).
ExplicitLod0Sampling = 1 << 5,
// Decorates the last vertex-processing stage's captured varyings with
// XfbBuffer/XfbStride/Offset (VK_EXT_transform_feedback). Set only for draws
// recorded while GL transform feedback is active, so plain draws keep the
// undecorated variant.
XfbCapture = 1 << 6,
// Rewrites the fragment stage's gl_FragCoord reads to GL's bottom-left window
// origin. Vulkan's gl_FragCoord.y IS the framebuffer row being written, and the
// default framebuffer's image is stored in display (top-left) order, so a shader
// that reads gl_FragCoord there sees `height - y_GL`. Set together with
// PositionYFlip (the two are the same fact about the same draws) except under a
// quarter turn, which this renderer does not convert rectangles for either.
FragCoordYFlip = 1 << 7,
// Replaces the vertex stage's gl_BaseVertex reads with zero. GL defines the builtin
// as zero for every drawing command that has no baseVertex parameter - all the
// DrawArrays forms - while Vulkan's BaseVertex reports firstVertex there. Set only
// for a non-indexed draw whose program actually reads the builtin, so nothing else
// acquires a second program/pipeline variant. See ZeroBaseVertexPass.
ZeroBaseVertex = 1 << 8,
};
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;
HashType hash = 0;
Vector<VkPipelineShaderStageCreateInfo> stages;
Vector<VkShaderModule> modules;
// Parallel to stages; identifies the exact module bytes handed to the driver when a
// pipeline creation fails. Sixteen bytes per stage instead of keeping the SPIR-V.
Vector<ShaderStageSpirvDigest> stageSpirvDigests;
// 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
// 256-binding cap while a real GL program uses 1-8, so the per-draw descriptor walk was
// scanning 256 slots to find a handful. MUST stay ascending: Vulkan consumes
// pDynamicOffsets in binding order and the writer pushes them in iteration order, so an
// unordered list would silently mis-pair dynamic offsets with their uniform blocks.
Vector<Uint32> activeBindings;
Vector<Uint32> dynamicBindings;
Vector<Int> uniformBlockIndexByBinding;
// Descriptor count per binding (1 except for a descriptor ARRAY - a UBO or storage
// block instance array, an image uniform array or a sampler uniform array - each of
// which occupies one binding with descriptorCount = N).
Vector<Uint16> bindingDescriptorCounts;
// Per-element GL uniform block indices for arrayed UBO bindings (count > 1);
// element 0 of a non-arrayed binding stays in uniformBlockIndexByBinding.
UnorderedMap<Uint32, Vector<Int>> arrayedUniformBlockIndicesByBinding;
Vector<String> samplerNameByBinding;
Vector<Int> samplerUniformLocationByBinding;
Vector<TextureTarget> samplerTextureTargetByBinding;
Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
// Shared by StorageImage and StorageTexelBuffer bindings: a binding is one kind or
// the other, never both, and both need exactly the same thing - the format the
// shader declared, so the per-draw resolve can tell a typed declaration from a
// formatless one. Kept as one pair rather than two so the move operations below
// cannot drift out of sync with a field that only one kind populates.
Vector<VkFormat> storageImageFormatByBinding;
Vector<Bool> storageImageUsesBindingFormatByBinding;
Vector<String> storageBlockNameByBinding;
Vector<Int> storageBlockIndexByBinding;
// Set once during ReflectLayout so the per-draw path can skip the whole
// storage-image preparation for the overwhelming majority of programs.
Bool hasStorageImages = false;
// Something about this program's descriptors could not be resolved - an opaque
// uniform array whose elements have no addressable uniform locations (the
// multi-dimensional case), or a binding remap that failed outright. The binding
// STAYS DECLARED in the descriptor set layout; declining is done here, by refusing
// every draw, and BindProgramUniformBuffers returns false so the draw setup skips
// the draw exactly as it does for any other bind failure.
//
// Keeping the layout intact is the load-bearing half. Shrinking it instead - which
// is what the first cut of this did - leaves the shader reading a descriptor the
// layout never declared, and lavapipe segfaults on that inside PIPELINE CREATION,
// in a JIT worker thread, before any draw runs where a refusal could help. The
// reason was logged once at MGLOG_I when the descriptor was declined.
Bool declinedDescriptors = false;
Int globalUboBinding = -1;
Uint32 activeVertexInputLocationMask = 0;
Array<GLenum, kMaxVertexInputLocations> vertexInputTypes{};
@@ -75,6 +180,76 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ShaderStage rasterizationProducerStage = ShaderStage::Unknown;
Uint32 producerOutputComponentCount = 0;
Uint32 fragmentInputComponentCount = 0;
// The fragment module declares the DepthReplacing execution mode (writes
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
Bool fragmentReplacesDepth = false;
// The vertex module declares the BaseVertex builtin. Selects the ZeroBaseVertex
// program variant for non-indexed draws, and is deliberately a property of the
// 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
// levels come from the PATCH_DEFAULT_*_LEVEL state); Vulkan does not - either both
// 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
// built-ins. A user-defined varying would arrive at the evaluation stage
// UNWRITTEN once a control stage sits between it and the vertex stage, which is
// silently wrong pixels rather than a crash - so those programs are declined
// 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).
mutable Uint64 lastUsedFrame = 0;
static inline VkDevice s_device = VK_NULL_HANDLE;
@@ -85,11 +260,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
hash = other.hash;
stages = std::move(other.stages);
modules = std::move(other.modules);
// Must travel with `modules`: these digests name the SPIR-V those exact
// shader modules were built from, and the pipeline-failure diagnostics
// print the two together. Leaving it behind used to merely lose the
// digests on a rehash; now that the cache is a robin-hood table, insertion
// SWAPS two entries, and a field that no move touches stays behind in the
// slot - pairing one program's modules with another program's digests, so
// 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);
dynamicBindings = std::move(other.dynamicBindings);
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
arrayedUniformBlockIndicesByBinding = std::move(other.arrayedUniformBlockIndicesByBinding);
samplerNameByBinding = std::move(other.samplerNameByBinding);
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
@@ -99,6 +286,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
std::move(other.storageImageUsesBindingFormatByBinding);
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
hasStorageImages = other.hasStorageImages;
declinedDescriptors = other.declinedDescriptors;
globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes;
@@ -107,15 +296,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
rasterizationProducerStage = other.rasterizationProducerStage;
producerOutputComponentCount = other.producerOutputComponentCount;
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;
other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0;
other.rasterizationProducerStage = ShaderStage::Unknown;
other.producerOutputComponentCount = 0;
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 {
if (this == &other) {
@@ -125,11 +331,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
hash = other.hash;
stages = std::move(other.stages);
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);
dynamicBindings = std::move(other.dynamicBindings);
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
arrayedUniformBlockIndicesByBinding = std::move(other.arrayedUniformBlockIndicesByBinding);
samplerNameByBinding = std::move(other.samplerNameByBinding);
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
@@ -139,6 +350,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
std::move(other.storageImageUsesBindingFormatByBinding);
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
hasStorageImages = other.hasStorageImages;
declinedDescriptors = other.declinedDescriptors;
globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes;
@@ -147,15 +360,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
rasterizationProducerStage = other.rasterizationProducerStage;
producerOutputComponentCount = other.producerOutputComponentCount;
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;
other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0;
other.rasterizationProducerStage = ShaderStage::Unknown;
other.producerOutputComponentCount = 0;
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;
}
@@ -182,27 +412,171 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
modules.clear();
stages.clear();
stageSpirvDigests.clear(); // the modules they describe are gone
}
};
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
Bool shaderDrawParametersEnabled = false,
Bool unformattedFloatStorageImagesEnabled = false)
// Notified when the OnFrameBoundary sweep destroys an aged-out cache entry,
// carrying the entry's content hash and the VkDescriptorSetLayout it owned.
// Dependent caches (compute pipelines, PipelineFactory entries, UniformManager's
// per-layout descriptor sets) must purge in the same step: after vkDestroy the
// layout handle value may be recycled for an unrelated layout, and the program
// hash may be re-inserted by a later rebuild of the same content.
class IEvictionObserver {
public:
virtual ~IEvictionObserver() = default;
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
};
// 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;
}
~ProgramFactory() = default;
// Destroys the pass-through tessellation control modules. Runs while the device is
// still alive for the same reason ~VkProgramObject's does: this factory outlives
// nothing that owns the device.
~ProgramFactory();
ProgramFactory(const ProgramFactory&) = delete;
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
const VkProgramObject& GetOrCreateProgram(
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
// The default framebuffer's current image height, baked as a literal into every
// FragCoordYFlip variant (there is no push-constant or specialization channel here, and
// adding one for a value that changes only on swapchain recreation would cost the draw
// path more than a recompile costs a resize). It is therefore part of those variants'
// identity: ComputeHash mixes it in when the bit is set, so a height change re-keys them
// and leaves every other program's hash untouched. Setting a NEW height also bumps the
// cache-structure epoch, because a caller holding a memoised VkProgramObject* would
// otherwise keep using a module compiled against the old height.
void SetDefaultFramebufferHeight(Uint32 height);
Uint32 GetDefaultFramebufferHeight() const { return m_defaultFramebufferHeight; }
// Bumped whenever m_cache's STRUCTURE changes (any insert or erase): the cache is
// an open-addressing map holding entries by value, so both moves existing entries.
// A caller that memoised a VkProgramObject* may keep dereferencing it only while
// this is unchanged; on a bump it must re-run GetOrCreateProgram.
Uint64 GetCacheStructureEpoch() const { return m_cacheStructureEpoch; }
// A memoised entry pointer bypasses GetOrCreateProgram, whose per-lookup stamp is
// what keeps an in-use entry out of OnFrameBoundary's idle sweep - so such a
// caller must re-stamp the entry itself, at least once per frame boundary.
void StampProgramUse(const VkProgramObject& entry) const { entry.lastUsedFrame = m_frameCounter; }
// Observer may be null (no notifications). Not owned.
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
// Frame boundary hook: ages the program cache and evicts long-unused entries
// (their command buffers retired many frames ago), mirroring
// VkRenderPassManager::OnPresent's sweep.
void OnFrameBoundary();
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
// stripped conservatively and forfeits its depth write.
static Bool ReflectedFragmentReplacesDepth(const SpvReflectShaderModule& reflectModule);
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule);
// True when an entry point declares the BaseVertex builtin, i.e. when a non-indexed
// draw with this program has to take the ZeroBaseVertex variant.
static Bool ReflectedReadsBaseVertexBuiltin(const SpvReflectShaderModule& reflectModule);
// 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
// `patchVertices` control points. Returned BY VALUE (a stage description is a POD, and
// the cache below is a rehashing map, so a pointer into it would not survive the next
// distinct patch size). `.module == VK_NULL_HANDLE` means the stage could not be built:
// 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, 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, 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 {
@@ -213,14 +587,27 @@ 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,
VkProgramObject& entry) const;
// 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<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
VkDevice m_device = VK_NULL_HANDLE;
Uint32 m_maxBindings = 0;
@@ -232,7 +619,42 @@ 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;
mutable ProgramLookupCache m_lastLookup;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameCounter = 0;
// 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 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.
//
// 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
@@ -157,7 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_I("Got %d surface formats:", swapchainCapabilities.surfaceFormats.size());
for (const auto& sf : swapchainCapabilities.surfaceFormats) {
MGLOG_I(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
MGLOG_D(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
}
const auto pickedSurfaceFormat = ChooseSwapchainSurfaceFormat(swapchainCapabilities.surfaceFormats);
@@ -166,7 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_I("Got %d present modes:", swapchainCapabilities.presentModes.size());
for (const auto& pm : swapchainCapabilities.presentModes) {
MGLOG_I(" %s", string_VkPresentModeKHR(pm));
MGLOG_D(" %s", string_VkPresentModeKHR(pm));
}
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
@@ -247,6 +247,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_surfaceFormat = {createInfo.imageFormat, createInfo.imageColorSpace};
m_extent = createInfo.imageExtent;
// The surface-space extent this swapchain was built from, i.e. before the
// quarter-turn swap above. Out-of-date checks must compare in THIS space: comparing a
// freshly queried currentExtent against the swapped m_extent flips axes every rotation
// and makes the comparison alternate forever.
m_surfaceExtent = defaultFramebufferExtent;
m_preTransform = createInfo.preTransform;
VK_VERIFY(vkCreateSwapchainKHR(device, &createInfo, nullptr, &m_swapchain));
@@ -257,6 +262,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_images.resize(imageCount, VK_NULL_HANDLE);
VK_VERIFY(vkGetSwapchainImagesKHR(device, m_swapchain, &imageCount, m_images.data()));
m_imageLayouts.assign(imageCount, VK_IMAGE_LAYOUT_UNDEFINED);
// Fresh swapchain images hold garbage until a render pass stores into them.
m_imageContentDefined.assign(imageCount, false);
m_depthStencilContentDefined.assign(imageCount, false);
CreateImageViews(device);
CreateDepthStencilResources(device, physicalDevice);
@@ -428,9 +436,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_images.clear();
m_imageLayouts.clear();
m_imageContentDefined.clear();
m_depthStencilContentDefined.clear();
m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
}
Bool SwapchainObject::IsImageContentDefined(Uint32 index) const {
MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range");
return m_imageContentDefined[index];
}
void SwapchainObject::SetImageContentDefined(Uint32 index, Bool defined) {
MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range");
m_imageContentDefined[index] = defined;
}
Bool SwapchainObject::IsDepthStencilContentDefined(Uint32 index) const {
MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(),
"Swapchain depth/stencil content index out of range");
return m_depthStencilContentDefined[index];
}
void SwapchainObject::SetDepthStencilContentDefined(Uint32 index, Bool defined) {
MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(),
"Swapchain depth/stencil content index out of range");
m_depthStencilContentDefined[index] = defined;
}
void SwapchainObject::SetAllDepthStencilContentUndefined() {
for (SizeT i = 0; i < m_depthStencilContentDefined.size(); ++i) {
m_depthStencilContentDefined[i] = false;
}
}
VkImage SwapchainObject::GetImage(Uint32 index) const {
MOBILEGL_ASSERT(index < m_images.size(), "Swapchain image index out of range");
return m_images[index];
@@ -35,6 +35,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSwapchainKHR GetHandle() const { return m_swapchain; }
const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; }
VkExtent2D GetExtent() const { return m_extent; }
// Surface-space extent (before the pre-rotation quarter-turn swap) this swapchain was
// created from - the value to compare a freshly queried currentExtent against.
VkExtent2D GetSurfaceExtent() const { return m_surfaceExtent; }
VkSurfaceTransformFlagBitsKHR GetPreTransform() const { return m_preTransform; }
const Vector<VkImage>& GetImages() const { return m_images; }
const Vector<VkImageView>& GetImageViews() const { return m_imageViews; }
@@ -49,6 +52,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void SetImageLayout(Uint32 index, VkImageLayout layout);
SizeT GetImageCount() const { return m_images.size(); }
// EGL content-validity tracking for the default framebuffer. A color
// buffer's content is undefined once its image has been presented
// (EGL_BUFFER_DESTROYED swap behaviour, the implementation default),
// and every ancillary (depth/stencil) buffer's content is undefined
// after ANY swap regardless of swap behaviour (EGL 1.5 §3.10.1). The
// render-pass manager turns an undefined attachment's tile load into
// LOAD_OP_DONT_CARE. Flags start false (a fresh swapchain image holds
// garbage) and a render pass storing into an attachment sets it back
// to defined.
Bool IsImageContentDefined(Uint32 index) const;
void SetImageContentDefined(Uint32 index, Bool defined);
Bool IsDepthStencilContentDefined(Uint32 index) const;
void SetDepthStencilContentDefined(Uint32 index, Bool defined);
void SetAllDepthStencilContentUndefined();
private:
void CreateImageViews(VkDevice device);
void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice);
@@ -63,6 +81,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
VkSurfaceFormatKHR m_surfaceFormat{};
VkExtent2D m_extent{};
VkExtent2D m_surfaceExtent{};
VkSurfaceTransformFlagBitsKHR m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
Vector<VkImage> m_images;
Vector<VkImageView> m_imageViews;
@@ -73,5 +92,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<VkDeviceMemory> m_depthStencilImageMemories;
Vector<VkImageView> m_depthStencilImageViews;
Vector<VkImageLayout> m_depthStencilImageLayouts;
Vector<Bool> m_imageContentDefined;
Vector<Bool> m_depthStencilContentDefined;
};
} // 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,
@@ -39,18 +53,66 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void Shutdown();
void BeginFrame(Uint32 frameIndex);
// A command buffer (re)began recording: descriptor bindings recorded into
// the previous buffer do not carry over, so drop the bind-dedup shadow.
void OnCommandBufferBoundary() { m_lastBindValid = false; }
// A ProgramFactory eviction just destroyed this layout: purge every frame
// slot's cached descriptor sets for it, so a recycled handle value can never
// stale-hit sets written for the dead layout's bindings. The sets are
// vkFreeDescriptorSets'd back to their pools (created with
// FREE_DESCRIPTOR_SET_BIT) and the pool accounting is credited, so program
// churn recycles pool capacity instead of abandoning it. GPU-safe: the layout
// only dies after >1024 idle frame boundaries, so no in-flight command buffer
// references its sets. This is the only eviction path for the per-layout
// caches - a live layout's entry must never be purged (its sets would be
// unreachable pool slots), so there is deliberately no age-based sweep here.
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
// One record per visited CombinedImageSampler DESCRIPTOR (post fallback substitution,
// in binding order, and within a binding in array-element order): the resolved texture
// and effective sampler, as never-reused lifetime ids so a freed-and-reallocated object
// at the same heap address can only MISS a comparison, never false-hit it (same ABA
// rule as SamplerResolveMemo). An arrayed binding contributes one record per element -
// element granularity is required, or swapping the textures of two elements of the same
// array would leave the record list identical and the fast path would keep a stale set.
struct SampledBindingRecord {
Uint64 textureLifetimeId = 0;
Uint64 samplerLifetimeId = 0;
};
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures);
Vector<MG_State::GLState::ITextureObject*>& outTextures,
Vector<SampledBindingRecord>* outBindingRecords = nullptr);
// Shadow-compare for the SetupDraw fast path: re-runs the CollectSampledTextures
// walk and reports whether every visited binding still resolves to the recorded
// (texture, effective sampler) pair. A texture bind generation bump alone (e.g. a
// redundant glBindSampler, which always bumps it) does not prove the sampled set
// moved; this walk does, without rebuilding the set or falling off the fast path.
Bool SampledBindingsUnchanged(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
const Vector<SampledBindingRecord>& previousRecords) const;
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
// sum, sampling-resolution generation (sampler params + texture shape), image
// epochs AND per-resource layout values - so the per-binding cached
// VkDescriptorImageInfo may be reused without re-running the resolve chain.
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 frameIndex,
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
const SamplerBindingOverride* samplerBindingOverride = nullptr);
const SamplerBindingOverride* samplerBindingOverride = nullptr,
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
@@ -58,15 +120,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
VkFormat resourceFormat, Bool useBindingFormat);
// True when the program reads at least one sampler and every one of them is bound to a
// texture whose GL level range is a single level. Such a sampler resolves to
// minLod = maxLod = 0 (see VkSamplerManager::GetOrCreateSampler), so an implicit-LOD sample
// and an explicit LOD 0 sample must read the same texel - which is what makes the
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
// only GL state, so a texture that ends up single-level for another reason (one uploaded
// level under a wide level range) merely misses the rewrite.
static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
private:
struct DescriptorPoolBucket {
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
// layout-destroyed purge can vkFreeDescriptorSets it back and credit the
// owning bucket's accounting.
struct CachedDescriptorSet {
VkDescriptorSet set = VK_NULL_HANDLE;
VkDescriptorPool pool = VK_NULL_HANDLE;
};
struct DescriptorSetCacheEntry {
Vector<VkDescriptorSet> sets;
Vector<CachedDescriptorSet> sets;
Uint32 cursor = 0;
};
@@ -82,28 +163,96 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static Bool ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
// Shared per-binding resolution for CollectSampledTextures and
// SampledBindingsUnchanged, so membership and comparison can never diverge:
// texture after the fallback substitution (may still be null when no fallback
// exists), effective sampler = unit override else the texture's own sampler.
// False = the binding is skipped (unbound with a non-2D fallback target).
// `element` indexes a sampler array inside the binding; see ResolveSamplerDescriptor.
Bool ResolveSampledBinding(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element,
MG_State::GLState::ITextureObject*& outTexture,
const MG_State::GLState::SamplerObject*& outSampler) const;
// Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures):
// the bound texture stays alive through the draw via GL binding state, so callers that
// only need the pointer skip the SharedPtr copy's atomic refcount churn.
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding);
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
// `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.
//
// trustUnchangedHint: reuse this binding's cached VkDescriptorImageInfo outright
// (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof
// obligations the caller carries). The cache is keyed by binding alone, so it is
// used ONLY for single-descriptor bindings - see m_samplerResolveMemo.
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
VkDescriptorImageInfo& outImageInfo) const;
Uint32 element, VkDescriptorImageInfo& outImageInfo,
Bool trustUnchangedHint = false) const;
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
VkDescriptorImageInfo& outImageInfo) const;
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 frameIndex, VkBufferView& outBufferView);
// GLSL `imageBuffer`: the same VkBufferView descriptor as the sampled texel buffer above,
// but resolved from an IMAGE unit (glBindImageTexture) rather than a texture unit, and
// made GPU-resident-writable because the shader may store to it. No `element` parameter:
// an imageBuffer ARRAY is refused at program creation, so a binding is always one
// descriptor (see the array gate in RemapDescriptorBindingsForVulkan).
Bool ResolveStorageTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 frameIndex, VkBufferView& outBufferView);
// `element` indexes a block INSTANCE array's descriptors; it is 0 for every ordinary
// block. Each element resolves through its own GL storage block, and so its own GL
// binding point, buffer and glBindBufferRange window.
Bool ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
VkDescriptorBufferInfo& outBufferInfo) const;
Uint32 element, VkDescriptorBufferInfo& outBufferInfo) const;
// `element` indexes an image ARRAY inside one binding; each element carries its own
// independently assigned GL image unit.
Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
VkDescriptorImageInfo& outImageInfo) const;
Uint32 element, VkDescriptorImageInfo& outImageInfo) const;
// Result of resolving a UBO binding: either a zero-copy direct bind to the app's resident
// VkBuffer (the GLES backend's approach - no per-draw copy) or the CPU payload to upload.
struct UboBindResult {
@@ -116,9 +265,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
UboBindResult& out) const;
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
Uint32 arrayElement, UboBindResult& out) const;
// Shared resolution of one dynamic-UBO binding element into the
// (buffer, range, dynamicOffset) triple the descriptor consumes: direct
// bind, global-slice reuse, or transient upload. Used by the full walk
// and by the dynamic-offset-only rebind (see FastRebindMemo).
Bool ResolveDynamicUboDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 arrayElement, Uint32 frameIndex, VkBuffer& outBuffer,
VkDeviceSize& outRange, Uint32& outDynamicOffset);
// The vkCmdBindDescriptorSets tail shared by the full walk and the
// dynamic-offset-only rebind: skips the driver call when this exact
// binding is already live on the command buffer (see the bind-dedup
// shadow below), otherwise binds and refreshes the shadow.
void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
const Vector<Uint32>& dynamicOffsets);
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,
@@ -126,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;
@@ -138,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.
@@ -147,14 +324,82 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<VkBufferView> m_texelBufferViewsScratch;
Vector<Uint32> m_dynamicOffsetsScratch;
// Descriptor-set reuse across consecutive draws (see BindProgramUniformBuffers).
// When a draw's resolved descriptor content is byte-identical to the previous
// draw's, reuse the same VkDescriptorSet and skip AcquireDescriptorSet +
// vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Reset each
// frame in BeginFrame because the frame's descriptor sets are recycled there.
VkDescriptorSet m_lastBoundDescriptorSet = VK_NULL_HANDLE;
Uint64 m_lastDescriptorSignature = 0;
Bool m_hasLastDescriptor = false;
// Descriptor-set reuse across recent draws (see BindProgramUniformBuffers).
// When a draw's resolved descriptor content is byte-identical to one memoized
// earlier, reuse that VkDescriptorSet and skip AcquireDescriptorSet +
// vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Four
// entries with round-robin replacement rather than one: draws alternating
// between two programs (MC's chunk<->entity ping-pong) would thrash a single
// slot into a full re-allocate+write every draw. Reset each frame in BeginFrame
// because the frame's descriptor sets are recycled there.
struct DescriptorReuseEntry {
Uint64 signature = 0;
VkDescriptorSet set = VK_NULL_HANDLE;
Bool valid = false;
};
static constexpr Uint32 kDescriptorReuseMemoSize = 4;
DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize];
Uint32 m_descriptorReuseMemoNext = 0;
// Dynamic-offset-only rebind (see BindProgramUniformBuffers): records the
// descriptor set selected by the last cacheable full walk of a program
// whose active bindings are exactly one dynamic UBO (single descriptor)
// plus combined-image samplers. When the next call proves every sampler
// descriptor input unchanged (samplerDescriptorsUnchangedHint) and the
// UBO re-resolves to the SAME VkBuffer+range - only the dynamic offset
// moved, the per-draw glUniform case - the walk collapses to: resolve one
// offset, rebind the recorded set with new pDynamicOffsets (Vulkan allows
// rebinding the same set with different dynamic offsets).
// Invalidation inventory: BeginFrame clears it (the frame's sets are
// recycled) and the frameIndex field guards cross-frame confusion on top;
// OnDescriptorSetLayoutDestroyed clears it (the set may be freed); a
// sampler-override walk clears it (mirrors m_descriptorReuseMemo); a
// program relink bumps the backend state version and thus programObj.hash
// so the key misses; the program lifetime id is never reused, so a
// deleted-and-recreated program misses; a texture/sampler/binding change
// drops the hint upstream; an arena wrap or growth resolves a different
// VkBuffer and misses. AcquireDescriptorSet's per-frame cursor only
// advances, so the recorded set is never re-written within its frame.
struct FastRebindMemo {
Bool valid = false;
Uint32 frameIndex = 0;
Uint64 programLifetimeId = 0;
ProgramFactory::HashType programHash = 0;
Uint32 uboBinding = 0;
VkBuffer uboBuffer = VK_NULL_HANDLE;
VkDeviceSize uboRange = 0;
VkDescriptorSet set = VK_NULL_HANDLE;
};
FastRebindMemo m_fastRebindMemo;
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
// block resolve to the same set AND the same dynamic offsets, so the
// driver call can be skipped outright. Command-buffer-scope state; reset
// via OnCommandBufferBoundary whenever a recording (re)begins. Keyed on
// layout+bind point, so a pipeline-layout switch always rebinds.
static constexpr Uint32 kMaxShadowedDynamicOffsets = 8;
Bool m_lastBindValid = false;
VkDescriptorSet m_lastBindSet = VK_NULL_HANDLE;
VkPipelineLayout m_lastBindLayout = VK_NULL_HANDLE;
VkPipelineBindPoint m_lastBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
Uint32 m_lastBindOffsetCount = 0;
Uint32 m_lastBindOffsets[kMaxShadowedDynamicOffsets] = {};
// Global-UBO transient-slice reuse: MC leaves the default uniform block
// untouched across long GUI/terrain runs, so the per-draw re-upload of
// the same bytes can reuse the slice uploaded earlier THIS frame (frame
// serial guards arena recycling; the content version guards writes).
struct GlobalUboSliceMemo {
Uint64 programLifetimeId = 0;
Uint64 frameSerial = 0;
Uint32 uboContentVersion = 0;
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceSize offset = 0;
VkDeviceSize range = 0;
};
static constexpr Uint32 kGlobalUboMemoSize = 4;
GlobalUboSliceMemo m_globalUboMemo[kGlobalUboMemoSize];
Uint32 m_globalUboMemoNext = 0;
// Per-binding fast path over VkSamplerManager's content-hashed sampler cache, which
// stays the source of truth: its key hashes all sampler+texture state, so two distinct
@@ -167,16 +412,56 @@ 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;
Uint32 viewLevelCount = 0;
Uint16 samplerVersion = 0;
Uint16 textureParamsVersion = 0;
Bool forceNearestFiltering = false;
Bool valid = false;
// ResolveSampledImageViewFormat is pure in (image format, numeric domain), but a
// domain mismatch walks a ~184-entry format table. Memo the resolution per binding
// so a reinterpreted sampler pays that scan once, not once per draw.
VkFormat viewFormatSource = VK_FORMAT_UNDEFINED;
SamplerNumericDomain viewFormatDomain = SamplerNumericDomain::Unknown;
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
Bool viewFormatValid = false;
// Whole resolved descriptor from this binding's last full resolve. Reused
// ONLY under ResolveSamplerDescriptor's trustUnchangedHint, whose caller
// proves every resolve input unchanged; cleared with the per-frame reset
// (the cached VkSampler outlives a frame only via a fresh resolve, which
// also re-stamps it against VkSamplerManager's frame-boundary sweep).
//
// This one field is keyed by binding but describes ONE descriptor, so it is
// written and read only for single-descriptor bindings. A sampler ARRAY's
// elements share the binding and would overwrite each other here - the last
// element resolved would then be handed to element 0 on the next hinted draw.
// Every other field above is self-validating (each compares its full key
// before reuse, and the view-format entry is a pure function of format and
// numeric domain), so an arrayed binding may keep using those.
VkDescriptorImageInfo info{};
Bool infoValid = false;
};
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
// Exclusive upper bound on the entries of m_samplerResolveMemo that any resolve
// has ever written. The vector is sized to the DEVICE binding cap (256 on desktop
// NVIDIA), but a program declares 1-8 bindings, so the per-frame reset below was
// memsetting ~22 KB of never-touched entries every frame - a measurable slice of
// the per-frame fixed cost on draw-light frames. Every site that can turn any of
// an entry's *Valid flags on raises this mark first, so entries at or above it are
// provably still in their constructed (all-invalid) state and clearing them is a
// no-op. Never lowered except by Initialize/Shutdown, which rebuild the vector.
mutable Uint32 m_samplerResolveMemoHighWater = 0;
void NoteSamplerResolveMemoTouched(Uint32 binding) const {
if (binding >= m_samplerResolveMemoHighWater) {
m_samplerResolveMemoHighWater = binding + 1;
}
}
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -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 {
@@ -29,10 +30,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsLong, sizeof(attr.IsLong)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
// The bound buffer's IDENTITY is a component of the key, and it has to be the
// buffer's never-reused lifetime id - NOT its heap address, which this used to
// hash. An address is recycled by the allocator, so a deleted-and-recreated
// buffer reproduces it; combined with a byte-identical attribute layout that
// reproduces the WHOLE content hash, and the hash is what
// TryBindResolvedVertexBindings accepts as proof that a memoised binding still
// reads the buffer it was resolved from. It did not: a destroyed buffer's GPU
// slice was bound for its successor's draw, which is how a transform-feedback
// capture came back holding a dead VAO's vertex data (0,0,0,1 - the previous
// test's positions) instead of its own.
// Zero for client memory (no buffer), which is a distinct identity of its own.
const Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
}
@@ -51,14 +64,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao) {
return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
// Per-draw fast path: the VAO carries a pointer to its resolved entry,
// valid while its config version and the cache's eviction epoch both
// match - no re-hash, no map lookup.
const void* memoState = nullptr;
Uint64 memoEpoch = 0;
if (vao.GetBackendStateMemo(memoState, memoEpoch) && memoEpoch == m_evictionEpoch) {
const auto* entry = static_cast<const BackendVertexInputState*>(memoState);
entry->lastUsedFrameBoundary = m_frameBoundaryCounter;
return *entry;
}
const BackendVertexInputState& entry = GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
vao.SetBackendStateMemo(&entry, m_evictionEpoch);
// Also mirror the layout identity and the two per-draw masks into the VAO's aux
// memo (pure VALUES derived from the VAO configuration, so config-version
// guarding alone is sound). The draw fast path reads them from the VAO object it
// already touched instead of chasing into this entry - see PackVertexInputAuxMemo.
vao.SetBackendAuxMemo(entry.layoutHash,
PackVertexInputAuxMasks(entry.unsupportedAttribMask, entry.attributeLocationMask));
return entry;
}
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
auto it = m_cache.find(hash);
if (it != m_cache.end()) {
return it->second;
it->second->lastUsedFrameBoundary = m_frameBoundaryCounter;
return *it->second;
}
VertexInputStateBuilder builder;
@@ -67,6 +99,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Uint32> bindingAttributeLocations;
Vector<Bool> bindingUsesClientMemory;
Vector<VertexStreamConversion> bindingConversions;
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
Uint32 unsupportedAttribMask = 0;
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
@@ -75,10 +108,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue;
}
const VkFormat sourceVkFormat =
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra);
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("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
"enabled but cannot be mapped to a VkFormat",
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
unsupportedAttribMask |= (1u << location);
@@ -86,14 +145,13 @@ 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)) {
vkFormat = fallbackFormat;
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
MGLOG_W("Vertex attribute location=%u format=%d lacks "
MGLOG_W_ONCE("Vertex attribute location=%u format=%d lacks "
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
"(type=%s size=%d normalized=%s integer=%s)",
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
@@ -103,7 +161,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (conversion == VertexStreamConversion::None) {
MGLOG_E("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
MGLOG_E_ONCE("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
location, static_cast<Int>(sourceVkFormat),
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
@@ -114,34 +172,52 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
if (attribByteSize == 0) {
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
MGLOG_E_ONCE("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
"enabled but cannot be sized",
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
unsupportedAttribMask |= (1u << location);
continue;
}
const Uint32 sourceStride =
attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize);
// Verbatim, zero included. The frontend already resolved a pointer call's
// "tightly packed" stride 0 into the element size (see VertexAttribute::Stride),
// so a zero here is the binding model's stride 0 - every vertex reads the same
// element - which is exactly what a zero VkVertexInputBindingDescription::stride
// means. Substituting the element size fetched a fresh element per vertex and ran
// off the end of the buffer (KHR-GL43.vertex_attrib_binding.basic-input-case7/8).
// Client-memory arrays cannot reach zero: they only exist on the pointer path.
const Uint32 sourceStride = static_cast<Uint32>(attr.Stride);
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
attr.Type == DataType::Uint2101010Rev;
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
// For a client-memory array attr.Offset holds the raw client pointer, and the
// draw path re-uploads the data to a 16-aligned transient slice with attribute
// offset 0, so only the stride can violate Vulkan's fetch alignment there.
const Bool clientMemoryAttribute = attr.Buffer == nullptr;
if (conversion == VertexStreamConversion::None && requiredAlignment > 1 &&
((sourceStride % requiredAlignment) != 0 || (attr.Offset % requiredAlignment) != 0)) {
((sourceStride % requiredAlignment) != 0 ||
(!clientMemoryAttribute && (attr.Offset % requiredAlignment) != 0))) {
// GL accepts arbitrary byte strides and offsets. Core Vulkan vertex fetches do not
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
// attribute into a tightly packed transient stream without changing its format.
conversion = VertexStreamConversion::Repack;
MGLOG_W("Vertex attribute location=%u uses Vulkan-incompatible alignment "
MGLOG_W_ONCE("Vertex attribute location=%u uses Vulkan-incompatible alignment "
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
location, attr.Offset, sourceStride, requiredAlignment);
}
Uint32 stride = sourceStride;
if (conversion == VertexStreamConversion::Repack) {
stride = static_cast<Uint32>(attribByteSize);
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
// A converted stream is tightly packed, so its stride is the converted element
// size - unless the source stride is zero, which does not describe a packing at
// all but "never advance". That survives the conversion unchanged: the draw path
// converts exactly one element and every vertex reads it.
if (sourceStride != 0) {
if (conversion == VertexStreamConversion::Repack) {
stride = static_cast<Uint32>(attribByteSize);
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32 ||
conversion == VertexStreamConversion::Float64ToFloat32) {
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
}
}
const VkVertexInputRate inputRate =
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
@@ -155,14 +231,51 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bindingConversions.push_back(conversion);
builder.AddBinding(binding, stride, inputRate);
builder.AddAttribute(location, binding, vkFormat, 0);
// Divisor 1 is what VK_VERTEX_INPUT_RATE_INSTANCE already means; only anything
// else needs the extension to say it.
if (inputRate == VK_VERTEX_INPUT_RATE_INSTANCE && attr.Divisor != 1) {
bindingDivisors.push_back({binding, static_cast<Uint32>(attr.Divisor)});
}
}
const auto& state = builder.Build();
auto& entry = m_cache[hash];
auto& slot = m_cache[hash];
if (!slot) {
slot = MakeUnique<BackendVertexInputState>();
}
BackendVertexInputState& entry = *slot;
entry.hash = hash;
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
entry.bindingDivisors = Move(bindingDivisors);
entry.bindings = builder.GetBindings();
entry.attributes = builder.GetAttributes();
// See the layoutHash declaration: hash only the resolved layout, never
// buffer identities, so identical layouts across VAOs/buffers agree.
XXHASH_VERIFY(XXH64_reset(m_hashState, 0));
for (const auto& binding : entry.bindings) {
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.binding, sizeof(binding.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.stride, sizeof(binding.stride)));
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.inputRate, sizeof(binding.inputRate)));
}
for (const auto& attribute : entry.attributes) {
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.location, sizeof(attribute.location)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.binding, sizeof(attribute.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
}
for (const auto& divisor : entry.bindingDivisors) {
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.binding, sizeof(divisor.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.divisor, sizeof(divisor.divisor)));
}
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
entry.layoutHash = XXH64_digest(m_hashState);
entry.attributeLocationMask = 0;
for (const auto& attribute : entry.attributes) {
if (attribute.location < 32u) {
entry.attributeLocationMask |= (1u << attribute.location);
}
}
entry.bindingBufferKeys = std::move(bindingBufferKeys);
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
@@ -172,11 +285,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.state = state;
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
entry.state.pVertexAttributeDescriptions = entry.attributes.empty() ? nullptr : entry.attributes.data();
if (!entry.bindingDivisors.empty()) {
entry.divisorState.vertexBindingDivisorCount = static_cast<Uint32>(entry.bindingDivisors.size());
entry.divisorState.pVertexBindingDivisors = entry.bindingDivisors.data();
entry.state.pNext = &entry.divisorState;
} else {
entry.state.pNext = nullptr;
}
return entry;
}
void VertexInputStateFactory::OnFrameBoundary() {
++m_frameBoundaryCounter;
// Sweep occasionally; evict entries whose last hit is far in the past.
// Erasure happens only here, never mid-frame: the draw path holds a
// reference into the current entry across its setup, and unordered_map
// erase would invalidate it. Entries are CPU-side only, so no GPU-idle
// proof is needed; an evicted entry that is used again is simply rebuilt
// from the VAO state (same hash, same content).
constexpr Uint64 kSweepInterval = 256;
constexpr Uint64 kRetireAgeBoundaries = 1024;
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
return;
}
for (auto it = m_cache.begin(); it != m_cache.end();) {
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. Advance through the
// process-wide source so the value stays unique across factory
// instances (see the member comment).
m_evictionEpoch = ++s_evictionEpochSource;
} else {
++it;
}
}
}
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
Bool isBgra) {
Bool isBgra, Bool isLong) {
if (isBgra) {
// GL_BGRA: four reversed-order components, always normalized (enforced at validation), only
// legal with GL_UNSIGNED_BYTE or a 2_10_10_10 type. The reversed VkFormats put the
@@ -201,6 +350,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case DataType::Int2101010Rev:
if (isInteger || size != 4) return VK_FORMAT_UNDEFINED;
return normalized ? VK_FORMAT_A2B10G10R10_SNORM_PACK32 : VK_FORMAT_A2B10G10R10_SSCALED_PACK32;
case DataType::Float64:
// A 64-bit attribute is fetched as its 32-bit word pair and bitcast back to double in the
// shader (PackDoubleVertexInputsPass does the shader half). That is bit-exact and, unlike
// VK_FORMAT_R64*_SFLOAT, needs no format capability: lavapipe reports bufferFeatures = 0
// 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;
case 2: return VK_FORMAT_R32G32B32A32_UINT;
// A dvec3/dvec4 input is 6/8 uint32 components: no single VkFormat, and GL spreads it
// over two attribute locations, which the location-per-VAO-index model here does not
// express. Declined rather than fetched wrong.
default: return VK_FORMAT_UNDEFINED;
}
case DataType::Float32:
switch (size) {
case 1: return VK_FORMAT_R32_SFLOAT;
@@ -23,10 +23,26 @@ 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 {
HashType hash = 0;
// Hash of the resolved Vulkan vertex layout only (bindings, attributes,
// unsupported mask) - NO buffer identities. `hash` mixes each bound
// buffer's never-reused LIFETIME ID, so per-chunk VBOs mint a fresh
// identity per buffer; keying pipelines on that minted one VkPipeline per
// chunk section for an identical layout, defeating pipeline reuse and the
// per-draw memo. Pipelines depend only on the layout, so they key on this
// instead.
HashType layoutHash = 0;
// Frame boundary of the last cache hit; entries idle past the
// OnFrameBoundary retirement age are evicted (CPU heap only).
// Mutable: the VAO's state-pointer memo fast path stamps it through
// a const entry reference.
mutable Uint64 lastUsedFrameBoundary = 0;
Vector<VkVertexInputBindingDescription> bindings;
Vector<VkVertexInputAttributeDescription> attributes;
Vector<SizeT> bindingBufferKeys;
@@ -38,6 +54,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// absent from `attributes`, so without this mask the draw path cannot tell them apart from
// a genuinely disabled array and would silently feed the shader the current attribute value.
Uint32 unsupportedAttribMask = 0;
// Bitmask of `attributes[i].location` - the draw path needs it up to
// three times per draw, so it is baked once at build time.
Uint32 attributeLocationMask = 0;
// Per-binding glVertexAttribDivisor values other than 1. Vulkan's instance input
// rate advances once per instance and nothing else, so anything else has to be
// stated through VK_EXT_vertex_attribute_divisor. Empty when every instanced
// binding uses divisor 1, which is what the plain input rate already means.
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
VkPipelineVertexInputDivisorStateCreateInfoEXT divisorState{
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_DIVISOR_STATE_CREATE_INFO_EXT
};
VkPipelineVertexInputStateCreateInfo state{
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
};
@@ -48,6 +75,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
~VertexInputStateFactory() = default;
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
// The VAO aux-memo payload GetOrCreateVertexInputState(vao) stamps: aux0 is the
// entry's layoutHash, aux1 packs (unsupportedAttribMask << 32) | attributeLocationMask.
// Readers that find the aux memo valid can use these without resolving the entry.
static Uint64 PackVertexInputAuxMasks(Uint32 unsupportedAttribMask, Uint32 attributeLocationMask) {
return (static_cast<Uint64>(unsupportedAttribMask) << 32) | attributeLocationMask;
}
HashType ComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
// Memoized ComputeHash: reuses the VAO's cached hash while its config version
// is unchanged. Use this on per-draw paths.
@@ -55,6 +89,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const BackendVertexInputState& GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
// Frame boundary hook: ages the cache and evicts entries not hit for many
// frames. The key mixes each bound buffer's never-reused lifetime id, so
// buffer/VAO churn keeps minting fresh keys - and does so by construction,
// not by luck: a recreated buffer can no longer land back on its dead
// predecessor's key. Without eviction the map grows for the whole session.
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
// and the draw path's entry reference never spans a frame boundary, so
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
// compare except on sweep boundaries.
void OnFrameBoundary();
static SizeT GetComponentSize(DataType type);
// Tightly-packed byte size of one vertex element for this attribute: componentSize * size for
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
@@ -62,14 +106,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra);
private:
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false);
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false,
Bool isLong = false);
static Bool IsScaledIntegerVertexFormat(VkFormat format);
static VkFormat ToFloat32VertexFormat(Int componentCount);
Bool SupportsVertexBufferFormat(VkFormat format) const;
const VulkanRendererConfig& m_config;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
UnorderedMap<HashType, BackendVertexInputState> m_cache;
// Values are heap-allocated: UnorderedMap is open-addressing, so INSERT
// invalidates references to stored values - and so does ERASE, which shifts
// the rest of the probe cluster into the hole and therefore moves entries
// other than the erased one. The draw path (and the VAOs' state-pointer
// memos) hold entry pointers across both; only the unique_ptr cell moves,
// never the pointee.
UnorderedMap<HashType, UniquePtr<BackendVertexInputState>> m_cache;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameBoundaryCounter = 0;
// Bumped whenever any cache entry is erased. VAOs memo a raw pointer to
// their heap-allocated entry (stable across map insert/rehash by
// construction); a memo is honored only while its recorded epoch
// matches, so an evicted entry can never be dereferenced through a
// stale memo.
//
// 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
@@ -7,6 +7,8 @@
// End of Source File Header
#include "VkBufferManager.h"
#include "../DirectVulkan.h"
#include "VulkanRenderer.h"
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
@@ -14,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.
@@ -21,7 +32,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
// "Every usage" has to mean every usage: a buffer texture reached through an IMAGE
// unit takes a VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER descriptor, and the write is
// invalid unless the buffer was created with this bit. Nothing asked for it until
// imageBuffer support existed, so the omission was invisible.
VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
// Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled
// (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled).
constexpr VkBufferUsageFlags kTransformFeedbackUsage =
VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT;
// The app writes into the persistent map with no explicit flush, so its memory must
// be host-coherent (Adreno host-visible memory is; requiring it keeps us portable).
constexpr VkMemoryPropertyFlags kPersistentBackedRequiredFlags =
@@ -53,6 +72,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
// The CPU is about to read a buffer a shader wrote. Its bytes live in coherent
// host-visible GPU storage (EnsureGpuResidentStorage adopts it when the buffer is
// bound as a shader storage buffer), so nothing needs copying - but coherence only
// says the writes are visible once they have happened, so the work has to retire
// first.
void Ops_ReadbackFromGpu(BufferObject& bufferObject) {
(void)bufferObject;
if (pVulkanRenderer) {
pVulkanRenderer->FinishPendingGpuWork();
}
}
void* Ops_AcquirePersistentMap(BufferObject& bufferObject) {
if (g_activeBufferManager) {
return g_activeBufferManager->AcquirePersistentMap(bufferObject);
@@ -76,6 +107,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.FlushMappedRange = Ops_FlushMappedRange,
.OnDestroy = Ops_OnDestroy,
.AcquirePersistentMap = Ops_AcquirePersistentMap,
.ReadbackFromGpu = Ops_ReadbackFromGpu,
};
} // namespace
@@ -107,6 +139,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
m_transientUploadArena.Shutdown();
m_unboundStorageBuffer.Destroy();
m_unboundTexelBuffer.Destroy();
DestroyAllDeferredReleases();
ReleaseAllLiveResources();
m_copyProvider = nullptr;
@@ -141,6 +175,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_transientUploadArena.BeginFrame(frameIndex);
}
void VkBufferManager::CollectAllDeferredReleases() {
// Per-resource releases only. Every one of them was deferred behind a BumpSliceEpoch,
// so no memo can still name the handle, and the caller has proved the GPU is idle.
//
// The transient arena's releases are deliberately NOT collected here. A buffer lands
// there when the arena outgrows it mid-frame (BufferArena::EnsureCapacity), and at
// that moment every slice already handed out from this frame's arena still names it -
// VkBufferResource::transientSlice above all, which AcquireStreamedSlice keeps
// serving for the whole frame serial on the strength of transientFrameSerial alone.
// Nothing bumps the slice epoch for those other resources, so freeing the buffer
// here left the streamed memo handing a destroyed VkBuffer to vkCmdBindIndexBuffer
// (llvmpipe then faulted inside the draw; the Create/Flywheel indirect retrace died
// exactly this way). Mid-frame drains do not advance m_frameSerial, so they must not
// free arena storage either: the arena's own ResetFrame/BeginFrame is the point where
// the slot's slices stop being reachable, and that is where these releases land.
for (Uint32 frameIndex = 0; frameIndex < m_deferredBufferReleases.size(); ++frameIndex) {
CollectDeferredReleases(frameIndex);
}
}
void VkBufferManager::NotifyDeviceIdle() {
// Everything submitted so far has completed. Work recorded for the
// current frame has not been submitted yet, so the current serial
@@ -214,8 +268,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkBufferManager::TrackLiveResource(const SharedPtr<VkBufferResource>& resource) {
if (m_liveResources.size() >= kLiveResourcePruneThreshold) {
// Sweep on a doubling watermark rather than on every insert past the threshold. The old
// form walked the whole vector for each new buffer once the list passed 256, and when the
// buffers are all live the walk removes nothing and the list grows by one - so creating N
// live buffers cost ~N^2/2 expired() checks. Reclamation semantics are unchanged: the sweep
// still removes exactly the expired entries, just less often and with the same bound on how
// much dead weight can accumulate (at most as many entries as were live at the last sweep).
if (m_liveResources.size() >= std::max<SizeT>(kLiveResourcePruneThreshold, 2 * m_liveResourcesLastPruned)) {
std::erase_if(m_liveResources, [](const WeakPtr<VkBufferResource>& weak) { return weak.expired(); });
m_liveResourcesLastPruned = m_liveResources.size();
}
m_liveResources.push_back(resource);
}
@@ -223,6 +284,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void VkBufferManager::ReleaseAllLiveResources() {
for (auto& weak : m_liveResources) {
if (auto resource = weak.lock()) {
BumpSliceEpoch(*resource);
resource->buffer.Destroy();
resource->storageSize = 0;
resource->usageFlags = 0;
@@ -237,6 +299,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool VkBufferManager::CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size,
VkBufferUsageFlags usage, VkMemoryPropertyFlags requiredFlags) {
// The only place a resident VkBuffer handle is minted, so every resident slice
// change funnels through here (callers release the old handle first).
BumpSliceEpoch(resource);
// Staged range copies write resident storage with vkCmdCopyBuffer.
usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT;
const Bool created = resource.buffer.Create({
@@ -248,7 +313,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.requiredFlags = requiredFlags,
});
if (!created || resource.buffer.Map() == nullptr) {
MGLOG_E("VkBufferManager::CreateResidentStorage failed (size=%llu)",
MGLOG_E_ONCE("VkBufferManager::CreateResidentStorage failed (size=%llu)",
static_cast<unsigned long long>(size));
resource.buffer.Destroy();
resource.storageSize = 0;
@@ -270,7 +335,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::SwapStorageAndUploadAll: upload failed");
MGLOG_E_ONCE("VkBufferManager::SwapStorageAndUploadAll: upload failed");
resource.pendingFullUpload = true;
return false;
}
@@ -323,8 +388,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!resource) {
return; // lazy: AcquireResidentSlice performs a full upload on creation
}
// A respecify can change the size, the usage hint (so the resident/streamed
// route), and the contents at once; retire every memo before deciding what to
// do about the storage.
BumpSliceEpoch(*resource);
// Any cached streaming slice refers to the previous contents.
resource->transientFrameSerial = 0;
// Redefining the store hands any adopted mapping back to the CPU shadow
// (BufferObject::RedefineStorage), so a buffer that reaches here persistent-mapped
// is an ordinary resident one again: it needs the busy-tracking and conditional
// orphan below, and the next AcquirePersistentMap has to mint storage for the new
// store rather than hand back a mapping of the old one.
resource->persistentMapped = false;
if (!resource->buffer.IsValid()) {
return; // streaming-only resource: shadow + serial are enough
}
@@ -345,7 +420,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::OnRespecify: in-place upload failed");
MGLOG_E_ONCE("VkBufferManager::OnRespecify: in-place upload failed");
resource->pendingFullUpload = true;
}
}
@@ -355,6 +430,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!resource) {
return;
}
// Drops the streaming memo below and may end in a storage swap or a deferred
// full re-upload, so no memoised slice survives this.
BumpSliceEpoch(*resource);
resource->transientFrameSerial = 0;
if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
return;
@@ -367,7 +445,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!IsResourceBusy(*resource)) {
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
MGLOG_E("VkBufferManager::OnSubData: host upload failed");
MGLOG_E_ONCE("VkBufferManager::OnSubData: host upload failed");
resource->pendingFullUpload = true;
}
return;
@@ -387,6 +465,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!resource) {
return;
}
BumpSliceEpoch(*resource);
resource->transientFrameSerial = 0;
if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
return;
@@ -403,7 +482,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) {
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
MGLOG_E("VkBufferManager::OnFlushMappedRange: host upload failed");
MGLOG_E_ONCE("VkBufferManager::OnFlushMappedRange: host upload failed");
resource->pendingFullUpload = true;
}
return;
@@ -446,6 +525,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
TrackLiveResource(resource);
}
// Bumped for the request, not just for the storage it may create. This is the
// one call the frontend makes when a buffer becomes persistently mapped for
// writing (BufferObject::AcquireMemoryRange), and a map the backend declines
// keeps mutating its shadow with no further API call - so it is what lets
// GetSliceEpochCounter stand for "no buffer needs a persistent-map range push".
BumpSliceEpoch(*resource);
// Idempotent: an already-backed buffer returns the same mapped base.
if (resource->persistentMapped && resource->buffer.IsValid() && resource->storageSize == size) {
return resource->buffer.GetMappedData();
@@ -456,7 +542,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// it from the current shadow - MappedData() is still the shadow here because the
// frontend adopts (and drops) the shadow only after this returns.
DeferRelease(std::move(resource->buffer));
if (!CreateResidentStorage(*resource, size, kPersistentBackedUsage, kPersistentBackedRequiredFlags)) {
const VkBufferUsageFlags persistentUsage =
kPersistentBackedUsage |
(m_initInfo.transformFeedbackUsageEnabled ? kTransformFeedbackUsage : 0);
if (!CreateResidentStorage(*resource, size, persistentUsage, kPersistentBackedRequiredFlags)) {
resource->persistentMapped = false;
resource->storageSize = 0;
resource->usageFlags = 0;
@@ -485,7 +574,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
if (size == 0) {
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
return false;
}
@@ -507,7 +596,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
if (!resource->buffer.Upload(bufferObject->MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
resource->buffer.Destroy();
resource->storageSize = 0;
resource->usageFlags = 0;
@@ -530,9 +619,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto resource = GetOrCreateResource(bufferObject);
bufferObject->SyncPersistentMappedRange();
// A persistently mapped resource's storage IS the application's copy of the bytes -
// the frontend adopted it in place of the shadow and hands out pointers into it, and
// a shader can have written bytes the shadow never saw (a transform feedback
// capture). Streaming a second copy would feed this draw the stale shadow, and the
// downgrade below would release the storage the application still points at,
// breaking the "never recreated" promise AcquirePersistentMap makes.
if (resource->persistentMapped) {
return AcquireResidentSlice(kind, bufferObject, outSlice);
}
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
if (size == 0) {
MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
MGLOG_E_ONCE("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
return false;
}
@@ -543,6 +642,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
// Idle-content promotion: see the field comments in VkBufferResource. The
// streak counts frame BOUNDARIES survived unchanged (the same-frame memo
// above swallows repeat draws), so a promotion needs the content stable
// for kStreamedPromotionStreak whole frames - one no-op frame does not
// trigger the resident round-trip, whose creation upload is itself a
// staged copy worth avoiding for content that is about to change again.
constexpr Uint32 kStreamedPromotionStreak = 2;
if (resource->promotedResident) {
if (resource->promotedChangeSerial == changeSerial &&
static_cast<VkDeviceSize>(bufferObject->GetSize()) == size) {
return AcquireResidentSlice(kind, bufferObject, outSlice);
}
resource->promotedResident = false;
resource->unchangedStreak = 0;
} else if (resource->transientChangeSerial == changeSerial && resource->transientSize == size &&
resource->transientFrameSerial != 0) {
if (++resource->unchangedStreak >= kStreamedPromotionStreak) {
// Promotion moves the buffer off the arena and onto resident storage.
resource->promotedResident = true;
resource->promotedChangeSerial = changeSerial;
BumpSliceEpoch(*resource);
if (AcquireResidentSlice(kind, bufferObject, outSlice)) {
return true;
}
resource->promotedResident = false; // resident creation failed: stream as before
}
} else {
resource->unchangedStreak = 0;
}
// A fresh arena allocation: a different slice than the last call handed back,
// and (below) the point where a promoted buffer's resident storage is released.
// The stable-promotion exit above returns before this, so a buffer the app has
// stopped touching keeps one slice for as long as it keeps its resident storage.
BumpSliceEpoch(*resource);
if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject->MappedData(), size, 16,
outSlice)) {
return false;
@@ -583,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:
@@ -595,7 +793,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case BufferKind::Uniform:
return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
case BufferKind::TextureBuffer:
return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT;
// Both texel roles, for the same reason vertex/index carry both bits: one GL buffer
// texture can be read as a samplerBuffer and written as an imageBuffer, and which of
// the two it is only becomes known when a shader that uses it is bound - long after
// the resident buffer was created. A VkBufferView for a storage-texel descriptor is
// invalid unless the buffer was created with the storage bit, so a buffer that
// acquired only the uniform bit could never be given one.
return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT;
case BufferKind::ShaderStorage:
return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
case BufferKind::Indirect:
@@ -31,6 +31,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VmaMemoryUsage transientMemoryUsage = VMA_MEMORY_USAGE_AUTO;
VmaAllocationCreateFlags transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
Bool transientPersistentMapping = false;
// VK_EXT_transform_feedback is enabled: persistent-map storage additionally
// carries the transform feedback usage so capture targets can bind directly.
Bool transformFeedbackUsageEnabled = false;
};
// The DirectVulkan storage behind one frontend buffer (pipe_resource analogue).
@@ -54,11 +57,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// never orphaned or recreated. Draw-time acquire binds it directly, no re-upload.
Bool persistentMapped = false;
// Bumped from a manager-wide counter every time anything that decides which
// BufferSlice an Acquire*Slice call hands back changes: storage created or
// released, a full re-upload becoming due, a promotion/demotion between
// resident and streamed storage, or a new per-frame arena slice. Callers that
// memoise a resolved slice compare this to prove the memo still describes the
// buffer. The counter is manager-wide (never per-resource) so a freshly
// created resource - including one that replaces a destroyed resource at the
// same address - can never reproduce a value some memo already holds. 0 means
// "no slice has ever been handed out", which no memo can match.
Uint64 sliceEpoch = 0;
// Cached transient (streaming) slice for the current frame.
BufferSlice transientSlice{};
Uint64 transientFrameSerial = 0;
Uint64 transientChangeSerial = 0;
VkDeviceSize transientSize = 0;
// Streaming re-copies the whole store into the per-frame arena on every
// frame, which is right for genuinely per-frame data but pure waste for a
// Dynamic-hinted buffer the app stopped touching. After the content
// survives kStreamedPromotionStreak frame boundaries unchanged it is
// promoted to resident storage (one final upload, then zero per-frame
// cost); the first content change demotes it back to streaming, and the
// streaming path's existing downgrade releases the resident store.
Uint32 unchangedStreak = 0;
Bool promotedResident = false;
Uint64 promotedChangeSerial = 0;
};
// Supplies a command buffer that is recording and outside any render pass,
@@ -77,6 +102,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Recreate all per-frame transient arenas
Bool RecreateTransientArenas(Uint32 frameCount);
void BeginFrame(Uint32 frameIndex);
// Drains every frame slot's deferred buffer/resource releases. Only valid when
// the caller has proven every queue submission complete; used by the present-less
// frame-boundary drain. Deliberately does NOT touch the transient arena's parked
// superseded blocks: those are still named by this frame's slices (see the
// definition), and only a frame rewind retires them.
void CollectAllDeferredReleases();
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
void NotifyDeviceIdle();
// A frame slot's submission fence has been waited: every serial up to
@@ -89,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,
@@ -113,6 +165,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource);
Uint64 GetFrameSerial() const { return m_frameSerial; }
// Highest value handed to any VkBufferResource::sliceEpoch. Unchanged since a
// memo was taken means no buffer this manager owns changed which slice it hands
// back, and none was persistently mapped, in between - so a memo of resolved
// slices needs no per-buffer re-check. See AcquirePersistentMap for the mapping half.
Uint64 GetSliceEpochCounter() const { return m_sliceEpochCounter; }
// Highest frame serial whose GPU work is known complete; serials at or
// below it may be considered signaled. Drives IsResourceBusy and the
// backend GL fence objects.
@@ -139,15 +196,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DestroyAllDeferredReleases();
void TrackLiveResource(const SharedPtr<VkBufferResource>& resource);
void ReleaseAllLiveResources();
// See VkBufferResource::sliceEpoch.
void BumpSliceEpoch(VkBufferResource& resource) { resource.sliceEpoch = ++m_sliceEpochCounter; }
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;
Vector<WeakPtr<VkBufferResource>> m_liveResources;
// Size m_liveResources had just after the last sweep; the next sweep waits for it to double.
SizeT m_liveResourcesLastPruned = 0;
Uint32 m_currentFrameIndex = 0;
Uint64 m_frameSerial = 1;
Uint64 m_completedSerialFloor = 0;
// Never reset (not even by Shutdown): a value handed to a resource must stay
// unique for the process, or a memo taken before a re-initialize could match
// a different resource's state after it.
Uint64 m_sliceEpochCounter = 0;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -76,7 +76,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkResult result =
vmaCreateBuffer(m_allocator, &bufferInfo, &allocationInfo, &m_buffer, &m_allocation, nullptr);
if (result != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
MGLOG_E_ONCE("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
m_allocator = nullptr;
m_buffer = VK_NULL_HANDLE;
m_allocation = nullptr;
@@ -108,7 +108,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkResult mapResult = vmaMapMemory(m_allocator, m_allocation, &m_mappedData);
if (mapResult != VK_SUCCESS || m_mappedData == nullptr) {
MGLOG_E("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
MGLOG_E_ONCE("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
m_mappedData = nullptr;
return nullptr;
}
@@ -138,14 +138,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool wasMapped = IsMapped();
void* mapped = wasMapped ? m_mappedData : Map();
if (mapped == nullptr) {
MGLOG_E("VkBufferObject::Upload failed: unable to map buffer");
MGLOG_E_ONCE("VkBufferObject::Upload failed: unable to map buffer");
return false;
}
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
if (flushResult != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
MGLOG_E_ONCE("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
if (!wasMapped) {
Unmap();
}
@@ -170,22 +170,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
if (result != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
MGLOG_E_ONCE("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
return false;
}
return true;
}
BufferSlice VkBufferObject::GetSlice(VkDeviceSize offset, VkDeviceSize size) const {
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
MOBILEGL_ASSERT(offset + resolvedSize <= m_size, "VkBufferObject::GetSlice range out of bounds");
BufferSlice slice{};
slice.buffer = m_buffer;
slice.offset = offset;
slice.size = resolvedSize;
slice.mapped = (m_mappedData != nullptr) ? static_cast<Uint8*>(m_mappedData) + offset : nullptr;
return slice;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -48,7 +48,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkBuffer GetHandle() const { return m_buffer; }
VkDeviceSize GetSize() const { return m_size; }
BufferSlice GetSlice(VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE) const;
// Inline: runs on the per-draw acquire path (a resident buffer bind is a
// GetSlice per binding), where an out-of-line call was measurable.
BufferSlice GetSlice(VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE) const {
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
MOBILEGL_ASSERT(offset + resolvedSize <= m_size, "VkBufferObject::GetSlice range out of bounds");
BufferSlice slice{};
slice.buffer = m_buffer;
slice.offset = offset;
slice.size = resolvedSize;
slice.mapped = (m_mappedData != nullptr) ? static_cast<Uint8*>(m_mappedData) + offset : nullptr;
return slice;
}
void* GetMappedData() const { return m_mappedData; }
Bool IsMapped() const { return m_mappedData != nullptr; }
Bool IsValid() const { return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr; }
@@ -8,15 +8,79 @@
#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"
#include <algorithm>
#include <cmath>
namespace MobileGL::MG_Backend::DirectVulkan {
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
return target >= TextureUploadTarget::CubeMapPositiveX &&
target <= TextureUploadTarget::CubeMapNegativeZ;
}
VkClearColorValue MakeVkClearColorValue(const ClearAttachmentPayload& payload, Bool formatLacksAlpha) {
VkClearColorValue clearValue{};
switch (payload.colorEncoding) {
case ClearColorEncoding::Int:
clearValue.int32[0] = payload.colorInt.x();
clearValue.int32[1] = payload.colorInt.y();
clearValue.int32[2] = payload.colorInt.z();
clearValue.int32[3] = formatLacksAlpha ? 1 : payload.colorInt.w();
break;
case ClearColorEncoding::Uint:
clearValue.uint32[0] = payload.colorUint.x();
clearValue.uint32[1] = payload.colorUint.y();
clearValue.uint32[2] = payload.colorUint.z();
clearValue.uint32[3] = formatLacksAlpha ? 1u : payload.colorUint.w();
break;
case ClearColorEncoding::Float:
clearValue.float32[0] = payload.color.x();
clearValue.float32[1] = payload.color.y();
clearValue.float32[2] = payload.color.z();
clearValue.float32[3] = formatLacksAlpha ? 1.0f : payload.color.w();
break;
}
return clearValue;
}
void PreCompensateSrgbClearColor(ClearAttachmentPayload& payload, VkFormat destinationFormat) {
if (payload.colorEncoding != ClearColorEncoding::Float) return;
// With GL_FRAMEBUFFER_SRGB enabled GL performs the encoding itself, so the driver doing it
// is exactly right and there is nothing to undo.
if (MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
if (ResolveSrgbAttachmentWriteFormat(destinationFormat, false) == destinationFormat) return;
// sRGB -> linear (GL 4.6 core 8.24), applied to the colour channels only: alpha is stored
// linearly in an sRGB format and must pass through untouched.
const auto toLinear = [](Float encoded) {
const Float value = std::clamp(encoded, 0.0f, 1.0f);
return value <= 0.04045f ? value / 12.92f : std::pow((value + 0.055f) / 1.055f, 2.4f);
};
payload.color = FloatVec4(toLinear(payload.color.x()), toLinear(payload.color.y()),
toLinear(payload.color.z()), payload.color.w());
}
void ForceOpaqueClearAlpha(ClearAttachmentPayload& payload) {
switch (payload.colorEncoding) {
case ClearColorEncoding::Int:
payload.colorInt = IntVec4(payload.colorInt.x(), payload.colorInt.y(), payload.colorInt.z(), 1);
break;
case ClearColorEncoding::Uint:
payload.colorUint = UintVec4(payload.colorUint.x(), payload.colorUint.y(), payload.colorUint.z(), 1u);
break;
case ClearColorEncoding::Float:
payload.color = FloatVec4(payload.color.x(), payload.color.y(), payload.color.z(), 1.0f);
break;
}
}
static Bool PendingClearMatchesTextureIdentity(const PendingClearKey& key, const TextureIdentity& identity) {
return key.texture == identity.texture && key.textureLifetimeId == identity.lifetimeId;
}
@@ -40,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) {
@@ -62,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,
@@ -93,9 +179,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const std::lock_guard<std::mutex> lock(m_mutex);
m_pendingClears.clear();
m_aliveObjects.clear();
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
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,
@@ -105,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;
@@ -127,6 +231,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_pendingClears.erase(key);
}
m_aliveObjects.erase(identity);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
Bool VkClearManager::LockTextureIdentityLocked(const TextureIdentity& identity,
@@ -216,11 +321,14 @@ 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);
}
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
@@ -234,10 +342,17 @@ 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);
}
Bool VkClearManager::HasPendingClear(MG_State::GLState::ITextureObject* texture) {
@@ -245,6 +360,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
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) {
@@ -260,6 +380,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (key.texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const std::lock_guard<std::mutex> lock(m_mutex);
if (m_pendingClears.find(key) == m_pendingClears.end()) {
@@ -287,6 +410,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (key.texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const std::lock_guard<std::mutex> lock(m_mutex);
if (!LockTextureLocked(key, outTexture)) {
@@ -325,7 +451,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
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;
@@ -345,6 +475,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return; // per-draw hot path: nothing pending anywhere
}
const TextureIdentity identity = MakeTextureIdentity(texture);
MGLOG_D("%s: Pop all pending clears for texture %d", __func__, texture->GetExternalIndex());
const std::lock_guard<std::mutex> lock(m_mutex);
@@ -361,6 +494,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto it = m_pendingClears.find(key);
if (it != m_pendingClears.end()) {
m_pendingClears.erase(it);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
}
@@ -14,6 +14,7 @@
#include "MG_Util/Math/VectorTypes.h"
#include <Includes.h>
#include <atomic>
#include <unordered_map>
namespace MobileGL::MG_Backend::DirectVulkan {
@@ -23,13 +24,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 stencil{};
};
// A colour clear reaches us from one of glClear/ClearBufferfv, ClearBufferiv or
// ClearBufferuiv, and Vulkan reads VkClearColorValue's union according to the destination
// image's format rather than converting between the members - a float written where an
// integer format is expected is reinterpreted bit for bit, not rounded. Remember which entry
// point supplied the value so the member written when the clear is materialized matches.
enum class ClearColorEncoding : Uint8 { Float, Int, Uint };
struct ClearAttachmentPayload {
GLbitfield mask = 0;
FloatVec4 color = FloatVec4(0.0f, 0.0f, 0.0f, 0.0f);
ClearColorEncoding colorEncoding = ClearColorEncoding::Float;
IntVec4 colorInt = IntVec4(0, 0, 0, 0);
UintVec4 colorUint = UintVec4(0u, 0u, 0u, 0u);
Float depth = 1.0f;
Uint32 stencil = 0;
};
// Builds the clear value for `payload` in the union member its encoding calls for.
// `formatLacksAlpha` applies GL's rule that a format without an alpha channel reads as one,
// expressed in whichever type matches (GL 4.6 core 15.2.3).
VkClearColorValue MakeVkClearColorValue(const ClearAttachmentPayload& payload, Bool formatLacksAlpha);
// Applies that same rule in place, for the paths that have to bake it into the payload before
// the destination is known.
void ForceOpaqueClearAlpha(ClearAttachmentPayload& payload);
// vkCmdClearColorImage names the image, so the driver applies the destination format's transfer
// function to whatever value it is handed. Every other write path in this backend goes through
// the UNORM twin view while GL_FRAMEBUFFER_SRGB is off (ResolveSrgbAttachmentWriteFormat) and
// therefore stores the raw value GL asked for. Rewrites `payload` to the linear colour whose
// encoding is that raw value, so a direct image clear of an sRGB destination agrees with them.
// A no-op for every other format, for integer clear encodings, and when GL is doing the
// encoding itself.
void PreCompensateSrgbClearColor(ClearAttachmentPayload& payload, VkFormat destinationFormat);
struct PendingClearKey {
MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 textureLifetimeId = 0;
@@ -85,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();
@@ -120,7 +150,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
Uint8 m_gcCounter = 0;
public:
// Lock-free probe for the consecutive-draw fast path: any pending clear
// forces the full SetupDraw path (which materializes/consumes it).
Bool HasAnyPendingClears() const { return m_pendingCount.load(std::memory_order_relaxed) != 0; }
private:
mutable std::mutex m_mutex;
// Lock-free mirror of m_pendingClears.size(), maintained under m_mutex
// by every mutation. The per-draw probes (HasPendingClear/GetPending*)
// read it before taking the lock: during draw batches the pending set
// is almost always empty, so this turns several locked map probes per
// draw into one relaxed load.
std::atomic<Uint32> m_pendingCount{0};
std::unordered_map<PendingClearKey, ClearAttachmentPayload, PendingClearKeyHash> m_pendingClears;
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
};
File diff suppressed because it is too large Load Diff
@@ -16,6 +16,7 @@
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include <Includes.h>
#include <unordered_map>
#include <vk_mem_alloc.h>
namespace MobileGL::MG_Backend::DirectVulkan {
@@ -42,6 +43,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
struct TrackedAttachmentLayoutInfo {
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
WeakPtr<MG_State::GLState::ITextureObject> texture;
// Identity-compare shortcut for the per-draw "does the active pass use
// this sampled texture" probe: comparing this against a LIVE texture's
// address needs no weak_ptr::lock (two refcount atomics per probe).
// May dangle once the texture dies - compare only, never dereference.
MG_State::GLState::ITextureObject* textureRaw = nullptr;
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
Uint32 textureMipLevel = 0;
Uint32 swapchainImageIndex = 0;
@@ -95,6 +101,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
std::swap(layers, that.layers);
std::swap(lastUsedFrame, that.lastUsedFrame);
}
// Move ASSIGNMENT, not just construction. The move constructor above and the
// destructor below each independently suppress the implicit one, which left the
// type move-constructible but not move-assignable - and therefore not swappable,
// which std::swap(pair&, pair&) requires. That was invisible while UnorderedMap
// only ever move-CONSTRUCTED an element into a fresh slot. ska::flat_hash_map
// probes robin-hood: inserting swaps the entry being placed against the one
// already sitting in the slot whenever it has travelled further from its desired
// position, so the mapped type has to be swappable or the table fails to
// instantiate at all.
//
// SWAP SEMANTICS, exactly like the move constructor: this does not release the
// destination's handles, it parks them in `that`, which destroys them when it
// dies. That is correct for the only caller - std::swap, whose temporary expires
// immediately - and it is what keeps the three-move sequence from destroying a
// live render pass. It is NOT correct for a hand-written `a = std::move(b)` where
// `a` held live handles and `b` outlives the statement: those handles would then
// survive until `b` dies. There is no such caller; add a destroy-then-steal
// assignment before writing one.
RenderPassEntry& operator=(RenderPassEntry&& that) noexcept {
if (this != &that) {
std::swap(hash, that.hash);
std::swap(renderPass, that.renderPass);
std::swap(framebuffer, that.framebuffer);
std::swap(compatibilityHash, that.compatibilityHash);
std::swap(pendingClearAttachments, that.pendingClearAttachments);
std::swap(trackedAttachmentLayouts, that.trackedAttachmentLayouts);
std::swap(attachmentCount, that.attachmentCount);
std::swap(colorAttachmentCount, that.colorAttachmentCount);
std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment);
std::swap(sampleCount, that.sampleCount);
std::swap(extent, that.extent);
std::swap(layers, that.layers);
std::swap(lastUsedFrame, that.lastUsedFrame);
}
return *this;
}
RenderPassEntry(
Uint64 hash,
VkRenderPass renderpass,
@@ -157,19 +199,68 @@ namespace MobileGL::MG_Backend::DirectVulkan {
class VkRenderPassManager {
public:
using HashType = Uint64;
// Notified once per OnPresent sweep with every aged-out entry's VkRenderPass
// value: pipelines are hashed on the raw handle, and once destroyed the value
// may be recycled for an incompatible pass, so dependent caches must purge
// everything keyed on them before any new pass can be created (the sweep and
// the notification run back-to-back with no creation in between; observers
// compare the values, never dereference them). Batched so a mass-idle cohort
// (shader-pack switch, dimension exit) costs the observer one pipeline-cache
// scan, not one per dying pass. The wholesale paths
// (Shutdown/RecreateSwapchain) do not notify - their callers already drop
// every pipeline outright.
class IEvictionObserver {
public:
virtual ~IEvictionObserver() = default;
virtual void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) = 0;
};
VkRenderPassManager(VkDevice device,
VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject);
~VkRenderPassManager();
// Observer may be null (no notifications). Not owned.
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
Bool Initialize();
void Shutdown();
HashType ComputeHash(
const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
Bool includePendingClear = true);
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex);
Bool includePendingClear = true,
Bool includeDefaultFboDepthStencil = true);
// drawUsesDepthStencil: whether the operation about to run inside the pass
// reads or writes the depth/stencil buffer (depth test or stencil test
// enabled, or a depth/stencil clear). Only consulted for the DEFAULT
// framebuffer: EGL undefines its ancillary buffers at every swap, so a
// default-FBO pass whose draws provably never touch depth/stencil is
// created WITHOUT the depth attachment - on a tiler that skips the whole
// depth tile load AND store. The flavor only escalates: once a pass with
// depth is active, later depth-less draws keep using it, and a depth-using
// 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).
//
// 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,
@@ -192,12 +283,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
UnorderedMap<Uint64, RenderPassEntry> m_renderPasses;
// Monotonic frame counter (bumped in OnPresent) for render-pass cache aging.
Uint64 m_frameCounter = 0;
IEvictionObserver* m_evictionObserver = nullptr;
// Bumped whenever a renderbuffer VkImage is (re)created; together with the texture
// manager's image epoch this invalidates the render-pass fast path on any attachment
// image recreation.
Uint64 m_renderbufferImageEpoch = 1;
public:
// Bumped whenever a renderbuffer backing is (re)created; consecutive-draw
// snapshots include it so an attachment respecify forces a re-resolve.
Uint64 GetRenderbufferImageEpoch() const { return m_renderbufferImageEpoch; }
private:
// Per-draw fast-path memo for GetOrCreateRenderPass (dirty-flag state tracking): when the
// framebuffer state is provably unchanged since the last resolution, the active render pass
// is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch /
@@ -205,17 +304,36 @@ 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;
Uint64 m_rpFastRbEpoch = 0;
Uint64 m_rpFastRenderPassHash = 0;
// Whether the memoized entry carries a depth/stencil attachment; a
// default-FBO resolution whose effective depth request differs must
// miss the memo (the depth-less/depth-full flavors hash differently).
Bool m_rpFastHadDepthStencil = false;
public:
struct RenderbufferResource {
// deadSinceFrame sentinel: the owning weak reference has not been observed
// expired. Dead resources age past every in-flight frame before Destroy
// (see CollectRenderbufferGarbage); the GPU may still reference the image
// for frames-in-flight frames after the GL object dies.
static constexpr Uint64 kNeverObservedDead = UINT64_MAX;
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr;
VkImageView view = VK_NULL_HANDLE;
// UNORM reinterpretation of an sRGB image, used as the attachment view while
// GL_FRAMEBUFFER_SRGB is disabled (raw writes). Null for non-sRGB formats.
VkImageView unormTwinView = VK_NULL_HANDLE;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
@@ -223,25 +341,75 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
Int samples = 0;
// m_frameCounter value at which the weak reference was first seen expired.
Uint64 deadSinceFrame = kNeverObservedDead;
void Destroy(VkDevice device, VmaAllocator allocator);
};
// Public so the renderer's blit/copy/readback bindings can source renderbuffer
// attachments the same way texture attachments go through the texture manager.
RenderbufferResource* GetOrCreateRenderbufferResource(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
ClearAttachmentPayload& outPayload) const;
private:
struct PendingRenderbufferClear {
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
ClearAttachmentPayload payload{};
};
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
// A superseded renderbuffer backing (glRenderbufferStorage respecify) parked
// until enough frame boundaries have passed that no in-flight command buffer
// can still reference it; destroyed in OnPresent (see RetireAgeFrames).
struct DeferredRenderbufferRelease {
VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr;
VkImageView view = VK_NULL_HANDLE;
VkImageView unormTwinView = VK_NULL_HANDLE;
Uint64 deferredAtFrame = 0;
};
// Node-based std::unordered_map, deliberately NOT the open-addressing UnorderedMap:
// callers cache a RenderbufferResource* - or a bare &resource->layout - and then make further
// calls that touch this map. BlitFramebuffer is the one that bit: it resolves the source and
// destination colour bindings (ResolveColorBlitBinding caches &rbResource->layout), then
// materializes the source's pending clear, which looks that same resource up again. Growing
// an open-addressed table relocates every element, so the cached pointer went on to name
// freed storage still holding the pre-clear VK_IMAGE_LAYOUT_UNDEFINED; BlitFramebuffer bailed
// out at "source image layout is undefined", silently dropping the blit -
// renderbuffers_storage_multisample read back zero instead of the clear colour on exactly the
// iterations that grew the table.
//
// Reordering the materialize ahead of the resolves - the fix ReadPixels got - does not cover
// this: the destination resolve still runs after the source pointer is taken. The depth blit,
// GetOrCreateRenderPass's depthRenderbufferResource and ReadDepthStencilPixels cache the same
// kind of pointer, so the invariant belongs in the container rather than in a per-call-site
// ordering rule. m_textureResources is node-based for the same reason.
//
// The case for keeping this node-based got STRONGER with ska::flat_hash_map, so do not read
// the paragraph above as merely historical: ska erases by shifting the rest of the probe
// cluster backwards into the hole, so erasing one renderbuffer relocates OTHER renderbuffers'
// entries - a cached pointer can now be invalidated by a key it has nothing to do with, which
// no call-site ordering rule can defend against. (What did change: ska's operator[] returns on
// a hit before it runs its grow check, so a plain lookup of a PRESENT key no longer relocates.
// That narrows the insert hazard; it does not touch the erase one.)
std::unordered_map<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
// Supported sample counts per attachment format, so per-draw resource lookups
// do not repeat vkGetPhysicalDeviceImageFormatProperties.
UnorderedMap<VkFormat, VkSampleCountFlags> m_attachmentSampleCountsByFormat;
RenderbufferResource* GetOrCreateRenderbufferResource(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
ClearAttachmentPayload& outPayload) const;
Bool HasPendingRenderbufferClear(
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
void CollectRenderbufferGarbage();
// Frame-boundary margin after which a resource last referenced by a retired
// GL object (or superseded backing) is provably past every in-flight frame.
Uint64 RetireAgeFrames() const;
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
void CollectDeferredRenderbufferReleases(Bool destroyAll);
static inline XXH64_state_t* m_hashState = XXH64_createState();
static inline ActiveRenderPassInfo s_activeRenderPass{};
@@ -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:
@@ -51,6 +201,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) {
return std::min(sampler.GetMinLod(), effectiveMaxLod);
}
// A single-level view can only ever deliver the base level, but the LOD clamp must not be
// collapsed to exactly 0: both GL and Vulkan pick magFilter over minFilter from the
// *clamped* lambda, so maxLod = 0 would make every fragment magnify and quietly retire the
// min filter. 0.25 is the value VkSamplerCreateInfo's own note prescribes for emulating
// GL's non-mipmapped minification - large enough for lambda to stay positive, small enough
// that a NEAREST mip mode still rounds down to level 0. Clamped rather than assigned, so a
// texture whose GL_TEXTURE_MAX_LOD really is 0 keeps magnifying as GL says it must.
Float ResolveSingleLevelMaxLod(const MG_State::GLState::SamplerObject& sampler, Bool singleLevelView) {
const Float maxLod = ResolveEffectiveMaxLod(sampler);
return singleLevelView ? std::min(maxLod, 0.25f) : maxLod;
}
} // namespace
Bool VkSamplerManager::Initialize(const InitInfo& initInfo) {
@@ -60,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;
@@ -89,15 +254,49 @@ 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() {
++m_frameBoundaryCounter;
// Sweep occasionally; destroy samplers whose last use is far past every
// in-flight frame. Destroy and erase must stay atomic, or Shutdown would
// double-free the handle; an evicted key that recurs simply re-creates
// its sampler on the next miss.
constexpr Uint64 kSweepInterval = 256;
constexpr Uint64 kRetireAgeBoundaries = 1024;
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
return;
}
for (auto it = m_samplers.begin(); it != m_samplers.end();) {
auto& entry = it->second;
if (m_frameBoundaryCounter - entry.lastUsedFrameBoundary > kRetireAgeBoundaries) {
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;
}
}
}
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering) 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));
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
XXHASH_VERIFY(XXH64_update(m_hashState, &singleLevelView, sizeof(singleLevelView)));
const auto minFilter = sampler.GetMinFilter();
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
@@ -111,7 +310,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT)));
const auto wrapR = sampler.GetWrapR();
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR)));
const auto maxLod = ResolveEffectiveMaxLod(sampler);
const auto maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
@@ -124,19 +323,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
const auto compareMode = sampler.GetCompareMode();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
const auto compareFunc = ResolveCompareFunc(sampler, texture);
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);
}
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering) {
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering);
Bool forceNearestFiltering, Uint32 viewLevelCount) {
// A view that exposes a single mip level has no second level to blend with, so GL's
// *_MIPMAP_* minification filters degenerate to plain filtering on the base level -
// sampling is unchanged by pinning the Vulkan sampler to NEAREST mip mode at LOD 0.
// It is not cosmetic: MobileGL backs such a view with a fully allocated mip chain whose
// tail is never written, and a LINEAR mip mode lets the texture unit issue the level+1
// fetch anyway. On Adreno that fetch lands in uninitialized UBWC pages (or past the
// 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;
// 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;
return it->second.handle;
}
@@ -144,8 +360,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter());
samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter());
samplerInfo.mipmapMode = forceNearestFiltering ? VK_SAMPLER_MIPMAP_MODE_NEAREST
: ToVkMipmapMode(sampler.GetMipmapMode());
samplerInfo.mipmapMode = (forceNearestFiltering || singleLevelView)
? VK_SAMPLER_MIPMAP_MODE_NEAREST
: ToVkMipmapMode(sampler.GetMipmapMode());
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
@@ -156,12 +373,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE;
samplerInfo.maxAnisotropy = maxAnisotropy;
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture));
samplerInfo.maxLod = ResolveEffectiveMaxLod(sampler);
samplerInfo.compareOp = ToVkCompareOp(sampler.GetSamplerCompareFunc());
// 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)");
@@ -169,6 +399,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.handle = vkSampler;
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;
}
@@ -228,48 +463,148 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
SamplerCompareFunc VkSamplerManager::ResolveCompareFunc(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture) {
const auto compareFunc = sampler.GetSamplerCompareFunc();
if (sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture &&
IsDepthTextureFormat(texture.GetFormat()) && compareFunc == SamplerCompareFunc::Always) {
return SamplerCompareFunc::LessEqual;
}
return compareFunc;
}
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
}
const auto& borderColor = texture.GetBorderColor();
const Bool isDepthTexture = IsDepthTextureFormat(texture.GetFormat());
// 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 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,33 +28,72 @@ 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);
void Shutdown();
// viewLevelCount is the mip-level count of the image view this sampler will be paired
// with; 0 means "unknown, do not narrow". See GetOrCreateSampler for why it matters.
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering = false);
Bool forceNearestFiltering = false,
Uint32 viewLevelCount = 0);
// Frame boundary hook: ages the sampler cache and destroys samplers not used
// for many frames. The key hashes continuous float state (lodBias, LOD clamps,
// anisotropy), so an app animating those would otherwise mint an unbounded
// stream of never-destroyed VkSamplers and eventually exhaust the device's
// maxSamplerAllocationCount. A sampler idle for over a thousand frame
// boundaries cannot be referenced by any in-flight command buffer (frames in
// flight are single digits), and every descriptor set the GPU consumes is
// written that same frame with live handles (the per-binding resolve memo and
// descriptor-set reuse are both frame-reset), so destruction here needs no
// fence wait. Self-gated: one counter bump and compare except on sweep
// 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;
Uint externalIndex = 0;
Uint16 version = 0;
// 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) 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 SamplerCompareFunc ResolveCompareFunc(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture);
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
@@ -66,7 +105,15 @@ 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;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -10,9 +10,12 @@
#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>
namespace MobileGL::MG_State::GLState {
class ITextureObject;
@@ -21,12 +24,111 @@ 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
// manager keys its per-draw fast path on this so an attachment's image recreation
// invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1).
Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; }
// Bumped whenever any tracked texture resource is erased; cached
// TextureResource pointers are valid only while this is unchanged.
Uint64 GetResourceEraseEpoch() const { return m_resourceEraseEpoch; }
struct TextureIdentity {
MG_State::GLState::ITextureObject* texture = nullptr;
@@ -52,6 +154,20 @@ public:
VkCommandPool commandPool = VK_NULL_HANDLE;
VkQueue graphicsQueue = VK_NULL_HANDLE;
Uint32 frameCount = 0;
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
// formats they will be viewed as, which is what lets a tiler keep them compressed.
Bool imageFormatListSupported = false;
// Union of shader stages sampled-read barriers may name on this device; the renderer
// builds it from the enabled features because geometry/tessellation stage bits are
// invalid in a barrier when their feature is off.
VkPipelineStageFlags sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
// Family of `graphicsQueue`; the manager creates its own command pool
// on it for the recycled upload-batch command buffers, so their parked
// allocations never sit in (and fragment) the renderer's shared pool
// that frame command buffers churn through every frame.
Uint32 graphicsQueueFamilyIndex = 0;
};
struct TextureResource {
@@ -60,12 +176,16 @@ public:
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
// May differ from the image format: sRGB images attach through their UNORM
// twin while GL_FRAMEBUFFER_SRGB is disabled.
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
Bool operator==(const AttachmentViewKey& other) const {
return mipLevel == other.mipLevel &&
baseArrayLayer == other.baseArrayLayer &&
layerCount == other.layerCount &&
viewType == other.viewType;
viewType == other.viewType &&
viewFormat == other.viewFormat;
}
};
@@ -76,6 +196,8 @@ public:
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.viewFormat)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
@@ -96,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;
}
};
@@ -114,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;
}
};
@@ -156,7 +301,25 @@ public:
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
VkImageCreateFlags imageCreateFlags = 0;
// Usage the live image was created with. STORAGE is only requested for textures that
// have actually been bound to a GL image unit, because on Adreno a storage-capable
// image loses UBWC bandwidth compression; a later image binding upgrades the usage
// and recreates the image, so the resolved usage has to be part of the compatibility
// check that decides whether the existing image can be kept.
VkImageUsageFlags usageFlags = 0;
// True once this image was (re)resolved while the texture was already marked as an
// image-unit texture. Distinguishes "not upgraded yet" from "cannot be upgraded"
// (a format whose optimalTilingFeatures lack STORAGE_IMAGE never gains the bit), so
// NeedsStorageImagePreparation cannot ask for a recreate that will never happen.
Bool storageUsageResolved = false;
Uint16 syncedTextureParamsVersion = 0;
// Recording generation (VkTextureManager::GetRecordingGeneration) of the last
// command referencing this image that was recorded into the CURRENT frame
// command buffer. An image untouched by the open recording may have its
// out-of-pass work (deferred clears, sampled-layout transitions) recorded
// into the frame's PRE command buffer - which executes strictly before the
// frame's commands - instead of splitting the active render pass.
Uint64 lastRecordingGeneration = 0;
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
Uint64 syncedContentVersion = 0;
@@ -164,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;
@@ -189,9 +358,13 @@ public:
std::swap(this->viewType, that.viewType);
std::swap(this->sampleCount, that.sampleCount);
std::swap(this->imageCreateFlags, that.imageCreateFlags);
std::swap(this->usageFlags, that.usageFlags);
std::swap(this->storageUsageResolved, that.storageUsageResolved);
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
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() {
@@ -250,9 +423,12 @@ public:
viewType = VK_IMAGE_VIEW_TYPE_2D;
sampleCount = VK_SAMPLE_COUNT_1_BIT;
imageCreateFlags = 0;
usageFlags = 0;
storageUsageResolved = false;
syncedTextureParamsVersion = 0;
syncedContentVersion = 0;
syncedMipLevelCount = 0;
syncedShapeVersion = 0;
}
~TextureResource() {
@@ -263,9 +439,78 @@ 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);
// Submits the accumulated texture-upload batch (one command buffer, one
// vkQueueSubmit, one pooled fence) if any uploads are pending. MUST run
// before any other vkQueueSubmit on the shared graphics queue whose
// commands may consume an image the batch writes - the frame command
// buffer submit (mid-frame flush, readback, Present) and the
// preserve-on-recreate copy are the existing callers. No-op when the
// batch is empty.
void FlushPendingUploads();
// Drains every frame slot's deferred image/view releases. Only valid when
// the caller has proven every queue submission complete; used by the
// 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);
@@ -284,18 +529,76 @@ 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);
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect);
// Recording-generation bookkeeping for the pre-pass command stream. The
// generation advances every time the frame command buffer (re)begins
// recording; a resource whose stamp does not match was not referenced by
// any command in the open recording, so its out-of-pass work may safely
// execute ahead of the whole recording (in the pre command buffer).
void AdvanceRecordingGeneration() { ++m_recordingGeneration; }
void StampResourceRecordingUse(TextureResource& resource) const {
resource.lastRecordingGeneration = m_recordingGeneration;
}
// Map-lookup variant for callers that only hold the GL texture object.
void StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture);
Bool WasTouchedThisRecording(const TextureResource& resource) const {
return resource.lastRecordingGeneration == m_recordingGeneration;
}
// Records that this texture is bound to a GL image unit, so its image must carry
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
// therefore before the render pass is committed: an image that has to be upgraded is
// recreated, which is illegal inside a render pass. Sticky for the texture's lifetime -
// GL lets an image binding come and go, and re-creating the image every time it does
// would cost far more than the compression it wins back.
void MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture);
// True when this texture is marked but its live image predates the mark, i.e. the next sync
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
// submit their pending recording first, so that copy cannot read pre-flush content.
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
// The same ordering question for the other recreate-and-preserve trigger: true when this
// texture's live image carries a shorter mip chain than a full one, so defining the missing
// levels recreates it and copies the old contents forward.
Bool NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const;
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
// texture as a storage image may need work that is illegal inside a render pass (resource
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
// `depthStencilTextureMode` is the texture's GL_DEPTH_STENCIL_TEXTURE_MODE; it only decides
// anything for an image that carries both aspects. Defaulted so the call sites that have no
// texture in hand keep the depth-aspect answer they have always given.
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect,
GLenum depthStencilTextureMode = GL_DEPTH_COMPONENT);
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
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();
@@ -325,6 +628,9 @@ public:
private:
// Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch().
Uint64 m_textureImageEpoch = 1;
// See AdvanceRecordingGeneration. Starts above every resource's default
// stamp of 0 so a fresh resource counts as untouched.
Uint64 m_recordingGeneration = 1;
Bool SyncTexture(MG_State::GLState::ITextureObject &texture,
TextureResource &outResource);
@@ -355,18 +661,31 @@ private:
void DeferViewRelease(VkImageView view);
void CollectDeferredReleases(Uint32 frameIndex);
void DestroyDeferredReleases();
// Frees the fence/command buffer/staging buffer of every in-flight texture
// upload whose fence has signaled (submission order = completion order on
// the single queue, so the scan stops at the first still-pending entry).
// waitAll blocks on every entry - Shutdown's drain.
void ReclaimCompletedUploads(Bool waitAll = false);
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
void EraseTrackedTexture(const TextureIdentity& identity);
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
SizeT PruneDeadTextures();
VkDevice m_device = VK_NULL_HANDLE;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
VmaAllocator m_allocator = nullptr;
VkCommandPool m_commandPool = VK_NULL_HANDLE;
// Dedicated pool for the recycled upload-batch command buffers (see
// InitInfo::graphicsQueueFamilyIndex).
VkCommandPool m_uploadCommandPool = VK_NULL_HANDLE;
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
Bool m_imageFormatListSupported = false;
Uint32 m_currentFrameIndex = 0;
Uint8 m_gcCounter = 0;
// Frame-boundary GC gate: counts BeginFrame calls, not draws, so texture churn
// through non-draw paths (FBO clears, readbacks) still reaches the prune.
Uint32 m_gcFrameCounter = 0;
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
// textures already fully synced in the current draw (small N -> flat scan).
Bool m_drawSyncScopeActive = false;
@@ -379,9 +698,105 @@ private:
TextureResource* resource = nullptr;
};
Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
// Cross-draw sampled-texture memo: the same few textures (atlas, lightmap)
// are resolved on every draw, so cache their resource pointers and skip the
// alive/resource map lookups. Node-based std::unordered_map keeps the
// pointees stable across inserts; erases bump m_resourceEraseEpoch, which
// every memo entry must match. SyncTexture still runs on memo hits, so
// content/param freshness is unaffected. A dead-then-reused texture address
// cannot false-hit: the new object carries a new lifetime id.
struct SyncedTextureMemoEntry {
const MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 lifetimeId = 0;
Uint64 eraseEpoch = 0;
TextureResource* resource = nullptr;
};
static constexpr Uint32 kSyncedTextureMemoSize = 8;
SyncedTextureMemoEntry m_syncedTextureMemo[kSyncedTextureMemoSize];
Uint32 m_syncedTextureMemoNext = 0;
Uint64 m_resourceEraseEpoch = 1;
// Formats whose mutable-image probe failed on this device; their images are created
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
// Formats whose 3D images refused VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT. Per format+usage,
// exactly like the mutable-format verdict above, so it is answered at image creation and
// remembered rather than probed once globally.
std::unordered_set<VkFormat> m_2dArrayCompatibleUnsupported;
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
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;
Vector<Vector<TextureResource>> m_deferredReleases;
Vector<Vector<VkImageView>> m_deferredViewReleases;
// --- Batched upload machinery ---
// Uploads within a frame are recorded into ONE shared command buffer and
// submitted with ONE vkQueueSubmit at FlushPendingUploads (the renderer
// flushes before every frame-command-buffer submit). Staging memory comes
// from a pool of persistently-mapped, reusable blocks instead of a
// vmaCreateBuffer per upload.
struct UploadStagingBlock {
VkBuffer buffer = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr;
Uint8* mapped = nullptr; // persistently mapped for the block's lifetime
VkDeviceSize capacity = 0;
VkDeviceSize cursor = 0; // bump cursor while the block backs the open batch
};
// Opens the batch command buffer lazily (allocates/reuses + begins recording).
VkCommandBuffer EnsureUploadBatchOpen();
// Bump-allocates `size` staging bytes for the open batch, growing onto a
// new/pooled block when the current one cannot fit. Returns the write
// pointer; outBuffer/outBaseOffset locate the space for copy commands.
Uint8* AcquireUploadStagingSpace(VkDeviceSize size, VkBuffer& outBuffer, VkDeviceSize& outBaseOffset);
void RecycleUploadStagingBlock(UploadStagingBlock&& block);
// Drops a recorded-but-unsubmitted batch on the floor. Shutdown only: the
// device is being torn down, so the lost texel data is unobservable.
void DiscardPendingUploadBatch();
void DestroyUploadPools();
Vector<UploadStagingBlock> m_freeUploadStagingBlocks;
VkDeviceSize m_freeUploadStagingBytes = 0;
Vector<VkCommandBuffer> m_freeUploadCommandBuffers;
Vector<VkFence> m_freeUploadFences;
Bool m_uploadBatchOpen = false;
VkCommandBuffer m_uploadBatchCommandBuffer = VK_NULL_HANDLE;
// Blocks whose staging bytes the open batch's copies reference (last =
// the block the bump cursor is currently allocating from).
Vector<UploadStagingBlock> m_uploadBatchBlocks;
// Images the open batch writes; consulted for the rare re-upload-after-
// draw flush and by DeferResourceRelease (an unsubmitted command buffer
// referencing a deferred-released image would escape every fence-based
// destruction proof, so the batch is flushed before the image is parked).
Vector<VkImage> m_uploadBatchImages;
VkDeviceSize m_uploadBatchStagingBytes = 0;
// Texture uploads are submitted out-of-band but NOT waited on (waiting
// behind the queue serialized the CPU against the previous frame's GPU
// work every time an animated atlas re-uploaded). Each flushed batch's
// transients are parked here and RECYCLED (fence reset to the fence pool,
// command buffer reset to the CB pool, staging blocks back to the block
// pool) once the batch fence signals.
struct PendingUploadReclaim {
VkFence fence = VK_NULL_HANDLE;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
Vector<UploadStagingBlock> stagingBlocks;
};
Vector<PendingUploadReclaim> m_pendingUploadReclaims;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -15,7 +15,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(initInfo.device != VK_NULL_HANDLE, "VkTimerQueryManager::Initialize requires valid VkDevice");
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkTimerQueryManager::Initialize requires non-zero frame count");
if (initInfo.timestampValidBits == 0 || initInfo.timestampPeriodNs <= 0.0f || initInfo.slotsPerPool == 0) {
MGLOG_W("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
MGLOG_W_ONCE("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
initInfo.timestampValidBits, initInfo.timestampPeriodNs, initInfo.slotsPerPool);
return false;
}
@@ -35,7 +35,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto& poolState : m_pools) {
const VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &poolState.pool);
if (result != VK_SUCCESS) {
MGLOG_E("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
MGLOG_E_ONCE("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
Shutdown();
return false;
}
@@ -90,7 +90,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& poolState = m_pools[frameIndex];
if (poolState.cursor >= m_slotsPerPool) {
if (!poolState.exhaustionWarned) {
MGLOG_W("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
MGLOG_W_ONCE("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
"this frame fall back to the frontend path",
frameIndex, m_slotsPerPool);
poolState.exhaustionWarned = true;
@@ -120,7 +120,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device, m_pools[record.poolIndex].pool, record.slot, 1, sizeof(resultWithAvailability),
resultWithAvailability, sizeof(Uint64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
if (result != VK_SUCCESS && result != VK_NOT_READY) {
MGLOG_E("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
MGLOG_E_ONCE("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
return false;
}
if (resultWithAvailability[1] == 0) {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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
+44 -3
View File
@@ -52,19 +52,60 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
} // namespace MobileGL::MG_Backend::DirectVulkan
namespace MobileGL::MG_Backend::DirectVulkan {
// GL renders into sRGB color attachments RAW while GL_FRAMEBUFFER_SRGB is disabled
// (the core-profile default); Vulkan sRGB attachments always encode on write. The
// attachment view (and render pass format) therefore drops to the UNORM twin
// whenever the capability is off. Sampled views keep the sRGB format (decode on
// sample is unconditional in GL).
inline VkFormat ResolveSrgbAttachmentWriteFormat(VkFormat format, bool framebufferSrgbEnabled) {
if (framebufferSrgbEnabled) return format;
switch (format) {
case VK_FORMAT_R8G8B8A8_SRGB:
return VK_FORMAT_R8G8B8A8_UNORM;
case VK_FORMAT_B8G8R8A8_SRGB:
return VK_FORMAT_B8G8R8A8_UNORM;
default:
return format;
}
}
} // namespace MobileGL::MG_Backend::DirectVulkan
// The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log
// call: appending its format to the base format while its arguments precede the base
// arguments makes every conversion read the wrong slot (a %s pulling an int crashes).
//
// MGLOG_F and deliberately NOT latched. VK_VERIFY is the invariant-check macro: a Vulkan call
// MobileGL believes it has already made legal came back non-success, which is a
// should-never-happen state, not an expected failure mode a user hits. Those fast-fail loudly
// and keep saying so - the log-quietness rules that latch W/E cover expected failures (driver
// capability gaps, app misuse), not broken internal invariants. MOBILEGL_ASSERT below traps in
// a DEBUG build; MGLOG_F is what makes the same condition visible in an INFO test run, where
// the assert is compiled out by contract.
//
// A soft, recoverable failure must therefore NOT be routed through VK_VERIFY. Check the
// VkResult directly and report it with MGLOG_E_ONCE - see VkTextureManager::SyncTextureResource,
// where a driver legitimately refuses an image the format pre-check accepted.
#define VK_VERIFY(expr, ...) \
do { \
VkResult _vk_verify_result = (expr); \
if (_vk_verify_result != VK_SUCCESS) { \
MGLOG_F("Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, \
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \
MGLOG_F("Vulkan error %s (%d) at %s:%d", \
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
_vk_verify_result, __FILE__, __LINE__); \
} \
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), _vk_verify_result, __FILE__, __LINE__); \
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d", \
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
_vk_verify_result, __FILE__, __LINE__); \
} while (0)
#define XXHASH_VERIFY(expr, ...) \
do { \
XXH_errorcode _xxh_verify_result = (expr); \
MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, _xxh_verify_result, __FILE__, __LINE__); \
if (_xxh_verify_result != XXH_OK) { \
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \
} \
MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d", _xxh_verify_result, __FILE__, \
__LINE__); \
} while (0)
+5 -1
View File
@@ -41,4 +41,8 @@ add_test(NAME SanityBench COMMAND SanityBench --benchmark_counters_tabular=true)
set_tests_properties(SanityBench PROPERTIES LABELS benchmark)
add_subdirectory(Program)
add_subdirectory(Buffer)
add_subdirectory(Buffer)
add_subdirectory(Driver)
add_subdirectory(Container)
add_subdirectory(ShaderCache)
add_subdirectory(Transpile)
@@ -0,0 +1,20 @@
cmake_minimum_required(VERSION 3.24)
add_executable(
UnorderedMapBench
UnorderedMapBench.cpp
)
target_include_directories(UnorderedMapBench PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
UnorderedMapBench PRIVATE
benchmark::benchmark
${LINK_LIBRARIES}
)
add_test(NAME UnorderedMapBench COMMAND UnorderedMapBench --benchmark_counters_tabular=true)
set_tests_properties(UnorderedMapBench PROPERTIES LABELS benchmark)
@@ -0,0 +1,248 @@
// MobileGL - MobileGL/MG_Benchmark/Container/UnorderedMapBench.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// The standing performance observatory for MobileGL::UnorderedMap.
//
// This benchmarks the ALIAS, never a concrete table, so whatever UnorderedMap
// names today is what gets measured - swap the container in MG_Util/Types.h and
// re-run this same binary to get a directly comparable set of numbers. That is
// the point of it: the container sits on per-draw paths, so a change to it needs
// evidence, and the evidence should be produced the same way every time.
//
// The workloads are the shapes the tree actually exercises, not generic hash-map
// microbenchmarks. Four key shapes, because they stress a hash function very
// differently:
// * SEQUENTIAL dense small integers - GL object names from the index generator
// (buffer/texture/framebuffer/sampler registries).
// * POINTER real heap addresses - StateBackendObjectRegistry keys on
// StateObject*. These are aligned, so their low bits are the
// least random part of the key; a table that indexes on raw low
// bits clusters badly here and one that mixes first does not.
// Taken from the real allocator rather than a synthetic stride,
// which would flatter whichever table mixes its bits.
// * DIGEST already well-mixed 64-bit values - the XXH64 pipeline,
// vertex-input-state and program memos.
// * NAME short strings - uniform/attribute name to location maps.
//
// Sizes sweep from 8 upward because the per-draw memos are usually SMALL; a table
// that only wins at 4096 entries has not won anything that matters here.
//
// Run: build-linux/MobileGL/MG_Benchmark/Container/UnorderedMapBench
// or: ctest -R UnorderedMapBench (label: benchmark)
#include <cstdint>
#include <memory>
#include <random>
#include <string>
#include <vector>
#include <benchmark/benchmark.h>
#include "MG_Util/Types.h"
using namespace MobileGL;
namespace {
constexpr Int64 kMinSize = 8;
constexpr Int64 kMaxSize = 4096;
// Keep the real allocations alive for the whole process: the POINTER shape is
// only honest if the keys are addresses the allocator actually handed out, and
// they have to stay unique (a freed address can be handed out twice).
std::vector<std::unique_ptr<char[]>>& PointerKeyStorage() {
static std::vector<std::unique_ptr<char[]>> storage;
return storage;
}
Vector<Uint64> SequentialKeys(SizeT n) {
Vector<Uint64> keys;
keys.reserve(n);
for (SizeT i = 0; i < n; ++i) keys.push_back(static_cast<Uint64>(i) + 1);
return keys;
}
Vector<Uint64> PointerKeys(SizeT n) {
auto& storage = PointerKeyStorage();
Vector<Uint64> keys;
keys.reserve(n);
std::mt19937_64 rng(0xBEEF);
std::vector<std::unique_ptr<char[]>> churn;
for (SizeT i = 0; i < n; ++i) {
// State objects are not all one size, and the allocator sees other
// traffic between them - a single uniform stride is not what this
// registry ever sees.
const SizeT sz = 96 + (rng() % 192);
auto p = std::make_unique<char[]>(sz);
keys.push_back(reinterpret_cast<Uint64>(p.get()));
storage.push_back(std::move(p));
if ((rng() & 3) == 0) churn.push_back(std::make_unique<char[]>(32 + (rng() % 128)));
}
return keys;
}
Vector<Uint64> DigestKeys(SizeT n) {
Vector<Uint64> keys;
keys.reserve(n);
std::mt19937_64 rng(0xC0FFEE);
for (SizeT i = 0; i < n; ++i) keys.push_back(rng());
return keys;
}
Vector<String> NameKeys(SizeT n) {
static const char* kPrefixes[] = {"u_", "a_", "mc_", "iris_", "gl_", "v_"};
Vector<String> keys;
keys.reserve(n);
for (SizeT i = 0; i < n; ++i) {
keys.push_back(String(kPrefixes[i % 6]) + "Uniform" + std::to_string(i) + "_xyz");
}
return keys;
}
// Key sets are built once per size and shared: generating them inside the timed
// loop would measure the generator (and, for POINTER, the allocator) instead of
// the table.
template <typename KeyVec, KeyVec (*Make)(SizeT)>
const KeyVec& CachedKeys(SizeT n) {
static UnorderedMap<SizeT, KeyVec> cache;
auto it = cache.find(n);
if (it != cache.end()) return it->second;
return cache.emplace(n, Make(n)).first->second;
}
template <typename Key>
UnorderedMap<Key, Uint64> Populated(const Vector<Key>& keys) {
UnorderedMap<Key, Uint64> map;
for (SizeT i = 0; i < keys.size(); ++i) map[keys[i]] = i;
return map;
}
// ---- the workloads ----------------------------------------------------
// The dominant per-draw operation by a wide margin: a populated cache that is
// read far more often than it is written.
template <typename KeyVec, KeyVec (*Make)(SizeT)>
void LookupHit(benchmark::State& state) {
const auto& keys = CachedKeys<KeyVec, Make>(static_cast<SizeT>(state.range(0)));
auto map = Populated(keys);
for (auto _ : state) {
for (const auto& k : keys) {
auto it = map.find(k);
benchmark::DoNotOptimize(it->second);
}
}
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(keys.size()));
}
// "Is this resource cached yet?" answered NO - the probe length on a miss is a
// different cost from a hit, and resource caches ask this constantly.
template <typename KeyVec, KeyVec (*Make)(SizeT)>
void LookupMiss(benchmark::State& state) {
const SizeT n = static_cast<SizeT>(state.range(0));
const auto& keys = CachedKeys<KeyVec, Make>(n);
auto map = Populated(keys);
const KeyVec absent = Make(n); // same shape, never inserted
for (auto _ : state) {
for (const auto& k : absent) {
benchmark::DoNotOptimize(map.find(k) != map.end());
}
}
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(absent.size()));
}
// Building a cache from empty, rehashes included.
template <typename KeyVec, KeyVec (*Make)(SizeT)>
void InsertGrow(benchmark::State& state) {
const auto& keys = CachedKeys<KeyVec, Make>(static_cast<SizeT>(state.range(0)));
for (auto _ : state) {
UnorderedMap<typename KeyVec::value_type, Uint64> map;
for (SizeT i = 0; i < keys.size(); ++i) map[keys[i]] = i;
benchmark::DoNotOptimize(map.size());
}
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(keys.size()));
}
// Cache eviction and refill: erase half by key, put them back. This is the
// aged-out-entry sweep the pipeline and vertex-input caches do.
template <typename KeyVec, KeyVec (*Make)(SizeT)>
void EraseChurn(benchmark::State& state) {
const auto& keys = CachedKeys<KeyVec, Make>(static_cast<SizeT>(state.range(0)));
for (auto _ : state) {
state.PauseTiming();
auto map = Populated(keys);
state.ResumeTiming();
for (SizeT i = 0; i < keys.size(); i += 2) benchmark::DoNotOptimize(map.erase(keys[i]));
for (SizeT i = 0; i < keys.size(); i += 2) map[keys[i]] = i;
benchmark::DoNotOptimize(map.size());
}
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(keys.size()));
}
// Mass eviction: erase-while-iterating across the whole table. This is the loop
// shape that a container's erase()-return contract can get wrong, and the one
// that fed garbage handles to vkDestroyPipeline when it was wrong before.
template <typename KeyVec, KeyVec (*Make)(SizeT)>
void EraseSweep(benchmark::State& state) {
const auto& keys = CachedKeys<KeyVec, Make>(static_cast<SizeT>(state.range(0)));
for (auto _ : state) {
state.PauseTiming();
auto map = Populated(keys);
state.ResumeTiming();
for (auto it = map.begin(); it != map.end();) it = map.erase(it);
benchmark::DoNotOptimize(map.size());
}
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(keys.size()));
}
// Whole-table walks: the per-frame sweeps that age entries out, and the
// teardown loops that destroy every Vulkan object a cache owns.
template <typename KeyVec, KeyVec (*Make)(SizeT)>
void Iterate(benchmark::State& state) {
const auto& keys = CachedKeys<KeyVec, Make>(static_cast<SizeT>(state.range(0)));
auto map = Populated(keys);
for (auto _ : state) {
Uint64 acc = 0;
for (const auto& entry : map) acc += entry.second;
benchmark::DoNotOptimize(acc);
}
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(keys.size()));
}
} // namespace
#define MGL_MAP_BENCH(WORKLOAD, SHAPE, VEC, MAKER) \
BENCHMARK_TEMPLATE(WORKLOAD, VEC, MAKER) \
->Name(#WORKLOAD "/" #SHAPE) \
->RangeMultiplier(8) \
->Range(kMinSize, kMaxSize)
MGL_MAP_BENCH(LookupHit, sequential, Vector<Uint64>, SequentialKeys);
MGL_MAP_BENCH(LookupHit, pointer, Vector<Uint64>, PointerKeys);
MGL_MAP_BENCH(LookupHit, digest, Vector<Uint64>, DigestKeys);
MGL_MAP_BENCH(LookupHit, name, Vector<String>, NameKeys);
MGL_MAP_BENCH(LookupMiss, sequential, Vector<Uint64>, SequentialKeys);
MGL_MAP_BENCH(LookupMiss, pointer, Vector<Uint64>, PointerKeys);
MGL_MAP_BENCH(LookupMiss, digest, Vector<Uint64>, DigestKeys);
MGL_MAP_BENCH(LookupMiss, name, Vector<String>, NameKeys);
MGL_MAP_BENCH(InsertGrow, sequential, Vector<Uint64>, SequentialKeys);
MGL_MAP_BENCH(InsertGrow, pointer, Vector<Uint64>, PointerKeys);
MGL_MAP_BENCH(InsertGrow, digest, Vector<Uint64>, DigestKeys);
MGL_MAP_BENCH(InsertGrow, name, Vector<String>, NameKeys);
MGL_MAP_BENCH(EraseChurn, sequential, Vector<Uint64>, SequentialKeys);
MGL_MAP_BENCH(EraseChurn, digest, Vector<Uint64>, DigestKeys);
MGL_MAP_BENCH(EraseChurn, name, Vector<String>, NameKeys);
MGL_MAP_BENCH(EraseSweep, sequential, Vector<Uint64>, SequentialKeys);
MGL_MAP_BENCH(EraseSweep, digest, Vector<Uint64>, DigestKeys);
MGL_MAP_BENCH(Iterate, sequential, Vector<Uint64>, SequentialKeys);
MGL_MAP_BENCH(Iterate, digest, Vector<Uint64>, DigestKeys);
BENCHMARK_MAIN();
@@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.24)
# A real, headless EGL client, deliberately NOT linked against MobileGL: it
# dlopens one EGL provider at runtime ($DRIVERBENCH_EGL_LIB - the system
# libEGL.so.1 for the native driver, or a libMobileGL.so path for either
# MobileGL backend), so the same binary measures all three stacks.
if (NOT UNIX OR APPLE OR ANDROID)
return()
endif()
add_executable(DriverBench DriverBench.c)
target_link_libraries(DriverBench PRIVATE dl)
add_test(NAME DriverBench COMMAND DriverBench draw_tiny)
set_tests_properties(DriverBench PROPERTIES LABELS benchmark)
+501
View File
@@ -0,0 +1,501 @@
/* MobileGL - MobileGL/MG_Benchmark/Driver/DriverBench.c
* 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
*
* Headless, EGL-based driver benchmark shaped like Minecraft's GL usage.
* Unlike the MobileGL_s microbenches next door this exercises a full GL
* stack: it dlopens ONE EGL provider ($DRIVERBENCH_EGL_LIB - the system
* libEGL.so.1 for the native driver, or a libMobileGL.so path for either
* MobileGL backend selected with MOBILEGL_BACKEND_TYPE), creates a desktop-GL
* context on a small pbuffer, renders into its own FBO and paces frames with
* glFinish. No window system is required: the default display is tried first
* so a desktop run reaches the real driver, and a headless box (CI, a build
* server) falls back to EGL_MESA_platform_surfaceless - see
* run_driver_bench.sh.
*
* Every case models one hot pattern from captured Minecraft traces:
* draw_tiny back-to-back glDrawElements, shared state (chunk batch)
* draw_uniform per-draw vec3 offset uniform + draw (chunk sections)
* draw_multi_vao per-draw VAO/VBO switch + draw (per-section buffers)
* tex_pingpong per-draw texture bind churn on one unit
* program_pingpong alternate two programs + mat4 upload (chunk<->entity)
* chunk_upload glBufferData(NULL) orphan + glBufferSubData + draw
* atlas_sprite N 16x16 glTexSubImage2D into a 1024x512 atlas + draw
* lightmap full 16x16 lightmap respecify per frame + draw
* scene_mix composite frame built from the knobs below
*
* Output: one CSV line per case:
* case,frames,ops_per_frame,median_frame_ms,ns_per_op,fps
*/
#include <dlfcn.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* ---- EGL constants ---- */
typedef void* EGLDisplay;
typedef void* EGLConfig;
typedef void* EGLContext;
typedef void* EGLSurface;
typedef int EGLint;
typedef unsigned int EGLBoolean;
typedef unsigned int EGLenum;
#define EGL_DEFAULT_DISPLAY ((void*)0)
#define EGL_NO_CONTEXT ((EGLContext)0)
#define EGL_NO_SURFACE ((EGLSurface)0)
#define EGL_FALSE 0
#define EGL_SURFACE_TYPE 0x3033
#define EGL_PBUFFER_BIT 0x0001
#define EGL_RENDERABLE_TYPE 0x3040
#define EGL_OPENGL_BIT 0x0008
#define EGL_RED_SIZE 0x3024
#define EGL_GREEN_SIZE 0x3023
#define EGL_BLUE_SIZE 0x3022
#define EGL_DEPTH_SIZE 0x3025
#define EGL_WIDTH 0x3057
#define EGL_HEIGHT 0x3056
#define EGL_NONE 0x3038
#define EGL_OPENGL_API 0x30A2
#define EGL_OPENGL_ES_API 0x30A0
#define EGL_OPENGL_ES3_BIT 0x0040
#define EGL_CONTEXT_CLIENT_VERSION 0x3098
#define EGL_CONTEXT_MAJOR_VERSION 0x3098
#define EGL_CONTEXT_MINOR_VERSION 0x30FB
#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD
#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001
#define EGL_PLATFORM_SURFACELESS_MESA 0x31DD
/* ---- GL constants ---- */
#define GL_COLOR_BUFFER_BIT 0x00004000
#define GL_DEPTH_BUFFER_BIT 0x00000100
#define GL_TRIANGLES 0x0004
#define GL_UNSIGNED_INT 0x1405
#define GL_SHORT 0x1402
#define GL_FLOAT 0x1406
#define GL_UNSIGNED_BYTE 0x1401
#define GL_ARRAY_BUFFER 0x8892
#define GL_ELEMENT_ARRAY_BUFFER 0x8893
#define GL_STATIC_DRAW 0x88E4
#define GL_TEXTURE_2D 0x0DE1
#define GL_TEXTURE0 0x84C0
#define GL_RGBA 0x1908
#define GL_RGBA8 0x8058
#define GL_DEPTH_COMPONENT24 0x81A6
#define GL_TEXTURE_MIN_FILTER 0x2801
#define GL_TEXTURE_MAG_FILTER 0x2800
#define GL_NEAREST 0x2600
#define GL_NEAREST_MIPMAP_LINEAR 0x2702
#define GL_DEPTH_TEST 0x0B71
#define GL_BLEND 0x0BE2
#define GL_SRC_ALPHA 0x0302
#define GL_ONE_MINUS_SRC_ALPHA 0x0303
#define GL_ONE 1
#define GL_ZERO 0
#define GL_VERTEX_SHADER 0x8B31
#define GL_FRAGMENT_SHADER 0x8B30
#define GL_COMPILE_STATUS 0x8B81
#define GL_LINK_STATUS 0x8B82
#define GL_VERSION 0x1F02
#define GL_RENDERER 0x1F01
#define GL_NO_ERROR 0
#define GL_FRAMEBUFFER 0x8D40
#define GL_RENDERBUFFER 0x8D41
#define GL_COLOR_ATTACHMENT0 0x8CE0
#define GL_DEPTH_ATTACHMENT 0x8D00
#define GL_FRAMEBUFFER_COMPLETE 0x8CD5
#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
#define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001
#define GL_UNIFORM_BUFFER 0x8A11
#define GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT 0x8A34
#define GL_DYNAMIC_DRAW 0x88E8
#define GL_STREAM_DRAW 0x88E0
#define GL_UNPACK_ALIGNMENT 0x0CF5
#define GL_UNPACK_ROW_LENGTH 0x0CF2
#define GL_UNPACK_SKIP_ROWS 0x0CF3
#define GL_UNPACK_SKIP_PIXELS 0x0CF4
#define GL_TEXTURE_WRAP_S 0x2802
#define GL_TEXTURE_WRAP_T 0x2803
#define GL_CLAMP_TO_EDGE 0x812F
#define GL_REPEAT 0x2901
typedef unsigned int GLuint;
typedef int GLint;
typedef int GLsizei;
typedef unsigned int GLenum;
typedef char GLchar;
typedef unsigned char GLboolean;
typedef long GLsizeiptr;
typedef long GLintptr;
/* ---- resolved entry points ---- */
static void* (*g_eglGetProcAddress)(const char*);
static void* g_provider;
#define GLF(ret, name, args) static ret(*name) args;
GLF(void, glClear, (unsigned))
GLF(void, glClearColor, (float, float, float, float))
GLF(void, glEnable, (GLenum))
GLF(void, glDisable, (GLenum))
GLF(void, glBlendFuncSeparate, (GLenum, GLenum, GLenum, GLenum))
GLF(void, glDrawBuffers, (GLsizei, const GLenum*))
GLF(void, glViewport, (GLint, GLint, GLsizei, GLsizei))
GLF(const unsigned char*, glGetString, (GLenum))
GLF(GLenum, glGetError, (void))
GLF(void, glFinish, (void))
GLF(void, glFlush, (void))
GLF(void, glGenBuffers, (GLsizei, GLuint*))
GLF(void, glBindBuffer, (GLenum, GLuint))
GLF(void, glBufferData, (GLenum, GLsizeiptr, const void*, GLenum))
GLF(void, glBufferSubData, (GLenum, GLintptr, GLsizeiptr, const void*))
GLF(void, glGenVertexArrays, (GLsizei, GLuint*))
GLF(void, glBindVertexArray, (GLuint))
GLF(void, glEnableVertexAttribArray, (GLuint))
GLF(void, glVertexAttribPointer, (GLuint, GLint, GLenum, GLboolean, GLsizei, const void*))
GLF(void, glGenTextures, (GLsizei, GLuint*))
GLF(void, glBindTexture, (GLenum, GLuint))
GLF(void, glActiveTexture, (GLenum))
GLF(void, glTexImage2D, (GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const void*))
GLF(void, glTexSubImage2D, (GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, const void*))
GLF(void, glTexParameteri, (GLenum, GLenum, GLint))
GLF(void, glPixelStorei, (GLenum, GLint))
GLF(void, glGetIntegerv, (GLenum, GLint*))
GLF(void, glGenerateMipmap, (GLenum))
GLF(GLuint, glCreateShader, (GLenum))
GLF(void, glShaderSource, (GLuint, GLsizei, const GLchar* const*, const GLint*))
GLF(void, glCompileShader, (GLuint))
GLF(void, glGetShaderiv, (GLuint, GLenum, GLint*))
GLF(void, glGetShaderInfoLog, (GLuint, GLsizei, GLsizei*, GLchar*))
GLF(GLuint, glCreateProgram, (void))
GLF(void, glAttachShader, (GLuint, GLuint))
GLF(void, glLinkProgram, (GLuint))
GLF(void, glGetProgramiv, (GLuint, GLenum, GLint*))
GLF(void, glUseProgram, (GLuint))
GLF(GLint, glGetUniformLocation, (GLuint, const GLchar*))
GLF(void, glUniform1i, (GLint, GLint))
GLF(void, glUniform3f, (GLint, float, float, float))
GLF(void, glUniformMatrix4fv, (GLint, GLsizei, GLboolean, const float*))
GLF(void, glDrawElements, (GLenum, GLsizei, GLenum, const void*))
GLF(void, glBindAttribLocation, (GLuint, GLuint, const GLchar*))
GLF(void, glUniform3fv, (GLint, GLsizei, const float*))
GLF(void, glDrawArrays, (GLenum, GLint, GLsizei))
GLF(void, glDrawElementsBaseVertex, (GLenum, GLsizei, GLenum, const void*, GLint))
GLF(void, glMultiDrawElementsBaseVertex,
(GLenum, const GLsizei*, GLenum, const void* const*, GLsizei, const GLint*))
GLF(void, glBindBufferRange, (GLenum, GLuint, GLuint, GLintptr, GLsizeiptr))
GLF(void, glBindBufferBase, (GLenum, GLuint, GLuint))
GLF(GLuint, glGetUniformBlockIndex, (GLuint, const GLchar*))
GLF(void, glUniformBlockBinding, (GLuint, GLuint, GLuint))
GLF(void, glGenSamplers, (GLsizei, GLuint*))
GLF(void, glBindSampler, (GLuint, GLuint))
GLF(void, glSamplerParameteri, (GLuint, GLenum, GLint))
GLF(void, glGenFramebuffers, (GLsizei, GLuint*))
GLF(void, glBindFramebuffer, (GLenum, GLuint))
GLF(void, glGenRenderbuffers, (GLsizei, GLuint*))
GLF(void, glBindRenderbuffer, (GLenum, GLuint))
GLF(void, glRenderbufferStorage, (GLenum, GLenum, GLsizei, GLsizei))
GLF(void, glFramebufferRenderbuffer, (GLenum, GLenum, GLenum, GLuint))
GLF(GLenum, glCheckFramebufferStatus, (GLenum))
GLF(void*, glFenceSync, (GLenum, unsigned))
GLF(GLenum, glClientWaitSync, (void*, unsigned, unsigned long long))
GLF(void, glDeleteSync, (void*))
static uint64_t now_ns(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
}
static int cmp_u64(const void* a, const void* b) {
uint64_t x = *(const uint64_t*)a, y = *(const uint64_t*)b;
return x < y ? -1 : x > y;
}
/* Scene, cases and the case table live next door so the Android plugin's
* in-process benchmark runs byte-identical bodies. */
static void bench_gl_failed(const char* what, const char* detail) {
fprintf(stderr, "FAIL: %s %s\n", what, detail ? detail : "");
exit(1);
}
/* GLES has glDrawElementsBaseVertex (3.2 core) but no multi-draw form of it, so
* against a native mobile driver the multi-draw case issues the same sub-draws
* one at a time - which is what the extension folds up, and what an application
* without it would have to write. Desktop GL and MobileGL take the real call. */
static void bench_multi_draw_elements_base_vertex(GLenum mode, const GLsizei* counts, GLenum type,
const void* const* offsets, GLsizei drawCount,
const GLint* baseVertices) {
if (glMultiDrawElementsBaseVertex) {
glMultiDrawElementsBaseVertex(mode, counts, type, offsets, drawCount, baseVertices);
return;
}
for (GLsizei i = 0; i < drawCount; ++i) {
glDrawElementsBaseVertex(mode, counts[i], type, offsets[i], baseVertices[i]);
}
}
#include "DriverBenchCases.inc"
/* ---- bench driver: fence-paced frames on the offscreen FBO ----------------
* Frames are closed with a real fence wait, not glFinish: MobileGL implements
* glFinish and glFlush as no-ops (MG_Impl/GLImpl/Exporting/Definitions.cpp),
* so a glFinish-paced loop would time only the CPU-side submit on a MobileGL
* backend while timing submit-plus-GPU on the native driver - the two numbers
* would not describe the same work. A sync object is honoured by every stack
* measured here.
*/
typedef void (*case_fn)(int frame, long a, long b);
static int g_warmup = 30, g_frames = 120;
static void end_frame_wait(void) {
if (glFenceSync && glClientWaitSync && glDeleteSync) {
void* sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
if (sync) {
glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 1000000000ull);
glDeleteSync(sync);
return;
}
}
glFinish();
}
static void run_case(const char* name, case_fn body, long a, long b, long opsPerFrame) {
static uint64_t samples[4096];
if (g_frames > 4096) g_frames = 4096;
end_frame_wait();
for (int i = 0; i < g_warmup; ++i) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
body(i, a, b);
end_frame_wait();
}
for (int i = 0; i < g_frames; ++i) {
uint64_t t0 = now_ns();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
body(i, a, b);
end_frame_wait();
samples[i] = now_ns() - t0;
}
qsort(samples, g_frames, sizeof(uint64_t), cmp_u64);
uint64_t med = samples[g_frames / 2];
double frameMs = med / 1e6;
double nsPerOp = opsPerFrame > 0 ? (double)med / (double)opsPerFrame : 0.0;
printf("%s,%d,%ld,%.3f,%.1f,%.1f\n", name, g_frames, opsPerFrame, frameMs, nsPerOp,
1e9 / (double)med);
fflush(stdout);
if (glGetError() != GL_NO_ERROR) fprintf(stderr, "WARN: GL error after %s\n", name);
}
/* A display that needs no window system. eglGetPlatformDisplay is EGL 1.5
* core and eglGetPlatformDisplayEXT is the EGL_EXT_platform_base spelling
* older loaders ship; both are client entry points, so they resolve before
* any display exists. Only the attribute-list types differ between the two
* and this passes none, so one cast covers both. */
static EGLDisplay surfaceless_display(void) {
void* fn = dlsym(g_provider, "eglGetPlatformDisplay");
if (!fn) fn = g_eglGetProcAddress("eglGetPlatformDisplay");
if (!fn) fn = dlsym(g_provider, "eglGetPlatformDisplayEXT");
if (!fn) fn = g_eglGetProcAddress("eglGetPlatformDisplayEXT");
if (!fn) return NULL;
return ((EGLDisplay(*)(EGLenum, void*, const void*))fn)(EGL_PLATFORM_SURFACELESS_MESA,
EGL_DEFAULT_DISPLAY, NULL);
}
/* ---- EGL bootstrap: one provider library, pbuffer, desktop-GL context ---- */
static int boot_egl(void) {
const char* libpath = getenv("DRIVERBENCH_EGL_LIB");
if (!libpath) libpath = "libEGL.so.1";
g_provider = dlopen(libpath, RTLD_LAZY | RTLD_LOCAL);
if (!g_provider) {
fprintf(stderr, "FAIL: dlopen %s: %s\n", libpath, dlerror());
return 1;
}
#define ESYM(name) \
void* p_##name = dlsym(g_provider, #name); \
if (!p_##name) { fprintf(stderr, "FAIL: dlsym %s\n", #name); return 1; }
ESYM(eglGetDisplay)
ESYM(eglInitialize)
ESYM(eglChooseConfig)
ESYM(eglBindAPI)
ESYM(eglCreateContext)
ESYM(eglCreatePbufferSurface)
ESYM(eglMakeCurrent)
ESYM(eglGetProcAddress)
ESYM(eglGetError)
g_eglGetProcAddress = (void* (*)(const char*))p_eglGetProcAddress;
EGLint (*getError)(void) = (EGLint(*)(void))p_eglGetError;
EGLBoolean (*initialize)(EGLDisplay, EGLint*, EGLint*) =
(EGLBoolean(*)(EGLDisplay, EGLint*, EGLint*))p_eglInitialize;
/* The default display first: it is the one a windowed app would get, and
* on a desktop it is the one that reaches the real GPU - which is the
* driver this bench exists to measure. It does need a window system,
* though; Mesa's default platform is X11, so with no $DISPLAY (CI, a
* build server, ssh without forwarding) eglInitialize fails. Fall back to
* EGL_MESA_platform_surfaceless rather than give up: every case draws into
* the FBO built by build_resources(), so no window is needed for any of
* the work being timed. */
EGLint maj = 0, min = 0;
const char* how = "default display";
EGLDisplay dpy = ((EGLDisplay(*)(void*))p_eglGetDisplay)(EGL_DEFAULT_DISPLAY);
if (!dpy || !initialize(dpy, &maj, &min)) {
dpy = surfaceless_display();
how = "surfaceless display";
if (!dpy || !initialize(dpy, &maj, &min)) {
fprintf(stderr, "FAIL: eglInitialize (0x%x)\n", getError());
return 1;
}
}
fprintf(stderr, "EGL %d.%d via %s (%s)\n", maj, min, libpath, how);
// Desktop GL first (that is what MobileGL exposes and what the cases are
// written against), GLES 3 second so the same binary can measure a device's
// native driver as the baseline. The .inc picks ESSL shader sources when the
// context turns out to be ES.
EGLBoolean (*chooseConfig)(EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*) =
(EGLBoolean(*)(EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*))p_eglChooseConfig;
EGLContext (*createContext)(EGLDisplay, EGLConfig, EGLContext, const EGLint*) =
(EGLContext(*)(EGLDisplay, EGLConfig, EGLContext, const EGLint*))p_eglCreateContext;
EGLBoolean (*bindApi)(EGLenum) = (EGLBoolean(*)(EGLenum))p_eglBindAPI;
EGLConfig cfg = NULL;
EGLint ncfg = 0;
EGLContext ctx = EGL_NO_CONTEXT;
if (bindApi(EGL_OPENGL_API)) {
const EGLint cfgAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8,
EGL_DEPTH_SIZE, 24, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE};
if (chooseConfig(dpy, cfgAttribs, &cfg, 1, &ncfg) && ncfg >= 1) {
const EGLint ctxAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 2,
EGL_CONTEXT_OPENGL_PROFILE_MASK,
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, EGL_NONE};
ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, ctxAttribs);
if (ctx == EGL_NO_CONTEXT) ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, NULL);
}
}
if (ctx == EGL_NO_CONTEXT) {
if (!bindApi(EGL_OPENGL_ES_API)) {
fprintf(stderr, "FAIL: neither OpenGL nor OpenGL ES is bindable on this provider\n");
return 1;
}
const EGLint esCfgAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_DEPTH_SIZE, 24,
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_NONE};
ncfg = 0;
if (!chooseConfig(dpy, esCfgAttribs, &cfg, 1, &ncfg) || ncfg < 1) {
// EGL_SURFACE_TYPE 0 matches any config: a stack that offers no
// pbuffer at all is still usable through the surfaceless context
// path below.
const EGLint relaxed[] = {EGL_SURFACE_TYPE, 0, EGL_RED_SIZE, 8, EGL_NONE};
if (!chooseConfig(dpy, relaxed, &cfg, 1, &ncfg) || ncfg < 1) {
fprintf(stderr, "FAIL: eglChooseConfig\n");
return 1;
}
}
const EGLint esCtxAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, esCtxAttribs);
}
if (ctx == EGL_NO_CONTEXT) {
fprintf(stderr, "FAIL: eglCreateContext (0x%x)\n", getError());
return 1;
}
/* The pbuffer only exists to have something to make current - nothing is
* ever drawn to it. Where there is no pbuffer config, EGL_NO_SURFACE is
* exactly what EGL_KHR_surfaceless_context takes, so the same call covers
* both. */
const EGLint pbAttribs[] = {EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE};
EGLSurface surf = ((EGLSurface(*)(EGLDisplay, EGLConfig, const EGLint*))p_eglCreatePbufferSurface)(
dpy, cfg, pbAttribs);
if (surf == EGL_NO_SURFACE)
fprintf(stderr, "no pbuffer (0x%x), using a surfaceless context\n", getError());
if (!((EGLBoolean(*)(EGLDisplay, EGLSurface, EGLSurface, EGLContext))p_eglMakeCurrent)(dpy, surf,
surf, ctx)) {
fprintf(stderr, "FAIL: eglMakeCurrent (0x%x)\n", getError());
return 1;
}
/* Core GL entry points: eglGetProcAddress first (EGL 1.5 serves core
* functions), provider dlsym as fallback (both glvnd and MobileGL export
* the gl* symbols directly). */
#define RESOLVE(name) \
do { \
*(void**)&name = g_eglGetProcAddress(#name); \
if (!name) *(void**)&name = dlsym(g_provider, #name); \
if (!name) { fprintf(stderr, "FAIL: resolve %s\n", #name); return 1; } \
} while (0)
RESOLVE(glClear); RESOLVE(glClearColor); RESOLVE(glEnable); RESOLVE(glViewport);
RESOLVE(glDisable); RESOLVE(glBlendFuncSeparate); RESOLVE(glDrawBuffers);
RESOLVE(glGetString); RESOLVE(glGetError); RESOLVE(glFinish); RESOLVE(glFlush);
RESOLVE(glGenBuffers); RESOLVE(glBindBuffer); RESOLVE(glBufferData); RESOLVE(glBufferSubData);
RESOLVE(glGenVertexArrays); RESOLVE(glBindVertexArray); RESOLVE(glEnableVertexAttribArray);
RESOLVE(glVertexAttribPointer); RESOLVE(glGenTextures); RESOLVE(glBindTexture);
RESOLVE(glActiveTexture); RESOLVE(glTexImage2D); RESOLVE(glTexSubImage2D);
RESOLVE(glTexParameteri); RESOLVE(glGenerateMipmap); RESOLVE(glCreateShader);
RESOLVE(glPixelStorei); RESOLVE(glGetIntegerv);
RESOLVE(glShaderSource); RESOLVE(glCompileShader); RESOLVE(glGetShaderiv);
RESOLVE(glGetShaderInfoLog); RESOLVE(glCreateProgram); RESOLVE(glAttachShader);
RESOLVE(glLinkProgram); RESOLVE(glGetProgramiv); RESOLVE(glUseProgram);
RESOLVE(glGetUniformLocation); RESOLVE(glUniform1i); RESOLVE(glUniform3f);
RESOLVE(glUniformMatrix4fv); RESOLVE(glDrawElements); RESOLVE(glBindAttribLocation);
RESOLVE(glUniform3fv); RESOLVE(glDrawArrays); RESOLVE(glDrawElementsBaseVertex);
RESOLVE(glBindBufferRange); RESOLVE(glBindBufferBase);
RESOLVE(glGetUniformBlockIndex); RESOLVE(glUniformBlockBinding);
RESOLVE(glGenSamplers); RESOLVE(glBindSampler); RESOLVE(glSamplerParameteri);
RESOLVE(glGenFramebuffers); RESOLVE(glBindFramebuffer); RESOLVE(glGenRenderbuffers);
RESOLVE(glBindRenderbuffer); RESOLVE(glRenderbufferStorage); RESOLVE(glFramebufferRenderbuffer);
RESOLVE(glCheckFramebufferStatus);
// Optional: end_frame_wait() falls back to glFinish when a stack has no
// sync objects, so resolve without failing the run.
*(void**)&glFenceSync = g_eglGetProcAddress("glFenceSync");
if (!glFenceSync) *(void**)&glFenceSync = dlsym(g_provider, "glFenceSync");
*(void**)&glClientWaitSync = g_eglGetProcAddress("glClientWaitSync");
if (!glClientWaitSync) *(void**)&glClientWaitSync = dlsym(g_provider, "glClientWaitSync");
*(void**)&glDeleteSync = g_eglGetProcAddress("glDeleteSync");
if (!glDeleteSync) *(void**)&glDeleteSync = dlsym(g_provider, "glDeleteSync");
// Desktop-only: GLES 3.2 has DrawElementsBaseVertex but no multi-draw form,
// so bench_multi_draw_elements_base_vertex() emulates it when this is null.
*(void**)&glMultiDrawElementsBaseVertex = g_eglGetProcAddress("glMultiDrawElementsBaseVertex");
if (!glMultiDrawElementsBaseVertex)
*(void**)&glMultiDrawElementsBaseVertex = dlsym(g_provider, "glMultiDrawElementsBaseVertex");
fprintf(stderr, "renderer: %s\n", glGetString(GL_RENDERER));
fprintf(stderr, "version: %s\n", glGetString(GL_VERSION));
return 0;
}
int main(int argc, char** argv) {
long draws = 2048;
if (getenv("DRIVERBENCH_DRAWS")) draws = atol(getenv("DRIVERBENCH_DRAWS"));
if (getenv("DRIVERBENCH_FRAMES")) g_frames = atoi(getenv("DRIVERBENCH_FRAMES"));
if (getenv("DRIVERBENCH_SPRITES")) g_mixSprites = atol(getenv("DRIVERBENCH_SPRITES"));
if (boot_egl()) return 1;
build_resources();
printf("case,frames,ops_per_frame,median_frame_ms,ns_per_op,fps\n");
for (int i = 0; i < kBenchCaseCount; ++i) {
const BenchCaseDesc* c = &kBenchCases[i];
if (argc > 1) {
int wanted = 0;
for (int j = 1; j < argc; ++j)
if (strcmp(argv[j], c->name) == 0) wanted = 1;
if (!wanted) continue;
}
// The generic cases scale with DRIVERBENCH_DRAWS; the mc_* rates are
// measured and must not move, or the numbers stop being comparable.
long a = c->a, ops = c->opsPerFrame;
if (strncmp(c->name, "mc_", 3) != 0 && a > 100) {
a = draws * a / 2048;
ops = c->opsPerFrame * draws / 2048;
}
run_case(c->name, c->fn, a, c->b, ops);
}
return 0;
}
@@ -0,0 +1,640 @@
/* MobileGL - MobileGL/MG_Benchmark/Driver/DriverBenchCases.inc
* Copyright (c) 2025-2026 MobileGL-Dev
* Licensed under the GNU Lesser General Public License v3.0:
* https://www.gnu.org/licenses/gpl-3.0.txt
* https://www.gnu.org/licenses/lgpl-3.0.txt
* SPDX-License-Identifier: LGPL-3.0-only
* End of Source File Header
*
* The benchmark scene and its cases, with no harness and no GL loader: the
* includer supplies both. DriverBench.c drives it through function pointers
* resolved from one EGL provider; MG_Util/SelfTest/DriverBenchJni.cpp drives
* it through MobileGL's own frontend entry points inside the Android plugin.
* Sharing the bodies is the point - a number from the phone and a number from
* the desktop have to describe the same work.
*
* The includer must have declared, before including this file: the GL types
* and enums used below, and callable gl* entry points with the standard
* signatures. bench_gl_failed() is called (and must be defined) when shader
* compilation or linking fails, so a caller can report the failure instead of
* dying inside a benchmark.
*/
/* ---- shared scene resources (Minecraft-shaped) ---- */
#define MAX_SECTIONS 512
static GLuint g_progChunk, g_progEntity;
static GLint g_uOffsetChunk, g_uMvpChunk, g_uMvpEntity;
static GLuint g_vao[MAX_SECTIONS], g_vbo[MAX_SECTIONS];
static GLuint g_sharedIbo;
static GLuint g_texAtlas, g_texLight, g_texEntity;
static int g_quadsPerSection = 128; /* 128 quads = 512 verts, 768 indices */
static unsigned char* g_scratch;
/* Uniform ring + sampler for the 26.2-shaped cases (see the case block below). */
static GLuint g_uboRing;
static GLint g_uboAlign = 256;
static size_t g_uboSlot = 256;
static GLuint g_sampler;
/* Two small offscreen targets for the 26.2-style render-pass churn case. */
static GLuint g_passFbo[2];
static GLuint g_passColor[2];
static float g_mvp[16] = {0.002f, 0, 0, 0, 0, 0.002f, 0, 0, 0, 0, -0.001f, 0, -1.f, -1.f, 0.f, 1.f};
/* Minecraft chunk vertex: pos 3f, color 4ub, uv 2f, packed light 2s -> 32 B */
#define VERT_STRIDE 32
static void fill_section_vertices(unsigned char* dst, int quads, unsigned seed) {
for (int q = 0; q < quads * 4; ++q) {
float* f = (float*)(dst + q * VERT_STRIDE);
unsigned r = seed = seed * 1664525u + 1013904223u;
f[0] = (float)(q & 31) * 8.0f + (float)(r & 7);
f[1] = (float)((q >> 5) & 31) * 8.0f;
f[2] = (float)(q % 7) * 0.1f;
dst[q * VERT_STRIDE + 12] = (unsigned char)r;
dst[q * VERT_STRIDE + 13] = (unsigned char)(r >> 8);
dst[q * VERT_STRIDE + 14] = (unsigned char)(r >> 16);
dst[q * VERT_STRIDE + 15] = 255;
f[4] = (float)(r & 1023) / 1024.0f;
f[5] = (float)((r >> 10) & 511) / 512.0f;
((short*)(dst + q * VERT_STRIDE + 24))[0] = 15 << 4;
((short*)(dst + q * VERT_STRIDE + 24))[1] = 15 << 4;
}
}
static GLuint make_shader(GLenum kind, const char* src) {
GLuint sh = glCreateShader(kind);
glShaderSource(sh, 1, &src, NULL);
glCompileShader(sh);
GLint ok = 0;
glGetShaderiv(sh, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[1024];
glGetShaderInfoLog(sh, sizeof log, NULL, log);
bench_gl_failed("shader compile", log);
return 0;
}
return sh;
}
static GLuint make_program(const char* vs_src, const char* fs_src) {
GLuint prog = glCreateProgram();
glAttachShader(prog, make_shader(GL_VERTEX_SHADER, vs_src));
glAttachShader(prog, make_shader(GL_FRAGMENT_SHADER, fs_src));
glBindAttribLocation(prog, 0, "aPos");
glBindAttribLocation(prog, 1, "aColor");
glBindAttribLocation(prog, 2, "aUv");
glBindAttribLocation(prog, 3, "aLight");
glLinkProgram(prog);
GLint ok = 0;
glGetProgramiv(prog, GL_LINK_STATUS, &ok);
if (!ok) {
bench_gl_failed("program link", "");
return 0;
}
return prog;
}
static const char* kChunkVs =
"#version 150 core\n"
"in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n"
"uniform mat4 uMvp; uniform vec3 uOffset;\n"
"out vec4 vColor; out vec2 vUv; out vec2 vLight;\n"
"void main(){ gl_Position = uMvp * vec4(aPos + uOffset, 1.0);\n"
" vColor = aColor; vUv = aUv; vLight = aLight * (1.0/256.0); }\n";
static const char* kChunkFs =
"#version 150 core\n"
"in vec4 vColor; in vec2 vUv; in vec2 vLight; out vec4 o;\n"
"uniform sampler2D uAtlas; uniform sampler2D uLight;\n"
"void main(){ o = texture(uAtlas, vUv) * vColor * texture(uLight, vLight); }\n";
static const char* kEntityVs =
"#version 150 core\n"
"in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n"
"uniform mat4 uMvp; uniform mat4 uModel;\n"
"out vec4 vColor; out vec2 vUv;\n"
"void main(){ gl_Position = uMvp * uModel * vec4(aPos, 1.0); vColor = aColor; vUv = aUv; }\n";
static const char* kEntityFs =
"#version 150 core\n"
"in vec4 vColor; in vec2 vUv; out vec4 o; uniform sampler2D uTex;\n"
"void main(){ o = texture(uTex, vUv) * vColor; }\n";
// ESSL 3.20 twins of the four shaders above. The bodies are identical; only the
// version line and the precision qualifiers differ, so the two paths compile the
// same work. Needed because this bench also runs against a device's native GLES
// driver as the baseline MobileGL is measured against, and that driver rejects
// desktop GLSL - while MobileGL is fed desktop GLSL on purpose, since translating
// it is the thing under test.
static const char* kChunkVsEs =
"#version 320 es\n"
"precision highp float;\n"
"in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n"
"uniform mat4 uMvp; uniform vec3 uOffset;\n"
"out vec4 vColor; out vec2 vUv; out vec2 vLight;\n"
"void main(){ gl_Position = uMvp * vec4(aPos + uOffset, 1.0);\n"
" vColor = aColor; vUv = aUv; vLight = aLight * (1.0/256.0); }\n";
static const char* kChunkFsEs =
"#version 320 es\n"
"precision mediump float;\n"
"in vec4 vColor; in vec2 vUv; in vec2 vLight; out vec4 o;\n"
"uniform sampler2D uAtlas; uniform sampler2D uLight;\n"
"void main(){ o = texture(uAtlas, vUv) * vColor * texture(uLight, vLight); }\n";
static const char* kEntityVsEs =
"#version 320 es\n"
"precision highp float;\n"
"in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n"
"uniform mat4 uMvp; uniform mat4 uModel;\n"
"out vec4 vColor; out vec2 vUv;\n"
"void main(){ gl_Position = uMvp * uModel * vec4(aPos, 1.0); vColor = aColor; vUv = aUv; }\n";
static const char* kEntityFsEs =
"#version 320 es\n"
"precision mediump float;\n"
"in vec4 vColor; in vec2 vUv; out vec4 o; uniform sampler2D uTex;\n"
"void main(){ o = texture(uTex, vUv) * vColor; }\n";
// True once build_resources() has seen a GL_VERSION beginning with "OpenGL ES".
static int g_isGlesContext = 0;
static void setup_vao(GLuint vao, GLuint vbo, GLuint ibo) {
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
glEnableVertexAttribArray(3);
glVertexAttribPointer(0, 3, GL_FLOAT, 0, VERT_STRIDE, (void*)0);
glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, 1, VERT_STRIDE, (void*)12);
glVertexAttribPointer(2, 2, GL_FLOAT, 0, VERT_STRIDE, (void*)16);
glVertexAttribPointer(3, 2, GL_SHORT, 0, VERT_STRIDE, (void*)24);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
}
static GLuint g_mainFbo;
static void build_resources(void) {
/* offscreen render target: 1280x720 RBO FBO, like CTS fbo surface mode */
GLuint fbo, rboColor, rboDepth;
glGenFramebuffers(1, &fbo);
g_mainFbo = fbo;
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glGenRenderbuffers(1, &rboColor);
glBindRenderbuffer(GL_RENDERBUFFER, rboColor);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1280, 720);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rboColor);
glGenRenderbuffers(1, &rboDepth);
glBindRenderbuffer(GL_RENDERBUFFER, rboDepth);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 1280, 720);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
bench_gl_failed("FBO incomplete", "");
return;
}
const char* versionString = (const char*)glGetString(GL_VERSION);
g_isGlesContext = versionString != NULL && strncmp(versionString, "OpenGL ES", 9) == 0;
g_progChunk = g_isGlesContext ? make_program(kChunkVsEs, kChunkFsEs) : make_program(kChunkVs, kChunkFs);
g_progEntity = g_isGlesContext ? make_program(kEntityVsEs, kEntityFsEs) : make_program(kEntityVs, kEntityFs);
glUseProgram(g_progChunk);
g_uMvpChunk = glGetUniformLocation(g_progChunk, "uMvp");
g_uOffsetChunk = glGetUniformLocation(g_progChunk, "uOffset");
glUniform1i(glGetUniformLocation(g_progChunk, "uAtlas"), 0);
glUniform1i(glGetUniformLocation(g_progChunk, "uLight"), 2);
glUniformMatrix4fv(g_uMvpChunk, 1, 0, g_mvp);
glUseProgram(g_progEntity);
g_uMvpEntity = glGetUniformLocation(g_progEntity, "uMvp");
glUniform1i(glGetUniformLocation(g_progEntity, "uTex"), 0);
glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp);
glUseProgram(g_progChunk);
/* shared quad index buffer, like Blaze3D's RenderSystem shared sequences */
int maxQuads = 4096;
unsigned* idx = (unsigned*)malloc((size_t)maxQuads * 6 * 4);
for (int q = 0; q < maxQuads; ++q) {
unsigned base = q * 4;
unsigned* p = idx + q * 6;
p[0] = base; p[1] = base + 1; p[2] = base + 2;
p[3] = base + 2; p[4] = base + 3; p[5] = base;
}
glGenBuffers(1, &g_sharedIbo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_sharedIbo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, maxQuads * 6 * 4, idx, GL_STATIC_DRAW);
free(idx);
g_scratch = (unsigned char*)malloc(4 * 1024 * 1024);
memset(g_scratch, 0x5a, 4 * 1024 * 1024);
glGenVertexArrays(MAX_SECTIONS, g_vao);
glGenBuffers(MAX_SECTIONS, g_vbo);
int bytes = g_quadsPerSection * 4 * VERT_STRIDE;
for (int i = 0; i < MAX_SECTIONS; ++i) {
fill_section_vertices(g_scratch, g_quadsPerSection, i * 7919u + 1);
glBindBuffer(GL_ARRAY_BUFFER, g_vbo[i]);
glBufferData(GL_ARRAY_BUFFER, bytes, g_scratch, GL_STATIC_DRAW);
setup_vao(g_vao[i], g_vbo[i], g_sharedIbo);
}
glGenTextures(1, &g_texAtlas);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1024, 512, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glGenerateMipmap(GL_TEXTURE_2D);
glGenTextures(1, &g_texLight);
glActiveTexture(GL_TEXTURE0 + 2);
glBindTexture(GL_TEXTURE_2D, g_texLight);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glGenTextures(1, &g_texEntity);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, g_texEntity);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
// Uniform ring the 26.2-style case sub-ranges into, sized like a real
// frame's worth of per-draw uniform slots.
GLint align = 256;
glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &align);
g_uboAlign = align > 0 ? align : 256;
g_uboSlot = (size_t)g_uboAlign;
glGenBuffers(1, &g_uboRing);
glBindBuffer(GL_UNIFORM_BUFFER, g_uboRing);
glBufferData(GL_UNIFORM_BUFFER, 4 * 1024 * 1024, g_scratch, GL_DYNAMIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
for (int i = 0; i < 2; ++i) {
glGenFramebuffers(1, &g_passFbo[i]);
glBindFramebuffer(GL_FRAMEBUFFER, g_passFbo[i]);
glGenRenderbuffers(1, &g_passColor[i]);
glBindRenderbuffer(GL_RENDERBUFFER, g_passColor[i]);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 256, 256);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, g_passColor[i]);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
bench_gl_failed("pass FBO incomplete", "");
return;
}
}
/* back to the main offscreen target the harness set up */
glBindFramebuffer(GL_FRAMEBUFFER, g_mainFbo);
glGenSamplers(1, &g_sampler);
glSamplerParameteri(g_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glSamplerParameteri(g_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glEnable(GL_DEPTH_TEST);
glClearColor(0.3f, 0.5f, 0.9f, 1.0f);
glViewport(0, 0, 1280, 720);
const GLenum setupError = glGetError();
if (setupError != GL_NO_ERROR) {
char message[64];
snprintf(message, sizeof message, "0x%04x", setupError);
bench_gl_failed("GL error during resource setup", message);
}
}
static void case_draw_tiny(int frame, long a, long b) {
(void)frame; (void)b;
glBindVertexArray(g_vao[0]);
for (long i = 0; i < a; ++i) glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
static void case_draw_uniform(int frame, long a, long b) {
(void)frame; (void)b;
glBindVertexArray(g_vao[0]);
for (long i = 0; i < a; ++i) {
glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
}
static void case_draw_multi_vao(int frame, long a, long b) {
(void)frame; (void)b;
for (long i = 0; i < a; ++i) {
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
}
static void case_tex_pingpong(int frame, long a, long b) {
(void)frame; (void)b;
glBindVertexArray(g_vao[0]);
for (long i = 0; i < a; ++i) {
glBindTexture(GL_TEXTURE_2D, (i & 1) ? g_texEntity : g_texAtlas);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
}
static void case_program_pingpong(int frame, long a, long b) {
(void)frame; (void)b;
glBindVertexArray(g_vao[0]);
for (long i = 0; i < a; ++i) {
if (i & 1) {
glUseProgram(g_progEntity);
glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp);
} else {
glUseProgram(g_progChunk);
glUniform3f(g_uOffsetChunk, (float)(i & 15), 0.0f, 0.0f);
}
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
glUseProgram(g_progChunk);
}
/* a = uploads per frame, b = bytes per upload (0 => section size) */
static void case_chunk_upload(int frame, long a, long b) {
if (b <= 0) b = g_quadsPerSection * 4 * VERT_STRIDE;
if (b > 4 * 1024 * 1024) b = 4 * 1024 * 1024;
for (long i = 0; i < a; ++i) {
int slot = (int)(((long)frame * a + i) % MAX_SECTIONS);
glBindBuffer(GL_ARRAY_BUFFER, g_vbo[slot]);
glBufferData(GL_ARRAY_BUFFER, b, NULL, GL_STATIC_DRAW); /* orphan */
glBufferSubData(GL_ARRAY_BUFFER, 0, b, g_scratch);
glBindVertexArray(g_vao[slot]);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
}
/* a = sprite updates per frame */
static void case_atlas_sprite(int frame, long a, long b) {
(void)b;
glBindVertexArray(g_vao[0]);
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
for (long i = 0; i < a; ++i) {
int x = (int)((frame * 13 + i * 17) % (1024 - 16));
int y = (int)((frame * 7 + i * 29) % (512 - 16));
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
}
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
/* a = lightmap updates (+draw) per frame */
static void case_lightmap(int frame, long a, long b) {
(void)frame; (void)b;
glBindVertexArray(g_vao[0]);
for (long i = 0; i < a; ++i) {
glActiveTexture(GL_TEXTURE0 + 2);
glBindTexture(GL_TEXTURE_2D, g_texLight);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
glActiveTexture(GL_TEXTURE0);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
}
/* Composite: a = total draws, b = uploads per frame. Mix modeled on trace
* analysis: chunk draws with per-draw offset uniform across sections, 10%
* entity-style program flips, per-frame lightmap + sprite updates, b chunk
* re-uploads. */
static long g_mixSprites = 8;
static void case_scene_mix(int frame, long a, long b) {
glActiveTexture(GL_TEXTURE0 + 2);
glBindTexture(GL_TEXTURE_2D, g_texLight);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
for (long i = 0; i < g_mixSprites; ++i) {
int x = (int)((frame * 13 + i * 17) % (1024 - 16));
int y = (int)((frame * 7 + i * 29) % (512 - 16));
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
}
for (long i = 0; i < b; ++i) {
int slot = (int)(((long)frame * b + i) % MAX_SECTIONS);
long bytes = g_quadsPerSection * 4 * VERT_STRIDE;
glBindBuffer(GL_ARRAY_BUFFER, g_vbo[slot]);
glBufferData(GL_ARRAY_BUFFER, bytes, NULL, GL_STATIC_DRAW);
glBufferSubData(GL_ARRAY_BUFFER, 0, bytes, g_scratch);
}
long entityEvery = 10;
for (long i = 0; i < a; ++i) {
if (i % entityEvery == entityEvery - 1) {
glUseProgram(g_progEntity);
glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp);
glBindTexture(GL_TEXTURE_2D, g_texEntity);
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
glUseProgram(g_progChunk);
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
} else {
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
}
}
/* ---- Trace-derived cases -------------------------------------------------
* Per-frame call mixes measured from the three captured Minecraft traces
* (render distance 32, 1280x720, hovering in-world). Each case reproduces one
* renderer's dominant per-draw sequence at its measured rate, so the number a
* backend posts here is directly comparable to what that game version asks of
* the driver every frame.
*
* vanilla 1.21.1 : 5495 glDrawElements, 5490 glBindVertexArray,
* 5487 glUniform3fv, 95 glTexSubImage2D (+382 glPixelStorei,
* 247 glTexParameteri), 23 glBufferData per frame
* fabric+sodium : 132 glMultiDrawElementsBaseVertex, 279 glBindVertexArray,
* 132 glUniform3f, 32 glBufferData per frame
* 26.2 snapshot : 3401 glDrawElementsBaseVertex, each preceded by
* glBindBufferRange + glBindBuffer (3639/3412 per frame)
*/
/* vanilla: bind VAO, push the chunk offset, draw. a = draws per frame. */
static void case_mc_vanilla_draw(int frame, long a, long b) {
(void)frame; (void)b;
float offset[3];
for (long i = 0; i < a; ++i) {
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
offset[0] = (float)(i & 15);
offset[1] = (float)((i >> 4) & 15);
offset[2] = 0.0f;
glUniform3fv(g_uOffsetChunk, 1, offset);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
}
/* sodium: one multi-draw covers many chunk sections out of a shared buffer.
* a = multi-draws per frame, b = sub-draws inside each. */
static void case_mc_sodium_multidraw(int frame, long a, long b) {
(void)frame;
enum { kMaxSub = 64 };
if (b <= 0 || b > kMaxSub) b = 32;
GLsizei counts[kMaxSub];
const void* offsets[kMaxSub];
GLint baseVertices[kMaxSub];
for (long s = 0; s < b; ++s) {
counts[s] = (GLsizei)(g_quadsPerSection * 6 / b);
offsets[s] = (const void*)(uintptr_t)(s * (g_quadsPerSection * 6 / b) * 4);
baseVertices[s] = 0;
}
for (long i = 0; i < a; ++i) {
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
glBindVertexArray(g_vao[i % MAX_SECTIONS]); /* sodium rebinds ~2x per draw */
glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f);
// Routed through the includer: GLES has no multi-draw-with-base-vertex, so
// a native-driver harness emulates it with the loop the extension folds up.
bench_multi_draw_elements_base_vertex(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets,
(GLsizei)b, baseVertices);
}
}
/* 26.2: every draw rebinds a fresh uniform-buffer range out of a ring.
* a = draws per frame. */
static void case_mc_ubo_range(int frame, long a, long b) {
(void)b;
const size_t slots = (4u * 1024u * 1024u) / g_uboSlot;
for (long i = 0; i < a; ++i) {
const size_t slot = (size_t)(((long)frame * a + i) % (long)slots);
glBindBufferRange(GL_UNIFORM_BUFFER, 0, g_uboRing, (GLintptr)(slot * g_uboSlot),
(GLsizeiptr)g_uboSlot);
glBindBuffer(GL_UNIFORM_BUFFER, g_uboRing);
glDrawElementsBaseVertex(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0, 0);
}
}
/* vanilla's animated-sprite path: every upload is wrapped in the pixel-store
* and filter state Blaze3D re-sets around it. a = uploads per frame. */
static void case_mc_tex_stream(int frame, long a, long b) {
(void)b;
glBindVertexArray(g_vao[0]);
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
for (long i = 0; i < a; ++i) {
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
int x = (int)((frame * 13 + i * 17) % (1024 - 16));
int y = (int)((frame * 7 + i * 29) % (512 - 16));
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
}
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
/* Blaze3D re-resolves uniform locations by name every frame. a = lookups. */
static void case_mc_uniform_lookup(int frame, long a, long b) {
(void)frame; (void)b;
static const char* names[4] = {"uMvp", "uOffset", "uAtlas", "uLight"};
volatile GLint sink = 0;
for (long i = 0; i < a; ++i) sink += glGetUniformLocation(g_progChunk, names[i & 3]);
(void)sink;
glBindVertexArray(g_vao[0]);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
/* 26.2 rebinds a sampler object per texture unit switch. a = switches. */
static void case_mc_sampler_churn(int frame, long a, long b) {
(void)frame; (void)b;
glBindVertexArray(g_vao[0]);
for (long i = 0; i < a; ++i) {
glActiveTexture(GL_TEXTURE0 + (GLenum)(i & 3));
glBindTexture(GL_TEXTURE_2D, (i & 1) ? g_texEntity : g_texAtlas);
glBindSampler((GLuint)(i & 3), g_sampler);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
glActiveTexture(GL_TEXTURE0);
}
/* 26.2 switches render targets constantly: 132 glBindFramebuffer and 198
* glDrawBuffers per frame. Pass switching is where a Vulkan backend pays for
* render-pass breaks, so this case is the one to watch on Magma. a = passes. */
static void case_mc_pass_switch(int frame, long a, long b) {
(void)frame; (void)b;
static const GLenum kColor0[1] = {GL_COLOR_ATTACHMENT0};
glBindVertexArray(g_vao[0]);
for (long i = 0; i < a; ++i) {
glBindFramebuffer(GL_FRAMEBUFFER, g_passFbo[i & 1]);
glDrawBuffers(1, kColor0);
glViewport(0, 0, 256, 256);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
glBindFramebuffer(GL_FRAMEBUFFER, g_mainFbo);
glViewport(0, 0, 1280, 720);
}
/* Blaze3D toggles blend around batches: 46 glEnable/glDisable pairs and 28
* glBlendFuncSeparate per vanilla frame. a = toggle pairs. */
static void case_mc_state_toggle(int frame, long a, long b) {
(void)frame; (void)b;
glBindVertexArray(g_vao[0]);
for (long i = 0; i < a; ++i) {
glEnable(GL_BLEND);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
glDisable(GL_BLEND);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
}
/* 26.2 re-sets texture parameters relentlessly - 612 glTexParameteri per frame,
* almost always to the value already in place. Measures redundant-param
* filtering. a = parameter writes. */
static void case_mc_tex_param(int frame, long a, long b) {
(void)frame; (void)b;
glBindVertexArray(g_vao[0]);
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
for (long i = 0; i < a; i += 4) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
}
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
/* Sodium switches programs mid-frame far more than vanilla: 62 glUseProgram and
* 60 mat4 uploads per frame. a = program switches. */
static void case_mc_use_program(int frame, long a, long b) {
(void)frame; (void)b;
glBindVertexArray(g_vao[0]);
for (long i = 0; i < a; ++i) {
if (i & 1) {
glUseProgram(g_progEntity);
glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp);
} else {
glUseProgram(g_progChunk);
glUniformMatrix4fv(g_uMvpChunk, 1, 0, g_mvp);
}
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
glUseProgram(g_progChunk);
}
/* ---- the case table both harnesses iterate --------------------------------
* a/b are the case's own knobs; opsPerFrame is what one bench frame is
* normalised by, so ns_per_op compares across renderers. The mc_* rates are
* the per-frame call counts measured from the captured traces.
*/
typedef void (*bench_case_fn)(int frame, long a, long b);
typedef struct {
const char* name;
bench_case_fn fn;
long a, b, opsPerFrame;
} BenchCaseDesc;
static const BenchCaseDesc kBenchCases[] = {
{"mc_vanilla_draw", case_mc_vanilla_draw, 5495, 0, 5495},
{"mc_sodium_multidraw", case_mc_sodium_multidraw, 132, 32, 132},
{"mc_ubo_range", case_mc_ubo_range, 3401, 0, 3401},
{"mc_tex_stream", case_mc_tex_stream, 95, 0, 95},
{"mc_uniform_lookup", case_mc_uniform_lookup, 41, 0, 41},
{"mc_sampler_churn", case_mc_sampler_churn, 306, 0, 306},
{"mc_pass_switch", case_mc_pass_switch, 132, 0, 132},
{"mc_state_toggle", case_mc_state_toggle, 46, 0, 46},
{"mc_tex_param", case_mc_tex_param, 612, 0, 612},
{"mc_use_program", case_mc_use_program, 62, 0, 62},
{"draw_tiny", case_draw_tiny, 2048, 0, 2048},
{"draw_uniform", case_draw_uniform, 2048, 0, 2048},
{"draw_multi_vao", case_draw_multi_vao, 2048, 0, 2048},
{"tex_pingpong", case_tex_pingpong, 1024, 0, 1024},
{"program_pingpong", case_program_pingpong, 512, 0, 512},
{"chunk_upload", case_chunk_upload, 24, 0, 24},
{"atlas_sprite", case_atlas_sprite, 32, 0, 32},
{"lightmap", case_lightmap, 4, 0, 4},
{"scene_mix", case_scene_mix, 2048, 12, 2048},
};
static const int kBenchCaseCount = (int)(sizeof kBenchCases / sizeof kBenchCases[0]);
@@ -0,0 +1,41 @@
#!/bin/bash
# Run the headless EGL DriverBench on one renderer:
# ./run_driver_bench.sh native [bench args...]
# ./run_driver_bench.sh espryt <libMobileGL.so> [bench args...]
# ./run_driver_bench.sh magma <libMobileGL.so> [bench args...]
# The bench dlopens exactly one EGL provider (DRIVERBENCH_EGL_LIB): the system
# libEGL.so.1 for native, or the given libMobileGL.so for a MobileGL backend -
# no LD_LIBRARY_PATH shadowing, so MobileGL's own loader still finds the real
# driver underneath.
#
# Pin the vendor libraries explicitly. A bare libEGL.so.1 on a glvnd system
# picks whatever vendor eglGetDisplay(EGL_DEFAULT_DISPLAY) resolves first,
# which is Mesa/llvmpipe here - a software rasteriser silently replacing the
# GPU under a benchmark. Override MGL_EGL_VENDOR / MGL_VK_ICD to test another
# driver.
set -eu
HERE=$(cd "$(dirname "$0")" && pwd)
BENCH=${DRIVERBENCH_BIN:-$HERE/DriverBench}
EGL_VENDOR=${MGL_EGL_VENDOR:-/usr/share/glvnd/egl_vendor.d/10_nvidia.json}
VK_ICD=${MGL_VK_ICD:-/usr/share/vulkan/icd.d/nvidia_icd.x86_64.json}
MODE=$1; shift
export __EGL_VENDOR_LIBRARY_FILENAMES=$EGL_VENDOR
export EGL_PLATFORM=${EGL_PLATFORM:-x11}
case "$MODE" in
native)
export DRIVERBENCH_EGL_LIB=${DRIVERBENCH_EGL_LIB:-libEGL.so.1}
;;
espryt)
export DRIVERBENCH_EGL_LIB=$(readlink -f "$1"); shift
export MOBILEGL_BACKEND_TYPE=DirectGLES
;;
magma)
export DRIVERBENCH_EGL_LIB=$(readlink -f "$1"); shift
export MOBILEGL_BACKEND_TYPE=DirectVulkan
export VK_ICD_FILENAMES=$VK_ICD
;;
*) echo "unknown mode: $MODE (native|espryt|magma)"; exit 1 ;;
esac
exec "$BENCH" "$@"
@@ -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
+33
View File
@@ -24,6 +24,7 @@ namespace MobileGL::MG_Impl::CGLImpl {
GLint Samples = 0;
GLint Profile = kCGLOGLPVersion_3_2_Core;
GLint RendererId = 0x4d474c;
GLint DisplayMask = 0;
};
struct ContextObject {
@@ -134,6 +135,9 @@ namespace MobileGL::MG_Impl::CGLImpl {
case kCGLPFARendererID:
pixelFormat.RendererId = value;
break;
case kCGLPFADisplayMask:
pixelFormat.DisplayMask = value;
break;
default:
break;
}
@@ -343,6 +347,9 @@ namespace MobileGL::MG_Impl::CGLImpl {
case kCGLPFARendererID:
*value = pixelFormat->RendererId;
return kCGLNoError;
case kCGLPFADisplayMask:
*value = pixelFormat->DisplayMask;
return kCGLNoError;
case kCGLPFAOpenGLProfile:
*value = pixelFormat->Profile;
return kCGLNoError;
@@ -481,6 +488,32 @@ namespace MobileGL::MG_Impl::CGLImpl {
return it == currentContexts.end() ? nullptr : it->second;
}
CGLError SetVirtualScreen(CGLContextObj ctx, GLint screen) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (!object) {
return kCGLBadContext;
}
if (screen != 0) {
return kCGLBadValue;
}
object->VirtualScreen = screen;
return kCGLNoError;
}
CGLError GetVirtualScreen(CGLContextObj ctx, GLint* screen) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (!object) {
return kCGLBadContext;
}
if (!screen) {
return kCGLBadAddress;
}
*screen = object->VirtualScreen;
return kCGLNoError;
}
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
+2
View File
@@ -32,6 +32,8 @@ namespace MobileGL::MG_Impl::CGLImpl {
CGLError SetCurrentContext(CGLContextObj ctx);
CGLContextObj GetCurrentContext();
CGLError SetVirtualScreen(CGLContextObj ctx, GLint screen);
CGLError GetVirtualScreen(CGLContextObj ctx, GLint* screen);
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params);
CGLError GetParameter(CGLContextObj ctx, CGLContextParameter pname, GLint* params);
CGLError UpdateContext(CGLContextObj ctx);
@@ -71,6 +71,14 @@ MOBILEGL_CGL_API CGLContextObj CGLGetCurrentContext(void) {
return MobileGL::MG_Impl::CGLImpl::GetCurrentContext();
}
MOBILEGL_CGL_API CGLError CGLSetVirtualScreen(CGLContextObj ctx, GLint screen) {
return MobileGL::MG_Impl::CGLImpl::SetVirtualScreen(ctx, screen);
}
MOBILEGL_CGL_API CGLError CGLGetVirtualScreen(CGLContextObj ctx, GLint* screen) {
return MobileGL::MG_Impl::CGLImpl::GetVirtualScreen(ctx, screen);
}
MOBILEGL_CGL_API CGLError CGLSetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
return MobileGL::MG_Impl::CGLImpl::SetParameter(ctx, pname, params);
}
@@ -10,8 +10,12 @@
#if defined(__APPLE__)
#include "MG_Impl/CGLImpl/CGLImpl.h"
#include "MG_Impl/GetProcAddress.h"
#include <CoreGraphics/CoreGraphics.h>
#include <CoreVideo/CVDisplayLink.h>
#include <cstdint>
#include <dlfcn.h>
namespace {
@@ -47,10 +51,52 @@ namespace {
return dlsym(handle, symbol);
}
CGDirectDisplayID DisplayForMask(GLint displayMask) {
constexpr std::uint32_t MaxDisplays = sizeof(CGOpenGLDisplayMask) * 8;
CGDirectDisplayID displays[MaxDisplays] = {};
std::uint32_t displayCount = 0;
if (displayMask != 0 &&
CGGetActiveDisplayList(MaxDisplays, displays, &displayCount) == kCGErrorSuccess) {
const auto mask = static_cast<CGOpenGLDisplayMask>(displayMask);
for (std::uint32_t i = 0; i < displayCount; ++i) {
if ((CGDisplayIDToOpenGLDisplayMask(displays[i]) & mask) != 0) {
return displays[i];
}
}
}
return CGMainDisplayID();
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
CVReturn MobileGLCVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(
CVDisplayLinkRef displayLink,
CGLContextObj context,
CGLPixelFormatObj pixelFormat) {
GLint virtualScreen = 0;
if (MobileGL::MG_Impl::CGLImpl::GetVirtualScreen(context, &virtualScreen) == kCGLNoError) {
GLint displayMask = 0;
if (!displayLink ||
MobileGL::MG_Impl::CGLImpl::DescribePixelFormat(
pixelFormat, virtualScreen, kCGLPFADisplayMask, &displayMask) != kCGLNoError) {
return kCVReturnInvalidArgument;
}
return CVDisplayLinkSetCurrentCGDisplay(displayLink, DisplayForMask(displayMask));
}
using OriginalFunction = CVReturn (*)(CVDisplayLinkRef, CGLContextObj, CGLPixelFormatObj);
static const auto original = reinterpret_cast<OriginalFunction>(
dlsym(RTLD_NEXT, "CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext"));
return original ? original(displayLink, context, pixelFormat) : kCVReturnError;
}
__attribute__((used)) static const DyldInterposeEntry kMobileGLDyldInterpose[]
__attribute__((section("__DATA,__interpose"))) = {
{reinterpret_cast<const void*>(MobileGLDlsym), reinterpret_cast<const void*>(dlsym)},
{reinterpret_cast<const void*>(MobileGLCVDisplayLinkSetCurrentCGDisplayFromOpenGLContext),
reinterpret_cast<const void*>(CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext)},
};
#pragma clang diagnostic pop
} // namespace
#endif
@@ -0,0 +1,10 @@
# Public CGL entry points.
_CGL*
# Public EGL entry points.
_egl*
# Public OpenGL and GLX entry points. OpenGL function names always use an
# uppercase letter or digit after the "gl" prefix; excluding lowercase here
# deliberately prevents glslang_* from matching this pattern.
_gl[A-Z0-9]*
+39 -16
View File
@@ -8,6 +8,7 @@
#include "EGLImpl.h"
#include "../GetProcAddress.h"
#include <Init.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_State/EGLState/Core.h>
#include <mutex>
@@ -20,11 +21,22 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLStateContext* GetState() {
if (!MG_State::pEGLContext) {
MGLOG_E("pEGLContext is null. MG_State may not be initialized.");
MGLOG_E_ONCE("pEGLContext is null. MG_State may not be initialized.");
}
return MG_State::pEGLContext.get();
}
// Entry points that can legitimately be an application's FIRST EGL
// call (display/proc-address/string queries) lazily bring MobileGL
// up here, so the library needs no static constructor and can
// re-initialize after the last eglTerminate tore everything down.
// Teardown-ish entry points keep using GetState() and fail benignly
// when MobileGL is not initialized.
EGLStateContext* GetStateEnsureInitialized() {
MobileGL::EnsureInitialized();
return GetState();
}
MG_Backend::BackendObject* GetBackendObject(EGLStateContext* state) {
auto* backendObject = MG_Backend::pActiveBackendObject.get();
if (!backendObject && state) {
@@ -49,6 +61,8 @@ namespace MobileGL::MG_Impl::EGLImpl {
return MG_Backend::WindowBackend::Android;
#elif defined(__APPLE__)
return MG_Backend::WindowBackend::MetalLayer;
#elif defined(_WIN32)
return MG_Backend::WindowBackend::Win32;
#elif defined(__linux__)
return MG_Backend::WindowBackend::X11;
#else
@@ -132,7 +146,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
state->DestroySurface(dpy, surface);
return EGL_NO_SURFACE;
}
@@ -158,11 +172,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
return EGL_FALSE;
}
if (!backendObject->SwapEGLBuffers(dpy, draw)) {
MGLOG_E("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
MGLOG_E_ONCE("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
state->SetError(EGL_BAD_SURFACE);
return EGL_FALSE;
}
@@ -187,7 +201,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_FALSE;
}
@@ -197,7 +211,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
return EGL_FALSE;
}
if (!backendObject->InitializeEGLDisplay(dpy, major, minor)) {
@@ -208,7 +222,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLDisplay GetDisplay(NativeDisplayType display) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_NO_DISPLAY;
}
@@ -251,7 +265,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (releaseCurrentRequest) {
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
MGLOG_E("eglMakeCurrent release failed in backend thread=%s", threadId.c_str());
MGLOG_E_ONCE("eglMakeCurrent release failed in backend thread=%s", threadId.c_str());
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
state->SetError(EGL_BAD_ACCESS);
return EGL_FALSE;
@@ -263,12 +277,12 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
return EGL_FALSE;
}
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
MGLOG_E("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
MGLOG_E_ONCE("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
dpy, draw, read, ctx);
state->SetError(EGL_BAD_ACCESS);
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
@@ -313,6 +327,14 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
backendObject->ReleaseEGLResources();
}
// The last initialized display is gone and nothing is current on any
// thread: tear the whole library down deterministically inside the
// EGL lifecycle (backend, GL/EGL state, glslang). A later EGL call
// re-initializes lazily via GetStateEnsureInitialized(); process exit
// then has nothing left to destroy.
if (!state->HasAnyInitializedDisplay() && !state->HasAnyCurrentContext()) {
MobileGL::Destroy();
}
return EGL_TRUE;
}
@@ -345,7 +367,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLBoolean BindAPI(EGLenum api) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_FALSE;
}
@@ -378,7 +400,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
char const* QueryString(EGLDisplay display, EGLint name) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return nullptr;
}
@@ -641,7 +663,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) {
(void)attrib_list;
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_NO_DISPLAY;
}
@@ -681,7 +703,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
state->DestroySurface(dpy, surface);
return EGL_NO_SURFACE;
}
@@ -704,7 +726,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
return EGL_FALSE;
}
width = std::max<EGLint>(width, 1);
@@ -737,11 +759,12 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!name) {
return nullptr;
}
MobileGL::EnsureInitialized();
MGLOG_D("eglGetProcAddress(%s)", name);
void* proc = MG_Impl::GetProcAddress(name);
if (!proc) {
MGLOG_W("Failed to get function: %s", name);
MGLOG_D("Failed to get function: %s", name);
return nullptr;
}
return (__eglMustCastToProperFunctionPointerType)proc;
+341 -52
View File
@@ -8,12 +8,17 @@
#include "GL_Buffer.h"
#include "Validators.h"
#include "../Texture/GL_Texture.h"
#include "../Getter/GL_Getter.h"
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Metrics/TextureMetrics.h>
#include <Config.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/Error.h>
#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 {
@@ -27,6 +32,8 @@ namespace MobileGL::MG_Impl::GLImpl {
NamedBufferData,
NamedBufferSubData,
CopyNamedBufferSubData,
ClearBufferData,
ClearBufferSubData,
ClearNamedBufferData,
ClearNamedBufferSubData,
MapBufferRange,
@@ -38,6 +45,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GetNamedBufferParameteriv,
GetNamedBufferParameteri64v,
GetNamedBufferPointerv,
GetNamedBufferSubData,
};
const char* GetBufferOpName(BufferOp op) {
@@ -60,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:
@@ -76,6 +88,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return "UnmapNamedBuffer";
case BufferOp::FlushMappedNamedBufferRange:
return "FlushMappedNamedBufferRange";
case BufferOp::GetNamedBufferSubData:
return "GetNamedBufferSubData";
case BufferOp::GetNamedBufferParameteriv:
return "GetNamedBufferParameteriv";
case BufferOp::GetNamedBufferParameteri64v:
@@ -89,25 +103,54 @@ namespace MobileGL::MG_Impl::GLImpl {
SharedPtr<MG_State::GLState::BufferObject> GetNamedBufferObject(GLuint buffer, BufferOp op);
// The size of one cleared element, which is what offset and size must be multiples of
// (GL 4.6 core 6.3). `internalformat` is restricted to the buffer-texture format table, and
// `format`/`type` describe the client-side pattern, so both are validated here and the
// caller only has to know how wide an element is.
SizeT GetClearPatternSize(GLenum internalformat, GLenum format, GLenum type, BufferOp op) {
if (format != GL_RED_INTEGER) {
if (!IsBufferTextureInternalFormat(internalformat)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
"Only GL_RED_INTEGER buffer clears are currently supported."));
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", GetBufferOpName(op),
std::format("internalformat 0x{:X} is not one of the sized formats a buffer clear accepts.",
internalformat)));
return 0;
}
if (internalformat == GL_R8UI && type == GL_UNSIGNED_BYTE) return sizeof(GLubyte);
if (internalformat == GL_R32UI && type == GL_UNSIGNED_INT) return sizeof(GLuint);
// Unlike internalformat, a bad format or type here is INVALID_VALUE rather than
// INVALID_ENUM (GL 4.6 core 6.3) - the odd one out among the enum arguments.
const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
if (inputFormat == TextureInputFormat::Unknown) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
std::format("format 0x{:X} is not a pixel format.", format)));
return 0;
}
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
std::format("Unsupported clear format tuple: internalformat=0x{:X}, "
"format=0x{:X}, type=0x{:X}",
internalformat, format, type)));
return 0;
const TexturePixelDataType pixelType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
if (pixelType == TexturePixelDataType::Unknown) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
std::format("type 0x{:X} is not a pixel type.", type)));
return 0;
}
const TextureInternalFormat internal =
MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
const SizeT elementSize = MG_Util::GetSizedInternalFormatSizeInBytes(internal);
if (elementSize == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
std::format("internalformat 0x{:X} has no known element size.",
internalformat)));
return 0;
}
return elementSize;
}
Bool ValidateBufferClearRange(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject, GLintptr offset,
@@ -148,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) {
@@ -330,12 +405,21 @@ namespace MobileGL::MG_Impl::GLImpl {
} else if (access & BufferMappingAccessBit::Write) {
*params = GL_WRITE_ONLY;
} else {
*params = 0;
*params = GL_READ_WRITE;
}
} else {
*params = 0;
// Initial value, and what glUnmapBuffer restores (GL 4.6 core table 6.2).
*params = GL_READ_WRITE;
}
break;
case GL_BUFFER_ACCESS_FLAGS:
// The MapBufferRange flags verbatim; glMapBuffer's access enum has already been
// normalised into the same bits. Zero while the buffer is not mapped.
*params = bufferObject->IsMapped()
? static_cast<GLint>(
MG_Util::ConvertBufferMappingAccessToGLEnum(bufferObject->GetMappingAccess()))
: 0;
break;
case GL_BUFFER_MAPPED:
*params = bufferObject->IsMapped() ? GL_TRUE : GL_FALSE;
break;
@@ -807,6 +891,10 @@ namespace MobileGL::MG_Impl::GLImpl {
Range1D mappedRange = bufferObject->GetMappedRange();
auto mappingAccess = bufferObject->GetMappingAccess();
// GL 4.6 6.5: the error is on OVERLAP with the mapped range, i.e. a half-open
// intersection test. There used to be a second test below this one asking only
// `offset + size >= mappedRange.start`, which rejects every write that starts
// before a mapped tail as well - it made a legal disjoint glBufferSubData fail.
if (bufferObject->IsMapped() && !(mappingAccess & BufferMappingAccessBit::Persistent) &&
(offset < mappedRange.end) && (offset + size > mappedRange.start)) {
MG_State::pGLContext->RecordError(
@@ -817,18 +905,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
if (bufferObject->IsMapped() && !(mappingAccess & BufferMappingAccessBit::Persistent)) {
Range1D mappedRange = bufferObject->GetMappedRange();
if (offset + size >= mappedRange.start) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BufferSubData_State",
"Cannot modify a mapped buffer object unless it was "
"mapped with GL_MAP_PERSISTENT_BIT."));
return;
}
}
bufferObject->UploadSubData({(void*)data, (SizeT)size}, offset);
}
@@ -878,6 +954,45 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
bufferObject->SyncGpuWrites();
bufferObject->DownloadSubData(data, static_cast<SizeT>(offset), static_cast<SizeT>(size));
}
void GetNamedBufferSubData_State(GLuint buffer, GLintptr offset, GLsizeiptr size, void* data) {
if (!data) {
// Match GetBufferSubData_State: a null pointer is a caller bug, not a GL-specified error.
return;
}
if (size < 0 || offset < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetNamedBufferSubData_State",
"Offset and size must be non-negative."));
return;
}
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::GetNamedBufferSubData);
if (!bufferObject) return;
if (static_cast<SizeT>(offset) + static_cast<SizeT>(size) > bufferObject->GetSize()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetNamedBufferSubData_State",
"Offset and size exceed buffer size."));
return;
}
if (bufferObject->IsMapped() &&
!(bufferObject->GetMappingAccess() & BufferMappingAccessBit::Persistent)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetNamedBufferSubData_State",
"Cannot read from a buffer object mapped without GL_MAP_PERSISTENT_BIT."));
return;
}
bufferObject->SyncGpuWrites();
bufferObject->DownloadSubData(data, static_cast<SizeT>(offset), static_cast<SizeT>(size));
}
@@ -920,6 +1035,11 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void BufferStorage_State(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) {
// Error precedence: "no buffer is bound to target" outranks a bad size or bad
// flags, so the binding has to be resolved before either is validated.
auto bufferObject = GetBoundBufferObject(target, BufferOp::BufferStorage);
if (!bufferObject) return;
if (size <= 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
@@ -928,8 +1048,6 @@ namespace MobileGL::MG_Impl::GLImpl {
}
if (!ValidateStorageFlags(flags, BufferOp::BufferStorage)) return;
auto bufferObject = GetBoundBufferObject(target, BufferOp::BufferStorage);
if (!bufferObject) return;
if (bufferObject->IsImmutableStorage()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -964,6 +1082,10 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void NamedBufferStorage_State(GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags) {
// Same precedence as BufferStorage_State: the buffer-name error comes first.
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::NamedBufferStorage);
if (!bufferObject) return;
if (size <= 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
@@ -972,8 +1094,6 @@ namespace MobileGL::MG_Impl::GLImpl {
}
if (!ValidateStorageFlags(flags, BufferOp::NamedBufferStorage)) return;
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::NamedBufferStorage);
if (!bufferObject) return;
if (bufferObject->IsImmutableStorage()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -1106,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) {
@@ -1351,6 +1488,14 @@ namespace MobileGL::MG_Impl::GLImpl {
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, pointIndex)) return;
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Transform feedback buffer bindings cannot change while transform "
"feedback is active."));
return;
}
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, pointIndex);
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, pointIndex);
@@ -1358,6 +1503,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (buffer == 0) {
point.Bind(nullptr);
point.SetRange(Range1D(0, 0));
GetBufferBindingSlot(bufferTarget).Bind(nullptr);
return;
}
@@ -1376,6 +1522,81 @@ namespace MobileGL::MG_Impl::GLImpl {
} else {
point.ClearRange();
}
// The indexed bind also binds to the generic binding point of the same target
// (GL 4.6 core 6.1.1). Callers rely on it: the texture_gather tests set up their
// SSBO with BindBufferBase and then size it through glBufferData on the generic
// target alone, which would otherwise raise GL_INVALID_OPERATION and leave the
// buffer with no storage.
GetBufferBindingSlot(bufferTarget).Bind(bufferObject);
}
// GL 4.6 core 6.1.1: the constraints glBindBufferRange puts on the (offset, size) pair.
// Every one of them is INVALID_VALUE, and all of them are checked before a single piece
// of state is written - a rejected bind must leave the binding point exactly as it was.
// They apply only to a non-zero buffer: buffer 0 detaches the binding point and ignores
// offset and size, which is also how glBindBuffersRange spells "reset this element"
// (a NULL buffers array, or a zero entry inside one).
static Bool ValidateBufferRangeOffsetAndSize(GLenum target, GLintptr offset, GLsizeiptr size,
const char* funcName, Bool hasBuffer = true) {
if (hasBuffer && size <= 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
std::format("size ({}) must be greater than zero.", size)));
return false;
}
if (offset < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
std::format("offset ({}) must not be negative.", offset)));
return false;
}
// GL_UNIFORM_BUFFER and GL_SHADER_STORAGE_BUFFER each constrain the offset to their own
// implementation-defined alignment, which glGetIntegerv already answers.
GLenum alignmentQuery = GL_NONE;
if (target == GL_SHADER_STORAGE_BUFFER) {
alignmentQuery = GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT;
} else if (target == GL_UNIFORM_BUFFER) {
alignmentQuery = GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT;
}
if (alignmentQuery != GL_NONE) {
GLint alignment = 0;
GetIntegerv(alignmentQuery, &alignment);
if (alignment > 0 && (offset % static_cast<GLintptr>(alignment)) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", funcName,
std::format("offset ({}) must be a multiple of {} ({}).", offset,
MG_Util::ConvertGLEnumToString(alignmentQuery), alignment)));
return false;
}
}
// GL 4.6 core 6.1.1 constrains the OFFSET to a multiple of four for both
// TRANSFORM_FEEDBACK_BUFFER and ATOMIC_COUNTER_BUFFER (the atomic-counter one has no
// queryable alignment pname, which is why it was missing here), and the SIZE only for
// transform feedback, whose capture is written in whole 32-bit components. Extending the
// size rule to atomic counters as well breaks a legal bind: the conformance suite splits
// MAX_ATOMIC_COUNTER_BUFFER_SIZE evenly across the binding points and that quotient is
// not required to land on four.
if ((target == GL_TRANSFORM_FEEDBACK_BUFFER || target == GL_ATOMIC_COUNTER_BUFFER) && (offset % 4) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
std::format("offset ({}) must be a multiple of 4 for {}.", offset,
MG_Util::ConvertGLEnumToString(target))));
return false;
}
if (target == GL_TRANSFORM_FEEDBACK_BUFFER && hasBuffer && (size % 4) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", funcName,
std::format("size ({}) must be a multiple of 4 for GL_TRANSFORM_FEEDBACK_BUFFER.", size)));
return false;
}
return true;
}
void BindBufferRange_State(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) {
@@ -1384,6 +1605,20 @@ namespace MobileGL::MG_Impl::GLImpl {
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
// The target's alignment rules are a property of the BINDING POINT, not of the buffer,
// so they apply even when buffer is zero - which is exactly how
// KHR-GL43.shader_storage_buffer_object.negative-api-bind probes the SSBO alignment
// (glBindBufferRange(SHADER_STORAGE_BUFFER, 0, 0, alignment - 1, 0)). Only the size
// rules need a buffer, since buffer 0 detaches the binding point and ignores size.
if (!ValidateBufferRangeOffsetAndSize(target, offset, size, __func__, /*hasBuffer: */ buffer != 0)) return;
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Transform feedback buffer bindings cannot change while transform "
"feedback is active."));
return;
}
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, index);
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, index);
@@ -1391,6 +1626,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (buffer == 0) {
point.Bind(nullptr);
point.SetRange(Range1D(0, 0));
GetBufferBindingSlot(bufferTarget).Bind(nullptr);
return;
}
@@ -1408,6 +1644,8 @@ namespace MobileGL::MG_Impl::GLImpl {
} else {
point.ClearRange();
}
// Also the generic binding point, exactly as BindBufferBase (GL 4.6 core 6.1.1).
GetBufferBindingSlot(bufferTarget).Bind(bufferObject);
}
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
@@ -1470,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);
}
@@ -1517,6 +1764,10 @@ namespace MobileGL::MG_Impl::GLImpl {
BufferSubData_State(target, offset, size, data);
}
void GetNamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, void* data) {
GetNamedBufferSubData_State(buffer, offset, size, data);
}
void GetBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, void* data) {
GetBufferSubData_State(target, offset, size, data);
}
@@ -1542,16 +1793,54 @@ namespace MobileGL::MG_Impl::GLImpl {
}
// ARB_multi_bind: defined by the spec as equivalent to a loop over the single-bind entry
// points (with buffer 0 resetting the binding point).
// points (with buffer 0 resetting the binding point) - but only AFTER an up-front check
// of the whole [first, first + count) range. Looping straight into the single-bind entry
// points reports the single-bind INVALID_VALUE for an out-of-range index instead of the
// multi-bind INVALID_OPERATION, and binds the in-range prefix before failing.
static Bool ValidateMultiBindBufferRange(GLenum target, GLuint first, GLsizei count, const char* funcName) {
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return false;
return BufferImpl::ValidateBufferBindingPointRange(bufferTarget, first, count, funcName);
}
// ARB_multi_bind states the equivalence to a loop of single binds "except that ... buffers
// will not be created if they do not exist": glBindBuffer instantiates a name glGenBuffers
// merely reserved, glBindBuffers* must refuse it and raise INVALID_OPERATION instead
// (KHR-GL44.multi_bind.errors_bind_buffers).
//
// Deliberately PER ELEMENT, not all-or-nothing: the equivalence the extension defines is a
// loop, so a bad entry costs its own binding point and nothing else. Rejecting the whole
// call instead cost multi_bind.functional_bind_buffers_base its bindings.
static Bool IsExistingBufferForMultiBind(GLuint buffer, GLsizei index, const char* funcName) {
if (buffer == 0 || MG_State::pGLContext->ValidateBufferObject(buffer)) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", funcName,
std::format("buffers[{}] ({}) is not the name of an existing buffer object.", index, buffer)));
return false;
}
void BindBuffersBase(GLenum target, GLuint first, GLsizei count, const GLuint* buffers) {
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
for (GLsizei i = 0; i < count; ++i) {
BindBufferBase_State(target, first + i, buffers ? buffers[i] : 0);
const GLuint buffer = buffers ? buffers[i] : 0;
if (!IsExistingBufferForMultiBind(buffer, i, __func__)) continue;
BindBufferBase_State(target, first + i, buffer);
}
}
// The (offset, size) constraints are the one part of glBindBuffersRange that stays
// per-element: ARB_multi_bind checks them separately for each binding point, leaves that
// point unchanged on failure, and still applies the remaining elements - which is exactly
// what looping into BindBufferRange_State does. Only the [first, first + count) range is
// an up-front, all-or-nothing check. Elements that name buffer 0 (or a NULL buffers array)
// reset the binding point through BindBufferBase_State and carry no offset/size to check.
void BindBuffersRange(GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
const GLsizeiptr* sizes) {
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
for (GLsizei i = 0; i < count; ++i) {
if (buffers && !IsExistingBufferForMultiBind(buffers[i], i, __func__)) continue;
if (!buffers || buffers[i] == 0) {
BindBufferBase_State(target, first + i, 0);
} else {
@@ -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);
@@ -41,6 +44,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLsizeiptr size);
void BufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const void* data);
void GetBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, void* data);
void GetNamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, void* data);
void BufferData(GLenum target, GLsizeiptr size, const void* data, GLenum usage);
void BindBuffer(GLenum target, GLuint buffer);
void GenBuffers(GLsizei n, GLuint* buffers);
+51 -14
View File
@@ -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) {
@@ -53,13 +54,53 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
return true;
}
Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index) {
SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(target);
if (target == BufferTarget::ShaderStorage && MG_Backend::pActiveBackendObject) {
const Int backendCount =
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0)));
namespace {
// The GL-visible number of indexed binding points for `target`.
SizeT GetBufferBindingPointLimit(BufferTarget target) {
SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(target);
if (target == BufferTarget::ShaderStorage && MG_Backend::pActiveBackendObject) {
const Int backendCount =
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0)));
}
if (target == BufferTarget::TransformFeedback) {
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS bounds the indexed capture
// 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
Bool ValidateBufferBindingPointRange(BufferTarget target, Uint first, GLsizei count, const char* funcName) {
if (count < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl", funcName,
"count must be non-negative."));
return false;
}
const SizeT pointCount = GetBufferBindingPointLimit(target);
if (static_cast<Uint64>(first) + static_cast<Uint64>(count) > static_cast<Uint64>(pointCount)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl/BufferImpl", funcName,
std::format("first + count ({} + {}) exceeds the {} indexed binding points of target {}.", first,
count, pointCount, MG_Util::ConvertBufferTargetToString(target))));
return false;
}
return true;
}
Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index) {
const SizeT pointCount = GetBufferBindingPointLimit(target);
if (index < pointCount) {
return true;
@@ -107,14 +148,10 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
}
Bool ValidateBufferMappingAccess(Flags<BufferMappingAccessBit> accessBits) {
if (accessBits == BufferMappingAccessBit::Null) {
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl",
"ValidateBufferMappingAccess",
"Access bits cannot be null."));
return false;
}
// An empty mask is a legal value for a bitfield - it just fails the rule that a mapping
// must ask for read or write access, which is INVALID_OPERATION and belongs to the callers
// (both of them check it immediately after this). Rejecting it here as INVALID_ENUM
// reported the wrong error and hid theirs.
const auto validBits = BufferMappingAccessBit::Read | BufferMappingAccessBit::Write |
BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer |
BufferMappingAccessBit::FlushExplicit | BufferMappingAccessBit::Unsynchronized |
@@ -17,4 +17,8 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
Bool ValidateBufferMappingAccess(Flags<BufferMappingAccessBit> accessBits);
Bool ValidateBufferBindingPointTarget(BufferTarget target);
Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index);
// ARB_multi_bind: glBindBuffersBase/Range validate the whole [first, first + count) range
// up front and report INVALID_OPERATION, where a single out-of-range index would be
// INVALID_VALUE. Naively looping the single-bind entry points reports the wrong class.
Bool ValidateBufferBindingPointRange(BufferTarget target, Uint first, GLsizei count, const char* funcName);
} // namespace MobileGL::MG_Impl::GLImpl::BufferImpl
+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
File diff suppressed because it is too large Load Diff
@@ -11,10 +11,31 @@
namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void BeginTransformFeedback(GLenum primitiveMode);
void EndTransformFeedback(void);
void PauseTransformFeedback(void);
void ResumeTransformFeedback(void);
void GenTransformFeedbacks(GLsizei n, GLuint* ids);
void CreateTransformFeedbacks(GLsizei n, GLuint* ids);
void DeleteTransformFeedbacks(GLsizei n, const GLuint* ids);
void TransformFeedbackBufferBase(GLuint xfb, GLuint index, GLuint buffer);
void TransformFeedbackBufferRange(GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);
void GetTransformFeedbackiv(GLuint xfb, GLenum pname, GLint* param);
void GetTransformFeedbacki_v(GLuint xfb, GLenum pname, GLuint index, GLint* param);
void GetTransformFeedbacki64_v(GLuint xfb, GLenum pname, GLuint index, GLint64* param);
void BindTransformFeedback(GLenum target, GLuint id);
GLboolean IsTransformFeedback(GLuint id);
void DrawTransformFeedback(GLenum mode, GLuint id);
void DrawTransformFeedbackInstanced(GLenum mode, GLuint id, GLsizei instancecount);
void DrawTransformFeedbackStream(GLenum mode, GLuint id, GLuint stream);
void DrawTransformFeedbackStreamInstanced(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount);
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,
+152 -161
View File
@@ -15,21 +15,23 @@
#include "../Texture/GL_Texture.h"
#include "../Drawing/GL_Drawing.h"
#include "../Program/GL_Program.h"
#include "../Program/GL_ProgramPipeline.h"
#include "../RenderState/GL_RenderState.h"
#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__) {
#define DECLARE_GL_FUNCTION_STUB_END(type, name, ...) \
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
return (type)1; \
}
#define DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(type, name, ...) \
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
}
#define DECLARE_GL_FUNCTION_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
@@ -158,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)
@@ -236,12 +238,12 @@ DECLARE_GL_FUNCTION_HEAD(void, DeleteVertexArrays, GLsizei n, const GLuint* arra
DECLARE_GL_FUNCTION_HEAD(void, GenVertexArrays, GLsizei n, GLuint* arrays) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenVertexArrays, n, arrays)
DECLARE_GL_FUNCTION_HEAD(GLboolean, IsVertexArray, GLuint array) DECLARE_GL_FUNCTION_END(GLboolean, IsVertexArray, array)
DECLARE_GL_FUNCTION_HEAD(void, GetIntegeri_v, GLenum target, GLuint index, GLint* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetIntegeri_v, target, index, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginTransformFeedback, GLenum primitiveMode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginTransformFeedback, primitiveMode)
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndTransformFeedback)
DECLARE_GL_FUNCTION_HEAD(void, BeginTransformFeedback, GLenum primitiveMode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginTransformFeedback, primitiveMode)
DECLARE_GL_FUNCTION_HEAD(void, EndTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndTransformFeedback)
DECLARE_GL_FUNCTION_HEAD(void, BindBufferRange, GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBufferRange, target, index, buffer, offset, size)
DECLARE_GL_FUNCTION_HEAD(void, BindBufferBase, GLenum target, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBufferBase, target, index, buffer)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackVaryings, GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TransformFeedbackVaryings, program, count, varyings, bufferMode)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbackVarying, GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size, GLenum* type, GLchar* name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbackVarying, program, index, bufSize, length, size, type, name)
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackVaryings, GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackVaryings, program, count, varyings, bufferMode)
DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbackVarying, GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size, GLenum* type, GLchar* name) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbackVarying, program, index, bufSize, length, size, type, name)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribIPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribIPointer, index, size, type, stride, pointer)
DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIiv, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIiv, index, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIuiv, GLuint index, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIuiv, index, pname, params)
@@ -292,17 +294,17 @@ DECLARE_GL_FUNCTION_HEAD(void, SamplerParameterfv, GLuint sampler, GLenum pname,
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameteriv, GLuint sampler, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameteriv, sampler, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterfv, GLuint sampler, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterfv, sampler, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribDivisor, GLuint index, GLuint divisor) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribDivisor, index, divisor)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedback, target, id)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacks, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsTransformFeedback, GLuint id) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsTransformFeedback, id)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedback)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedback)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramBinary, GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramBinary, program, bufSize, length, binaryFormat, binary)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramBinary, GLuint program, GLenum binaryFormat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramBinary, program, binaryFormat, binary, length)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramParameteri, GLuint program, GLenum pname, GLint value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramParameteri, program, pname, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateFramebuffer, GLenum target, GLsizei numAttachments, const GLenum* attachments) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateFramebuffer, target, numAttachments, attachments)
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateSubFramebuffer, GLenum target, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateSubFramebuffer, target, numAttachments, attachments, x, y, width, height)
DECLARE_GL_FUNCTION_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindTransformFeedback, target, id)
DECLARE_GL_FUNCTION_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids)
DECLARE_GL_FUNCTION_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenTransformFeedbacks, n, ids)
DECLARE_GL_FUNCTION_HEAD(GLboolean, IsTransformFeedback, GLuint id) DECLARE_GL_FUNCTION_END(GLboolean, IsTransformFeedback, id)
DECLARE_GL_FUNCTION_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PauseTransformFeedback)
DECLARE_GL_FUNCTION_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ResumeTransformFeedback)
DECLARE_GL_FUNCTION_HEAD(void, GetProgramBinary, GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramBinary, program, bufSize, length, binaryFormat, binary)
DECLARE_GL_FUNCTION_HEAD(void, ProgramBinary, GLuint program, GLenum binaryFormat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramBinary, program, binaryFormat, binary, length)
DECLARE_GL_FUNCTION_HEAD(void, ProgramParameteri, GLuint program, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramParameteri, program, pname, value)
DECLARE_GL_FUNCTION_HEAD(void, InvalidateFramebuffer, GLenum target, GLsizei numAttachments, const GLenum* attachments) DECLARE_GL_FUNCTION_END_NO_RETURN(void, InvalidateFramebuffer, target, numAttachments, attachments)
DECLARE_GL_FUNCTION_HEAD(void, InvalidateSubFramebuffer, GLenum target, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, InvalidateSubFramebuffer, target, numAttachments, attachments, x, y, width, height)
DECLARE_GL_FUNCTION_HEAD(void, TexStorage2D, GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage2D, target, levels, internalformat, width, height)
DECLARE_GL_FUNCTION_HEAD(void, TexStorage3D, GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage3D, target, levels, internalformat, width, height, depth)
DECLARE_GL_FUNCTION_HEAD(void, GetInternalformativ, GLenum target, GLenum internalformat, GLenum pname, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetInternalformativ, target, internalformat, pname, bufSize, params)
@@ -310,21 +312,21 @@ DECLARE_GL_FUNCTION_HEAD(void, DispatchCompute, GLuint num_groups_x, GLuint num_
DECLARE_GL_FUNCTION_HEAD(void, DispatchComputeIndirect, GLintptr indirect) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DispatchComputeIndirect, indirect)
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysIndirect, GLenum mode, const void* indirect) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysIndirect, mode, indirect)
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsIndirect, GLenum mode, GLenum type, const void* indirect) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsIndirect, mode, type, indirect)
DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferParameteri, GLenum target, GLenum pname, GLint param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferParameteri, target, pname, param)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetFramebufferParameteriv, GLenum target, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetFramebufferParameteriv, target, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, FramebufferParameteri, GLenum target, GLenum pname, GLint param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferParameteri, target, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, GetFramebufferParameteriv, GLenum target, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetFramebufferParameteriv, target, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetProgramInterfaceiv, GLuint program, GLenum programInterface, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramInterfaceiv, program, programInterface, pname, params)
DECLARE_GL_FUNCTION_HEAD(GLuint, GetProgramResourceIndex, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLuint, GetProgramResourceIndex, program, programInterface, name)
DECLARE_GL_FUNCTION_HEAD(void, GetProgramResourceName, GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length, GLchar* name) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramResourceName, program, programInterface, index, bufSize, length, name)
DECLARE_GL_FUNCTION_HEAD(void, GetProgramResourceiv, GLuint program, GLenum programInterface, GLuint index, GLsizei propCount, const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramResourceiv, program, programInterface, index, propCount, props, bufSize, length, params)
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocation, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocation, program, programInterface, name)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UseProgramStages, GLuint pipeline, GLbitfield stages, GLuint program) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UseProgramStages, pipeline, stages, program)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ActiveShaderProgram, GLuint pipeline, GLuint program) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ActiveShaderProgram, pipeline, program)
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, CreateShaderProgramv, GLenum type, GLsizei count, const GLchar* const* strings) DECLARE_GL_FUNCTION_STUB_END(GLuint, CreateShaderProgramv, type, count, strings)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindProgramPipeline, pipeline)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteProgramPipelines, GLsizei n, const GLuint* pipelines) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteProgramPipelines, n, pipelines)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenProgramPipelines, n, pipelines)
DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsProgramPipeline, pipeline)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramPipelineiv, GLuint pipeline, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramPipelineiv, pipeline, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, UseProgramStages, GLuint pipeline, GLbitfield stages, GLuint program) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UseProgramStages, pipeline, stages, program)
DECLARE_GL_FUNCTION_HEAD(void, ActiveShaderProgram, GLuint pipeline, GLuint program) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ActiveShaderProgram, pipeline, program)
DECLARE_GL_FUNCTION_HEAD(GLuint, CreateShaderProgramv, GLenum type, GLsizei count, const GLchar* const* strings) DECLARE_GL_FUNCTION_END(GLuint, CreateShaderProgramv, type, count, strings)
DECLARE_GL_FUNCTION_HEAD(void, BindProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindProgramPipeline, pipeline)
DECLARE_GL_FUNCTION_HEAD(void, DeleteProgramPipelines, GLsizei n, const GLuint* pipelines) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DeleteProgramPipelines, n, pipelines)
DECLARE_GL_FUNCTION_HEAD(void, GenProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenProgramPipelines, n, pipelines)
DECLARE_GL_FUNCTION_HEAD(GLboolean, IsProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_END(GLboolean, IsProgramPipeline, pipeline)
DECLARE_GL_FUNCTION_HEAD(void, GetProgramPipelineiv, GLuint pipeline, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramPipelineiv, pipeline, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform1i, GLuint program, GLint location, GLint v0) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform1i, program, location, v0)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform2i, GLuint program, GLint location, GLint v0, GLint v1) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform2i, program, location, v0, v1)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform3i, GLuint program, GLint location, GLint v0, GLint v1, GLint v2) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform3i, program, location, v0, v1, v2)
@@ -358,8 +360,8 @@ DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix2x4fv, GLuint program, GLint
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4x2fv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4x2fv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix3x4fv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix3x4fv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4x3fv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4x3fv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ValidateProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ValidateProgramPipeline, pipeline)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramPipelineInfoLog, GLuint pipeline, GLsizei bufSize, GLsizei* length, GLchar* infoLog) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramPipelineInfoLog, pipeline, bufSize, length, infoLog)
DECLARE_GL_FUNCTION_HEAD(void, ValidateProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ValidateProgramPipeline, pipeline)
DECLARE_GL_FUNCTION_HEAD(void, GetProgramPipelineInfoLog, GLuint pipeline, GLsizei bufSize, GLsizei* length, GLchar* infoLog) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramPipelineInfoLog, pipeline, bufSize, length, infoLog)
DECLARE_GL_FUNCTION_HEAD(void, BindImageTexture, GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindImageTexture, unit, texture, level, layered, layer, access, format)
DECLARE_GL_FUNCTION_HEAD(void, GetBooleani_v, GLenum target, GLuint index, GLboolean* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetBooleani_v, target, index, data)
DECLARE_GL_FUNCTION_HEAD(void, MemoryBarrier, GLbitfield barriers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MemoryBarrier, barriers)
@@ -377,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)
@@ -418,13 +406,13 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawRangeElementsBaseVertex, GLenum mode, GLuint
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertex, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertex, mode, count, type, indices, instancecount, basevertex)
DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture, GLenum target, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture, target, attachment, texture, level)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBox, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrimitiveBoundingBox, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
DECLARE_GL_FUNCTION_STUB_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_STUB_END(GLenum, GetGraphicsResetStatus)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ReadnPixels, x, y, width, height, format, type, bufSize, data)
DECLARE_GL_FUNCTION_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_END(GLenum, GetGraphicsResetStatus)
DECLARE_GL_FUNCTION_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ReadnPixels, x, y, width, height, format, type, bufSize, data)
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_STUB_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PatchParameteri, pname, 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)
DECLARE_GL_FUNCTION_HEAD(void, GetTexParameterIiv, GLenum target, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTexParameterIiv, target, pname, params)
@@ -434,7 +422,7 @@ DECLARE_GL_FUNCTION_HEAD(void, SamplerParameterIuiv, GLuint sampler, GLenum pnam
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterIiv, GLuint sampler, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterIiv, sampler, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterIuiv, GLuint sampler, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterIuiv, sampler, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, TexBuffer, GLenum target, GLenum internalformat, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexBuffer, target, internalformat, buffer)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexBufferRange, GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexBufferRange, target, internalformat, buffer, offset, size)
DECLARE_GL_FUNCTION_HEAD(void, TexBufferRange, GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexBufferRange, target, internalformat, buffer, offset, size)
DECLARE_GL_FUNCTION_HEAD(void, TexStorage3DMultisample, GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage3DMultisample, target, samples, internalformat, width, height, depth, fixedsamplelocations)
DECLARE_GL_FUNCTION_HEAD(void*, MapBufferRange, GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access) DECLARE_GL_FUNCTION_END(void*, MapBufferRange, target, offset, length, access)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearIndex, GLfloat c) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearIndex, c)
@@ -724,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)
@@ -909,24 +897,24 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ColorP4ui, GLenum type, GLuint color) DECLAR
DECLARE_GL_FUNCTION_STUB_HEAD(void, ColorP4uiv, GLenum type, const GLuint* color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ColorP4uiv, type, color)
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColorP3ui, GLenum type, GLuint color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColorP3ui, type, color)
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColorP3uiv, GLenum type, const GLuint* color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColorP3uiv, type, color)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1d, GLint location, GLdouble x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1d, location, x)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2d, GLint location, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2d, location, x, y)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3d, GLint location, GLdouble x, GLdouble y, GLdouble z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3d, location, x, y, z)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform4d, GLint location, GLdouble x, GLdouble y, GLdouble z, GLdouble w) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform4d, location, x, y, z, w)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1dv, location, count, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2dv, location, count, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3dv, location, count, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform4dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform4dv, location, count, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix2dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix3dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix4dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix2x3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix2x3dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix2x4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix2x4dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix3x2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix3x2dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix3x4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix3x4dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix4x2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix4x2dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix4x3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix4x3dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformdv, GLuint program, GLint location, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformdv, program, location, params)
DECLARE_GL_FUNCTION_HEAD(void, Uniform1d, GLint location, GLdouble x) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform1d, location, x)
DECLARE_GL_FUNCTION_HEAD(void, Uniform2d, GLint location, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform2d, location, x, y)
DECLARE_GL_FUNCTION_HEAD(void, Uniform3d, GLint location, GLdouble x, GLdouble y, GLdouble z) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform3d, location, x, y, z)
DECLARE_GL_FUNCTION_HEAD(void, Uniform4d, GLint location, GLdouble x, GLdouble y, GLdouble z, GLdouble w) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform4d, location, x, y, z, w)
DECLARE_GL_FUNCTION_HEAD(void, Uniform1dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform1dv, location, count, value)
DECLARE_GL_FUNCTION_HEAD(void, Uniform2dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform2dv, location, count, value)
DECLARE_GL_FUNCTION_HEAD(void, Uniform3dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform3dv, location, count, value)
DECLARE_GL_FUNCTION_HEAD(void, Uniform4dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform4dv, location, count, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix2dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix3dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix4dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix2x3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix2x3dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix2x4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix2x4dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix3x2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix3x2dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix3x4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix3x4dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix4x2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix4x2dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix4x3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix4x3dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, GetUniformdv, GLuint program, GLint location, GLdouble* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetUniformdv, program, location, params)
DECLARE_GL_FUNCTION_STUB_HEAD(GLint, GetSubroutineUniformLocation, GLuint program, GLenum shadertype, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END(GLint, GetSubroutineUniformLocation, program, shadertype, name)
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, GetSubroutineIndex, GLuint program, GLenum shadertype, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END(GLuint, GetSubroutineIndex, program, shadertype, name)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveSubroutineUniformiv, GLuint program, GLenum shadertype, GLuint index, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveSubroutineUniformiv, program, shadertype, index, pname, values)
@@ -935,29 +923,29 @@ 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_STUB_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedback, mode, id)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginQueryIndexed, target, index, id)
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndQueryIndexed, GLenum target, GLuint index) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndQueryIndexed, target, index)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryIndexediv, GLenum target, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryIndexediv, target, index, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform1d, GLuint program, GLint location, GLdouble v0) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform1d, program, location, v0)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform1dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform1dv, program, location, count, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform2d, GLuint program, GLint location, GLdouble v0, GLdouble v1) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform2d, program, location, v0, v1)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform2dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform2dv, program, location, count, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform3d, GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform3d, program, location, v0, v1, v2)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform3dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform3dv, program, location, count, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform4d, GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform4d, program, location, v0, v1, v2, v3)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform4dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform4dv, program, location, count, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix2dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix3dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix4dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix2x3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix2x3dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix3x2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix3x2dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix2x4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix2x4dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix4x2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix4x2dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix3x4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix3x4dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix4x3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix4x3dv, program, location, count, transpose, value)
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)
DECLARE_GL_FUNCTION_HEAD(void, EndQueryIndexed, GLenum target, GLuint index) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndQueryIndexed, target, index)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryIndexediv, GLenum target, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryIndexediv, target, index, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform1d, GLuint program, GLint location, GLdouble v0) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform1d, program, location, v0)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform1dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform1dv, program, location, count, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform2d, GLuint program, GLint location, GLdouble v0, GLdouble v1) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform2d, program, location, v0, v1)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform2dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform2dv, program, location, count, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform3d, GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform3d, program, location, v0, v1, v2)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform3dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform3dv, program, location, count, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform4d, GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform4d, program, location, v0, v1, v2, v3)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform4dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform4dv, program, location, count, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix2dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix3dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix2x3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix2x3dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix3x2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix3x2dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix2x4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix2x4dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4x2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4x2dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix3x4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix3x4dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4x3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4x3dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL1d, GLuint index, GLdouble x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL1d, index, x)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL2d, GLuint index, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL2d, index, x, y)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL3d, GLuint index, GLdouble x, GLdouble y, GLdouble z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL3d, index, x, y, z)
@@ -968,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_STUB_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetFloati_v, target, index, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetDoublei_v, target, index, data)
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_STUB_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_STUB_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, 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_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)
@@ -995,24 +983,24 @@ 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_STUB_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset)
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)
DECLARE_GL_FUNCTION_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data)
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersBase, GLenum target, GLuint first, GLsizei count, const GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersBase, target, first, count, buffers)
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersRange, GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizeiptr* sizes) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersRange, target, first, count, buffers, offsets, sizes)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTextures, first, count, textures)
DECLARE_GL_FUNCTION_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindTextures, first, count, textures)
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_STUB_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindImageTextures, first, count, textures)
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_STUB_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackBufferBase, GLuint xfb, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TransformFeedbackBufferBase, xfb, index, buffer)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackBufferRange, GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TransformFeedbackBufferRange, xfb, index, buffer, offset, size)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbackiv, GLuint xfb, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbackiv, xfb, pname, param)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbacki_v, GLuint xfb, GLenum pname, GLuint index, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbacki_v, xfb, pname, index, param)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbacki64_v, GLuint xfb, GLenum pname, GLuint index, GLint64* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbacki64_v, xfb, pname, index, param)
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)
DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbackiv, GLuint xfb, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbackiv, xfb, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbacki_v, GLuint xfb, GLenum pname, GLuint index, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbacki_v, xfb, pname, index, param)
DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbacki64_v, GLuint xfb, GLenum pname, GLuint index, GLint64* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbacki64_v, xfb, pname, index, param)
DECLARE_GL_FUNCTION_HEAD(void, CreateBuffers, GLsizei n, GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateBuffers, n, buffers)
DECLARE_GL_FUNCTION_HEAD(void, NamedBufferStorage, GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedBufferStorage, buffer, size, data, flags)
DECLARE_GL_FUNCTION_HEAD(void, NamedBufferData, GLuint buffer, GLsizeiptr size, const void* data, GLenum usage) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedBufferData, buffer, size, data, usage)
@@ -1025,32 +1013,32 @@ DECLARE_GL_FUNCTION_HEAD(void, FlushMappedNamedBufferRange, GLuint buffer, GLint
DECLARE_GL_FUNCTION_HEAD(void, GetNamedBufferParameteriv, GLuint buffer, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedBufferParameteriv, buffer, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetNamedBufferParameteri64v, GLuint buffer, GLenum pname, GLint64* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedBufferParameteri64v, buffer, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetNamedBufferPointerv, GLuint buffer, GLenum pname, void** params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedBufferPointerv, buffer, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetNamedBufferSubData, GLuint buffer, GLintptr offset, GLsizeiptr size, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetNamedBufferSubData, buffer, offset, size, data)
DECLARE_GL_FUNCTION_HEAD(void, GetNamedBufferSubData, GLuint buffer, GLintptr offset, GLsizeiptr size, void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedBufferSubData, buffer, offset, size, data)
DECLARE_GL_FUNCTION_HEAD(void, CreateFramebuffers, GLsizei n, GLuint* framebuffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateFramebuffers, n, framebuffers)
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferRenderbuffer, GLuint framebuffer, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferRenderbuffer, framebuffer, attachment, renderbuffertarget, renderbuffer)
DECLARE_GL_FUNCTION_STUB_HEAD(void, NamedFramebufferParameteri, GLuint framebuffer, GLenum pname, GLint param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, NamedFramebufferParameteri, framebuffer, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferParameteri, GLuint framebuffer, GLenum pname, GLint param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferParameteri, framebuffer, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferTexture, GLuint framebuffer, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferTexture, framebuffer, attachment, texture, level)
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferTextureLayer, GLuint framebuffer, GLenum attachment, GLuint texture, GLint level, GLint layer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferTextureLayer, framebuffer, attachment, texture, level, layer)
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferDrawBuffer, GLuint framebuffer, GLenum buf) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferDrawBuffer, framebuffer, buf)
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferDrawBuffers, GLuint framebuffer, GLsizei n, const GLenum* bufs) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferDrawBuffers, framebuffer, n, bufs)
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferReadBuffer, GLuint framebuffer, GLenum src) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferReadBuffer, framebuffer, src)
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateNamedFramebufferData, GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateNamedFramebufferData, framebuffer, numAttachments, attachments)
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateNamedFramebufferSubData, GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateNamedFramebufferSubData, framebuffer, numAttachments, attachments, x, y, width, height)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearNamedFramebufferiv, GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLint* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearNamedFramebufferiv, framebuffer, buffer, drawbuffer, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearNamedFramebufferuiv, GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLuint* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearNamedFramebufferuiv, framebuffer, buffer, drawbuffer, value)
DECLARE_GL_FUNCTION_HEAD(void, InvalidateNamedFramebufferData, GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments) DECLARE_GL_FUNCTION_END_NO_RETURN(void, InvalidateNamedFramebufferData, framebuffer, numAttachments, attachments)
DECLARE_GL_FUNCTION_HEAD(void, InvalidateNamedFramebufferSubData, GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, InvalidateNamedFramebufferSubData, framebuffer, numAttachments, attachments, x, y, width, height)
DECLARE_GL_FUNCTION_HEAD(void, ClearNamedFramebufferiv, GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLint* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearNamedFramebufferiv, framebuffer, buffer, drawbuffer, value)
DECLARE_GL_FUNCTION_HEAD(void, ClearNamedFramebufferuiv, GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLuint* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearNamedFramebufferuiv, framebuffer, buffer, drawbuffer, value)
DECLARE_GL_FUNCTION_HEAD(void, ClearNamedFramebufferfv, GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLfloat* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearNamedFramebufferfv, framebuffer, buffer, drawbuffer, value)
DECLARE_GL_FUNCTION_HEAD(void, ClearNamedFramebufferfi, GLuint framebuffer, GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearNamedFramebufferfi, framebuffer, buffer, drawbuffer, depth, stencil)
DECLARE_GL_FUNCTION_HEAD(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) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BlitNamedFramebuffer, readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter)
DECLARE_GL_FUNCTION_HEAD(GLenum, CheckNamedFramebufferStatus, GLuint framebuffer, GLenum target) DECLARE_GL_FUNCTION_END(GLenum, CheckNamedFramebufferStatus, framebuffer, target)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetNamedFramebufferParameteriv, GLuint framebuffer, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetNamedFramebufferParameteriv, framebuffer, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, GetNamedFramebufferParameteriv, GLuint framebuffer, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedFramebufferParameteriv, framebuffer, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, GetNamedFramebufferAttachmentParameteriv, GLuint framebuffer, GLenum attachment, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedFramebufferAttachmentParameteriv, framebuffer, attachment, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, CreateRenderbuffers, GLsizei n, GLuint* renderbuffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateRenderbuffers, n, renderbuffers)
DECLARE_GL_FUNCTION_HEAD(void, NamedRenderbufferStorage, GLuint renderbuffer, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedRenderbufferStorage, renderbuffer, internalformat, width, height)
DECLARE_GL_FUNCTION_HEAD(void, NamedRenderbufferStorageMultisample, GLuint renderbuffer, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedRenderbufferStorageMultisample, renderbuffer, samples, internalformat, width, height)
DECLARE_GL_FUNCTION_HEAD(void, GetNamedRenderbufferParameteriv, GLuint renderbuffer, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedRenderbufferParameteriv, renderbuffer, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, CreateTextures, GLenum target, GLsizei n, GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTextures, target, n, textures)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBuffer, GLuint texture, GLenum internalformat, GLuint buffer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBuffer, texture, internalformat, buffer)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBufferRange, GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBufferRange, texture, internalformat, buffer, offset, size)
DECLARE_GL_FUNCTION_HEAD(void, TextureBuffer, GLuint texture, GLenum internalformat, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBuffer, texture, internalformat, buffer)
DECLARE_GL_FUNCTION_HEAD(void, TextureBufferRange, GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBufferRange, texture, internalformat, buffer, offset, size)
DECLARE_GL_FUNCTION_HEAD(void, TextureStorage1D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage1D, texture, levels, internalformat, width)
DECLARE_GL_FUNCTION_HEAD(void, TextureStorage2D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage2D, texture, levels, internalformat, width, height)
DECLARE_GL_FUNCTION_HEAD(void, TextureStorage3D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage3D, texture, levels, internalformat, width, height, depth)
@@ -1059,12 +1047,12 @@ 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_STUB_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_STUB_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_STUB_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
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_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_STUB_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, 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)
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterf, GLuint texture, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterf, texture, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterfv, GLuint texture, GLenum pname, const GLfloat* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterfv, texture, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, TextureParameteri, GLuint texture, GLenum pname, GLint param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameteri, texture, pname, param)
@@ -1074,7 +1062,7 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureParameteriv, GLuint texture, GLenum pname,
DECLARE_GL_FUNCTION_HEAD(void, GenerateTextureMipmap, GLuint texture) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenerateTextureMipmap, texture)
DECLARE_GL_FUNCTION_HEAD(void, BindTextureUnit, GLuint unit, GLuint texture) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindTextureUnit, unit, texture)
DECLARE_GL_FUNCTION_HEAD(void, GetTextureImage, GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureImage, texture, level, format, type, bufSize, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImage, GLuint texture, GLint level, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImage, texture, level, bufSize, pixels)
DECLARE_GL_FUNCTION_HEAD(void, GetCompressedTextureImage, GLuint texture, GLint level, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetCompressedTextureImage, texture, level, bufSize, pixels)
DECLARE_GL_FUNCTION_HEAD(void, GetTextureLevelParameterfv, GLuint texture, GLint level, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureLevelParameterfv, texture, level, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetTextureLevelParameteriv, GLuint texture, GLint level, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureLevelParameteriv, texture, level, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetTextureParameterfv, GLuint texture, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureParameterfv, texture, pname, params)
@@ -1090,18 +1078,18 @@ DECLARE_GL_FUNCTION_HEAD(void, VertexArrayVertexBuffers, GLuint vaobj, GLuint fi
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribBinding, GLuint vaobj, GLuint attribindex, GLuint bindingindex) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribBinding, vaobj, attribindex, bindingindex)
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribFormat, vaobj, attribindex, size, type, normalized, relativeoffset)
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribIFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribIFormat, vaobj, attribindex, size, type, relativeoffset)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexArrayAttribLFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexArrayAttribLFormat, vaobj, attribindex, size, type, relativeoffset)
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribLFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribLFormat, vaobj, attribindex, size, type, relativeoffset)
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayBindingDivisor, GLuint vaobj, GLuint bindingindex, GLuint divisor) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayBindingDivisor, vaobj, bindingindex, divisor)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexArrayiv, GLuint vaobj, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexArrayiv, vaobj, pname, param)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexArrayIndexediv, GLuint vaobj, GLuint index, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexArrayIndexediv, vaobj, index, pname, param)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexArrayIndexed64iv, GLuint vaobj, GLuint index, GLenum pname, GLint64* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexArrayIndexed64iv, vaobj, index, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, GetVertexArrayiv, GLuint vaobj, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexArrayiv, vaobj, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, GetVertexArrayIndexediv, GLuint vaobj, GLuint index, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexArrayIndexediv, vaobj, index, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, GetVertexArrayIndexed64iv, GLuint vaobj, GLuint index, GLenum pname, GLint64* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexArrayIndexed64iv, vaobj, index, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, CreateSamplers, GLsizei n, GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateSamplers, n, samplers)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CreateProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CreateProgramPipelines, n, pipelines)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CreateQueries, GLenum target, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CreateQueries, target, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjecti64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjecti64v, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjectiv, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjectiv, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjectui64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjectui64v, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjectuiv, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjectuiv, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_HEAD(void, CreateProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateProgramPipelines, n, pipelines)
DECLARE_GL_FUNCTION_HEAD(void, CreateQueries, GLenum target, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateQueries, target, n, ids)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryBufferObjecti64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryBufferObjecti64v, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryBufferObjectiv, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryBufferObjectiv, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryBufferObjectui64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryBufferObjectui64v, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryBufferObjectuiv, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryBufferObjectuiv, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_HEAD(void, GetTextureSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, bufSize, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, bufSize, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnCompressedTexImage, GLenum target, GLint lod, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnCompressedTexImage, target, lod, bufSize, pixels)
@@ -1119,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)
@@ -1162,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)
@@ -1272,7 +1260,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord4ivARB, GLenum target, const GL
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord4sARB, GLenum target, GLshort s, GLshort t, GLshort r, GLshort q) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord4sARB, target, s, t, r, q)
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord4svARB, GLenum target, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord4svARB, target, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjectivARB, GLuint id, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjectivARB, id, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, MaxShaderCompilerThreadsARB, GLuint count) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MaxShaderCompilerThreadsARB, count)
DECLARE_GL_FUNCTION_HEAD(void, MaxShaderCompilerThreadsARB, GLuint count) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MaxShaderCompilerThreadsARB, count)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfARB, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfARB, pname, param)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfvARB, GLenum pname, const GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfvARB, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnTexImageARB, GLenum target, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnTexImageARB, target, level, format, type, bufSize, img)
@@ -1380,7 +1368,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3ivARB, const GLint* v) DECLARE_GL_
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3sARB, GLshort x, GLshort y, GLshort z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3sARB, x, y, z)
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3svARB, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3svARB, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BlendBarrierKHR, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BlendBarrierKHR, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, MaxShaderCompilerThreadsKHR, GLuint count) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MaxShaderCompilerThreadsKHR, count)
DECLARE_GL_FUNCTION_HEAD(void, MaxShaderCompilerThreadsKHR, GLuint count) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MaxShaderCompilerThreadsKHR, count)
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord1bOES, GLenum texture, GLbyte s) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord1bOES, texture, s)
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord1bvOES, GLenum texture, const GLbyte* coords) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord1bvOES, texture, coords)
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord2bOES, GLenum texture, GLbyte s, GLbyte t) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord2bOES, texture, s, t)
@@ -1847,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_STUB_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_STUB_END_NO_RETURN(void, CompressedTextureSubImage2DEXT, texture, target, 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, 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_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)
@@ -2061,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)
@@ -2558,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)
@@ -2583,7 +2571,10 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackStreamAttribsNV, GLsizei co
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedbackNV, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedbackNV, target, id)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacksNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacksNV, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacksNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacksNV, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsTransformFeedbackNV, GLuint id) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsTransformFeedbackNV, id)
MOBILEGL_GL_API GLboolean glIsTransformFeedbackNV(GLuint id) {
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
return GL_FALSE;
}
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedbackNV, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackNV, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackNV, mode, id)
@@ -3177,5 +3168,5 @@ MOBILEGL_GL_API void glVertexAttribDivisorARB(GLuint index, GLuint divisor) {
}
MOBILEGL_GL_API void glWindowRectanglesEXT(GLenum mode, GLsizei count, const GLint* box) {
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
}
File diff suppressed because it is too large Load Diff
@@ -14,6 +14,8 @@
namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
void ReadnPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize,
void* data);
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
@@ -56,7 +58,19 @@ namespace MobileGL::MG_Impl::GLImpl {
void NamedFramebufferReadBuffer(GLuint framebuffer, GLenum src);
void ClearNamedFramebufferfv(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferfi(GLuint framebuffer, GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void InvalidateNamedFramebufferData(GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments);
void InvalidateNamedFramebufferSubData(GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments,
GLint x, GLint y, GLsizei width, GLsizei height);
void InvalidateFramebuffer(GLenum target, GLsizei numAttachments, const GLenum* attachments);
void InvalidateSubFramebuffer(GLenum target, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y,
GLsizei width, GLsizei height);
void ClearNamedFramebufferiv(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferuiv(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLuint* value);
GLenum CheckNamedFramebufferStatus(GLuint framebuffer, GLenum target);
void GetFramebufferParameteriv(GLenum target, GLenum pname, GLint* params);
void FramebufferParameteri(GLenum target, GLenum pname, GLint param);
void GetNamedFramebufferParameteriv(GLuint framebuffer, GLenum pname, GLint* params);
void NamedFramebufferParameteri(GLuint framebuffer, GLenum pname, GLint param);
void GetNamedFramebufferAttachmentParameteriv(GLuint framebuffer, GLenum attachment, GLenum pname, GLint* params);
void BlitNamedFramebuffer(GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1,
GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask,
@@ -78,6 +92,6 @@ namespace MobileGL::MG_Impl::GLImpl {
SharedPtr<MG_State::GLState::ITextureObject> stencilAttachment;
};
extern UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo;
extern UniquePtr<DefaultFramebufferInfo>& pDefaultFramebufferInfo;
} // namespace FramebufferImpl
} // namespace MobileGL::MG_Impl::GLImpl
@@ -7,6 +7,7 @@
// End of Source File Header
#include "Validators.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
@@ -60,6 +61,26 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
return true;
}
Bool ValidateColorAttachmentInRange(FramebufferAttachmentType attachment, const char* caller) {
const auto first = static_cast<SizeT>(FramebufferAttachmentType::Color0);
const auto index = static_cast<SizeT>(attachment);
if (index < first) return true;
const auto colorIndex = index - first;
const auto limit = static_cast<SizeT>(
MG_Backend::pActiveBackendObject ? MG_Backend::pActiveBackendObject->GetDynamicParameters()
.MaxColorAttachments
: static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS));
if (colorIndex >= limit) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl/FramebufferImpl", caller,
std::format("Colour attachment {} is beyond GL_MAX_COLOR_ATTACHMENTS ({}).", colorIndex, limit)));
return false;
}
return true;
}
Bool ValidateRenderbufferTarget(RenderbufferTarget target) {
if (target == RenderbufferTarget::Unknown) {
using namespace MG_Util;
@@ -97,4 +118,100 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
std::format("Renderbuffer name {} is not valid.", index)));
return false;
}
Bool ValidateFramebufferParameterPname(GLenum pname, Bool isDefaultFramebuffer, Bool forSetter,
const char* caller) {
Bool isDefaultParameter = false;
switch (pname) {
case GL_FRAMEBUFFER_DEFAULT_WIDTH:
case GL_FRAMEBUFFER_DEFAULT_HEIGHT:
case GL_FRAMEBUFFER_DEFAULT_LAYERS:
case GL_FRAMEBUFFER_DEFAULT_SAMPLES:
case GL_FRAMEBUFFER_DEFAULT_FIXED_SAMPLE_LOCATIONS:
isDefaultParameter = true;
break;
case GL_DOUBLEBUFFER:
case GL_IMPLEMENTATION_COLOR_READ_FORMAT:
case GL_IMPLEMENTATION_COLOR_READ_TYPE:
case GL_SAMPLES:
case GL_SAMPLE_BUFFERS:
case GL_STEREO:
// Queryable only; glFramebufferParameteri sets none of these.
if (forSetter) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl/FramebufferImpl", caller,
std::format("pname {} is not settable on a framebuffer.",
MG_Util::ConvertGLEnumToString(pname))));
return false;
}
break;
default:
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl/FramebufferImpl", caller,
std::format("pname {} is not a framebuffer parameter.",
MG_Util::ConvertGLEnumToString(pname))));
return false;
}
// The default framebuffer has no DEFAULT_* state of its own - its shape comes from the
// surface - so those names are accepted enums it simply cannot answer or accept.
if (isDefaultFramebuffer && isDefaultParameter) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl/FramebufferImpl", caller,
std::format("pname {} does not apply to the default framebuffer.",
MG_Util::ConvertGLEnumToString(pname))));
return false;
}
return true;
}
Bool ValidateReadFramebufferForCopy(const char* caller) {
auto& framebufferObject =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
if (!framebufferObject || !framebufferObject->CheckCompleteness()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidFramebufferOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", caller,
"Read framebuffer is not framebuffer complete."));
return false;
}
const FramebufferAttachmentType readBuffer = framebufferObject->GetReadBuffer();
if (readBuffer == FramebufferAttachmentType::None ||
!framebufferObject->GetAttachment(readBuffer).IsValid()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", caller,
"Read buffer names no attachment of the read framebuffer."));
return false;
}
// SAMPLE_BUFFERS is one whenever the read buffer resolves to multisample storage. A
// multisample texture says so by its target - its sample count can legally be one - while a
// renderbuffer says so by having been given a non-zero sample count.
const auto& readAttachment = framebufferObject->GetAttachment(readBuffer);
Bool isMultisampled = false;
if (readAttachment.IsRenderbuffer() && readAttachment.GetRenderbuffer()) {
isMultisampled = readAttachment.GetRenderbuffer()->GetSamples() > 0;
} else if (readAttachment.IsTexture() && readAttachment.GetTexture()) {
const auto target = readAttachment.GetTexture()->GetTarget();
isMultisampled = target == TextureTarget::Texture2DMultisample ||
target == TextureTarget::Texture2DMultisampleArray;
}
if (isMultisampled) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", caller,
"Cannot copy from a multisampled read framebuffer."));
return false;
}
return true;
}
} // namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl
@@ -14,6 +14,21 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
Bool ValidateFramebufferTarget(FramebufferTarget target);
Bool ValidateFramebufferName(Uint index, Bool allowZero = true);
Bool ValidateFramebufferAttachmentType(FramebufferAttachmentType attachment);
// GL_COLOR_ATTACHMENTn is a token per n up to 31, but only the first GL_MAX_COLOR_ATTACHMENTS of
// them name an attachment point of a framebuffer object; the rest are INVALID_OPERATION for the
// attaching entry points (GL 4.6 core 9.2.7). Non-colour attachments pass through unchanged.
Bool ValidateColorAttachmentInRange(FramebufferAttachmentType attachment, const char* caller);
Bool ValidateRenderbufferTarget(RenderbufferTarget target);
Bool ValidateRenderbufferName(Uint index, Bool allowZero = true);
// The read-framebuffer preconditions the CopyTexSubImage family shares (GL 4.6 core 8.6): the
// read framebuffer must be complete, its read buffer must name a real attachment, and it must
// not be multisampled. Incompleteness is INVALID_FRAMEBUFFER_OPERATION, the other two are
// INVALID_OPERATION.
Bool ValidateReadFramebufferForCopy(const char* caller);
// The pname sets of glGet/FramebufferParameteri (GL 4.6 core 9.2.3). Order matters and is part
// of the contract: a name outside the table is INVALID_ENUM, and only then is a name that the
// DEFAULT framebuffer does not answer INVALID_OPERATION. Testing the framebuffer kind first
// would turn GL_FRAMEBUFFER_DEFAULT_WIDTH on framebuffer zero into the wrong error.
Bool ValidateFramebufferParameterPname(GLenum pname, Bool isDefaultFramebuffer, Bool forSetter,
const char* caller);
} // namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl
File diff suppressed because it is too large Load Diff
@@ -19,6 +19,30 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetIntegerv(GLenum pname, GLint* params);
void GetInteger64v(GLenum pname, GLint64* params);
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetFloati_v(GLenum target, GLuint index, GLfloat* data);
void GetDoublei_v(GLenum target, GLuint index, GLdouble* data);
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);
@@ -42,6 +48,10 @@ namespace MobileGL::MG_Impl::GLImpl {
GLboolean IsProgram(GLuint program);
GLboolean IsShader(GLuint shader);
void LinkProgram(GLuint program);
// GL_KHR_parallel_shader_compile / GL_ARB_parallel_shader_compile. Both names are the
// same entry point; see MaxShaderCompilerThreadsKHR_State for the semantics of count.
void MaxShaderCompilerThreadsKHR(GLuint count);
void MaxShaderCompilerThreadsARB(GLuint count);
void ShaderSource(GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length);
void UseProgram(GLuint program);
void Uniform1f(GLint location, GLfloat v0);
@@ -136,6 +146,49 @@ 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);
void ProgramUniform1d(GLuint program, GLint location, GLdouble v0);
void ProgramUniform1dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
void Uniform2d(GLint location, GLdouble v0, GLdouble v1);
void Uniform2dv(GLint location, GLsizei count, const GLdouble* value);
void ProgramUniform2d(GLuint program, GLint location, GLdouble v0, GLdouble v1);
void ProgramUniform2dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
void Uniform3d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2);
void Uniform3dv(GLint location, GLsizei count, const GLdouble* value);
void ProgramUniform3d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2);
void ProgramUniform3dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
void Uniform4d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3);
void Uniform4dv(GLint location, GLsizei count, const GLdouble* value);
void ProgramUniform4d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3);
void ProgramUniform4dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
void UniformMatrix2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void UniformMatrix3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void UniformMatrix4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void UniformMatrix2x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix2x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void UniformMatrix2x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix2x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void UniformMatrix3x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix3x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void UniformMatrix3x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix3x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void UniformMatrix4x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix4x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void UniformMatrix4x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix4x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void GetUniformdv(GLuint program, GLint location, GLdouble* params);
void ValidateProgram(GLuint program);
void ProgramParameteri(GLuint program, GLenum pname, GLint value);
GLuint CreateShaderProgramv(GLenum type, GLsizei count, const GLchar* const* strings);
void GetProgramBinary(GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary);
void ProgramBinary(GLuint program, GLenum binaryFormat, const void* binary, GLsizei length);
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode);
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
GLenum* type, GLchar* name);
} // namespace MobileGL::MG_Impl::GLImpl
@@ -0,0 +1,250 @@
// MobileGL - MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.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_ProgramPipeline.h"
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
void RecordPipelineError(ErrorCode code, const char* function, String message) {
MG_State::pGLContext->RecordError(
code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function, Move(message)));
}
// GL 4.6 core 7.4 asks only that the name came from GenProgramPipelines and has not been
// deleted - so a name that was reserved and never bound is legal here, and the command
// MATERIALIZES it rather than rejecting it.
//
// Requiring a bound object instead is what broke every separable-program conformance case
// across three families: the CTS reserves a name, calls glUseProgramStages three times and
// only then binds, which is the order the spec's own example uses. Each of those calls
// failed with INVALID_OPERATION, so the stage programs were never recorded - the pipeline
// stayed empty, GetProgramForDraw flattened nothing and the draw painted nothing, and the
// rejected calls' error was left in the queue for the harness to find. One cause, both
// symptoms.
const SharedPtr<MG_State::GLState::ProgramPipelineObject>* TryGetPipeline(GLuint pipeline,
const char* function) {
const auto& object = MG_State::pGLContext->MaterializeProgramPipelineObject(pipeline);
if (!object) {
RecordPipelineError(ErrorCode::InvalidOperation, function,
std::format("Program pipeline {} does not exist.", pipeline));
return nullptr;
}
return &object;
}
Bool ValidatePipelineCount(GLsizei n, const char* function) {
if (n < 0) {
RecordPipelineError(ErrorCode::InvalidValue, function, "n must be non-negative.");
return false;
}
return true;
}
// GL 4.6 core table 7.1 maps each stage bit onto a shader stage.
Bool TryResolveStageBit(GLbitfield bit, ShaderStage& outStage) {
switch (bit) {
case GL_VERTEX_SHADER_BIT: outStage = ShaderStage::Vertex; return true;
case GL_TESS_CONTROL_SHADER_BIT: outStage = ShaderStage::TessControl; return true;
case GL_TESS_EVALUATION_SHADER_BIT: outStage = ShaderStage::TessEval; return true;
case GL_GEOMETRY_SHADER_BIT: outStage = ShaderStage::Geometry; return true;
case GL_FRAGMENT_SHADER_BIT: outStage = ShaderStage::Fragment; return true;
case GL_COMPUTE_SHADER_BIT: outStage = ShaderStage::Compute; return true;
default: return false;
}
}
constexpr GLbitfield kAllStageBits = GL_VERTEX_SHADER_BIT | GL_TESS_CONTROL_SHADER_BIT |
GL_TESS_EVALUATION_SHADER_BIT | GL_GEOMETRY_SHADER_BIT |
GL_FRAGMENT_SHADER_BIT | GL_COMPUTE_SHADER_BIT;
} // namespace
void GenProgramPipelines(GLsizei n, GLuint* pipelines) {
if (!ValidatePipelineCount(n, __func__)) return;
if (n == 0 || !pipelines) return;
static thread_local Vector<GLuint> names;
MG_State::pGLContext->GenProgramPipelineNames(static_cast<Uint>(n), names);
Memcpy(pipelines, names.data(), static_cast<SizeT>(n) * sizeof(GLuint));
}
void CreateProgramPipelines(GLsizei n, GLuint* pipelines) {
if (!ValidatePipelineCount(n, __func__)) return;
if (n == 0 || !pipelines) return;
static thread_local Vector<GLuint> names;
MG_State::pGLContext->GenProgramPipelineNames(static_cast<Uint>(n), names);
for (GLsizei i = 0; i < n; ++i) {
pipelines[i] = names[static_cast<SizeT>(i)];
MG_State::pGLContext->CreateProgramPipelineObject(names[static_cast<SizeT>(i)]);
}
}
void DeleteProgramPipelines(GLsizei n, const GLuint* pipelines) {
if (!ValidatePipelineCount(n, __func__)) return;
if (!pipelines) return;
for (GLsizei i = 0; i < n; ++i) {
// Deleting zero, an unknown name, or a name that was only reserved is silently ignored.
MG_State::pGLContext->MarkProgramPipelineForDeletion(pipelines[i]);
}
}
void BindProgramPipeline(GLuint pipeline) {
if (pipeline != 0 && !MG_State::pGLContext->ValidateProgramPipelineName(pipeline)) {
RecordPipelineError(ErrorCode::InvalidOperation, __func__,
std::format("Program pipeline name {} is not valid.", pipeline));
return;
}
MG_State::pGLContext->BindProgramPipelineObject(pipeline);
}
GLboolean IsProgramPipeline(GLuint pipeline) {
return MG_State::pGLContext->IsProgramPipelineObject(pipeline) ? GL_TRUE : GL_FALSE;
}
void GetProgramPipelineiv(GLuint pipeline, GLenum pname, GLint* params) {
const auto* pipelineObject = TryGetPipeline(pipeline, __func__);
if (!pipelineObject || !params) return;
const auto stageProgramName = [&](ShaderStage stage) -> GLint {
const auto& program = (*pipelineObject)->GetStageProgram(stage);
return program ? static_cast<GLint>(program->GetExternalIndex()) : 0;
};
switch (pname) {
case GL_ACTIVE_PROGRAM: {
const auto& active = (*pipelineObject)->GetActiveProgram();
*params = active ? static_cast<GLint>(active->GetExternalIndex()) : 0;
break;
}
case GL_VERTEX_SHADER: *params = stageProgramName(ShaderStage::Vertex); break;
case GL_TESS_CONTROL_SHADER: *params = stageProgramName(ShaderStage::TessControl); break;
case GL_TESS_EVALUATION_SHADER: *params = stageProgramName(ShaderStage::TessEval); break;
case GL_GEOMETRY_SHADER: *params = stageProgramName(ShaderStage::Geometry); break;
case GL_FRAGMENT_SHADER: *params = stageProgramName(ShaderStage::Fragment); break;
case GL_COMPUTE_SHADER: *params = stageProgramName(ShaderStage::Compute); break;
case GL_VALIDATE_STATUS: *params = (*pipelineObject)->GetValidateStatus() ? GL_TRUE : GL_FALSE; break;
case GL_INFO_LOG_LENGTH: {
// GL counts the null terminator, and reports 0 rather than 1 for an empty log.
const auto& log = (*pipelineObject)->GetInfoLog();
*params = log.empty() ? 0 : static_cast<GLint>(log.length()) + 1;
break;
}
default:
RecordPipelineError(ErrorCode::InvalidEnum, __func__,
std::format("pname {} is not a program pipeline parameter.",
MG_Util::ConvertGLEnumToString(pname)));
break;
}
}
void GetProgramPipelineInfoLog(GLuint pipeline, GLsizei bufSize, GLsizei* length, GLchar* infoLog) {
const auto* pipelineObject = TryGetPipeline(pipeline, __func__);
if (!pipelineObject) return;
if (bufSize < 0) {
RecordPipelineError(ErrorCode::InvalidValue, __func__, "bufSize must be non-negative.");
return;
}
if (bufSize == 0 || !infoLog) {
if (length) *length = 0;
return;
}
const auto& log = (*pipelineObject)->GetInfoLog();
const auto copied = std::min<GLsizei>(bufSize - 1, static_cast<GLsizei>(log.length()));
if (copied > 0) Memcpy(infoLog, log.data(), static_cast<SizeT>(copied));
infoLog[copied] = '\0';
if (length) *length = copied;
}
void UseProgramStages(GLuint pipeline, GLbitfield stages, GLuint program) {
if (stages != GL_ALL_SHADER_BITS && (stages & ~kAllStageBits) != 0) {
RecordPipelineError(ErrorCode::InvalidValue, __func__, "stages names a bit that is not a shader stage.");
return;
}
const auto* pipelineObject = TryGetPipeline(pipeline, __func__);
if (!pipelineObject) return;
SharedPtr<MG_State::GLState::ProgramObject> programObject;
if (program != 0) {
if (!MG_State::pGLContext->ValidateProgramName(program)) {
RecordPipelineError(ErrorCode::InvalidValue, __func__,
std::format("{} is not the name of a program object.", program));
return;
}
programObject = MG_State::pGLContext->GetProgramObject(program);
if (!programObject) {
RecordPipelineError(ErrorCode::InvalidValue, __func__,
std::format("{} is not the name of a program object.", program));
return;
}
if (!programObject->GetLinkStatus()) {
RecordPipelineError(ErrorCode::InvalidOperation, __func__,
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;
for (GLbitfield bit = 1; bit != 0 && bit <= kAllStageBits; bit <<= 1) {
if ((selected & bit) == 0) continue;
ShaderStage stage = ShaderStage::Unknown;
if (!TryResolveStageBit(bit, stage)) continue;
// program == 0 clears the stage, which is what a null program reference means here.
(*pipelineObject)->SetStageProgram(stage, programObject);
}
}
void ActiveShaderProgram(GLuint pipeline, GLuint program) {
const auto* pipelineObject = TryGetPipeline(pipeline, __func__);
if (!pipelineObject) return;
if (program == 0) {
(*pipelineObject)->SetActiveProgram(nullptr);
return;
}
if (!MG_State::pGLContext->ValidateProgramName(program)) {
RecordPipelineError(ErrorCode::InvalidValue, __func__,
std::format("{} is not the name of a program object.", program));
return;
}
auto programObject = MG_State::pGLContext->GetProgramObject(program);
if (!programObject) {
RecordPipelineError(ErrorCode::InvalidValue, __func__,
std::format("{} is not the name of a program object.", program));
return;
}
if (!programObject->GetLinkStatus()) {
RecordPipelineError(ErrorCode::InvalidOperation, __func__,
std::format("Program {} has not been linked successfully.", program));
return;
}
(*pipelineObject)->SetActiveProgram(programObject);
}
void ValidateProgramPipeline(GLuint pipeline) {
const auto* pipelineObject = TryGetPipeline(pipeline, __func__);
if (!pipelineObject) return;
// Nothing here can fail today: MobileGL links each stage program on its own, so there is no
// cross-stage interface to re-check at validation time. The log stays empty, which GL allows.
(*pipelineObject)->SetValidateStatus(true);
}
} // namespace MobileGL::MG_Impl::GLImpl

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