Compare commits

..
Author SHA1 Message Date
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 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
116 changed files with 10017 additions and 1172 deletions
+5 -8
View File
@@ -209,7 +209,7 @@ jobs:
- name: Load trace cases
id: trace-cases
run: |
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk)" >> "$GITHUB_OUTPUT"
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk-matrix)" >> "$GITHUB_OUTPUT"
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
trace-fixtures:
@@ -337,13 +337,7 @@ jobs:
strategy:
fail-fast: false
max-parallel: 4
matrix:
backend:
- name: DirectGLES
gpu: software
- name: DirectVulkan
gpu: lavapipe
case: ${{ fromJSON(needs.trace-cases.outputs.android) }}
matrix: ${{ fromJSON(needs.trace-cases.outputs.android) }}
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@v1.0
@@ -426,6 +420,9 @@ jobs:
MOBILEGL_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_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_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}"
+14 -6
View File
@@ -265,6 +265,9 @@ jobs:
# crash stack without burning a CI round on an in-workflow debugger.
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
MOBILEGL_DERIVE_NUM_SUBGROUPS: "1"
MOBILEGL_ITERATIONRP_FIX_BARRIER: "1"
run: |
ulimit -c unlimited
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
@@ -491,6 +494,7 @@ jobs:
- benchmark
- integration
outputs:
matrix: ${{ steps.trace-cases.outputs.matrix }}
names: ${{ steps.trace-cases.outputs.names }}
steps:
- name: Checkout repo
@@ -498,7 +502,9 @@ jobs:
- name: Load trace cases
id: trace-cases
run: echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
run: |
echo "matrix=$(python3 tools/trace_replay/trace_cases.py --ci --format github-test-matrix)" >> "$GITHUB_OUTPUT"
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
trace-fixtures:
name: trace fixture (${{ matrix.case }})
@@ -577,11 +583,7 @@ jobs:
strategy:
fail-fast: false
max-parallel: 4
matrix:
backend:
- DirectGLES
- DirectVulkan
case: ${{ fromJSON(needs.trace-cases.outputs.names) }}
matrix: ${{ fromJSON(needs.trace-cases.outputs.matrix) }}
steps:
- name: Set Swap Space
@@ -640,6 +642,12 @@ jobs:
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
fi
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
&& [ '${{ matrix.case }}' = 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' ]; then
export MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1
export MOBILEGL_DERIVE_NUM_SUBGROUPS=1
export MOBILEGL_ITERATIONRP_FIX_BARRIER=1
fi
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
# runner has, so force it on for the OIT case it exists to fix. ForceOn
# bypasses only the vendor gate, so this exercises the real strip on
+1
View File
@@ -27,3 +27,4 @@ MobileGL/MG*/cmake-build*
tools/trace_replay/work/
__pycache__/
*.py[cod]
/.gradle
+14 -1
View File
@@ -182,6 +182,7 @@ set(ENABLE_SPVREMAPPER OFF CACHE BOOL "Enable SPVRemapper" FORCE)
set(ENABLE_OPT ON CACHE BOOL "Enable SPIRV-Tools opt usage in glslang" FORCE)
set(BUILD_EXTERNAL ON CACHE BOOL "Build external deps in External/" FORCE)
set(ENABLE_GLSLANG_INSTALL OFF CACHE BOOL "Install glslang targets" FORCE)
set(SPIRV_SKIP_EXECUTABLES ON CACHE BOOL "Skip building SPIRV-Tools executables" FORCE)
set(SPIRV_CROSS_C_API ON CACHE BOOL "Enable C API" FORCE)
set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE)
@@ -283,6 +284,10 @@ set(SOURCE_FILES
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/BakeImageFormatsPass.cpp
@@ -297,6 +302,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
MobileGL/MG_Util/SelfTest/DriverPost.cpp
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
@@ -457,7 +463,7 @@ set(MOBILEGL_INCLUDE_DIR
# Header-only submodule: no add_subdirectory, no link target. Only
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
# pimpl so no consumer target needs this path.
${CMAKE_SOURCE_DIR}/3rdparty/asio/asio/include
${CMAKE_SOURCE_DIR}/3rdparty/asio/include
)
add_library(${CMAKE_PROJECT_NAME} SHARED
@@ -669,3 +675,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()
+37 -10
View File
@@ -66,22 +66,53 @@ namespace MobileGL::MG_Config {
// - DISPLAY: X11 session variable, not MobileGL configuration.
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
// (see MG_Util/Debug/Log.cpp).
// - MOBILEGL_VALIDATE_SPIRV: test suites like SpirvPassTest exercise
// ShaderCompiler without ever running MobileGL::Initialize(), and every
// Initialize() re-runs MG_ConfigLoader::Init, which would clobber a
// programmatic override stored here (see ShaderCompiler.cpp,
// SpirvValidationEnabled).
struct FeaturesTable {
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
Bool DisableTimerQuery = false;
// MOBILEGL_ENABLE_SPIRV_VALIDATION: validate generated and transformed SPIR-V.
// Disabled by default because validation is a diagnostics-only cost.
Bool EnableSpirvValidation = false;
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
Bool UseAngle = 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.
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
// including the opt-in emulated compute path below.
Bool DisableSubgroup = 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
// FixIterationRPSubgroupScratch below instead). Off by default.
Bool MagmaEmulateSubgroup = false;
// MOBILEGL_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 FixIterationRPSubgroupScratch = QuirkOverride::Auto;
// MOBILEGL_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 IterationRPFixBarrier = false;
// MOBILEGL_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 DeriveNumSubgroups = 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
@@ -130,10 +161,6 @@ namespace MobileGL::MG_Config {
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
// version request.
Bool RelaxedSemantics = false;
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
+6 -1
View File
@@ -162,11 +162,17 @@ namespace MobileGL::MG_ConfigLoader {
inline void InitFeatures() {
auto& features = MG_Config::Features;
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
#endif
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
features.FixIterationRPSubgroupScratch =
QueryEnvQuirkOverride("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
features.IterationRPFixBarrier = QueryEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER");
features.DeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_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);
@@ -179,7 +185,6 @@ namespace MobileGL::MG_ConfigLoader {
features.EsprytForceDepthStencilReadbackEmulation =
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
features.MagmaDisableBlendedDepthWriteQuirk =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
+6
View File
@@ -15,6 +15,7 @@
#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>
@@ -51,6 +52,11 @@ namespace MobileGL {
// before a re-initialized library could pair them with the wrong
// backend's DeleteSync).
MG_Impl::GLImpl::DestroyAllSyncObjects();
// Queries die with their contexts for the same reason, and their registry
// is the same shape of process-global map: drain it here too, while the
// function table can still pair each backend handle with the backend that
// minted it.
MG_Impl::GLImpl::DestroyAllQueryObjects();
MG_Backend::pActiveBackendObject.reset();
MG_State::pGLContext.reset();
MG_State::pEGLContext.reset();
@@ -712,9 +712,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
{
.TargetGLVersion = {4, 0, 0}, // GL target version
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
.Extensions = BuildAdvertisedExtensions(false, false),
// Baseline advertisement (no runtime capabilities yet); reconciled once
// the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
.IsCompatibilityProfile = false // Is Compatibility Profile
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
@@ -734,9 +734,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// thread can only observe the extension string after the
// advertisement for its context has settled; rebuilding the whole
// list keeps the re-run after a context recreation idempotent.
void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) {
MutableRendererInfo().RendererGLInfo.Extensions =
BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported);
void UpdateAdvertisedCapabilityExtensions(const MG_External::GLESCapabilities& capabilities) {
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
capabilities.SupportsDrawIndirect,
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance);
}
} // namespace
@@ -779,11 +781,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and
// GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on
// UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension
// list is first built).
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy);
// Now that g_GLESCapabilities knows the host extensions, entry points, and ES version,
// reconcile every runtime-gated advertisement (see the comment on
// UpdateAdvertisedCapabilityExtensions for why this cannot happen when the list is first
// built).
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities);
UpdateDynamicBackendParameters();
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
PrintFormatCapabilities(GetFormatCapabilities());
@@ -924,11 +926,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
return MutableRendererInfo();
}
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
Bool drawIndirectSupported,
Bool nonZeroIndirectBaseInstanceSupported) {
Vector<GLExtension> extensions = {
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
E_GL_ARB_texture_storage_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_shader_draw_parameters,
@@ -955,6 +959,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
// extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1.
E_GL_ARB_get_program_binary};
// Minecraft 26.3 checks this prerequisite before it even considers
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
// folds the version and pointer checks into SupportsDrawIndirect.
if (drawIndirectSupported) {
extensions.push_back(E_GL_ARB_draw_indirect);
}
// ARB_base_instance also defines the last word of an indirect command. Direct calls are
// emulated on every Espryt device, but without host GL_EXT_base_instance a native indirect
// draw cannot shift divisor attributes by a GPU-authored non-zero value, so do not promise
// that incomplete case.
if (drawIndirectSupported && nonZeroIndirectBaseInstanceSupported) {
extensions.push_back(E_GL_ARB_base_instance);
}
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the host ES
// driver's: the compiler threads are MobileGL's, and glCompileShader/glLinkProgram
// are serviced entirely inside the frontend. Whether the device driver advertises
@@ -67,9 +67,12 @@ 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 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);
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
// initialized backend returns from GetBackendAPIVersionString (and that ends up
+52 -12
View File
@@ -1431,6 +1431,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_fboSyncedSlotVersions[SizeT(target)] = slotVersion;
g_fboSyncedObjectVersions[SizeT(target)] = objectVersion;
g_fboSyncedObjects[SizeT(target)] = fbo;
g_fboSyncedBackendIdGenerations[SizeT(target)] = g_attachmentBackendIdGeneration;
}
void SyncCurrentFBO() {
@@ -1461,9 +1462,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Uint16 slotVersion = slot.GetVersion();
const Uint16 objectVersion = currentFBO ? currentFBO->GetObjectVersion() : 0;
auto* currentPtr = currentFBO.get();
// The backend-id generation joins the triple: a backend texture re-mint
// (RecreateBackendTexture) moves no frontend version, so without it the
// early-out would keep the driver FBO on the deleted texture name.
if (slotVersion == g_fboSyncedSlotVersions[SizeT(target)] &&
objectVersion == g_fboSyncedObjectVersions[SizeT(target)] &&
currentPtr == g_fboSyncedObjects[SizeT(target)]) {
currentPtr == g_fboSyncedObjects[SizeT(target)] &&
g_fboSyncedBackendIdGenerations[SizeT(target)] == g_attachmentBackendIdGeneration) {
lastUpdatedFBO = currentPtr;
continue;
}
@@ -2129,6 +2134,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Bool g_broadcastMemoValid = false;
static Uint g_broadcastMemoCount = 1;
// The identity+version key above is only monotonic WITHIN one GLContext: a
// library teardown + re-init frees every FramebufferObject and restarts the
// draw slot's counter at zero, so a recycled FBO address with coinciding
// fresh versions would false-hit. Cleared at the same boundaries as the
// structurally identical SyncCurrentFBO trio (InvalidateFramebufferBindingCache).
void InvalidateBroadcastMemo() {
g_broadcastMemoValid = false;
}
void SyncCurrentProgram(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -2312,6 +2326,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
FramebufferImpl::g_fboSyncedSlotVersions[(SizeT)target] = slot.GetVersion();
FramebufferImpl::g_fboSyncedObjectVersions[(SizeT)target] = fbo ? fbo->GetObjectVersion() : 0;
FramebufferImpl::g_fboSyncedObjects[(SizeT)target] = fbo.get();
FramebufferImpl::g_fboSyncedBackendIdGenerations[(SizeT)target] =
FramebufferImpl::g_attachmentBackendIdGeneration;
}
static void BindCurrentProgramWithResources(
@@ -3061,10 +3077,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
static Bool SupportsNativeIndirectDraws() {
const auto& version = g_GLESCapabilities.GLESVersion;
const Bool esVersionOk = version.Major > 3 || (version.Major == 3 && version.Minor >= 1);
return esVersionOk && g_GLESFuncs.glDrawElementsIndirect != nullptr &&
g_GLESFuncs.glDrawArraysIndirect != nullptr;
return g_GLESCapabilities.SupportsDrawIndirect;
}
// Runs an (indexed) indirect multi-draw. When a GL_DRAW_INDIRECT_BUFFER is bound the draws
@@ -3953,8 +3966,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool resolved = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
if (resolved) {
DrainBlitErrors();
// A blit is scissored like a draw (the replicate path's guard documents the
// same rule): the application's box would clip this resolve into the
// scratch, and the second blit would then copy never-written scratch texels
// into the destination - silently, since scissor clipping raises no GL
// error. Disable for the staging blit only; the caller-visible blit below
// keeps the blit's native scissor semantics. Tracked via the render-state
// shadow, exactly like ScopedScissorDisable.
const Bool scissorWasEnabled =
(RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0;
if (scissorWasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
g_GLESFuncs.glBlitFramebuffer(left, bottom, right, top, 0, 0, width, height, GL_COLOR_BUFFER_BIT,
GL_NEAREST);
if (scissorWasEnabled) g_GLESFuncs.glEnable(GL_SCISSOR_TEST);
resolved = g_GLESFuncs.glGetError() == GL_NO_ERROR;
}
if (resolved) {
@@ -4100,13 +4124,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
// The per-draw-buffer colour masks are not covered by the non-indexed
// glColorMask above.
for (Uint index = 0; index < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++index) {
const BoolVec4& colorMask = RenderStateImpl::g_syncedRenderStateParameters.ColorMasks[index];
if (g_GLESFuncs.glColorMaski) {
g_GLESFuncs.glColorMaski(index, colorMask.x() ? GL_TRUE : GL_FALSE,
colorMask.y() ? GL_TRUE : GL_FALSE, colorMask.z() ? GL_TRUE : GL_FALSE,
colorMask.w() ? GL_TRUE : GL_FALSE);
// glColorMask above. Restore what the SYNC actually pushed, not the raw
// application masks: a widened attachment's alpha write is forced off by
// SyncRenderState and memoized in g_syncedColorMaskAlphaWidenMask, and the
// next sync early-outs on an unchanged version - restoring the undoctored
// mask here would leave alpha writes enabled on the widened buffer with
// nothing left to repair it. Same three-way pointer fallback as
// SyncRenderState's push: gating on the core name alone left EXT/OES-only
// devices holding buffer 0's mask broadcast across every buffer.
const auto colorMaskiFn = g_GLESFuncs.glColorMaski ? g_GLESFuncs.glColorMaski
: g_GLESFuncs.glColorMaskiEXT ? g_GLESFuncs.glColorMaskiEXT
: g_GLESFuncs.glColorMaskiOES;
if (colorMaskiFn) {
for (Uint index = 0; index < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++index) {
BoolVec4 colorMask = RenderStateImpl::g_syncedRenderStateParameters.ColorMasks[index];
if (index < 32 && (RenderStateImpl::g_syncedColorMaskAlphaWidenMask & (1u << index)) != 0) {
colorMask.w() = false;
}
colorMaskiFn(index, colorMask.x() ? GL_TRUE : GL_FALSE, colorMask.y() ? GL_TRUE : GL_FALSE,
colorMask.z() ? GL_TRUE : GL_FALSE, colorMask.w() ? GL_TRUE : GL_FALSE);
}
}
if (m_pausedTransformFeedback && g_GLESFuncs.glResumeTransformFeedback) {
@@ -8307,6 +8343,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Conservatively drop the redundant-glUseProgram guard: re-issuing one bind
// after a MakeCurrent is cheaper than trusting a possibly-reset context.
PrgramImpl::g_lastUsedBackendProgramId = 0;
// The GLContext becoming current may be a fresh one whose slot versions
// restarted at zero; the broadcast memo's key is only monotonic within one.
PrgramImpl::InvalidateBroadcastMemo();
BufferImpl::InvalidateIndexedBufferBindingCache();
BufferImpl::InvalidatePixelBufferBindingCaches();
FramebufferImpl::InvalidateFramebufferBindingCache();
@@ -8835,6 +8874,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
FramebufferImpl::InvalidateFramebufferBindingCache();
VertexArrayImpl::InvalidateVAOBindingCache();
PixelStoreImpl::InvalidatePackStateCache();
PrgramImpl::InvalidateBroadcastMemo();
// Texture ids belong to the dying context; wrappers destroyed later must
// not glDeleteTextures a recycled name in a successor context.
++g_backendContextGeneration;
+82 -16
View File
@@ -577,6 +577,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// immutable storage, and any prior mutable store is replaced anyway.
if (resource->id != 0) {
NoteBufferIdDeleted(resource->id);
// Driver VAOs may have this id baked into attribute/element bindings
// keyed on frontend versions this re-mint does not move.
++g_bufferBackendIdGeneration;
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
resource->id = 0;
resource->immutableStorage = false;
@@ -729,8 +732,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
void Ops_ReadbackFromGpu(BufferObject& bufferObject) {
auto* resource = ResourceOf(bufferObject);
if (!resource || resource->id == 0 || !resource->storageInitialized) return;
if (resource->persistentMapped) return; // shadow already IS the GPU storage
if (!CanTouchGLNow() || resource->contextGeneration != g_bufferContextGeneration) return;
if (resource->persistentMapped) {
// Host writes to a persistent map must not race shader writes already queued
// on this context. There is no backend copy to read back in this case.
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
return;
}
if (!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) return;
const SizeT size = std::min<SizeT>(bufferObject.GetSize(), resource->storageSize);
if (size == 0) return;
@@ -828,6 +836,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_bufferMutationEpoch.fetch_add(1, std::memory_order_release);
}
// See the declaration: re-mints of a live resource's driver id. Written only on
// the context thread (both re-mint sites run there), read only by the VAO sync.
Uint64 g_bufferBackendIdGeneration = 0;
void RegisterBufferBackendOps() {
MG_State::GLState::SetBufferBackendOps(&g_glesBufferBackendOps);
// Frontend writes issued while ops were unregistered advanced change
@@ -955,6 +967,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// here, on the thread that can, and the id is re-minted below.
if (resource->immutableStorage && !resource->persistentMapped && resource->id != 0) {
NoteBufferIdDeleted(resource->id);
// Same as the persistent-map re-mint: the dying id may be baked into
// driver VAO bindings whose frontend versions do not move for this.
++g_bufferBackendIdGeneration;
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
resource->id = 0;
resource->immutableStorage = false;
@@ -1635,8 +1650,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
// PrepareForDraw's BindCurrentVAO establishes the draw binding regardless.
const Uint32 currentConfigVersion = stateVAOObject->GetConfigVersion();
const Uint16 currentIndexBufferVersion = stateVAOObject->GetIndexBufferBindingSlot().GetVersion();
const Bool attributesDirty = !m_hasSyncedConfigVersion || m_syncedConfigVersion != currentConfigVersion;
const Bool indexBufferDirty = currentIndexBufferVersion != m_syncedIndexBufferVersion;
// A live buffer's driver id was re-minted since this twin's last emit
// (persistent-map adoption / immutable-store retire): every baked binding may
// hold the dead id while every frontend version still matches, so force a
// full re-emit. Read once; each buffer re-mints at most once per walk (its
// first EnsureBufferResource this draw), before its id is baked, so stamping
// the entry value at the end is exact - and a stale stamp only costs one
// extra full emit.
const Uint64 currentBufferIdGeneration = BufferImpl::g_bufferBackendIdGeneration;
const Bool bufferIdsRemitted = m_syncedBufferIdGeneration != currentBufferIdGeneration;
const Bool attributesDirty =
bufferIdsRemitted || !m_hasSyncedConfigVersion || m_syncedConfigVersion != currentConfigVersion;
// Identity joins the version compare: the slot version is a wrapping Uint16,
// so a wrapped-back count with a different buffer bound must still read dirty.
const MG_State::GLState::BufferObject* currentIndexBufferObject =
stateVAOObject->GetIndexBufferBindingSlot().GetBoundObject().get();
const Bool indexBufferDirty = bufferIdsRemitted ||
currentIndexBufferVersion != m_syncedIndexBufferVersion ||
currentIndexBufferObject != m_syncedIndexBufferObject;
// The baseInstance shift lives in the attribute offsets the driver already holds, so
// a change of baseInstance has to re-emit the divisor'd arrays even when the frontend
@@ -1670,10 +1701,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
Bool needsSyncFormat = allAttributeVersions[attribIndex].FormatVersion !=
m_syncedAttributeVersions[attribIndex].FormatVersion;
Bool needsSyncBuffer = allAttributeVersions[attribIndex].BufferVersion !=
m_syncedAttributeVersions[attribIndex].BufferVersion;
Bool needsSyncFormat = bufferIdsRemitted || allAttributeVersions[attribIndex].FormatVersion !=
m_syncedAttributeVersions[attribIndex].FormatVersion;
Bool needsSyncBuffer = bufferIdsRemitted || allAttributeVersions[attribIndex].BufferVersion !=
m_syncedAttributeVersions[attribIndex].BufferVersion;
if (!needsSyncFormat && !needsSyncBuffer && !needsSyncBaseInstance) continue;
// Defence in depth. The frontend already declines glVertexAttribLFormat on this
@@ -1793,6 +1824,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (indexBufferSynced) {
m_syncedIndexBufferVersion = currentIndexBufferVersion;
m_syncedIndexBufferObject = currentIndexBufferObject;
}
}
@@ -1804,6 +1836,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (emitAttributes) {
m_syncedFetchBaseInstance = fetchBaseInstance;
}
m_syncedBufferIdGeneration = currentBufferIdGeneration;
}
void BackendVertexArrayObject::SyncClientSideAttributesForDrawArrays(
@@ -1941,6 +1974,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BackendTextureObject::RecreateBackendTexture() {
if (m_backendTextureId != 0) {
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
// Application FBO twins that attached the dying id memoize on FRONTEND
// attachment versions, which this backend-side re-mint does not move;
// without this bump their driver FBOs would keep the deleted name
// attached forever (see g_attachmentBackendIdGeneration).
++FramebufferImpl::g_attachmentBackendIdGeneration;
if (m_contextGeneration == g_backendContextGeneration) {
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
}
@@ -3512,6 +3550,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_backendReadBuffer = GL_NONE;
std::fill(m_syncedFrontendAttachmentVersions.begin(), m_syncedFrontendAttachmentVersions.end(),
static_cast<Uint16>(~0u));
// Every attachment version is invalidated above, so the next walk re-attaches
// everything regardless; stamp the generation so it does not re-arm twice.
m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration;
}
static Bool SyncAttachmentObject(GLenum glFBOTarget,
@@ -4000,6 +4041,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
// -------------------- Attach texture to backend FBO -----------------------
// A backend texture id was re-minted since this twin's last walk
// (RecreateBackendTexture): any point here may still hold the dead id while
// its frontend attachment version is unchanged, so the memo below would skip
// exactly the attachment that needs repair. Re-arm every point first.
if (m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration) {
std::fill(m_syncedFrontendAttachmentVersions.begin(), m_syncedFrontendAttachmentVersions.end(),
static_cast<Uint16>(~0u));
m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration;
}
const auto& attachments = stateFBOObject->GetAllAttachmentObjects();
const auto& attachmentVersions = stateFBOObject->GetAllFramebufferAttachmentVersions();
for (SizeT i = 0; i < attachments.size(); ++i) {
@@ -4088,6 +4138,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
#endif
}
// The walk itself can re-mint an id (SyncAttachmentObject ->
// SyncMipmapsToBackend -> RecreateBackendTexture), invalidating points this
// walk already attached or version-skipped - e.g. one texture attached at two
// points. Re-enter until the generation is quiescent: every pass syncs each
// dirty texture clean, so each repeat finds strictly fewer re-mints and the
// common case (no re-mint) never takes a second pass. The head's draw/read-
// buffer syncs are memoized against their own shadows, so a repeat re-walks
// only the attachments.
if (m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration) {
SyncToBackend(stateFBOObject, asTarget);
}
}
GLenum BackendFramebufferObject::GetBackendAttachmentType(FramebufferAttachmentType frontendAtt) const {
@@ -4114,6 +4176,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions = {0};
Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
g_fboSyncedObjects = {};
Uint64 g_attachmentBackendIdGeneration = 0;
Array<Uint64, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedBackendIdGenerations = {0};
} // namespace FramebufferImpl
namespace ScratchFBOImpl {
@@ -4741,6 +4805,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("%s:", src.empty() ? "" : src.c_str());
}
auto& shaderSpirvs = stateProgramObject->GetGeneratedSpirv();
const Bool enableSpirvValidation = stateProgramObject->GetSpirvValidationEnabled();
// Blocks a transform-feedback capture request names a member of ("StageData" of
// "StageData.attrib[0]"). The Adreno ES driver accepts such a request, links, and
@@ -4794,7 +4859,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<unsigned int> loweredSpirv;
const Vector<unsigned int>* effectiveSpirv = &spirvCode;
if (glShaderType == GL_VERTEX_SHADER &&
MG_Util::ShaderTranspiler::ShaderCompiler::LowerDrawParametersForEssl(spirvCode, loweredSpirv) &&
MG_Util::ShaderTranspiler::ShaderCompiler::LowerDrawParametersForEssl(spirvCode, loweredSpirv, enableSpirvValidation) &&
!loweredSpirv.empty()) {
effectiveSpirv = &loweredSpirv;
}
@@ -4804,7 +4869,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<unsigned int> splitArrayInputSpirv;
if (glShaderType == GL_VERTEX_SHADER &&
MG_Util::ShaderTranspiler::ShaderCompiler::SplitArrayVertexInputsForEssl(
*effectiveSpirv, splitArrayInputSpirv) &&
*effectiveSpirv, splitArrayInputSpirv, enableSpirvValidation) &&
!splitArrayInputSpirv.empty() && splitArrayInputSpirv != *effectiveSpirv) {
// Only when the pass ACTUALLY split something. The optimizer hands back a
// re-serialised copy either way, and adopting that copy for every vertex
@@ -4826,7 +4891,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!xfbCaptureBlockNames.empty() &&
MG_Util::ShaderTranspiler::ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(
*effectiveSpirv, xfbCaptureBlockNames, stageFlattenedXfbBlockNames,
flattenedXfbSpirv) &&
flattenedXfbSpirv, enableSpirvValidation) &&
!flattenedXfbSpirv.empty() && !stageFlattenedXfbBlockNames.empty()) {
effectiveSpirv = &flattenedXfbSpirv;
flattenedXfbBlockNames.insert(stageFlattenedXfbBlockNames.begin(),
@@ -4842,7 +4907,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// declare the member highp; nothing else about emission changes.
Vector<unsigned int> uboPrecisionSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(
*effectiveSpirv, uboPrecisionSpirv) &&
*effectiveSpirv, uboPrecisionSpirv, enableSpirvValidation) &&
!uboPrecisionSpirv.empty()) {
effectiveSpirv = &uboPrecisionSpirv;
}
@@ -4857,7 +4922,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<unsigned int> noperspectiveSpirv;
if (!g_GLESCapabilities.SupportsNoperspectiveInterpolation &&
MG_Util::ShaderTranspiler::ShaderCompiler::EmulateNoPerspectiveForEssl(
*effectiveSpirv, noperspectiveSpirv) &&
*effectiveSpirv, noperspectiveSpirv, enableSpirvValidation) &&
!noperspectiveSpirv.empty()) {
effectiveSpirv = &noperspectiveSpirv;
}
@@ -4867,7 +4932,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// divides the coordinate of every normalized-coordinate lookup by the texture
// size, which is the whole of the difference between the two.
Vector<unsigned int> rectLoweredSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv) &&
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv, enableSpirvValidation) &&
!rectLoweredSpirv.empty()) {
effectiveSpirv = &rectLoweredSpirv;
}
@@ -4881,7 +4946,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// coordinate to (u, 0, layer) - before SPIRV-Cross can apply its own.
Vector<unsigned int> arrayImageSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DArrayImagesForEssl(*effectiveSpirv,
arrayImageSpirv) &&
arrayImageSpirv, enableSpirvValidation) &&
!arrayImageSpirv.empty()) {
effectiveSpirv = &arrayImageSpirv;
}
@@ -4900,7 +4965,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!imageFormatBake.glFormatByUniformName.empty() &&
MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresFormatlessStorageImage(*effectiveSpirv) &&
MG_Util::ShaderTranspiler::ShaderCompiler::BakeImageFormatsForEssl(
*effectiveSpirv, imageFormatBake.glFormatByUniformName, imageFormatSpirv) &&
*effectiveSpirv, imageFormatBake.glFormatByUniformName, imageFormatSpirv,
enableSpirvValidation) &&
!imageFormatSpirv.empty()) {
effectiveSpirv = &imageFormatSpirv;
}
@@ -4916,7 +4982,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<unsigned int> outputIndexSpirv;
if (glShaderType == GL_FRAGMENT_SHADER &&
MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(
*effectiveSpirv, outputIndexSpirv) &&
*effectiveSpirv, outputIndexSpirv, enableSpirvValidation) &&
!outputIndexSpirv.empty()) {
effectiveSpirv = &outputIndexSpirv;
}
+36
View File
@@ -346,6 +346,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// client-attribute staging buffers): scrub every buffer-binding shadow that
// could false-skip when the name is recycled.
void NoteBufferIdDeleted(Uint id);
// Bumped whenever a live GLESBufferResource's driver id is retired and re-minted
// while its frontend buffer stays alive (persistent-map adoption, immutable-store
// retire). The VAO twins' baked glVertexAttribPointer / element-array bindings
// key on FRONTEND versions, which a backend-side re-mint does not move - without
// this generation the driver VAO would keep fetching through the deleted id (or
// its retained store) forever. Compared and stamped by
// BackendVertexArrayObject::SyncToBackend.
extern Uint64 g_bufferBackendIdGeneration;
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
// (id, range) already at that index matches, like the array-buffer/texture/
@@ -465,6 +473,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
Bool m_isInitialized = false;
Uint16 m_syncedIndexBufferVersion = 0;
// Identity of the buffer the version above was stamped against. Raw and never
// dereferenced: the slot version is a wrapping Uint16 (see the ResolvedDrawBuffers
// IBO memo and the packed_pixels postmortem at BindCurrentFBO), so the version
// alone would read a wrapped-back count with a different buffer bound as clean.
const MG_State::GLState::BufferObject* m_syncedIndexBufferObject = nullptr;
// Aggregate gate over the per-attribute walk below: the frontend bumps its config
// version on every per-attribute version bump (the three Bump*Version functions are
// its only writers), so an unchanged config version proves every per-attribute
@@ -480,6 +493,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Kept here because it describes what was last EMITTED, which is what the next sync
// has to correct.
Uint32 m_syncedFetchBaseInstance = 0;
// BufferImpl::g_bufferBackendIdGeneration as of this twin's last emit. A
// mismatch means some live buffer's driver id was re-minted since; the ids
// baked into the driver VAO's attribute/element bindings may be dead even
// though every frontend version matches, so the next sync re-emits them all.
Uint64 m_syncedBufferIdGeneration = 0;
};
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
@@ -799,6 +817,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
using FramebufferObject = MG_State::GLState::FramebufferObject;
FramebufferObject::FramebufferAttachmentVersionArray m_syncedFrontendAttachmentVersions = {0};
// g_attachmentBackendIdGeneration as of this twin's last attachment walk. A
// mismatch means some backend texture id was re-minted since, and any of this
// twin's attachment points may still hold the dead id even though the frontend
// attachment versions match - so the walk re-attaches everything first.
Uint64 m_syncedBackendIdGeneration = 0;
};
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
@@ -888,6 +911,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
g_fboSyncedObjects;
// Bumped whenever a live backend texture's driver id is re-minted while its
// frontend texture may still be attached to application FBOs
// (BackendTextureObject::RecreateBackendTexture - e.g. a respecify of a texture
// whose backend storage went immutable). The FBO twins' attachment memos key on
// FRONTEND attachment versions, which a backend-side re-mint does not move, so
// the driver FBO would keep the deleted texture name attached forever. The
// SyncCurrentFBO gate compares this generation (below) to re-enter the sync,
// and each twin re-arms its per-attachment memo on a mismatch (SyncToBackend).
extern Uint64 g_attachmentBackendIdGeneration;
// What g_attachmentBackendIdGeneration was when SyncCurrentFBO last stamped each
// target; part of the synced tuple above.
extern Array<Uint64, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedBackendIdGenerations;
// Driver-level READ/DRAW framebuffer-binding shadow. Every backend
// glBindFramebuffer routes through BindFramebufferId so scoped helpers can
// save/restore the current binding without a glGetIntegerv round-trip (that
@@ -9,7 +9,9 @@
#include "BackendObject_DirectVulkan.h"
#include "MG_Backend/BackendObject.h"
#include "DirectVulkan.h"
#include "SubgroupSupportPolicy.h"
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include "MG_State/GLState/Core.h"
#include "MG_State/GLState/TextureState/TextureState.h"
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
@@ -383,6 +385,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MG_State::pGLContext) {
MG_State::pGLContext->InvalidateCompileEnv();
}
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
MutableFormatCapabilities());
PrintFormatCapabilities(GetFormatCapabilities());
@@ -497,20 +502,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.ExtraVendor = Nullopt,
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
.TargetGLSLVersion = {4, 6, 0},
// Baseline advertisement (no shader subgroup, no timer queries); a
// live backend reconciles its copy in UpdateAdvertisedExtensions.
.Extensions = BuildAdvertisedExtensions(false, false, false),
// Baseline advertisement (no runtime-gated capabilities); a live
// backend reconciles its copy in UpdateAdvertisedExtensions.
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
.IsCompatibilityProfile = false},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
return rendererInfo;
}
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported) {
Bool anisotropicFilteringSupported,
Bool nonZeroIndirectBaseInstanceSupported) {
Vector<GLExtension> extensions = {
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_multi_draw_indirect,
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_draw_indirect,
E_GL_ARB_multi_draw_indirect,
E_GL_ARB_indirect_parameters, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_texture_multisample,
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters,
@@ -530,6 +537,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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.DisableSubgroup) {
extensions.push_back(E_GL_KHR_shader_subgroup);
}
@@ -678,6 +692,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_vulkanCaps = capabilities;
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MG_State::pGLContext) {
MG_State::pGLContext->InvalidateCompileEnv();
}
MutableFormatCapabilities().Clear();
}
@@ -688,9 +705,16 @@ 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());
}
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
@@ -924,6 +948,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.SubgroupSupportedFeatures =
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
} else if (ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup)) {
// MOBILEGL_MAGMA_EMULATE_SUBGROUP on a device with no native subgroups: the
// advertised values describe the 32-lane virtual subgroup the compute
// lowering implements (SubgroupSupportPolicy.h / EmulateSubgroupsPass).
// GL requires the advertisement and the execution to agree, and on this
// path the emulation is what executes; only the compute stage is offered.
m_dynamicParameters.SubgroupSize = kEmulatedSubgroupSize;
m_dynamicParameters.SubgroupSupportedStages = kEmulatedSubgroupStages;
m_dynamicParameters.SubgroupSupportedFeatures = kEmulatedSubgroupFeatures;
m_dynamicParameters.SubgroupQuadOperationsInAllStages = false;
MGLOG_I("DirectVulkan: emulating 32-lane compute subgroups "
"(MOBILEGL_MAGMA_EMULATE_SUBGROUP, no native subgroup support)");
} else {
m_dynamicParameters.SubgroupSize = 0;
m_dynamicParameters.SubgroupSupportedStages = 0;
@@ -62,8 +62,8 @@ 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.
@@ -74,7 +74,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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);
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
// string an initialized backend returns from GetBackendAPIVersionString (and that
@@ -69,6 +69,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// slot's ownership unambiguous.
Uint64 programLifetimeId = 0;
Uint32 backendStateVersion = 0;
// glShaderStorageBlockBinding deliberately does NOT bump the backend state
// version, and the pipeline composite is unnamed so the in-place patch in
// DirectVulkan::ShaderStorageBlockBinding can never reach its slot - the
// mirror replay bumps only the program's block-binding version. Without this
// key the composite's slot kept serving the pre-rebind block.binding.
Uint32 blockBindingVersion = 0;
Vector<StorageBlockResource> storageBlocks;
Vector<BufferVariableResource> bufferVariables;
GLint computeWorkGroupSize[3] = {1, 1, 1};
@@ -156,18 +162,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& cache = g_programResourceCaches[program.GetExternalIndex()];
const Uint64 programLifetimeId = program.GetLifetimeId();
const Uint32 backendStateVersion = program.GetBackendStateVersion();
const Uint32 blockBindingVersion = program.GetBlockBindingVersion();
// The lifetime id must match too: a new program that reuses a deleted
// program's name and happens to land on the same backendStateVersion (both
// count from zero) would otherwise be served the dead program's reflection.
if (cache.programLifetimeId == programLifetimeId &&
cache.backendStateVersion == backendStateVersion &&
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
if (cache.blockBindingVersion != blockBindingVersion) {
// Only the block bindings moved (glShaderStorageBlockBinding, or the
// pipeline composite's mirror replay - neither touches the backend
// state version): the reflection itself is unchanged, so re-apply the
// overrides by name instead of re-running spirv-reflect. Overrides
// only ever accumulate, so a block without one still holds its
// declared binding.
for (auto& block : cache.storageBlocks) {
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
if (rebound >= 0) block.binding = static_cast<Uint32>(rebound);
}
cache.blockBindingVersion = blockBindingVersion;
}
return cache;
}
cache = {};
cache.programLifetimeId = programLifetimeId;
cache.backendStateVersion = backendStateVersion;
cache.blockBindingVersion = blockBindingVersion;
Vector<SpvReflectShaderModule> modules;
Vector<Bool> validModules;
@@ -33,6 +33,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
using SpvcSession = MG_Util::ShaderTranspiler::SpvcSession;
using SessionUsageBit = MG_Util::ShaderTranspiler::SessionUsageBit;
// Local size of a compute module, read from OpExecutionMode LocalSize; all-zero
// when absent. The compile chain pins SPIR-V 1.3, where a literal local size
// always reaches the module as this execution mode (LocalSizeId does not exist
// yet).
struct ComputeLocalSize {
Uint32 x = 0;
Uint32 y = 0;
Uint32 z = 0;
Uint64 Total() const { return static_cast<Uint64>(x) * y * z; }
};
ComputeLocalSize TryGetComputeLocalSize(const Vector<Uint>& spirv) {
constexpr SizeT kHeaderWords = 5;
constexpr Uint32 kOpExecutionMode = 16;
constexpr Uint32 kModeLocalSize = 17;
for (SizeT offset = kHeaderWords; offset < spirv.size();) {
const Uint32 wordCount = spirv[offset] >> 16u;
const Uint32 opcode = spirv[offset] & 0xffffu;
if (wordCount == 0 || offset + wordCount > spirv.size()) break;
if (opcode == kOpExecutionMode && wordCount >= 6 && spirv[offset + 2] == kModeLocalSize) {
return {spirv[offset + 3], spirv[offset + 4], spirv[offset + 5]};
}
offset += wordCount;
}
return {};
}
struct DescriptorKey {
ProgramFactory::DescriptorBindingKind kind = ProgramFactory::DescriptorBindingKind::None;
String name;
@@ -2973,8 +2999,73 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bindings.push_back(layoutBinding);
}
// UPDATE_AFTER_BIND is strictly an optional per-layout acceleration. The GL
// descriptor model still resolves every sampler uniform element independently
// (including its texture-unit sampler-object override); selecting this path
// changes neither that resolution nor the set versioning in UniformManager.
// A conservative count keeps a layout on ordinary descriptors whenever any
// relevant update-after-bind limit is not large enough, rather than asking a
// driver to reject it during vkCreateDescriptorSetLayout.
Uint32 updateAfterBindSamplers = 0;
Uint32 updateAfterBindUniformBuffers = 0;
Uint32 updateAfterBindStorageBuffers = 0;
Uint32 updateAfterBindSampledImages = 0;
Uint32 updateAfterBindStorageImages = 0;
for (Uint32 binding = 0; binding < m_maxBindings; ++binding) {
const Uint32 count = entry.bindingDescriptorCounts[binding];
switch (entry.bindingKinds[binding]) {
case DescriptorBindingKind::UniformBufferDynamic:
updateAfterBindUniformBuffers += count;
break;
case DescriptorBindingKind::CombinedImageSampler:
updateAfterBindSamplers += count;
updateAfterBindSampledImages += count;
break;
case DescriptorBindingKind::UniformTexelBuffer:
updateAfterBindSampledImages += count;
break;
case DescriptorBindingKind::StorageBuffer:
case DescriptorBindingKind::StorageTexelBuffer:
updateAfterBindStorageBuffers += count;
break;
case DescriptorBindingKind::StorageImage:
updateAfterBindStorageImages += count;
break;
case DescriptorBindingKind::None:
break;
}
}
const Uint32 updateAfterBindResources = updateAfterBindUniformBuffers + updateAfterBindStorageBuffers +
updateAfterBindSampledImages + updateAfterBindStorageImages;
const auto& uab = m_updateAfterBindLimits;
entry.usesUpdateAfterBind =
uab.enabled && updateAfterBindSamplers <= uab.maxPerStageSamplers &&
updateAfterBindUniformBuffers <= uab.maxPerStageUniformBuffers &&
updateAfterBindStorageBuffers <= uab.maxPerStageStorageBuffers &&
updateAfterBindSampledImages <= uab.maxPerStageSampledImages &&
updateAfterBindStorageImages <= uab.maxPerStageStorageImages &&
updateAfterBindResources <= uab.maxPerStageResources &&
updateAfterBindSamplers <= uab.maxSetSamplers &&
updateAfterBindUniformBuffers <= uab.maxSetUniformBuffers &&
updateAfterBindUniformBuffers <= uab.maxSetUniformBuffersDynamic &&
updateAfterBindStorageBuffers <= uab.maxSetStorageBuffers &&
updateAfterBindStorageBuffers <= uab.maxSetStorageBuffersDynamic &&
updateAfterBindSampledImages <= uab.maxSetSampledImages &&
updateAfterBindStorageImages <= uab.maxSetStorageImages;
Vector<VkDescriptorBindingFlags> bindingFlags;
VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlagsInfo{};
if (entry.usesUpdateAfterBind) {
bindingFlags.assign(bindings.size(), VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT);
bindingFlagsInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO;
bindingFlagsInfo.bindingCount = static_cast<Uint32>(bindingFlags.size());
bindingFlagsInfo.pBindingFlags = bindingFlags.data();
}
VkDescriptorSetLayoutCreateInfo setLayoutInfo{};
setLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
setLayoutInfo.flags = entry.usesUpdateAfterBind ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0;
setLayoutInfo.pNext = entry.usesUpdateAfterBind ? &bindingFlagsInfo : nullptr;
setLayoutInfo.bindingCount = static_cast<Uint32>(bindings.size());
setLayoutInfo.pBindings = bindings.data();
VK_VERIFY(vkCreateDescriptorSetLayout(m_device, &setLayoutInfo, nullptr, &entry.descriptorSetLayout),
@@ -3054,6 +3145,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& shaders = program.GetAttachedShaders();
auto& spirv = program.GetGeneratedSpirv();
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
const Bool enableSpirvValidation = program.GetSpirvValidationEnabled();
if (enableSpirvValidation) {
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
}
const ShaderStage fixupStage = PickClipFixupStage(shaders);
@@ -3094,12 +3189,81 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
// GL_KHR_shader_subgroup handling (SubgroupSupportPolicy.h). Native subgroup
// operations execute natively; module repairs keep the GL contract intact
// around them. The opt-in emulation path replaces them only on devices with no
// subgroup support at all (MOBILEGL_MAGMA_EMULATE_SUBGROUP).
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Compute) {
// Program 203 broadcasts the first reduction through
// prefixSumCache[0], then lets the second reduction overwrite that
// scratch without first rendezvousing all readers. Patch that exact
// fingerprint before either native or emulated subgroup lowering.
if (m_subgroupPolicy.fixIterationRPBarrier) {
Vector<Uint> patchedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPBarrierForVulkan(
moduleSpirvs[i], patchedSpirv, enableSpirvValidation)) {
moduleSpirvs[i] = std::move(patchedSpirv);
} else {
MGLOG_E("ProgramFactory: iterationRP barrier patch failed for program %u; "
"Program 203 keeps its shared-scratch race",
program.GetExternalIndex());
}
}
if (m_subgroupPolicy.emulateSubgroups) {
Vector<Uint> emulatedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::EmulateSubgroupsForVulkan(
moduleSpirvs[i], emulatedSpirv,
m_subgroupPolicy.maxComputeSharedMemoryBytes, enableSpirvValidation)) {
moduleSpirvs[i] = std::move(emulatedSpirv);
} else {
MGLOG_E("ProgramFactory: subgroup emulation failed for program %u; the "
"module keeps subgroup operations the device cannot execute",
program.GetExternalIndex());
}
} else {
// iterationRP under-declares its cross-subgroup scratch
// (prefixSumCache[32] for 512 invocations); on a sub-16-lane device
// grow that one fingerprinted array to what the topology needs.
if (m_subgroupPolicy.fixIterationRPSubgroupScratch) {
Vector<Uint> patchedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
moduleSpirvs[i], patchedSpirv, m_subgroupPolicy.nativeSubgroupSize,
m_subgroupPolicy.maxComputeSharedMemoryBytes,
enableSpirvValidation)) {
moduleSpirvs[i] = std::move(patchedSpirv);
} else {
MGLOG_E("ProgramFactory: iterationRP subgroup scratch patch failed for "
"program %u; the pack's declared array sizes stay in effect",
program.GetExternalIndex());
}
}
// gl_NumSubgroups must agree with the gl_SubgroupID range GL promises;
// derive it from the workgroup dimensions and gl_SubgroupSize instead of
// trusting a driver builtin that can disagree with the topology the same
// dispatch emits (Adreno reports 1 while emitting IDs 0..7 for a
// 512-invocation, 64-wide workgroup). The ceil() partition this derives
// is pinned by REQUIRE_FULL_SUBGROUPS at pipeline creation whenever the
// workgroup shape makes that flag legal (see the stage setup below).
if (m_subgroupPolicy.deriveNumSubgroups) {
Vector<Uint> derivedNumSubgroupsSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::DeriveNumSubgroupsForVulkan(
moduleSpirvs[i], derivedNumSubgroupsSpirv, enableSpirvValidation)) {
moduleSpirvs[i] = std::move(derivedNumSubgroupsSpirv);
} else {
MGLOG_E("ProgramFactory: failed to derive gl_NumSubgroups for program %u; "
"compute shaders may observe a driver-inconsistent subgroup count",
program.GetExternalIndex());
}
}
}
}
// Vulkan's SPIR-V environment has no rectangle image dimension, so a
// GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really
// stored as - which addresses [0,1] where the application addressed texels.
{
Vector<Uint> rectLoweredSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(moduleSpirvs[i], rectLoweredSpirv) &&
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(moduleSpirvs[i], rectLoweredSpirv, enableSpirvValidation) &&
!rectLoweredSpirv.empty()) {
moduleSpirvs[i] = Move(rectLoweredSpirv);
}
@@ -3112,7 +3276,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
{
Vector<Uint> invariantSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::DecoratePositionInvariantForVulkan(
moduleSpirvs[i], invariantSpirv)) {
moduleSpirvs[i], invariantSpirv, enableSpirvValidation)) {
moduleSpirvs[i] = std::move(invariantSpirv);
} else {
// The pass round-trips through SPIRV-Tools IR, so an unparseable module
@@ -3136,7 +3300,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_shaderDrawParametersEnabled) {
Vector<Uint> rebasedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
rebasedSpirv)) {
rebasedSpirv, enableSpirvValidation)) {
moduleSpirvs[i] = std::move(rebasedSpirv);
} else {
MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; "
@@ -3154,7 +3318,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
(flags & CompileOptionBit::ZeroBaseVertex)) {
Vector<Uint> zeroedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroBaseVertexForVulkan(moduleSpirvs[i],
zeroedSpirv)) {
zeroedSpirv, enableSpirvValidation)) {
moduleSpirvs[i] = std::move(zeroedSpirv);
} else {
// Failing open keeps the native builtin, which is the pre-fix behavior:
@@ -3177,7 +3341,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) {
Vector<Uint> packedSpirv;
const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan(
moduleSpirvs[i], packedSpirv);
moduleSpirvs[i], packedSpirv, enableSpirvValidation);
MOBILEGL_ASSERT(packOk,
"ProgramFactory: 64-bit vertex input packing failed for program %u; the "
"vertex-input format and the shader input type now disagree",
@@ -3201,7 +3365,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (m_unformattedFloatStorageImagesEnabled) {
Vector<Uint> unformattedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
moduleSpirvs[i], unformattedSpirv)) {
moduleSpirvs[i], unformattedSpirv, enableSpirvValidation)) {
moduleSpirvs[i] = std::move(unformattedSpirv);
} else {
MGLOG_E("ProgramFactory: failed to make float storage images unformatted for program %u",
@@ -3222,7 +3386,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
#else
// Final module the driver receives; also checked in the INFO-level CI/test
// lanes, where the DEBUG gate above is compiled out.
if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) {
if (enableSpirvValidation) {
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex());
}
#endif
@@ -3239,6 +3403,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
stage.stage = ToVkStage(shaderStage);
stage.module = module;
stage.pName = "main";
// Pin the full-subgroup launch the derived gl_NumSubgroups assumes. Legal
// exactly when the computeFullSubgroups feature is enabled and local_size_x is
// a multiple of the subgroup size (VUID-VkPipelineShaderStageCreateInfo-
// flags-02759/-02785), and only worth requesting while the resulting subgroup
// count fits the device's maxComputeWorkgroupSubgroups (lavapipe caps it at
// 32, below a 512-invocation dispatch's 64). With the bit set, "Full
// Subgroups" guarantees every subgroup launches with all invocations active,
// making the subgroup count exactly invocations / size. Shapes the flag
// cannot cover (e.g. 32x16 on a 64-wide device) fall back to the driver's
// own - spec-encouraged - tight partitioning, which the DriverPost witness
// verifies per device.
if (shaderStage == ShaderStage::Compute && m_subgroupPolicy.requireFullSubgroups &&
!m_subgroupPolicy.emulateSubgroups && m_subgroupPolicy.nativeSubgroupSize != 0) {
const ComputeLocalSize localSize = TryGetComputeLocalSize(moduleSpv);
const Uint64 fullSubgroupCount =
localSize.Total() / m_subgroupPolicy.nativeSubgroupSize;
if (localSize.x != 0 && localSize.x % m_subgroupPolicy.nativeSubgroupSize == 0 &&
fullSubgroupCount <= m_subgroupPolicy.maxComputeWorkgroupSubgroups) {
stage.flags |= VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT;
}
}
entry.modules.push_back(module);
entry.stages.push_back(stage);
@@ -3299,14 +3484,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkDescriptorSetLayout descriptorSetLayout = it->second.descriptorSetLayout;
MGLOG_D("ProgramFactory::OnFrameBoundary: evicting idle program entry hash=0x%llx",
static_cast<unsigned long long>(hash));
// erase runs ~VkProgramObject (modules/layouts destroyed); notify after
// so an observer never observes a half-destroyed entry through a lookup.
// Observers only need the handle values to purge their keyed caches.
++m_cacheStructureEpoch; // erase moves/kills entries: memoised pointers die
it = m_cache.erase(it);
// The observer destroys dependent pipelines and frees descriptor sets while
// this entry still owns its layout. Vulkan requires every descriptor set to be
// freed before its VkDescriptorSetLayout is destroyed.
if (m_evictionObserver != nullptr) {
m_evictionObserver->OnProgramEvicted(hash, descriptorSetLayout);
}
++m_cacheStructureEpoch; // erase moves/kills entries: memoised pointers die
it = m_cache.erase(it);
} else {
++it;
}
@@ -3440,7 +3625,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
#else
if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) {
if (m_enableSpirvValidation) {
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
}
#endif
@@ -76,6 +76,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
using CompileOptionFlags = Flags<CompileOptionBit>;
using HashType = Uint64;
struct UpdateAfterBindLimits {
Bool enabled = false;
Uint32 maxPerStageSamplers = 0;
Uint32 maxPerStageUniformBuffers = 0;
Uint32 maxPerStageStorageBuffers = 0;
Uint32 maxPerStageSampledImages = 0;
Uint32 maxPerStageStorageImages = 0;
Uint32 maxPerStageResources = 0;
Uint32 maxSetSamplers = 0;
Uint32 maxSetUniformBuffers = 0;
Uint32 maxSetUniformBuffersDynamic = 0;
Uint32 maxSetStorageBuffers = 0;
Uint32 maxSetStorageBuffersDynamic = 0;
Uint32 maxSetSampledImages = 0;
Uint32 maxSetStorageImages = 0;
};
struct VkProgramObject {
static constexpr Uint32 kMaxVertexInputLocations = 32;
@@ -88,6 +105,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Layout data (previously in separate VkProgramLayout)
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
// True only when this layout passed every descriptor-indexing feature and
// update-after-bind limit gate at reflection time. It controls both the
// layout/binding flags and the pool class used by UniformManager.
Bool usesUpdateAfterBind = false;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
Vector<DescriptorBindingKind> bindingKinds;
// The bindings this program actually declares, ascending. bindingKinds is sized to the
@@ -196,6 +217,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// a pipeline failure would be reported against the wrong SPIR-V.
stageSpirvDigests = std::move(other.stageSpirvDigests);
descriptorSetLayout = other.descriptorSetLayout;
usesUpdateAfterBind = other.usesUpdateAfterBind;
pipelineLayout = other.pipelineLayout;
bindingKinds = std::move(other.bindingKinds);
activeBindings = std::move(other.activeBindings);
@@ -224,11 +246,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
needsPassthroughTessControl = other.needsPassthroughTessControl;
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
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;
@@ -240,6 +264,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
other.readsBaseVertexBuiltin = false;
other.writesViewportIndexBuiltin = false;
other.needsPassthroughTessControl = false;
other.passthroughTessControlEmulatable = false;
other.lastUsedFrame = 0;
@@ -254,6 +279,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
modules = std::move(other.modules);
stageSpirvDigests = std::move(other.stageSpirvDigests); // travels with `modules` - see the move ctor
descriptorSetLayout = other.descriptorSetLayout;
usesUpdateAfterBind = other.usesUpdateAfterBind;
pipelineLayout = other.pipelineLayout;
bindingKinds = std::move(other.bindingKinds);
activeBindings = std::move(other.activeBindings);
@@ -282,11 +308,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
needsPassthroughTessControl = other.needsPassthroughTessControl;
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
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;
@@ -298,6 +326,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
other.readsBaseVertexBuiltin = false;
other.writesViewportIndexBuiltin = false;
other.needsPassthroughTessControl = false;
other.passthroughTessControlEmulatable = false;
other.lastUsedFrame = 0;
@@ -343,12 +372,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
};
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
Bool shaderDrawParametersEnabled = false,
Bool unformattedFloatStorageImagesEnabled = false)
// How this factory's compute modules implement GL_KHR_shader_subgroup. Computed
// once at renderer initialization (SubgroupSupportPolicy.h + the device's
// subgroup properties) so lowering can never disagree with the advertised
// capabilities. Native subgroup operations always execute natively; the two
// repair passes patch modules AROUND them, and the emulation only replaces them
// on opted-in devices with no subgroup support at all.
struct SubgroupLoweringPolicy {
Bool emulateSubgroups = false; // MOBILEGL_MAGMA_EMULATE_SUBGROUP, no-native-support devices
Bool fixIterationRPSubgroupScratch = false; // patch iterationRP's under-declared scratch
Bool fixIterationRPBarrier = false; // repair Program 203's shared-scratch race
Bool deriveNumSubgroups = false; // repair the NumSubgroups builtin
Bool requireFullSubgroups = false; // computeFullSubgroups enabled on the device
Uint32 nativeSubgroupSize = 0;
// Full-subgroup launches are bounded by this device limit; a dispatch whose
// workgroup needs more subgroups than this cannot request the flag.
Uint32 maxComputeWorkgroupSubgroups = 0;
// VkPhysicalDeviceLimits::maxComputeSharedMemorySize; bounds the scratch the
// emulation pass may add (0 falls back to the Vulkan minimum, 16384).
Uint32 maxComputeSharedMemoryBytes = 0;
};
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings,
Bool shaderDrawParametersEnabled,
Bool unformattedFloatStorageImagesEnabled,
Bool enableSpirvValidation,
UpdateAfterBindLimits updateAfterBindLimits,
SubgroupLoweringPolicy subgroupPolicy)
: m_device(device), m_maxBindings(maxBindings), m_config(config),
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled),
m_enableSpirvValidation(enableSpirvValidation),
m_updateAfterBindLimits(updateAfterBindLimits),
m_subgroupPolicy(subgroupPolicy) {
VkProgramObject::s_device = device;
}
// Destroys the pass-through tessellation control modules. Runs while the device is
@@ -471,6 +527,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// True only when the logical device enabled both
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
Bool m_unformattedFloatStorageImagesEnabled = 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;
@@ -156,13 +156,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frame.descriptorPools.clear();
VkDescriptorPool initialPool = VK_NULL_HANDLE;
if (!CreateDescriptorPool(m_setsPerFrame, initialPool)) {
if (!CreateDescriptorPool(m_setsPerFrame, false, initialPool)) {
MGLOG_E_ONCE("UniformDescriptorBinder::Initialize failed: cannot create frame descriptor pool %u",
frameIndex);
Shutdown();
return false;
}
frame.descriptorPools.push_back({initialPool, m_setsPerFrame, 0});
frame.descriptorPools.push_back({initialPool, m_setsPerFrame, 0, false});
MGLOG_D("UniformDescriptorBinder: frame %u descriptor pool created (maxSets=%u)", frameIndex,
m_setsPerFrame);
}
@@ -305,7 +305,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// texture/sampler resolution, completeness probe, sync, layout handling, sampler
// and view lookups - would recompute the identical descriptor.
if (trustUnchangedHint && descriptorMemoUsable && binding < m_samplerResolveMemo.size() &&
m_samplerResolveMemo[binding].infoValid) {
m_samplerResolveMemo[binding].infoValid &&
m_samplerResolveMemo[binding].infoProgramLifetimeId == program.GetLifetimeId()) {
outImageInfo = m_samplerResolveMemo[binding].info;
return true;
}
@@ -504,6 +505,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (binding < m_samplerResolveMemo.size()) {
if (descriptorMemoUsable) {
m_samplerResolveMemo[binding].info = outImageInfo;
m_samplerResolveMemo[binding].infoProgramLifetimeId = program.GetLifetimeId();
m_samplerResolveMemo[binding].infoValid = true;
} else {
// An arrayed binding publishes nothing here, and clears what a previous program
@@ -540,11 +542,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
outImageInfo = {
.sampler = m_samplerManager->GetOrCreateSampler(*samplerBindingOverride.sampler,
*samplerBindingOverride.texture),
*samplerBindingOverride.texture,
samplerBindingOverride.forceNearestFiltering,
resource->sampledLevelCount),
.imageView = samplerBindingOverride.imageView != VK_NULL_HANDLE ?
samplerBindingOverride.imageView :
(resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView),
.imageLayout = resource->layout,
.imageLayout = samplerBindingOverride.imageLayout != VK_IMAGE_LAYOUT_UNDEFINED ?
samplerBindingOverride.imageLayout : resource->layout,
};
return outImageInfo.sampler != VK_NULL_HANDLE;
}
@@ -1267,6 +1272,87 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
Bool UniformManager::SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
GLint imageLevel, GLenum imageAccess) {
return imageAccess != GL_READ_ONLY && imageLevel >= samplerBaseLevel && imageLevel <= samplerMaxLevel;
}
Bool UniformManager::CollectSamplerImageFeedback(
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<SamplerImageFeedbackBinding>& outBindings) const {
outBindings.clear();
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr,
"CollectSamplerImageFeedback: GL context is null");
if (programObj.declinedDescriptors) return true;
for (const Uint32 samplerBinding : programObj.activeBindings) {
if (samplerBinding >= m_maxBindings ||
programObj.bindingKinds[samplerBinding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
continue;
}
const Uint32 samplerCount = BindingDescriptorCount(programObj, samplerBinding);
for (Uint32 samplerElement = 0; samplerElement < samplerCount; ++samplerElement) {
MG_State::GLState::ITextureObject* sampledTexture = nullptr;
const MG_State::GLState::SamplerObject* sampledSampler = nullptr;
if (!ResolveSampledBinding(program, programObj, samplerBinding, samplerElement,
sampledTexture, sampledSampler) ||
sampledTexture == nullptr || sampledSampler == nullptr ||
MG_State::GLState::SamplesAsIncompleteTexture(sampledTexture, sampledSampler)) {
// ResolveSamplerDescriptor uses a fallback in these cases, which cannot
// alias the image-unit binding of the original texture.
continue;
}
// Multisample source images intentionally omit TRANSFER_SRC usage. Keep their existing
// direct binding instead of turning otherwise valid sampler2DMS/image2DMS dispatches
// into failed dispatches; a correct snapshot for them needs a same-sample-count path.
const TextureTarget sampledTarget = sampledTexture->GetTarget();
if (sampledTarget == TextureTarget::Texture2DMultisample ||
sampledTarget == TextureTarget::Texture2DMultisampleArray) {
continue;
}
const auto& levelRange = sampledTexture->GetLevelRange();
Bool aliasesWritableImage = false;
for (const Uint32 imageBinding : programObj.activeBindings) {
if (imageBinding >= m_maxBindings ||
programObj.bindingKinds[imageBinding] != ProgramFactory::DescriptorBindingKind::StorageImage) {
continue;
}
if (imageBinding >= programObj.samplerUniformLocationByBinding.size()) return false;
const Int baseLocation = programObj.samplerUniformLocationByBinding[imageBinding];
if (baseLocation < 0) return false;
const Uint32 imageCount = BindingDescriptorCount(programObj, imageBinding);
for (Uint32 imageElement = 0; imageElement < imageCount; ++imageElement) {
const Int location = ResolveDescriptorElementLocation(program, baseLocation, imageElement);
if (location < 0) return false;
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
return false;
}
const auto& image = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
// A sampler view exposes all layers of its target; equal texture plus an
// overlapping mip therefore aliases the writable image subresource.
if (image.Texture.get() == sampledTexture &&
SamplerOverlapsWritableImageSubresource(levelRange.x(), levelRange.y(),
image.Level, image.Access)) {
aliasesWritableImage = true;
break;
}
}
if (aliasesWritableImage) break;
}
if (aliasesWritableImage) {
outBindings.push_back({.samplerBinding = samplerBinding,
.samplerElement = samplerElement,
.texture = sampledTexture,
.sampler = sampledSampler,
.numericDomain = programObj.samplerNumericDomainByBinding[samplerBinding]});
}
}
}
return true;
}
Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 arrayElement, UboBindResult& out) const {
@@ -1388,7 +1474,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
Bool UniformManager::CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const {
Bool UniformManager::CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const {
outPool = VK_NULL_HANDLE;
if (m_device == VK_NULL_HANDLE || maxSets == 0 || m_maxBindings == 0) {
return false;
@@ -1431,7 +1517,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// (OnDescriptorSetLayoutDestroyed) so program churn recycles pool capacity.
// The cost is on set allocation only, which happens when a layout's per-frame
// cache grows - never on the per-draw reuse path.
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT |
(updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0);
poolInfo.maxSets = maxSets;
poolInfo.poolSizeCount = static_cast<Uint32>(std::size(poolSizes));
poolInfo.pPoolSizes = poolSizes;
@@ -1445,24 +1532,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
Bool UniformManager::GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex) {
Bool UniformManager::GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind) {
if (frame.descriptorPools.empty()) {
return false;
}
const auto& currentBucket = frame.descriptorPools[frame.activeDescriptorPoolIndex];
const Uint32 currentMaxSets = std::max<Uint32>(1, currentBucket.maxSets);
const auto matchingBucket = std::find_if(
frame.descriptorPools.begin(), frame.descriptorPools.end(),
[updateAfterBind](const DescriptorPoolBucket& candidate) { return candidate.updateAfterBind == updateAfterBind; });
const Uint32 currentMaxSets = matchingBucket != frame.descriptorPools.end()
? std::max<Uint32>(1, matchingBucket->maxSets)
: m_setsPerFrame;
const Uint32 grownMaxSets = currentMaxSets <= (std::numeric_limits<Uint32>::max() / 2) ? (currentMaxSets * 2)
: currentMaxSets;
VkDescriptorPool grownPool = VK_NULL_HANDLE;
if (!CreateDescriptorPool(grownMaxSets, grownPool)) {
if (!CreateDescriptorPool(grownMaxSets, updateAfterBind, grownPool)) {
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
currentMaxSets, grownMaxSets);
return false;
}
frame.descriptorPools.push_back({grownPool, grownMaxSets, 0});
frame.descriptorPools.push_back({grownPool, grownMaxSets, 0, updateAfterBind});
frame.activeDescriptorPoolIndex = static_cast<Uint32>(frame.descriptorPools.size() - 1);
MGLOG_D(
"UniformDescriptorBinder: frame %u descriptor pool exhausted, grew pool (%u -> %u sets), poolCount=%zu",
@@ -1472,14 +1563,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkResult UniformManager::AllocateDescriptorSetsFromActivePool(Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet) {
auto& frame = m_frames[frameIndex];
if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size()) {
frame.activeDescriptorPoolIndex = 0;
}
if (frame.descriptorPools[frame.activeDescriptorPoolIndex].allocatedSets >=
frame.descriptorPools[frame.activeDescriptorPoolIndex].maxSets) {
const Bool updateAfterBind = programObj.usesUpdateAfterBind;
if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size() ||
frame.descriptorPools[frame.activeDescriptorPoolIndex].updateAfterBind != updateAfterBind ||
frame.descriptorPools[frame.activeDescriptorPoolIndex].allocatedSets >=
frame.descriptorPools[frame.activeDescriptorPoolIndex].maxSets) {
const auto availableBucket = std::find_if(
frame.descriptorPools.begin(), frame.descriptorPools.end(),
[](const DescriptorPoolBucket& candidate) { return candidate.allocatedSets < candidate.maxSets; });
[updateAfterBind](const DescriptorPoolBucket& candidate) {
return candidate.updateAfterBind == updateAfterBind && candidate.allocatedSets < candidate.maxSets;
});
if (availableBucket == frame.descriptorPools.end()) {
outDescriptorSet = VK_NULL_HANDLE;
return VK_ERROR_OUT_OF_POOL_MEMORY;
@@ -1515,7 +1608,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} else {
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
if (!GrowFrameDescriptorPool(frame, frameIndex)) {
if (!GrowFrameDescriptorPool(frame, frameIndex, programObj.usesUpdateAfterBind)) {
MGLOG_E_ONCE("UniformDescriptorBinder::AcquireDescriptorSet failed: descriptor pool growth failed");
return allocResult;
}
@@ -1633,7 +1726,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 frameIndex,
VkPipelineBindPoint bindPoint,
const SamplerBindingOverride* samplerBindingOverride,
Bool samplerDescriptorsUnchangedHint) {
Bool samplerDescriptorsUnchangedHint,
const Vector<SamplerBindingOverride>* samplerBindingOverrides) {
// This program has a descriptor MobileGL could not resolve (see
// VkProgramObject::declinedDescriptors). Refusing here is the whole of the decline: the
// binding is still declared in the layout, so the pipeline is consistent with the shader
@@ -1660,7 +1754,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// sampler binding, and an unchanged (buffer, range) for the single
// dynamic UBO covers the rest - except the dynamic offset, which rebinding
// the SAME set delivers without any descriptor write.
const Bool cacheable = (samplerBindingOverride == nullptr);
const Bool cacheable = samplerBindingOverride == nullptr &&
(samplerBindingOverrides == nullptr || samplerBindingOverrides->empty());
if (cacheable && samplerDescriptorsUnchangedHint && m_fastRebindMemo.valid &&
m_fastRebindMemo.frameIndex == frameIndex &&
m_fastRebindMemo.programLifetimeId == program.GetLifetimeId() &&
@@ -1884,13 +1979,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const SizeT firstImageInfoIndex = imageInfos.size();
for (Uint32 element = 0; element < descriptorCount; ++element) {
VkDescriptorImageInfo imageInfo{};
Bool hasImage = false;
if (overrideThisBinding && element == 0) {
hasImage = ResolveSamplerDescriptorOverride(*samplerBindingOverride, imageInfo);
} else {
hasImage = ResolveSamplerDescriptor(commandBuffer, program, programObj, binding, element,
imageInfo, samplerDescriptorsUnchangedHint);
const SamplerBindingOverride* overrideForElement =
overrideThisBinding && element == 0 ? samplerBindingOverride : nullptr;
if (overrideForElement == nullptr && samplerBindingOverrides != nullptr) {
const auto overrideIt = std::find_if(
samplerBindingOverrides->begin(), samplerBindingOverrides->end(),
[binding, element](const SamplerBindingOverride& candidate) {
return candidate.binding == binding && candidate.element == element;
});
if (overrideIt != samplerBindingOverrides->end()) {
overrideForElement = &*overrideIt;
}
}
const Bool hasImage = overrideForElement != nullptr
? ResolveSamplerDescriptorOverride(*overrideForElement, imageInfo)
: ResolveSamplerDescriptor(commandBuffer, program, programObj, binding,
element, imageInfo,
samplerDescriptorsUnchangedHint);
if (!hasImage) {
MGLOG_E_ONCE(
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
@@ -26,9 +26,20 @@ 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;
};
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;
};
Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
@@ -79,6 +90,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
Bool CollectSamplerImageFeedback(
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<SamplerImageFeedbackBinding>& outBindings) const;
static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
GLint imageLevel, GLenum imageAccess);
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
// every input of every combined-image-sampler resolution is unchanged since the
// previous draw's resolve - same (texture, sampler) per binding, texture params
@@ -91,7 +108,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 frameIndex,
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
const SamplerBindingOverride* samplerBindingOverride = nullptr,
Bool samplerDescriptorsUnchangedHint = false);
Bool samplerDescriptorsUnchangedHint = false,
const Vector<SamplerBindingOverride>* samplerBindingOverrides = nullptr);
// Pure format-policy helper kept public for host regression tests. Formatted storage
// images use their shader qualifier; transformed float images use glBindImageTexture's
@@ -114,6 +132,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorPool handle = VK_NULL_HANDLE;
Uint32 maxSets = 0;
Uint32 allocatedSets = 0;
Bool updateAfterBind = false;
};
// A cached descriptor set together with the pool it was allocated from, so a
@@ -223,8 +242,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
const Vector<Uint32>& dynamicOffsets);
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
Bool CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const;
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind);
VkResult AllocateDescriptorSetsFromActivePool(
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
VkResult AcquireDescriptorSet(Uint32 frameIndex,
@@ -341,8 +360,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// lifetime id, so a freed-and-reallocated sampler or texture at the same heap address
// always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a
// stale guess can only miss and fall through to the hash, never resolve wrong. Still
// reset each frame alongside the descriptor-set cache. Indexed by binding.
// reset each frame alongside the descriptor-set cache. Indexed by binding, but the
// whole-descriptor entry is additionally keyed by program lifetime: Vulkan binding
// numbers are layout-local and unrelated programs routinely reuse binding 0/1.
struct SamplerResolveMemo {
Uint64 infoProgramLifetimeId = 0;
Uint64 samplerLifetimeId = 0;
Uint64 textureLifetimeId = 0;
VkSampler sampler = VK_NULL_HANDLE;
@@ -287,8 +287,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
it = m_cache.erase(it);
// Invalidate every VAO's state-pointer memo: the erased node's
// address may be reused by a future insert.
++m_evictionEpoch;
// address may be reused by a future insert. Advance through the
// process-wide source so the value stays unique across factory
// instances (see the member comment).
m_evictionEpoch = ++s_evictionEpochSource;
} else {
++it;
}
@@ -125,7 +125,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// construction); a memo is honored only while its recorded epoch
// matches, so an evicted entry can never be dereferenced through a
// stale memo.
Uint64 m_evictionEpoch = 1;
//
// Drawn from a process-wide source, never a per-instance counter: the VAO
// memos outlive this factory (they live on pGLContext's VAOs, the renderer
// is destroyed and recreated on EGL surface release/re-create), so a fresh
// factory restarting at a dead factory's epoch value would honor its
// dangling entry pointers. The constructor takes a value strictly greater
// than anything a predecessor ever stamped, so a dead factory's memo can
// never compare equal here - the same never-reused idiom as the lifetime ids.
// Single-threaded like the rest of the factory (renderer-thread only).
static inline Uint64 s_evictionEpochSource = 0;
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -166,7 +166,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;
@@ -831,6 +831,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// recreated since (texture + renderbuffer image epochs), and no pending clear (which alters
// load ops). Any of these differing forces the full recompute below. Portable to VK 1.1.
if (activeRenderPass != nullptr && m_rpFastValid && m_rpFastFbo == &fbo &&
m_rpFastFboLifetimeId == fbo.GetLifetimeId() &&
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
@@ -855,6 +856,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// epochs AFTER ComputeHash: its attachment SyncTexture can create an image (bump the epoch).
m_rpFastValid = true;
m_rpFastFbo = &fbo;
m_rpFastFboLifetimeId = fbo.GetLifetimeId();
m_rpFastFboVersion = fbo.GetObjectVersion();
m_rpFastSwapchainIndex = swapchainImageIndex;
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
@@ -1507,7 +1509,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ClearAttachmentPayload clearPayload{};
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
if (pending.hasInlinePayload) {
clearPayload = pending.inlinePayload;
// The inline payload was snapshotted when the entry was CREATED, but the
// clear VALUE is not part of the entry's hash - a cache hit with a newer
// glClear would replay the creation-time value and drop the new one (the
// texture path below is immune because it re-reads the live payload).
// Same defense as ClearAttachmentsOnActiveRenderPass: prefer the live
// pending clear, fall back to the snapshot only when none is queued.
if (s_renderPassManager != nullptr &&
s_renderPassManager->GetPendingRenderbufferClear(pending.renderbuffer, clearPayload)) {
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0 && pending.renderbuffer != nullptr &&
MG_Util::GetBaseInternalFormatComponentCount(pending.renderbuffer->GetInternalFormat()) ==
3) {
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
ForceOpaqueClearAlpha(clearPayload);
}
} else {
clearPayload = pending.inlinePayload;
}
} else {
if (pending.key.texture == nullptr ||
!s_clearManager->GetPendingClear(pending.key, clearPayload, liveTexture)) {
@@ -289,6 +289,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
Bool m_rpFastValid = false;
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
// The FBO's never-reused lifetime id joins the raw pointer + Uint16 version:
// a deleted FBO reallocated at the same address whose fresh setup performed
// the same number of version bumps would otherwise compare equal (both count
// from 0), serving the dead framebuffer's pass to the new object.
Uint64 m_rpFastFboLifetimeId = 0;
Uint16 m_rpFastFboVersion = 0;
Uint32 m_rpFastSwapchainIndex = 0;
Uint64 m_rpFastTexEpoch = 0;
@@ -1291,6 +1291,158 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return ok;
}
Bool VkTextureManager::SnapshotTextureForSampling(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture,
SamplerNumericDomain numericDomain,
VkPipelineStageFlags consumerShaderStageMask,
SampledTextureSnapshot& outSnapshot) {
outSnapshot = {};
TextureResource* source = SyncTextureAndGetDescriptor(texture);
if (source == nullptr || source->image == VK_NULL_HANDLE || source->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
source->sampledLevelCount == 0) {
return false;
}
const VkFormat sampledFormat = ResolveSampledImageViewFormat(source->format, numericDomain);
if (sampledFormat == VK_FORMAT_UNDEFINED ||
!AreSampledImageViewFormatsCompatible(source->format, sampledFormat)) {
MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d cannot create sampled view format=%d from image format=%d",
texture.GetExternalIndex(), static_cast<Int>(sampledFormat), static_cast<Int>(source->format));
return false;
}
if (sampledFormat != source->format &&
(source->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d needs unavailable mutable image format=%d for sampled view=%d",
texture.GetExternalIndex(), static_cast<Int>(source->format), static_cast<Int>(sampledFormat));
return false;
}
VkImageType imageType = VK_IMAGE_TYPE_2D;
switch (source->viewType) {
case VK_IMAGE_VIEW_TYPE_1D:
case VK_IMAGE_VIEW_TYPE_1D_ARRAY:
imageType = VK_IMAGE_TYPE_1D;
break;
case VK_IMAGE_VIEW_TYPE_3D:
imageType = VK_IMAGE_TYPE_3D;
break;
default:
break;
}
TextureResource snapshot{};
VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.flags = source->imageCreateFlags;
imageInfo.imageType = imageType;
imageInfo.extent = {source->extent.width, source->extent.height, source->depth};
imageInfo.mipLevels = source->mipLevels;
imageInfo.arrayLayers = source->arrayLayers;
imageInfo.format = source->format;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
// Keep the temporary's view-format list just as narrow as the source's sampler use. This
// has no storage-image usage, so unlike an app image binding the exact list is knowable.
Vector<VkFormat> viewFormats;
VkImageFormatListCreateInfo formatListInfo{};
if (m_imageFormatListSupported && (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
viewFormats.push_back(source->format);
if (sampledFormat != source->format) {
viewFormats.push_back(sampledFormat);
}
formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
formatListInfo.viewFormatCount = static_cast<Uint32>(viewFormats.size());
formatListInfo.pViewFormats = viewFormats.data();
imageInfo.pNext = &formatListInfo;
}
VmaAllocationCreateInfo allocationInfo{};
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
const VkResult createResult =
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &snapshot.image, &snapshot.allocation, nullptr);
if (createResult != VK_SUCCESS) {
MGLOG_E_ONCE("SnapshotTextureForSampling: vmaCreateImage failed result=%d textureId=%d", createResult,
texture.GetExternalIndex());
return false;
}
snapshot.extent = source->extent;
snapshot.depth = source->depth;
snapshot.arrayLayers = source->arrayLayers;
snapshot.mipLevels = source->mipLevels;
snapshot.sampledBaseMipLevel = source->sampledBaseMipLevel;
snapshot.sampledLevelCount = source->sampledLevelCount;
snapshot.format = source->format;
snapshot.aspect = source->aspect;
snapshot.viewType = source->viewType;
snapshot.sampleCount = VK_SAMPLE_COUNT_1_BIT;
snapshot.imageCreateFlags = imageInfo.flags;
snapshot.usageFlags = imageInfo.usage;
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
const VkImageAspectFlags sampledAspect =
ResolveSampledImageViewAspectMask(snapshot.aspect, texture.GetDepthStencilTextureMode());
snapshot.sampledView = CreateImageView(snapshot.image, sampledFormat, sampledAspect, snapshot.viewType,
snapshot.sampledBaseMipLevel, snapshot.sampledLevelCount, 0,
snapshot.arrayLayers, &sampledComponents);
if (snapshot.sampledView == VK_NULL_HANDLE) {
MGLOG_E_ONCE("SnapshotTextureForSampling: failed to create sampled view textureId=%d", texture.GetExternalIndex());
return false;
}
VkPipelineStageFlags sourceStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags sourceAccessMask = 0;
const VkImageLayout sourceLayout = source->layout;
GetImageTransitionSourceState(sourceLayout, sourceStageMask, sourceAccessMask);
if (!TransitionImageLayout(commandBuffer, source->image, source->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
sourceStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, sourceAccessMask,
VK_ACCESS_TRANSFER_READ_BIT, source->aspect, 0, source->mipLevels) ||
!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
VK_ACCESS_TRANSFER_WRITE_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel,
snapshot.sampledLevelCount)) {
return false;
}
Vector<VkImageCopy> copyRegions;
copyRegions.reserve(snapshot.sampledLevelCount);
for (Uint32 level = snapshot.sampledBaseMipLevel;
level < snapshot.sampledBaseMipLevel + snapshot.sampledLevelCount; ++level) {
VkImageCopy copy{};
copy.srcSubresource = {source->aspect, level, 0, source->arrayLayers};
copy.dstSubresource = {snapshot.aspect, level, 0, snapshot.arrayLayers};
copy.extent = {std::max(source->extent.width >> level, 1u),
std::max(source->extent.height >> level, 1u),
std::max(source->depth >> level, 1u)};
copyRegions.push_back(copy);
}
vkCmdCopyImage(commandBuffer, source->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, snapshot.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<Uint32>(copyRegions.size()), copyRegions.data());
if (!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout,
ResolveSampledReadOnlyLayout(snapshot.aspect), VK_PIPELINE_STAGE_TRANSFER_BIT,
consumerShaderStageMask, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_SHADER_READ_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel,
snapshot.sampledLevelCount) ||
!TransitionImageLayout(commandBuffer, source->image, source->layout, sourceLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, consumerShaderStageMask,
VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
source->aspect, 0, source->mipLevels)) {
return false;
}
StampResourceRecordingUse(*source);
outSnapshot = {.imageView = snapshot.sampledView, .layout = snapshot.layout};
DeferResourceRelease(Move(snapshot));
return true;
}
void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) {
m_storageImageTextures.insert(MakeTextureIdentity(&texture));
}
@@ -1841,6 +1993,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
texture.GetExternalIndex(),
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage));
// The preserved image was written by GPU work that may still be in flight
// (preserve requires layout != UNDEFINED); park it on the deferred ring
// like every other destruction path instead of letting the unique_ptr
// destroy it synchronously under the GPU.
if (preservedResource) {
DeferResourceRelease(Move(*preservedResource));
}
return false;
}
}
@@ -1863,6 +2022,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static_cast<Int>(imageInfo.samples), static_cast<Int>(imageInfo.format));
resource.image = VK_NULL_HANDLE;
resource.allocation = nullptr;
// Same as the probe failure above: the preserved live image must go through
// the deferred ring, never a synchronous destructor while frames that
// reference it are still in flight.
if (preservedResource) {
DeferResourceRelease(Move(*preservedResource));
}
return false;
}
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
@@ -310,6 +310,11 @@ public:
static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
};
struct SampledTextureSnapshot {
VkImageView imageView = VK_NULL_HANDLE;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
};
Bool Initialize(const InitInfo& initInfo);
void Shutdown();
void BeginFrame(Uint32 frameIndex);
@@ -343,6 +348,13 @@ public:
VkImageLayout newLayout);
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
// Copies the complete sampler-visible mip range into a transient sampled image. The source is
// restored to its prior layout, so image-store descriptors continue to name the original image.
// The transient ownership is tied to the current frame slot and is safe through its submission.
Bool SnapshotTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture,
SamplerNumericDomain numericDomain,
VkPipelineStageFlags consumerShaderStageMask,
SampledTextureSnapshot& outSnapshot);
// Recording-generation bookkeeping for the pre-pass command stream. The
// generation advances every time the frame command buffer (re)begins
@@ -8,6 +8,7 @@
#include "VulkanRenderer.h"
#include "MG_Backend/DirectVulkan/SubgroupSupportPolicy.h"
#include "MG_Backend/DirectGLES/Utils.h"
#include "VertexInputStateFactory.h"
#include "VertexInputStateBuilder.h"
@@ -238,11 +239,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// nothing else across all of gl33.
//
// The mapping below is derived from - and at full extent exactly reproduces - the pixel
// mapping RemapDefaultFboReadbackToGLOrientation has always used:
// mapping VulkanRenderer::RemapDefaultFramebufferReadback uses:
// identity : image(x, H-1-y) -> flip Y
// 180 : image(W-1-x, y) -> mirror X (the rotation already flips the rows)
// Quarter turns swap the axes; nothing in this renderer models that (the readback declines to
// remap them and the viewport path only rescales), so they are left exactly as they were.
// Quarter turns swap the axes and are handled by MapDefaultFramebufferReadbackRect rather than
// this same-axis helper.
struct DefaultFramebufferRectMapping {
Bool flipY = false;
Bool mirrorX = false;
@@ -1480,7 +1481,8 @@ void main() {
const char* label = nullptr;
};
static Uint32 ComputeMaxProgramBindings(const VkPhysicalDeviceProperties& properties) {
static Uint32 ComputeMaxProgramBindings(const VkPhysicalDeviceProperties& properties,
const ProgramFactory::UpdateAfterBindLimits& updateAfterBindLimits) {
const auto& limits = properties.limits;
static constexpr Uint32 kMinProgramBindings = 16;
static constexpr Uint32 kMaxProgramBindingsCap = 256;
@@ -1495,6 +1497,21 @@ void main() {
maxBindings = std::min(maxBindings, maxCombinedImageSamplers);
maxBindings = std::min(maxBindings, maxSampledImages + maxDynamicUniformBuffers);
if (updateAfterBindLimits.enabled) {
const Uint32 updateAfterBindSamplers = std::min(updateAfterBindLimits.maxPerStageSamplers,
updateAfterBindLimits.maxSetSamplers);
const Uint32 updateAfterBindSampledImages = std::min(updateAfterBindLimits.maxPerStageSampledImages,
updateAfterBindLimits.maxSetSampledImages);
const Uint32 updateAfterBindDynamicUniformBuffers =
std::min(updateAfterBindLimits.maxPerStageUniformBuffers,
updateAfterBindLimits.maxSetUniformBuffersDynamic);
Uint32 updateAfterBindBindings = updateAfterBindLimits.maxPerStageResources;
updateAfterBindBindings = std::min(updateAfterBindBindings, updateAfterBindSamplers);
updateAfterBindBindings =
std::min(updateAfterBindBindings, updateAfterBindSampledImages + updateAfterBindDynamicUniformBuffers);
maxBindings = std::max(maxBindings, updateAfterBindBindings);
}
maxBindings = std::max(kMinProgramBindings, maxBindings);
maxBindings = std::min(kMaxProgramBindingsCap, maxBindings);
return maxBindings;
@@ -2151,47 +2168,6 @@ void main() {
return static_cast<Uint8>(value * 255.0f + 0.5f);
}
// Re-order the copied BLOCK - not the whole image - from the default framebuffer's stored
// orientation into GL's. The caller has already aimed the copy at the right place with
// MapDefaultFramebufferRectAxis, so what arrives here is exactly the requested
// rectWidth x rectHeight rect, and all that is left is the order of rows (identity) or of
// columns (180) WITHIN it.
//
// This used to iterate the full swapchain extent and index both sides with that stride,
// which is why its caller could only use it on an exact full-extent read - and why every
// partial glReadPixels of the default framebuffer came back in Vulkan row order. Only
// identity/180 share the swapchain extent with the default framebuffer; 90/270 swap
// extents and are still declined.
static Bool RemapDefaultFboReadbackToGLOrientation(const Uint8* rawPixels,
Uint32 rectWidth,
Uint32 rectHeight,
VkSurfaceTransformFlagBitsKHR preTransform,
SizeT texelSize,
Uint8* outPixels) {
if (IsQuarterTurnPreTransform(preTransform)) {
return false;
}
if (rectWidth == 0 || rectHeight == 0 || texelSize == 0) {
return false;
}
const DefaultFramebufferRectMapping mapping = GetDefaultFramebufferRectMapping(preTransform);
const SizeT rowBytes = static_cast<SizeT>(rectWidth) * texelSize;
for (Uint32 outY = 0; outY < rectHeight; ++outY) {
const Uint32 srcY = mapping.flipY ? (rectHeight - 1 - outY) : outY;
const Uint8* srcRow = rawPixels + static_cast<SizeT>(srcY) * rowBytes;
Uint8* dstRow = outPixels + static_cast<SizeT>(outY) * rowBytes;
if (!mapping.mirrorX) {
Memcpy(dstRow, srcRow, rowBytes);
continue;
}
for (Uint32 outX = 0; outX < rectWidth; ++outX) {
Memcpy(dstRow + static_cast<SizeT>(outX) * texelSize,
srcRow + static_cast<SizeT>(rectWidth - 1 - outX) * texelSize, texelSize);
}
}
return true;
}
static SizeT AlignPixelRow(SizeT rowBytes, Int alignment) {
const SizeT resolvedAlignment = static_cast<SizeT>(std::max(alignment, 1));
return (rowBytes + resolvedAlignment - 1) & ~(resolvedAlignment - 1);
@@ -2738,6 +2714,95 @@ void main() {
return formatInfo.texel_block_size;
}
Bool VulkanRenderer::MapDefaultFramebufferReadbackRect(
GLint x, GLint y, GLsizei width, GLsizei height, VkExtent2D imageExtent,
VkSurfaceTransformFlagBitsKHR preTransform, VkOffset2D* imageOffset,
VkExtent2D* imageCopyExtent) {
if (width <= 0 || height <= 0 || imageOffset == nullptr || imageCopyExtent == nullptr) {
return false;
}
const Int imageWidth = static_cast<Int>(imageExtent.width);
const Int imageHeight = static_cast<Int>(imageExtent.height);
Int mappedX = x;
Int mappedY = y;
Uint32 mappedWidth = static_cast<Uint32>(width);
Uint32 mappedHeight = static_cast<Uint32>(height);
// InsertPositionFixup first flips GL Y and then applies the surface transform. In pixel
// coordinates that gives these half-open rectangle mappings into the stored image:
// identity: (x, H-y-h), 90: (y, x), 180: (W-x-w, y), 270: (H-y-h, W-x-w).
// Quarter turns also transpose the copied block's extent.
switch (preTransform) {
case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR:
mappedX = y;
mappedY = x;
mappedWidth = static_cast<Uint32>(height);
mappedHeight = static_cast<Uint32>(width);
break;
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
mappedX = imageWidth - x - width;
mappedY = y;
break;
case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR:
mappedX = imageWidth - y - height;
mappedY = imageHeight - x - width;
mappedWidth = static_cast<Uint32>(height);
mappedHeight = static_cast<Uint32>(width);
break;
default:
mappedY = imageHeight - y - height;
break;
}
if (mappedX < 0 || mappedY < 0 || mappedWidth > imageExtent.width ||
mappedHeight > imageExtent.height ||
static_cast<Uint64>(mappedX) + mappedWidth > imageExtent.width ||
static_cast<Uint64>(mappedY) + mappedHeight > imageExtent.height) {
return false;
}
*imageOffset = {mappedX, mappedY};
*imageCopyExtent = {mappedWidth, mappedHeight};
return true;
}
Bool VulkanRenderer::RemapDefaultFramebufferReadback(
const Uint8* rawPixels, Uint32 logicalWidth, Uint32 logicalHeight,
VkSurfaceTransformFlagBitsKHR preTransform, SizeT texelSize, Uint8* outPixels) {
if (rawPixels == nullptr || outPixels == nullptr || logicalWidth == 0 || logicalHeight == 0 ||
texelSize == 0) {
return false;
}
const Uint32 rawWidth = IsQuarterTurnPreTransform(preTransform) ? logicalHeight : logicalWidth;
for (Uint32 outY = 0; outY < logicalHeight; ++outY) {
for (Uint32 outX = 0; outX < logicalWidth; ++outX) {
Uint32 srcX = outX;
Uint32 srcY = outY;
switch (preTransform) {
case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR:
srcX = outY;
srcY = outX;
break;
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
srcX = logicalWidth - 1 - outX;
break;
case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR:
srcX = logicalHeight - 1 - outY;
srcY = logicalWidth - 1 - outX;
break;
default:
srcY = logicalHeight - 1 - outY;
break;
}
Memcpy(outPixels + (static_cast<SizeT>(outY) * logicalWidth + outX) * texelSize,
rawPixels + (static_cast<SizeT>(srcY) * rawWidth + srcX) * texelSize,
texelSize);
}
}
return true;
}
Bool VulkanRenderer::ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
GLsizei width, GLsizei height, GLenum destinationFormat,
GLenum destinationType, SizeT destinationRowStride,
@@ -2962,7 +3027,7 @@ void main() {
succeeded = m_renderPassManager->Initialize();
MOBILEGL_ASSERT(succeeded, "VkRenderPassManager initialization failed.");
const Uint32 maxProgramBindings = ComputeMaxProgramBindings(m_physicalDevice.properties);
const Uint32 maxProgramBindings = ComputeMaxProgramBindings(m_physicalDevice.properties, m_updateAfterBindLimits);
MGLOG_I("DirectVulkan: using %u program descriptor bindings", maxProgramBindings);
if (IsPowerVRDevice(m_physicalDevice.properties)) {
m_config.DisablePipelineCache = true;
@@ -2994,9 +3059,23 @@ void main() {
}
PipelineFactory::SetSuppressBlendedDepthWrite(suppressBlendedDepthWrite);
}
ProgramFactory::SubgroupLoweringPolicy subgroupPolicy{};
subgroupPolicy.emulateSubgroups = ShouldEmulateSubgroups(m_nativeSubgroupSupported);
subgroupPolicy.fixIterationRPSubgroupScratch =
m_nativeSubgroupSupported && ShouldFixIterationRPSubgroupScratch();
subgroupPolicy.fixIterationRPBarrier = ShouldFixIterationRPBarrier();
subgroupPolicy.deriveNumSubgroups =
m_nativeSubgroupSupported && ShouldDeriveNumSubgroups();
subgroupPolicy.requireFullSubgroups = m_computeFullSubgroupsFeatureEnabled;
subgroupPolicy.nativeSubgroupSize = m_nativeSubgroupSize;
subgroupPolicy.maxComputeWorkgroupSubgroups = m_maxComputeWorkgroupSubgroups;
subgroupPolicy.maxComputeSharedMemoryBytes =
m_physicalDevice.properties.limits.maxComputeSharedMemorySize;
m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings,
m_shaderDrawParametersFeatureEnabled,
m_unformattedFloatStorageImagesEnabled);
m_unformattedFloatStorageImagesEnabled,
MG_Config::Features.EnableSpirvValidation,
m_updateAfterBindLimits, subgroupPolicy);
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
// The swapchain already exists at this point (Initialize creates it first), so seed the
// height the factory could not be told about from CreateSwapchain.
@@ -3245,6 +3324,11 @@ void main() {
indexView.indexByteSize > bufferSize - indexView.indexByteOffset) {
return false;
}
// Recorded-but-unexecuted GPU writes (XFB capture, SSBO, storage texel
// buffer) land in the coherent mapping this scan is about to read;
// submit-and-wait first, exactly like the restart-index rewrite does.
// A no-op unless the gpu-write flag is set.
indexBufferShared->SyncGpuWrites();
indexBufferShared->SyncPersistentMappedRange();
indexBytes = indexBufferShared->MappedData() + indexView.indexByteOffset;
} else {
@@ -3482,6 +3566,16 @@ void main() {
const Uint8* sourceData, SizeT sourceStride,
SizeT elementSize, SizeT elementCount,
BufferSlice& outSlice) -> Bool {
// A resolved stride of 0 is the binding model's "never advance" (see the
// factory's layout notes): exactly one element is converted and every vertex
// reads it. That single element is read at offset 0, so the stride is never
// actually used - but both converters reject 0 as a degenerate input, which
// made the documented single-element conversion unreachable and silently
// dropped every draw using such a binding. Substitute the element's own
// size; the caller's cache key still carries the distinct stride 0.
if (sourceStride == 0 && elementCount == 1) {
sourceStride = elementSize;
}
const void* uploadData = nullptr;
VkDeviceSize uploadSize = 0;
switch (conversion) {
@@ -3615,6 +3709,12 @@ void main() {
return false;
}
// A GPU-written source (XFB capture, SSBO, storage texel buffer) has its
// bytes produced by commands that are merely RECORDED at this point, and
// MappedData() aliases the coherent GPU memory they will write into -
// converting now would read pre-write garbage. Submit-and-wait first,
// mirroring the restart-index rewrite; a flag-test no-op otherwise.
sourceBufferShared->SyncGpuWrites();
sourceBufferShared->SyncPersistentMappedRange();
const SizeT availableElementCount =
sourceStride == 0 ? 1 : 1 + (sourceSize - baseOffset - elementSize) / sourceStride;
@@ -3908,7 +4008,7 @@ void main() {
// Skips the per-draw GetBackendResource chase into a cold resource object.
Bool sliceStillValid = false;
const Uint64 frameSerial = m_bufferManager.GetFrameSerial();
if (indexMemo->indexFrameSerial == frameSerial &&
if (indexMemo->indexFrameSerial == frameSerial && !indexMemo->indexBufferMapped &&
indexMemo->indexSliceEpochCounter == m_bufferManager.GetSliceEpochCounter()) {
sliceStillValid = true;
}
@@ -3971,6 +4071,9 @@ void main() {
indexMemo->indexVkBuffer = slice.buffer;
indexMemo->indexSliceOffset = slice.offset;
indexMemo->indexFrameSerial = m_bufferManager.GetFrameSerial();
// A host-mapped EBO can mutate its shadow with no epoch bump; the hit
// path declines on this flag (mirror of anyBufferMapped).
indexMemo->indexBufferMapped = indexBufferShared->IsMapped();
}
}
const VkDeviceSize indexBindOffset =
@@ -5346,7 +5449,81 @@ void main() {
}
return true;
}
Bool VulkanRenderer::PrepareSamplerImageFeedbackSnapshots(
FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
VkPipelineStageFlags consumerShaderStageMask) {
auto& feedbackBindings = m_samplerImageFeedbackScratch;
auto& overrides = m_samplerImageBindingOverridesScratch;
overrides.clear();
if (!programObj.hasStorageImages) {
feedbackBindings.clear();
return true;
}
if (!m_uniformManager->CollectSamplerImageFeedback(program, programObj, feedbackBindings)) {
MGLOG_E_ONCE("%s: failed to collect sampler/image feedback for program=%u", __func__,
program.GetExternalIndex());
return false;
}
if (feedbackBindings.empty()) {
return true;
}
// Copy and layout barriers cannot be recorded inside a render pass. A graphics draw only
// gets here after an actual sampled/writable-image mip overlap was found, so ordinary
// graphics draws retain the active pass.
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
}
struct SnapshotCacheEntry {
MG_State::GLState::ITextureObject* texture = nullptr;
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
VkTextureManager::SampledTextureSnapshot snapshot{};
};
Vector<SnapshotCacheEntry> snapshotCache;
snapshotCache.reserve(feedbackBindings.size());
overrides.reserve(feedbackBindings.size());
for (const auto& feedback : feedbackBindings) {
VkTextureManager::SampledTextureSnapshot snapshot{};
const auto existing = std::find_if(
snapshotCache.begin(), snapshotCache.end(), [&feedback](const SnapshotCacheEntry& candidate) {
return candidate.texture == feedback.texture && candidate.numericDomain == feedback.numericDomain;
});
if (existing != snapshotCache.end()) {
snapshot = existing->snapshot;
} else {
if (!m_textureManager->SnapshotTextureForSampling(frame.commandBuffer, *feedback.texture,
feedback.numericDomain, consumerShaderStageMask,
snapshot) ||
snapshot.imageView == VK_NULL_HANDLE) {
MGLOG_E_ONCE("%s: failed to snapshot textureId=%d for sampler binding=%u element=%u", __func__,
feedback.texture != nullptr ? feedback.texture->GetExternalIndex() : 0,
feedback.samplerBinding, feedback.samplerElement);
return false;
}
snapshotCache.push_back({.texture = feedback.texture,
.numericDomain = feedback.numericDomain,
.snapshot = snapshot});
}
overrides.push_back({
.binding = feedback.samplerBinding,
.element = feedback.samplerElement,
.texture = feedback.texture,
.sampler = feedback.sampler,
.imageView = snapshot.imageView,
.imageLayout = snapshot.layout,
.forceNearestFiltering = feedback.numericDomain == SamplerNumericDomain::SignedInteger ||
feedback.numericDomain == SamplerNumericDomain::UnsignedInteger,
});
if (program.GetExternalIndex() == 194 && feedback.texture->GetExternalIndex() == 75) {
MGLOG_D_ONCE("sampler/image feedback snapshot: program=194 texture=75 binding=%u element=%u view=%p",
feedback.samplerBinding, feedback.samplerElement, snapshot.imageView);
}
}
return true;
}
// The scissor rectangle Vulkan needs for ARB_viewport_array index `index`. Vulkan has no
// per-viewport scissor-test TOGGLE - a scissor rectangle always applies - so an index whose
@@ -5566,6 +5743,22 @@ void main() {
if (program.GetBackendStateVersion() != snap.programVersion) {
return false;
}
// glBegin/EndTransformFeedback moves no key this fast path otherwise observes
// (the design makes capture a compile-option FLAG precisely because no version
// bumps, VulkanRenderer.h's pipeline-memo note) - but the snapshot bakes that
// flag into resolvedTransformFlags and the pipeline. Recompute the one dynamic
// bit (the full path's exact predicate) and decline on a mismatch, or the first
// captured draw after glBeginTransformFeedback would bind the undecorated
// variant and silently capture nothing while the CPU bookkeeping advances.
const Bool wantsXfbCapture = m_transformFeedbackFeatureEnabled &&
MG_State::pGLContext->IsTransformFeedbackActive() &&
program.GetTransformFeedbackVaryingCount() > 0;
const Bool snapHasXfbCapture =
static_cast<Bool>(ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags) &
ProgramFactory::CompileOptionBit::XfbCapture);
if (wantsXfbCapture != snapHasXfbCapture) {
return false;
}
// A changed VAO does NOT decline: the VAO only feeds the pipeline's vertex
// input state (re-resolved below through the layout-keyed memo, so N VAOs
// sharing one attribute layout share one pipeline) and the vertex/index
@@ -5579,6 +5772,7 @@ void main() {
const auto& drawFbo =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
if (static_cast<const void*>(drawFbo.get()) != snap.drawFbo ||
drawFbo->GetLifetimeId() != snap.drawFboLifetimeId ||
drawFbo->GetObjectVersion() != snap.fboVersion) {
return false;
}
@@ -5762,8 +5956,14 @@ void main() {
}
const Uint64 samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
if (samplingResolutionGeneration != snap.samplingResolutionGeneration) {
snap.samplingResolutionGeneration = samplingResolutionGeneration;
samplerDescriptorsUnchanged = false;
// Decline, not re-arm: snap.resolvedTransformFlags bakes the
// ExplicitLod0Sampling verdict, which reads the effective sampler's
// filters/aniso/LOD range - exactly the state this counter tracks.
// Re-arming the stamp here would rebuild the descriptors but keep the
// stale SPIR-V variant forever (every later draw compares equal again).
// Same shape as the erase-epoch declines above; costs one full-path draw
// per sampler/shape change, and the full path's LOD memo re-probes.
return false;
}
// Everything the full path would re-resolve is provably unchanged - or, for
@@ -5943,11 +6143,18 @@ void main() {
const Uint64 lodProgramLifetimeId = program.GetLifetimeId();
const Uint32 lodProgramVersion = program.GetBackendStateVersion();
const Uint64 lodBindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
// The probe also reads the EFFECTIVE sampler's filters/aniso/LOD range
// (ProgramSamplesOnlySingleLevelTextures), and those setters bump ONLY the
// sampling-resolution generation - not the texture params version the sum
// below covers. Without this key a filter/aniso change would keep serving
// the stale verdict.
const Uint64 lodSamplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
Bool lodMemoHit = false;
if (m_lastLodDecisionValid && m_lastSampledSetValid &&
m_lastLodProgramLifetimeId == lodProgramLifetimeId &&
m_lastLodProgramVersion == lodProgramVersion &&
m_lastLodBindGeneration == lodBindGeneration && m_lastLodBaseFlags == transformFlags &&
m_lastLodBindGeneration == lodBindGeneration &&
m_lastLodSamplingGeneration == lodSamplingGeneration && m_lastLodBaseFlags == transformFlags &&
m_lastSampledSetProgramLifetimeId == lodProgramLifetimeId &&
m_lastSampledSetProgramVersion == lodProgramVersion &&
m_lastSampledSetBindGeneration == lodBindGeneration) {
@@ -5972,6 +6179,7 @@ void main() {
m_lastLodProgramLifetimeId = lodProgramLifetimeId;
m_lastLodProgramVersion = lodProgramVersion;
m_lastLodBindGeneration = lodBindGeneration;
m_lastLodSamplingGeneration = lodSamplingGeneration;
m_lastLodBaseFlags = baseFlags;
m_lastLodResultFlags = transformFlags;
m_lastLodParamsSum = 0; // filled below once the sampled set is known
@@ -6028,6 +6236,11 @@ void main() {
MGLOG_E_ONCE("SetupDraw skipped: storage image preparation failed");
return false;
}
if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj,
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT)) {
MGLOG_E_ONCE("SetupDraw skipped: sampler/image feedback snapshot failed");
return false;
}
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
@@ -6272,7 +6485,9 @@ void main() {
}
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex());
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(),
VK_PIPELINE_BIND_POINT_GRAPHICS, nullptr, false,
m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch);
if (!boundUniforms) {
MGLOG_E_ONCE("SetupDraw skipped: BindProgramUniformBuffers failed");
return false;
@@ -6308,6 +6523,7 @@ void main() {
snap.vaoLifetimeId = vao.GetLifetimeId();
snap.vaoConfigVersion = vao.GetConfigVersion();
snap.drawFbo = drawFbo.get();
snap.drawFboLifetimeId = drawFbo->GetLifetimeId();
snap.fboVersion = drawFbo->GetObjectVersion();
snap.drawFboIsDefault = drawFboIsDefault;
snap.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin);
@@ -6395,6 +6611,11 @@ void main() {
MGLOG_E_ONCE("DispatchCompute skipped: storage image preparation failed");
return;
}
if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT)) {
MGLOG_E_ONCE("DispatchCompute skipped: sampler/image feedback snapshot failed");
return;
}
const VkPipeline pipeline = GetOrCreateComputePipeline(programObj);
if (pipeline == VK_NULL_HANDLE) {
@@ -6406,7 +6627,8 @@ void main() {
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(),
VK_PIPELINE_BIND_POINT_COMPUTE);
VK_PIPELINE_BIND_POINT_COMPUTE, nullptr, false,
m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch);
if (!boundUniforms) {
MGLOG_E_ONCE("DispatchCompute skipped: BindProgramUniformBuffers failed");
return;
@@ -6441,6 +6663,11 @@ void main() {
MGLOG_E_ONCE("DispatchComputeIndirect skipped: storage image preparation failed");
return;
}
if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT)) {
MGLOG_E_ONCE("DispatchComputeIndirect skipped: sampler/image feedback snapshot failed");
return;
}
const VkPipeline pipeline = GetOrCreateComputePipeline(programObj);
if (pipeline == VK_NULL_HANDLE) {
@@ -6452,7 +6679,8 @@ void main() {
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(),
VK_PIPELINE_BIND_POINT_COMPUTE);
VK_PIPELINE_BIND_POINT_COMPUTE, nullptr, false,
m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch);
if (!boundUniforms) {
MGLOG_E_ONCE("DispatchComputeIndirect skipped: BindProgramUniformBuffers failed");
return;
@@ -9138,19 +9366,18 @@ void main() {
// The GL rect, aimed at the default framebuffer's stored orientation. Using the GL y
// verbatim copied rows [y, y+h) counted from the TOP of the image, i.e. the wrong band for
// every read that was not full-height.
Int32 copyOffsetX = x;
Int32 copyOffsetY = y;
VkOffset2D copyOffset{x, y};
VkExtent2D copyExtent{static_cast<Uint32>(width), static_cast<Uint32>(height)};
if (readIsDefaultFbo) {
const VkExtent2D defaultFboExtent = m_swapchainObject.GetExtent();
const DefaultFramebufferRectMapping mapping =
GetDefaultFramebufferRectMapping(m_swapchainObject.GetPreTransform());
copyOffsetX = MapDefaultFramebufferRectAxis(x, width, static_cast<Int>(defaultFboExtent.width),
mapping.mirrorX);
copyOffsetY = MapDefaultFramebufferRectAxis(y, height, static_cast<Int>(defaultFboExtent.height),
mapping.flipY);
const Bool mapped = MapDefaultFramebufferReadbackRect(
x, y, width, height, defaultFboExtent, m_swapchainObject.GetPreTransform(), &copyOffset,
&copyExtent);
MOBILEGL_ASSERT(mapped, "ReadPixels: default framebuffer read rectangle is out of bounds");
if (!mapped) return;
}
copyRegion.imageOffset = {copyOffsetX, copyOffsetY, static_cast<Int32>(srcBinding.depthOffset)};
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
copyRegion.imageOffset = {copyOffset.x, copyOffset.y, static_cast<Int32>(srcBinding.depthOffset)};
copyRegion.imageExtent = {copyExtent.width, copyExtent.height, 1};
vkCmdCopyImageToBuffer(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
readback.GetHandle(), 1, &copyRegion);
@@ -9192,16 +9419,14 @@ void main() {
// already aimed with the same mapping. The gate is exactly what made every partial
// read of the default framebuffer come back in Vulkan row order.
Vector<Uint8> remapped(static_cast<SizeT>(width) * static_cast<SizeT>(height) * sourceTexelSize);
if (RemapDefaultFboReadbackToGLOrientation(mapped, static_cast<Uint32>(width),
static_cast<Uint32>(height), preTransform, sourceTexelSize,
remapped.data())) {
if (RemapDefaultFramebufferReadback(mapped, static_cast<Uint32>(width),
static_cast<Uint32>(height), preTransform, sourceTexelSize,
remapped.data())) {
PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, 1, format, type, pixels,
/*applyPackImageParams=*/false, /*applyReadColorClamp=*/true);
return;
}
// Only a quarter-turn pre-transform reaches this, and nothing in this renderer models
// one. MGLOG_I because the INFO builds are the ones that run conformance.
MGLOG_D("DirectVulkan::ReadPixels: default-FBO remap declined (w=%d h=%d preTransform=%d); falling back "
MGLOG_D("DirectVulkan::ReadPixels: default-FBO remap failed (w=%d h=%d preTransform=%d); falling back "
"to raw readback",
width, height, static_cast<Int>(preTransform));
}
@@ -9582,16 +9807,15 @@ void main() {
// The swapchain's depth/stencil image is stored display-side-up like its colour twin, so
// the GL rect has to be mapped into that space before the copy and the copied rows
// re-oriented afterwards - the same two halves the colour ReadPixels path applies.
Int32 copyOffsetX = x;
Int32 copyOffsetY = y;
VkOffset2D copyOffset{x, y};
VkExtent2D copyExtent{static_cast<Uint32>(width), static_cast<Uint32>(height)};
if (defaultFramebufferOrientation) {
const VkExtent2D defaultFboExtent = m_swapchainObject.GetExtent();
const DefaultFramebufferRectMapping mapping =
GetDefaultFramebufferRectMapping(m_swapchainObject.GetPreTransform());
copyOffsetX = MapDefaultFramebufferRectAxis(x, width, static_cast<Int>(defaultFboExtent.width),
mapping.mirrorX);
copyOffsetY = MapDefaultFramebufferRectAxis(y, height, static_cast<Int>(defaultFboExtent.height),
mapping.flipY);
const Bool mapped = MapDefaultFramebufferReadbackRect(
x, y, width, height, defaultFboExtent, m_swapchainObject.GetPreTransform(), &copyOffset,
&copyExtent);
MOBILEGL_ASSERT(mapped, "ReadDepthStencilPixels: default framebuffer read rectangle is out of bounds");
if (!mapped) return;
}
VkBufferImageCopy regions[2]{};
@@ -9603,8 +9827,8 @@ void main() {
region.imageSubresource.mipLevel = mipLevel;
region.imageSubresource.baseArrayLayer = baseArrayLayer;
region.imageSubresource.layerCount = 1;
region.imageOffset = {copyOffsetX, copyOffsetY, 0};
region.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
region.imageOffset = {copyOffset.x, copyOffset.y, 0};
region.imageExtent = {copyExtent.width, copyExtent.height, 1};
}
if (wantStencil) {
auto& region = regions[regionCount++];
@@ -9613,8 +9837,8 @@ void main() {
region.imageSubresource.mipLevel = mipLevel;
region.imageSubresource.baseArrayLayer = baseArrayLayer;
region.imageSubresource.layerCount = 1;
region.imageOffset = {copyOffsetX, copyOffsetY, 0};
region.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
region.imageOffset = {copyOffset.x, copyOffset.y, 0};
region.imageExtent = {copyExtent.width, copyExtent.height, 1};
}
vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(),
regionCount, regions);
@@ -9648,23 +9872,21 @@ void main() {
Bool remapped = true;
if (wantDepth && depthCopyBytes > 0) {
remappedDepth.resize(pixelCount * depthCopyBytes);
remapped = RemapDefaultFboReadbackToGLOrientation(depthSrc, static_cast<Uint32>(width),
static_cast<Uint32>(height), preTransform,
depthCopyBytes, remappedDepth.data());
remapped = RemapDefaultFramebufferReadback(depthSrc, static_cast<Uint32>(width),
static_cast<Uint32>(height), preTransform,
depthCopyBytes, remappedDepth.data());
}
if (remapped && wantStencil) {
remappedStencil.resize(pixelCount);
remapped = RemapDefaultFboReadbackToGLOrientation(stencilSrc, static_cast<Uint32>(width),
static_cast<Uint32>(height), preTransform, 1,
remappedStencil.data());
remapped = RemapDefaultFramebufferReadback(stencilSrc, static_cast<Uint32>(width),
static_cast<Uint32>(height), preTransform, 1,
remappedStencil.data());
}
if (remapped) {
if (!remappedDepth.empty()) depthSrc = remappedDepth.data();
if (!remappedStencil.empty()) stencilSrc = remappedStencil.data();
} else {
// Only a quarter-turn pre-transform reaches this, and nothing in this renderer
// models one. MGLOG_I because the INFO builds are the ones that run conformance.
MGLOG_D("DirectVulkan::ReadDepthStencilPixels: default-FBO remap declined (w=%d h=%d "
MGLOG_D("DirectVulkan::ReadDepthStencilPixels: default-FBO remap failed (w=%d h=%d "
"preTransform=%d); falling back to raw readback",
width, height, static_cast<Int>(preTransform));
}
@@ -11857,6 +12079,17 @@ void main() {
}
void VulkanRenderer::CreateInstance() {
#if defined(VK_USE_PLATFORM_METAL_EXT)
// MoltenVK snapshots its configuration when the loader first discovers the ICD. Set
// this before instance-extension enumeration, while preserving an explicit user value.
if (std::getenv("MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS") == nullptr) {
if (::setenv("MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS", "1", 0) == 0) {
MGLOG_I("MoltenVK: enabling Metal argument buffers");
} else {
MGLOG_W("MoltenVK: could not enable Metal argument buffers before ICD discovery");
}
}
#endif
m_extensions = EnumerateInstanceExtensions();
MGLOG_I("Got %d Vulkan instance extensions: ", m_extensions.size());
for (auto& extension : m_extensions) {
@@ -11998,17 +12231,19 @@ void main() {
auto debugMessengerCreateInfo = PopulateDebugMessengerCreateInfo();
// Layers
const void* instanceCreatePNext = nullptr;
if (m_validationLayersEnabled) {
MGLOG_I("Enabling validation layer...");
instanceInfo.enabledLayerCount = static_cast<uint32_t>(std::size(s_validationLayerNames));
instanceInfo.ppEnabledLayerNames = s_validationLayerNames;
// Chaining the messenger create-info is only legal with the extension on.
instanceInfo.pNext = debugUtilsAvailable ? &debugMessengerCreateInfo : nullptr;
instanceCreatePNext = debugUtilsAvailable ? &debugMessengerCreateInfo : nullptr;
} else {
instanceInfo.enabledLayerCount = 0;
instanceInfo.pNext = nullptr;
}
instanceInfo.pNext = instanceCreatePNext;
VK_VERIFY(vkCreateInstance(&instanceInfo, nullptr, &m_instance), "vkCreateInstance failed");
if (debugUtilsAvailable) {
@@ -12434,6 +12669,75 @@ void main() {
vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceFeatures2KHR"));
}
m_updateAfterBindLimits = {};
VkPhysicalDeviceDescriptorIndexingFeatures descriptorIndexingFeatures{};
descriptorIndexingFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES;
VkPhysicalDeviceDescriptorIndexingProperties descriptorIndexingProperties{};
descriptorIndexingProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_PROPERTIES;
const Bool descriptorIndexingCore = m_physicalDevice.properties.apiVersion >= VK_API_VERSION_1_2;
const Bool descriptorIndexingExtension =
IsExtensionSupported(availableExtensions, VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME);
auto getPhysicalDeviceProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2"));
if (getPhysicalDeviceProperties2 == nullptr) {
getPhysicalDeviceProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2KHR"));
}
if ((descriptorIndexingCore || descriptorIndexingExtension) && getPhysicalDeviceFeatures2 != nullptr &&
getPhysicalDeviceProperties2 != nullptr) {
VkPhysicalDeviceFeatures2 featureQuery{};
featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
featureQuery.pNext = &descriptorIndexingFeatures;
getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery);
VkPhysicalDeviceProperties2 propertyQuery{};
propertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
propertyQuery.pNext = &descriptorIndexingProperties;
getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery);
// This renderer emits every descriptor category listed below, including
// dynamic UBOs and combined image samplers. Do not enable a partial
// descriptor-indexing contract: it would make a later reflected program
// fail in the driver instead of choosing its ordinary descriptor layout.
const Bool allUpdateAfterBindFeatures =
descriptorIndexingFeatures.descriptorBindingUniformBufferUpdateAfterBind == VK_TRUE &&
descriptorIndexingFeatures.descriptorBindingSampledImageUpdateAfterBind == VK_TRUE &&
descriptorIndexingFeatures.descriptorBindingStorageImageUpdateAfterBind == VK_TRUE &&
descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind == VK_TRUE &&
descriptorIndexingFeatures.descriptorBindingUniformTexelBufferUpdateAfterBind == VK_TRUE &&
descriptorIndexingFeatures.descriptorBindingStorageTexelBufferUpdateAfterBind == VK_TRUE &&
(!deviceFeatures.robustBufferAccess || descriptorIndexingProperties.robustBufferAccessUpdateAfterBind);
if (allUpdateAfterBindFeatures) {
if (!descriptorIndexingCore && !IsExtensionAlreadyEnabled(
enabledDeviceExtensions,
VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME)) {
enabledDeviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME);
}
descriptorIndexingFeatures.pNext = const_cast<void*>(deviceCreateInfo.pNext);
deviceCreateInfo.pNext = &descriptorIndexingFeatures;
m_updateAfterBindLimits = {
true,
descriptorIndexingProperties.maxPerStageDescriptorUpdateAfterBindSamplers,
descriptorIndexingProperties.maxPerStageDescriptorUpdateAfterBindUniformBuffers,
descriptorIndexingProperties.maxPerStageDescriptorUpdateAfterBindStorageBuffers,
descriptorIndexingProperties.maxPerStageDescriptorUpdateAfterBindSampledImages,
descriptorIndexingProperties.maxPerStageDescriptorUpdateAfterBindStorageImages,
descriptorIndexingProperties.maxPerStageUpdateAfterBindResources,
descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindSamplers,
descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindUniformBuffers,
descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindUniformBuffersDynamic,
descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindStorageBuffers,
descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindStorageBuffersDynamic,
descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindSampledImages,
descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindStorageImages};
MGLOG_I("Vulkan: update-after-bind descriptor layouts enabled");
} else {
MGLOG_I("Vulkan: descriptor indexing is present but lacks the complete update-after-bind feature set; "
"using ordinary descriptor layouts");
}
} else {
MGLOG_I("Vulkan: descriptor indexing unavailable; using ordinary descriptor layouts");
}
VkPhysicalDeviceIndexTypeUint8Features indexTypeUint8Features{};
indexTypeUint8Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INDEX_TYPE_UINT8_FEATURES;
if (indexTypeUint8ExtensionName != nullptr) {
@@ -12505,6 +12809,73 @@ void main() {
}
}
// Native subgroup topology, and VK_EXT_subgroup_size_control's
// computeFullSubgroups feature. REQUIRE_FULL_SUBGROUPS on a compute stage is what
// turns the derived gl_NumSubgroups (DeriveNumSubgroupsPass) from
// encouraged-but-unspecified driver behaviour into a spec guarantee: with the bit
// set and local_size_x a multiple of the subgroup size, every subgroup launches
// full, so the subgroup count is exactly invocations / size ("Full Subgroups",
// VUID-VkPipelineShaderStageCreateInfo-flags-02759/-02785).
m_nativeSubgroupSize = 0;
m_nativeSubgroupSupported = false;
m_computeFullSubgroupsFeatureEnabled = false;
if (getPhysicalDeviceProperties2 != nullptr) {
VkPhysicalDeviceSubgroupProperties subgroupProperties{};
subgroupProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES;
VkPhysicalDeviceProperties2 subgroupPropertyQuery{};
subgroupPropertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
subgroupPropertyQuery.pNext = &subgroupProperties;
getPhysicalDeviceProperties2(m_physicalDevice.handle, &subgroupPropertyQuery);
// Mirrors the loader's HasUsableShaderSubgroupSupport gate, including the
// MOBILEGL_DISABLE_SUBGROUP escape hatch, so the module lowerings can never
// disagree with the advertised capabilities.
const Bool usableSubgroups =
subgroupProperties.subgroupSize > 0 &&
(subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 &&
(subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0;
if (usableSubgroups && !MG_Config::Features.DisableSubgroup) {
m_nativeSubgroupSize = subgroupProperties.subgroupSize;
m_nativeSubgroupSupported = true;
}
}
VkPhysicalDeviceSubgroupSizeControlFeaturesEXT subgroupSizeControlFeatures{};
subgroupSizeControlFeatures.sType =
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES_EXT;
m_maxComputeWorkgroupSubgroups = 0;
if (m_nativeSubgroupSupported &&
IsExtensionSupported(availableExtensions, VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME) &&
getPhysicalDeviceFeatures2 != nullptr) {
VkPhysicalDeviceFeatures2 featureQuery{};
featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
featureQuery.pNext = &subgroupSizeControlFeatures;
getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery);
if (getPhysicalDeviceProperties2 != nullptr) {
VkPhysicalDeviceSubgroupSizeControlPropertiesEXT subgroupSizeControlProperties{};
subgroupSizeControlProperties.sType =
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_PROPERTIES_EXT;
VkPhysicalDeviceProperties2 propertyQuery{};
propertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
propertyQuery.pNext = &subgroupSizeControlProperties;
getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery);
m_maxComputeWorkgroupSubgroups =
subgroupSizeControlProperties.maxComputeWorkgroupSubgroups;
}
if (subgroupSizeControlFeatures.computeFullSubgroups == VK_TRUE) {
if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions,
VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME)) {
enabledDeviceExtensions.push_back(VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME);
}
// Only the full-subgroups guarantee is wanted; required/varying subgroup
// sizes stay unrequested.
subgroupSizeControlFeatures.subgroupSizeControl = VK_FALSE;
subgroupSizeControlFeatures.pNext = const_cast<void*>(deviceCreateInfo.pNext);
deviceCreateInfo.pNext = &subgroupSizeControlFeatures;
m_computeFullSubgroupsFeatureEnabled = true;
MGLOG_I("Enabled optional device extension: %s (computeFullSubgroups)",
VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME);
}
}
// VK_EXT_transform_feedback backs GL transform feedback capture.
m_transformFeedbackFeatureEnabled = false;
VkPhysicalDeviceTransformFeedbackFeaturesEXT transformFeedbackFeatures{};
@@ -13511,6 +13882,15 @@ void main() {
VkPipeline pipeline = VK_NULL_HANDLE;
VK_VERIFY(vkCreateComputePipelines(m_device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline),
"GetOrCreateComputePipeline, vkCreateComputePipelines");
// A failed creation must never be memoized - same contract as
// PipelineFactory::GetOrCreatePipeline: caching the null would serve it back
// for the rest of the process and every dispatch of this program would be
// silently skipped. Retrying costs one failed vkCreateComputePipelines per
// dispatch, which is the correct price.
if (pipeline == VK_NULL_HANDLE) {
MGLOG_E("GetOrCreateComputePipeline: vkCreateComputePipelines failed; not caching the failure");
return VK_NULL_HANDLE;
}
m_computePipelines.emplace(programObj.hash, pipeline);
return pipeline;
}
@@ -229,6 +229,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
VkImageLayout dstRestoreLayout, Bool stencilAspect);
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
// Map a GL bottom-left-origin rectangle into the display-oriented swapchain image.
// Quarter-turn surface transforms swap the copy extent's axes.
static Bool MapDefaultFramebufferReadbackRect(GLint x, GLint y, GLsizei width, GLsizei height,
VkExtent2D imageExtent,
VkSurfaceTransformFlagBitsKHR preTransform,
VkOffset2D* imageOffset, VkExtent2D* imageCopyExtent);
// Reorder a tightly packed block copied with MapDefaultFramebufferReadbackRect back into
// GL row order. The input block has swapped dimensions for 90/270 degree transforms.
static Bool RemapDefaultFramebufferReadback(const Uint8* rawPixels, Uint32 logicalWidth,
Uint32 logicalHeight,
VkSurfaceTransformFlagBitsKHR preTransform,
SizeT texelSize, Uint8* outPixels);
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
GLsizei width, GLsizei height, GLenum destinationFormat,
GLenum destinationType, SizeT destinationRowStride,
@@ -298,6 +310,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
// honored rather than accepted-and-ignored.
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
// ARB_base_instance extends indirect command records with a non-zero firstInstance and
// requires gl_InstanceID to remain zero-based. Vulkan needs both features to honor that
// complete contract: one legalizes the command word, the other enables the shader rebase.
Bool IsNonZeroIndirectBaseInstanceSupported() const {
return m_drawIndirectFirstInstanceFeatureEnabled && m_shaderDrawParametersFeatureEnabled;
}
// Ensures the frame command buffer is recording (same lazy pattern as
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
// frame's pool. Null when unsupported or the pool is exhausted.
@@ -536,7 +554,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool m_samplerAnisotropyFeatureEnabled = false;
Bool m_shaderDrawParametersExtensionEnabled = false;
Bool m_shaderDrawParametersFeatureEnabled = false;
// Native subgroup topology, queried at device creation for the compute-module
// subgroup repairs (SubgroupSupportPolicy.h) and the REQUIRE_FULL_SUBGROUPS
// stage flag; 0 / false when the device has no usable compute subgroups or
// MOBILEGL_DISABLE_SUBGROUP forced them off.
Uint32 m_nativeSubgroupSize = 0;
Bool m_nativeSubgroupSupported = false;
Bool m_computeFullSubgroupsFeatureEnabled = false;
// VkPhysicalDeviceSubgroupSizeControlProperties::maxComputeWorkgroupSubgroups;
// 0 when the extension (and therefore the full-subgroups flag) is unavailable.
Uint32 m_maxComputeWorkgroupSubgroups = 0;
Bool m_unformattedFloatStorageImagesEnabled = false;
// Set only after descriptor-indexing feature AND property queries prove that
// update-after-bind is legal for every descriptor category this renderer emits.
ProgramFactory::UpdateAfterBindLimits m_updateAfterBindLimits{};
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
// drive a runtime fallback when the device lacks them.
@@ -776,6 +807,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 m_lastLodProgramVersion = 0;
Uint64 m_lastLodBindGeneration = 0;
Uint64 m_lastLodParamsSum = 0;
// Sampling-resolution generation at probe time. The probe reads the effective
// sampler's filters/aniso/LOD range, whose setters bump only this counter -
// the params-version sum above never moves for them.
Uint64 m_lastLodSamplingGeneration = 0;
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
@@ -813,6 +848,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 vaoLifetimeId = 0;
Uint32 vaoConfigVersion = 0;
const void* drawFbo = nullptr;
// Never-reused lifetime id beside the raw pointer + Uint16 version: a
// deleted FBO recycled at the same address with the same fresh version
// count would otherwise compare equal (same ABA as the render-pass
// manager's fast-path memo).
Uint64 drawFboLifetimeId = 0;
Uint16 fboVersion = 0;
Bool drawFboIsDefault = false;
Uint renderStateVersion = 0;
@@ -908,6 +948,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// already sampleable.
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
Vector<UniformManager::SamplerImageFeedbackBinding> m_samplerImageFeedbackScratch;
Vector<UniformManager::SamplerBindingOverride> m_samplerImageBindingOverridesScratch;
Vector<VkBuffer> m_vertexBuffersScratch;
Vector<VkDeviceSize> m_vertexOffsetsScratch;
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
@@ -1034,6 +1076,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkBuffer indexVkBuffer = VK_NULL_HANDLE;
VkDeviceSize indexSliceOffset = 0;
Uint64 indexFrameSerial = 0;
// The EBO carried a host map when the slice was recorded - the mirror of
// anyBufferMapped on the vertex half. A shadow-backed (non-adopted)
// persistent map mutates its shadow with no API call and no epoch bump, so
// the one-compare rescue must decline and re-run the acquire, whose
// SyncPersistentMappedRange is the push-down. A map taken AFTER the record
// is already covered: AcquirePersistentMap bumps the slice epoch for the
// request itself, adopted or declined.
Bool indexBufferMapped = false;
// Bound per draw (first bindingCount elements).
VkBuffer vkBuffers[kMaxBindings] = {};
@@ -1127,6 +1177,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
// Vulkan forbids a sampled descriptor and writable storage descriptor from naming the
// same image subresource in one shader operation. Snapshot only the sampler side; the
// storage descriptor continues to name the application texture.
Bool PrepareSamplerImageFeedbackSnapshots(
FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
VkPipelineStageFlags consumerShaderStageMask);
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
// bias, line width, stencil), gated behind one render-state-parameters-version
@@ -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.DisableSubgroup;
}
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.FixIterationRPSubgroupScratch !=
MG_Config::QuirkOverride::ForceOff;
}
inline Bool ShouldFixIterationRPBarrier() {
return MG_Config::Features.IterationRPFixBarrier;
}
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.DeriveNumSubgroups != MG_Config::QuirkOverride::ForceOff;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
+85 -30
View File
@@ -18,6 +18,7 @@
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
#include <MG_Util/Texture/PixelStoreProcessor.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
@@ -31,6 +32,8 @@ namespace MobileGL::MG_Impl::GLImpl {
NamedBufferData,
NamedBufferSubData,
CopyNamedBufferSubData,
ClearBufferData,
ClearBufferSubData,
ClearNamedBufferData,
ClearNamedBufferSubData,
MapBufferRange,
@@ -65,6 +68,10 @@ namespace MobileGL::MG_Impl::GLImpl {
return "NamedBufferSubData";
case BufferOp::CopyNamedBufferSubData:
return "CopyNamedBufferSubData";
case BufferOp::ClearBufferData:
return "ClearBufferData";
case BufferOp::ClearBufferSubData:
return "ClearBufferSubData";
case BufferOp::ClearNamedBufferData:
return "ClearNamedBufferData";
case BufferOp::ClearNamedBufferSubData:
@@ -143,16 +150,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return 0;
}
// The pattern is replicated verbatim, which is only the whole story while the client
// layout already matches the internal format - the case every entry point in practice
// uses, and the only one the conversion machinery here can express. Say so rather than
// quietly writing a differently-sized pattern.
const SizeT sourceSize = MG_Util::GetInputBytesPerPixel(inputFormat, pixelType);
if (sourceSize != elementSize) {
MGLOG_W_ONCE("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
"converting between them is not implemented",
GetBufferOpName(op), sourceSize, internalformat, elementSize);
}
return elementSize;
}
@@ -194,27 +191,59 @@ namespace MobileGL::MG_Impl::GLImpl {
return true;
}
void ClearNamedBufferRange_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size,
GLenum format, GLenum type, const void* data, BufferOp op) {
Bool BuildClearPattern(GLenum internalformat, GLenum format, GLenum type, const void* data,
SizeT patternSize, BufferOp op, Vector<Uint8>& pattern) {
const TextureInternalFormat internal = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
const TexturePixelDataType inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
Vector<Uint8> zeroInput;
const void* inputPixel = data;
if (inputPixel == nullptr) {
const SizeT inputSize = MG_Util::GetInputBytesPerPixel(inputFormat, inputType);
if (inputSize == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
"format and type do not describe a source pixel."));
return false;
}
zeroInput.resize(inputSize);
inputPixel = zeroInput.data();
}
if (!MG_Util::PixelStoreProcessor::ConvertOnePixelToInternal(
internal, inputFormat, inputType, inputPixel, pattern)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", GetBufferOpName(op),
std::format("Cannot convert one ({}, {}) pixel into internalformat 0x{:X}.",
MG_Util::ConvertGLEnumToString(format), MG_Util::ConvertGLEnumToString(type),
internalformat)));
return false;
}
if (data == nullptr) {
// GL defines a null clear value as all zero bits in the destination store, while
// retaining the format/type validation above.
pattern.assign(patternSize, 0);
}
return true;
}
void ClearBufferRange_State(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
GLenum internalformat, GLintptr offset, GLsizeiptr size,
GLenum format, GLenum type, const void* data, BufferOp op) {
const SizeT patternSize = GetClearPatternSize(internalformat, format, type, op);
if (patternSize == 0) return;
auto bufferObject = GetNamedBufferObject(buffer, op);
if (!bufferObject) return;
if (!ValidateBufferClearRange(bufferObject, offset, size, patternSize, op)) return;
if (size == 0) return;
Vector<Uint8> clearData(static_cast<SizeT>(size));
if (data) {
const auto* pattern = static_cast<const Uint8*>(data);
for (SizeT at = 0; at < clearData.size(); at += patternSize) {
Memcpy(clearData.data() + at, pattern, patternSize);
}
} else {
Memset(clearData.data(), 0, clearData.size());
}
bufferObject->UploadSubData({clearData.data(), clearData.size()}, static_cast<SizeT>(offset));
Vector<Uint8> pattern;
if (!BuildClearPattern(internalformat, format, type, data, patternSize, op, pattern)) return;
bufferObject->FillSubData({pattern.data(), pattern.size()}, static_cast<SizeT>(offset),
static_cast<SizeT>(size));
}
auto& GetBufferBindingSlot(BufferTarget target) {
@@ -1197,17 +1226,34 @@ namespace MobileGL::MG_Impl::GLImpl {
static_cast<SizeT>(writeOffset), static_cast<SizeT>(size));
}
void ClearBufferData_State(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) {
auto bufferObject = GetBoundBufferObject(target, BufferOp::ClearBufferData);
if (!bufferObject) return;
ClearBufferRange_State(bufferObject, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
type, data, BufferOp::ClearBufferData);
}
void ClearBufferSubData_State(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size,
GLenum format, GLenum type, const void* data) {
auto bufferObject = GetBoundBufferObject(target, BufferOp::ClearBufferSubData);
if (!bufferObject) return;
ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
BufferOp::ClearBufferSubData);
}
void ClearNamedBufferData_State(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferData);
if (!bufferObject) return;
ClearNamedBufferRange_State(buffer, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
type, data, BufferOp::ClearNamedBufferData);
ClearBufferRange_State(bufferObject, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
type, data, BufferOp::ClearNamedBufferData);
}
void ClearNamedBufferSubData_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size,
GLenum format, GLenum type, const void* data) {
ClearNamedBufferRange_State(buffer, internalformat, offset, size, format, type, data,
BufferOp::ClearNamedBufferSubData);
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferSubData);
if (!bufferObject) return;
ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
BufferOp::ClearNamedBufferSubData);
}
void* MapNamedBuffer_State(GLuint buffer, GLenum access) {
@@ -1662,6 +1708,15 @@ namespace MobileGL::MG_Impl::GLImpl {
CopyNamedBufferSubData_State(readBuffer, writeBuffer, readOffset, writeOffset, size);
}
void ClearBufferData(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) {
ClearBufferData_State(target, internalformat, format, type, data);
}
void ClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
GLenum type, const void* data) {
ClearBufferSubData_State(target, internalformat, offset, size, format, type, data);
}
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
ClearNamedBufferData_State(buffer, internalformat, format, type, data);
}
@@ -27,6 +27,9 @@ namespace MobileGL::MG_Impl::GLImpl {
void NamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data);
void CopyNamedBufferSubData(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset,
GLsizeiptr size);
void ClearBufferData(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data);
void ClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
GLenum type, const void* data);
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data);
void ClearNamedBufferSubData(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
GLenum type, const void* data);
@@ -985,8 +985,8 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLen
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
DECLARE_GL_FUNCTION_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
DECLARE_GL_FUNCTION_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInternalformati64v, GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInternalformati64v, target, internalformat, pname, count, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth)
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexImage, GLuint texture, GLint level) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexImage, texture, level)
+12 -13
View File
@@ -1057,21 +1057,20 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
static Bool allowVSOnlyPrograms;
static Bool initialized = false;
if (!initialized) {
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) {
MGLOG_E_ONCE("activeBackendObject is not initialized!");
return;
}
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
allowVSOnlyPrograms = (Int)rendererInfo.StaticBackendCapability.AllowVSOnlyPrograms;
}
// Read fresh every link, never latched in a static: the capability is
// per-backend, and a latch would freeze it across a backend teardown +
// re-initialization (the previous function-static memo here never even set
// its own initialized flag, so it re-read every call anyway - this makes
// the always-fresh behavior the stated one). A struct-field read per
// glLinkProgram costs nothing.
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (activeBackendObject) {
programObject->SetMaxFragmentOutputColorNumber(activeBackendObject->GetDynamicParameters().MaxDrawBuffers);
if (!activeBackendObject) {
MGLOG_E_ONCE("activeBackendObject is not initialized!");
return;
}
const Bool allowVSOnlyPrograms =
activeBackendObject->GetRendererInfo().StaticBackendCapability.AllowVSOnlyPrograms;
programObject->SetMaxFragmentOutputColorNumber(activeBackendObject->GetDynamicParameters().MaxDrawBuffers);
programObject->Link(!allowVSOnlyPrograms);
}
@@ -648,4 +648,39 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
GetQueryiv(target, pname, params);
}
void DestroyAllQueryObjects() {
// Detach the registry under the lock, release outside it - same discipline
// (and the same accepted teardown race) as DestroyAllSyncObjects. Without
// this drain, every query the app left undeleted survived full library
// teardown in the process-global registry: the objects and their backend
// wrappers leaked across Destroy/Initialize cycles, stale ids kept
// answering IsQuery == GL_TRUE in the re-initialized library, and a later
// glDeleteQueries could hand the OLD backend's handle to a DIFFERENT
// backend's DeleteBackendQuery, which casts it to the wrong wrapper type.
UnorderedMap<GLuint, QueryObject*> orphans;
{
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
orphans.swap(g_liveQueryObjects);
g_activeTimeElapsedQueryId = 0;
g_activePrimitivesWrittenQueryId = 0;
g_activePrimitivesGeneratedQueryId = 0;
g_activeSamplesPassedQueryId = 0;
}
if (orphans.empty()) {
return;
}
// Backend handles must be released by the backend that created them, so
// this runs while the function table is still populated. Both backends'
// DeleteBackendQuery are generation-guarded, so a handle whose renderer
// or ES context is already gone frees only the wrapper.
const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery;
for (const auto& [_, queryObject] : orphans) {
if (deleteBackendQuery && queryObject->backendHandle) {
deleteBackendQuery(queryObject->backendHandle);
}
delete queryObject;
}
MGLOG_D("DestroyAllQueryObjects: reclaimed %zu query object(s) the app left undeleted", orphans.size());
}
} // namespace MobileGL::MG_Impl::GLImpl
+9
View File
@@ -29,4 +29,13 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
void QueryCounter(GLuint id, GLenum target);
// Destroys every still-registered query object exactly as DeleteQueries would.
// GL requires queries to die with their context; called only from full library
// teardown (DestroyImpl), where no context survives on any thread, so the
// process-global registry can be drained wholesale. Must run while the backend
// function table is still populated: each backend handle has to be released by
// the backend that created it, never by a later re-initialized one (whose
// DeleteBackendQuery would cast the wrapper to the wrong backend's type).
// Same contract as DestroyAllSyncObjects.
void DestroyAllQueryObjects();
} // namespace MobileGL::MG_Impl::GLImpl
+27 -4
View File
@@ -24,9 +24,14 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(MGL_ITEST_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..)
# Only meaningful where MobileGL_s exists (i.e. not Android).
if (NOT TARGET MobileGL_s)
message(STATUS "MobileGL_s is not available; skipping the integration test module")
# Desktop links the static implementation directly. Android runs the same
# executable from adb shell and links the shipping shared library instead.
if (ANDROID)
set(MGL_ITEST_MOBILEGL_TARGET MobileGL)
elseif (TARGET MobileGL_s)
set(MGL_ITEST_MOBILEGL_TARGET MobileGL_s)
else()
message(STATUS "No MobileGL library target is available; skipping the integration test module")
return()
endif()
@@ -68,6 +73,9 @@ add_executable(MobileGLIntegrationTest
Scenarios/DoublePrecisionScenario.cpp
Scenarios/UniformInitializerScenario.cpp
Scenarios/SwizzleAccessRoutineScenario.cpp
Scenarios/IterationRPFirstReductionScenario.cpp
Scenarios/IterationRPProgram203Scenario.cpp
Scenarios/IterationRPScratchFixScenario.cpp
Scenarios/ProgramPipelineScenario.cpp
Scenarios/ImageLoadStoreSsoScenario.cpp
Scenarios/ImageTargetKindScenario.cpp
@@ -92,9 +100,20 @@ target_include_directories(MobileGLIntegrationTest PRIVATE
# gtest, not gtest_main: Main.cpp installs the harness banner itself.
target_link_libraries(MobileGLIntegrationTest PRIVATE
GTest::gtest
MobileGL_s
${MGL_ITEST_MOBILEGL_TARGET}
)
if (ANDROID)
find_library(MGL_ITEST_ANDROID_LIBRARY android REQUIRED)
find_library(MGL_ITEST_LOG_LIBRARY log REQUIRED)
find_library(MGL_ITEST_MEDIANDK_LIBRARY mediandk REQUIRED)
target_link_libraries(MobileGLIntegrationTest PRIVATE
${MGL_ITEST_ANDROID_LIBRARY}
${MGL_ITEST_LOG_LIBRARY}
${MGL_ITEST_MEDIANDK_LIBRARY}
)
endif()
if (MSVC)
# Same reason as MG_Test/Backend/DirectVulkan: the GLES headers declare gl*
# as dllimport on Windows, so the in-library GL entry-point definitions only
@@ -103,6 +122,10 @@ if (MSVC)
endif()
target_compile_definitions(MobileGLIntegrationTest PRIVATE -DNOMINMAX)
if (ANDROID)
return()
endif()
# --- ctest wiring --------------------------------------------------------
# A bare libEGL on a glvnd box resolves to whatever vendor comes first, which is
# usually Mesa/llvmpipe - a software rasteriser silently replacing the GPU under
@@ -15,6 +15,16 @@
#include <ostream>
#include <sstream>
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#elif defined(__ANDROID__)
#include <android/hardware_buffer.h>
#include <android/native_window.h>
#include <media/NdkImage.h>
#include <media/NdkImageReader.h>
#endif
// MobileGL's own headers, in the order MobileGL/Includes.h uses them: GL/gl.h
// first, then glcorearb.h for the 3.x+ entry points. This binary links
// MobileGL_s, so every gl*/egl* below binds to MobileGL's implementation, not
@@ -32,7 +42,7 @@
// the only construction that is actually predictive here: MobileGL ABORTS
// (MOBILEGL_ASSERT -> SIGTRAP) rather than returning an error on an unusable
// platform, so nothing the parent can call in-process is allowed to be wrong.
#if !defined(_WIN32) && !defined(__APPLE__) && __has_include(<sys/wait.h>)
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && __has_include(<sys/wait.h>)
#define MGITEST_HAVE_FORK_PREFLIGHT 1
#include <csignal>
#include <ctime>
@@ -53,6 +63,83 @@ namespace MGITest {
constexpr int kSurfaceWidth = 128;
constexpr int kSurfaceHeight = 96;
#if defined(_WIN32)
HWND g_testWindow = nullptr;
HWND CreateTestWindow() {
static const wchar_t* const kClassName = L"MobileGLIntegrationTestWindow";
static bool registered = false;
if (!registered) {
WNDCLASSW windowClass{};
windowClass.lpfnWndProc = DefWindowProcW;
windowClass.hInstance = GetModuleHandleW(nullptr);
windowClass.lpszClassName = kClassName;
if (RegisterClassW(&windowClass) == 0 && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) {
return nullptr;
}
registered = true;
}
return CreateWindowExW(0, kClassName, L"MobileGL Integration Test", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, kSurfaceWidth, kSurfaceHeight, nullptr, nullptr,
GetModuleHandleW(nullptr), nullptr);
}
#elif defined(__ANDROID__)
AImageReader* g_imageReader = nullptr;
ANativeWindow* g_imageReaderWindow = nullptr;
void DrainImageReader(void*, AImageReader* reader) {
AImage* image = nullptr;
if (AImageReader_acquireNextImage(reader, &image) == AMEDIA_OK && image != nullptr) {
AImage_delete(image);
}
}
bool CreateImageReaderWindow() {
if (g_imageReaderWindow != nullptr) return true;
constexpr int kMaxImages = 4;
const media_status_t status = AImageReader_newWithUsage(
kSurfaceWidth, kSurfaceHeight, AIMAGE_FORMAT_RGBA_8888,
AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT,
kMaxImages, &g_imageReader);
if (status != AMEDIA_OK || g_imageReader == nullptr) return false;
AImageReader_ImageListener listener = {nullptr, DrainImageReader};
AImageReader_setImageListener(g_imageReader, &listener);
if (AImageReader_getWindow(g_imageReader, &g_imageReaderWindow) != AMEDIA_OK ||
g_imageReaderWindow == nullptr) {
AImageReader_setImageListener(g_imageReader, nullptr);
AImageReader_delete(g_imageReader);
g_imageReader = nullptr;
return false;
}
ANativeWindow_acquire(g_imageReaderWindow);
return true;
}
void DestroyImageReaderWindow() {
if (g_imageReaderWindow != nullptr) {
ANativeWindow_release(g_imageReaderWindow);
g_imageReaderWindow = nullptr;
}
if (g_imageReader != nullptr) {
AImageReader_setImageListener(g_imageReader, nullptr);
AImageReader_delete(g_imageReader);
g_imageReader = nullptr;
}
}
#endif
bool UseWindowSurface() {
#if defined(_WIN32)
const char* value = std::getenv("MOBILEGL_ITEST_WINDOW_SURFACE");
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
#elif defined(__ANDROID__)
return true;
#else
return false;
#endif
}
std::string EnvOr(const char* name, const char* fallback) {
const char* value = std::getenv(name);
return (value != nullptr && value[0] != '\0') ? std::string(value) : std::string(fallback);
@@ -87,10 +174,10 @@ namespace MGITest {
// callers). surfaceless is the platform with no window-system dependency at
// all; the surface this file then creates is still a pbuffer, which every
// platform supports and which the amendment to this rule requires as the
// fallback shape. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
// fallback shape on desktop. Android instead supplies an AImageReader
// ANativeWindow. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
// driver that consults them directly cannot reintroduce the dependency
// behind EGL's back. Desktop-only file: MG_IntegrationTest never builds
// for Android, so no device path is affected.
// behind EGL's back.
void EnsureHeadlessPlatform() {
#if defined(__linux__) && !defined(__ANDROID__)
static bool done = false;
@@ -134,8 +221,9 @@ namespace MGITest {
return 3;
}
const bool useWindowSurface = UseWindowSurface();
const EGLint configAttribs[] = {EGL_SURFACE_TYPE,
EGL_PBUFFER_BIT,
useWindowSurface ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT,
EGL_RED_SIZE,
8,
EGL_GREEN_SIZE,
@@ -152,7 +240,9 @@ namespace MGITest {
EGLConfig config = nullptr;
EGLint configCount = 0;
if (eglChooseConfig(display, configAttribs, &config, 1, &configCount) != EGL_TRUE || configCount < 1) {
outReason = WithEglError("eglChooseConfig found no pbuffer-capable RGBA8/D24 config");
outReason = WithEglError(useWindowSurface
? "eglChooseConfig found no window-capable RGBA8/D24 config"
: "eglChooseConfig found no pbuffer-capable RGBA8/D24 config");
return 4;
}
@@ -166,10 +256,32 @@ namespace MGITest {
return 5;
}
const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE};
EGLSurface surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
EGLSurface surface = EGL_NO_SURFACE;
if (useWindowSurface) {
#if defined(_WIN32)
if (g_testWindow == nullptr) g_testWindow = CreateTestWindow();
if (g_testWindow == nullptr) {
outReason = "failed to create the Windows integration-test window";
return 6;
}
surface = eglCreateWindowSurface(display, config, g_testWindow, nullptr);
#elif defined(__ANDROID__)
if (!CreateImageReaderWindow()) {
outReason = "failed to create the Android AImageReader integration-test window";
return 6;
}
surface = eglCreateWindowSurface(display, config, g_imageReaderWindow, nullptr);
#endif
} else {
const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE};
surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
}
if (surface == EGL_NO_SURFACE) {
outReason = WithEglError("eglCreatePbufferSurface failed");
#if defined(__ANDROID__)
DestroyImageReaderWindow();
#endif
outReason = WithEglError(useWindowSurface ? "eglCreateWindowSurface failed"
: "eglCreatePbufferSurface failed");
return 6;
}
// The step that brings the whole backend up (DirectVulkan creates its
@@ -491,6 +603,14 @@ namespace MGITest {
if (m_context != nullptr) eglDestroyContext(display, static_cast<EGLContext>(m_context));
if (m_surface != nullptr) eglDestroySurface(display, static_cast<EGLSurface>(m_surface));
eglTerminate(display);
#if defined(_WIN32)
if (g_testWindow != nullptr) {
DestroyWindow(g_testWindow);
g_testWindow = nullptr;
}
#elif defined(__ANDROID__)
DestroyImageReaderWindow();
#endif
m_context = nullptr;
m_surface = nullptr;
m_display = nullptr;
@@ -14,11 +14,11 @@
// inspects backend state - both bugs this module pins were invisible to
// state-level assertions and visible only in pixels.
//
// Headless by construction, following MG_Benchmark/Driver/DriverBench.c: an EGL
// context on a PBUFFER surface. No window, no window manager, no human. Unlike
// DriverBench the scenarios do draw to the DEFAULT framebuffer (that is where
// the Y-flip lives) and do call eglSwapBuffers (that is the frame boundary the
// cross-frame scenarios need to be real).
// Headless by construction: desktop uses an EGL pbuffer and Android uses an
// AImageReader-backed ANativeWindow that needs no Activity. No window manager,
// no human. Unlike DriverBench the scenarios do draw to the DEFAULT framebuffer
// (that is where the Y-flip lives) and do call eglSwapBuffers (that is the frame
// boundary the cross-frame scenarios need to be real).
//
// One process is one backend: MOBILEGL_BACKEND_TYPE is latched at
// initialization, so the CMake wiring runs this binary once per backend rather
+7 -1
View File
@@ -31,8 +31,14 @@ namespace {
// silently bound to a workstation's window system is a different
// run from CI's and must be visible as one in the log.
const char* eglPlatform = std::getenv("EGL_PLATFORM");
std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless, EGL_PLATFORM=%s)\n",
#if defined(__ANDROID__)
constexpr const char* surfaceKind = "AImageReader window";
#else
constexpr const char* surfaceKind = "pbuffer";
#endif
std::fprintf(stderr, " renderer: %s\n surface: %dx%d %s (headless, EGL_PLATFORM=%s)\n",
gl.RendererString().c_str(), gl.Width(), gl.Height(),
surfaceKind,
eglPlatform != nullptr ? eglPlatform : "<unset>");
} else if (MGITest::RequireGpu()) {
std::fprintf(stderr,
@@ -0,0 +1,898 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IterationRPFirstReductionScenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - ITERATIONRP'S FIRST SUBGROUP REDUCTION.
//
// iterationRP reduces a 32 x 16 exposure tile with a vector subgroup inclusive add,
// then a shared-memory scan of subgroup totals. The source assumes that every
// subgroup has a last lane, that there are 2..32 subgroups, and that local index
// 511 belongs to the last subgroup and its last lane. Those are source assumptions,
// not API contracts. This probe intentionally does not repair them: it records the
// observed topology and makes each handoff independently observable.
#include <algorithm>
#include <array>
#include <bit>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <limits>
#include <sstream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr std::size_t kInvocationCount = 512;
constexpr std::size_t kScanStageCount = 6;
constexpr std::uint32_t kQuietNanBits = 0x7fc00000u;
constexpr std::size_t kNoSlot = std::numeric_limits<std::size_t>::max();
struct UVec4 {
std::uint32_t x;
std::uint32_t y;
std::uint32_t z;
std::uint32_t w;
};
struct Vec4 {
float x;
float y;
float z;
float w;
};
// Matches the std430 block exactly. uvec4/vec4 arrays have a 16-byte
// stride, floats are a dense scalar array, and the outer scan array is
// stage-major in both GLSL and C++.
struct ProbeOutput {
std::array<UVec4, kInvocationCount> invocation;
std::array<UVec4, kInvocationCount> subgroup;
std::array<Vec4, kInvocationCount> reduction;
std::array<float, kInvocationCount> finalAverage;
std::array<std::array<float, kInvocationCount>, kScanStageCount> scanAfter;
};
static_assert(sizeof(UVec4) == 16);
static_assert(sizeof(Vec4) == 16);
static_assert(std::is_standard_layout_v<ProbeOutput>);
static_assert(offsetof(ProbeOutput, invocation) == 0);
static_assert(offsetof(ProbeOutput, subgroup) == 8192);
static_assert(offsetof(ProbeOutput, reduction) == 16384);
static_assert(offsetof(ProbeOutput, finalAverage) == 24576);
static_assert(offsetof(ProbeOutput, scanAfter) == 26624);
static_assert(sizeof(ProbeOutput) == 38912);
enum class InputMode {
SampledRgba32f,
IndexedSsbo,
};
const char* InputModeName(InputMode mode) {
return mode == InputMode::SampledRgba32f ? "sampled RGBA32F" : "indexed SSBO";
}
std::uint32_t FloatBits(float value) {
return std::bit_cast<std::uint32_t>(value);
}
bool SameBits(float lhs, float rhs) {
return FloatBits(lhs) == FloatBits(rhs);
}
bool IsQuietNanSentinel(float value) {
return FloatBits(value) == kQuietNanBits;
}
bool DrainGlErrors() {
bool hadError = false;
while (glGetError() != GL_NO_ERROR) hadError = true;
return hadError;
}
bool HasExtension(const char* wanted) {
GLint extensionCount = 0;
glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount);
for (GLint i = 0; i < extensionCount; ++i) {
const auto* extension = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, static_cast<GLuint>(i)));
if (extension != nullptr && std::string(extension) == wanted) return true;
}
return false;
}
struct CapabilityInfo {
bool subgroupExtension = false;
GLint subgroupSize = 0;
GLint supportedStages = 0;
GLint supportedFeatures = 0;
GLint maxComputeStorageBlocks = 0;
GLint maxStorageBindings = 0;
GLint maxWorkGroupInvocations = 0;
std::array<GLint, 3> maxWorkGroupSize{};
bool queryHadError = false;
// iterationRP's source contract needs gl_NumSubgroups in [2, 32] for its 512
// invocations, i.e. an advertised subgroup width in [16, 256]. A device
// outside that window (lavapipe's 8-lane subgroups give 64 subgroups) cannot
// run the fixture's verbatim reduction at all, so the scenario SKIPS there -
// the pack itself replays through the FixIterationRPSubgroupScratch patch, which
// this probe deliberately does not model. The width only gates the domain;
// lane placement and group counts still come from observed values alone.
bool SubgroupWidthInSourceDomain() const {
return subgroupSize >= 16 && subgroupSize <= 256;
}
bool SupportsProbe() const {
const auto stages = static_cast<GLbitfield>(supportedStages);
const auto features = static_cast<GLbitfield>(supportedFeatures);
return !queryHadError && subgroupExtension &&
(stages & GL_COMPUTE_SHADER_BIT) != 0 &&
(features & (GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR)) ==
(GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR) &&
SubgroupWidthInSourceDomain() &&
maxComputeStorageBlocks >= 2 && maxStorageBindings >= 2 &&
maxWorkGroupInvocations >= static_cast<GLint>(kInvocationCount) && maxWorkGroupSize[0] >= 32 &&
maxWorkGroupSize[1] >= 16 && maxWorkGroupSize[2] >= 1;
}
std::string MissingRequirements() const {
std::vector<std::string> missing;
const auto stages = static_cast<GLbitfield>(supportedStages);
const auto features = static_cast<GLbitfield>(supportedFeatures);
if (queryHadError) missing.emplace_back("a subgroup/compute capability query generated GL error");
if (!subgroupExtension) missing.emplace_back("GL_KHR_shader_subgroup");
if ((stages & GL_COMPUTE_SHADER_BIT) == 0) {
missing.emplace_back("GL_COMPUTE_SHADER_BIT in GL_SUBGROUP_SUPPORTED_STAGES_KHR");
}
const auto requiredFeatures =
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR;
if ((features & requiredFeatures) != requiredFeatures) {
missing.emplace_back("basic|arithmetic in GL_SUBGROUP_SUPPORTED_FEATURES_KHR");
}
if (!SubgroupWidthInSourceDomain()) {
missing.emplace_back(
"GL_SUBGROUP_SIZE_KHR in [16, 256] (iterationRP's source contract needs "
"gl_NumSubgroups in [2, 32] for 512 invocations; width " +
std::to_string(subgroupSize) + " is outside the fixture's domain)");
}
if (maxComputeStorageBlocks < 2 || maxStorageBindings < 2) {
missing.emplace_back("two compute SSBO bindings");
}
if (maxWorkGroupInvocations < static_cast<GLint>(kInvocationCount) || maxWorkGroupSize[0] < 32 ||
maxWorkGroupSize[1] < 16 || maxWorkGroupSize[2] < 1) {
missing.emplace_back("a 32x16x1 / 512-invocation compute workgroup");
}
std::ostringstream message;
for (std::size_t i = 0; i < missing.size(); ++i) {
if (i != 0) message << ", ";
message << missing[i];
}
return message.str();
}
};
CapabilityInfo QueryCapabilities() {
CapabilityInfo info;
DrainGlErrors();
info.subgroupExtension = HasExtension("GL_KHR_shader_subgroup");
glGetIntegerv(GL_SUBGROUP_SIZE_KHR, &info.subgroupSize);
glGetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &info.supportedStages);
glGetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &info.supportedFeatures);
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &info.maxComputeStorageBlocks);
glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &info.maxStorageBindings);
glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &info.maxWorkGroupInvocations);
for (GLuint axis = 0; axis < info.maxWorkGroupSize.size(); ++axis) {
glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, axis, &info.maxWorkGroupSize[axis]);
}
info.queryHadError = DrainGlErrors();
return info;
}
void PrintMetadata(const CapabilityInfo& info, std::ostream& output) {
output << "IterationRPFirstReductionScenario metadata: "
<< "GL_SUBGROUP_SIZE_KHR=" << info.subgroupSize
<< ", GL_SUBGROUP_SUPPORTED_STAGES_KHR=0x" << std::hex
<< static_cast<GLbitfield>(info.supportedStages)
<< ", GL_SUBGROUP_SUPPORTED_FEATURES_KHR=0x"
<< static_cast<GLbitfield>(info.supportedFeatures) << std::dec
<< ", subgroupExtension=" << info.subgroupExtension
<< ", GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS=" << info.maxComputeStorageBlocks
<< ", GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS=" << info.maxStorageBindings
<< ", GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS=" << info.maxWorkGroupInvocations
<< ", GL_MAX_COMPUTE_WORK_GROUP_SIZE=" << info.maxWorkGroupSize[0] << 'x'
<< info.maxWorkGroupSize[1] << 'x' << info.maxWorkGroupSize[2]
<< ", queryHadError=" << info.queryHadError << '\n';
}
bool DumpRequested() {
const char* value = std::getenv("MOBILEGL_ITEST_SUBGROUP_PROBE_DUMP");
return value != nullptr && std::string(value) == "1";
}
constexpr const char* kShaderPreamble = R"(#version 430 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 1) buffer SubgroupProbeOutput {
uvec4 invocation[512];
uvec4 subgroup[512];
vec4 reduction[512];
float finalAverage[512];
float scanAfter[6][512];
} outProbe;
shared vec2 prefixSumCache[32];
)";
constexpr const char* kSampledInput = R"(
uniform sampler2D colortex2;
uniform vec2 pixelSize;
)";
constexpr const char* kIndexedInput = R"(
layout(std430, binding = 0) readonly buffer Input {
float value[512];
} inputData;
)";
// Only the expression producing tileExposure differs between the two
// tests. The remainder is the iterationRP first reduction, with stores
// placed after its existing barriers to expose each handoff.
constexpr const char* kSampledTileExposure = R"(
vec2 texCoord = (vec2(gl_GlobalInvocationID.xy) + 0.5) *
vec2(1.0 / 32.0, 1.0 / 16.0);
vec2 sampleCoord = texCoord * (1.0 / 64.0);
sampleCoord.x += (15.0 / 32.0) + pixelSize.x * 12.0;
float tileExposure = dot(
textureLod(colortex2, sampleCoord, 0.0).rgb,
vec3(0.2125, 0.7154, 0.0721));
)";
constexpr const char* kIndexedTileExposure = R"(
float tileExposure = inputData.value[gl_LocalInvocationIndex];
)";
constexpr const char* kReductionBody = R"(
vec2 sampleLuminance = vec2(tileExposure, 0.0);
sampleLuminance = subgroupInclusiveAdd(sampleLuminance);
float nativeInclusive = sampleLuminance.x;
// This is a uniform, safety-only branch: it leaves an invalid source
// contract visible without indexing past the 32-entry cache or underflowing
// loopLength - 1. It is deliberately a failure on the CPU, not a skip.
bool sourceDomain = gl_NumSubgroups >= 2u && gl_NumSubgroups <= 32u;
if (!sourceDomain) {
float qNaN = uintBitsToFloat(0x7fc00000u);
uint localIndex = gl_LocalInvocationIndex;
outProbe.invocation[localIndex] = uvec4(localIndex, gl_LocalInvocationID);
outProbe.subgroup[localIndex] = uvec4(gl_SubgroupSize, gl_NumSubgroups, gl_SubgroupID,
gl_SubgroupInvocationID);
outProbe.reduction[localIndex] = vec4(tileExposure, nativeInclusive, qNaN, qNaN);
outProbe.finalAverage[localIndex] = qNaN;
for (uint stage = 0u; stage < 6u; ++stage)
outProbe.scanAfter[stage][localIndex] = qNaN;
return;
}
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
barrier();
float sourceRawSubtotal = prefixSumCache[gl_SubgroupID].x;
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) {
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
sampleLuminance += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
}
barrier();
outProbe.scanAfter[scanStage][gl_LocalInvocationIndex] = sampleLuminance.x;
}
float sourceMergedPrefix = sampleLuminance.x;
if (gl_LocalInvocationIndex == 511u)
prefixSumCache[0] = sampleLuminance / 512.0;
barrier();
float avg = prefixSumCache[0].x;
uint localIndex = gl_LocalInvocationIndex;
outProbe.invocation[localIndex] = uvec4(localIndex, gl_LocalInvocationID);
outProbe.subgroup[localIndex] = uvec4(gl_SubgroupSize, gl_NumSubgroups, gl_SubgroupID,
gl_SubgroupInvocationID);
outProbe.reduction[localIndex] = vec4(tileExposure, nativeInclusive, sourceRawSubtotal, sourceMergedPrefix);
outProbe.finalAverage[localIndex] = avg;
}
)";
std::string BuildProbeShader(InputMode mode) {
std::string source = kShaderPreamble;
source += mode == InputMode::SampledRgba32f ? kSampledInput : kIndexedInput;
source += "\nvoid main() {\n";
source += mode == InputMode::SampledRgba32f ? kSampledTileExposure : kIndexedTileExposure;
source += kReductionBody;
return source;
}
std::string FormatFloat(float value) {
std::ostringstream text;
text << std::hexfloat << value;
return text.str();
}
struct ValidationResult {
bool ok = true;
std::string phase;
std::string message;
bool scanStageMismatch = false;
int scanStage = -1;
bool ownerEvaluated = false;
bool index511IsSourceLastLaneWriter = false;
bool index511IsHighestSubgroupMember = false;
std::uint32_t highestObservedSubgroup = 0;
};
ValidationResult Failure(std::string phase, std::string message) {
ValidationResult result;
result.ok = false;
result.phase = std::move(phase);
result.message = std::move(message);
return result;
}
constexpr float kSampledLuminance = 0.2125f + 0.7154f + 0.0721f;
float ExpectedInput(InputMode mode, std::uint32_t localIndex) {
return mode == InputMode::SampledRgba32f ? kSampledLuminance : static_cast<float>(localIndex + 1u);
}
ValidationResult ValidateProbe(const ProbeOutput& output, InputMode mode) {
std::array<std::size_t, kInvocationCount> slotForLocal{};
slotForLocal.fill(kNoSlot);
// 1. Record identity. Slots are only used to locate each reported
// local index; all subgroup behavior below groups recorded IDs/lanes.
for (std::size_t slot = 0; slot < kInvocationCount; ++slot) {
const std::uint32_t localIndex = output.invocation[slot].x;
if (localIndex >= kInvocationCount) {
std::ostringstream message;
message << "output slot " << slot << " reports localIndex " << localIndex << " outside [0, 511]";
return Failure("record identity", message.str());
}
if (slotForLocal[localIndex] != kNoSlot) {
std::ostringstream message;
message << "localIndex " << localIndex << " appears in output slots " << slotForLocal[localIndex]
<< " and " << slot;
return Failure("record identity", message.str());
}
slotForLocal[localIndex] = slot;
}
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
if (slotForLocal[localIndex] == kNoSlot) {
std::ostringstream message;
message << "localIndex " << localIndex << " is missing from all 512 records";
return Failure("record identity", message.str());
}
}
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
const std::size_t slot = slotForLocal[localIndex];
const UVec4& invocation = output.invocation[slot];
const std::uint32_t expectedX = static_cast<std::uint32_t>(localIndex % 32u);
const std::uint32_t expectedY = static_cast<std::uint32_t>(localIndex / 32u);
if (invocation.y != expectedX || invocation.z != expectedY || invocation.w != 0u) {
std::ostringstream message;
message << "localIndex " << localIndex << " reports local invocation (" << invocation.y << ','
<< invocation.z << ',' << invocation.w << "), expected (" << expectedX << ',' << expectedY
<< ",0)";
return Failure("record identity", message.str());
}
const float expectedInput = ExpectedInput(mode, static_cast<std::uint32_t>(localIndex));
const float actualInput = output.reduction[slot].x;
if (!SameBits(actualInput, expectedInput)) {
std::ostringstream message;
message << "localIndex " << localIndex << " input was " << FormatFloat(actualInput) << ", expected "
<< FormatFloat(expectedInput);
return Failure("input", message.str());
}
}
// 2. Observed topology. Do not derive lanes or subgroup membership
// from local invocation indices: only the values the shader recorded
// participate in grouping.
const std::uint32_t reportedNumSubgroups = output.subgroup[slotForLocal[0]].y;
if (reportedNumSubgroups == 0u) {
return Failure("observed topology", "localIndex 0 reported gl_NumSubgroups == 0");
}
if (reportedNumSubgroups > kInvocationCount) {
std::ostringstream message;
message << "reported gl_NumSubgroups=" << reportedNumSubgroups
<< " exceeds the 512 recorded invocations, so at least one subgroup ID is missing";
return Failure("observed topology", message.str());
}
std::vector<std::vector<std::size_t>> subgroupSlots(reportedNumSubgroups);
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
const std::size_t slot = slotForLocal[localIndex];
const UVec4& subgroup = output.subgroup[slot];
if (subgroup.x == 0u || subgroup.y == 0u) {
std::ostringstream message;
message << "localIndex " << localIndex << " reported subgroupSize=" << subgroup.x
<< ", numSubgroups=" << subgroup.y;
return Failure("observed topology", message.str());
}
if (subgroup.y != reportedNumSubgroups) {
std::ostringstream message;
message << "localIndex " << localIndex << " reported numSubgroups=" << subgroup.y
<< ", while localIndex 0 reported " << reportedNumSubgroups;
return Failure("observed topology", message.str());
}
if (subgroup.z >= reportedNumSubgroups) {
std::ostringstream message;
message << "localIndex " << localIndex << " reported subgroupID=" << subgroup.z
<< " outside [0, " << (reportedNumSubgroups - 1u) << ']';
return Failure("observed topology", message.str());
}
if (subgroup.w >= subgroup.x) {
std::ostringstream message;
message << "localIndex " << localIndex << " reported laneID=" << subgroup.w
<< " outside its subgroupSize=" << subgroup.x;
return Failure("observed topology", message.str());
}
subgroupSlots[subgroup.z].push_back(slot);
}
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
if (subgroupSlots[subgroupID].empty()) {
std::ostringstream message;
message << "reported gl_NumSubgroups=" << reportedNumSubgroups
<< " but subgroupID " << subgroupID << " has no recorded members";
return Failure("observed topology", message.str());
}
auto& members = subgroupSlots[subgroupID];
std::sort(members.begin(), members.end(), [&output](std::size_t lhs, std::size_t rhs) {
return output.subgroup[lhs].w < output.subgroup[rhs].w;
});
for (std::size_t i = 1; i < members.size(); ++i) {
if (output.subgroup[members[i - 1]].w == output.subgroup[members[i]].w) {
std::ostringstream message;
message << "subgroupID " << subgroupID << " contains duplicate laneID "
<< output.subgroup[members[i]].w;
return Failure("observed topology", message.str());
}
}
}
// 3. Native subgroup arithmetic, in the actual lane ordering emitted
// by the driver. The fixture values and all partial sums are exactly
// representable binary32 values, so compare representation, not epsilon.
std::array<float, kInvocationCount> nativePrefix{};
std::vector<float> nativeSubtotal(reportedNumSubgroups, 0.0f);
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
float inclusive = 0.0f;
for (const std::size_t slot : subgroupSlots[subgroupID]) {
const std::uint32_t localIndex = output.invocation[slot].x;
inclusive += ExpectedInput(mode, localIndex);
nativePrefix[slot] = inclusive;
const float actualNative = output.reduction[slot].y;
if (!SameBits(actualNative, inclusive)) {
std::ostringstream message;
message << "subgroupID " << subgroupID << ", laneID " << output.subgroup[slot].w
<< ", localIndex " << localIndex << " nativeInclusive was " << FormatFloat(actualNative)
<< ", expected " << FormatFloat(inclusive);
return Failure("native subgroup arithmetic", message.str());
}
}
nativeSubtotal[subgroupID] = inclusive;
}
// sourceDomain is the narrow source-side safety branch. It is checked
// after native arithmetic so an unsupported source topology still
// reports native subgroup behavior before failing explicitly.
if (reportedNumSubgroups < 2u || reportedNumSubgroups > 32u) {
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
const std::size_t slot = slotForLocal[localIndex];
const Vec4& reduction = output.reduction[slot];
if (!IsQuietNanSentinel(reduction.z) || !IsQuietNanSentinel(reduction.w) ||
!IsQuietNanSentinel(output.finalAverage[slot])) {
std::ostringstream message;
message << "iterationRP source reduction has no valid contract for gl_NumSubgroups="
<< reportedNumSubgroups << "; localIndex " << localIndex
<< " did not preserve its qNaN source-reduction sentinel";
return Failure("source domain", message.str());
}
for (std::size_t stage = 0; stage < kScanStageCount; ++stage) {
if (!IsQuietNanSentinel(output.scanAfter[stage][slot])) {
std::ostringstream message;
message << "iterationRP source reduction has no valid contract for gl_NumSubgroups="
<< reportedNumSubgroups << "; localIndex " << localIndex << ", scan stage " << stage
<< " did not preserve its qNaN source-reduction sentinel";
return Failure("source domain", message.str());
}
}
}
std::ostringstream message;
message << "iterationRP source reduction has no valid contract for observed gl_NumSubgroups="
<< reportedNumSubgroups << " (requires 2..32); native subgroup results were recorded";
return Failure("source domain", message.str());
}
// 4. iterationRP source writer and first shared-memory handoff.
std::vector<std::size_t> sourceWriter(reportedNumSubgroups, kNoSlot);
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
std::size_t writerCount = 0;
for (const std::size_t slot : subgroupSlots[subgroupID]) {
const UVec4& subgroup = output.subgroup[slot];
if (subgroup.w == subgroup.x - 1u) {
sourceWriter[subgroupID] = slot;
++writerCount;
}
}
if (writerCount != 1u) {
std::ostringstream message;
message << "subgroupID " << subgroupID << " has " << writerCount
<< " recorded lane(s) where laneID == subgroupSize - 1; iterationRP leaves that "
"shared-cache entry unwritten";
return Failure("source writer", message.str());
}
for (const std::size_t slot : subgroupSlots[subgroupID]) {
const float actualRawSubtotal = output.reduction[slot].z;
if (!SameBits(actualRawSubtotal, nativeSubtotal[subgroupID])) {
std::ostringstream message;
message << "subgroupID " << subgroupID << ", localIndex " << output.invocation[slot].x
<< " sourceRawSubtotal was " << FormatFloat(actualRawSubtotal) << ", expected "
<< FormatFloat(nativeSubtotal[subgroupID]);
return Failure("source raw subtotal", message.str());
}
}
}
// 5. Reproduce the source loop exactly, including the redundant final
// scan iteration on power-of-two subgroup counts. Reads and writes in
// one iteration target disjoint cache entries, so update the cache at
// the CPU equivalent of the source barrier.
std::array<float, kInvocationCount> mergedPrefix = nativePrefix;
std::vector<float> cache = nativeSubtotal;
std::uint32_t loopLength = std::bit_width(reportedNumSubgroups) - 1u;
loopLength +=
static_cast<std::uint32_t>(reportedNumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (std::uint32_t scanStage = 0u; scanStage < loopLength; ++scanStage) {
std::vector<float> cacheAfterStage = cache;
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
if ((subgroupID & (1u << scanStage)) == 0u) continue;
const std::uint32_t sourceCacheIndex = (subgroupID >> scanStage << scanStage) - 1u;
const float sourcePrefix = cache[sourceCacheIndex];
for (const std::size_t slot : subgroupSlots[subgroupID]) {
mergedPrefix[slot] += sourcePrefix;
}
cacheAfterStage[subgroupID] = mergedPrefix[sourceWriter[subgroupID]];
}
cache.swap(cacheAfterStage);
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
const std::size_t slot = slotForLocal[localIndex];
const float actualAfterStage = output.scanAfter[scanStage][slot];
if (!SameBits(actualAfterStage, mergedPrefix[slot])) {
std::ostringstream message;
message << "scanStage " << scanStage << ", subgroupID " << output.subgroup[slot].z
<< ", laneID " << output.subgroup[slot].w << ", localIndex " << localIndex
<< " scanAfter was " << FormatFloat(actualAfterStage) << ", expected "
<< FormatFloat(mergedPrefix[slot]);
ValidationResult result = Failure("source scan", message.str());
result.scanStageMismatch = true;
result.scanStage = static_cast<int>(scanStage);
return result;
}
}
}
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
const std::size_t slot = slotForLocal[localIndex];
const float actualMergedPrefix = output.reduction[slot].w;
if (!SameBits(actualMergedPrefix, mergedPrefix[slot])) {
std::ostringstream message;
message << "localIndex " << localIndex << " sourceMergedPrefix was "
<< FormatFloat(actualMergedPrefix) << ", expected " << FormatFloat(mergedPrefix[slot]);
return Failure("source scan", message.str());
}
}
// 6. Final owner and average. The uniformity check is intentionally
// separate from the source's topology contract at local index 511.
const float firstAverage = output.finalAverage[slotForLocal[0]];
for (std::size_t localIndex = 1; localIndex < kInvocationCount; ++localIndex) {
const float actualAverage = output.finalAverage[slotForLocal[localIndex]];
if (!SameBits(actualAverage, firstAverage)) {
std::ostringstream message;
message << "finalAverage differs: localIndex 0 has " << FormatFloat(firstAverage)
<< ", localIndex " << localIndex << " has " << FormatFloat(actualAverage);
return Failure("final average", message.str());
}
}
ValidationResult ownerResult;
ownerResult.ownerEvaluated = true;
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
if (!subgroupSlots[subgroupID].empty()) {
ownerResult.highestObservedSubgroup = std::max(ownerResult.highestObservedSubgroup, subgroupID);
}
}
const std::size_t index511Slot = slotForLocal[kInvocationCount - 1u];
const UVec4& index511Subgroup = output.subgroup[index511Slot];
ownerResult.index511IsSourceLastLaneWriter =
index511Subgroup.w == index511Subgroup.x - 1u;
ownerResult.index511IsHighestSubgroupMember =
index511Subgroup.z == ownerResult.highestObservedSubgroup;
if (!ownerResult.index511IsSourceLastLaneWriter || !ownerResult.index511IsHighestSubgroupMember) {
std::ostringstream message;
message << "iterationRP topology incompatibility: localIndex 511 is sourceLastLaneWriter="
<< ownerResult.index511IsSourceLastLaneWriter << ", highestSubgroupMember="
<< ownerResult.index511IsHighestSubgroupMember << " (subgroupID=" << index511Subgroup.z
<< ", highest observed subgroupID=" << ownerResult.highestObservedSubgroup << ')';
ownerResult.ok = false;
ownerResult.phase = "final average";
ownerResult.message = message.str();
return ownerResult;
}
float total = 0.0f;
for (const float subtotal : nativeSubtotal) total += subtotal;
float sampledExpectedTotal = 0.0f;
for (std::size_t i = 0; i < kInvocationCount; ++i) sampledExpectedTotal += kSampledLuminance;
const float expectedTotal = mode == InputMode::IndexedSsbo ? 131328.0f : sampledExpectedTotal;
if (!SameBits(total, expectedTotal) || !SameBits(mergedPrefix[index511Slot], expectedTotal)) {
std::ostringstream message;
message << "iterationRP source total was " << FormatFloat(mergedPrefix[index511Slot])
<< " (native total " << FormatFloat(total) << "), expected " << FormatFloat(expectedTotal);
ownerResult.ok = false;
ownerResult.phase = "final average";
ownerResult.message = message.str();
return ownerResult;
}
const float expectedAverage = mode == InputMode::IndexedSsbo ? 256.5f : sampledExpectedTotal / 512.0f;
if (!SameBits(firstAverage, expectedAverage)) {
std::ostringstream message;
message << "finalAverage was " << FormatFloat(firstAverage) << ", expected "
<< FormatFloat(expectedAverage);
ownerResult.ok = false;
ownerResult.phase = "final average";
ownerResult.message = message.str();
return ownerResult;
}
return ownerResult;
}
void DumpProbe(const ProbeOutput& output, const CapabilityInfo& capabilities, const ValidationResult& validation,
bool includeScanStages) {
PrintMetadata(capabilities, std::cout);
if (validation.ok) {
std::cout << "IterationRPFirstReductionScenario firstFailure=none\n";
} else {
std::cout << "IterationRPFirstReductionScenario firstFailure=" << validation.phase << ": "
<< validation.message << '\n';
}
std::cout << "localIndex,localX,localY,localZ,subgroupSize,numSubgroups,subgroupID,laneID,input,"
"nativeInclusive,subgroupSubtotal,mergedPrefix,finalAverage\n";
for (std::size_t slot = 0; slot < kInvocationCount; ++slot) {
const UVec4& invocation = output.invocation[slot];
const UVec4& subgroup = output.subgroup[slot];
const Vec4& reduction = output.reduction[slot];
std::cout << invocation.x << ',' << invocation.y << ',' << invocation.z << ',' << invocation.w << ','
<< subgroup.x << ',' << subgroup.y << ',' << subgroup.z << ',' << subgroup.w << ','
<< std::hexfloat << reduction.x << ',' << reduction.y << ',' << reduction.z << ','
<< reduction.w << ',' << output.finalAverage[slot] << std::defaultfloat << '\n';
}
if (includeScanStages) {
std::cout << "scanStage,localIndex,scanAfter\n";
for (std::size_t scanStage = 0; scanStage < kScanStageCount; ++scanStage) {
for (std::size_t slot = 0; slot < kInvocationCount; ++slot) {
std::cout << scanStage << ',' << output.invocation[slot].x << ',' << std::hexfloat
<< output.scanAfter[scanStage][slot] << std::defaultfloat << '\n';
}
}
}
}
class IterationRPFirstReductionScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_capabilities = QueryCapabilities();
// GL_SUBGROUP_SIZE_KHR gates only whether the fixture's source contract
// can hold on this device (SubgroupWidthInSourceDomain); it is
// deliberately never used to infer lane placement or an expected group
// count - those come from observed values alone.
PrintMetadata(m_capabilities, std::cout);
RecordProperty("iterationrp_gl_subgroup_size_khr", std::to_string(m_capabilities.subgroupSize));
if (!m_capabilities.SupportsProbe()) {
GTEST_SKIP() << "subgroup probe requires " << m_capabilities.MissingRequirements();
}
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
if (m_texture != 0) glDeleteTextures(1, &m_texture);
if (m_inputBuffer != 0) glDeleteBuffers(1, &m_inputBuffer);
if (m_outputBuffer != 0) glDeleteBuffers(1, &m_outputBuffer);
if (m_program != 0) glDeleteProgram(m_program);
m_texture = 0;
m_inputBuffer = 0;
m_outputBuffer = 0;
m_program = 0;
}
GLuint CompileComputeProgram(const std::string& source, std::string* outError) {
const char* text = source.c_str();
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
if (shader == 0) {
*outError = "glCreateShader(GL_COMPUTE_SHADER) returned 0";
return 0;
}
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[8192] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
*outError = std::string("the subgroup probe compute shader did not compile: ") + log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[8192] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
*outError = std::string("the subgroup probe compute program did not link: ") + log;
glDeleteProgram(program);
return 0;
}
return program;
}
bool RunProbe(InputMode mode, ProbeOutput* output, std::string* outError) {
m_program = CompileComputeProgram(BuildProbeShader(mode), outError);
if (m_program == 0) return false;
ProbeOutput poison{};
std::memset(&poison, 0xa5, sizeof(poison));
glGenBuffers(1, &m_outputBuffer);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_outputBuffer);
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(ProbeOutput), &poison, GL_DYNAMIC_COPY);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_outputBuffer);
if (mode == InputMode::IndexedSsbo) {
std::array<float, kInvocationCount> values{};
for (std::size_t i = 0; i < values.size(); ++i) values[i] = static_cast<float>(i + 1u);
glGenBuffers(1, &m_inputBuffer);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_inputBuffer);
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(values), values.data(), GL_STATIC_DRAW);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_inputBuffer);
} else {
constexpr std::array<float, 4> kOneTexel = {1.0f, 1.0f, 1.0f, 1.0f};
glGenTextures(1, &m_texture);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, m_texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 1, 1, 0, GL_RGBA, GL_FLOAT, kOneTexel.data());
}
if (const GLenum error = FirstGLError(); error != GL_NO_ERROR) {
std::ostringstream message;
message << "subgroup probe resource setup left " << GLErrorName(error);
*outError = message.str();
return false;
}
glUseProgram(m_program);
if (mode == InputMode::SampledRgba32f) {
const GLint sampler = glGetUniformLocation(m_program, "colortex2");
const GLint pixelSize = glGetUniformLocation(m_program, "pixelSize");
if (sampler == -1 || pixelSize == -1) {
*outError = "the sampled probe uniforms were optimized away or not reflected";
return false;
}
glUniform1i(sampler, 3);
glUniform2f(pixelSize, 1.0f / 854.0f, 1.0f / 480.0f);
}
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_outputBuffer);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(ProbeOutput), output);
if (const GLenum error = FirstGLError(); error != GL_NO_ERROR) {
std::ostringstream message;
message << "subgroup probe dispatch/readback left " << GLErrorName(error);
*outError = message.str();
return false;
}
return true;
}
void RunAndValidate(InputMode mode) {
ProbeOutput output{};
std::string error;
ASSERT_TRUE(RunProbe(mode, &output, &error)) << InputModeName(mode) << ": " << error;
const ValidationResult validation = ValidateProbe(output, mode);
if (validation.ownerEvaluated) {
RecordProperty("iterationrp_index511_source_last_lane_writer",
validation.index511IsSourceLastLaneWriter ? "true" : "false");
RecordProperty("iterationrp_index511_highest_subgroup_member",
validation.index511IsHighestSubgroupMember ? "true" : "false");
RecordProperty("iterationrp_highest_observed_subgroup",
std::to_string(validation.highestObservedSubgroup));
std::cout << "IterationRPFirstReductionScenario owner: localIndex511 sourceLastLaneWriter="
<< validation.index511IsSourceLastLaneWriter << ", highestSubgroupMember="
<< validation.index511IsHighestSubgroupMember << ", highestObservedSubgroup="
<< validation.highestObservedSubgroup << '\n';
}
if (!validation.ok || DumpRequested()) {
DumpProbe(output, m_capabilities, validation, validation.scanStageMismatch || DumpRequested());
}
EXPECT_TRUE(validation.ok) << validation.phase << ": " << validation.message;
}
CapabilityInfo m_capabilities;
GLuint m_program = 0;
GLuint m_inputBuffer = 0;
GLuint m_outputBuffer = 0;
GLuint m_texture = 0;
};
} // namespace
TEST_F(IterationRPFirstReductionScenario, SampledRgba32fFirstAverage) {
if (!Ready() || IsSkipped()) return;
RunAndValidate(InputMode::SampledRgba32f);
}
TEST_F(IterationRPFirstReductionScenario, IndexedInputTopologyAndReduction) {
if (!Ready() || IsSkipped()) return;
RunAndValidate(InputMode::IndexedSsbo);
}
} // namespace MGITest
@@ -0,0 +1,379 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IterationRPProgram203Scenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Full iterationRP Program 203 golden input/output fixture. The original shader
// consumes deterministic complete textures and uniforms, then its complete
// 512x513 RG16F output image is compared against fixed half-float golden bits.
// This catches both a wrong exposure slot and collateral scratch corruption.
#include <array>
#include <bit>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kSceneWidth = 854;
constexpr int kSceneHeight = 480;
constexpr int kPixelDataWidth = 512;
constexpr int kPixelDataHeight = 513;
constexpr std::size_t kSceneTexelCount =
static_cast<std::size_t>(kSceneWidth) * kSceneHeight;
constexpr std::size_t kPixelDataTexelCount =
static_cast<std::size_t>(kPixelDataWidth) * kPixelDataHeight;
struct Rgba32f {
float r, g, b, a;
};
struct Rg16 {
std::uint16_t r, g;
};
static_assert(sizeof(Rgba32f) == 16);
static_assert(sizeof(Rg16) == 4);
// Captured from the fixed fixture on Adreno 830. These are the exact
// RG16F storage bits for (0.806640625, 8.2578125), not rounded decimal
// comparisons performed by the test.
constexpr Rg16 kGoldenExposure = {0x3a74u, 0x4821u};
constexpr const char* kCommonSource = R"glsl(
#version 430 core
#extension GL_KHR_shader_subgroup_arithmetic : require
uniform int frameCounter;
uniform float frameTime;
uniform float aspectRatio;
uniform vec2 pixelSize;
uniform float nightVision;
uniform float darknessLightFactor;
uniform sampler2D colortex2;
uniform sampler2D pixelData2D;
layout(rg16f) uniform image2D img_pixelData2D;
float remapSaturate(float x, float e0, float e1) {
return clamp((x - e0) / (e1 - e0), 0.0f, 1.0f);
}
float GetExposureValue(float luminance) {
float aeCurve = 0.65f;
aeCurve = mix(aeCurve, clamp(aeCurve * 1.2f, 0.0f, 1.0f), nightVision);
aeCurve *= remapSaturate(luminance, 2.0f, 1.0f) * 0.6f + 0.4f;
float ae = pow(luminance, -aeCurve);
ae *= 1.0f - min(darknessLightFactor * 2.0f, 0.9f);
ae *= 8.5f;
return ae;
}
)glsl";
constexpr const char* kOriginalMain = R"glsl(
layout(local_size_x = 32, local_size_y = 16) in;
shared vec2 prefixSumCache[32];
void main() {
vec2 texCoord = (vec2(gl_GlobalInvocationID.xy) + 0.5f) * vec2(1.0f / 32.0f, 1.0f / 16.0f);
vec2 sampleCoord = texCoord * (1.0f / 64.0f);
sampleCoord.x += (15.0f / 32.0f) + pixelSize.x * 12.0f;
float tileExposure = dot(textureLod(colortex2, sampleCoord, 0.0f).rgb,
vec3(0.2125f, 0.7154f, 0.0721f));
vec2 sampleLuminance = vec2(tileExposure, 0.0f);
sampleLuminance = subgroupInclusiveAdd(sampleLuminance);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
barrier();
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint i = 0u; i < loopLength; ++i) {
if ((gl_SubgroupID & (1u << i)) > 0u) {
sampleLuminance += prefixSumCache[(gl_SubgroupID >> i << i) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
}
barrier();
}
if (gl_LocalInvocationIndex == 511u)
prefixSumCache[0] = sampleLuminance / 512.0f;
barrier();
float avg = prefixSumCache[0].x;
vec2 tileDistance = texCoord * 2.0f - 1.0f;
tileDistance.y /= aspectRatio;
float centerDistance = length(tileDistance);
float tileWeight = remapSaturate(centerDistance, 0.6f, 0.4f);
tileExposure = max(7.0E-7f, tileExposure);
float lumaWeight = avg / tileExposure;
lumaWeight = pow(lumaWeight, remapSaturate(avg, 0.02f, 0.001f) * 0.4f + 0.2f);
tileWeight *= lumaWeight;
vec2 sampleExposure = vec2(tileExposure * tileWeight, tileWeight);
sampleExposure = subgroupInclusiveAdd(sampleExposure);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleExposure;
barrier();
for (uint i = 0u; i < loopLength; ++i) {
if ((gl_SubgroupID & (1u << i)) > 0u) {
sampleExposure += prefixSumCache[(gl_SubgroupID >> i << i) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleExposure;
}
barrier();
}
if (gl_LocalInvocationIndex == 511u) {
float avgExposure = max(sampleExposure.x / sampleExposure.y * 29.3f, 1.0E-10f);
avgExposure = log2(avgExposure);
float prevAvgExposure = log2(texelFetch(pixelData2D, ivec2(0, 0), 0).x);
float frameTimeFixed = frameTime + step(frameCounter, 20) * 100.0f;
float exposureTime = clamp(frameTimeFixed * 2.0f, 0.0f, 1.0f);
avgExposure = mix(prevAvgExposure, avgExposure, exposureTime);
avgExposure = max(exp2(avgExposure), 1.0E-5f);
float exposure = GetExposureValue(avgExposure);
imageStore(img_pixelData2D, ivec2(0, 0), vec4(avgExposure, exposure, 0.0f, 0.0f));
}
}
)glsl";
GLuint CompileCompute(const char* mainSource, std::string* error) {
const std::array<const GLchar*, 2> sources = {kCommonSource, mainSource};
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, static_cast<GLsizei>(sources.size()), sources.data(), nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled != GL_TRUE) {
std::array<char, 8192> log{};
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size() - 1), nullptr, log.data());
*error = log.data();
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked != GL_TRUE) {
std::array<char, 8192> log{};
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size() - 1), nullptr, log.data());
*error = log.data();
glDeleteProgram(program);
return 0;
}
return program;
}
std::vector<Rgba32f> MakeSceneInput() {
std::vector<Rgba32f> texels(kSceneTexelCount);
for (int y = 0; y < kSceneHeight; ++y) {
for (int x = 0; x < kSceneWidth; ++x) {
std::uint32_t h = static_cast<std::uint32_t>(x) * 0x9e3779b9u;
h ^= static_cast<std::uint32_t>(y) * 0x85ebca6bu;
h ^= h >> 16u;
h *= 0x7feb352du;
h ^= h >> 15u;
const float noise = static_cast<float>(h & 0xffffu) / 65535.0f;
float base = 0.0002f + noise * 0.075f;
const float dx = static_cast<float>(x - 420);
const float dy = static_cast<float>(y - 4);
base += 0.65f * std::exp(-(dx * dx + dy * dy) / 18.0f);
if (((x + y * 17) % 113) == 0) base += 1.75f;
texels[static_cast<std::size_t>(y) * kSceneWidth + x] =
{base * 0.83f, base * 1.07f, base * 1.31f, 1.0f};
}
}
return texels;
}
std::uint16_t FloatToHalf(float value) {
const std::uint32_t bits = std::bit_cast<std::uint32_t>(value);
const std::uint32_t sign = (bits >> 16u) & 0x8000u;
const std::uint32_t exponent = (bits >> 23u) & 0xffu;
std::uint32_t mantissa = bits & 0x7fffffu;
if (exponent == 0xffu) {
return static_cast<std::uint16_t>(sign | (mantissa == 0 ? 0x7c00u : 0x7e00u));
}
int halfExponent = static_cast<int>(exponent) - 127 + 15;
if (halfExponent >= 31) return static_cast<std::uint16_t>(sign | 0x7c00u);
if (halfExponent <= 0) {
if (halfExponent < -10) return static_cast<std::uint16_t>(sign);
mantissa |= 0x800000u;
const unsigned shift = static_cast<unsigned>(14 - halfExponent);
const std::uint32_t rounded = mantissa + ((1u << (shift - 1u)) - 1u) +
((mantissa >> shift) & 1u);
return static_cast<std::uint16_t>(sign | (rounded >> shift));
}
mantissa += 0xfffu + ((mantissa >> 13u) & 1u);
if ((mantissa & 0x800000u) != 0) {
mantissa = 0;
if (++halfExponent >= 31) return static_cast<std::uint16_t>(sign | 0x7c00u);
}
return static_cast<std::uint16_t>(sign | (static_cast<std::uint32_t>(halfExponent) << 10u) |
(mantissa >> 13u));
}
std::vector<Rg16> MakePixelDataInput() {
std::vector<Rg16> texels(kPixelDataTexelCount);
for (std::size_t i = 0; i < texels.size(); ++i) {
texels[i] = {FloatToHalf(0.35f + static_cast<float>(i % 97u) * 0.0025f),
FloatToHalf(-0.45f + static_cast<float>(i % 89u) * 0.01f)};
}
texels[0] = {FloatToHalf(0.73f), FloatToHalf(1.25f)};
return texels;
}
std::vector<Rg16> MakeGoldenOutput() {
std::vector<Rg16> golden = MakePixelDataInput();
golden[0] = kGoldenExposure;
return golden;
}
GLuint MakeTexture(GLenum internalFormat, GLenum format, GLenum type, int width, int height,
const void* data) {
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(internalFormat), width, height, 0, format,
type, data);
return texture;
}
void BindAndDispatch(GLuint program, GLuint scene, GLuint pixelData) {
glUseProgram(program);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, scene);
glUniform1i(glGetUniformLocation(program, "colortex2"), 3);
glActiveTexture(GL_TEXTURE4);
glBindTexture(GL_TEXTURE_2D, pixelData);
glUniform1i(glGetUniformLocation(program, "pixelData2D"), 4);
glBindImageTexture(0, pixelData, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RG16F);
glUniform1i(glGetUniformLocation(program, "img_pixelData2D"), 0);
glUniform1i(glGetUniformLocation(program, "frameCounter"), 100);
glUniform1f(glGetUniformLocation(program, "frameTime"), 1.0f / 60.0f);
glUniform1f(glGetUniformLocation(program, "aspectRatio"),
static_cast<float>(kSceneWidth) / kSceneHeight);
glUniform2f(glGetUniformLocation(program, "pixelSize"), 1.0f / kSceneWidth, 1.0f / kSceneHeight);
glUniform1f(glGetUniformLocation(program, "nightVision"), 0.23f);
glUniform1f(glGetUniformLocation(program, "darknessLightFactor"), 0.08f);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
}
std::vector<Rg16> ReadWholeRgTexture(GLuint texture) {
std::vector<Rg16> texels(kPixelDataTexelCount);
glBindTexture(GL_TEXTURE_2D, texture);
glGetTexImage(GL_TEXTURE_2D, 0, GL_RG, GL_HALF_FLOAT, texels.data());
return texels;
}
class IterationRPProgram203Scenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
GLint stages = 0;
GLint features = 0;
GLint invocations = 0;
glGetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &stages);
glGetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &features);
glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &invocations);
const GLbitfield required =
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR;
if ((static_cast<GLbitfield>(stages) & GL_COMPUTE_SHADER_BIT) == 0 ||
(static_cast<GLbitfield>(features) & required) != required || invocations < 512) {
GTEST_SKIP() << "requires 512-invocation basic+arithmetic compute subgroups";
}
std::string error;
m_original = CompileCompute(kOriginalMain, &error);
ASSERT_NE(m_original, 0u) << "original Program 203: " << error;
const std::vector<Rgba32f> scene = MakeSceneInput();
const std::vector<Rg16> pixelData = MakePixelDataInput();
m_scene = MakeTexture(GL_RGBA16F, GL_RGBA, GL_FLOAT, kSceneWidth, kSceneHeight, scene.data());
m_originalOutput =
MakeTexture(GL_RG16F, GL_RG, GL_HALF_FLOAT, kPixelDataWidth, kPixelDataHeight,
pixelData.data());
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
}
void TearDown() override {
if (!Ready()) return;
const std::array<GLuint, 2> textures = {m_scene, m_originalOutput};
glDeleteTextures(static_cast<GLsizei>(textures.size()), textures.data());
if (m_original != 0) glDeleteProgram(m_original);
}
GLuint m_original = 0;
GLuint m_scene = 0;
GLuint m_originalOutput = 0;
};
} // namespace
TEST_F(IterationRPProgram203Scenario, FixedCompleteInputProducesFixedCompleteGoldenOutput) {
if (!Ready()) return;
BindAndDispatch(m_original, m_scene, m_originalOutput);
glFinish();
const std::vector<Rg16> actual = ReadWholeRgTexture(m_originalOutput);
const std::vector<Rg16> expected = MakeGoldenOutput();
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
std::size_t mismatchTexels = 0;
std::size_t firstMismatch = actual.size();
for (std::size_t i = 0; i < actual.size(); ++i) {
if (actual[i].r != expected[i].r || actual[i].g != expected[i].g) {
if (firstMismatch == actual.size()) firstMismatch = i;
++mismatchTexels;
}
}
RecordProperty("program203_output_width", kPixelDataWidth);
RecordProperty("program203_output_height", kPixelDataHeight);
RecordProperty("program203_compared_texels", static_cast<long long>(actual.size()));
RecordProperty("program203_mismatch_texels", static_cast<long long>(mismatchTexels));
std::cout << "IterationRPProgram203Scenario complete-output actualExposureBits=(0x" << std::hex
<< actual[0].r << ", 0x" << actual[0].g << ") goldenExposureBits=(0x" << expected[0].r
<< ", 0x" << expected[0].g << std::dec << ") mismatches=" << mismatchTexels << '/'
<< actual.size() << '\n';
if (firstMismatch != actual.size()) {
const std::size_t x = firstMismatch % kPixelDataWidth;
const std::size_t y = firstMismatch / kPixelDataWidth;
ADD_FAILURE() << "complete Program 203 output differs at " << x << ',' << y
<< ": actual half bits=(0x" << std::hex << actual[firstMismatch].r << ", 0x"
<< actual[firstMismatch].g << ") golden half bits=(0x" << expected[firstMismatch].r
<< ", 0x" << expected[firstMismatch].g << std::dec << "); mismatched "
<< mismatchTexels << " of " << actual.size() << " texels";
}
EXPECT_EQ(mismatchTexels, 0u);
}
} // namespace MGITest
@@ -0,0 +1,302 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IterationRPScratchFixScenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - THE FIXTURE-SHAPED SUBGROUP REDUCTION, ON WHATEVER WIDTH THE DEVICE HAS.
//
// iterationRP hard-sizes the scratch its subgroup prefix scans write through
// prefixSumCache[gl_SubgroupID], and ships that idiom twice: the auto-exposure pass
// declares `shared vec2 prefixSumCache[32]` for a 512-invocation workgroup, and the
// RTW importance warp declares `shared float prefixSumCache[64]` for a 1024-invocation
// one. Both algorithms are width-agnostic; only the static lengths bake in "at most 32
// (respectively 64) subgroups", which every desktop capture satisfies and an 8-lane
// device (lavapipe: 64 and 128 subgroups) does not. DirectVulkan patches exactly that with
// FixIterationRPSubgroupScratchPass, growing the array to ceil(invocations / native
// width) on the modules that match the pack's reduction fingerprint.
//
// This scenario replays the fixture's reduction shape verbatim - the same 32-entry
// declaration, the same last-lane handoff, the same findMSB combine loop, and NO
// domain guard - and asserts only the width-independent result: the workgroup total.
// The inputs are small integers, so the fp32 sum is exact under any lane order and any
// association; a correct run produces the exact constant on a 4-lane device and a
// 128-lane device alike. Without the patch, a sub-16-lane device indexes the
// 32-entry array out of bounds - on lavapipe that is literal heap corruption - and
// this scenario is the regression test that keeps the patch working, and it runs on every device that
// has basic+arithmetic compute subgroups (unlike IterationRPFirstReductionScenario,
// which probes the UNREPAIRED source contract and must skip outside [16, 256]).
#include <cstdint>
#include <cstring>
#include <string>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr std::uint32_t kInvocationCount = 512u;
// sum of 0..511, exactly representable and associativity-proof in fp32.
constexpr float kExpectedTotal = 130816.0f;
// The RTW warp's shape: 1024 invocations into a 64-entry float scratch.
constexpr std::uint32_t kWideInvocationCount = 1024u;
// sum of 0..1023, likewise exact in fp32.
constexpr float kWideExpectedTotal = 523776.0f;
constexpr const char* kComputeSource = R"(#version 430 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output {
float total;
uint numSubgroups;
uint maxSubgroupId;
} outputData;
shared vec2 prefixSumCache[32];
void main() {
vec2 sampleLuminance = vec2(float(gl_LocalInvocationIndex), 0.0);
sampleLuminance = subgroupInclusiveAdd(sampleLuminance);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
barrier();
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) {
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
sampleLuminance += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
}
barrier();
}
if (gl_LocalInvocationIndex == 511u) {
outputData.total = sampleLuminance.x;
outputData.numSubgroups = gl_NumSubgroups;
}
atomicMax(outputData.maxSubgroupId, gl_SubgroupID);
}
)";
// The RTW importance warp's shape: a plain float scan over 1024 invocations
// into a 64-entry scratch. Same idiom, different dimensions - which is exactly
// what a fingerprint pinned to the exposure pass's shape walks past.
constexpr const char* kWideComputeSource = R"(#version 430 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 1024) in;
layout(std430, binding = 0) buffer Output {
float total;
uint numSubgroups;
uint maxSubgroupId;
} outputData;
shared float prefixSumCache[64];
void main() {
float importance = float(gl_LocalInvocationID.x);
float prefixSum = subgroupInclusiveAdd(importance);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = prefixSum;
barrier();
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) {
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
prefixSum += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = prefixSum;
}
barrier();
}
if (gl_LocalInvocationID.x == 1023u) {
outputData.total = prefixSum;
outputData.numSubgroups = gl_NumSubgroups;
}
atomicMax(outputData.maxSubgroupId, gl_SubgroupID);
}
)";
struct OutputBlock {
float total = -1.0f;
std::uint32_t numSubgroups = 0;
std::uint32_t maxSubgroupId = 0;
};
bool HasExtension(const char* wanted) {
GLint extensionCount = 0;
glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount);
for (GLint i = 0; i < extensionCount; ++i) {
const auto* extension =
reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, static_cast<GLuint>(i)));
if (extension != nullptr && std::string(extension) == wanted) return true;
}
return false;
}
class IterationRPScratchFixScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
GLint stages = 0;
GLint features = 0;
GLint invocations = 0;
const bool subgroupExtension = HasExtension("GL_KHR_shader_subgroup");
if (subgroupExtension) {
glGetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &stages);
glGetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &features);
}
glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &invocations);
const GLbitfield requiredFeatures =
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR;
if (!subgroupExtension || (static_cast<GLbitfield>(stages) & GL_COMPUTE_SHADER_BIT) == 0 ||
(static_cast<GLbitfield>(features) & requiredFeatures) != requiredFeatures ||
invocations < static_cast<GLint>(kInvocationCount)) {
GTEST_SKIP() << "needs GL_KHR_shader_subgroup basic+arithmetic in compute and a "
"512-invocation workgroup";
}
m_maxInvocations = static_cast<std::uint32_t>(invocations);
glGenBuffers(1, &m_output);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
// maxSubgroupId starts at zero HOST-side: the word is touched only by
// atomicMax during the dispatch, since a plain shader-side zeroing store
// would race the other invocations' atomics (barrier() orders shared
// memory, not SSBO stores).
const OutputBlock poison{-1.0f, 0xa5a5a5a5u, 0u};
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(OutputBlock), &poison, GL_DYNAMIC_READ);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_output);
}
void TearDown() override {
if (!Ready()) return;
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
if (m_output != 0) glDeleteBuffers(1, &m_output);
if (m_program != 0) glDeleteProgram(m_program);
}
unsigned int CompileComputeProgram(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[2048] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute shader did not compile: ") + log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[2048] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute program did not link: ") + log;
glDeleteProgram(program);
return 0;
}
return program;
}
// Re-poisons the block, compiles the shape under test and runs it once.
OutputBlock Dispatch(const char* source) {
const OutputBlock poison{-1.0f, 0xa5a5a5a5u, 0u};
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(OutputBlock), &poison);
m_program = CompileComputeProgram(source);
EXPECT_NE(m_program, 0u) << m_buildLog;
if (m_program == 0u) return OutputBlock{};
glUseProgram(m_program);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
OutputBlock block{};
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(OutputBlock), &block);
return block;
}
GLuint m_program = 0;
GLuint m_output = 0;
std::uint32_t m_maxInvocations = 0;
std::string m_buildLog;
};
} // namespace
TEST_F(IterationRPScratchFixScenario, FixtureShapedReductionSumsEveryInvocation) {
const OutputBlock block = Dispatch(kComputeSource);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
// The topology diagnostics catch the failure modes by name before the sum does:
// an out-of-bounds handoff corrupts the total, a wrong gl_NumSubgroups breaks
// the combine loop's length.
ASSERT_NE(block.numSubgroups, 0xa5a5a5a5u) << "invocation 511 never reached its store";
EXPECT_GE(block.numSubgroups, 1u);
EXPECT_LE(block.numSubgroups, kInvocationCount);
EXPECT_LT(block.maxSubgroupId, block.numSubgroups)
<< "gl_SubgroupID exceeds gl_NumSubgroups - the inconsistency "
"DeriveNumSubgroupsPass exists to repair";
// Integer-valued fp32 inputs: the workgroup total is exact under any subgroup
// width, lane order, and association. This is the value iterationRP's exposure
// average is built from; without FixIterationRPSubgroupScratchPass an 8-lane
// device writes prefixSumCache[32..63] out of bounds and this comparison fails.
EXPECT_EQ(block.total, kExpectedTotal)
<< "workgroup reduction produced " << block.total << " with gl_NumSubgroups="
<< block.numSubgroups;
}
// The pack's second instance of the same bug, and the one that kept the CI
// retrace red after the exposure pass alone was patched.
TEST_F(IterationRPScratchFixScenario, WideFixtureShapedReductionSumsEveryInvocation) {
if (m_maxInvocations < kWideInvocationCount) {
GTEST_SKIP() << "needs a " << kWideInvocationCount << "-invocation workgroup";
}
const OutputBlock block = Dispatch(kWideComputeSource);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
ASSERT_NE(block.numSubgroups, 0xa5a5a5a5u) << "invocation 1023 never reached its store";
EXPECT_GE(block.numSubgroups, 1u);
EXPECT_LE(block.numSubgroups, kWideInvocationCount);
EXPECT_LT(block.maxSubgroupId, block.numSubgroups)
<< "gl_SubgroupID exceeds gl_NumSubgroups - the inconsistency "
"DeriveNumSubgroupsPass exists to repair";
// Without the patch an 8-lane device writes prefixSumCache[64..127] out of
// bounds and this comparison fails.
EXPECT_EQ(block.total, kWideExpectedTotal)
<< "workgroup reduction produced " << block.total << " with gl_NumSubgroups="
<< block.numSubgroups;
}
} // namespace MGITest
@@ -428,15 +428,15 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
std::vector<GLfloat> pixels(static_cast<size_t>(kWidth) * kHeight, 0.0f);
glReadPixels(0, 0, kWidth, kHeight, GL_RED, GL_FLOAT, pixels.data());
for (int i = 0; i < kViewportCount; ++i) {
const float near = static_cast<float>(i) / 16.0f;
const float far = 1.0f - static_cast<float>(i) / 16.0f;
const float nearDepth = static_cast<float>(i) / 16.0f;
const float farDepth = 1.0f - static_cast<float>(i) / 16.0f;
// The tolerance covers depth-buffer-free rasterization of gl_FragCoord.z on a
// software rasterizer; the per-index values are 1/16 apart, so it cannot let a
// neighbouring viewport's range through, and viewport 0's range (0, 1) differs
// from every other index by at least 1/16.
EXPECT_NEAR(pixels[i], near, 1.0e-3f)
EXPECT_NEAR(pixels[i], nearDepth, 1.0e-3f)
<< "viewport " << i << " near-plane depth; got viewport 0's range if this is 0";
EXPECT_NEAR(pixels[static_cast<size_t>(kWidth) + i], far, 1.0e-3f)
EXPECT_NEAR(pixels[static_cast<size_t>(kWidth) + i], farDepth, 1.0e-3f)
<< "viewport " << i << " far-plane depth; got viewport 0's range if this is 1";
}
@@ -250,6 +250,34 @@ namespace MobileGL::MG_State::GLState {
NotifyContentWrite(atOffset, data.size);
}
void BufferObject::FillSubData(DataPtr pattern, SizeT atOffset, SizeT size) {
MOBILEGL_ASSERT(pattern.data != nullptr && pattern.size > 0,
"FillSubData requires a non-empty pattern.");
MOBILEGL_ASSERT(size % pattern.size == 0,
"FillSubData size (%zu) must be a multiple of pattern size (%zu).", size, pattern.size);
MOBILEGL_ASSERT(atOffset <= m_size && size <= m_size - atOffset,
"FillSubData out of bounds: atOffset (%zu) + size (%zu) > m_size (%zu)", atOffset, size,
m_size);
MOBILEGL_ASSERT(!m_isMapped || (m_mappingAccess & BufferMappingAccessBit::Persistent),
"Cannot fill data while buffer is non-persistently mapped.");
if (size == 0) return;
// A clear is ordered after all earlier GPU writes. Partial clears additionally need the
// retained shadow bytes; whole-store clears need the same synchronization before writing
// an adopted persistent mapping that the GPU may still be accessing.
SyncGpuWrites();
Uint8* dst = m_resource.Bytes() + atOffset;
if (pattern.size == 1) {
Memset(dst, *static_cast<const Uint8*>(pattern.data), size);
} else {
for (SizeT at = 0; at < size; at += pattern.size) {
Memcpy(dst + at, pattern.data, pattern.size);
}
}
NotifyContentWrite(atOffset, size);
}
void BufferObject::DownloadSubData(void* dst, SizeT atOffset, SizeT size) const {
MOBILEGL_ASSERT(atOffset + size <= m_size,
"DownloadSubData out of bounds: atOffset (%zu) + size (%zu) > m_size (%zu)", atOffset, size,
@@ -132,6 +132,9 @@ namespace MobileGL {
void UploadData(DataPtr data, SizeT atOffset);
void UploadSubData(DataPtr data, SizeT atOffset);
// Repeats one already-converted element through [atOffset, atOffset + size) and
// publishes the range as one content mutation.
void FillSubData(DataPtr pattern, SizeT atOffset, SizeT size);
// Reads `size` bytes from the CPU shadow at `atOffset` into `dst` (glGetBufferSubData).
// The shadow reflects CPU writes (BufferData/SubData/maps) and backend write-backs, but not
// arbitrary GPU-side writes.
+16 -3
View File
@@ -39,6 +39,11 @@ namespace MobileGL::MG_State {
return m_compileEnv;
}
void GLContext::InvalidateCompileEnv() {
m_compileEnv.reset();
m_compileEnvBackend = nullptr;
}
// Error
void GLContext::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
// Invariant I1, mechanically enforced: the GL error state is GL-thread-owned.
@@ -641,9 +646,17 @@ namespace MobileGL::MG_State {
for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
if (!stageProgram) continue;
for (const auto& shader : stageProgram->GetAttachedShaders()) {
if (!shader || static_cast<SizeT>(shader->GetShaderStage()) != stage) continue;
composite->AttachShader(shader);
// The stage program contributes the shaders its LAST LINK consumed, never
// its live attach list: per GL 4.6 7.3/7.4 a pipeline stage executes the
// stage program as last linked - glAttachShader and glCompileShader take
// effect only at the program's next link - and neither of those moves the
// link version this cache keys on, so reading live state here would let a
// post-link attach or recompile leak into the composite while the signature
// still hits. The pinned (source, node) makes the composite's Link()
// consume the very inputs that link consumed.
for (const auto& ref : stageProgram->GetLinkedShaderSnapshot()) {
if (!ref.shader || static_cast<SizeT>(ref.shader->GetShaderStage()) != stage) continue;
composite->AttachShaderWithPinnedLinkInput(ref);
anyStage = true;
}
}
+4 -1
View File
@@ -413,9 +413,12 @@ namespace MobileGL {
// cannot be captured in MG_State::Init() - that runs BEFORE MG_Backend::Init(),
// so there is no backend to query yet. Re-captured whenever the active backend
// object changes, which also rolls the fingerprint and therefore invalidates
// every P0b preprocess memo keyed against the old one.
// every P0b preprocess memo keyed against the old one. A backend whose dynamic
// capabilities become available without changing object identity must call
// InvalidateCompileEnv() after publishing them.
// GL thread only.
const SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv>& GetCompileEnv();
void InvalidateCompileEnv();
private:
// State Components
@@ -9,7 +9,18 @@
#include "FramebufferObject.h"
#include "MG_Util/Types.h"
#include <atomic>
namespace MobileGL::MG_State::GLState {
// Starts at 1 so a zero-initialized memo slot can never carry a live object's id.
// Atomic for the same reason as the VAO counter: it costs nothing, and a duplicate
// id would resurrect exactly the ABA this id exists to kill.
static std::atomic<Uint64> s_nextFramebufferLifetimeId{1};
Uint64 FramebufferObject::AllocateLifetimeId() {
return s_nextFramebufferLifetimeId.fetch_add(1, std::memory_order_relaxed);
}
// FramebufferAttachmentObject
FramebufferAttachmentObject::FramebufferAttachmentObject(
const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget textureUploadTarget, Int level,
@@ -148,13 +148,25 @@ namespace MobileGL {
Uint16 GetObjectVersion() const { return m_objectVersion; }
// Globally-unique, never-reused id for THIS object's lifetime - the same
// contract as VertexArrayObject::GetLifetimeId(), and needed for the same
// reason: neither the GL name nor the heap address can tell a
// deleted-and-recreated framebuffer from the original, and m_objectVersion
// starts at 0 for every new object, so a backend memo keyed on
// (pointer, version) alone would silently inherit the dead object's entry
// (see VkRenderPassManager's per-draw fast-path memo).
Uint64 GetLifetimeId() const { return m_lifetimeId; }
Uint GetExternalIndex() const;
Bool IsDefaultFramebuffer() const { return m_externalIndex == 0; }
private:
static Uint64 AllocateLifetimeId();
void BumpAttachmentVersion(FramebufferAttachmentType type);
const Uint m_externalIndex = 0;
const Uint64 m_lifetimeId = AllocateLifetimeId();
FramebufferAttachmentObjectArray m_attachmentObjects;
FramebufferAttachmentVersionArray m_attachmentVersions;
@@ -60,6 +60,8 @@ namespace MobileGL::MG_State::GLState {
Uint externalIndex = 0; // logs only
Vector<LinkShaderInput> shaders; // already stage-sorted
SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env;
// Startup configuration copied with the task, never read from worker code.
Bool enableSpirvValidation = false;
// The four "takes effect at the next link" request maps. Snapshotted rather than
// referenced, which is precisely what makes glBindAttribLocation and friends
// legal to call over a pending link without cancelling it: the pending link keeps
@@ -393,6 +393,14 @@ namespace MobileGL::MG_State::GLState {
return true;
}
bool ProgramObject::AttachShaderWithPinnedLinkInput(const LinkedShaderRef& ref) {
if (!AttachShader(ref.shader)) {
return false;
}
m_pinnedLinkInputs[ref.shader.get()] = ref;
return true;
}
SizeT ProgramObject::DetachShader(const SharedPtr<ShaderObject>& shader) {
MGLOG_D("DetachShader called for shader %p from ProgramObject %u", shader.get(), m_externalIndex);
if (!ShaderIsAttached(shader)) {
@@ -475,6 +483,8 @@ namespace MobileGL::MG_State::GLState {
AddDefaultFragmentShaderIfMissing();
}
if (m_shaders.empty()) {
// This IS the last link now, and it consumed nothing.
m_linkedShaderSnapshot.clear();
m_artifacts.infoLog = "No shader objects are attached to program.";
MGLOG_E("ProgramObject %u: Link failed - no shader objects attached.", m_externalIndex);
return;
@@ -494,6 +504,7 @@ namespace MobileGL::MG_State::GLState {
auto task = MakeShared<ProgramLinkTask>();
task->in.externalIndex = m_externalIndex;
task->in.env = MG_Util::ShaderTranspiler::GetCurrentCompileEnv();
task->in.enableSpirvValidation = MG_Config::Features.EnableSpirvValidation;
task->in.explicitAttribLocations = m_explicitAttribLocations;
task->in.explicitFragDataLocation = m_explicitFragDataLocation;
task->in.explicitFragDataIndex = m_explicitFragDataIndex;
@@ -504,8 +515,19 @@ namespace MobileGL::MG_State::GLState {
Vector<SharedPtr<ShaderCompileTask>> deps;
deps.reserve(m_shaders.size());
task->in.shaders.reserve(m_shaders.size());
m_linkedShaderSnapshot.clear();
m_linkedShaderSnapshot.reserve(m_shaders.size());
for (const auto& shader : m_shaders) {
const SharedPtr<ShaderCompileTask>& node = shader->CompiledNodeForLink();
// A pipeline composite pins the (source, node) each stage program's LAST link
// consumed (AttachShaderWithPinnedLinkInput); an ordinary program takes the
// shader's current ones. Without the pin a post-link recompile would leak a
// shader the stage program never linked into the composite.
SharedPtr<const String> sourcePtr = shader->GetShaderSourcePtr();
SharedPtr<ShaderCompileTask> node = shader->CompiledNodeForLink();
if (const auto pinned = m_pinnedLinkInputs.find(shader.get()); pinned != m_pinnedLinkInputs.end()) {
sourcePtr = pinned->second.source;
node = pinned->second.node;
}
if (node) {
// This link is now an observer of that node's result, and the ShaderObject is
// no longer the only route to it: without the marker, the ordinary
@@ -514,7 +536,10 @@ namespace MobileGL::MG_State::GLState {
node->MarkLinkReferenced();
if (!node->IsTerminal()) deps.push_back(node);
}
task->in.shaders.push_back({shader->GetShaderStage(), shader->GetShaderSourcePtr(), node});
task->in.shaders.push_back({shader->GetShaderStage(), sourcePtr, node});
// What "as last linked" will mean for this program from now on - the pipeline
// composite cache rebuilds from exactly this set (GetProgramForDraw).
m_linkedShaderSnapshot.push_back({shader, sourcePtr, node});
}
// Phase B of the same link: SPIR-V generation, spirv-opt and the global-UBO routing
@@ -60,6 +60,26 @@ namespace MobileGL::MG_State::GLState {
Vector<SharedPtr<ShaderObject>>& GetAttachedShaders();
const Vector<SharedPtr<ShaderObject>>& GetAttachedShaders() const;
// One shader exactly as this program's last Link() consumed it: the object, the
// source snapshot, and the compile node taken at that link's enqueue. GL 4.6 7.3/7.4
// makes this triple - not the live attach list, not the shader's current compile -
// what a program pipeline stage executes ("as last linked"): glAttachShader and
// glCompileShader take effect only at the program's next link, yet neither moves
// m_linkVersion, so anything keyed on the link generation must consume this
// snapshot rather than re-read the live state.
struct LinkedShaderRef {
SharedPtr<ShaderObject> shader;
SharedPtr<const String> source;
SharedPtr<ShaderCompileTask> node;
};
// The last link's full input set; empty when this program has never linked (or its
// last link had no shaders attached). GL-thread-owned, rebuilt in Link()'s prologue.
const Vector<LinkedShaderRef>& GetLinkedShaderSnapshot() const { return m_linkedShaderSnapshot; }
// Pipeline-composite attach: AttachShader plus a pin that makes THIS program's
// Link() consume ref's (source, node) instead of the shader's current ones, so a
// post-link recompile of the stage program's shader cannot leak into the composite.
bool AttachShaderWithPinnedLinkInput(const LinkedShaderRef& ref);
const String& GetInfoLog() const { return Artifacts().infoLog; }
// glCreateShaderProgramv folds the shader's compile log into the program's log, which
// is the only place a caller can read it from once the shader name is gone.
@@ -786,6 +806,13 @@ namespace MobileGL::MG_State::GLState {
// order - and the name is the only coordinate all three agree on. Absent from the map
// means "never rebound", and the shader's declared binding still stands.
void SetShaderStorageBlockBinding(const String& blockName, Uint binding) {
// Equality bail-out like SetUniformBlockBinding's: the pipeline composite
// mirror replays every override each draw, and without this every replay
// would churn m_blockBindingVersion and rebuild whatever keys on it.
const auto it = Artifacts().shaderStorageBlockBinding.find(blockName);
if (it != Artifacts().shaderStorageBlockBinding.end() && it->second == static_cast<Int>(binding)) {
return;
}
Artifacts().shaderStorageBlockBinding[blockName] = static_cast<Int>(binding);
// Deliberately NOT m_backendStateVersion: Espryt's entry point never forces a
// program build off this, and bumping that version would start doing so. The
@@ -819,6 +846,9 @@ namespace MobileGL::MG_State::GLState {
// backend asks this exactly where it used to ask GetLinkStatus(), i.e. right before
// it builds or draws with the program.
Bool GetSpirvStatus() const { return Spirv().spirvStatus; }
// Copied from the link task that generated this program's SPIR-V. Backends use it for
// their final transforms, which must honor the same diagnostic setting as phase B.
Bool GetSpirvValidationEnabled() const { return Spirv().enableSpirvValidation; }
// The linked glslang reflection itself, for the ONE consumer that needs resource
// lists no typed getter above exposes: the GL program-interface query layer
@@ -985,6 +1015,7 @@ namespace MobileGL::MG_State::GLState {
// cannot be lifted out of glslang's reflection instead.
struct SpirvArtifacts {
Vector<Vector<unsigned>> generatedSpirv;
Bool enableSpirvValidation = false;
// Byte offset of each uniform location inside globalUboScratch, or
// kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass.
Vector<Uint> uniformOffsets;
@@ -1219,6 +1250,13 @@ namespace MobileGL::MG_State::GLState {
// glGetAttachedShaders / GL_ATTACHED_SHADERS / the orphan-shader sweep need no join.
Vector<SharedPtr<ShaderObject>> m_shaders;
Vector<SharedPtr<ShaderObject>> m_detachedShaders; // Store detached shaders and remove on next link
// See GetLinkedShaderSnapshot. Holding the SharedPtrs here is deliberate: the
// "as last linked" set must survive detach-and-delete of its shaders (the
// glCreateShaderProgramv shape) until the next link replaces it.
Vector<LinkedShaderRef> m_linkedShaderSnapshot;
// See AttachShaderWithPinnedLinkInput. Populated only on pipeline composites,
// which never detach, so entries need no removal path. GL-thread-owned.
UnorderedMap<const ShaderObject*, LinkedShaderRef> m_pinnedLinkInputs;
// Link INPUTS (all "take effect at the next link" per GL): glBindAttribLocation,
// glBindFragDataLocation(Indexed), glTransformFeedbackVaryings, and the draw-buffer
@@ -102,7 +102,11 @@ namespace MobileGL::MG_State::GLState {
}
MGLOG_D("ProgramObject %u: Starting SPIR-V generation", externalIndex);
GenerateSpirv(handoff, externalIndex);
const Bool deferOutputValidationForDirectVulkan =
m_phaseA->in.env != nullptr && m_phaseA->in.env->backend == BackendType::DirectVulkan;
const Bool enableSpirvValidation = m_phaseA->in.enableSpirvValidation;
artifacts.enableSpirvValidation = enableSpirvValidation;
GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation);
// GlslangToSpv was the only consumer of the parsed ASTs; everything after this point
// works on the SPIR-V and on the TProgram's own self-contained reflection pool. Drop
// them here rather than at the end of the body, which is ~87% of this node's runtime
@@ -137,7 +141,9 @@ namespace MobileGL::MG_State::GLState {
artifacts.generatedSpirv.size());
}
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex) {
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex,
const Bool deferOutputValidationForDirectVulkan,
const Bool enableSpirvValidation) {
/* As we passed first stage compilation/linking,
* we'll assume all the operations here should
* pass. We may be able to employ some optimizations
@@ -169,7 +175,8 @@ namespace MobileGL::MG_State::GLState {
Bool allOptimized = true;
{
for (auto& spv : artifacts.generatedSpirv) {
auto success = ShaderCompiler::SanitizeAndOptimizeBinary(spv, spv);
auto success = ShaderCompiler::SanitizeAndOptimizeBinary(
spv, spv, !deferOutputValidationForDirectVulkan, enableSpirvValidation);
if (!success) {
// The one genuine phase-B failure mode: one of the seven optimizer passes
// reported failure, so `spv` is whatever the run left behind. A fordebug
@@ -65,7 +65,8 @@ namespace MobileGL::MG_State::GLState {
private:
void RunBody() override;
void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex,
Bool deferOutputValidationForDirectVulkan, Bool enableSpirvValidation);
void BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
// Worker-side MGLOG replacement, replayed by the join on the GL thread. Same reason as
@@ -140,6 +140,13 @@ namespace MobileGL::MG_State::GLState {
}
void ShaderObject::Compile() {
// The compile-environment snapshot is taken HERE, on the GL thread, and handed to
// the job. Everything the pipeline needs to know about the device comes through it,
// never through pActiveBackendObject - that is what makes the body movable.
// Hoisted above the memo check because the memo must be env-disciplined too (below).
const SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env =
MG_Util::ShaderTranspiler::GetCurrentCompileEnv();
// P0b layer 1, as a tri-state: the memo is "the node in m_compiled was built from
// the string m_source still points at". SetShaderSource only swaps that pointer when
// the text actually differs, so this is a pointer compare, and it covers Pending as
@@ -152,7 +159,18 @@ namespace MobileGL::MG_State::GLState {
// ClaimParsedShader's on-demand re-parse needs - a real recompile would have handed
// the next link a fresh parse, the no-op hands it a fresh re-parse of the identical
// source instead. Same result, one parse either way.
if (HasMemoizedCompile()) return;
//
// The environment joins the check (ShaderSourceKey.h's memo-hazard rule: a memo
// must never be handed back under an environment other than the one it was
// computed against). Layers 2 and 3 key on the fingerprint, but this memo sits
// ABOVE both, so without this compare a node computed against a dead environment
// - e.g. a compute shader rejected against the pre-capability fallback limits -
// would keep answering forever while a fresh object with byte-identical source
// compiles fine. The fingerprint is a content hash, so a republish of identical
// capabilities still hits.
if (HasMemoizedCompile() && m_compiled->env != nullptr && m_compiled->env->fingerprint == env->fingerprint) {
return;
}
// Two reasons to stay on this thread, one rule. Without the async flag the whole
// path must be byte-identical to the synchronous implementation, and a cache-less
@@ -168,12 +186,6 @@ namespace MobileGL::MG_State::GLState {
// glMaxShaderCompilerThreadsKHR(0) and a flag-off build both bypass sharing exactly
// as they bypass the pool, and their behaviour stays byte-identical to pre-stage-6.
const Bool runOnPool = m_preprocessCache && MG_Util::Async::AsyncShaderCompileActive();
// The compile-environment snapshot is taken HERE, on the GL thread, and handed to
// the job. Everything the pipeline needs to know about the device comes through it,
// never through pActiveBackendObject - that is what makes the body movable.
const SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env =
MG_Util::ShaderTranspiler::GetCurrentCompileEnv();
const Uint64 sourceHash = ShaderPreprocessCache::HashSource(*m_source);
// ---- P1 stage 6: adopt an equivalent compile instead of enqueueing a duplicate ----
@@ -8,9 +8,28 @@
#include <gtest/gtest.h>
#include <iostream>
#include <utility>
#include <vector>
#include <vulkan/vulkan.h>
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
TEST(DirectVulkanSanity, ProgramMovePreservesViewportIndexUsage) {
using VkProgramObject = MobileGL::MG_Backend::DirectVulkan::ProgramFactory::VkProgramObject;
VkProgramObject moveConstructedSource;
moveConstructedSource.writesViewportIndexBuiltin = true;
VkProgramObject moveConstructed(std::move(moveConstructedSource));
EXPECT_TRUE(moveConstructed.writesViewportIndexBuiltin);
EXPECT_FALSE(moveConstructedSource.writesViewportIndexBuiltin);
VkProgramObject moveAssignedSource;
moveAssignedSource.writesViewportIndexBuiltin = true;
VkProgramObject moveAssigned;
moveAssigned = std::move(moveAssignedSource);
EXPECT_TRUE(moveAssigned.writesViewportIndexBuiltin);
EXPECT_FALSE(moveAssignedSource.writesViewportIndexBuiltin);
}
TEST(DirectVulkanSanity, ExtensionEnumeration) {
uint32_t extensionCount = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
@@ -725,22 +725,57 @@ TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSu
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
};
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false);
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false);
EXPECT_FALSE(contains(without, MobileGL::E_GL_EXT_texture_filter_anisotropic));
EXPECT_FALSE(contains(without, MobileGL::E_GL_ARB_texture_filter_anisotropic));
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true);
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true, false, false);
EXPECT_TRUE(contains(with, MobileGL::E_GL_EXT_texture_filter_anisotropic));
EXPECT_TRUE(contains(with, MobileGL::E_GL_ARB_texture_filter_anisotropic));
// Same rule on the Vulkan backend, where the gate is the samplerAnisotropy device feature.
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false);
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false);
EXPECT_FALSE(contains(vkWithout, MobileGL::E_GL_EXT_texture_filter_anisotropic));
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true);
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true, false);
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_EXT_texture_filter_anisotropic));
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_texture_filter_anisotropic));
}
// Minecraft 26.3 checks ARB_draw_indirect before it considers the already-advertised
// ARB_multi_draw_indirect, then separately requires ARB_base_instance before enabling its terrain
// indirect path. Pin both strings and, just as importantly, the non-zero firstInstance gate.
TEST(IndirectDrawAdvertisement, MatchesEachBackendsUsableCommandSemantics) {
const auto contains = [](const MobileGL::Vector<MobileGL::GLExtension>& extensions,
MobileGL::GLExtension wanted) {
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
};
const auto esWithoutIndirect =
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false);
EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_draw_indirect));
EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_base_instance));
const auto esWithoutBaseInstance =
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, false);
EXPECT_TRUE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect));
EXPECT_FALSE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance));
const auto esWithBoth =
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, true);
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_draw_indirect));
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_base_instance));
const auto vkWithoutBaseInstance =
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false);
EXPECT_TRUE(contains(vkWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect));
EXPECT_FALSE(contains(vkWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance));
const auto vkWithBoth =
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, true);
EXPECT_TRUE(contains(vkWithBoth, MobileGL::E_GL_ARB_draw_indirect));
EXPECT_TRUE(contains(vkWithBoth, MobileGL::E_GL_ARB_base_instance));
}
TEST(TextureAnisotropyCapabilities, MaxAnisotropyIsQueriedOnlyWhenTheExtensionIsPresent) {
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
@@ -836,3 +871,63 @@ TEST(MultiDrawCapabilities, ExtensionWithoutResolvedPointerIsNotSupport) {
EXPECT_FALSE(caps.SupportsMultiDrawIndirect);
EXPECT_FALSE(caps.SupportsMultiDrawElementsBaseVertex);
}
TEST(DrawIndirectCapabilities, RequiresEs31AndBothCoreEntryPoints) {
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
auto funcs = MakeFakeGLESFunctions();
funcs.glDrawElementsIndirect = [](GLenum, GLenum, const void*) {};
MobileGL::MG_External::GLESCapabilities supportedCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(supportedCaps, funcs));
EXPECT_TRUE(supportedCaps.SupportsDrawIndirect);
// The same pointers on an ES 3.0 context are not core entry points and cannot back the
// desktop extension contract.
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.glesMinorVersion = 0;
MobileGL::MG_External::GLESCapabilities es30Caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es30Caps, funcs));
EXPECT_FALSE(es30Caps.SupportsDrawIndirect);
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
const auto missingElements = MakeFakeGLESFunctions();
MobileGL::MG_External::GLESCapabilities missingEntryPointCaps;
ASSERT_TRUE(
MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(missingEntryPointCaps, missingElements));
EXPECT_FALSE(missingEntryPointCaps.SupportsDrawIndirect);
}
TEST(BaseInstanceCapabilities, RequiresTheExtensionAndAllThreeEntryPoints) {
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
auto funcs = MakeFakeGLESFunctions();
funcs.glDrawArraysInstancedBaseInstanceEXT = [](GLenum, GLint, GLsizei, GLsizei, GLuint) {};
funcs.glDrawElementsInstancedBaseInstanceEXT =
[](GLenum, GLsizei, GLenum, const void*, GLsizei, GLuint) {};
funcs.glDrawElementsInstancedBaseVertexBaseInstanceEXT =
[](GLenum, GLsizei, GLenum, const void*, GLsizei, GLint, GLuint) {};
// Resolved stubs alone must never make the capability true.
MobileGL::MG_External::GLESCapabilities pointersOnlyCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(pointersOnlyCaps, funcs));
EXPECT_FALSE(pointersOnlyCaps.SupportsBaseInstance);
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.extensions.emplace_back("GL_EXT_base_instance");
MobileGL::MG_External::GLESCapabilities supportedCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(supportedCaps, funcs));
EXPECT_TRUE(supportedCaps.SupportsBaseInstance);
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.extensions.emplace_back("GL_EXT_base_instance");
funcs.glDrawElementsInstancedBaseInstanceEXT = nullptr;
MobileGL::MG_External::GLESCapabilities missingEntryPointCaps;
ASSERT_TRUE(
MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(missingEntryPointCaps, funcs));
EXPECT_FALSE(missingEntryPointCaps.SupportsBaseInstance);
}
+112
View File
@@ -16,6 +16,7 @@
#include <MG_State/GLState/Core.h>
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Impl/GetProcAddress.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
using namespace MobileGL;
@@ -599,6 +600,117 @@ TEST_F(BufferTest, ClearNamedBufferSubDataRepeatsPattern) {
EXPECT_EQ(actual, (Vector<Uint32>{0, pattern, pattern, pattern, 0}));
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(BufferTest, ClearBufferSubDataInitializesIrisStaticSsboRange) {
GLuint buffer = 0;
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
Vector<Uint8> initial(32, 0x7F);
MobileGL::MG_Impl::GLImpl::BufferData(
GL_SHADER_STORAGE_BUFFER, initial.size(), initial.data(), GL_STATIC_DRAW);
const GLbyte zero = 0;
const auto clear = reinterpret_cast<PFNGLCLEARBUFFERSUBDATAPROC>(
MobileGL::MG_Impl::GetProcAddress("glClearBufferSubData"));
ASSERT_NE(clear, nullptr);
clear(GL_SHADER_STORAGE_BUFFER, GL_R8, 4, 24, GL_RED, GL_BYTE, &zero);
Vector<Uint8> actual(initial.size());
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size());
EXPECT_EQ(actual, (Vector<Uint8>{0x7F, 0x7F, 0x7F, 0x7F,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0x7F, 0x7F, 0x7F, 0x7F}));
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
DrainPendingGlErrors();
}
TEST_F(BufferTest, ClearBufferSubDataInitializesCompleteIrisStaticSsbo) {
constexpr SizeT irisStaticSsboSize = 5'000'192;
GLuint buffer = 0;
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
Vector<Uint8> initial(irisStaticSsboSize, 0x7F);
MobileGL::MG_Impl::GLImpl::BufferData(
GL_SHADER_STORAGE_BUFFER, initial.size(), initial.data(), GL_STATIC_DRAW);
const GLbyte zero = 0;
MobileGL::MG_Impl::GLImpl::ClearBufferSubData(
GL_SHADER_STORAGE_BUFFER, GL_R8, 0, irisStaticSsboSize, GL_RED, GL_BYTE, &zero);
Vector<Uint8> actual(irisStaticSsboSize);
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size());
EXPECT_EQ(actual, Vector<Uint8>(irisStaticSsboSize, 0));
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
DrainPendingGlErrors();
}
TEST_F(BufferTest, ClearBufferDataConvertsOneClientPixelBeforeRepeatingIt) {
GLuint buffer = 0;
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer);
Vector<Uint32> initial(4, 0u);
MobileGL::MG_Impl::GLImpl::BufferData(GL_ARRAY_BUFFER, initial.size() * sizeof(Uint32), initial.data(),
GL_STATIC_DRAW);
const Uint8 value = 0xAB;
MobileGL::MG_Impl::GLImpl::ClearBufferData(
GL_ARRAY_BUFFER, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE, &value);
Vector<Uint32> actual(initial.size());
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size() * sizeof(Uint32));
EXPECT_EQ(actual, Vector<Uint32>(initial.size(), value));
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, 0);
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
DrainPendingGlErrors();
}
TEST_F(BufferTest, ClearBufferSubDataRejectsUnboundTarget) {
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
const GLbyte zero = 0;
MobileGL::MG_Impl::GLImpl::ClearBufferSubData(
GL_SHADER_STORAGE_BUFFER, GL_R8, 0, 1, GL_RED, GL_BYTE, &zero);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
TEST_F(BufferTest, ClearBufferDataRejectsInvalidPixelFormatTypePairs) {
GLuint buffer = 0;
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer);
const Vector<Uint8> initial{0x7F, 0x7F};
MobileGL::MG_Impl::GLImpl::BufferData(GL_ARRAY_BUFFER, initial.size(), initial.data(), GL_STATIC_DRAW);
const Uint16 packed = 0;
MobileGL::MG_Impl::GLImpl::ClearBufferData(
GL_ARRAY_BUFFER, GL_R16, GL_RED, GL_UNSIGNED_SHORT_5_6_5, &packed);
ExpectSingleGlError(GL_INVALID_VALUE);
MobileGL::MG_Impl::GLImpl::ClearBufferData(
GL_ARRAY_BUFFER, GL_R16, GL_RED, GL_UNSIGNED_SHORT_5_6_5, nullptr);
ExpectSingleGlError(GL_INVALID_VALUE);
Vector<Uint8> actual(initial.size());
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size());
EXPECT_EQ(actual, initial);
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, 0);
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
DrainPendingGlErrors();
}
// GL 4.6 core 6.5: glBufferSubData fails only when the written range OVERLAPS the mapped range.
+1
View File
@@ -78,6 +78,7 @@ add_subdirectory(Query)
add_subdirectory(Pipeline)
add_subdirectory(ShaderTranspiler)
add_subdirectory(Util)
add_subdirectory(SelfTest)
# The DirectGLES post-transpile ESSL passes are pure String -> String, so unlike the
# DirectVulkan suite below this one needs no device and always builds.
add_subdirectory(Backend/DirectGLES)
@@ -516,17 +516,17 @@ TEST_F(ParallelShaderCompileTest, MaxShaderCompilerThreadsIgnoresTheCurrentBudge
TEST_F(ParallelShaderCompileTest, BothBackendsAdvertiseTheExtensionIffAsyncIsEnabled) {
{
const AsyncModeScope async(true);
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false),
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false),
E_GL_KHR_parallel_shader_compile));
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false),
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
E_GL_KHR_parallel_shader_compile));
}
{
const AsyncModeScope async(false);
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false),
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false),
E_GL_KHR_parallel_shader_compile))
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false),
EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
E_GL_KHR_parallel_shader_compile))
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
}
+4 -1
View File
@@ -2692,11 +2692,13 @@ out vec4 fragColor;
float fma
(float a, float b, float c) { return a * b + c; }
float sinh(float x, float y) { return x * y; }
float length_squared(vec3 value) { return dot(value, value); }
float round(float x) { return floor(x + 0.5); }
float min3(float a, float b, float c) { return min(min(a, b), c); }
void main() {
fragColor = vec4(fma(0.1, 0.2, 0.3), sinh(0.4, 2.0), round(1.25), min3(0.1, 0.2, 0.3));
fragColor = vec4(fma(0.1, 0.2, 0.3), sinh(0.4, 2.0), round(1.25),
min3(0.1, 0.2, 0.3) + length_squared(vec3(0.1, 0.2, 0.3)));
}
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
@@ -2707,6 +2709,7 @@ void main() {
if (essl.find("fragColor") == String::npos) continue; // fragment module only
EXPECT_NE(essl.find("mg_fma("), String::npos) << essl;
EXPECT_NE(essl.find("mg_sinh("), String::npos) << essl;
EXPECT_NE(essl.find("mg_length_squared("), String::npos) << essl;
EXPECT_NE(essl.find("mg_round("), String::npos) << essl;
EXPECT_NE(essl.find("mg_min3("), String::npos) << essl;
EXPECT_EQ(essl.find("float fma("), String::npos) << essl;
+38 -200
View File
@@ -52,20 +52,24 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
OpEntryPoint Fragment %main "main" %outColor
OpExecutionMode %main OriginUpperLeft
OpName %globalSampler "sampler"
OpName %globalNew "new"
OpName %paramSampler "sampler"
OpName %paramNew "new"
OpName %main "main"
OpDecorate %outColor Location 0
%void = OpTypeVoid
%float = OpTypeFloat 32
%v4float = OpTypeVector %float 4
%mainFn = OpTypeFunction %void
%paramFn = OpTypeFunction %void %float
%paramFn = OpTypeFunction %void %float %float
%outV4Ptr = OpTypePointer Output %v4float
%privatePtr = OpTypePointer Private %float
%outColor = OpVariable %outV4Ptr Output
%globalSampler = OpVariable %privatePtr Private
%globalNew = OpVariable %privatePtr Private
%helper = OpFunction %void None %paramFn
%paramSampler = OpFunctionParameter %float
%paramNew = OpFunctionParameter %float
%helperBody = OpLabel
OpReturn
OpFunctionEnd
@@ -91,6 +95,7 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
EXPECT_NE(outputText.find("\"MGL_COMPAT_sampler\""), String::npos);
EXPECT_NE(outputText.find("\"MGL_COMPAT_new\""), String::npos);
SizeT exactSamplerNameCount = 0;
SizeT searchOffset = 0;
@@ -99,6 +104,14 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
searchOffset += std::strlen("\"sampler\"");
}
EXPECT_EQ(exactSamplerNameCount, 1u);
SizeT exactNewNameCount = 0;
searchOffset = 0;
while ((searchOffset = outputText.find("\"new\"", searchOffset)) != String::npos) {
++exactNewNameCount;
searchOffset += std::strlen("\"new\"");
}
EXPECT_EQ(exactNewNameCount, 1u);
}
TEST_F(ProgramUtilTest, UnformattedFloatStorageImagesKeepIntegerAtomicImagesTyped) {
@@ -2060,153 +2073,6 @@ void main() {
EXPECT_NE(source.find("layout(std140) uniform Blk"), String::npos);
}
namespace {
String MakeLinearSubgroupPrefixScanShader() {
return R"(#version 460 core
#extension GL_KHR_shader_subgroup_arithmetic : enable
layout(local_size_x = 1024) in;
shared float prefixSumCache[64];
layout(std430, binding = 0) writeonly buffer OutputBuffer {
float outputValues[];
};
void main() {
float importance = 1.0f;
float prefixSum = subgroupInclusiveAdd(importance);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) prefixSumCache[gl_SubgroupID] = prefixSum;
barrier();
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint i = 0; i < loopLength; i++) {
if ((gl_SubgroupID & (1u << i)) > 0u) {
prefixSum += prefixSumCache[(gl_SubgroupID >> i << i) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) prefixSumCache[gl_SubgroupID] = prefixSum;
}
barrier();
}
if (gl_LocalInvocationID.x == uint(1024 - 1)) prefixSumCache[0] = prefixSum;
barrier();
float sum = prefixSumCache[0];
float warp = (prefixSum - importance) / sum - float(gl_LocalInvocationID.x + 1u) / float(1024);
outputValues[gl_GlobalInvocationID.x] = warp;
}
)";
}
} // namespace
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanUsesSharedMemoryAndProducesValidSpirv) {
using namespace MG_Util::ShaderTranspiler;
String source = MakeLinearSubgroupPrefixScanShader();
ASSERT_TRUE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
EXPECT_NE(source.find("shared float prefixSumCache[1024]"), String::npos) << source;
EXPECT_NE(source.find("mglVirtualSubgroupInvocation"), String::npos) << source;
EXPECT_NE(source.find("for (uint mglPrefixLane"), String::npos) << source;
EXPECT_EQ(source.find("subgroupInclusiveAdd"), String::npos) << source;
EXPECT_EQ(source.find("gl_Subgroup"), String::npos) << source;
const String onceRewritten = source;
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
EXPECT_EQ(source, onceRewritten);
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
ASSERT_TRUE(shaderResult) << shaderResult.error().log << "\nsource:\n" << source;
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
ASSERT_TRUE(programResult) << programResult.error().log;
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
ASSERT_TRUE(binaryResult) << binaryResult.error().log;
ASSERT_EQ(binaryResult->size(), 1u);
String validationDiagnostics;
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
tools.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) {
validationDiagnostics += message;
validationDiagnostics += '\n';
});
EXPECT_TRUE(tools.Validate(binaryResult->front())) << validationDiagnostics;
String spirvText;
ASSERT_TRUE(tools.Disassemble(binaryResult->front(), &spirvText));
EXPECT_EQ(spirvText.find("OpGroupNonUniform"), String::npos) << spirvText;
}
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanRejectsOtherStagesAndSubgroupWidths) {
using namespace MG_Util::ShaderTranspiler;
const String original = MakeLinearSubgroupPrefixScanShader();
for (const auto& [stage, subgroupSize] :
{std::pair{ShaderStage::Compute, Uint32{32}}, std::pair{ShaderStage::Fragment, Uint32{64}},
std::pair{ShaderStage::Compute, Uint32{96}}}) {
String source = original;
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(stage, subgroupSize, source));
EXPECT_EQ(source, original);
}
}
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanRejectsPartialOrUnsafeTemplateMatches) {
using namespace MG_Util::ShaderTranspiler;
const auto expectUnchanged = [](String source) {
const String original = source;
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
EXPECT_EQ(source, original);
};
String wrongLocalSize = MakeLinearSubgroupPrefixScanShader();
wrongLocalSize.replace(wrongLocalSize.find("local_size_x = 1024"), std::strlen("local_size_x = 1024"),
"local_size_x = 512");
expectUnchanged(std::move(wrongLocalSize));
String cacheHasAnotherUse = MakeLinearSubgroupPrefixScanShader();
cacheHasAnotherUse.insert(cacheHasAnotherUse.find("float importance"), "prefixSumCache[0] = 0.0f;\n ");
expectUnchanged(std::move(cacheHasAnotherUse));
String extraSubgroupBuiltin = MakeLinearSubgroupPrefixScanShader();
extraSubgroupBuiltin.insert(extraSubgroupBuiltin.find("float importance"),
"uvec4 extraMask = gl_SubgroupEqMask;\n ");
expectUnchanged(std::move(extraSubgroupBuiltin));
String alteredBarrier = MakeLinearSubgroupPrefixScanShader();
alteredBarrier.replace(alteredBarrier.find("barrier();"), std::strlen("barrier();"), "memoryBarrierShared();");
expectUnchanged(std::move(alteredBarrier));
String nestedScan = MakeLinearSubgroupPrefixScanShader();
nestedScan.insert(nestedScan.find("float prefixSum ="), "if (importance > 0.0f) {\n ");
const SizeT consumerEnd = nestedScan.find(';', nestedScan.find("float warp ="));
ASSERT_NE(consumerEnd, String::npos);
nestedScan.insert(consumerEnd + 1, "\n }");
expectUnchanged(std::move(nestedScan));
// ARB/NV spellings of lane-width-sensitive builtins must block the rewrite exactly
// like their KHR counterparts.
String arbSubgroupBuiltin = MakeLinearSubgroupPrefixScanShader();
arbSubgroupBuiltin.insert(arbSubgroupBuiltin.find("float importance"),
"uint arbLane = gl_SubGroupInvocationARB;\n ");
expectUnchanged(std::move(arbSubgroupBuiltin));
String arbBallotCall = MakeLinearSubgroupPrefixScanShader();
arbBallotCall.insert(arbBallotCall.find("float importance"),
"uint64_t arbMask = ballotARB(true);\n ");
expectUnchanged(std::move(arbBallotCall));
String nvWarpBuiltin = MakeLinearSubgroupPrefixScanShader();
nvWarpBuiltin.insert(nvWarpBuiltin.find("float importance"),
"uint warpSize = gl_WarpSizeNV;\n ");
expectUnchanged(std::move(nvWarpBuiltin));
String nvShuffleCall = MakeLinearSubgroupPrefixScanShader();
nvShuffleCall.insert(nvShuffleCall.find("float importance"),
"float other = shuffleNV(1.0f, 0u, 32u);\n ");
expectUnchanged(std::move(nvShuffleCall));
}
// The LEXICAL half must fire at the source level (before the parse) for the
// preempt-list names - the end-to-end ESSL tests cannot tell which half did the
// rename, and for these names the parse would fail without the source rewrite.
@@ -2435,9 +2301,6 @@ TEST_F(ProgramUtilTest, CompileEnvFingerprintTracksEveryInput) {
otherExtensions.advertisedExtensions.push_back(MobileGL::E_GL_ARB_gpu_shader_int64);
EXPECT_NE(ComputeCompileEnvFingerprint(otherExtensions), baseline);
CompileEnv otherQuirk = base;
otherQuirk.subgroupPrefixScanQuirk = MobileGL::MG_Config::QuirkOverride::ForceOn;
EXPECT_NE(ComputeCompileEnvFingerprint(otherQuirk), baseline);
}
// The no-backend fallback must stay exactly what the pipeline used to do inline:
@@ -2836,16 +2699,6 @@ vec4 helperTint() { return vec4(1.0); }
return binaryResult->front();
}
struct SpirvValidationScope {
bool previous;
explicit SpirvValidationScope(bool enabled)
: previous(MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) {
MG_Util::ShaderTranspiler::ShaderCompiler::SetSpirvValidationEnabled(enabled);
}
~SpirvValidationScope() {
MG_Util::ShaderTranspiler::ShaderCompiler::SetSpirvValidationEnabled(previous);
}
};
} // namespace
TEST_F(ProgramUtilTest, DeadPrivateChainVertexInputIsEliminatedFromOptimizedBinary) {
@@ -2867,7 +2720,7 @@ TEST_F(ProgramUtilTest, DeadPrivateChainVertexInputIsEliminatedFromOptimizedBina
<< "entry-point-with-calls shape it exists for";
Vector<Uint32> optimized;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, true));
const SpirvVariableCensus after = TakeVariableCensus(optimized);
EXPECT_EQ(after.inputCount, 1u)
@@ -2905,7 +2758,7 @@ void main() {
ASSERT_GE(before.outputCount, 3u);
Vector<Uint32> optimized;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, true));
EXPECT_EQ(TakeVariableCensus(optimized).outputCount, before.outputCount)
<< "a declared-but-unwritten output was deleted; a fragment stage reading it now "
<< "fails to link (ES) or breaks the Vulkan stage interface";
@@ -2964,17 +2817,15 @@ void main() {
// succeeds - fail-open call sites downstream must not see a different world),
// and the failure latch is the signal. This is the catch that took a device
// bisect to find when the validator was off everywhere.
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, true));
EXPECT_GT(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "an invalid optimized module must bump the validation-failure latch";
}
{
// The shipping configuration: same result, no validation, latch untouched.
SpirvValidationScope validationOff(false);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, false));
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
}
}
@@ -3044,10 +2895,9 @@ void main() {
ASSERT_FALSE(raw.empty());
ASSERT_GE(CountRectImageTypes(raw), 1u) << "glslang no longer emits Dim::Rect for sampler2DRect";
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> optimized;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, true));
EXPECT_EQ(CountRectImageTypes(optimized), 0u);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "a rectangle module must leave the chain valid, not latched as a failure";
@@ -3072,10 +2922,9 @@ void main() {
ASSERT_TRUE(AnyLocationOnUniformStorage(raw))
<< "glslang no longer keeps the explicit uniform location; the strip pass may be obsolete";
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> optimized;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, true));
EXPECT_FALSE(AnyLocationOnUniformStorage(optimized));
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "the stripped module must validate clean";
@@ -3172,11 +3021,10 @@ void main() {
<< "the fixture must reproduce the defect before the fix is asked to remove it:\n"
<< DisassembleSpirv(raw);
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> legalized;
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized));
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized, true));
ASSERT_FALSE(legalized.empty());
const String disassembly = DisassembleSpirv(legalized);
@@ -3213,11 +3061,10 @@ void main() {
ASSERT_TRUE(LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(raw))
<< DisassembleSpirv(raw);
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> legalized;
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized));
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized, true));
ASSERT_FALSE(legalized.empty());
const String disassembly = DisassembleSpirv(legalized);
@@ -3257,11 +3104,10 @@ void main() {
ASSERT_FALSE(raw.empty());
ASSERT_TRUE(LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(raw));
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> legalized;
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized));
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized, true));
ASSERT_FALSE(legalized.empty());
const String disassembly = DisassembleSpirv(legalized);
@@ -3300,7 +3146,7 @@ void main() {
ASSERT_FALSE(LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(raw));
Vector<Uint32> legalized;
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized));
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized, true));
EXPECT_EQ(legalized, raw) << "the module must not be rewritten - not even re-serialized - when "
"nothing indexes a fragment output dynamically";
}
@@ -3550,11 +3396,10 @@ TEST_F(ProgramUtilTest, Lower1DArrayImagesRewritesTheTypeAndWidensTheCoordinate)
ASSERT_EQ(Count1DArrayStorageImageTypes(spirv), 1u)
<< "the shared chain must leave the 1D-array image for this pass to handle";
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
ASSERT_FALSE(lowered.empty());
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 0u)
@@ -3602,11 +3447,10 @@ void main() { ssb.sum = imageLoad(i0, ivec2(2, 3)).r + imageLoad(i1, ivec3(1, 1,
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
ASSERT_EQ(Count1DArrayStorageImageTypes(spirv), 1u);
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
ASSERT_FALSE(lowered.empty());
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 0u) << DisassembleSpirv(lowered);
@@ -3636,7 +3480,7 @@ void main() { ssb.sum = imageLoad(i0, 2).r; }
ASSERT_FALSE(spirv.empty());
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
EXPECT_EQ(lowered, spirv) << "a non-arrayed 1D storage image must pass through byte for byte";
const String essl = DecompileToEssl(lowered);
@@ -3660,7 +3504,7 @@ void main() { fragColor = texture(uTex, vUv); }
ASSERT_FALSE(spirv.empty());
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
EXPECT_EQ(lowered, spirv) << "a sampled 1D-array image must pass through byte for byte";
}
@@ -3684,7 +3528,7 @@ void main() { ssb.sum = uint(imageSize(i0).x) + imageLoad(i0, ivec2(0, 0)).r; }
<< "the fixture must contain the shape the pass declines";
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
EXPECT_EQ(lowered, spirv) << "a declined module must be handed back untouched, not partly rewritten";
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 1u)
<< "declining means the 1D-array type is still there for the driver to reject";
@@ -3735,11 +3579,10 @@ void main() { imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(15u,
// Precondition: SPIRV-Cross prints no format for it, which is the ESSL the driver refuses.
EXPECT_EQ(DecompileToEssl(spirv).find("r32ui"), String::npos);
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked, true));
ASSERT_FALSE(baked.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked)) << DisassembleSpirv(baked);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
@@ -3800,7 +3643,7 @@ void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
Vector<Uint32> baked;
// Even asked to, with a format of the right component class.
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked, true));
EXPECT_EQ(baked, spirv) << "a module with nothing format-less must pass through byte for byte";
EXPECT_NE(DecompileToEssl(baked).find("rgba32ui"), String::npos);
}
@@ -3822,11 +3665,10 @@ void main() { writeIt(uni_image); }
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked, true));
EXPECT_EQ(baked, spirv) << "a shape the retype cannot follow must leave the module untouched, "
"not partly rewritten:\n"
<< DisassembleSpirv(baked);
@@ -3848,18 +3690,17 @@ void main() { imageStore(uni_image, ivec2(0), vec4(1.0)); }
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked, true));
EXPECT_EQ(baked, spirv) << "a declined module must be handed back untouched, not partly rewritten";
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
// ...and the same image with a float bind format is baked, so the decline above is about the
// class and not about the pass refusing float images.
Vector<Uint32> bakedFloat;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32f}}, bakedFloat));
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32f}}, bakedFloat, true));
EXPECT_NE(DecompileToEssl(bakedFloat).find("r32f"), String::npos) << DisassembleSpirv(bakedFloat);
}
@@ -3883,12 +3724,11 @@ void main() {
ASSERT_EQ(CountSpirvOpcode(DisassembleSpirv(spirv), "OpTypeImage"), 1u)
<< "the fixture must have the two images sharing one type:\n" << DisassembleSpirv(spirv);
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(
spirv, {{"imgA", kGlR32ui}, {"imgB", kGlRgba32ui}}, baked));
spirv, {{"imgA", kGlR32ui}, {"imgB", kGlRgba32ui}}, baked, true));
ASSERT_FALSE(baked.empty());
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "splitting the shared type must not leave a dangling or duplicate declaration:\n"
@@ -3921,11 +3761,10 @@ void main() {
ASSERT_EQ(CountSpirvOpcode(DisassembleSpirv(spirv), "OpTypeImage"), 2u)
<< "the fixture needs one Unknown-format and one r32ui image type:\n" << DisassembleSpirv(spirv);
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"formatless", kGlR32ui}}, baked));
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"formatless", kGlR32ui}}, baked, true));
ASSERT_FALSE(baked.empty());
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "the baked image collided with the module's own r32ui image and left a duplicate type:\n"
@@ -3950,11 +3789,10 @@ void main() {
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"imgs", kGlR32ui}}, baked));
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"imgs", kGlR32ui}}, baked, true));
ASSERT_FALSE(baked.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked)) << DisassembleSpirv(baked);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
@@ -3980,7 +3818,7 @@ void main() { fragColor = texture(uni_sampler, vUv); }
<< "a sampled image must not read as a format-less STORAGE image:\n" << DisassembleSpirv(spirv);
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_sampler", kGlR32ui}}, baked));
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_sampler", kGlR32ui}}, baked, true));
EXPECT_EQ(baked, spirv) << "a sampled image must pass through byte for byte";
}
+81
View File
@@ -31,6 +31,7 @@
#include <MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h>
#include <MG_Util/Math/HalfFloat.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
#include <MG_Util/Debug/Log.h>
#include <MG_Util/Types.h>
@@ -710,6 +711,37 @@ TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) {
EXPECT_TRUE(backend.GetDynamicParameters().SubgroupQuadOperationsInAllStages);
}
TEST(DirectVulkanSanity, CapabilityRefreshInvalidatesTheCachedCompileEnvironment) {
using namespace MobileGL;
auto previousContext = Move(MG_State::pGLContext);
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
auto backend = MakeUnique<MG_Backend::DirectVulkan::BackendObject_DirectVulkan>();
auto* backendPtr = backend.get();
MG_Backend::pActiveBackendObject = Move(backend);
const auto before = MG_State::pGLContext->GetCompileEnv();
EXPECT_EQ(before->params.SubgroupSize, 0u);
MG_External::VulkanCapabilities caps;
caps.SupportsShaderSubgroup = true;
caps.SubgroupSize = 8;
caps.SubgroupSupportedStages = VK_SHADER_STAGE_COMPUTE_BIT;
caps.SubgroupSupportedOperations = VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_ARITHMETIC_BIT;
backendPtr->ApplyVulkanCapabilitiesForTesting(caps);
const auto after = MG_State::pGLContext->GetCompileEnv();
EXPECT_NE(after.get(), before.get());
EXPECT_NE(after->fingerprint, before->fingerprint);
EXPECT_EQ(after->backend, BackendType::DirectVulkan);
EXPECT_EQ(after->params.SubgroupSize, 8u);
MG_Backend::pActiveBackendObject = Move(previousBackend);
MG_State::pGLContext = Move(previousContext);
}
TEST(DirectVulkanSanity, KeepsOptionalGpuShaderInt64BranchForVoxyQuadDecode) {
using namespace MobileGL;
@@ -936,6 +968,45 @@ TEST(DirectVulkanSanity, ReadbackUsesTheSourceFormatTexelSize) {
EXPECT_EQ(VulkanRenderer::GetReadbackTexelSize(VK_FORMAT_R32G32B32A32_SFLOAT), 16u);
}
TEST(DirectVulkanSanity, DefaultFramebufferQuarterTurnReadbackMapsRectAndPixels) {
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
using MobileGL::Uint8;
VkOffset2D offset{};
VkExtent2D copyExtent{};
ASSERT_TRUE(VulkanRenderer::MapDefaultFramebufferReadbackRect(
1, 0, 2, 1, VkExtent2D{2, 3}, VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR,
&offset, &copyExtent));
EXPECT_EQ(offset.x, 0);
EXPECT_EQ(offset.y, 1);
EXPECT_EQ(copyExtent.width, 1u);
EXPECT_EQ(copyExtent.height, 2u);
ASSERT_TRUE(VulkanRenderer::MapDefaultFramebufferReadbackRect(
1, 0, 2, 1, VkExtent2D{2, 3}, VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR,
&offset, &copyExtent));
EXPECT_EQ(offset.x, 1);
EXPECT_EQ(offset.y, 0);
EXPECT_EQ(copyExtent.width, 1u);
EXPECT_EQ(copyExtent.height, 2u);
// Logical GL rows, bottom to top, are abc / def. The display-oriented swapchain blocks are
// transposed in opposite directions for 90 and 270 degrees.
const Uint8 raw90[] = {'a', 'd', 'b', 'e', 'c', 'f'};
const Uint8 raw270[] = {'f', 'c', 'e', 'b', 'd', 'a'};
const Uint8 expected[] = {'a', 'b', 'c', 'd', 'e', 'f'};
Uint8 result[sizeof(expected)]{};
ASSERT_TRUE(VulkanRenderer::RemapDefaultFramebufferReadback(
raw90, 3, 2, VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR, 1, result));
EXPECT_TRUE(std::equal(std::begin(expected), std::end(expected), std::begin(result)));
std::fill(std::begin(result), std::end(result), 0);
ASSERT_TRUE(VulkanRenderer::RemapDefaultFramebufferReadback(
raw270, 3, 2, VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR, 1, result));
EXPECT_TRUE(std::equal(std::begin(expected), std::end(expected), std::begin(result)));
}
TEST(DirectVulkanSanity, ReadbackConvertsRgba8AndRgba16fPixels) {
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
using MobileGL::MG_Util::EncodeFloatToHalfBits;
@@ -2316,3 +2387,13 @@ TEST(DirectGLESTextureSync, UnitMemoRefusesToDriveATwinFromAnotherTexture) {
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
}
TEST(DirectVulkanSanity, GraphicsSamplerFeedbackOnlyAliasesWritableOverlappingMip) {
using MobileGL::MG_Backend::DirectVulkan::UniformManager;
EXPECT_TRUE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 2, GL_WRITE_ONLY));
EXPECT_TRUE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 3, GL_READ_WRITE));
EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 2, GL_READ_ONLY));
EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 0, GL_WRITE_ONLY));
EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 4, GL_WRITE_ONLY));
}
+19
View File
@@ -0,0 +1,19 @@
# MobileGL - MobileGL/MG_Test/SelfTest/CMakeLists.txt
add_executable(
DriverPostIterationRPWitnessTest
DriverPostIterationRPWitnessTest.cpp
)
target_include_directories(DriverPostIterationRPWitnessTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(DriverPostIterationRPWitnessTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
include(GoogleTest)
gtest_discover_tests(DriverPostIterationRPWitnessTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -0,0 +1,188 @@
// MobileGL - MobileGL/MG_Test/SelfTest/DriverPostIterationRPWitnessTest.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#include <string>
#include "MG_Util/SelfTest/DriverPostIterationRPWitness.h"
namespace MobileGL::MG_Util::SelfTest {
namespace {
IterationRPWitnessOutput MakeValidWitness(std::uint32_t numSubgroups) {
IterationRPWitnessOutput output{};
output.magic = kIterationRPWitnessMagic;
output.numSubgroups = numSubgroups;
output.loopLength = ComputeIterationRPWitnessLoopLength(numSubgroups);
output.seenSubgroupMask =
numSubgroups == kIterationRPWitnessMaxSubgroups ? 0xffffffffu : (1u << numSubgroups) - 1u;
// Valid test layouts use equal contiguous groups of the indexed
// 1..512 input. The compact witness only needs their independent sums.
const std::uint32_t subgroupSize = kIterationRPWitnessInvocationCount / numSubgroups;
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
const std::uint32_t first = subgroup * subgroupSize + 1u;
const std::uint32_t last = first + subgroupSize - 1u;
output.lastLaneWriterCount[subgroup] = 1u;
output.indexedInputTotal[subgroup] = subgroupSize * (first + last) / 2u;
output.rawPrefix[subgroup] = {static_cast<float>(output.indexedInputTotal[subgroup]), 0.0f};
}
output.owner511 = {subgroupSize, numSubgroups, numSubgroups - 1u, subgroupSize - 1u};
auto cache = output.rawPrefix;
for (std::uint32_t scanStage = 0u; scanStage < output.loopLength; ++scanStage) {
auto cacheAfterStage = cache;
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
if ((subgroup & (1u << scanStage)) == 0u) continue;
const std::uint32_t sourceCacheIndex = (subgroup >> scanStage << scanStage) - 1u;
cacheAfterStage[subgroup].x += cache[sourceCacheIndex].x;
cacheAfterStage[subgroup].y += cache[sourceCacheIndex].y;
}
cache = cacheAfterStage;
output.scanCache[scanStage] = cache;
}
output.finalAverage = {256.5f, 0.0f};
return output;
}
IterationRPWitnessLimits MakeSufficientLimits() {
IterationRPWitnessLimits limits;
limits.computeStageSupported = true;
limits.basicSubgroupSupported = true;
limits.arithmeticSubgroupSupported = true;
limits.subgroupSize = 32u;
limits.maxComputeWorkGroupInvocations = kIterationRPWitnessInvocationCount;
limits.maxComputeWorkGroupSize = {32u, 16u, 1u};
limits.maxComputeSharedMemorySize = kIterationRPWitnessSharedMemoryBytes;
limits.maxPerStageDescriptorStorageBuffers = 1u;
limits.maxDescriptorSetStorageBuffers = 1u;
limits.maxBoundDescriptorSets = 1u;
limits.maxStorageBufferRange = sizeof(IterationRPWitnessOutput);
return limits;
}
} // namespace
TEST(DriverPostIterationRPWitnessTest, ValidTwoSubgroupWitness) {
const IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(MakeValidWitness(2u));
ASSERT_TRUE(validation.ok) << validation.detail;
EXPECT_EQ(validation.detail, "N=2, owner511=id1/lane255, 2 scan stages, average=(256.5,0)");
}
TEST(DriverPostIterationRPWitnessTest, ValidThirtyTwoSubgroupWitness) {
const IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(MakeValidWitness(32u));
ASSERT_TRUE(validation.ok) << validation.detail;
EXPECT_EQ(validation.detail, "N=32, owner511=id31/lane15, 6 scan stages, average=(256.5,0)");
}
TEST(DriverPostIterationRPWitnessTest, RejectsNonuniformNumSubgroups) {
IterationRPWitnessOutput output = MakeValidWitness(16u);
output.topologyFlags |= IterationRPWitnessNonuniformNumSubgroups;
const IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(output);
EXPECT_FALSE(validation.ok);
EXPECT_EQ(validation.failure, IterationRPWitnessValidationFailure::Topology);
EXPECT_NE(validation.detail.find("gl_NumSubgroups differed"), std::string::npos);
}
TEST(DriverPostIterationRPWitnessTest, RejectsMissingAndOutOfRangeSubgroupIds) {
IterationRPWitnessOutput missing = MakeValidWitness(16u);
missing.seenSubgroupMask &= ~(1u << 7u);
IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(missing);
EXPECT_FALSE(validation.ok);
EXPECT_NE(validation.detail.find("seen subgroup-ID mask"), std::string::npos);
IterationRPWitnessOutput outOfRange = MakeValidWitness(16u);
outOfRange.topologyFlags |= IterationRPWitnessInvalidSubgroupId;
validation = ValidateIterationRPWitness(outOfRange);
EXPECT_FALSE(validation.ok);
EXPECT_NE(validation.detail.find("invalid gl_SubgroupID"), std::string::npos);
}
TEST(DriverPostIterationRPWitnessTest, RejectsInvalidMultipleAndMissingLastLaneWriters) {
IterationRPWitnessOutput invalidLane = MakeValidWitness(16u);
invalidLane.topologyFlags |= IterationRPWitnessInvalidSubgroupLane;
IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(invalidLane);
EXPECT_FALSE(validation.ok);
EXPECT_NE(validation.detail.find("invalid subgroup lane"), std::string::npos);
IterationRPWitnessOutput multiple = MakeValidWitness(16u);
multiple.lastLaneWriterCount[4] = 2u;
validation = ValidateIterationRPWitness(multiple);
EXPECT_FALSE(validation.ok);
EXPECT_NE(validation.detail.find("subgroup 4 has 2 source last-lane writers"), std::string::npos);
IterationRPWitnessOutput missing = MakeValidWitness(16u);
missing.lastLaneWriterCount[6] = 0u;
validation = ValidateIterationRPWitness(missing);
EXPECT_FALSE(validation.ok);
EXPECT_NE(validation.detail.find("subgroup 6 has 0 source last-lane writers"), std::string::npos);
}
TEST(DriverPostIterationRPWitnessTest, ReportsEarliestCorruptSourceScanStage) {
IterationRPWitnessOutput output = MakeValidWitness(32u);
output.scanCache[0][1].x += 1.0f;
output.scanCache[3][5].x += 1.0f;
IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(output);
EXPECT_FALSE(validation.ok);
EXPECT_EQ(validation.failure, IterationRPWitnessValidationFailure::SourceScan);
EXPECT_EQ(validation.scanStage, 0u);
EXPECT_NE(validation.detail.find("source scan stage 0, subgroup 1"), std::string::npos);
output = MakeValidWitness(32u);
output.scanCache[3][5].x += 1.0f;
validation = ValidateIterationRPWitness(output);
EXPECT_FALSE(validation.ok);
EXPECT_EQ(validation.failure, IterationRPWitnessValidationFailure::SourceScan);
EXPECT_EQ(validation.scanStage, 3u);
EXPECT_NE(validation.detail.find("source scan stage 3, subgroup 5"), std::string::npos);
}
TEST(DriverPostIterationRPWitnessTest, RejectsOwner511OutsideHighestFinalLane) {
IterationRPWitnessOutput output = MakeValidWitness(16u);
output.owner511.z = 14u;
const IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(output);
EXPECT_FALSE(validation.ok);
EXPECT_EQ(validation.failure, IterationRPWitnessValidationFailure::FinalOwner);
EXPECT_NE(validation.detail.find("not in the highest subgroup"), std::string::npos);
}
TEST(DriverPostIterationRPWitnessTest, RejectsIncorrectVectorFinalAverage) {
IterationRPWitnessOutput output = MakeValidWitness(16u);
output.finalAverage.y = 1.0f;
const IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(output);
EXPECT_FALSE(validation.ok);
EXPECT_EQ(validation.failure, IterationRPWitnessValidationFailure::FinalAverage);
EXPECT_NE(validation.detail.find("final average"), std::string::npos);
}
TEST(DriverPostIterationRPWitnessTest, MissingNativeFeatureIsTheOnlySkipCondition) {
for (const auto toggleMissingFeature : {0u, 1u, 2u}) {
IterationRPWitnessLimits limits = MakeSufficientLimits();
if (toggleMissingFeature == 0u) limits.computeStageSupported = false;
if (toggleMissingFeature == 1u) limits.basicSubgroupSupported = false;
if (toggleMissingFeature == 2u) limits.arithmeticSubgroupSupported = false;
const IterationRPWitnessEligibilityResult eligibility = EvaluateIterationRPWitnessEligibility(limits);
EXPECT_EQ(eligibility.eligibility, IterationRPWitnessEligibility::SkipUnsupportedNativeFeatureSet)
<< eligibility.detail;
}
IterationRPWitnessLimits zeroSubgroupSize = MakeSufficientLimits();
zeroSubgroupSize.subgroupSize = 0u;
IterationRPWitnessEligibilityResult eligibility = EvaluateIterationRPWitnessEligibility(zeroSubgroupSize);
EXPECT_EQ(eligibility.eligibility, IterationRPWitnessEligibility::FailInadequateLimits) << eligibility.detail;
IterationRPWitnessLimits limits = MakeSufficientLimits();
limits.maxComputeWorkGroupInvocations = 511u;
eligibility = EvaluateIterationRPWitnessEligibility(limits);
EXPECT_EQ(eligibility.eligibility, IterationRPWitnessEligibility::FailInadequateLimits) << eligibility.detail;
limits = MakeSufficientLimits();
limits.maxStorageBufferRange = sizeof(IterationRPWitnessOutput) - 1u;
eligibility = EvaluateIterationRPWitnessEligibility(limits);
EXPECT_EQ(eligibility.eligibility, IterationRPWitnessEligibility::FailInadequateLimits) << eligibility.detail;
}
} // namespace MobileGL::MG_Util::SelfTest
@@ -3,6 +3,10 @@ cmake_minimum_required(VERSION 3.14)
add_executable(
SpirvPassTest
SpirvPassTest.cpp
DeriveNumSubgroupsTest.cpp
FixIterationRPBarrierTest.cpp
FixIterationRPSubgroupScratchTest.cpp
EmulateSubgroupsTest.cpp
DemoteFloat64Test.cpp
FlattenXfbInterfaceBlocksTest.cpp
)
@@ -154,7 +154,6 @@ class DemoteFloat64Test : public ::testing::Test {
protected:
void SetUp() override {
MobileGL::Initialize();
ShaderCompiler::SetSpirvValidationEnabled(true);
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
}
@@ -175,7 +174,7 @@ TEST_F(DemoteFloat64Test, DemotesEveryWidthAndDropsTheCapability) {
ASSERT_TRUE(DeclaresFloat64Capability(input));
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
EXPECT_EQ(CountFloatTypesOfWidth(output, 64), 0u) << Disassemble(output);
// And exactly one 32-bit float type survives: the merge has to happen, or spirv-val rejects
@@ -210,7 +209,7 @@ void main() {
<< Disassemble(input);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
// std140 for the demoted members: float at 4, vec2 at 8, vec3 at 16 (aligned like a vec4),
// vec4 at 32, mat4 at 48 with a 16-byte column stride, the array at 112 with the std140
@@ -241,7 +240,7 @@ void main() {
EXPECT_EQ(CollectOffsetsOf(input, "Ssbo"), (Vector<Uint32>{0, 32, 64})) << Disassemble(input);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
// std430, so the array packs at its element size rather than being rounded to 16: float at 0,
// vec4 at 16, float[4] at 32 with a 4-byte stride. A storage block must NOT come out std140,
@@ -273,7 +272,7 @@ void main() {
ASSERT_FALSE(before.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
// Only the block that actually narrowed is re-laid-out. Touching the other one would be
// churn at best, and a disagreement with glslang's own layout at worst.
@@ -287,7 +286,7 @@ TEST_F(DemoteFloat64Test, FoldsTheConversionsThatBecameIdentities) {
ASSERT_GT(CountFConverts(input), 0u) << "the fixture no longer converts between the two widths";
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
// SPIR-V requires the two component widths of an OpFConvert to differ, so every one of them
// has to be gone: both sides are 32 bits now.
@@ -307,7 +306,7 @@ void main() {
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
// A 64-bit literal is two words wide and a 32-bit one is a single word, so a constant left
// unconverted is not merely imprecise - it is an unparseable instruction. Disassembling both
@@ -326,7 +325,7 @@ void main() { gl_Position = inPos; }
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
// The pass reports SuccessWithoutChange here, and SPIRV-Tools asserts (in assert-enabled
// builds) that such a run round-trips byte-identically.
EXPECT_EQ(output, input);
@@ -351,7 +350,7 @@ void main() {
ASSERT_EQ(CountFloatTypesOfWidth(input, 64), 1u) << Disassemble(input);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
EXPECT_EQ(output, input) << Disassemble(output);
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64(output));
}
@@ -362,7 +361,7 @@ TEST_F(DemoteFloat64Test, ModuleDeclaresFloat64AnswersBothWays) {
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64(wide));
Vector<Uint32> demoted;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(wide, demoted));
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(wide, demoted, true));
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64(demoted));
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64({}));
@@ -375,7 +374,7 @@ TEST_F(DemoteFloat64Test, TheSharedChainDemotesToo) {
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true));
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64(output)) << Disassemble(output);
}
@@ -459,7 +458,7 @@ TEST_P(DemoteFloat64EsslTest, TheDemotedModuleCanBeEmittedAsEssl) {
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true));
SpvcSession session(output, SessionUsageBit::Transpile);
spvc_compiler_options options;
@@ -482,7 +481,7 @@ TEST_P(DemoteFloat64EsslTest, TheDemotedModuleCanBeEmittedAsEssl) {
TEST_F(DemoteFloat64Test, RejectsGarbageInput) {
const Vector<Uint32> notSpirv{0xdeadbeefu, 0u, 0u, 0u, 0u};
Vector<Uint32> output;
EXPECT_FALSE(ShaderCompiler::DemoteFloat64ToFloat32(notSpirv, output));
EXPECT_FALSE(ShaderCompiler::DemoteFloat64ToFloat32(notSpirv, output, true));
}
// EliminateFloatEqualsZeroPass turns a comparison against 0.0 into an epsilon test, a
@@ -502,7 +501,7 @@ namespace {
EXPECT_FALSE(input.empty());
if (input.empty()) return false;
Vector<Uint32> output;
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true));
return Disassemble(output).find("FAbs") != String::npos;
}
@@ -0,0 +1,147 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/DeriveNumSubgroupsTest.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#define SPV_ENABLE_UTILITY_CODE
#include "glslang/SPIRV/spirv.hpp11"
#undef SPV_ENABLE_UTILITY_CODE
#include "Includes.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
constexpr SizeT kSpirvHeaderWordCount = 5u;
template <typename Visitor>
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
const Uint32 wordCount = spirv[offset] >> 16u;
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
offset += wordCount;
}
}
Vector<Uint32> CompileCompute(const String& source) {
using namespace MobileGL::MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
Uint32 FindBuiltinTarget(const Vector<Uint32>& spirv, spv::BuiltIn builtin) {
Uint32 target = 0u;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == spv::Op::OpDecorate && wordCount >= 4u &&
static_cast<spv::Decoration>(words[2]) == spv::Decoration::BuiltIn &&
static_cast<spv::BuiltIn>(words[3]) == builtin) {
target = words[1];
}
});
return target;
}
Uint32 CountLoadsFrom(const Vector<Uint32>& spirv, Uint32 pointerId) {
Uint32 count = 0u;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == spv::Op::OpLoad && wordCount >= 4u && words[3] == pointerId) ++count;
});
return count;
}
Uint32 CountOpcode(const Vector<Uint32>& spirv, spv::Op wanted) {
Uint32 count = 0u;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
if (opcode == wanted) ++count;
});
return count;
}
bool Validates(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
return tools.Validate(spirv);
}
constexpr const char* kNumSubgroupsOnlySource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { uint value; } outputData;
void main() {
if (gl_LocalInvocationIndex == 0u)
outputData.value = gl_NumSubgroups;
}
)";
constexpr const char* kNoNumSubgroupsSource = R"(#version 450 core
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { uint value; } outputData;
void main() {
if (gl_LocalInvocationIndex == 0u)
outputData.value = gl_WorkGroupSize.x;
}
)";
} // namespace
TEST(DeriveNumSubgroupsPass, ReplacesBuiltinLoadAndSynthesizesSubgroupSize) {
const Vector<Uint32> input = CompileCompute(kNumSubgroupsOnlySource);
ASSERT_FALSE(input.empty());
const Uint32 inputNumSubgroups = FindBuiltinTarget(input, spv::BuiltIn::NumSubgroups);
ASSERT_NE(inputNumSubgroups, 0u);
EXPECT_EQ(CountLoadsFrom(input, inputNumSubgroups), 1u);
EXPECT_EQ(FindBuiltinTarget(input, spv::BuiltIn::SubgroupSize), 0u);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(input, output, true));
ASSERT_TRUE(Validates(output));
const Uint32 outputNumSubgroups = FindBuiltinTarget(output, spv::BuiltIn::NumSubgroups);
const Uint32 outputSubgroupSize = FindBuiltinTarget(output, spv::BuiltIn::SubgroupSize);
ASSERT_NE(outputNumSubgroups, 0u);
ASSERT_NE(outputSubgroupSize, 0u);
EXPECT_EQ(CountLoadsFrom(output, outputNumSubgroups), 0u);
EXPECT_EQ(CountLoadsFrom(output, outputSubgroupSize), 1u);
EXPECT_EQ(CountOpcode(output, spv::Op::OpCompositeExtract), 3u);
EXPECT_EQ(CountOpcode(output, spv::Op::OpIMul), 2u);
EXPECT_EQ(CountOpcode(output, spv::Op::OpUDiv), 1u);
}
TEST(DeriveNumSubgroupsPass, IsIdempotent) {
Vector<Uint32> once;
ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(CompileCompute(kNumSubgroupsOnlySource), once, true));
Vector<Uint32> twice;
ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(once, twice, true));
EXPECT_EQ(twice, once);
}
TEST(DeriveNumSubgroupsPass, LeavesUnrelatedComputeShaderUntouched) {
const Vector<Uint32> input = CompileCompute(kNoNumSubgroupsSource);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(input, output, true));
EXPECT_EQ(output, input);
EXPECT_EQ(FindBuiltinTarget(output, spv::BuiltIn::SubgroupSize), 0u);
}
@@ -0,0 +1,248 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/EmulateSubgroupsTest.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#define SPV_ENABLE_UTILITY_CODE
#include "glslang/SPIRV/spirv.hpp11"
#undef SPV_ENABLE_UTILITY_CODE
#include "Includes.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
constexpr SizeT kSpirvHeaderWordCount = 5u;
template <typename Visitor>
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
const Uint32 wordCount = spirv[offset] >> 16u;
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
offset += wordCount;
}
}
Vector<Uint32> CompileStage(GLenum stage, const String& source) {
using namespace MobileGL::MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
Uint32 CountGroupNonUniform(const Vector<Uint32>& spirv) {
Uint32 count = 0;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
if (opcode >= spv::Op::OpGroupNonUniformElect && opcode <= spv::Op::OpGroupNonUniformQuadSwap) {
++count;
}
});
return count;
}
Uint32 CountGroupNonUniformCapabilities(const Vector<Uint32>& spirv) {
Uint32 count = 0;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != spv::Op::OpCapability || wordCount < 2u) return;
const auto capability = static_cast<spv::Capability>(words[1]);
if (capability >= spv::Capability::GroupNonUniform &&
capability <= spv::Capability::GroupNonUniformQuad) {
++count;
}
});
return count;
}
Uint32 CountOpcode(const Vector<Uint32>& spirv, spv::Op wanted) {
Uint32 count = 0;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
if (opcode == wanted) ++count;
});
return count;
}
bool HasWorkgroupVariable(const Vector<Uint32>& spirv) {
bool found = false;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == spv::Op::OpVariable && wordCount >= 4u &&
static_cast<spv::StorageClass>(words[3]) == spv::StorageClass::Workgroup) {
found = true;
}
});
return found;
}
bool Validates(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
tools.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t& position,
const char* message) {
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
});
return tools.Validate(spirv);
}
// One shader touching every lowered category: builtins, vote, arithmetic
// scans, ballot math, shuffles, clustered and quad operations.
constexpr const char* kEveryCategorySource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_vote : require
#extension GL_KHR_shader_subgroup_arithmetic : require
#extension GL_KHR_shader_subgroup_ballot : require
#extension GL_KHR_shader_subgroup_shuffle : require
#extension GL_KHR_shader_subgroup_shuffle_relative : require
#extension GL_KHR_shader_subgroup_clustered : require
#extension GL_KHR_shader_subgroup_quad : require
layout(local_size_x = 48, local_size_y = 1, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { float value[]; } outputData;
void main() {
uint slot = gl_LocalInvocationIndex * 24u;
float v = float(gl_LocalInvocationIndex + 1u);
outputData.value[slot + 0u] = float(gl_SubgroupSize);
outputData.value[slot + 1u] = float(gl_NumSubgroups);
outputData.value[slot + 2u] = float(gl_SubgroupID);
outputData.value[slot + 3u] = float(gl_SubgroupInvocationID);
outputData.value[slot + 4u] = float(gl_SubgroupEqMask.x + gl_SubgroupLtMask.x);
outputData.value[slot + 5u] = subgroupElect() ? 1.0 : 0.0;
outputData.value[slot + 6u] = subgroupAll(v > 0.0) ? 1.0 : 0.0;
outputData.value[slot + 7u] = subgroupAny(v > 40.0) ? 1.0 : 0.0;
outputData.value[slot + 8u] = subgroupAllEqual(gl_WorkGroupID.x) ? 1.0 : 0.0;
outputData.value[slot + 9u] = subgroupAdd(v);
outputData.value[slot + 10u] = subgroupInclusiveAdd(v);
outputData.value[slot + 11u] = subgroupExclusiveMax(v);
outputData.value[slot + 12u] = float(subgroupMin(gl_LocalInvocationIndex));
uvec4 ballot = subgroupBallot((gl_LocalInvocationIndex & 1u) == 0u);
outputData.value[slot + 13u] = float(subgroupBallotBitCount(ballot));
outputData.value[slot + 14u] = float(subgroupBallotFindLSB(ballot));
outputData.value[slot + 15u] = float(subgroupBallotFindMSB(ballot));
outputData.value[slot + 16u] = subgroupInverseBallot(ballot) ? 1.0 : 0.0;
outputData.value[slot + 17u] = subgroupBallotBitExtract(ballot, 3u) ? 1.0 : 0.0;
outputData.value[slot + 18u] = subgroupBroadcast(v, 2u);
outputData.value[slot + 19u] = subgroupBroadcastFirst(v);
outputData.value[slot + 20u] = subgroupShuffle(v, gl_SubgroupInvocationID ^ 5u);
outputData.value[slot + 21u] = subgroupShuffleXor(v, 1u) + subgroupShuffleUp(v, 1u) +
subgroupShuffleDown(v, 1u);
outputData.value[slot + 22u] = subgroupClusteredAdd(v, 4u);
outputData.value[slot + 23u] = subgroupQuadBroadcast(v, 1u) + subgroupQuadSwapHorizontal(v);
subgroupBarrier();
subgroupMemoryBarrierShared();
}
)";
constexpr const char* kNoSubgroupSource = R"(#version 450 core
layout(local_size_x = 64) in;
layout(std430, binding = 0) buffer Output { uint value; } outputData;
void main() {
if (gl_LocalInvocationIndex == 0u) outputData.value = gl_WorkGroupSize.x;
}
)";
// An extended subgroup instruction (SPV_KHR_subgroup_rotate) alongside core
// ones: outside the lowered set, so the pass must fail rather than emit
// "subgroup-free" output that still rotates.
constexpr const char* kRotateSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
#extension GL_KHR_shader_subgroup_rotate : require
layout(local_size_x = 64) in;
layout(std430, binding = 0) buffer Output { float value[]; } outputData;
void main() {
float v = subgroupAdd(float(gl_SubgroupInvocationID));
outputData.value[gl_LocalInvocationIndex] = subgroupRotate(v, 1u);
}
)";
// A 1024-invocation workgroup exchanging a vec4 and a float: the lowering
// would need 16 KiB + 4 KiB of scratch, past the Vulkan-minimum shared
// budget of 16384 bytes.
constexpr const char* kScratchHungrySource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 1024) in;
layout(std430, binding = 0) buffer Output { vec4 value[]; } outputData;
void main() {
vec4 wide = subgroupAdd(vec4(float(gl_LocalInvocationIndex)));
wide.x += subgroupInclusiveAdd(float(gl_SubgroupInvocationID));
outputData.value[gl_LocalInvocationIndex] = wide;
}
)";
} // namespace
TEST(EmulateSubgroupsPass, LowersEveryCategoryToSharedMemory) {
const Vector<Uint32> input = CompileStage(GL_COMPUTE_SHADER, kEveryCategorySource);
ASSERT_FALSE(input.empty());
ASSERT_GT(CountGroupNonUniform(input), 0u);
ASSERT_GT(CountGroupNonUniformCapabilities(input), 0u);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::EmulateSubgroupsForVulkan(input, output, 16384u, true));
ASSERT_TRUE(Validates(output));
// The whole point: nothing subgroup-shaped survives, so the module runs on a
// device with no subgroup support at all.
EXPECT_EQ(CountGroupNonUniform(output), 0u);
EXPECT_EQ(CountGroupNonUniformCapabilities(output), 0u);
// The exchanges go through workgroup-shared scratch behind control barriers.
EXPECT_TRUE(HasWorkgroupVariable(output));
EXPECT_GT(CountOpcode(output, spv::Op::OpControlBarrier), CountOpcode(input, spv::Op::OpControlBarrier));
}
TEST(EmulateSubgroupsPass, IsIdempotent) {
const Vector<Uint32> input = CompileStage(GL_COMPUTE_SHADER, kEveryCategorySource);
ASSERT_FALSE(input.empty());
Vector<Uint32> once;
ASSERT_TRUE(ShaderCompiler::EmulateSubgroupsForVulkan(input, once, 16384u, true));
Vector<Uint32> twice;
ASSERT_TRUE(ShaderCompiler::EmulateSubgroupsForVulkan(once, twice, 16384u, true));
EXPECT_EQ(twice, once);
}
TEST(EmulateSubgroupsPass, LeavesSubgroupFreeComputeUntouched) {
const Vector<Uint32> input = CompileStage(GL_COMPUTE_SHADER, kNoSubgroupSource);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::EmulateSubgroupsForVulkan(input, output, 16384u, true));
EXPECT_EQ(output, input);
}
TEST(EmulateSubgroupsPass, RefusesExtendedSubgroupInstructions) {
const Vector<Uint32> input = CompileStage(GL_COMPUTE_SHADER, kRotateSource);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
EXPECT_FALSE(ShaderCompiler::EmulateSubgroupsForVulkan(input, output, 16384u, false));
}
TEST(EmulateSubgroupsPass, RefusesAModuleOverTheScratchBudget) {
const Vector<Uint32> input = CompileStage(GL_COMPUTE_SHADER, kScratchHungrySource);
ASSERT_FALSE(input.empty());
// vec4 scratch (1024 slots * 16 bytes) plus float scratch (4 KiB) exceeds
// the 16 KiB Vulkan-minimum budget.
Vector<Uint32> output;
EXPECT_FALSE(ShaderCompiler::EmulateSubgroupsForVulkan(input, output, 16384u, false));
// A device advertising more shared memory takes the same module fine.
Vector<Uint32> roomier;
EXPECT_TRUE(ShaderCompiler::EmulateSubgroupsForVulkan(input, roomier, 32768u, true));
EXPECT_TRUE(Validates(roomier));
}
@@ -0,0 +1,206 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/FixIterationRPBarrierTest.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#define SPV_ENABLE_UTILITY_CODE
#include "glslang/SPIRV/spirv.hpp11"
#undef SPV_ENABLE_UTILITY_CODE
#include "Includes.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
#include <map>
#include <vector>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
constexpr SizeT kSpirvHeaderWordCount = 5u;
template <typename Visitor>
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
const Uint32 wordCount = spirv[offset] >> 16u;
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
offset += wordCount;
}
}
Vector<Uint32> CompileCompute(const String& source) {
using namespace MobileGL::MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
bool Validates(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
tools.SetMessageConsumer(
[](spv_message_level_t, const char*, const spv_position_t& position, const char* message) {
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
});
return tools.Validate(spirv);
}
Uint32 CountOpcode(const Vector<Uint32>& spirv, spv::Op wanted) {
Uint32 count = 0u;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
if (opcode == wanted) ++count;
});
return count;
}
bool HasWorkgroupBarrierImmediatelyBeforeSecondScan(const Vector<Uint32>& spirv) {
std::map<Uint32, Uint32> uintConstants;
spv::Op previous = spv::Op::OpNop;
Uint32 scanCount = 0u;
bool found = false;
const Uint32* previousWords = nullptr;
Uint32 previousWordCount = 0u;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == spv::Op::OpConstant && wordCount >= 4u) {
uintConstants[words[2]] = words[3];
}
if (opcode == spv::Op::OpGroupNonUniformFAdd && wordCount >= 6u &&
static_cast<spv::GroupOperation>(words[4]) == spv::GroupOperation::InclusiveScan && ++scanCount == 2u &&
previous == spv::Op::OpControlBarrier && previousWordCount == 4u) {
found =
uintConstants[previousWords[1]] == static_cast<Uint32>(spv::Scope::Workgroup) &&
uintConstants[previousWords[2]] == static_cast<Uint32>(spv::Scope::Workgroup) &&
uintConstants[previousWords[3]] == (static_cast<Uint32>(spv::MemorySemanticsMask::AcquireRelease) |
static_cast<Uint32>(spv::MemorySemanticsMask::WorkgroupMemory));
}
previous = opcode;
previousWords = words;
previousWordCount = wordCount;
});
return found;
}
constexpr const char* kProgram203RaceShape = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { vec2 value; } outputData;
shared vec2 prefixSumCache[32];
void main() {
vec2 sampleLuminance = subgroupInclusiveAdd(
vec2(float(gl_LocalInvocationIndex), 1.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
barrier();
if (gl_LocalInvocationIndex == 511u)
prefixSumCache[0] = sampleLuminance / 512.0;
barrier();
float avg = prefixSumCache[0].x;
float weight = avg > 0.0 ? float(gl_LocalInvocationIndex + 1u) / avg : 0.0;
vec2 sampleExposure = subgroupInclusiveAdd(vec2(weight, 1.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleExposure;
barrier();
if (gl_LocalInvocationIndex == 511u)
outputData.value = sampleExposure;
}
)";
constexpr const char* kAlreadySynchronizedShape = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { vec2 value; } outputData;
shared vec2 prefixSumCache[32];
void main() {
vec2 first = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 1.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = first;
barrier();
if (gl_LocalInvocationIndex == 511u) prefixSumCache[0] = first / 512.0;
barrier();
float avg = prefixSumCache[0].x;
barrier();
vec2 second = subgroupInclusiveAdd(vec2(avg, 1.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = second;
barrier();
if (gl_LocalInvocationIndex == 511u) outputData.value = second;
}
)";
constexpr const char* kForeignSingleScanShape = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { vec2 value; } outputData;
shared vec2 prefixSumCache[32];
void main() {
vec2 value = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 1.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = value;
barrier();
if (gl_LocalInvocationIndex == 0u) outputData.value = prefixSumCache[0];
}
)";
} // namespace
TEST(FixIterationRPBarrierPass, InsertsWorkgroupBarrierBeforeSecondReduction) {
const Vector<Uint32> input = CompileCompute(kProgram203RaceShape);
ASSERT_FALSE(input.empty());
const Uint32 inputBarrierCount = CountOpcode(input, spv::Op::OpControlBarrier);
EXPECT_FALSE(HasWorkgroupBarrierImmediatelyBeforeSecondScan(input));
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, output, true));
EXPECT_EQ(CountOpcode(output, spv::Op::OpControlBarrier), inputBarrierCount + 1u);
EXPECT_TRUE(HasWorkgroupBarrierImmediatelyBeforeSecondScan(output));
EXPECT_TRUE(Validates(output));
}
TEST(FixIterationRPBarrierPass, LeavesOtherShapesByteIdentical) {
const Vector<Uint32> input = CompileCompute(kForeignSingleScanShape);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, output, true));
EXPECT_EQ(output, input);
}
TEST(FixIterationRPBarrierPass, LeavesAnAlreadySynchronizedShaderByteIdentical) {
const Vector<Uint32> input = CompileCompute(kAlreadySynchronizedShape);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, output, true));
EXPECT_EQ(output, input);
}
TEST(FixIterationRPBarrierPass, IsIdempotent) {
const Vector<Uint32> input = CompileCompute(kProgram203RaceShape);
ASSERT_FALSE(input.empty());
Vector<Uint32> once;
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, once, true));
Vector<Uint32> twice;
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(once, twice, true));
EXPECT_EQ(twice, once);
}
@@ -0,0 +1,336 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/FixIterationRPSubgroupScratchTest.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#define SPV_ENABLE_UTILITY_CODE
#include "glslang/SPIRV/spirv.hpp11"
#undef SPV_ENABLE_UTILITY_CODE
#include "Includes.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
#include <algorithm>
#include <map>
#include <vector>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
constexpr SizeT kSpirvHeaderWordCount = 5u;
template <typename Visitor>
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
const Uint32 wordCount = spirv[offset] >> 16u;
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
offset += wordCount;
}
}
Vector<Uint32> CompileCompute(const String& source) {
using namespace MobileGL::MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
// The declared lengths of every Workgroup-storage array variable, sorted.
std::vector<Uint32> WorkgroupArrayLengths(const Vector<Uint32>& spirv) {
std::map<Uint32, Uint32> constantValues; // constant id -> value
std::map<Uint32, Uint32> arrayLengthIds; // array type id -> length constant id
std::map<Uint32, Uint32> pointerPointees; // pointer type id -> pointee type id
std::vector<Uint32> workgroupPointerTypes; // type ids of Workgroup variables
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
switch (opcode) {
case spv::Op::OpConstant:
if (wordCount >= 4u) constantValues[words[2]] = words[3];
break;
case spv::Op::OpTypeArray:
if (wordCount >= 4u) arrayLengthIds[words[1]] = words[3];
break;
case spv::Op::OpTypePointer:
if (wordCount >= 4u &&
static_cast<spv::StorageClass>(words[2]) == spv::StorageClass::Workgroup) {
pointerPointees[words[1]] = words[3];
}
break;
case spv::Op::OpVariable:
if (wordCount >= 4u &&
static_cast<spv::StorageClass>(words[3]) == spv::StorageClass::Workgroup) {
workgroupPointerTypes.push_back(words[1]);
}
break;
default:
break;
}
});
std::vector<Uint32> lengths;
for (const Uint32 pointerTypeId : workgroupPointerTypes) {
const auto pointee = pointerPointees.find(pointerTypeId);
if (pointee == pointerPointees.end()) continue;
const auto lengthId = arrayLengthIds.find(pointee->second);
if (lengthId == arrayLengthIds.end()) continue;
const auto value = constantValues.find(lengthId->second);
if (value != constantValues.end()) lengths.push_back(value->second);
}
std::sort(lengths.begin(), lengths.end());
return lengths;
}
bool Validates(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
tools.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t& position,
const char* message) {
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
});
return tools.Validate(spirv);
}
// iterationRP's exposure reduction, as the pack ships it: 32x16 (512
// invocations), subgroupInclusiveAdd on a vec2, and a 32-entry
// gl_SubgroupID-indexed scratch. A second, plainly indexed array rides along
// to prove the patch is surgical.
constexpr const char* kExposureShapedSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { float value; } outputData;
shared vec2 prefixSumCache[32];
shared float plainScratch[4];
void main() {
vec2 sampleLuminance = vec2(float(gl_LocalInvocationIndex), 0.0);
sampleLuminance = subgroupInclusiveAdd(sampleLuminance);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
plainScratch[gl_LocalInvocationIndex & 3u] = sampleLuminance.x;
barrier();
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) {
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
sampleLuminance += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
}
barrier();
}
if (gl_LocalInvocationIndex == 511u)
outputData.value = prefixSumCache[0].x / 512.0 + plainScratch[0];
}
)";
// The pack's OTHER instance of the same bug, which a fingerprint pinned to the
// exposure pass's dimensions walks straight past: the RTW importance warp
// scans a plain float across 1024 invocations into a 64-entry scratch.
constexpr const char* kRtwWarpShapedSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 1024) in;
layout(std430, binding = 0) buffer Output { float value; } outputData;
shared float prefixSumCache[64];
void main() {
float importance = float(gl_LocalInvocationID.x) * 0.5;
float prefixSum = subgroupInclusiveAdd(importance);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = prefixSum;
barrier();
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) {
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
prefixSum += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = prefixSum;
}
barrier();
}
if (gl_LocalInvocationID.x == 1023u) outputData.value = prefixSumCache[0];
}
)";
// A subgroup scan, but the scratch is indexed per invocation rather than per
// subgroup: its size is not a subgroup-count assumption, so it is not ours.
constexpr const char* kInvocationIndexedSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { float value; } outputData;
shared vec2 perInvocation[32];
void main() {
vec2 v = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 0.0));
perInvocation[gl_LocalInvocationIndex & 31u] = v;
barrier();
if (gl_LocalInvocationIndex == 0u) outputData.value = perInvocation[0].x;
}
)";
// gl_SubgroupID-indexed, but no subgroup scan feeds it and the element type is
// not the pack's float accumulator.
constexpr const char* kNonFloatScratchSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { uint value; } outputData;
shared uint tally[32];
void main() {
float scan = subgroupInclusiveAdd(float(gl_LocalInvocationIndex));
tally[gl_SubgroupID] = uint(scan);
barrier();
if (gl_LocalInvocationIndex == 0u) outputData.value = tally[0];
}
)";
// gl_SubgroupID-indexed, but masked into range: the declaration is bounded by
// construction, not a subgroup-count assumption, so it is not the pack's bug.
constexpr const char* kMaskedSubgroupIndexSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { float value; } outputData;
shared vec2 bounded[8];
void main() {
vec2 v = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 0.0));
bounded[gl_SubgroupID & 7u] = v;
barrier();
if (gl_LocalInvocationIndex == 0u) outputData.value = bounded[0].x;
}
)";
// Neither of the pack's shapes: a small per-subgroup array in a 256-invocation
// workgroup, used to prove the width gate keeps EVERY module inert at >= 16 lanes.
constexpr const char* kForeignShapeSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 256) in;
layout(std430, binding = 0) buffer Output { float value; } outputData;
shared float partial[4];
void main() {
float v = subgroupInclusiveAdd(float(gl_LocalInvocationID.x));
if (gl_SubgroupID < 4u) partial[gl_SubgroupID] = v;
barrier();
if (gl_LocalInvocationID.x == 0u) outputData.value = partial[0];
}
)";
// No subgroup construct at all.
constexpr const char* kSubgroupFreeSource = R"(#version 450 core
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { float value; } outputData;
shared vec2 scratch[32];
void main() {
scratch[gl_LocalInvocationIndex & 31u] = vec2(float(gl_LocalInvocationIndex), 0.0);
barrier();
if (gl_LocalInvocationIndex == 0u) outputData.value = scratch[0].x;
}
)";
} // namespace
TEST(FixIterationRPSubgroupScratchPass, GrowsTheExposureScratchForNarrowSubgroups) {
const Vector<Uint32> input = CompileCompute(kExposureShapedSource);
ASSERT_FALSE(input.empty());
ASSERT_EQ(WorkgroupArrayLengths(input), (std::vector<Uint32>{4u, 32u}));
// lavapipe: 8-lane subgroups over 512 invocations need 64 entries; the
// plainly indexed neighbour must keep its 4.
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, 32768u, true));
EXPECT_EQ(WorkgroupArrayLengths(output), (std::vector<Uint32>{4u, 64u}));
EXPECT_TRUE(Validates(output));
}
// The regression the CI retrace caught: patching only the exposure pass leaves
// this one writing 128 subgroups into 64 entries, and the frame stays wrong.
TEST(FixIterationRPSubgroupScratchPass, GrowsTheRtwWarpScratchForNarrowSubgroups) {
const Vector<Uint32> input = CompileCompute(kRtwWarpShapedSource);
ASSERT_FALSE(input.empty());
ASSERT_EQ(WorkgroupArrayLengths(input), (std::vector<Uint32>{64u}));
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, 32768u, true));
EXPECT_EQ(WorkgroupArrayLengths(output), (std::vector<Uint32>{128u}));
EXPECT_TRUE(Validates(output));
}
TEST(FixIterationRPSubgroupScratchPass, LeavesPackWidthAssumptionsAloneOnWideDevices) {
// Both shapes are sized for >= 16 lanes (512/16 = 32, 1024/16 = 64), so on
// every such device the modules must pass through byte-identical.
for (const char* source : {kExposureShapedSource, kRtwWarpShapedSource}) {
const Vector<Uint32> input = CompileCompute(source);
ASSERT_FALSE(input.empty());
for (const Uint32 nativeSize : {16u, 32u, 64u, 128u}) {
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
input, output, nativeSize, 32768u, true));
EXPECT_EQ(output, input) << "native width " << nativeSize;
}
}
}
TEST(FixIterationRPSubgroupScratchPass, RefusesAModuleOutsideTheIdiom) {
for (const char* source : {kInvocationIndexedSource, kNonFloatScratchSource,
kSubgroupFreeSource, kMaskedSubgroupIndexSource}) {
const Vector<Uint32> input = CompileCompute(source);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, 32768u, true));
EXPECT_EQ(output, input);
}
}
// A grown array that would not fit the device's shared memory is left alone:
// a pipeline that cannot be created is worse than the pack's own overrun.
// The width gate is what keeps unrelated shaders untouched on the devices the pack
// was written for: at >= 16 lanes nothing is rewritten, whatever its shape.
TEST(FixIterationRPSubgroupScratchPass, LeavesEveryModuleAloneAtThePacksAssumedWidth) {
const Vector<Uint32> input = CompileCompute(kForeignShapeSource);
ASSERT_FALSE(input.empty());
for (const Uint32 nativeSize : {16u, 32u, 64u}) {
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
input, output, nativeSize, 32768u, true));
EXPECT_EQ(output, input) << "native width " << nativeSize;
}
}
TEST(FixIterationRPSubgroupScratchPass, RefusesGrowthThatWouldNotFitSharedMemory) {
const Vector<Uint32> input = CompileCompute(kRtwWarpShapedSource);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, 256u, true));
EXPECT_EQ(output, input);
}
TEST(FixIterationRPSubgroupScratchPass, IsIdempotent) {
for (const char* source : {kExposureShapedSource, kRtwWarpShapedSource}) {
const Vector<Uint32> input = CompileCompute(source);
ASSERT_FALSE(input.empty());
Vector<Uint32> once;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, once, 8u, 32768u, true));
Vector<Uint32> twice;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(once, twice, 8u, 32768u, true));
EXPECT_EQ(twice, once);
}
}
@@ -98,7 +98,6 @@ class FlattenXfbInterfaceBlocksTest : public ::testing::Test {
protected:
void SetUp() override {
MobileGL::Initialize();
ShaderCompiler::SetSpirvValidationEnabled(true);
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
}
@@ -116,7 +115,7 @@ TEST_F(FlattenXfbInterfaceBlocksTest, FlattensACapturedBlockIntoOneVariablePerMe
std::set<String> flattened;
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output));
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output, true));
ASSERT_FALSE(output.empty());
EXPECT_EQ(flattened, (std::set<String>{"StageData"}));
@@ -145,7 +144,7 @@ TEST_F(FlattenXfbInterfaceBlocksTest, TheEmittedDeclarationIsAPlainArrayNotABloc
std::set<String> flattened;
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output));
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output, true));
const String after = Transpile(output);
EXPECT_NE(after.find("StageData_attrib[16]"), String::npos) << after;
@@ -162,7 +161,7 @@ TEST_F(FlattenXfbInterfaceBlocksTest, GivesEachMemberItsOwnConsecutiveLocations)
std::set<String> flattened;
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output));
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output, true));
ASSERT_FALSE(output.empty());
const String dis = Disassemble(output);
@@ -184,7 +183,7 @@ TEST_F(FlattenXfbInterfaceBlocksTest, LeavesABlockNoCaptureNamesAlone) {
std::set<String> flattened;
Vector<Uint32> output;
ASSERT_TRUE(
ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"SomeOtherBlock"}, flattened, output));
ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"SomeOtherBlock"}, flattened, output, true));
EXPECT_TRUE(flattened.empty());
const String after = Transpile(output);
@@ -200,7 +199,7 @@ TEST_F(FlattenXfbInterfaceBlocksTest, DeclinesAnEmptyRequestWithoutRewriting) {
std::set<String> flattened;
Vector<Uint32> output;
EXPECT_FALSE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {}, flattened, output));
EXPECT_FALSE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {}, flattened, output, true));
EXPECT_TRUE(flattened.empty());
EXPECT_TRUE(output.empty());
}
@@ -955,6 +955,8 @@ namespace MobileGL::MG_Util::BackendLoader {
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);
const Bool esAtLeast31 = caps.GLESVersion.Major > 3 ||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1);
caps.SupportsDrawIndirect = esAtLeast31 && glesFuncs.glDrawArraysIndirect != nullptr &&
glesFuncs.glDrawElementsIndirect != nullptr;
caps.SupportsDrawElementsBaseVertex = (esAtLeast32 || hasDrawElementsBaseVertexExtension) &&
glesFuncs.glDrawElementsBaseVertex != nullptr;
caps.SupportsComputeShader = esAtLeast31 && glesFuncs.glDispatchCompute != nullptr &&
@@ -976,6 +978,7 @@ namespace MobileGL::MG_Util::BackendLoader {
MGLOG_I(" indexed glColorMaski: %s", caps.SupportsIndexedColorMask ? "yes" : "no");
MGLOG_I(" dual-source blend (EXT_blend_func_extended): %s",
caps.SupportsDualSourceBlend ? "yes" : "no");
MGLOG_I(" draw indirect (ES 3.1 core): %s", caps.SupportsDrawIndirect ? "yes" : "no");
MGLOG_I(" multi-draw indirect (EXT_multi_draw_indirect): %s",
caps.SupportsMultiDrawIndirect ? "yes" : "no");
MGLOG_I(" multi-draw base vertex (EXT/OES_draw_elements_base_vertex + EXT_multi_draw_arrays): %s",
@@ -1149,6 +1149,10 @@ namespace MobileGL {
// GLES 3.2 core or GL_OES_shader_multisample_interpolation exposes
// interpolateAtOffset and the three fragment-offset limit queries.
Bool SupportsShaderMultisampleInterpolation = false;
// ES 3.1+ exposes glDrawArraysIndirect / glDrawElementsIndirect in core. Keep the
// version and both entry-point checks together so extension advertisement and the
// DirectGLES dispatch path cannot disagree on whether native indirect draws exist.
Bool SupportsDrawIndirect = false;
// GL_EXT_multi_draw_indirect is present AND glMultiDrawArraysIndirectEXT /
// glMultiDrawElementsIndirectEXT both resolved. Multi-draw is not core in any ES
// version, and eglGetProcAddress may return a live-looking stub on drivers without
+2 -2
View File
@@ -33,13 +33,13 @@ namespace MobileGL {
std::string GetThreadName() {
char buffer[64] = {0};
#if defined(_WIN32) && !defined(__MINGW32__)
#if defined(_WIN32)
PWSTR desc = nullptr;
if (SUCCEEDED(GetThreadDescription(GetCurrentThread(), &desc))) {
WideCharToMultiByte(CP_UTF8, 0, desc, -1, buffer, sizeof(buffer), nullptr, nullptr);
LocalFree(desc);
}
#elif defined(__ANDROID__) || defined(__linux__) || defined(__APPLE__) || defined(__MINGW32__)
#elif defined(__ANDROID__) || defined(__linux__) || defined(__APPLE__)
pthread_getname_np(pthread_self(), buffer, sizeof(buffer));
#endif
return buffer[0] ? buffer : "UnknownThread";
+458 -3
View File
@@ -7,6 +7,8 @@
// End of Source File Header
#include "DriverPost.h"
#include "DriverPostIterationRPWitness.h"
#include "DriverPostIterationRPWitnessSpv.h"
#include "MG_Util/BackendLoaders/OpenGL/Loader.h"
#include <Config.h>
#include <MGGitHash.h>
@@ -24,6 +26,8 @@
#include <MG_Util/Texture/TextureFormatProcessor.h>
#include <MG_Util/Async/ShaderCompilePool.h>
#include <chrono>
#include <cstring>
#include <limits>
#include <thread>
#if !defined(_WIN32)
@@ -1168,7 +1172,9 @@ namespace MobileGL::MG_Util::SelfTest {
backendApiVersionString = MG_Backend::DirectGLES::FormatBackendAPIVersionString(
summary.caps.GLESRendererString, summary.caps.GLESVersion.Major, summary.caps.GLESVersion.Minor);
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectGLES::BuildAdvertisedExtensions(
summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy));
summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy,
summary.caps.SupportsDrawIndirect,
summary.caps.SupportsDrawIndirect && summary.caps.SupportsBaseInstance));
}
AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString,
advertisedExtensions);
@@ -1452,6 +1458,437 @@ namespace MobileGL::MG_Util::SelfTest {
disabledNote);
}
// Native iterationRP compute witness. This deliberately uses a separate
// throwaway Vulkan device rather than the real renderer's queues, and it
// treats MOBILEGL_DISABLE_SUBGROUP as irrelevant: the row reports what the
// driver does, not what MobileGL elects to advertise to applications.
void ProbeVulkanIterationRPWitness(ReportBuilder& builder, PFN_vkGetInstanceProcAddr getInstanceProcAddr,
VkInstance instance, VkPhysicalDevice physicalDevice,
Uint32 computeQueueFamilyIndex,
const VkPhysicalDeviceProperties& properties,
Bool subgroupPropertiesAvailable,
const VkPhysicalDeviceSubgroupProperties& subgroupProperties) {
constexpr const char* RowName = "Subgroup first-reduction witness";
const auto fail = [&](String detail) { builder.Fail(RowName, Move(detail)); };
if (!subgroupPropertiesAvailable) {
fail("vkGetPhysicalDeviceProperties2 could not provide raw Vulkan subgroup properties");
return;
}
IterationRPWitnessLimits limits{};
limits.computeStageSupported =
(subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0;
limits.basicSubgroupSupported =
(subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0;
limits.arithmeticSubgroupSupported =
(subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_ARITHMETIC_BIT) != 0;
limits.subgroupSize = subgroupProperties.subgroupSize;
limits.maxComputeWorkGroupInvocations = properties.limits.maxComputeWorkGroupInvocations;
limits.maxComputeWorkGroupSize = {properties.limits.maxComputeWorkGroupSize[0],
properties.limits.maxComputeWorkGroupSize[1],
properties.limits.maxComputeWorkGroupSize[2]};
limits.maxComputeSharedMemorySize = properties.limits.maxComputeSharedMemorySize;
limits.maxPerStageDescriptorStorageBuffers = properties.limits.maxPerStageDescriptorStorageBuffers;
limits.maxDescriptorSetStorageBuffers = properties.limits.maxDescriptorSetStorageBuffers;
limits.maxBoundDescriptorSets = properties.limits.maxBoundDescriptorSets;
limits.maxStorageBufferRange = properties.limits.maxStorageBufferRange;
const IterationRPWitnessEligibilityResult eligibility = EvaluateIterationRPWitnessEligibility(limits);
if (eligibility.eligibility == IterationRPWitnessEligibility::SkipUnsupportedNativeFeatureSet) {
builder.Info(RowName, eligibility.detail);
return;
}
if (eligibility.eligibility == IterationRPWitnessEligibility::FailInadequateLimits) {
fail(eligibility.detail);
return;
}
if (computeQueueFamilyIndex == std::numeric_limits<Uint32>::max()) {
fail("no compute queue family is available for the native Vulkan witness");
return;
}
const auto vkGetPhysicalDeviceMemoryPropertiesFn =
reinterpret_cast<PFN_vkGetPhysicalDeviceMemoryProperties>(
getInstanceProcAddr(instance, "vkGetPhysicalDeviceMemoryProperties"));
const auto vkCreateDeviceFn =
reinterpret_cast<PFN_vkCreateDevice>(getInstanceProcAddr(instance, "vkCreateDevice"));
const auto vkDestroyDeviceFn =
reinterpret_cast<PFN_vkDestroyDevice>(getInstanceProcAddr(instance, "vkDestroyDevice"));
const auto vkGetDeviceQueueFn =
reinterpret_cast<PFN_vkGetDeviceQueue>(getInstanceProcAddr(instance, "vkGetDeviceQueue"));
const auto vkCreateBufferFn =
reinterpret_cast<PFN_vkCreateBuffer>(getInstanceProcAddr(instance, "vkCreateBuffer"));
const auto vkDestroyBufferFn =
reinterpret_cast<PFN_vkDestroyBuffer>(getInstanceProcAddr(instance, "vkDestroyBuffer"));
const auto vkGetBufferMemoryRequirementsFn = reinterpret_cast<PFN_vkGetBufferMemoryRequirements>(
getInstanceProcAddr(instance, "vkGetBufferMemoryRequirements"));
const auto vkAllocateMemoryFn =
reinterpret_cast<PFN_vkAllocateMemory>(getInstanceProcAddr(instance, "vkAllocateMemory"));
const auto vkFreeMemoryFn =
reinterpret_cast<PFN_vkFreeMemory>(getInstanceProcAddr(instance, "vkFreeMemory"));
const auto vkBindBufferMemoryFn =
reinterpret_cast<PFN_vkBindBufferMemory>(getInstanceProcAddr(instance, "vkBindBufferMemory"));
const auto vkMapMemoryFn =
reinterpret_cast<PFN_vkMapMemory>(getInstanceProcAddr(instance, "vkMapMemory"));
const auto vkUnmapMemoryFn =
reinterpret_cast<PFN_vkUnmapMemory>(getInstanceProcAddr(instance, "vkUnmapMemory"));
const auto vkCreateDescriptorSetLayoutFn = reinterpret_cast<PFN_vkCreateDescriptorSetLayout>(
getInstanceProcAddr(instance, "vkCreateDescriptorSetLayout"));
const auto vkDestroyDescriptorSetLayoutFn = reinterpret_cast<PFN_vkDestroyDescriptorSetLayout>(
getInstanceProcAddr(instance, "vkDestroyDescriptorSetLayout"));
const auto vkCreateDescriptorPoolFn =
reinterpret_cast<PFN_vkCreateDescriptorPool>(getInstanceProcAddr(instance, "vkCreateDescriptorPool"));
const auto vkDestroyDescriptorPoolFn = reinterpret_cast<PFN_vkDestroyDescriptorPool>(
getInstanceProcAddr(instance, "vkDestroyDescriptorPool"));
const auto vkAllocateDescriptorSetsFn = reinterpret_cast<PFN_vkAllocateDescriptorSets>(
getInstanceProcAddr(instance, "vkAllocateDescriptorSets"));
const auto vkUpdateDescriptorSetsFn =
reinterpret_cast<PFN_vkUpdateDescriptorSets>(getInstanceProcAddr(instance, "vkUpdateDescriptorSets"));
const auto vkCreateShaderModuleFn =
reinterpret_cast<PFN_vkCreateShaderModule>(getInstanceProcAddr(instance, "vkCreateShaderModule"));
const auto vkDestroyShaderModuleFn =
reinterpret_cast<PFN_vkDestroyShaderModule>(getInstanceProcAddr(instance, "vkDestroyShaderModule"));
const auto vkCreatePipelineLayoutFn =
reinterpret_cast<PFN_vkCreatePipelineLayout>(getInstanceProcAddr(instance, "vkCreatePipelineLayout"));
const auto vkDestroyPipelineLayoutFn = reinterpret_cast<PFN_vkDestroyPipelineLayout>(
getInstanceProcAddr(instance, "vkDestroyPipelineLayout"));
const auto vkCreateComputePipelinesFn = reinterpret_cast<PFN_vkCreateComputePipelines>(
getInstanceProcAddr(instance, "vkCreateComputePipelines"));
const auto vkDestroyPipelineFn =
reinterpret_cast<PFN_vkDestroyPipeline>(getInstanceProcAddr(instance, "vkDestroyPipeline"));
const auto vkCreateCommandPoolFn =
reinterpret_cast<PFN_vkCreateCommandPool>(getInstanceProcAddr(instance, "vkCreateCommandPool"));
const auto vkDestroyCommandPoolFn =
reinterpret_cast<PFN_vkDestroyCommandPool>(getInstanceProcAddr(instance, "vkDestroyCommandPool"));
const auto vkAllocateCommandBuffersFn = reinterpret_cast<PFN_vkAllocateCommandBuffers>(
getInstanceProcAddr(instance, "vkAllocateCommandBuffers"));
const auto vkBeginCommandBufferFn =
reinterpret_cast<PFN_vkBeginCommandBuffer>(getInstanceProcAddr(instance, "vkBeginCommandBuffer"));
const auto vkEndCommandBufferFn =
reinterpret_cast<PFN_vkEndCommandBuffer>(getInstanceProcAddr(instance, "vkEndCommandBuffer"));
const auto vkCmdBindPipelineFn =
reinterpret_cast<PFN_vkCmdBindPipeline>(getInstanceProcAddr(instance, "vkCmdBindPipeline"));
const auto vkCmdBindDescriptorSetsFn = reinterpret_cast<PFN_vkCmdBindDescriptorSets>(
getInstanceProcAddr(instance, "vkCmdBindDescriptorSets"));
const auto vkCmdDispatchFn =
reinterpret_cast<PFN_vkCmdDispatch>(getInstanceProcAddr(instance, "vkCmdDispatch"));
const auto vkCmdPipelineBarrierFn =
reinterpret_cast<PFN_vkCmdPipelineBarrier>(getInstanceProcAddr(instance, "vkCmdPipelineBarrier"));
const auto vkCreateFenceFn =
reinterpret_cast<PFN_vkCreateFence>(getInstanceProcAddr(instance, "vkCreateFence"));
const auto vkDestroyFenceFn =
reinterpret_cast<PFN_vkDestroyFence>(getInstanceProcAddr(instance, "vkDestroyFence"));
const auto vkQueueSubmitFn =
reinterpret_cast<PFN_vkQueueSubmit>(getInstanceProcAddr(instance, "vkQueueSubmit"));
const auto vkWaitForFencesFn =
reinterpret_cast<PFN_vkWaitForFences>(getInstanceProcAddr(instance, "vkWaitForFences"));
const auto vkDeviceWaitIdleFn =
reinterpret_cast<PFN_vkDeviceWaitIdle>(getInstanceProcAddr(instance, "vkDeviceWaitIdle"));
if (vkGetPhysicalDeviceMemoryPropertiesFn == nullptr || vkCreateDeviceFn == nullptr ||
vkDestroyDeviceFn == nullptr || vkGetDeviceQueueFn == nullptr || vkCreateBufferFn == nullptr ||
vkDestroyBufferFn == nullptr || vkGetBufferMemoryRequirementsFn == nullptr ||
vkAllocateMemoryFn == nullptr || vkFreeMemoryFn == nullptr || vkBindBufferMemoryFn == nullptr ||
vkMapMemoryFn == nullptr || vkUnmapMemoryFn == nullptr || vkCreateDescriptorSetLayoutFn == nullptr ||
vkDestroyDescriptorSetLayoutFn == nullptr || vkCreateDescriptorPoolFn == nullptr ||
vkDestroyDescriptorPoolFn == nullptr || vkAllocateDescriptorSetsFn == nullptr ||
vkUpdateDescriptorSetsFn == nullptr || vkCreateShaderModuleFn == nullptr ||
vkDestroyShaderModuleFn == nullptr || vkCreatePipelineLayoutFn == nullptr ||
vkDestroyPipelineLayoutFn == nullptr || vkCreateComputePipelinesFn == nullptr ||
vkDestroyPipelineFn == nullptr || vkCreateCommandPoolFn == nullptr || vkDestroyCommandPoolFn == nullptr ||
vkAllocateCommandBuffersFn == nullptr || vkBeginCommandBufferFn == nullptr ||
vkEndCommandBufferFn == nullptr || vkCmdBindPipelineFn == nullptr ||
vkCmdBindDescriptorSetsFn == nullptr || vkCmdDispatchFn == nullptr ||
vkCmdPipelineBarrierFn == nullptr || vkCreateFenceFn == nullptr || vkDestroyFenceFn == nullptr ||
vkQueueSubmitFn == nullptr || vkWaitForFencesFn == nullptr || vkDeviceWaitIdleFn == nullptr) {
fail("vkGetInstanceProcAddr could not resolve the Vulkan entry points required for the witness");
return;
}
const Float queuePriority = 1.0f;
VkDeviceQueueCreateInfo queueInfo{};
queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueInfo.queueFamilyIndex = computeQueueFamilyIndex;
queueInfo.queueCount = 1;
queueInfo.pQueuePriorities = &queuePriority;
VkDeviceCreateInfo deviceInfo{};
deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceInfo.queueCreateInfoCount = 1;
deviceInfo.pQueueCreateInfos = &queueInfo;
VkDevice device = VK_NULL_HANDLE;
VkResult result = vkCreateDeviceFn(physicalDevice, &deviceInfo, nullptr, &device);
if (result != VK_SUCCESS || device == VK_NULL_HANDLE) {
fail(format("vkCreateDevice failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkBuffer outputBuffer = VK_NULL_HANDLE;
VkDeviceMemory outputMemory = VK_NULL_HANDLE;
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
VkShaderModule shaderModule = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkPipeline pipeline = VK_NULL_HANDLE;
VkCommandPool commandPool = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
void* mappedOutput = nullptr;
Bool fenceWaitTimedOut = false;
const ScopeGuard destroyDeviceObjects([&]() {
if (fenceWaitTimedOut) {
// Match ProbeVulkanTimerQuery: the command may still execute
// after a timeout, so intentionally retain every device-owned
// resource rather than risking a forever wait or UAF in the ICD.
return;
}
vkDeviceWaitIdleFn(device);
if (fence != VK_NULL_HANDLE) vkDestroyFenceFn(device, fence, nullptr);
if (commandPool != VK_NULL_HANDLE) vkDestroyCommandPoolFn(device, commandPool, nullptr);
if (pipeline != VK_NULL_HANDLE) vkDestroyPipelineFn(device, pipeline, nullptr);
if (pipelineLayout != VK_NULL_HANDLE) vkDestroyPipelineLayoutFn(device, pipelineLayout, nullptr);
if (shaderModule != VK_NULL_HANDLE) vkDestroyShaderModuleFn(device, shaderModule, nullptr);
if (descriptorPool != VK_NULL_HANDLE) vkDestroyDescriptorPoolFn(device, descriptorPool, nullptr);
if (descriptorSetLayout != VK_NULL_HANDLE) {
vkDestroyDescriptorSetLayoutFn(device, descriptorSetLayout, nullptr);
}
if (mappedOutput != nullptr) vkUnmapMemoryFn(device, outputMemory);
if (outputBuffer != VK_NULL_HANDLE) vkDestroyBufferFn(device, outputBuffer, nullptr);
if (outputMemory != VK_NULL_HANDLE) vkFreeMemoryFn(device, outputMemory, nullptr);
vkDestroyDeviceFn(device, nullptr);
});
VkQueue queue = VK_NULL_HANDLE;
vkGetDeviceQueueFn(device, computeQueueFamilyIndex, 0, &queue);
if (queue == VK_NULL_HANDLE) {
fail("vkGetDeviceQueue returned a null compute queue");
return;
}
VkBufferCreateInfo bufferInfo{};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = sizeof(IterationRPWitnessOutput);
bufferInfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
result = vkCreateBufferFn(device, &bufferInfo, nullptr, &outputBuffer);
if (result != VK_SUCCESS) {
fail(format("vkCreateBuffer(output SSBO) failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkMemoryRequirements memoryRequirements{};
vkGetBufferMemoryRequirementsFn(device, outputBuffer, &memoryRequirements);
VkPhysicalDeviceMemoryProperties memoryProperties{};
vkGetPhysicalDeviceMemoryPropertiesFn(physicalDevice, &memoryProperties);
Uint32 memoryTypeIndex = std::numeric_limits<Uint32>::max();
for (Uint32 index = 0; index < memoryProperties.memoryTypeCount; ++index) {
const Bool compatible = (memoryRequirements.memoryTypeBits & (1u << index)) != 0u;
const VkMemoryPropertyFlags required = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
if (compatible && (memoryProperties.memoryTypes[index].propertyFlags & required) == required) {
memoryTypeIndex = index;
break;
}
}
if (memoryTypeIndex == std::numeric_limits<Uint32>::max()) {
fail("no host-visible/coherent memory type is compatible with the output SSBO");
return;
}
VkMemoryAllocateInfo memoryInfo{};
memoryInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
memoryInfo.allocationSize = memoryRequirements.size;
memoryInfo.memoryTypeIndex = memoryTypeIndex;
result = vkAllocateMemoryFn(device, &memoryInfo, nullptr, &outputMemory);
if (result != VK_SUCCESS) {
fail(format("vkAllocateMemory(output SSBO) failed (VkResult = {})", static_cast<Int>(result)));
return;
}
result = vkBindBufferMemoryFn(device, outputBuffer, outputMemory, 0);
if (result != VK_SUCCESS) {
fail(format("vkBindBufferMemory(output SSBO) failed (VkResult = {})", static_cast<Int>(result)));
return;
}
result = vkMapMemoryFn(device, outputMemory, 0, sizeof(IterationRPWitnessOutput), 0, &mappedOutput);
if (result != VK_SUCCESS || mappedOutput == nullptr) {
fail(format("vkMapMemory(output SSBO) failed (VkResult = {})", static_cast<Int>(result)));
return;
}
std::memset(mappedOutput, 0xa5, sizeof(IterationRPWitnessOutput));
VkDescriptorSetLayoutBinding outputBinding{};
outputBinding.binding = 0;
outputBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
outputBinding.descriptorCount = 1;
outputBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
VkDescriptorSetLayoutCreateInfo descriptorSetLayoutInfo{};
descriptorSetLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
descriptorSetLayoutInfo.bindingCount = 1;
descriptorSetLayoutInfo.pBindings = &outputBinding;
result = vkCreateDescriptorSetLayoutFn(device, &descriptorSetLayoutInfo, nullptr, &descriptorSetLayout);
if (result != VK_SUCCESS) {
fail(format("vkCreateDescriptorSetLayout failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkDescriptorPoolSize poolSize{};
poolSize.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
poolSize.descriptorCount = 1;
VkDescriptorPoolCreateInfo descriptorPoolInfo{};
descriptorPoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
descriptorPoolInfo.maxSets = 1;
descriptorPoolInfo.poolSizeCount = 1;
descriptorPoolInfo.pPoolSizes = &poolSize;
result = vkCreateDescriptorPoolFn(device, &descriptorPoolInfo, nullptr, &descriptorPool);
if (result != VK_SUCCESS) {
fail(format("vkCreateDescriptorPool failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
VkDescriptorSetAllocateInfo descriptorSetInfo{};
descriptorSetInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
descriptorSetInfo.descriptorPool = descriptorPool;
descriptorSetInfo.descriptorSetCount = 1;
descriptorSetInfo.pSetLayouts = &descriptorSetLayout;
result = vkAllocateDescriptorSetsFn(device, &descriptorSetInfo, &descriptorSet);
if (result != VK_SUCCESS) {
fail(format("vkAllocateDescriptorSets failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkDescriptorBufferInfo outputDescriptor{};
outputDescriptor.buffer = outputBuffer;
outputDescriptor.offset = 0;
outputDescriptor.range = sizeof(IterationRPWitnessOutput);
VkWriteDescriptorSet descriptorWrite{};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = descriptorSet;
descriptorWrite.dstBinding = 0;
descriptorWrite.descriptorCount = 1;
descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
descriptorWrite.pBufferInfo = &outputDescriptor;
vkUpdateDescriptorSetsFn(device, 1, &descriptorWrite, 0, nullptr);
VkShaderModuleCreateInfo shaderModuleInfo{};
shaderModuleInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
shaderModuleInfo.codeSize = sizeof(kDriverPostIterationRPWitnessSpv);
shaderModuleInfo.pCode = kDriverPostIterationRPWitnessSpv;
result = vkCreateShaderModuleFn(device, &shaderModuleInfo, nullptr, &shaderModule);
if (result != VK_SUCCESS) {
fail(format("vkCreateShaderModule failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
result = vkCreatePipelineLayoutFn(device, &pipelineLayoutInfo, nullptr, &pipelineLayout);
if (result != VK_SUCCESS) {
fail(format("vkCreatePipelineLayout failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkPipelineShaderStageCreateInfo shaderStage{};
shaderStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
shaderStage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
shaderStage.module = shaderModule;
shaderStage.pName = "main";
VkComputePipelineCreateInfo pipelineInfo{};
pipelineInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
pipelineInfo.stage = shaderStage;
pipelineInfo.layout = pipelineLayout;
result = vkCreateComputePipelinesFn(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline);
if (result != VK_SUCCESS) {
fail(format("vkCreateComputePipelines failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkCommandPoolCreateInfo commandPoolInfo{};
commandPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
commandPoolInfo.queueFamilyIndex = computeQueueFamilyIndex;
result = vkCreateCommandPoolFn(device, &commandPoolInfo, nullptr, &commandPool);
if (result != VK_SUCCESS) {
fail(format("vkCreateCommandPool failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkCommandBufferAllocateInfo commandBufferInfo{};
commandBufferInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
commandBufferInfo.commandPool = commandPool;
commandBufferInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
commandBufferInfo.commandBufferCount = 1;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
result = vkAllocateCommandBuffersFn(device, &commandBufferInfo, &commandBuffer);
if (result != VK_SUCCESS) {
fail(format("vkAllocateCommandBuffers failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkCommandBufferBeginInfo commandBufferBeginInfo{};
commandBufferBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
commandBufferBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
result = vkBeginCommandBufferFn(commandBuffer, &commandBufferBeginInfo);
if (result != VK_SUCCESS) {
fail(format("vkBeginCommandBuffer failed (VkResult = {})", static_cast<Int>(result)));
return;
}
vkCmdBindPipelineFn(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
vkCmdBindDescriptorSetsFn(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1,
&descriptorSet, 0, nullptr);
vkCmdDispatchFn(commandBuffer, 1, 1, 1);
VkBufferMemoryBarrier hostReadBarrier{};
hostReadBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER;
hostReadBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
hostReadBarrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
hostReadBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
hostReadBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
hostReadBarrier.buffer = outputBuffer;
hostReadBarrier.offset = 0;
hostReadBarrier.size = sizeof(IterationRPWitnessOutput);
vkCmdPipelineBarrierFn(commandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0,
0, nullptr, 1, &hostReadBarrier, 0, nullptr);
result = vkEndCommandBufferFn(commandBuffer);
if (result != VK_SUCCESS) {
fail(format("vkEndCommandBuffer failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
result = vkCreateFenceFn(device, &fenceInfo, nullptr, &fence);
if (result != VK_SUCCESS) {
fail(format("vkCreateFence failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
result = vkQueueSubmitFn(queue, 1, &submitInfo, fence);
if (result != VK_SUCCESS) {
fail(format("vkQueueSubmit failed (VkResult = {})", static_cast<Int>(result)));
return;
}
constexpr Uint64 FenceTimeoutNs = 5'000'000'000ull;
result = vkWaitForFencesFn(device, 1, &fence, VK_TRUE, FenceTimeoutNs);
if (result != VK_SUCCESS) {
fenceWaitTimedOut = true;
fail(format("vkWaitForFences did not signal within 5 s (VkResult = {})", static_cast<Int>(result)));
return;
}
IterationRPWitnessOutput output{};
std::memcpy(&output, mappedOutput, sizeof(output));
const IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(output);
if (!validation.ok) {
fail(validation.detail);
return;
}
builder.Pass(RowName, validation.detail);
}
// Everything the "MobileGL reported ..." rows need from the Vulkan device probe.
struct VulkanProbeSummary {
Bool devicePropsValid = false;
@@ -1461,6 +1898,8 @@ namespace MobileGL::MG_Util::SelfTest {
Bool shaderSubgroupUsable = false;
Bool timerQueriesSupported = false;
Bool samplerAnisotropySupported = false;
Bool drawIndirectFirstInstanceSupported = false;
Bool shaderDrawParametersSupported = false;
};
} // namespace
@@ -1665,6 +2104,7 @@ namespace MobileGL::MG_Util::SelfTest {
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
Uint32 graphicsQueueFamilyIndex = 0;
Uint32 graphicsQueueTimestampValidBits = 0;
Uint32 computeQueueFamilyIndex = std::numeric_limits<Uint32>::max();
for (VkPhysicalDevice candidate : devices) {
Uint32 queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyPropertiesFn(candidate, &queueFamilyCount, nullptr);
@@ -1680,6 +2120,13 @@ namespace MobileGL::MG_Util::SelfTest {
}
}
if (physicalDevice != VK_NULL_HANDLE) {
for (Uint32 familyIndex = 0; familyIndex < queueFamilyCount; ++familyIndex) {
const VkQueueFamilyProperties& family = queueFamilies[familyIndex];
if (family.queueCount > 0 && (family.queueFlags & VK_QUEUE_COMPUTE_BIT) != 0) {
computeQueueFamilyIndex = familyIndex;
break;
}
}
break;
}
}
@@ -1744,6 +2191,7 @@ namespace MobileGL::MG_Util::SelfTest {
VkPhysicalDeviceFeatures features{};
vkGetPhysicalDeviceFeaturesFn(physicalDevice, &features);
summary.samplerAnisotropySupported = features.samplerAnisotropy == VK_TRUE;
summary.drawIndirectFirstInstanceSupported = features.drawIndirectFirstInstance == VK_TRUE;
if (features.multiDrawIndirect == VK_TRUE) {
builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands");
} else {
@@ -1910,6 +2358,7 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Warn("shaderDrawParameters",
"unavailable; shaders using gl_DrawID/gl_BaseInstance will not work");
}
summary.shaderDrawParametersSupported = shaderDrawParameters;
Bool provokingVertexLast = false;
Bool transformFeedbackPreservesProvokingVertex = false;
@@ -2014,13 +2463,15 @@ namespace MobileGL::MG_Util::SelfTest {
"change every N instances change every one");
}
VkPhysicalDeviceSubgroupProperties subgroupProperties{};
Bool subgroupPropertiesAvailable = false;
if (vkGetPhysicalDeviceProperties2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
VkPhysicalDeviceSubgroupProperties subgroupProperties{};
subgroupProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES;
VkPhysicalDeviceProperties2 properties2{};
properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
properties2.pNext = &subgroupProperties;
vkGetPhysicalDeviceProperties2Fn(physicalDevice, &properties2);
subgroupPropertiesAvailable = true;
const Bool subgroupUsable = subgroupProperties.subgroupSize > 0 &&
(subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 &&
(subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0;
@@ -2040,6 +2491,9 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Warn("Compute shader subgroup", "subgroup properties could not be queried");
}
ProbeVulkanIterationRPWitness(builder, getInstanceProcAddr, instance, physicalDevice, computeQueueFamilyIndex,
properties, subgroupPropertiesAvailable, subgroupProperties);
if (HasVkExtension(deviceExtensions, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME)) {
builder.Pass("VK_KHR_draw_indirect_count",
"supported (count-buffer indirect draws run as single native "
@@ -2108,7 +2562,8 @@ namespace MobileGL::MG_Util::SelfTest {
backendApiVersionString = MG_Backend::DirectVulkan::FormatBackendAPIVersionString(
summary.deviceName, summary.apiVersionString, summary.driverVersionString);
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(
summary.shaderSubgroupUsable, summary.timerQueriesSupported, summary.samplerAnisotropySupported));
summary.shaderSubgroupUsable, summary.timerQueriesSupported, summary.samplerAnisotropySupported,
summary.drawIndirectFirstInstanceSupported && summary.shaderDrawParametersSupported));
}
AppendMobileGLReportedRows(builder, MG_Backend::DirectVulkan::GetRendererIdentity(), backendApiVersionString,
advertisedExtensions);
@@ -0,0 +1,164 @@
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.comp
// 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
//
// Native Vulkan GLSL 450 witness for iterationRP's first subgroup reduction.
// It is intentionally independent of the GL 430 integration scenario. The body
// below preserves iterationRP's source reduction; the surrounding diagnostics
// only observe its topology and cache handoffs.
#version 450
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
const uint kTopologyNonuniformNumSubgroups = 1u << 0u;
const uint kTopologyInvalidNumSubgroups = 1u << 1u;
const uint kTopologyInvalidSubgroupId = 1u << 2u;
const uint kTopologyInvalidSubgroupLane = 1u << 3u;
const uint kWitnessMagic = 0x50323033u;
layout(std430, set = 0, binding = 0) buffer IterationRPWitnessOutput {
uint magic;
uint topologyFlags;
uint numSubgroups;
uint loopLength;
uint seenSubgroupMask;
uvec4 owner511;
uint lastLaneWriterCount[32];
uint indexedInputTotal[32];
vec2 rawPrefix[32];
vec2 scanCache[6][32];
vec2 finalAverage;
} outWitness;
// iterationRP's cache stays separate from all diagnostic shared state. In
// particular, no instrumentation stores through prefixSumCache except source
// writes retained below.
shared vec2 prefixSumCache[32];
shared uint canonicalNumSubgroups;
shared uint topologyFlagsShared;
shared uint seenSubgroupMaskShared;
shared uint lastLaneWriterCountShared[32];
shared uint indexedInputTotalShared[32];
void main() {
const uint localInvocationIndex = gl_LocalInvocationIndex;
// Host memory is deliberately poisoned before dispatch. Initialize only
// shared atomic diagnostic state; owner, average, and magic remain poisoned
// until their required post-source-barrier writes below.
if (localInvocationIndex == 0u) {
canonicalNumSubgroups = 0u;
topologyFlagsShared = 0u;
seenSubgroupMaskShared = 0u;
}
if (localInvocationIndex < 32u) {
lastLaneWriterCountShared[localInvocationIndex] = 0u;
indexedInputTotalShared[localInvocationIndex] = 0u;
}
memoryBarrierShared();
barrier();
// Invocation zero defines the canonical domain. It is broadcast through
// shared memory before every invocation records its own raw observation.
if (localInvocationIndex == 0u) {
canonicalNumSubgroups = gl_NumSubgroups;
outWitness.numSubgroups = gl_NumSubgroups;
}
barrier();
const uint canonicalN = canonicalNumSubgroups;
if (gl_NumSubgroups != canonicalN)
atomicOr(topologyFlagsShared, kTopologyNonuniformNumSubgroups);
if (gl_NumSubgroups < 2u || gl_NumSubgroups > 32u)
atomicOr(topologyFlagsShared, kTopologyInvalidNumSubgroups);
if (gl_SubgroupID >= canonicalN || gl_SubgroupID >= 32u)
atomicOr(topologyFlagsShared, kTopologyInvalidSubgroupId);
if (gl_SubgroupSize == 0u || gl_SubgroupInvocationID >= gl_SubgroupSize)
atomicOr(topologyFlagsShared, kTopologyInvalidSubgroupLane);
// Keep all atomic collection bounded by the canonical valid domain. A
// nonuniform/broken report reaches the uniform safety branch below instead
// of making some lanes return before a barrier.
const bool canonicalDomain = canonicalN >= 2u && canonicalN <= 32u;
const bool idInCanonicalDomain = canonicalDomain && gl_SubgroupID < canonicalN;
if (idInCanonicalDomain) {
atomicOr(seenSubgroupMaskShared, 1u << gl_SubgroupID);
atomicAdd(indexedInputTotalShared[gl_SubgroupID], localInvocationIndex + 1u);
if (gl_SubgroupSize != 0u && gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
atomicAdd(lastLaneWriterCountShared[gl_SubgroupID], 1u);
}
memoryBarrierShared();
barrier();
if (localInvocationIndex == 0u)
outWitness.seenSubgroupMask = seenSubgroupMaskShared;
if (localInvocationIndex < 32u) {
outWitness.lastLaneWriterCount[localInvocationIndex] = lastLaneWriterCountShared[localInvocationIndex];
outWitness.indexedInputTotal[localInvocationIndex] = indexedInputTotalShared[localInvocationIndex];
}
// This branch is uniform after collection and is solely a safety guard for
// broken topology reports. The valid side retains iterationRP verbatim.
const bool sourceDomain = canonicalDomain && topologyFlagsShared == 0u;
if (sourceDomain) {
vec2 sampleLuminance = vec2(float(gl_LocalInvocationIndex + 1u), 0.0);
sampleLuminance = subgroupInclusiveAdd(sampleLuminance);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
barrier();
if (gl_LocalInvocationIndex < gl_NumSubgroups)
outWitness.rawPrefix[gl_LocalInvocationIndex] = prefixSumCache[gl_LocalInvocationIndex];
barrier();
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
if (gl_LocalInvocationIndex == 0u)
outWitness.loopLength = loopLength;
for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) {
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
sampleLuminance += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
}
barrier();
if (gl_LocalInvocationIndex < gl_NumSubgroups)
outWitness.scanCache[scanStage][gl_LocalInvocationIndex] =
prefixSumCache[gl_LocalInvocationIndex];
// A second, diagnostic-only barrier prevents a faster invocation
// from entering the next source stage while another reads this cache.
barrier();
}
if (gl_LocalInvocationIndex == 511u)
prefixSumCache[0] = sampleLuminance / 512.0;
barrier();
if (gl_LocalInvocationIndex == 511u) {
outWitness.owner511 = uvec4(gl_SubgroupSize, gl_NumSubgroups, gl_SubgroupID,
gl_SubgroupInvocationID);
outWitness.finalAverage = prefixSumCache[0];
}
}
// Both sides of the uniform branch reach this barrier. The magic is the
// completion latch and therefore cannot be written before the final barrier.
barrier();
if (localInvocationIndex == 0u) {
outWitness.topologyFlags = topologyFlagsShared;
outWitness.magic = kWitnessMagic;
}
}
@@ -0,0 +1,277 @@
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "DriverPostIterationRPWitness.h"
#include <bit>
#include <sstream>
#include <utility>
#include <vector>
namespace MobileGL::MG_Util::SelfTest {
namespace {
[[nodiscard]] IterationRPWitnessValidationResult Failure(IterationRPWitnessValidationFailure failure,
std::string detail,
std::uint32_t scanStage = 0u,
std::uint32_t subgroup = 0u) {
IterationRPWitnessValidationResult result;
result.ok = false;
result.failure = failure;
result.scanStage = scanStage;
result.subgroup = subgroup;
result.detail = std::move(detail);
return result;
}
[[nodiscard]] std::uint32_t FloatBits(float value) {
return std::bit_cast<std::uint32_t>(value);
}
[[nodiscard]] bool SameBits(float lhs, float rhs) {
return FloatBits(lhs) == FloatBits(rhs);
}
[[nodiscard]] bool SameBits(const IterationRPWitnessVec2& lhs, const IterationRPWitnessVec2& rhs) {
return SameBits(lhs.x, rhs.x) && SameBits(lhs.y, rhs.y);
}
[[nodiscard]] std::string Vec2String(const IterationRPWitnessVec2& value) {
std::ostringstream output;
output << '(' << value.x << ',' << value.y << ')';
return output.str();
}
[[nodiscard]] std::uint32_t ExpectedSeenSubgroupMask(std::uint32_t numSubgroups) {
return numSubgroups == kIterationRPWitnessMaxSubgroups ? 0xffffffffu : (1u << numSubgroups) - 1u;
}
[[nodiscard]] std::string JoinRequirements(const std::vector<std::string>& requirements) {
std::ostringstream output;
for (std::size_t i = 0; i < requirements.size(); ++i) {
if (i != 0u) output << "; ";
output << requirements[i];
}
return output.str();
}
} // namespace
IterationRPWitnessEligibilityResult
EvaluateIterationRPWitnessEligibility(const IterationRPWitnessLimits& limits) {
// This classification deliberately precedes numeric limits. An absent native
// compute/basic/arithmetic subgroup contract means there is nothing to witness,
// whereas every resource/entry-point failure on a capable device is a POST FAIL.
if (!limits.computeStageSupported || !limits.basicSubgroupSupported || !limits.arithmeticSubgroupSupported) {
std::vector<std::string> missing;
if (!limits.computeStageSupported) missing.emplace_back("VK_SHADER_STAGE_COMPUTE_BIT");
if (!limits.basicSubgroupSupported) missing.emplace_back("VK_SUBGROUP_FEATURE_BASIC_BIT");
if (!limits.arithmeticSubgroupSupported) missing.emplace_back("VK_SUBGROUP_FEATURE_ARITHMETIC_BIT");
return {IterationRPWitnessEligibility::SkipUnsupportedNativeFeatureSet,
"skipped because the native compute/basic/arithmetic subgroup feature set is unsupported (missing " +
JoinRequirements(missing) + ')'};
}
std::vector<std::string> inadequate;
if (limits.subgroupSize == 0u) {
inadequate.emplace_back("subgroupSize == 0");
}
if (limits.maxComputeWorkGroupInvocations < kIterationRPWitnessInvocationCount) {
inadequate.emplace_back("maxComputeWorkGroupInvocations < 512");
}
if (limits.maxComputeWorkGroupSize[0] < 32u || limits.maxComputeWorkGroupSize[1] < 16u ||
limits.maxComputeWorkGroupSize[2] < 1u) {
inadequate.emplace_back("maxComputeWorkGroupSize does not cover 32x16x1");
}
if (limits.maxComputeSharedMemorySize < kIterationRPWitnessSharedMemoryBytes) {
inadequate.emplace_back("maxComputeSharedMemorySize < " +
std::to_string(kIterationRPWitnessSharedMemoryBytes));
}
if (limits.maxPerStageDescriptorStorageBuffers < 1u) {
inadequate.emplace_back("maxPerStageDescriptorStorageBuffers < 1");
}
if (limits.maxDescriptorSetStorageBuffers < 1u) {
inadequate.emplace_back("maxDescriptorSetStorageBuffers < 1");
}
if (limits.maxBoundDescriptorSets < 1u) {
inadequate.emplace_back("maxBoundDescriptorSets < 1");
}
if (limits.maxStorageBufferRange < sizeof(IterationRPWitnessOutput)) {
inadequate.emplace_back("maxStorageBufferRange < " +
std::to_string(sizeof(IterationRPWitnessOutput)));
}
if (!inadequate.empty()) {
return {IterationRPWitnessEligibility::FailInadequateLimits,
"insufficient Vulkan limits for a 32x16x1 workgroup, one output SSBO, and " +
std::to_string(kIterationRPWitnessSharedMemoryBytes) + " bytes of shared memory: " +
JoinRequirements(inadequate)};
}
return {IterationRPWitnessEligibility::Execute, {}};
}
std::uint32_t ComputeIterationRPWitnessLoopLength(std::uint32_t numSubgroups) {
if (numSubgroups < 2u || numSubgroups > kIterationRPWitnessMaxSubgroups) return 0u;
// Exact C++ spelling of the source's findMSB-based calculation. In
// particular, its final iteration for powers of two is intentional.
std::uint32_t loopLength = 0u;
for (std::uint32_t value = numSubgroups; value > 1u; value >>= 1u) {
++loopLength;
}
loopLength += static_cast<std::uint32_t>(numSubgroups - (1u << (loopLength - 1u)) > 0u);
return loopLength;
}
IterationRPWitnessValidationResult ValidateIterationRPWitness(const IterationRPWitnessOutput& output) {
// 1. Completion. A poisoned or unwritten result must never turn into a
// topology diagnosis, because it says nothing about execution.
if (output.magic != kIterationRPWitnessMagic) {
std::ostringstream detail;
detail << "completion: magic was 0x" << std::hex << output.magic << ", expected 0x"
<< kIterationRPWitnessMagic;
return Failure(IterationRPWitnessValidationFailure::Completion, detail.str());
}
// 2. Observed topology. All checks consume observations written by the
// shader, rather than inferring subgroup layout from invocation indices.
const std::uint32_t numSubgroups = output.numSubgroups;
if (numSubgroups < 2u || numSubgroups > kIterationRPWitnessMaxSubgroups) {
std::ostringstream detail;
detail << "topology: canonical gl_NumSubgroups=" << numSubgroups << " is outside [2, 32]";
return Failure(IterationRPWitnessValidationFailure::Topology, detail.str());
}
if ((output.topologyFlags & IterationRPWitnessNonuniformNumSubgroups) != 0u) {
return Failure(IterationRPWitnessValidationFailure::Topology,
"topology: gl_NumSubgroups differed across workgroup");
}
if ((output.topologyFlags & IterationRPWitnessInvalidNumSubgroups) != 0u) {
return Failure(IterationRPWitnessValidationFailure::Topology,
"topology: an invocation reported gl_NumSubgroups outside [2, 32]");
}
if ((output.topologyFlags & IterationRPWitnessInvalidSubgroupId) != 0u) {
return Failure(IterationRPWitnessValidationFailure::Topology,
"topology: an invocation reported an invalid gl_SubgroupID");
}
if ((output.topologyFlags & IterationRPWitnessInvalidSubgroupLane) != 0u) {
return Failure(IterationRPWitnessValidationFailure::Topology,
"topology: an invocation reported an invalid subgroup lane");
}
if ((output.topologyFlags & ~(IterationRPWitnessNonuniformNumSubgroups |
IterationRPWitnessInvalidNumSubgroups |
IterationRPWitnessInvalidSubgroupId |
IterationRPWitnessInvalidSubgroupLane)) != 0u) {
std::ostringstream detail;
detail << "topology: unknown topology flags 0x" << std::hex << output.topologyFlags;
return Failure(IterationRPWitnessValidationFailure::Topology, detail.str());
}
const std::uint32_t expectedMask = ExpectedSeenSubgroupMask(numSubgroups);
if (output.seenSubgroupMask != expectedMask) {
std::ostringstream detail;
detail << "topology: seen subgroup-ID mask was 0x" << std::hex << output.seenSubgroupMask
<< ", expected 0x" << expectedMask;
return Failure(IterationRPWitnessValidationFailure::Topology, detail.str());
}
const std::uint32_t expectedLoopLength = ComputeIterationRPWitnessLoopLength(numSubgroups);
if (output.loopLength != expectedLoopLength) {
std::ostringstream detail;
detail << "topology: loopLength was " << std::dec << output.loopLength << ", expected "
<< expectedLoopLength;
return Failure(IterationRPWitnessValidationFailure::Topology, detail.str());
}
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
if (output.lastLaneWriterCount[subgroup] != 1u) {
std::ostringstream detail;
detail << "topology: subgroup " << subgroup << " has "
<< output.lastLaneWriterCount[subgroup] << " source last-lane writers, expected exactly 1";
return Failure(IterationRPWitnessValidationFailure::Topology, detail.str(), 0u, subgroup);
}
}
if (output.owner511.y != numSubgroups) {
std::ostringstream detail;
detail << "final owner: invocation 511 reported gl_NumSubgroups=" << output.owner511.y << ", expected "
<< numSubgroups;
return Failure(IterationRPWitnessValidationFailure::FinalOwner, detail.str());
}
if (output.owner511.z != numSubgroups - 1u) {
std::ostringstream detail;
detail << "final owner: invocation 511 is not in the highest subgroup (id" << output.owner511.z
<< ", expected id" << (numSubgroups - 1u) << ')';
return Failure(IterationRPWitnessValidationFailure::FinalOwner, detail.str());
}
if (output.owner511.x == 0u || output.owner511.w != output.owner511.x - 1u) {
std::ostringstream detail;
detail << "final owner: invocation 511 is not the last lane of highest subgroup (size "
<< output.owner511.x << ", lane " << output.owner511.w << ')';
return Failure(IterationRPWitnessValidationFailure::FinalOwner, detail.str());
}
// 3. Initial subgroup handoff. The atomic scalar totals are independent
// of subgroupInclusiveAdd; their sum and the cache values establish that
// the final lanes handed off the native vector inclusive-add results.
std::uint64_t indexedTotal = 0u;
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
indexedTotal += output.indexedInputTotal[subgroup];
}
if (indexedTotal != 131328u) {
std::ostringstream detail;
detail << "initial subgroup handoff: indexed input total was " << indexedTotal << ", expected 131328";
return Failure(IterationRPWitnessValidationFailure::InitialSubgroupHandoff, detail.str());
}
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
const IterationRPWitnessVec2 expected = {static_cast<float>(output.indexedInputTotal[subgroup]), 0.0f};
if (!SameBits(output.rawPrefix[subgroup], expected)) {
std::ostringstream detail;
detail << "initial subgroup handoff: subgroup " << subgroup << " rawPrefix observed "
<< Vec2String(output.rawPrefix[subgroup]) << ", expected " << Vec2String(expected);
return Failure(IterationRPWitnessValidationFailure::InitialSubgroupHandoff, detail.str(), 0u,
subgroup);
}
}
// 4. Source scan. Do not substitute a conventional scan: this reproduces
// the source cache index expression and stage ordering word for word.
std::array<IterationRPWitnessVec2, kIterationRPWitnessMaxSubgroups> expectedCache = output.rawPrefix;
for (std::uint32_t scanStage = 0u; scanStage < expectedLoopLength; ++scanStage) {
auto cacheAfterStage = expectedCache;
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
if ((subgroup & (1u << scanStage)) > 0u) {
const std::uint32_t sourceCacheIndex = (subgroup >> scanStage << scanStage) - 1u;
cacheAfterStage[subgroup].x += expectedCache[sourceCacheIndex].x;
cacheAfterStage[subgroup].y += expectedCache[sourceCacheIndex].y;
}
}
expectedCache = cacheAfterStage;
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
if (!SameBits(output.scanCache[scanStage][subgroup], expectedCache[subgroup])) {
std::ostringstream detail;
detail << "source scan stage " << scanStage << ", subgroup " << subgroup << ": observed "
<< Vec2String(output.scanCache[scanStage][subgroup]) << ", expected "
<< Vec2String(expectedCache[subgroup]);
return Failure(IterationRPWitnessValidationFailure::SourceScan, detail.str(), scanStage, subgroup);
}
}
}
// 5. The owner contract was checked above with the other topology facts;
// this final result remains a separate exact-vector check.
const IterationRPWitnessVec2 expectedAverage = {256.5f, 0.0f};
if (!SameBits(output.finalAverage, expectedAverage)) {
std::ostringstream detail;
detail << "final average: observed " << Vec2String(output.finalAverage) << ", expected "
<< Vec2String(expectedAverage);
return Failure(IterationRPWitnessValidationFailure::FinalAverage, detail.str());
}
std::ostringstream detail;
detail << "N=" << numSubgroups << ", owner511=id" << output.owner511.z << "/lane" << output.owner511.w
<< ", " << expectedLoopLength << " scan stages, average=" << Vec2String(output.finalAverage);
IterationRPWitnessValidationResult result;
result.ok = true;
result.failure = IterationRPWitnessValidationFailure::None;
result.detail = detail.str();
return result;
}
} // namespace MobileGL::MG_Util::SelfTest
@@ -0,0 +1,153 @@
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.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
//
// Compact, native-Vulkan iterationRP first-reduction witness ABI and its pure
// validator. The types below deliberately mirror DriverPostIterationRPWitness.comp's
// single std430 storage block; changing either side requires updating the static
// layout assertions here.
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <type_traits>
namespace MobileGL::MG_Util::SelfTest {
// "P203": the pack's trace program id, kept stable so the checked-in witness
// SPIR-V (DriverPostIterationRPWitnessSpv.h) needs no regeneration.
constexpr std::uint32_t kIterationRPWitnessMagic = 0x50323033u;
constexpr std::uint32_t kIterationRPWitnessInvocationCount = 512u;
constexpr std::uint32_t kIterationRPWitnessMaxSubgroups = 32u;
constexpr std::uint32_t kIterationRPWitnessMaxScanStages = 6u;
// These bit values are shared with the GLSL source. They document failures in
// topology observations rather than guessing a topology from local IDs on the host.
enum IterationRPWitnessTopologyFlag : std::uint32_t {
IterationRPWitnessNonuniformNumSubgroups = 1u << 0u,
IterationRPWitnessInvalidNumSubgroups = 1u << 1u,
IterationRPWitnessInvalidSubgroupId = 1u << 2u,
IterationRPWitnessInvalidSubgroupLane = 1u << 3u,
};
struct alignas(8) IterationRPWitnessVec2 {
float x;
float y;
};
struct alignas(16) IterationRPWitnessUVec4 {
std::uint32_t x;
std::uint32_t y;
std::uint32_t z;
std::uint32_t w;
};
// std430 layout of DriverPostIterationRPWitness.comp's IterationRPWitnessOutput block.
struct alignas(16) IterationRPWitnessOutput {
std::uint32_t magic;
std::uint32_t topologyFlags;
std::uint32_t numSubgroups;
std::uint32_t loopLength;
std::uint32_t seenSubgroupMask;
IterationRPWitnessUVec4 owner511;
std::array<std::uint32_t, kIterationRPWitnessMaxSubgroups> lastLaneWriterCount;
std::array<std::uint32_t, kIterationRPWitnessMaxSubgroups> indexedInputTotal;
std::array<IterationRPWitnessVec2, kIterationRPWitnessMaxSubgroups> rawPrefix;
std::array<std::array<IterationRPWitnessVec2, kIterationRPWitnessMaxSubgroups>,
kIterationRPWitnessMaxScanStages>
scanCache;
IterationRPWitnessVec2 finalAverage;
};
static_assert(std::is_standard_layout_v<IterationRPWitnessVec2>);
static_assert(std::is_standard_layout_v<IterationRPWitnessUVec4>);
static_assert(std::is_standard_layout_v<IterationRPWitnessOutput>);
static_assert(sizeof(IterationRPWitnessVec2) == 8u);
static_assert(alignof(IterationRPWitnessVec2) == 8u);
static_assert(sizeof(IterationRPWitnessUVec4) == 16u);
static_assert(alignof(IterationRPWitnessUVec4) == 16u);
static_assert(offsetof(IterationRPWitnessOutput, magic) == 0u);
static_assert(offsetof(IterationRPWitnessOutput, topologyFlags) == 4u);
static_assert(offsetof(IterationRPWitnessOutput, numSubgroups) == 8u);
static_assert(offsetof(IterationRPWitnessOutput, loopLength) == 12u);
static_assert(offsetof(IterationRPWitnessOutput, seenSubgroupMask) == 16u);
static_assert(offsetof(IterationRPWitnessOutput, owner511) == 32u);
static_assert(offsetof(IterationRPWitnessOutput, lastLaneWriterCount) == 48u);
static_assert(offsetof(IterationRPWitnessOutput, indexedInputTotal) == 176u);
static_assert(offsetof(IterationRPWitnessOutput, rawPrefix) == 304u);
static_assert(offsetof(IterationRPWitnessOutput, scanCache) == 560u);
static_assert(offsetof(IterationRPWitnessOutput, finalAverage) == 2096u);
static_assert(sizeof(IterationRPWitnessOutput) == 2112u);
// The witness uses prefixSumCache[32], three scalar shared diagnostics, and
// two 32-entry scalar diagnostic arrays in the GLSL source. Keep this
// independent of the output SSBO size.
constexpr std::uint32_t kIterationRPWitnessSharedMemoryBytes =
kIterationRPWitnessMaxSubgroups * sizeof(IterationRPWitnessVec2) +
3u * sizeof(std::uint32_t) +
2u * kIterationRPWitnessMaxSubgroups * sizeof(std::uint32_t);
enum class IterationRPWitnessEligibility {
Execute,
SkipUnsupportedNativeFeatureSet,
FailInadequateLimits,
};
// The raw physical-device conditions needed by the native witness. This is
// intentionally distinct from MobileGL's advertised-extension policy.
struct IterationRPWitnessLimits {
bool computeStageSupported = false;
bool basicSubgroupSupported = false;
bool arithmeticSubgroupSupported = false;
std::uint32_t subgroupSize = 0u;
std::uint32_t maxComputeWorkGroupInvocations = 0u;
std::array<std::uint32_t, 3> maxComputeWorkGroupSize{};
std::uint32_t maxComputeSharedMemorySize = 0u;
std::uint32_t maxPerStageDescriptorStorageBuffers = 0u;
std::uint32_t maxDescriptorSetStorageBuffers = 0u;
std::uint32_t maxBoundDescriptorSets = 0u;
std::uint64_t maxStorageBufferRange = 0u;
};
struct IterationRPWitnessEligibilityResult {
IterationRPWitnessEligibility eligibility = IterationRPWitnessEligibility::FailInadequateLimits;
std::string detail;
};
enum class IterationRPWitnessValidationFailure {
None,
Completion,
Topology,
InitialSubgroupHandoff,
SourceScan,
FinalOwner,
FinalAverage,
};
struct IterationRPWitnessValidationResult {
bool ok = false;
IterationRPWitnessValidationFailure failure = IterationRPWitnessValidationFailure::Completion;
std::uint32_t scanStage = 0u;
std::uint32_t subgroup = 0u;
std::string detail;
};
[[nodiscard]] IterationRPWitnessEligibilityResult
EvaluateIterationRPWitnessEligibility(const IterationRPWitnessLimits& limits);
// Mirrors the source's findMSB expression for valid N in [2, 32].
[[nodiscard]] std::uint32_t ComputeIterationRPWitnessLoopLength(std::uint32_t numSubgroups);
[[nodiscard]] IterationRPWitnessValidationResult
ValidateIterationRPWitness(const IterationRPWitnessOutput& output);
} // namespace MobileGL::MG_Util::SelfTest
@@ -0,0 +1,297 @@
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitnessSpv.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
//
// Generated from DriverPostIterationRPWitness.comp with:
// glslangValidator --target-env vulkan1.1 -V DriverPostIterationRPWitness.comp
// Validated with spirv-val --target-env vulkan1.1. Do not edit words by hand.
//
// The stored words predate the Program203 -> IterationRP source rename, so their
// embedded OpName debug strings still spell the old identifiers; regeneration from
// the renamed source produces semantically identical code differing only in those
// strings. The witness magic stays 0x50323033 ("P203" - the trace's program id) so
// these words remain valid without regeneration.
#pragma once
#include <cstddef>
#include <cstdint>
namespace MobileGL::MG_Util::SelfTest {
inline constexpr std::uint32_t kDriverPostIterationRPWitnessSpv[] = {
0x07230203u, 0x00010300u, 0x0008000bu, 0x00000145u, 0x00000000u, 0x00020011u, 0x00000001u, 0x00020011u,
0x0000003du, 0x00020011u, 0x0000003fu, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x000a000fu, 0x00000005u, 0x00000004u, 0x6e69616du,
0x00000000u, 0x0000000au, 0x0000002au, 0x00000050u, 0x0000005eu, 0x00000064u, 0x00060010u, 0x00000004u,
0x00000011u, 0x00000020u, 0x00000010u, 0x00000001u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x000a0004u,
0x4b5f4c47u, 0x735f5248u, 0x65646168u, 0x75735f72u, 0x6f726762u, 0x615f7075u, 0x68746972u, 0x6974656du,
0x00000063u, 0x00090004u, 0x4b5f4c47u, 0x735f5248u, 0x65646168u, 0x75735f72u, 0x6f726762u, 0x625f7075u,
0x63697361u, 0x00000000u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00080005u, 0x00000008u,
0x61636f6cu, 0x766e496cu, 0x7461636fu, 0x496e6f69u, 0x7865646eu, 0x00000000u, 0x00080005u, 0x0000000au,
0x4c5f6c67u, 0x6c61636fu, 0x6f766e49u, 0x69746163u, 0x6e496e6fu, 0x00786564u, 0x00080005u, 0x00000013u,
0x6f6e6163u, 0x6163696eu, 0x6d754e6cu, 0x67627553u, 0x70756f72u, 0x00000073u, 0x00070005u, 0x00000014u,
0x6f706f74u, 0x79676f6cu, 0x67616c46u, 0x61685373u, 0x00646572u, 0x00080005u, 0x00000015u, 0x6e656573u,
0x67627553u, 0x70756f72u, 0x6b73614du, 0x72616853u, 0x00006465u, 0x00090005u, 0x0000001du, 0x7473616cu,
0x656e614cu, 0x74697257u, 0x6f437265u, 0x53746e75u, 0x65726168u, 0x00000064u, 0x00080005u, 0x00000020u,
0x65646e69u, 0x49646578u, 0x7475706eu, 0x61746f54u, 0x6168536cu, 0x00646572u, 0x00060005u, 0x0000002au,
0x4e5f6c67u, 0x75536d75u, 0x6f726762u, 0x00737075u, 0x00080005u, 0x00000035u, 0x676f7250u, 0x326d6172u,
0x69573330u, 0x73656e74u, 0x74754f73u, 0x00747570u, 0x00050006u, 0x00000035u, 0x00000000u, 0x6967616du,
0x00000063u, 0x00070006u, 0x00000035u, 0x00000001u, 0x6f706f74u, 0x79676f6cu, 0x67616c46u, 0x00000073u,
0x00070006u, 0x00000035u, 0x00000002u, 0x536d756eu, 0x72676275u, 0x7370756fu, 0x00000000u, 0x00060006u,
0x00000035u, 0x00000003u, 0x706f6f6cu, 0x676e654cu, 0x00006874u, 0x00080006u, 0x00000035u, 0x00000004u,
0x6e656573u, 0x67627553u, 0x70756f72u, 0x6b73614du, 0x00000000u, 0x00060006u, 0x00000035u, 0x00000005u,
0x656e776fu, 0x31313572u, 0x00000000u, 0x00080006u, 0x00000035u, 0x00000006u, 0x7473616cu, 0x656e614cu,
0x74697257u, 0x6f437265u, 0x00746e75u, 0x00080006u, 0x00000035u, 0x00000007u, 0x65646e69u, 0x49646578u,
0x7475706eu, 0x61746f54u, 0x0000006cu, 0x00060006u, 0x00000035u, 0x00000008u, 0x50776172u, 0x69666572u,
0x00000078u, 0x00060006u, 0x00000035u, 0x00000009u, 0x6e616373u, 0x68636143u, 0x00000065u, 0x00070006u,
0x00000035u, 0x0000000au, 0x616e6966u, 0x6576416cu, 0x65676172u, 0x00000000u, 0x00050005u, 0x00000037u,
0x5774756fu, 0x656e7469u, 0x00007373u, 0x00050005u, 0x0000003du, 0x6f6e6163u, 0x6163696eu, 0x00004e6cu,
0x00060005u, 0x00000050u, 0x535f6c67u, 0x72676275u, 0x4970756fu, 0x00000044u, 0x00060005u, 0x0000005eu,
0x535f6c67u, 0x72676275u, 0x5370756fu, 0x00657a69u, 0x00080005u, 0x00000064u, 0x535f6c67u, 0x72676275u,
0x4970756fu, 0x636f766eu, 0x6f697461u, 0x0044496eu, 0x00060005u, 0x0000006eu, 0x6f6e6163u, 0x6163696eu,
0x6d6f446cu, 0x006e6961u, 0x00070005u, 0x00000074u, 0x6e496469u, 0x6f6e6143u, 0x6163696eu, 0x6d6f446cu,
0x006e6961u, 0x00060005u, 0x000000acu, 0x72756f73u, 0x6f446563u, 0x6e69616du, 0x00000000u, 0x00060005u,
0x000000b7u, 0x706d6173u, 0x754c656cu, 0x616e696du, 0x0065636eu, 0x00060005u, 0x000000c8u, 0x66657270u,
0x75537869u, 0x6361436du, 0x00006568u, 0x00050005u, 0x000000d9u, 0x706f6f6cu, 0x676e654cu, 0x00006874u,
0x00050005u, 0x000000edu, 0x6e616373u, 0x67617453u, 0x00000065u, 0x00040047u, 0x0000000au, 0x0000000bu,
0x0000001du, 0x00040047u, 0x0000002au, 0x0000000bu, 0x00000026u, 0x00040047u, 0x0000002du, 0x00000006u,
0x00000004u, 0x00040047u, 0x0000002eu, 0x00000006u, 0x00000004u, 0x00040047u, 0x00000031u, 0x00000006u,
0x00000008u, 0x00040047u, 0x00000032u, 0x00000006u, 0x00000008u, 0x00040047u, 0x00000034u, 0x00000006u,
0x00000100u, 0x00030047u, 0x00000035u, 0x00000002u, 0x00050048u, 0x00000035u, 0x00000000u, 0x00000023u,
0x00000000u, 0x00050048u, 0x00000035u, 0x00000001u, 0x00000023u, 0x00000004u, 0x00050048u, 0x00000035u,
0x00000002u, 0x00000023u, 0x00000008u, 0x00050048u, 0x00000035u, 0x00000003u, 0x00000023u, 0x0000000cu,
0x00050048u, 0x00000035u, 0x00000004u, 0x00000023u, 0x00000010u, 0x00050048u, 0x00000035u, 0x00000005u,
0x00000023u, 0x00000020u, 0x00050048u, 0x00000035u, 0x00000006u, 0x00000023u, 0x00000030u, 0x00050048u,
0x00000035u, 0x00000007u, 0x00000023u, 0x000000b0u, 0x00050048u, 0x00000035u, 0x00000008u, 0x00000023u,
0x00000130u, 0x00050048u, 0x00000035u, 0x00000009u, 0x00000023u, 0x00000230u, 0x00050048u, 0x00000035u,
0x0000000au, 0x00000023u, 0x00000830u, 0x00040047u, 0x00000037u, 0x00000021u, 0x00000000u, 0x00040047u,
0x00000037u, 0x00000022u, 0x00000000u, 0x00040047u, 0x00000050u, 0x0000000bu, 0x00000028u, 0x00030047u,
0x0000005eu, 0x00000000u, 0x00040047u, 0x0000005eu, 0x0000000bu, 0x00000024u, 0x00030047u, 0x0000005fu,
0x00000000u, 0x00030047u, 0x00000064u, 0x00000000u, 0x00040047u, 0x00000064u, 0x0000000bu, 0x00000029u,
0x00030047u, 0x00000065u, 0x00000000u, 0x00030047u, 0x00000066u, 0x00000000u, 0x00030047u, 0x00000087u,
0x00000000u, 0x00030047u, 0x0000008bu, 0x00000000u, 0x00030047u, 0x0000008cu, 0x00000000u, 0x00030047u,
0x0000008du, 0x00000000u, 0x00030047u, 0x000000c0u, 0x00000000u, 0x00030047u, 0x000000c1u, 0x00000000u,
0x00030047u, 0x000000c2u, 0x00000000u, 0x00030047u, 0x00000107u, 0x00000000u, 0x00030047u, 0x00000108u,
0x00000000u, 0x00030047u, 0x00000109u, 0x00000000u, 0x00030047u, 0x0000012fu, 0x00000000u, 0x00030047u,
0x00000132u, 0x00000000u, 0x00040047u, 0x00000144u, 0x0000000bu, 0x00000019u, 0x00020013u, 0x00000002u,
0x00030021u, 0x00000003u, 0x00000002u, 0x00040015u, 0x00000006u, 0x00000020u, 0x00000000u, 0x00040020u,
0x00000007u, 0x00000007u, 0x00000006u, 0x00040020u, 0x00000009u, 0x00000001u, 0x00000006u, 0x0004003bu,
0x00000009u, 0x0000000au, 0x00000001u, 0x0004002bu, 0x00000006u, 0x0000000du, 0x00000000u, 0x00020014u,
0x0000000eu, 0x00040020u, 0x00000012u, 0x00000004u, 0x00000006u, 0x0004003bu, 0x00000012u, 0x00000013u,
0x00000004u, 0x0004003bu, 0x00000012u, 0x00000014u, 0x00000004u, 0x0004003bu, 0x00000012u, 0x00000015u,
0x00000004u, 0x0004002bu, 0x00000006u, 0x00000017u, 0x00000020u, 0x0004001cu, 0x0000001bu, 0x00000006u,
0x00000017u, 0x00040020u, 0x0000001cu, 0x00000004u, 0x0000001bu, 0x0004003bu, 0x0000001cu, 0x0000001du,
0x00000004u, 0x0004003bu, 0x0000001cu, 0x00000020u, 0x00000004u, 0x0004002bu, 0x00000006u, 0x00000023u,
0x00000001u, 0x0004002bu, 0x00000006u, 0x00000024u, 0x00000108u, 0x0004002bu, 0x00000006u, 0x00000025u,
0x00000002u, 0x0004003bu, 0x00000009u, 0x0000002au, 0x00000001u, 0x00040017u, 0x0000002cu, 0x00000006u,
0x00000004u, 0x0004001cu, 0x0000002du, 0x00000006u, 0x00000017u, 0x0004001cu, 0x0000002eu, 0x00000006u,
0x00000017u, 0x00030016u, 0x0000002fu, 0x00000020u, 0x00040017u, 0x00000030u, 0x0000002fu, 0x00000002u,
0x0004001cu, 0x00000031u, 0x00000030u, 0x00000017u, 0x0004001cu, 0x00000032u, 0x00000030u, 0x00000017u,
0x0004002bu, 0x00000006u, 0x00000033u, 0x00000006u, 0x0004001cu, 0x00000034u, 0x00000032u, 0x00000033u,
0x000d001eu, 0x00000035u, 0x00000006u, 0x00000006u, 0x00000006u, 0x00000006u, 0x00000006u, 0x0000002cu,
0x0000002du, 0x0000002eu, 0x00000031u, 0x00000034u, 0x00000030u, 0x00040020u, 0x00000036u, 0x0000000cu,
0x00000035u, 0x0004003bu, 0x00000036u, 0x00000037u, 0x0000000cu, 0x00040015u, 0x00000038u, 0x00000020u,
0x00000001u, 0x0004002bu, 0x00000038u, 0x00000039u, 0x00000002u, 0x00040020u, 0x0000003bu, 0x0000000cu,
0x00000006u, 0x0004003bu, 0x00000009u, 0x00000050u, 0x00000001u, 0x0004002bu, 0x00000006u, 0x0000005cu,
0x00000004u, 0x0004003bu, 0x00000009u, 0x0000005eu, 0x00000001u, 0x0004003bu, 0x00000009u, 0x00000064u,
0x00000001u, 0x0004002bu, 0x00000006u, 0x0000006bu, 0x00000008u, 0x00040020u, 0x0000006du, 0x00000007u,
0x0000000eu, 0x0004002bu, 0x00000038u, 0x00000099u, 0x00000004u, 0x0004002bu, 0x00000038u, 0x000000a0u,
0x00000006u, 0x0004002bu, 0x00000038u, 0x000000a6u, 0x00000007u, 0x00040020u, 0x000000b6u, 0x00000007u,
0x00000030u, 0x0004002bu, 0x0000002fu, 0x000000bbu, 0x00000000u, 0x0004002bu, 0x00000006u, 0x000000beu,
0x00000003u, 0x0004001cu, 0x000000c6u, 0x00000030u, 0x00000017u, 0x00040020u, 0x000000c7u, 0x00000004u,
0x000000c6u, 0x0004003bu, 0x000000c7u, 0x000000c8u, 0x00000004u, 0x00040020u, 0x000000cbu, 0x00000004u,
0x00000030u, 0x0004002bu, 0x00000038u, 0x000000d2u, 0x00000008u, 0x00040020u, 0x000000d7u, 0x0000000cu,
0x00000030u, 0x0004002bu, 0x00000038u, 0x000000eau, 0x00000003u, 0x0004002bu, 0x00000038u, 0x00000115u,
0x00000009u, 0x0004002bu, 0x00000038u, 0x0000011du, 0x00000001u, 0x0004002bu, 0x00000006u, 0x00000120u,
0x000001ffu, 0x0004002bu, 0x00000038u, 0x00000124u, 0x00000000u, 0x0004002bu, 0x0000002fu, 0x00000126u,
0x44000000u, 0x0004002bu, 0x00000038u, 0x0000012eu, 0x00000005u, 0x00040020u, 0x00000134u, 0x0000000cu,
0x0000002cu, 0x0004002bu, 0x00000038u, 0x00000136u, 0x0000000au, 0x0004002bu, 0x00000006u, 0x00000140u,
0x50323033u, 0x00040017u, 0x00000142u, 0x00000006u, 0x00000003u, 0x0004002bu, 0x00000006u, 0x00000143u,
0x00000010u, 0x0006002cu, 0x00000142u, 0x00000144u, 0x00000017u, 0x00000143u, 0x00000023u, 0x00050036u,
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003bu, 0x00000007u,
0x00000008u, 0x00000007u, 0x0004003bu, 0x00000007u, 0x0000003du, 0x00000007u, 0x0004003bu, 0x0000006du,
0x0000006eu, 0x00000007u, 0x0004003bu, 0x0000006du, 0x00000074u, 0x00000007u, 0x0004003bu, 0x0000006du,
0x000000acu, 0x00000007u, 0x0004003bu, 0x000000b6u, 0x000000b7u, 0x00000007u, 0x0004003bu, 0x00000007u,
0x000000d9u, 0x00000007u, 0x0004003bu, 0x00000007u, 0x000000edu, 0x00000007u, 0x0004003du, 0x00000006u,
0x0000000bu, 0x0000000au, 0x0003003eu, 0x00000008u, 0x0000000bu, 0x0004003du, 0x00000006u, 0x0000000cu,
0x00000008u, 0x000500aau, 0x0000000eu, 0x0000000fu, 0x0000000cu, 0x0000000du, 0x000300f7u, 0x00000011u,
0x00000000u, 0x000400fau, 0x0000000fu, 0x00000010u, 0x00000011u, 0x000200f8u, 0x00000010u, 0x0003003eu,
0x00000013u, 0x0000000du, 0x0003003eu, 0x00000014u, 0x0000000du, 0x0003003eu, 0x00000015u, 0x0000000du,
0x000200f9u, 0x00000011u, 0x000200f8u, 0x00000011u, 0x0004003du, 0x00000006u, 0x00000016u, 0x00000008u,
0x000500b0u, 0x0000000eu, 0x00000018u, 0x00000016u, 0x00000017u, 0x000300f7u, 0x0000001au, 0x00000000u,
0x000400fau, 0x00000018u, 0x00000019u, 0x0000001au, 0x000200f8u, 0x00000019u, 0x0004003du, 0x00000006u,
0x0000001eu, 0x00000008u, 0x00050041u, 0x00000012u, 0x0000001fu, 0x0000001du, 0x0000001eu, 0x0003003eu,
0x0000001fu, 0x0000000du, 0x0004003du, 0x00000006u, 0x00000021u, 0x00000008u, 0x00050041u, 0x00000012u,
0x00000022u, 0x00000020u, 0x00000021u, 0x0003003eu, 0x00000022u, 0x0000000du, 0x000200f9u, 0x0000001au,
0x000200f8u, 0x0000001au, 0x000300e1u, 0x00000023u, 0x00000024u, 0x000400e0u, 0x00000025u, 0x00000025u,
0x00000024u, 0x0004003du, 0x00000006u, 0x00000026u, 0x00000008u, 0x000500aau, 0x0000000eu, 0x00000027u,
0x00000026u, 0x0000000du, 0x000300f7u, 0x00000029u, 0x00000000u, 0x000400fau, 0x00000027u, 0x00000028u,
0x00000029u, 0x000200f8u, 0x00000028u, 0x0004003du, 0x00000006u, 0x0000002bu, 0x0000002au, 0x0003003eu,
0x00000013u, 0x0000002bu, 0x0004003du, 0x00000006u, 0x0000003au, 0x0000002au, 0x00050041u, 0x0000003bu,
0x0000003cu, 0x00000037u, 0x00000039u, 0x0003003eu, 0x0000003cu, 0x0000003au, 0x000200f9u, 0x00000029u,
0x000200f8u, 0x00000029u, 0x000400e0u, 0x00000025u, 0x00000025u, 0x00000024u, 0x0004003du, 0x00000006u,
0x0000003eu, 0x00000013u, 0x0003003eu, 0x0000003du, 0x0000003eu, 0x0004003du, 0x00000006u, 0x0000003fu,
0x0000002au, 0x0004003du, 0x00000006u, 0x00000040u, 0x0000003du, 0x000500abu, 0x0000000eu, 0x00000041u,
0x0000003fu, 0x00000040u, 0x000300f7u, 0x00000043u, 0x00000000u, 0x000400fau, 0x00000041u, 0x00000042u,
0x00000043u, 0x000200f8u, 0x00000042u, 0x000700f1u, 0x00000006u, 0x00000044u, 0x00000014u, 0x00000023u,
0x0000000du, 0x00000023u, 0x000200f9u, 0x00000043u, 0x000200f8u, 0x00000043u, 0x0004003du, 0x00000006u,
0x00000045u, 0x0000002au, 0x000500b0u, 0x0000000eu, 0x00000046u, 0x00000045u, 0x00000025u, 0x000400a8u,
0x0000000eu, 0x00000047u, 0x00000046u, 0x000300f7u, 0x00000049u, 0x00000000u, 0x000400fau, 0x00000047u,
0x00000048u, 0x00000049u, 0x000200f8u, 0x00000048u, 0x0004003du, 0x00000006u, 0x0000004au, 0x0000002au,
0x000500acu, 0x0000000eu, 0x0000004bu, 0x0000004au, 0x00000017u, 0x000200f9u, 0x00000049u, 0x000200f8u,
0x00000049u, 0x000700f5u, 0x0000000eu, 0x0000004cu, 0x00000046u, 0x00000043u, 0x0000004bu, 0x00000048u,
0x000300f7u, 0x0000004eu, 0x00000000u, 0x000400fau, 0x0000004cu, 0x0000004du, 0x0000004eu, 0x000200f8u,
0x0000004du, 0x000700f1u, 0x00000006u, 0x0000004fu, 0x00000014u, 0x00000023u, 0x0000000du, 0x00000025u,
0x000200f9u, 0x0000004eu, 0x000200f8u, 0x0000004eu, 0x0004003du, 0x00000006u, 0x00000051u, 0x00000050u,
0x0004003du, 0x00000006u, 0x00000052u, 0x0000003du, 0x000500aeu, 0x0000000eu, 0x00000053u, 0x00000051u,
0x00000052u, 0x000400a8u, 0x0000000eu, 0x00000054u, 0x00000053u, 0x000300f7u, 0x00000056u, 0x00000000u,
0x000400fau, 0x00000054u, 0x00000055u, 0x00000056u, 0x000200f8u, 0x00000055u, 0x0004003du, 0x00000006u,
0x00000057u, 0x00000050u, 0x000500aeu, 0x0000000eu, 0x00000058u, 0x00000057u, 0x00000017u, 0x000200f9u,
0x00000056u, 0x000200f8u, 0x00000056u, 0x000700f5u, 0x0000000eu, 0x00000059u, 0x00000053u, 0x0000004eu,
0x00000058u, 0x00000055u, 0x000300f7u, 0x0000005bu, 0x00000000u, 0x000400fau, 0x00000059u, 0x0000005au,
0x0000005bu, 0x000200f8u, 0x0000005au, 0x000700f1u, 0x00000006u, 0x0000005du, 0x00000014u, 0x00000023u,
0x0000000du, 0x0000005cu, 0x000200f9u, 0x0000005bu, 0x000200f8u, 0x0000005bu, 0x0004003du, 0x00000006u,
0x0000005fu, 0x0000005eu, 0x000500aau, 0x0000000eu, 0x00000060u, 0x0000005fu, 0x0000000du, 0x000400a8u,
0x0000000eu, 0x00000061u, 0x00000060u, 0x000300f7u, 0x00000063u, 0x00000000u, 0x000400fau, 0x00000061u,
0x00000062u, 0x00000063u, 0x000200f8u, 0x00000062u, 0x0004003du, 0x00000006u, 0x00000065u, 0x00000064u,
0x0004003du, 0x00000006u, 0x00000066u, 0x0000005eu, 0x000500aeu, 0x0000000eu, 0x00000067u, 0x00000065u,
0x00000066u, 0x000200f9u, 0x00000063u, 0x000200f8u, 0x00000063u, 0x000700f5u, 0x0000000eu, 0x00000068u,
0x00000060u, 0x0000005bu, 0x00000067u, 0x00000062u, 0x000300f7u, 0x0000006au, 0x00000000u, 0x000400fau,
0x00000068u, 0x00000069u, 0x0000006au, 0x000200f8u, 0x00000069u, 0x000700f1u, 0x00000006u, 0x0000006cu,
0x00000014u, 0x00000023u, 0x0000000du, 0x0000006bu, 0x000200f9u, 0x0000006au, 0x000200f8u, 0x0000006au,
0x0004003du, 0x00000006u, 0x0000006fu, 0x0000003du, 0x000500aeu, 0x0000000eu, 0x00000070u, 0x0000006fu,
0x00000025u, 0x0004003du, 0x00000006u, 0x00000071u, 0x0000003du, 0x000500b2u, 0x0000000eu, 0x00000072u,
0x00000071u, 0x00000017u, 0x000500a7u, 0x0000000eu, 0x00000073u, 0x00000070u, 0x00000072u, 0x0003003eu,
0x0000006eu, 0x00000073u, 0x0004003du, 0x0000000eu, 0x00000075u, 0x0000006eu, 0x000300f7u, 0x00000077u,
0x00000000u, 0x000400fau, 0x00000075u, 0x00000076u, 0x00000077u, 0x000200f8u, 0x00000076u, 0x0004003du,
0x00000006u, 0x00000078u, 0x00000050u, 0x0004003du, 0x00000006u, 0x00000079u, 0x0000003du, 0x000500b0u,
0x0000000eu, 0x0000007au, 0x00000078u, 0x00000079u, 0x000200f9u, 0x00000077u, 0x000200f8u, 0x00000077u,
0x000700f5u, 0x0000000eu, 0x0000007bu, 0x00000075u, 0x0000006au, 0x0000007au, 0x00000076u, 0x0003003eu,
0x00000074u, 0x0000007bu, 0x0004003du, 0x0000000eu, 0x0000007cu, 0x00000074u, 0x000300f7u, 0x0000007eu,
0x00000000u, 0x000400fau, 0x0000007cu, 0x0000007du, 0x0000007eu, 0x000200f8u, 0x0000007du, 0x0004003du,
0x00000006u, 0x0000007fu, 0x00000050u, 0x000500c4u, 0x00000006u, 0x00000080u, 0x00000023u, 0x0000007fu,
0x000700f1u, 0x00000006u, 0x00000081u, 0x00000015u, 0x00000023u, 0x0000000du, 0x00000080u, 0x0004003du,
0x00000006u, 0x00000082u, 0x00000050u, 0x00050041u, 0x00000012u, 0x00000083u, 0x00000020u, 0x00000082u,
0x0004003du, 0x00000006u, 0x00000084u, 0x00000008u, 0x00050080u, 0x00000006u, 0x00000085u, 0x00000084u,
0x00000023u, 0x000700eau, 0x00000006u, 0x00000086u, 0x00000083u, 0x00000023u, 0x0000000du, 0x00000085u,
0x0004003du, 0x00000006u, 0x00000087u, 0x0000005eu, 0x000500abu, 0x0000000eu, 0x00000088u, 0x00000087u,
0x0000000du, 0x000300f7u, 0x0000008au, 0x00000000u, 0x000400fau, 0x00000088u, 0x00000089u, 0x0000008au,
0x000200f8u, 0x00000089u, 0x0004003du, 0x00000006u, 0x0000008bu, 0x00000064u, 0x0004003du, 0x00000006u,
0x0000008cu, 0x0000005eu, 0x00050082u, 0x00000006u, 0x0000008du, 0x0000008cu, 0x00000023u, 0x000500aau,
0x0000000eu, 0x0000008eu, 0x0000008bu, 0x0000008du, 0x000200f9u, 0x0000008au, 0x000200f8u, 0x0000008au,
0x000700f5u, 0x0000000eu, 0x0000008fu, 0x00000088u, 0x0000007du, 0x0000008eu, 0x00000089u, 0x000300f7u,
0x00000091u, 0x00000000u, 0x000400fau, 0x0000008fu, 0x00000090u, 0x00000091u, 0x000200f8u, 0x00000090u,
0x0004003du, 0x00000006u, 0x00000092u, 0x00000050u, 0x00050041u, 0x00000012u, 0x00000093u, 0x0000001du,
0x00000092u, 0x000700eau, 0x00000006u, 0x00000094u, 0x00000093u, 0x00000023u, 0x0000000du, 0x00000023u,
0x000200f9u, 0x00000091u, 0x000200f8u, 0x00000091u, 0x000200f9u, 0x0000007eu, 0x000200f8u, 0x0000007eu,
0x000300e1u, 0x00000023u, 0x00000024u, 0x000400e0u, 0x00000025u, 0x00000025u, 0x00000024u, 0x0004003du,
0x00000006u, 0x00000095u, 0x00000008u, 0x000500aau, 0x0000000eu, 0x00000096u, 0x00000095u, 0x0000000du,
0x000300f7u, 0x00000098u, 0x00000000u, 0x000400fau, 0x00000096u, 0x00000097u, 0x00000098u, 0x000200f8u,
0x00000097u, 0x0004003du, 0x00000006u, 0x0000009au, 0x00000015u, 0x00050041u, 0x0000003bu, 0x0000009bu,
0x00000037u, 0x00000099u, 0x0003003eu, 0x0000009bu, 0x0000009au, 0x000200f9u, 0x00000098u, 0x000200f8u,
0x00000098u, 0x0004003du, 0x00000006u, 0x0000009cu, 0x00000008u, 0x000500b0u, 0x0000000eu, 0x0000009du,
0x0000009cu, 0x00000017u, 0x000300f7u, 0x0000009fu, 0x00000000u, 0x000400fau, 0x0000009du, 0x0000009eu,
0x0000009fu, 0x000200f8u, 0x0000009eu, 0x0004003du, 0x00000006u, 0x000000a1u, 0x00000008u, 0x0004003du,
0x00000006u, 0x000000a2u, 0x00000008u, 0x00050041u, 0x00000012u, 0x000000a3u, 0x0000001du, 0x000000a2u,
0x0004003du, 0x00000006u, 0x000000a4u, 0x000000a3u, 0x00060041u, 0x0000003bu, 0x000000a5u, 0x00000037u,
0x000000a0u, 0x000000a1u, 0x0003003eu, 0x000000a5u, 0x000000a4u, 0x0004003du, 0x00000006u, 0x000000a7u,
0x00000008u, 0x0004003du, 0x00000006u, 0x000000a8u, 0x00000008u, 0x00050041u, 0x00000012u, 0x000000a9u,
0x00000020u, 0x000000a8u, 0x0004003du, 0x00000006u, 0x000000aau, 0x000000a9u, 0x00060041u, 0x0000003bu,
0x000000abu, 0x00000037u, 0x000000a6u, 0x000000a7u, 0x0003003eu, 0x000000abu, 0x000000aau, 0x000200f9u,
0x0000009fu, 0x000200f8u, 0x0000009fu, 0x0004003du, 0x0000000eu, 0x000000adu, 0x0000006eu, 0x000300f7u,
0x000000afu, 0x00000000u, 0x000400fau, 0x000000adu, 0x000000aeu, 0x000000afu, 0x000200f8u, 0x000000aeu,
0x0004003du, 0x00000006u, 0x000000b0u, 0x00000014u, 0x000500aau, 0x0000000eu, 0x000000b1u, 0x000000b0u,
0x0000000du, 0x000200f9u, 0x000000afu, 0x000200f8u, 0x000000afu, 0x000700f5u, 0x0000000eu, 0x000000b2u,
0x000000adu, 0x0000009fu, 0x000000b1u, 0x000000aeu, 0x0003003eu, 0x000000acu, 0x000000b2u, 0x0004003du,
0x0000000eu, 0x000000b3u, 0x000000acu, 0x000300f7u, 0x000000b5u, 0x00000000u, 0x000400fau, 0x000000b3u,
0x000000b4u, 0x000000b5u, 0x000200f8u, 0x000000b4u, 0x0004003du, 0x00000006u, 0x000000b8u, 0x0000000au,
0x00050080u, 0x00000006u, 0x000000b9u, 0x000000b8u, 0x00000023u, 0x00040070u, 0x0000002fu, 0x000000bau,
0x000000b9u, 0x00050050u, 0x00000030u, 0x000000bcu, 0x000000bau, 0x000000bbu, 0x0003003eu, 0x000000b7u,
0x000000bcu, 0x0004003du, 0x00000030u, 0x000000bdu, 0x000000b7u, 0x0006015eu, 0x00000030u, 0x000000bfu,
0x000000beu, 0x00000001u, 0x000000bdu, 0x0003003eu, 0x000000b7u, 0x000000bfu, 0x0004003du, 0x00000006u,
0x000000c0u, 0x00000064u, 0x0004003du, 0x00000006u, 0x000000c1u, 0x0000005eu, 0x00050082u, 0x00000006u,
0x000000c2u, 0x000000c1u, 0x00000023u, 0x000500aau, 0x0000000eu, 0x000000c3u, 0x000000c0u, 0x000000c2u,
0x000300f7u, 0x000000c5u, 0x00000000u, 0x000400fau, 0x000000c3u, 0x000000c4u, 0x000000c5u, 0x000200f8u,
0x000000c4u, 0x0004003du, 0x00000006u, 0x000000c9u, 0x00000050u, 0x0004003du, 0x00000030u, 0x000000cau,
0x000000b7u, 0x00050041u, 0x000000cbu, 0x000000ccu, 0x000000c8u, 0x000000c9u, 0x0003003eu, 0x000000ccu,
0x000000cau, 0x000200f9u, 0x000000c5u, 0x000200f8u, 0x000000c5u, 0x000400e0u, 0x00000025u, 0x00000025u,
0x00000024u, 0x0004003du, 0x00000006u, 0x000000cdu, 0x0000000au, 0x0004003du, 0x00000006u, 0x000000ceu,
0x0000002au, 0x000500b0u, 0x0000000eu, 0x000000cfu, 0x000000cdu, 0x000000ceu, 0x000300f7u, 0x000000d1u,
0x00000000u, 0x000400fau, 0x000000cfu, 0x000000d0u, 0x000000d1u, 0x000200f8u, 0x000000d0u, 0x0004003du,
0x00000006u, 0x000000d3u, 0x0000000au, 0x0004003du, 0x00000006u, 0x000000d4u, 0x0000000au, 0x00050041u,
0x000000cbu, 0x000000d5u, 0x000000c8u, 0x000000d4u, 0x0004003du, 0x00000030u, 0x000000d6u, 0x000000d5u,
0x00060041u, 0x000000d7u, 0x000000d8u, 0x00000037u, 0x000000d2u, 0x000000d3u, 0x0003003eu, 0x000000d8u,
0x000000d6u, 0x000200f9u, 0x000000d1u, 0x000200f8u, 0x000000d1u, 0x000400e0u, 0x00000025u, 0x00000025u,
0x00000024u, 0x0004003du, 0x00000006u, 0x000000dau, 0x0000002au, 0x0006000cu, 0x00000038u, 0x000000dbu,
0x00000001u, 0x0000004bu, 0x000000dau, 0x0004007cu, 0x00000006u, 0x000000dcu, 0x000000dbu, 0x0003003eu,
0x000000d9u, 0x000000dcu, 0x0004003du, 0x00000006u, 0x000000ddu, 0x0000002au, 0x0004003du, 0x00000006u,
0x000000deu, 0x000000d9u, 0x00050082u, 0x00000006u, 0x000000dfu, 0x000000deu, 0x00000023u, 0x000500c4u,
0x00000006u, 0x000000e0u, 0x00000023u, 0x000000dfu, 0x00050082u, 0x00000006u, 0x000000e1u, 0x000000ddu,
0x000000e0u, 0x000500acu, 0x0000000eu, 0x000000e2u, 0x000000e1u, 0x0000000du, 0x000600a9u, 0x00000006u,
0x000000e3u, 0x000000e2u, 0x00000023u, 0x0000000du, 0x0004003du, 0x00000006u, 0x000000e4u, 0x000000d9u,
0x00050080u, 0x00000006u, 0x000000e5u, 0x000000e4u, 0x000000e3u, 0x0003003eu, 0x000000d9u, 0x000000e5u,
0x0004003du, 0x00000006u, 0x000000e6u, 0x0000000au, 0x000500aau, 0x0000000eu, 0x000000e7u, 0x000000e6u,
0x0000000du, 0x000300f7u, 0x000000e9u, 0x00000000u, 0x000400fau, 0x000000e7u, 0x000000e8u, 0x000000e9u,
0x000200f8u, 0x000000e8u, 0x0004003du, 0x00000006u, 0x000000ebu, 0x000000d9u, 0x00050041u, 0x0000003bu,
0x000000ecu, 0x00000037u, 0x000000eau, 0x0003003eu, 0x000000ecu, 0x000000ebu, 0x000200f9u, 0x000000e9u,
0x000200f8u, 0x000000e9u, 0x0003003eu, 0x000000edu, 0x0000000du, 0x000200f9u, 0x000000eeu, 0x000200f8u,
0x000000eeu, 0x000400f6u, 0x000000f0u, 0x000000f1u, 0x00000000u, 0x000200f9u, 0x000000f2u, 0x000200f8u,
0x000000f2u, 0x0004003du, 0x00000006u, 0x000000f3u, 0x000000edu, 0x0004003du, 0x00000006u, 0x000000f4u,
0x000000d9u, 0x000500b0u, 0x0000000eu, 0x000000f5u, 0x000000f3u, 0x000000f4u, 0x000400fau, 0x000000f5u,
0x000000efu, 0x000000f0u, 0x000200f8u, 0x000000efu, 0x0004003du, 0x00000006u, 0x000000f6u, 0x00000050u,
0x0004003du, 0x00000006u, 0x000000f7u, 0x000000edu, 0x000500c4u, 0x00000006u, 0x000000f8u, 0x00000023u,
0x000000f7u, 0x000500c7u, 0x00000006u, 0x000000f9u, 0x000000f6u, 0x000000f8u, 0x000500acu, 0x0000000eu,
0x000000fau, 0x000000f9u, 0x0000000du, 0x000300f7u, 0x000000fcu, 0x00000000u, 0x000400fau, 0x000000fau,
0x000000fbu, 0x000000fcu, 0x000200f8u, 0x000000fbu, 0x0004003du, 0x00000006u, 0x000000fdu, 0x00000050u,
0x0004003du, 0x00000006u, 0x000000feu, 0x000000edu, 0x000500c2u, 0x00000006u, 0x000000ffu, 0x000000fdu,
0x000000feu, 0x0004003du, 0x00000006u, 0x00000100u, 0x000000edu, 0x000500c4u, 0x00000006u, 0x00000101u,
0x000000ffu, 0x00000100u, 0x00050082u, 0x00000006u, 0x00000102u, 0x00000101u, 0x00000023u, 0x00050041u,
0x000000cbu, 0x00000103u, 0x000000c8u, 0x00000102u, 0x0004003du, 0x00000030u, 0x00000104u, 0x00000103u,
0x0004003du, 0x00000030u, 0x00000105u, 0x000000b7u, 0x00050081u, 0x00000030u, 0x00000106u, 0x00000105u,
0x00000104u, 0x0003003eu, 0x000000b7u, 0x00000106u, 0x0004003du, 0x00000006u, 0x00000107u, 0x00000064u,
0x0004003du, 0x00000006u, 0x00000108u, 0x0000005eu, 0x00050082u, 0x00000006u, 0x00000109u, 0x00000108u,
0x00000023u, 0x000500aau, 0x0000000eu, 0x0000010au, 0x00000107u, 0x00000109u, 0x000300f7u, 0x0000010cu,
0x00000000u, 0x000400fau, 0x0000010au, 0x0000010bu, 0x0000010cu, 0x000200f8u, 0x0000010bu, 0x0004003du,
0x00000006u, 0x0000010du, 0x00000050u, 0x0004003du, 0x00000030u, 0x0000010eu, 0x000000b7u, 0x00050041u,
0x000000cbu, 0x0000010fu, 0x000000c8u, 0x0000010du, 0x0003003eu, 0x0000010fu, 0x0000010eu, 0x000200f9u,
0x0000010cu, 0x000200f8u, 0x0000010cu, 0x000200f9u, 0x000000fcu, 0x000200f8u, 0x000000fcu, 0x000400e0u,
0x00000025u, 0x00000025u, 0x00000024u, 0x0004003du, 0x00000006u, 0x00000110u, 0x0000000au, 0x0004003du,
0x00000006u, 0x00000111u, 0x0000002au, 0x000500b0u, 0x0000000eu, 0x00000112u, 0x00000110u, 0x00000111u,
0x000300f7u, 0x00000114u, 0x00000000u, 0x000400fau, 0x00000112u, 0x00000113u, 0x00000114u, 0x000200f8u,
0x00000113u, 0x0004003du, 0x00000006u, 0x00000116u, 0x000000edu, 0x0004003du, 0x00000006u, 0x00000117u,
0x0000000au, 0x0004003du, 0x00000006u, 0x00000118u, 0x0000000au, 0x00050041u, 0x000000cbu, 0x00000119u,
0x000000c8u, 0x00000118u, 0x0004003du, 0x00000030u, 0x0000011au, 0x00000119u, 0x00070041u, 0x000000d7u,
0x0000011bu, 0x00000037u, 0x00000115u, 0x00000116u, 0x00000117u, 0x0003003eu, 0x0000011bu, 0x0000011au,
0x000200f9u, 0x00000114u, 0x000200f8u, 0x00000114u, 0x000400e0u, 0x00000025u, 0x00000025u, 0x00000024u,
0x000200f9u, 0x000000f1u, 0x000200f8u, 0x000000f1u, 0x0004003du, 0x00000006u, 0x0000011cu, 0x000000edu,
0x00050080u, 0x00000006u, 0x0000011eu, 0x0000011cu, 0x0000011du, 0x0003003eu, 0x000000edu, 0x0000011eu,
0x000200f9u, 0x000000eeu, 0x000200f8u, 0x000000f0u, 0x0004003du, 0x00000006u, 0x0000011fu, 0x0000000au,
0x000500aau, 0x0000000eu, 0x00000121u, 0x0000011fu, 0x00000120u, 0x000300f7u, 0x00000123u, 0x00000000u,
0x000400fau, 0x00000121u, 0x00000122u, 0x00000123u, 0x000200f8u, 0x00000122u, 0x0004003du, 0x00000030u,
0x00000125u, 0x000000b7u, 0x00050050u, 0x00000030u, 0x00000127u, 0x00000126u, 0x00000126u, 0x00050088u,
0x00000030u, 0x00000128u, 0x00000125u, 0x00000127u, 0x00050041u, 0x000000cbu, 0x00000129u, 0x000000c8u,
0x00000124u, 0x0003003eu, 0x00000129u, 0x00000128u, 0x000200f9u, 0x00000123u, 0x000200f8u, 0x00000123u,
0x000400e0u, 0x00000025u, 0x00000025u, 0x00000024u, 0x0004003du, 0x00000006u, 0x0000012au, 0x0000000au,
0x000500aau, 0x0000000eu, 0x0000012bu, 0x0000012au, 0x00000120u, 0x000300f7u, 0x0000012du, 0x00000000u,
0x000400fau, 0x0000012bu, 0x0000012cu, 0x0000012du, 0x000200f8u, 0x0000012cu, 0x0004003du, 0x00000006u,
0x0000012fu, 0x0000005eu, 0x0004003du, 0x00000006u, 0x00000130u, 0x0000002au, 0x0004003du, 0x00000006u,
0x00000131u, 0x00000050u, 0x0004003du, 0x00000006u, 0x00000132u, 0x00000064u, 0x00070050u, 0x0000002cu,
0x00000133u, 0x0000012fu, 0x00000130u, 0x00000131u, 0x00000132u, 0x00050041u, 0x00000134u, 0x00000135u,
0x00000037u, 0x0000012eu, 0x0003003eu, 0x00000135u, 0x00000133u, 0x00050041u, 0x000000cbu, 0x00000137u,
0x000000c8u, 0x00000124u, 0x0004003du, 0x00000030u, 0x00000138u, 0x00000137u, 0x00050041u, 0x000000d7u,
0x00000139u, 0x00000037u, 0x00000136u, 0x0003003eu, 0x00000139u, 0x00000138u, 0x000200f9u, 0x0000012du,
0x000200f8u, 0x0000012du, 0x000200f9u, 0x000000b5u, 0x000200f8u, 0x000000b5u, 0x000400e0u, 0x00000025u,
0x00000025u, 0x00000024u, 0x0004003du, 0x00000006u, 0x0000013au, 0x00000008u, 0x000500aau, 0x0000000eu,
0x0000013bu, 0x0000013au, 0x0000000du, 0x000300f7u, 0x0000013du, 0x00000000u, 0x000400fau, 0x0000013bu,
0x0000013cu, 0x0000013du, 0x000200f8u, 0x0000013cu, 0x0004003du, 0x00000006u, 0x0000013eu, 0x00000014u,
0x00050041u, 0x0000003bu, 0x0000013fu, 0x00000037u, 0x0000011du, 0x0003003eu, 0x0000013fu, 0x0000013eu,
0x00050041u, 0x0000003bu, 0x00000141u, 0x00000037u, 0x00000124u, 0x0003003eu, 0x00000141u, 0x00000140u,
0x000200f9u, 0x0000013du, 0x000200f8u, 0x0000013du, 0x000100fdu, 0x00010038u,
};
inline constexpr std::size_t kDriverPostIterationRPWitnessSpvWordCount =
sizeof(kDriverPostIterationRPWitnessSpv) / sizeof(kDriverPostIterationRPWitnessSpv[0]);
} // namespace MobileGL::MG_Util::SelfTest
@@ -38,7 +38,6 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
HashBytes(state, env.advertisedExtensions.data(),
env.advertisedExtensions.size() * sizeof(GLExtension));
}
HashValue(state, env.subgroupPrefixScanQuirk);
return state;
}
@@ -73,8 +72,6 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
kFrontendMaxComputeWorkGroupInvocations)
: kFrontendMaxComputeWorkGroupInvocations;
env->subgroupPrefixScanQuirk = MG_Config::Features.SubgroupPrefixScanQuirk;
env->fingerprint = ComputeCompileEnvFingerprint(*env);
return env;
}
@@ -84,7 +81,6 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// computed, and this must not run before MG_Config is loaded.
static const SharedPtr<const CompileEnv> kDefault = [] {
auto env = MakeShared<CompileEnv>();
env->subgroupPrefixScanQuirk = MG_Config::Features.SubgroupPrefixScanQuirk;
env->fingerprint = ComputeCompileEnvFingerprint(*env);
return SharedPtr<const CompileEnv>(Move(env));
}();
@@ -12,9 +12,9 @@
#include <MG_Backend/BackendObject.h>
namespace MobileGL::MG_Util::ShaderTranspiler {
// Everything the shader compile/link pipeline reads from OUTSIDE its own (stage, source)
// inputs: backend identity, backend limits, the advertised extension list, and the one
// config quirk the source rewriter branches on.
// everything outside (stage, source) this reads - advertised extensions and backend limits -
// so the transformation is a pure function of its three arguments and can run on a worker
// thread.
//
// Why it exists (P1): every one of those reads is a reach-back into
// MG_Backend::pActiveBackendObject / gBackendFunctionsTable, and one of them
@@ -46,9 +46,6 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
MG_Backend::DynamicBackendParameters params{}; // by value, never by reference
Vector<GLExtension> advertisedExtensions;
// --- config the source rewriter branches on ---
MG_Config::QuirkOverride subgroupPrefixScanQuirk = MG_Config::QuirkOverride::Auto;
Uint64 fingerprint = 0; // set by CaptureCompileEnv()
Bool HasBackend() const { return backend != BackendType::Unknown; }
@@ -61,7 +61,7 @@ namespace MobileGL {
"imageAtomicXor", "imageLoad", "imageSize", "imageStore", "imulExtended",
"intBitsToFloat", "interpolateAtCentroid", "interpolateAtOffset",
"interpolateAtSample", "inverse", "inversesqrt", "isinf", "isnan",
"ldexp", "length", "lessThan", "lessThanEqual", "log", "log2",
"ldexp", "length", "length_squared", "lessThan", "lessThanEqual", "log", "log2",
"matrixCompMult", "max", "max3", "memoryBarrier",
"memoryBarrierAtomicCounter", "memoryBarrierBuffer", "memoryBarrierImage",
"memoryBarrierShared", "mid3", "min", "min3", "mix", "mod", "modf",
@@ -25,6 +25,10 @@
#include "SpirvPasses/SplitArrayVertexInputsPass.h"
#include "SpirvPasses/RebaseInstanceIndexPass.h"
#include "SpirvPasses/ZeroBaseVertexPass.h"
#include "SpirvPasses/DeriveNumSubgroupsPass.h"
#include "SpirvPasses/EmulateSubgroupsPass.h"
#include "SpirvPasses/FixIterationRPBarrierPass.h"
#include "SpirvPasses/FixIterationRPSubgroupScratchPass.h"
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
#include "SpirvPasses/Lower1DArrayImagesPass.h"
#include "SpirvPasses/BakeImageFormatsPass.h"
@@ -369,12 +373,6 @@ namespace MobileGL {
return allSpirv;
}
// -1 unresolved, 0 off, 1 on. Resolved once from MOBILEGL_VALIDATE_SPIRV on first
// use. A live getenv rather than an MG_Config::Features field, for the same reason
// Config.h already exempts MOBILEGL_LOG_FILE_PATH: suites like SpirvPassTest never
// run MobileGL::Initialize(), and every Initialize() re-runs MG_ConfigLoader::Init,
// which would clobber a programmatic override stored in the feature table.
static std::atomic<int> g_validateSpirv{-1};
// Total validation failures observed this process. This latch - not the wrappers'
// return values - is the test-lane signal: validation must never change what a
// wrapper returns, or the validating lanes would render differently from the
@@ -383,28 +381,6 @@ namespace MobileGL {
static std::atomic<Uint64> g_spirvValidationFailures{0};
namespace {
// Test lanes (desktop/CI/WSL) validate by default; device builds do not -
// validation costs real time per module, and on device the driver is the
// final validator anyway. MOBILEGL_VALIDATE_SPIRV overrides in either
// direction, using the ConfigLoader truthy rule.
constexpr bool kValidateSpirvDefault =
#if defined(__ANDROID__)
false;
#else
true;
#endif
bool IsTruthySpirvEnvValue(const char* value) {
if (value == nullptr || value[0] == '\0') {
return false;
}
String lowered(value);
for (auto& c : lowered) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
return lowered != "0" && lowered != "false";
}
// spirv-tools' validator lazily constructs function-local static tables on
// its first run, which on this codebase happens on a ShaderCompilePool
// worker. Function-local statics are destroyed in reverse construction
@@ -445,11 +421,6 @@ namespace MobileGL {
tools.Validate(warmup);
}
std::atexit(+[] {
// Flip validation off first: a validator table this warmup does
// not know about (a future spirv-tools bump) would still be
// destroyed before this handler, and workers must stop entering
// Validate before the drain waits for them.
g_validateSpirv.store(0, std::memory_order_release);
Async::ShaderCompilePool::StopAndDrainProcessPoolAtExit();
});
});
@@ -478,10 +449,10 @@ namespace MobileGL {
// Validation is decoupled from control flow on purpose: a failure logs and
// bumps the latch, and the caller proceeds exactly as the shipping (non-
// validating) configuration would. Tests assert on the latch delta.
void ValidateOrLatch(const char* site, const Vector<Uint32>& binary) {
if (!ShaderCompiler::SpirvValidationEnabled()) {
return;
}
void ValidateOrLatch(const char* site, const Vector<Uint32>& binary,
const bool enableSpirvValidation) {
if (!enableSpirvValidation) return;
PinValidatorTablesForProcessExit();
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
tools.SetMessageConsumer(MakeSpirvMessageConsumer(site));
if (!tools.Validate(binary)) {
@@ -502,39 +473,23 @@ namespace MobileGL {
// spirv-tools drops pass diagnostics on the floor.
bool RunOptimizerChecked(const char* site, spvtools::Optimizer& optimizer,
const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary, const bool validateOutput,
const bool enableSpirvValidation) {
spvtools::OptimizerOptions options;
options.set_run_validator(false);
optimizer.SetMessageConsumer(MakeSpirvMessageConsumer(site));
if (!optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options)) {
return false;
}
ValidateOrLatch(site, outputBinary);
if (validateOutput) {
ValidateOrLatch(site, outputBinary, enableSpirvValidation);
}
return true;
}
} // namespace
bool ShaderCompiler::SpirvValidationEnabled() {
int state = g_validateSpirv.load(std::memory_order_acquire);
if (state < 0) {
const char* env = std::getenv("MOBILEGL_VALIDATE_SPIRV");
const bool resolved = env != nullptr ? IsTruthySpirvEnvValue(env) : kValidateSpirvDefault;
int expected = -1;
g_validateSpirv.compare_exchange_strong(expected, resolved ? 1 : 0,
std::memory_order_acq_rel);
state = g_validateSpirv.load(std::memory_order_acquire);
if (state == 1) {
PinValidatorTablesForProcessExit();
}
}
return state == 1;
}
void ShaderCompiler::SetSpirvValidationEnabled(bool enabled) {
g_validateSpirv.store(enabled ? 1 : 0, std::memory_order_release);
if (enabled) {
PinValidatorTablesForProcessExit();
}
void ShaderCompiler::PrepareSpirvValidation() {
PinValidatorTablesForProcessExit();
}
Uint64 ShaderCompiler::NoteSpirvValidationFailure() {
@@ -604,16 +559,19 @@ namespace MobileGL {
}
bool ShaderCompiler::DemoteFloat64ToFloat32(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(DemoteFloat64Pass::CreateDemoteFloat64Pass());
return RunOptimizerChecked("DemoteFloat64ToFloat32", optimizer, inputBinary, outputBinary);
return RunOptimizerChecked("DemoteFloat64ToFloat32", optimizer, inputBinary, outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary,
const bool validateOutput,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
@@ -663,38 +621,41 @@ namespace MobileGL {
optimizer.RegisterPass(DemoteFloat64Pass::CreateDemoteFloat64Pass());
return RunOptimizerChecked("SanitizeAndOptimizeBinary", optimizer, inputBinary,
outputBinary);
outputBinary, validateOutput, enableSpirvValidation);
}
bool ShaderCompiler::LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(LowerDrawParametersPass::CreateLowerDrawParametersPass());
return RunOptimizerChecked("LowerDrawParametersForEssl", optimizer, inputBinary,
outputBinary);
outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::SplitArrayVertexInputsForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(SplitArrayVertexInputsPass::CreateSplitArrayVertexInputsPass());
return RunOptimizerChecked("SplitArrayVertexInputsForEssl", optimizer, inputBinary,
outputBinary);
outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::BakeImageFormatsForEssl(const Vector<Uint32>& inputBinary,
const UnorderedMap<String, Uint>& glFormatByName,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
if (glFormatByName.empty()) return false;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(BakeImageFormatsPass::CreateBakeImageFormatsPass(glFormatByName));
return RunOptimizerChecked("BakeImageFormatsForEssl", optimizer, inputBinary, outputBinary);
return RunOptimizerChecked("BakeImageFormatsForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::DeclaresFormatlessStorageImage(const Vector<Uint32>& binary) {
@@ -718,7 +679,8 @@ namespace MobileGL {
bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
const std::set<String>& blockNames,
std::set<String>& flattenedBlockNames,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
if (blockNames.empty()) return false;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
@@ -726,7 +688,7 @@ namespace MobileGL {
blockNames, &flattenedBlockNames));
return RunOptimizerChecked("FlattenXfbInterfaceBlocksForEssl", optimizer, inputBinary,
outputBinary);
outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock(
@@ -736,48 +698,53 @@ namespace MobileGL {
}
bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(PackDoubleVertexInputsPass::CreatePackDoubleVertexInputsPass());
return RunOptimizerChecked("PackDoubleVertexInputsForVulkan", optimizer, inputBinary,
outputBinary);
outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(
StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass());
return RunOptimizerChecked("StripUboMemberRelaxedPrecisionForEssl", optimizer,
inputBinary, outputBinary);
inputBinary, outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::StripNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(StripNoPerspectivePass::CreateStripNoPerspectivePass());
return RunOptimizerChecked("StripNoPerspectiveForEssl", optimizer, inputBinary,
outputBinary);
outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(EmulateNoPerspectivePass::CreateEmulateNoPerspectivePass());
return RunOptimizerChecked("EmulateNoPerspectiveForEssl", optimizer, inputBinary,
outputBinary);
outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
// Detection gates everything: a module with no dynamically indexed fragment
@@ -810,7 +777,7 @@ namespace MobileGL {
Vector<uint32_t> folded;
if (!RunOptimizerChecked("LegalizeFragmentOutputIndexingForEssl.fold", folder, inputBinary,
folded) ||
folded, true, enableSpirvValidation) ||
folded.empty()) {
// Fail open onto the fallback rather than onto the illegal module.
folded = inputBinary;
@@ -829,7 +796,7 @@ namespace MobileGL {
lowerer.RegisterPass(CreateAggressiveDCEPass(false));
if (!RunOptimizerChecked("LegalizeFragmentOutputIndexingForEssl.lower", lowerer, folded,
outputBinary) ||
outputBinary, true, enableSpirvValidation) ||
outputBinary.empty()) {
outputBinary = folded;
return true;
@@ -846,16 +813,17 @@ namespace MobileGL {
}
bool ShaderCompiler::LowerRectImages(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass());
return RunOptimizerChecked("LowerRectImages", optimizer, inputBinary, outputBinary);
return RunOptimizerChecked("LowerRectImages", optimizer, inputBinary, outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::Lower1DArrayImagesForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
using namespace spvtools;
// Declined rather than half-translated: after the rewrite the image is a 2D
@@ -897,40 +865,90 @@ namespace MobileGL {
// second Shader. Deduplicating afterwards collapses all three at once.
optimizer.RegisterPass(CreateRemoveDuplicatesPass());
return RunOptimizerChecked("Lower1DArrayImagesForEssl", optimizer, inputBinary, outputBinary);
return RunOptimizerChecked("Lower1DArrayImagesForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(RebaseInstanceIndexPass::CreateRebaseInstanceIndexPass());
return RunOptimizerChecked("RebaseInstanceIndexForVulkan", optimizer, inputBinary,
outputBinary);
outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::ZeroBaseVertexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(ZeroBaseVertexPass::CreateZeroBaseVertexPass());
return RunOptimizerChecked("ZeroBaseVertexForVulkan", optimizer, inputBinary, outputBinary);
return RunOptimizerChecked("ZeroBaseVertexForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::DeriveNumSubgroupsForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(DeriveNumSubgroupsPass::CreateDeriveNumSubgroupsPass());
return RunOptimizerChecked("DeriveNumSubgroupsForVulkan", optimizer, inputBinary,
outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::EmulateSubgroupsForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
const Uint32 maxWorkgroupScratchBytes,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(
EmulateSubgroupsPass::CreateEmulateSubgroupsPass(maxWorkgroupScratchBytes));
return RunOptimizerChecked("EmulateSubgroupsForVulkan", optimizer, inputBinary,
outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
const Uint32 nativeSubgroupSize, const Uint32 maxWorkgroupScratchBytes,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(
FixIterationRPSubgroupScratchPass::CreateFixIterationRPSubgroupScratchPass(
nativeSubgroupSize, maxWorkgroupScratchBytes));
return RunOptimizerChecked("FixIterationRPSubgroupScratchForVulkan", optimizer,
inputBinary, outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::FixIterationRPBarrierForVulkan(
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(FixIterationRPBarrierPass::CreateFixIterationRPBarrierPass());
return RunOptimizerChecked("FixIterationRPBarrierForVulkan", optimizer,
inputBinary, outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(DecoratePositionInvariantPass::CreateDecoratePositionInvariantPass());
return RunOptimizerChecked("DecoratePositionInvariantForVulkan", optimizer, inputBinary,
outputBinary);
outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary) {
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
constexpr SizeT kSpirvHeaderWordCount = 5;
outputBinary.clear();
if (inputBinary.size() < kSpirvHeaderWordCount || inputBinary[0] != spv::MagicNumber) {
@@ -1058,7 +1076,8 @@ namespace MobileGL {
addedCapabilities.begin(), addedCapabilities.end());
// Hand-rolled word walk, so no Optimizer wrapper ever sees this rewrite;
// check the modified module explicitly in validating lanes.
ValidateOrLatch("UseUnformattedFloatStorageImagesForVulkan", outputBinary);
ValidateOrLatch("UseUnformattedFloatStorageImagesForVulkan", outputBinary,
enableSpirvValidation);
return true;
}
@@ -23,19 +23,23 @@ namespace MobileGL {
static Result<SharedPtr<glslang::TProgram>> LinkProgram(const ProgramAttrib& attrib);
static Result<Vector<Vector<unsigned>>> GetSpirvBinaryFromProgram(const ProgramBinaryAttrib& attrib);
static bool SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
Vector<uint32_t>& outputBinary,
bool validateOutput = true,
bool enableSpirvValidation = false);
// Demotes DrawIndex/BaseInstance/BaseVertex builtins to plain Private globals
// (mg_DrawID/mg_BaseInstance/mg_BaseVertex) so SPIRV-Cross can emit ESSL.
// Only for backends without native draw-parameter support (DirectGLES).
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Replaces an ARRAY vertex input with one input per element at consecutive
// locations, seeding a Private copy of the array so indexed reads still work.
// GLSL ES has no array vertex inputs and SPIRV-Cross refuses the whole module
// rather than emulating them, so without this the stage never reaches the
// driver. Only for the DirectGLES transpile path.
static bool SplitArrayVertexInputsForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Replaces the named interface BLOCKS with one variable per member, named
// "<Block>_<member>", shadowing the block itself so the body is untouched. The
// Adreno ES driver silently captures NOTHING for a transform-feedback varying
@@ -46,7 +50,8 @@ namespace MobileGL {
static bool FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
const std::set<String>& blockNames,
std::set<String>& flattenedBlockNames,
Vector<uint32_t>& outputBinary);
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// The capture request "StageData.attrib[0]" as the pass above renamed it,
// "StageData_attrib[0]", or false when it does not name a member of a block
// that was flattened.
@@ -58,17 +63,20 @@ namespace MobileGL {
// drivers reject cross-stage uniform blocks whose member precisions differ.
// Only for the DirectGLES transpile path.
static bool StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Removes NoPerspective decorations so SPIRV-Cross emits plain (smooth) ESSL varyings.
// DirectGLES fallback only, for devices lacking GL_NV_shader_noperspective_interpolation
// (SPIRV-Cross would otherwise require that extension and the driver would reject it).
static bool StripNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Emulates noperspective (screen-linear) interpolation via gl_Position.w / gl_FragCoord.w
// so no NV extension is needed; strips what it cannot emulate. DirectGLES fallback for
// devices lacking GL_NV_shader_noperspective_interpolation. See EmulateNoPerspectivePass.
static bool EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Makes every index into a fragment-output array a constant integral
// expression, which is what GLSL ES requires and SPIR-V does not. Runs the
// stock folding chain first (loop unrolling folds the loop-derived indices
@@ -79,7 +87,8 @@ namespace MobileGL {
// dynamically, which is every shader but a handful.
// See LegalizeFragmentOutputIndexPass.
static bool LegalizeFragmentOutputIndexingForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
@@ -88,7 +97,8 @@ namespace MobileGL {
// divides the coordinate of each normalized-coordinate lookup by the texture
// size and rewrites the image type to 2D. See NormalizeRectCoordinatesPass for
// what it declines and why.
static bool LowerRectImages(const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary);
static bool LowerRectImages(const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// GL_TEXTURE_1D_ARRAY storage images rewritten to the 2D-array shape the texture
// is actually stored in on ES, with the layer moved from the coordinate's second
// component to its third. DirectGLES transpile path only - Vulkan binds a real
@@ -96,7 +106,8 @@ namespace MobileGL {
// through untouched when the module declares no such image, which is every shader
// but a handful. See Lower1DArrayImagesPass for what it declines and why.
static bool Lower1DArrayImagesForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Gives each format-less storage image the format bound to its image unit, so
// the emitted ESSL can carry the format layout qualifier GLSL ES requires of
// every image and desktop GLSL lets a writeonly declaration omit. `glFormatByName`
@@ -105,7 +116,8 @@ namespace MobileGL {
// natively. See BakeImageFormatsPass for what it declines and why.
static bool BakeImageFormatsForEssl(const Vector<Uint32>& inputBinary,
const UnorderedMap<String, Uint>& glFormatByName,
Vector<uint32_t>& outputBinary);
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Whether the module declares a storage image with no format qualifier at all,
// i.e. whether BakeImageFormatsForEssl could change anything. One module parse,
// so the ~every shader that declares none pays no optimizer run.
@@ -124,27 +136,71 @@ namespace MobileGL {
// emitted text instead.
static bool SpirvCrossCanPrintEsslImageFormat(Uint glInternalFormat);
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Builds the non-indexed-draw variant of a vertex shader: every gl_BaseVertex
// read becomes zero, which is what GL defines for a command carrying no
// baseVertex parameter while Vulkan's builtin would report firstVertex.
// See ZeroBaseVertexPass.
static bool ZeroBaseVertexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Replaces compute gl_NumSubgroups loads with ceil(workgroup invocations /
// gl_SubgroupSize). DirectVulkan only; this repairs drivers whose builtin
// disagrees with the subgroup IDs the same dispatch emits (Adreno reports 1
// while emitting IDs 0..7). The ceil() partition is only spec-guaranteed
// under VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT, which
// the caller requests whenever it is legal for the workgroup shape; see
// DeriveNumSubgroupsPass.
static bool DeriveNumSubgroupsForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Lowers every GL_KHR_shader_subgroup construct in a compute module onto a
// 32-lane virtual subgroup built from workgroup-shared memory. Last-resort
// path for devices with NO native subgroup support, opt-in via
// MOBILEGL_MAGMA_EMULATE_SUBGROUP=1; a device with native subgroup
// operations always uses them. maxWorkgroupScratchBytes bounds the shared
// scratch the lowering may add (pass the device's
// maxComputeSharedMemorySize; 0 falls back to the 16384-byte Vulkan
// minimum). See EmulateSubgroupsPass.
static bool EmulateSubgroupsForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
Uint32 maxWorkgroupScratchBytes,
bool enableSpirvValidation = false);
// Grows iterationRP's under-declared gl_SubgroupID-indexed scratch to the
// subgroup count the device actually partitions into, fingerprint-gated to
// that pack's reduction idiom; every other module - and every device whose
// width the pack already assumed - passes through byte-identical.
// maxWorkgroupScratchBytes bounds the growth (pass the device's
// maxComputeSharedMemorySize; 0 falls back to the 16384-byte Vulkan
// minimum). See FixIterationRPSubgroupScratchPass.
static bool FixIterationRPSubgroupScratchForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
Uint32 nativeSubgroupSize,
Uint32 maxWorkgroupScratchBytes,
bool enableSpirvValidation = false);
// Inserts the missing workgroup rendezvous between Program 203's two
// prefixSumCache reductions. Fingerprint-gated to the iterationRP shape;
// unrelated and already-repaired modules pass through byte-identical.
static bool FixIterationRPBarrierForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Re-declares 64-bit float vertex inputs as their 32-bit unsigned word pair
// (double -> uvec2, dvec2 -> uvec4) and bitcasts them back to double at entry, so no
// VK_FORMAT_R64*_SFLOAT is needed - lavapipe advertises none of them for vertex
// buffers. Vertex stage, DirectVulkan only; pairs with the Float64 case in
// VertexInputStateFactory::ToVkVertexFormat.
static bool PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Adds the Invariant decoration to every Position builtin output. GL apps
// routinely rely on cross-program position invariance for multi-pass
// equality depth tests (e.g. GEQUAL re-draws of the same geometry), and
// mobile drivers that optimize per-pipeline break that without the
// decoration. DirectVulkan only.
static bool DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Replaces the declared format of float storage images with Unknown and adds the
// matching SPIR-V capabilities. DirectVulkan uses this only when both Vulkan
// shaderStorageImage*WithoutFormat features are enabled, allowing the
@@ -152,13 +208,15 @@ namespace MobileGL {
// storage images deliberately keep their declared format for GL-compatible bit
// reinterpretation paths (for example, R32F storage accessed as r32ui).
static bool UseUnformattedFloatStorageImagesForVulkan(
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary);
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Rewrites every 64-bit float in the module to a 32-bit one, preserving every
// block offset and stride exactly (see DemoteFloat64Pass). Already part of
// SanitizeAndOptimizeBinary, which is where production reaches it; exposed
// separately so a test can drive the demotion on its own.
static bool DemoteFloat64ToFloat32(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
static Result<String> DecompileShader(SpvcSession& session);
// Parses one trivial shader in each configuration the production path can
@@ -185,18 +243,16 @@ namespace MobileGL {
// no way left to warm it.
static void ResetPrewarmLatch();
// Test-environment SPIR-V validation. When enabled, every Optimizer wrapper
// in this file validates its OUTPUT binary - the bytes a driver can actually
// receive - and a failure logs the VUID (via MGLOG_I; see the consumer for
// why not MGLOG_E) and bumps the failure latch below WITHOUT changing the
// wrapper's return value: control flow must stay identical between the
// validating and shipping configurations, or fail-open call sites would make
// the two render differently. Resolved lazily from MOBILEGL_VALIDATE_SPIRV;
// defaults on for desktop/CI/WSL builds and off for device (__ANDROID__)
// builds. The setter wins over the environment and is safe to call from test
// fixtures at any time.
static bool SpirvValidationEnabled();
static void SetSpirvValidationEnabled(bool enabled);
// Validation is an explicit immutable option of each compiler operation. The
// program-link task snapshots MOBILEGL_ENABLE_SPIRV_VALIDATION before it can run
// on a worker; standalone callers pass true directly. A failure logs the VUID and
// bumps the latch below WITHOUT changing a wrapper's return value, so validating
// and shipping configurations preserve identical rendering control flow.
// Makes validator table lifetime safe before an external final-module validator
// runs. This has no configuration state; callers invoke it only for an enabled
// task-local validation option.
static void PrepareSpirvValidation();
// The test-lane enforcement signal: total validation failures observed this
// process. Tests snapshot it, run the operation under scrutiny, and assert
@@ -23,6 +23,7 @@
namespace {
using MobileGL::SizeT;
using MobileGL::String;
using MobileGL::Uint32;
using MobileGL::Vector;
bool IsIdentifierChar(char ch) {
@@ -181,331 +182,6 @@ namespace {
return std::all_of(token.text.begin() + 1, token.text.end(), IsIdentifierChar);
}
class TokenCursor {
public:
TokenCursor(const Vector<CodeToken>& tokens, SizeT position) : m_tokens(tokens), m_position(position) {}
bool Consume(const char* expected) {
if (m_position >= m_tokens.size() || m_tokens[m_position].text != expected) {
return false;
}
++m_position;
return true;
}
bool ConsumeAnyIdentifier(String& identifier) {
if (m_position >= m_tokens.size() || !IsIdentifierToken(m_tokens[m_position])) {
return false;
}
identifier = m_tokens[m_position++].text;
return true;
}
bool ConsumeAnyIdentifier() {
if (m_position >= m_tokens.size() || !IsIdentifierToken(m_tokens[m_position])) {
return false;
}
++m_position;
return true;
}
bool ConsumeIdentifier(const String& expected) {
if (m_position >= m_tokens.size() || !IsIdentifierToken(m_tokens[m_position]) ||
m_tokens[m_position].text != expected) {
return false;
}
++m_position;
return true;
}
SizeT Position() const { return m_position; }
private:
const Vector<CodeToken>& m_tokens;
SizeT m_position;
};
SizeT CountToken(const Vector<CodeToken>& tokens, const String& tokenText) {
return static_cast<SizeT>(std::count_if(tokens.begin(), tokens.end(),
[&](const CodeToken& token) { return token.text == tokenText; }));
}
bool HasIdentifierWithPrefixOutsideAllowed(const Vector<CodeToken>& tokens, const String& prefix,
std::initializer_list<const char*> allowedIdentifiers) {
return std::any_of(tokens.begin(), tokens.end(), [&](const CodeToken& token) {
if (!IsIdentifierToken(token) || !token.text.starts_with(prefix)) {
return false;
}
return std::none_of(allowedIdentifiers.begin(), allowedIdentifiers.end(),
[&](const char* allowed) { return token.text == allowed; });
});
}
bool MatchTokenSequence(const Vector<CodeToken>& tokens, SizeT position,
std::initializer_list<const char*> expected) {
if (position + expected.size() > tokens.size()) {
return false;
}
for (const char* token : expected) {
if (tokens[position++].text != token) {
return false;
}
}
return true;
}
struct LinearPrefixScanMatch {
SizeT sharedArraySizeBegin = 0;
SizeT sharedArraySizeEnd = 0;
SizeT scanBegin = 0;
SizeT scanEnd = 0;
String cache;
String importance;
String prefixSum;
String loopLength;
String loopIndex;
String sum;
};
bool ParseLinearPrefixScanTemplate(const Vector<CodeToken>& tokens, LinearPrefixScanMatch& match) {
// The workaround deliberately recognizes one complete algorithm, not merely the
// subgroupInclusiveAdd token. Changing scratch storage is only safe when that storage is
// private to this scan and the workgroup has exactly 1024 X invocations.
SizeT localSizeDeclarationCount = 0;
for (SizeT i = 0; i < tokens.size(); ++i) {
if (MatchTokenSequence(tokens, i, {"layout", "(", "local_size_x", "=", "1024", ")", "in", ";"})) {
++localSizeDeclarationCount;
}
}
if (localSizeDeclarationCount != 1) {
return false;
}
SizeT sharedDeclarationIndex = String::npos;
SizeT sharedDeclarationCount = 0;
String cacheName;
for (SizeT i = 0; i + 6 < tokens.size(); ++i) {
if (tokens[i].text != "shared" || tokens[i + 1].text != "float" || !IsIdentifierToken(tokens[i + 2]) ||
tokens[i + 3].text != "[" || tokens[i + 4].text != "64" || tokens[i + 5].text != "]" ||
tokens[i + 6].text != ";") {
continue;
}
++sharedDeclarationCount;
sharedDeclarationIndex = i;
cacheName = tokens[i + 2].text;
}
if (sharedDeclarationCount != 1) {
return false;
}
SizeT scanTokenIndex = String::npos;
SizeT scanCount = 0;
for (SizeT i = 0; i + 7 < tokens.size(); ++i) {
if (tokens[i].text == "float" && IsIdentifierToken(tokens[i + 1]) && tokens[i + 2].text == "=" &&
tokens[i + 3].text == "subgroupInclusiveAdd" && tokens[i + 4].text == "(" &&
IsIdentifierToken(tokens[i + 5]) && tokens[i + 6].text == ")" && tokens[i + 7].text == ";") {
++scanCount;
scanTokenIndex = i;
}
}
if (scanCount != 1 || sharedDeclarationIndex >= scanTokenIndex) {
return false;
}
TokenCursor cursor(tokens, scanTokenIndex);
String prefixSum;
String importance;
String loopLength;
String loopIndex;
String sum;
if (!cursor.Consume("float") || !cursor.ConsumeAnyIdentifier(prefixSum) || !cursor.Consume("=") ||
!cursor.Consume("subgroupInclusiveAdd") || !cursor.Consume("(") ||
!cursor.ConsumeAnyIdentifier(importance) || !cursor.Consume(")") || !cursor.Consume(";") ||
!cursor.Consume("if") || !cursor.Consume("(") || !cursor.Consume("gl_SubgroupInvocationID") ||
!cursor.Consume("==") || !cursor.Consume("gl_SubgroupSize") || !cursor.Consume("-") ||
!cursor.Consume("1u") || !cursor.Consume(")") || !cursor.ConsumeIdentifier(cacheName) ||
!cursor.Consume("[") || !cursor.Consume("gl_SubgroupID") || !cursor.Consume("]") || !cursor.Consume("=") ||
!cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume(";") || !cursor.Consume("barrier") ||
!cursor.Consume("(") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("uint") ||
!cursor.ConsumeAnyIdentifier(loopLength) || !cursor.Consume("=") || !cursor.Consume("uint") ||
!cursor.Consume("(") || !cursor.Consume("findMSB") || !cursor.Consume("(") ||
!cursor.Consume("gl_NumSubgroups") || !cursor.Consume(")") || !cursor.Consume(")") ||
!cursor.Consume(";") || !cursor.ConsumeIdentifier(loopLength) || !cursor.Consume("+=") ||
!cursor.Consume("uint") || !cursor.Consume("(") || !cursor.Consume("gl_NumSubgroups") ||
!cursor.Consume("-") || !cursor.Consume("(") || !cursor.Consume("1u") || !cursor.Consume("<<") ||
!cursor.Consume("(") || !cursor.ConsumeIdentifier(loopLength) || !cursor.Consume("-") ||
!cursor.Consume("1u") || !cursor.Consume(")") || !cursor.Consume(")") || !cursor.Consume(">") ||
!cursor.Consume("0u") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("for") ||
!cursor.Consume("(") || !cursor.Consume("uint") || !cursor.ConsumeAnyIdentifier(loopIndex) ||
!cursor.Consume("=") || !cursor.Consume("0") || !cursor.Consume(";") ||
!cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume("<") || !cursor.ConsumeIdentifier(loopLength) ||
!cursor.Consume(";") || !cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume("++") ||
!cursor.Consume(")") || !cursor.Consume("{") || !cursor.Consume("if") || !cursor.Consume("(") ||
!cursor.Consume("(") || !cursor.Consume("gl_SubgroupID") || !cursor.Consume("&") || !cursor.Consume("(") ||
!cursor.Consume("1u") || !cursor.Consume("<<") || !cursor.ConsumeIdentifier(loopIndex) ||
!cursor.Consume(")") || !cursor.Consume(")") || !cursor.Consume(">") || !cursor.Consume("0u") ||
!cursor.Consume(")") || !cursor.Consume("{") || !cursor.ConsumeIdentifier(prefixSum) ||
!cursor.Consume("+=") || !cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") ||
!cursor.Consume("(") || !cursor.Consume("gl_SubgroupID") || !cursor.Consume(">>") ||
!cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume("<<") || !cursor.ConsumeIdentifier(loopIndex) ||
!cursor.Consume(")") || !cursor.Consume("-") || !cursor.Consume("1u") || !cursor.Consume("]") ||
!cursor.Consume(";") || !cursor.Consume("if") || !cursor.Consume("(") ||
!cursor.Consume("gl_SubgroupInvocationID") || !cursor.Consume("==") || !cursor.Consume("gl_SubgroupSize") ||
!cursor.Consume("-") || !cursor.Consume("1u") || !cursor.Consume(")") ||
!cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") || !cursor.Consume("gl_SubgroupID") ||
!cursor.Consume("]") || !cursor.Consume("=") || !cursor.ConsumeIdentifier(prefixSum) ||
!cursor.Consume(";") || !cursor.Consume("}") || !cursor.Consume("barrier") || !cursor.Consume("(") ||
!cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("}") || !cursor.Consume("if") ||
!cursor.Consume("(") || !cursor.Consume("gl_LocalInvocationID") || !cursor.Consume(".") ||
!cursor.Consume("x") || !cursor.Consume("==") || !cursor.Consume("uint") || !cursor.Consume("(") ||
!cursor.Consume("1024") || !cursor.Consume("-") || !cursor.Consume("1") || !cursor.Consume(")") ||
!cursor.Consume(")") || !cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") ||
!cursor.Consume("0") || !cursor.Consume("]") || !cursor.Consume("=") ||
!cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume(";") || !cursor.Consume("barrier") ||
!cursor.Consume("(") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("float") ||
!cursor.ConsumeAnyIdentifier(sum) || !cursor.Consume("=") || !cursor.ConsumeIdentifier(cacheName) ||
!cursor.Consume("[") || !cursor.Consume("0") || !cursor.Consume("]") || !cursor.Consume(";")) {
return false;
}
const SizeT scanEndToken = cursor.Position() - 1;
// Require the scan's immediate consumer as well. This makes the match specific to a
// linear distribution warp, and avoids changing unrelated prefix scans which may rely on
// the implementation's native subgroup partitioning.
if (!cursor.Consume("float") || !cursor.ConsumeAnyIdentifier() || !cursor.Consume("=") ||
!cursor.Consume("(") || !cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume("-") ||
!cursor.ConsumeIdentifier(importance) || !cursor.Consume(")") || !cursor.Consume("/") ||
!cursor.ConsumeIdentifier(sum) || !cursor.Consume("-") || !cursor.Consume("float") ||
!cursor.Consume("(") || !cursor.Consume("gl_LocalInvocationID") || !cursor.Consume(".") ||
!cursor.Consume("x") || !cursor.Consume("+") || !cursor.Consume("1u") || !cursor.Consume(")") ||
!cursor.Consume("/") || !cursor.Consume("float") || !cursor.Consume("(") || !cursor.Consume("1024") ||
!cursor.Consume(")") || !cursor.Consume(";")) {
return false;
}
// No other use may share the scratch array, and no additional subgroup operation or
// builtin may silently retain native-64 semantics after this module becomes virtual-32.
if (CountToken(tokens, cacheName) != 6 || CountToken(tokens, "subgroupInclusiveAdd") != 1 ||
CountToken(tokens, "gl_SubgroupInvocationID") != 2 || CountToken(tokens, "gl_SubgroupSize") != 2 ||
CountToken(tokens, "gl_SubgroupID") != 4 || CountToken(tokens, "gl_NumSubgroups") != 2 ||
CountToken(tokens, "gl_LocalInvocationID") != 2 || CountToken(tokens, "barrier") != 3 ||
CountToken(tokens, "findMSB") != 1 ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "subgroup", {"subgroupInclusiveAdd"}) ||
HasIdentifierWithPrefixOutsideAllowed(
tokens, "gl_Subgroup",
{"gl_SubgroupInvocationID", "gl_SubgroupSize", "gl_SubgroupID", "gl_NumSubgroups"}) ||
// ARB/NV spellings of lane-width-sensitive builtins and functions
// (gl_SubGroupSizeARB, ballotARB, gl_WarpSizeNV, shuffleNV, ...) must block the
// rewrite just like their KHR counterparts: they would silently keep native-width
// semantics in a module rewritten to the virtual 32-lane model.
HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_SubGroup", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_Warp", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_Thread", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_SMID", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "ballot", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "shuffle", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "readInvocation", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "readFirstInvocation", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "anyInvocation", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "allInvocations", {})) {
return false;
}
// The scan must be at the top level of the sole main() body. Its existing barriers already
// require uniform control flow; this check prevents us from introducing extra barriers in
// a nested branch or loop.
SizeT mainOpenBrace = String::npos;
SizeT mainCloseBrace = String::npos;
SizeT mainCount = 0;
for (SizeT i = 0; i + 4 < tokens.size(); ++i) {
if (!MatchTokenSequence(tokens, i, {"void", "main", "(", ")", "{"})) {
continue;
}
++mainCount;
mainOpenBrace = i + 4;
int depth = 1;
for (SizeT j = mainOpenBrace + 1; j < tokens.size(); ++j) {
if (tokens[j].text == "{")
++depth;
else if (tokens[j].text == "}" && --depth == 0) {
mainCloseBrace = j;
break;
}
}
}
if (mainCount != 1 || mainCloseBrace == String::npos || scanTokenIndex <= mainOpenBrace ||
scanEndToken >= mainCloseBrace) {
return false;
}
int depthAtScan = 1;
for (SizeT i = mainOpenBrace + 1; i < scanTokenIndex; ++i) {
if (tokens[i].text == "{")
++depthAtScan;
else if (tokens[i].text == "}")
--depthAtScan;
}
if (depthAtScan != 1) {
return false;
}
constexpr const char* injectedNames[] = {"mglPrefixScanLane", "mglVirtualSubgroupInvocation",
"mglVirtualSubgroup", "mglVirtualSubgroupBase",
"mglPrefixLane", "mglVirtualSubgroupCount"};
for (const char* injectedName : injectedNames) {
if (CountToken(tokens, injectedName) != 0) {
return false;
}
}
match.sharedArraySizeBegin = tokens[sharedDeclarationIndex + 4].begin;
match.sharedArraySizeEnd = tokens[sharedDeclarationIndex + 4].end;
match.scanBegin = tokens[scanTokenIndex].begin;
match.scanEnd = tokens[scanEndToken].end;
match.cache = std::move(cacheName);
match.importance = std::move(importance);
match.prefixSum = std::move(prefixSum);
match.loopLength = std::move(loopLength);
match.loopIndex = std::move(loopIndex);
match.sum = std::move(sum);
return true;
}
String BuildLinearPrefixScanReplacement(const LinearPrefixScanMatch& match) {
String replacement;
replacement.reserve(1800);
replacement += "uint mglPrefixScanLane = gl_LocalInvocationID.x;\n";
replacement += "uint mglVirtualSubgroupInvocation = mglPrefixScanLane & 31u;\n";
replacement += "uint mglVirtualSubgroup = mglPrefixScanLane >> 5u;\n";
replacement += "const uint mglVirtualSubgroupCount = 32u;\n";
replacement += match.cache + "[mglPrefixScanLane] = " + match.importance + ";\n";
replacement += "barrier();\n";
replacement += "float " + match.prefixSum + " = 0.0f;\n";
replacement += "uint mglVirtualSubgroupBase = mglVirtualSubgroup << 5u;\n";
replacement += "for (uint mglPrefixLane = mglVirtualSubgroupBase; "
"mglPrefixLane <= mglPrefixScanLane; ++mglPrefixLane) {\n";
replacement += match.prefixSum + " += " + match.cache + "[mglPrefixLane];\n";
replacement += "}\n";
replacement += "barrier();\n";
replacement += "if (mglVirtualSubgroupInvocation == 31u) " + match.cache +
"[mglVirtualSubgroup] = " + match.prefixSum + ";\n";
replacement += "barrier();\n";
replacement += "uint " + match.loopLength + " = uint(findMSB(mglVirtualSubgroupCount));\n";
replacement +=
match.loopLength + " += uint(mglVirtualSubgroupCount - (1u << (" + match.loopLength + " - 1u)) > 0u);\n";
replacement += "for (uint " + match.loopIndex + " = 0u; " + match.loopIndex + " < " + match.loopLength +
"; ++" + match.loopIndex + ") {\n";
replacement += "if ((mglVirtualSubgroup & (1u << " + match.loopIndex + ")) > 0u) {\n";
replacement += match.prefixSum + " += " + match.cache + "[(mglVirtualSubgroup >> " + match.loopIndex + " << " +
match.loopIndex + ") - 1u];\n";
replacement += "if (mglVirtualSubgroupInvocation == 31u) " + match.cache +
"[mglVirtualSubgroup] = " + match.prefixSum + ";\n";
replacement += "}\nbarrier();\n}\n";
replacement += "if (mglPrefixScanLane == 1023u) " + match.cache + "[0] = " + match.prefixSum + ";\n";
replacement += "barrier();\n";
replacement += "float " + match.sum + " = " + match.cache + "[0];";
return replacement;
}
void SkipDirectiveWhitespace(const MobileGL::String& source, SizeT& pos, SizeT lineEnd) {
while (pos < lineEnd && std::isspace(static_cast<unsigned char>(source[pos]))) {
pos++;
@@ -1253,117 +929,6 @@ namespace {
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
Bool RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage stage, Uint32 nativeSubgroupSize,
String& source) {
constexpr Uint32 capturedSubgroupSize = 32;
if (stage != ShaderStage::Compute || nativeSubgroupSize <= capturedSubgroupSize ||
nativeSubgroupSize % capturedSubgroupSize != 0) {
return false;
}
// Vulkan subgroup widths are powers of two. Keep the workaround restricted to
// wider widths which are a power-of-two multiple of the captured 32-lane model.
const Uint32 subgroupScale = nativeSubgroupSize / capturedSubgroupSize;
if ((subgroupScale & (subgroupScale - 1u)) != 0u) {
return false;
}
const Vector<CodeToken> tokens = TokenizeCode(source);
LinearPrefixScanMatch match;
if (!ParseLinearPrefixScanTemplate(tokens, match)) {
// Diagnosability: when the trigger op is present but the template no longer
// matches (e.g. the pack shipped a new shader revision), the affected device
// silently falls back to the driver's miscompiled path. Make that visible.
if (CountToken(tokens, "subgroupInclusiveAdd") > 0) {
MGLOG_W_ONCE("%s: subgroupInclusiveAdd present but the linear prefix-scan template "
"did not match; the wide-subgroup rewrite was NOT applied",
__func__);
}
return false;
}
const String replacement = BuildLinearPrefixScanReplacement(match);
source.replace(match.scanBegin, match.scanEnd - match.scanBegin, replacement);
// The declaration occurs before the replaced scan, so its original offsets remain
// valid after the first replacement.
source.replace(match.sharedArraySizeBegin, match.sharedArraySizeEnd - match.sharedArraySizeBegin,
"1024");
return true;
}
namespace {
struct ShaderSourceQuirkContext {
ShaderStage stage = ShaderStage::Unknown;
BackendType backend = BackendType::Unknown;
MG_Backend::GpuVendorKind vendor = MG_Backend::GpuVendorKind::Unknown;
Uint32 subgroupSize = 0;
};
// Device-quirk registry. Every entry is a narrowly scoped source rewrite that
// works around a specific driver defect. A quirk runs when its env override
// forces it on, or when the override is Auto and DeviceApplies matches the
// detected device. ForceOn bypasses only the device gate - each Apply keeps
// its own structural safety checks. Add new per-device workarounds here
// instead of open-coding them in PreprocessShaderSource.
struct ShaderSourceQuirk {
const char* name;
// Reads the override out of the captured env, never out of the live
// MG_Config table: a worker must see the same config the GL thread saw.
MG_Config::QuirkOverride (*GetOverride)(const CompileEnv&);
Bool (*DeviceApplies)(const ShaderSourceQuirkContext&);
Bool (*Apply)(const ShaderSourceQuirkContext&, String&);
};
constexpr ShaderSourceQuirk kShaderSourceQuirks[] = {
{
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN
"subgroup-prefix-scan-rewrite",
[](const CompileEnv& env) { return env.subgroupPrefixScanQuirk; },
[](const ShaderSourceQuirkContext& ctx) {
// Qualcomm's Vulkan driver miscompiles the recognized float
// InclusiveScan pattern for native subgroups wider than the
// captured 32 lanes; other vendors compile it correctly and
// should keep their native scan.
return ctx.backend == BackendType::DirectVulkan &&
ctx.vendor == MG_Backend::GpuVendorKind::Qualcomm;
},
[](const ShaderSourceQuirkContext& ctx, String& source) {
return RewriteLinearSubgroupPrefixScanForVulkan(ctx.stage, ctx.subgroupSize,
source);
},
},
};
void ApplyShaderSourceQuirks(const CompileEnv& env, ShaderStage stage, String& source) {
// No backend at capture time means no device to match a quirk against,
// and (as before) no quirk can fire - not even a forced one, because
// every Apply reads device parameters that do not exist yet.
if (!env.HasBackend()) {
return;
}
const ShaderSourceQuirkContext quirkContext{
stage,
env.backend,
env.params.GpuVendor,
env.params.SubgroupSize,
};
for (const ShaderSourceQuirk& quirk : kShaderSourceQuirks) {
const MG_Config::QuirkOverride quirkOverride = quirk.GetOverride(env);
if (quirkOverride == MG_Config::QuirkOverride::ForceOff) {
continue;
}
if (quirkOverride == MG_Config::QuirkOverride::Auto &&
!quirk.DeviceApplies(quirkContext)) {
continue;
}
if (quirk.Apply(quirkContext, source)) {
MGLOG_D("ApplyShaderSourceQuirks: applied '%s'%s", quirk.name,
quirkOverride == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : "");
}
}
}
} // namespace
void PreprocessShaderSource(ShaderStage stage, String& source) {
PreprocessShaderSource(stage, source, *GetCurrentCompileEnv());
}
@@ -1403,7 +968,6 @@ namespace MobileGL {
ModernizeLegacyGLSL(stage, source, afterVersion);
InjectDepthRangeBuiltinShim(stage, source, afterVersion);
ApplyShaderSourceQuirks(env, stage, source);
}
Bool RetargetLegacyVersionDirectiveTo460(String& source) {
@@ -30,18 +30,6 @@ namespace MobileGL {
// tests and diagnostics that drive the preprocessor standalone.
void PreprocessShaderSource(ShaderStage stage, String& source);
// Some desktop-captured compute shaders build a workgroup-wide linear prefix scan
// from subgroupInclusiveAdd plus a shared array of subgroup totals. Qualcomm's
// Vulkan driver miscompiles that exact float InclusiveScan path for native subgroups
// wider than the capture's 32 lanes. For the narrowly recognized, uniform-control-
// flow template, replace the subgroup-local scan with a shared-memory, strict
// left-fold over virtual 32-lane segments. Returns true only when the complete safe
// template was recognized and rewritten. PreprocessShaderSource reaches this through
// its device-quirk registry: by default only on detected Qualcomm Vulkan devices,
// overridable either way with MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN=1/0. The explicit
// entry point exists for deterministic tests.
Bool RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage stage, Uint32 nativeSubgroupSize, String& source);
// Rewrites a "#version 330 core" directive that PreprocessShaderSource normalized down
// from a legacy desktop version back up to "#version 460 core". Returns false (leaving
// the source untouched) for anything else: ES, compatibility, or an already-modern
@@ -91,9 +91,15 @@ namespace MobileGL {
BlockRelayout(IRContext* irContext, Bool std140)
: m_irContext(irContext), m_std140(std140) {}
// Size and alignment of `typeId`, applying every stride decoration it implies
// on the way down. Zero size means "not a type this layout knows how to
// describe"; the caller then leaves the block alone rather than guessing.
// Size and alignment of `typeId`, QUEUING every offset/stride decoration it
// implies on the way down. Zero size means "not a type this layout knows how
// to describe"; the caller then leaves the block alone rather than guessing.
// The queue is what makes that fallback honest: measurement must be
// side-effect-free until it is known to succeed, or a mid-struct failure
// would leave the block half-relaid-out - members before the failing one at
// compacted 32-bit offsets, members after it at the original 64-bit ones, a
// layout matching neither convention. Commit() flushes the queue and is
// called only on a successful Measure of the whole block.
struct Extent {
Uint32 size = 0;
Uint32 alignment = 0;
@@ -108,7 +114,29 @@ namespace MobileGL {
return extent;
}
// Flushes the decoration writes a successful Measure queued. Call exactly
// once, only when Measure returned a non-zero size; a failed measurement's
// queue dies with this per-block instance, leaving the module untouched.
void Commit() {
for (const PendingDecoration& pending : m_pendingWrites) {
if (pending.member) {
ApplyMemberDecoration(pending.targetId, pending.memberIndex, pending.decoration,
pending.value);
} else {
ApplyTypeDecoration(pending.targetId, pending.decoration, pending.value);
}
}
m_pendingWrites.clear();
}
private:
struct PendingDecoration {
Bool member = false;
Uint32 targetId = 0;
Uint32 memberIndex = 0;
spv::Decoration decoration = spv::Decoration::Offset;
Uint32 value = 0;
};
Extent MeasureUncached(Uint32 typeId) {
const Instruction* type = m_irContext->get_def_use_mgr()->GetDef(typeId);
if (type == nullptr) return {};
@@ -204,7 +232,17 @@ namespace MobileGL {
return length->GetSingleWordInOperand(0);
}
// Queue-only during measurement; the module is mutated in Commit().
void SetTypeDecoration(Uint32 targetId, spv::Decoration decoration, Uint32 value) {
m_pendingWrites.push_back({false, targetId, 0, decoration, value});
}
void SetMemberDecoration(Uint32 structId, Uint32 member, spv::Decoration decoration,
Uint32 value) {
m_pendingWrites.push_back({true, structId, member, decoration, value});
}
void ApplyTypeDecoration(Uint32 targetId, spv::Decoration decoration, Uint32 value) {
for (Instruction& annotation : m_irContext->annotations()) {
if (annotation.opcode() != spv::Op::OpDecorate) continue;
if (annotation.GetSingleWordInOperand(0) != targetId) continue;
@@ -216,8 +254,8 @@ namespace MobileGL {
}
}
void SetMemberDecoration(Uint32 structId, Uint32 member, spv::Decoration decoration,
Uint32 value) {
void ApplyMemberDecoration(Uint32 structId, Uint32 member, spv::Decoration decoration,
Uint32 value) {
for (Instruction& annotation : m_irContext->annotations()) {
if (annotation.opcode() != spv::Op::OpMemberDecorate) continue;
if (annotation.GetSingleWordInOperand(0) != structId) continue;
@@ -233,6 +271,7 @@ namespace MobileGL {
IRContext* m_irContext = nullptr;
Bool m_std140 = true;
std::unordered_map<Uint32, Extent> m_extents;
std::vector<PendingDecoration> m_pendingWrites;
};
} // namespace
@@ -529,9 +568,12 @@ namespace MobileGL {
// A member shape the layout rules here do not describe. Leaving the block
// at its 64-bit offsets keeps the module valid for Vulkan; SPIRV-Cross will
// decline it for ESSL, which is the same outcome as before the demotion.
// Nothing was written: Measure only queues, and the queue dies here.
MGLOG_D("DemoteFloat64Pass: block %%%u contains a member this pass cannot lay "
"out; its 64-bit offsets are left in place",
blockType->result_id());
} else {
relayout.Commit();
}
}
@@ -0,0 +1,257 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "DeriveNumSubgroupsPass.h"
#include "spirv.hpp"
#include "source/opt/constants.h"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.h"
#include "source/opt/ir_context.h"
#include "source/opt/module.h"
#include "source/util/make_unique.h"
#include <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
using spvtools::opt::Operand;
Instruction* FindBuiltinDefinition(IRContext* context, spv::BuiltIn builtin) {
auto* defUseMgr = context->get_def_use_mgr();
for (auto& annotation : context->annotations()) {
if (annotation.opcode() != spv::Op::OpDecorate || annotation.NumInOperands() < 3) {
continue;
}
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
spv::Decoration::BuiltIn) {
continue;
}
if (static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2)) != builtin) {
continue;
}
return defUseMgr->GetDef(annotation.GetSingleWordInOperand(0));
}
return nullptr;
}
bool IsInputPointerTo(IRContext* context, const Instruction* variable, uint32_t pointeeTypeId) {
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable ||
variable->NumInOperands() < 1 ||
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) !=
spv::StorageClass::Input) {
return false;
}
const Instruction* pointerType = context->get_def_use_mgr()->GetDef(variable->type_id());
return pointerType != nullptr && pointerType->opcode() == spv::Op::OpTypePointer &&
pointerType->NumInOperands() >= 2 &&
static_cast<spv::StorageClass>(pointerType->GetSingleWordInOperand(0)) ==
spv::StorageClass::Input &&
pointerType->GetSingleWordInOperand(1) == pointeeTypeId;
}
bool IsUnsignedInt32(IRContext* context, uint32_t typeId) {
const Instruction* type = context->get_def_use_mgr()->GetDef(typeId);
return type != nullptr && type->opcode() == spv::Op::OpTypeInt &&
type->NumInOperands() >= 2 && type->GetSingleWordInOperand(0) == 32u &&
type->GetSingleWordInOperand(1) == 0u;
}
uint32_t SynthesizeSubgroupSizeVariable(IRContext* context, uint32_t pointerTypeId) {
const uint32_t variableId = context->TakeNextId();
context->AddGlobalValue(spvtools::MakeUnique<Instruction>(
context, spv::Op::OpVariable, pointerTypeId, variableId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_STORAGE_CLASS,
{static_cast<uint32_t>(spv::StorageClass::Input)}}}));
context->AddAnnotationInst(spvtools::MakeUnique<Instruction>(
context, spv::Op::OpDecorate, 0, 0,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {variableId}},
{SPV_OPERAND_TYPE_DECORATION,
{static_cast<uint32_t>(spv::Decoration::BuiltIn)}},
{SPV_OPERAND_TYPE_LITERAL_INTEGER,
{static_cast<uint32_t>(spv::BuiltIn::SubgroupSize)}}}));
for (Instruction& entryPoint : context->module()->entry_points()) {
entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {variableId}});
}
return variableId;
}
} // namespace
spvtools::opt::Pass::Status DeriveNumSubgroupsPass::Process() {
auto* irContext = context();
auto* defUseMgr = irContext->get_def_use_mgr();
Instruction* numSubgroupsVar = FindBuiltinDefinition(irContext, spv::BuiltIn::NumSubgroups);
if (numSubgroupsVar == nullptr) {
return Status::SuccessWithoutChange;
}
std::vector<Instruction*> numSubgroupsLoads;
bool sawUnexpectedUser = false;
const uint32_t numSubgroupsVarId = numSubgroupsVar->result_id();
defUseMgr->ForEachUser(numSubgroupsVar, [&](Instruction* user) {
switch (user->opcode()) {
case spv::Op::OpLoad:
if (user->NumInOperands() >= 1 &&
user->GetSingleWordInOperand(0) == numSubgroupsVarId) {
numSubgroupsLoads.push_back(user);
} else {
sawUnexpectedUser = true;
}
return;
case spv::Op::OpDecorate:
case spv::Op::OpDecorateId:
case spv::Op::OpDecorateString:
case spv::Op::OpName:
case spv::Op::OpEntryPoint:
return;
default:
sawUnexpectedUser = true;
return;
}
});
if (sawUnexpectedUser) {
return Status::Failure;
}
if (numSubgroupsLoads.empty()) {
return Status::SuccessWithoutChange;
}
const uint32_t valueTypeId = numSubgroupsLoads.front()->type_id();
if (!IsUnsignedInt32(irContext, valueTypeId) ||
!IsInputPointerTo(irContext, numSubgroupsVar, valueTypeId)) {
return Status::Failure;
}
for (const Instruction* load : numSubgroupsLoads) {
if (load->type_id() != valueTypeId) {
return Status::Failure;
}
}
Instruction* workgroupSize = FindBuiltinDefinition(irContext, spv::BuiltIn::WorkgroupSize);
if (workgroupSize == nullptr ||
(workgroupSize->opcode() != spv::Op::OpConstantComposite &&
workgroupSize->opcode() != spv::Op::OpSpecConstantComposite)) {
return Status::Failure;
}
const Instruction* workgroupSizeType = defUseMgr->GetDef(workgroupSize->type_id());
if (workgroupSizeType == nullptr || workgroupSizeType->opcode() != spv::Op::OpTypeVector ||
workgroupSizeType->NumInOperands() < 2 ||
workgroupSizeType->GetSingleWordInOperand(0) != valueTypeId ||
workgroupSizeType->GetSingleWordInOperand(1) != 3u) {
return Status::Failure;
}
Instruction* subgroupSizeVar = FindBuiltinDefinition(irContext, spv::BuiltIn::SubgroupSize);
if (subgroupSizeVar != nullptr &&
!IsInputPointerTo(irContext, subgroupSizeVar, valueTypeId)) {
return Status::Failure;
}
auto* constantMgr = irContext->get_constant_mgr();
auto* typeMgr = irContext->get_type_mgr();
const auto* valueType = typeMgr->GetType(valueTypeId);
if (valueType == nullptr) {
return Status::Failure;
}
const auto* one = constantMgr->GetConstant(valueType, {1u});
const Instruction* oneInst =
one != nullptr ? constantMgr->GetDefiningInstruction(one, valueTypeId) : nullptr;
if (oneInst == nullptr) {
return Status::Failure;
}
const uint32_t oneId = oneInst->result_id();
const uint32_t subgroupSizeVarId = subgroupSizeVar != nullptr
? subgroupSizeVar->result_id()
: SynthesizeSubgroupSizeVariable(irContext, numSubgroupsVar->type_id());
const uint32_t workgroupSizeId = workgroupSize->result_id();
// ceil(local invocation count / SubgroupSize): the subgroup count of a
// full-subgroup launch. Vulkan only guarantees that partition under
// REQUIRE_FULL_SUBGROUPS - which ProgramFactory requests whenever
// local_size_x is a multiple of the subgroup size makes it legal
// (VUID-VkPipelineShaderStageCreateInfo-flags-02759) - and calls the
// tighter behaviour "encouraged" everywhere else; the DriverPost witness
// verifies it per device where the flag cannot be set. The absence of
// ALLOW_VARYING_SUBGROUP_SIZE pins only the SubgroupSize builtin itself.
// `(count - 1) / size + 1` avoids an addition overflow at count + size - 1.
for (Instruction* load : numSubgroupsLoads) {
const uint32_t localSizeXId = irContext->TakeNextId();
const uint32_t localSizeYId = irContext->TakeNextId();
const uint32_t localSizeZId = irContext->TakeNextId();
const uint32_t localSizeXYId = irContext->TakeNextId();
const uint32_t invocationCountId = irContext->TakeNextId();
const uint32_t adjustedCountId = irContext->TakeNextId();
const uint32_t subgroupSizeId = irContext->TakeNextId();
const uint32_t quotientId = irContext->TakeNextId();
load->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpCompositeExtract, valueTypeId, localSizeXId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {workgroupSizeId}},
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {0u}}}));
load->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpCompositeExtract, valueTypeId, localSizeYId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {workgroupSizeId}},
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {1u}}}));
load->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpCompositeExtract, valueTypeId, localSizeZId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {workgroupSizeId}},
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {2u}}}));
load->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpIMul, valueTypeId, localSizeXYId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {localSizeXId}},
{SPV_OPERAND_TYPE_ID, {localSizeYId}}}));
load->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpIMul, valueTypeId, invocationCountId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {localSizeXYId}},
{SPV_OPERAND_TYPE_ID, {localSizeZId}}}));
load->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpISub, valueTypeId, adjustedCountId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {invocationCountId}},
{SPV_OPERAND_TYPE_ID, {oneId}}}));
load->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpLoad, valueTypeId, subgroupSizeId,
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {subgroupSizeVarId}}}));
load->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpUDiv, valueTypeId, quotientId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {adjustedCountId}},
{SPV_OPERAND_TYPE_ID, {subgroupSizeId}}}));
// Preserve the original result id so every downstream use automatically sees
// the derived value instead of the driver's NumSubgroups builtin.
load->SetOpcode(spv::Op::OpIAdd);
load->SetInOperands(Instruction::OperandList{
{SPV_OPERAND_TYPE_ID, {quotientId}},
{SPV_OPERAND_TYPE_ID, {oneId}}});
}
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken DeriveNumSubgroupsPass::CreateDeriveNumSubgroupsPass() {
return spvtools::Optimizer::PassToken(MakeUnique<DeriveNumSubgroupsPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,39 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.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 "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// Replaces compute-stage NumSubgroups builtin loads with
// ceil(WorkgroupSize.x * WorkgroupSize.y * WorkgroupSize.z / SubgroupSize).
//
// That is the subgroup count of a full-subgroup launch - guaranteed by Vulkan
// under REQUIRE_FULL_SUBGROUPS (which ProgramFactory requests whenever the
// workgroup shape makes it legal), spec-"encouraged" and witness-verified
// (DriverPost) elsewhere. Deriving it repairs drivers that expose the real
// SubgroupId topology but return an inconsistent NumSubgroups value, breaking
// GL's gl_SubgroupID < gl_NumSubgroups contract. This is a DirectVulkan
// semantic repair, not a source-shader rewrite; the application's subgroup
// arithmetic and shared-memory logic remain unchanged.
class DeriveNumSubgroupsPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "derive-num-subgroups"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateDeriveNumSubgroupsPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,73 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.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 "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// Lowers every GL_KHR_shader_subgroup construct in a compute module onto a
// 32-lane VIRTUAL subgroup implemented with workgroup-shared memory. Virtual
// subgroups partition the workgroup by gl_LocalInvocationIndex:
// lane = index & 31, id = index >> 5, count = ceil(invocations / 32).
//
// This is a LAST-RESORT path, never a substitute for real subgroups: it only
// runs when MOBILEGL_MAGMA_EMULATE_SUBGROUP=1 is set explicitly and the device
// has no native subgroup support at all (SubgroupSupportPolicy.h). A device
// with native subgroup operations - however narrow - uses them natively, with
// FixIterationRPSubgroupScratchPass patching the known pack bug instead.
//
// Lowered constructs:
// - the builtins gl_SubgroupSize / gl_SubgroupInvocationID / gl_SubgroupID /
// gl_NumSubgroups and the five gl_Subgroup*Mask ballot builtins;
// - OpGroupNonUniform{Elect,All,Any,AllEqual,Broadcast,BroadcastFirst,
// Ballot,InverseBallot,BallotBitExtract,BallotBitCount,BallotFind{L,M}SB,
// Shuffle,ShuffleXor,ShuffleUp,ShuffleDown,
// <arithmetic/min/max/bitwise/logical reduce+scans+clustered>,
// QuadBroadcast,QuadSwap};
// - subgroupBarrier()/subgroupMemoryBarrier*() (their Subgroup scopes widen
// to Workgroup, which is strictly stronger).
// The output uses no GroupNonUniform* instruction or capability at all, which
// is what lets it run on devices with no subgroup feature bits.
//
// Semantic contract, narrower than native subgroups in exactly one way: every
// emulated exchange synchronizes through OpControlBarrier, so subgroup
// operations must sit in WORKGROUP-uniform control flow (the shape every
// Iris-style pack reduction has). GLSL already imposes this for barrier();
// a subgroup op in divergent flow - legal on native subgroups - is undefined
// here.
//
// Fails (Status::Failure, leaving the input module unchanged) on anything it
// cannot lower faithfully: extended subgroup ops (partitioned-NV, rotate,
// quad-all/any), non-32-bit participating types, spec-constant workgroup
// sizes, a subgroup builtin reached by anything but a direct OpLoad, or a
// module whose lowering would add more workgroup scratch than
// maxWorkgroupScratchBytes (pass the device's maxComputeSharedMemorySize;
// 0 falls back to the 16384-byte Vulkan minimum).
class EmulateSubgroupsPass : public spvtools::opt::Pass {
public:
explicit EmulateSubgroupsPass(Uint32 maxWorkgroupScratchBytes)
: m_maxWorkgroupScratchBytes(maxWorkgroupScratchBytes) {}
const char* name() const override { return "emulate-subgroups"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateEmulateSubgroupsPass(
Uint32 maxWorkgroupScratchBytes);
private:
Uint32 m_maxWorkgroupScratchBytes;
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,232 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "FixIterationRPBarrierPass.h"
#include "spirv.hpp"
#include "source/opt/constants.h"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.h"
#include "source/opt/ir_context.h"
#include "source/opt/module.h"
#include "source/util/make_unique.h"
#include <vector>
namespace MobileGL::MG_Util::ShaderTranspiler {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
using spvtools::opt::Operand;
const Instruction* RootVariable(IRContext* context, uint32_t pointerId) {
const Instruction* def = context->get_def_use_mgr()->GetDef(pointerId);
while (def != nullptr) {
switch (def->opcode()) {
case spv::Op::OpVariable:
return def;
case spv::Op::OpAccessChain:
case spv::Op::OpInBoundsAccessChain:
case spv::Op::OpCopyObject:
def = context->get_def_use_mgr()->GetDef(def->GetSingleWordInOperand(0));
break;
default:
return nullptr;
}
}
return nullptr;
}
bool IsUintConstant(IRContext* context, uint32_t id, uint32_t wanted) {
const Instruction* def = context->get_def_use_mgr()->GetDef(id);
return def != nullptr && def->opcode() == spv::Op::OpConstant && def->NumInOperands() == 1u &&
def->GetSingleWordInOperand(0) == wanted;
}
bool IsZeroElementPointer(IRContext* context, uint32_t pointerId, const Instruction** root) {
const Instruction* pointer = context->get_def_use_mgr()->GetDef(pointerId);
if (pointer == nullptr ||
(pointer->opcode() != spv::Op::OpAccessChain && pointer->opcode() != spv::Op::OpInBoundsAccessChain) ||
pointer->NumInOperands() < 2u) {
return false;
}
for (uint32_t i = 1u; i < pointer->NumInOperands(); ++i) {
if (!IsUintConstant(context, pointer->GetSingleWordInOperand(i), 0u)) return false;
}
*root = RootVariable(context, pointerId);
return *root != nullptr;
}
bool IsWorkgroupVec2Array(IRContext* context, const Instruction* variable) {
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable || variable->NumInOperands() < 1u ||
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) != spv::StorageClass::Workgroup) {
return false;
}
auto* defUseMgr = context->get_def_use_mgr();
const Instruction* pointerType = defUseMgr->GetDef(variable->type_id());
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer ||
pointerType->NumInOperands() < 2u) {
return false;
}
const Instruction* arrayType = defUseMgr->GetDef(pointerType->GetSingleWordInOperand(1));
if (arrayType == nullptr || arrayType->opcode() != spv::Op::OpTypeArray ||
arrayType->NumInOperands() < 2u) {
return false;
}
const Instruction* length = defUseMgr->GetDef(arrayType->GetSingleWordInOperand(1));
if (length == nullptr || length->opcode() != spv::Op::OpConstant || length->NumInOperands() != 1u) {
return false;
}
const uint32_t arrayLength = length->GetSingleWordInOperand(0);
if (arrayLength < 32u || arrayLength > 512u) return false;
const Instruction* vectorType = defUseMgr->GetDef(arrayType->GetSingleWordInOperand(0));
if (vectorType == nullptr || vectorType->opcode() != spv::Op::OpTypeVector ||
vectorType->NumInOperands() < 2u || vectorType->GetSingleWordInOperand(1) != 2u) {
return false;
}
const Instruction* scalarType = defUseMgr->GetDef(vectorType->GetSingleWordInOperand(0));
return scalarType != nullptr && scalarType->opcode() == spv::Op::OpTypeFloat &&
scalarType->NumInOperands() == 1u && scalarType->GetSingleWordInOperand(0) == 32u;
}
bool IsVec2FloatInclusiveAdd(IRContext* context, const Instruction* inst) {
if (inst->opcode() != spv::Op::OpGroupNonUniformFAdd || inst->NumInOperands() < 3u ||
static_cast<spv::GroupOperation>(inst->GetSingleWordInOperand(1)) !=
spv::GroupOperation::InclusiveScan) {
return false;
}
const Instruction* vectorType = context->get_def_use_mgr()->GetDef(inst->type_id());
if (vectorType == nullptr || vectorType->opcode() != spv::Op::OpTypeVector ||
vectorType->NumInOperands() < 2u || vectorType->GetSingleWordInOperand(1) != 2u) {
return false;
}
const Instruction* scalarType = context->get_def_use_mgr()->GetDef(vectorType->GetSingleWordInOperand(0));
return scalarType != nullptr && scalarType->opcode() == spv::Op::OpTypeFloat &&
scalarType->NumInOperands() == 1u && scalarType->GetSingleWordInOperand(0) == 32u;
}
bool HasProgram203LocalSize(IRContext* context) {
for (const Instruction& entryPoint : context->module()->entry_points()) {
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) !=
spv::ExecutionModel::GLCompute) {
return false;
}
}
for (const Instruction& mode : context->module()->execution_modes()) {
if (mode.opcode() == spv::Op::OpExecutionMode && mode.NumInOperands() >= 5u &&
static_cast<spv::ExecutionMode>(mode.GetSingleWordInOperand(1)) == spv::ExecutionMode::LocalSize) {
return mode.GetSingleWordInOperand(2) == 32u && mode.GetSingleWordInOperand(3) == 16u &&
mode.GetSingleWordInOperand(4) == 1u;
}
}
return false;
}
bool IsStoreToRoot(IRContext* context, const Instruction* inst, const Instruction* root) {
return inst->opcode() == spv::Op::OpStore && inst->NumInOperands() >= 2u &&
RootVariable(context, inst->GetSingleWordInOperand(0)) == root;
}
} // namespace
spvtools::opt::Pass::Status FixIterationRPBarrierPass::Process() {
auto* irContext = context();
if (!HasProgram203LocalSize(irContext)) return Status::SuccessWithoutChange;
for (auto& function : *irContext->module()) {
std::vector<Instruction*> instructions;
std::vector<size_t> scans;
for (auto& block : function) {
for (auto& inst : block) {
if (IsVec2FloatInclusiveAdd(irContext, &inst)) scans.push_back(instructions.size());
instructions.push_back(&inst);
}
}
// Program 203 has exactly two vec2 inclusive adds: the luminance reduction
// and the weighted-exposure reduction. More or fewer is not our fingerprint.
if (scans.size() != 2u) continue;
const size_t firstScan = scans[0];
const size_t secondScan = scans[1];
const Instruction* scratch = nullptr;
size_t averageLoad = instructions.size();
for (size_t i = firstScan + 1u; i < secondScan; ++i) {
Instruction* inst = instructions[i];
if (inst->opcode() != spv::Op::OpLoad || inst->NumInOperands() < 1u) continue;
const Instruction* root = nullptr;
if (!IsZeroElementPointer(irContext, inst->GetSingleWordInOperand(0), &root) ||
!IsWorkgroupVec2Array(irContext, root)) {
continue;
}
// The broadcast is read as prefixSumCache[0].x, hence a scalar load.
const Instruction* type = irContext->get_def_use_mgr()->GetDef(inst->type_id());
if (type == nullptr || type->opcode() != spv::Op::OpTypeFloat || type->NumInOperands() != 1u ||
type->GetSingleWordInOperand(0) != 32u) {
continue;
}
scratch = root;
averageLoad = i;
break;
}
if (scratch == nullptr) continue;
bool sawZeroBroadcastStore = false;
bool sawPublishBarrier = false;
for (size_t i = firstScan + 1u; i < averageLoad; ++i) {
const Instruction* root = nullptr;
if (instructions[i]->opcode() == spv::Op::OpStore &&
IsZeroElementPointer(irContext, instructions[i]->GetSingleWordInOperand(0), &root) &&
root == scratch) {
sawZeroBroadcastStore = true;
} else if (sawZeroBroadcastStore && instructions[i]->opcode() == spv::Op::OpControlBarrier) {
sawPublishBarrier = true;
}
}
if (!sawZeroBroadcastStore || !sawPublishBarrier) continue;
bool alreadySynchronized = false;
for (size_t i = averageLoad + 1u; i < secondScan; ++i) {
if (instructions[i]->opcode() == spv::Op::OpControlBarrier) {
alreadySynchronized = true;
break;
}
}
if (alreadySynchronized) return Status::SuccessWithoutChange;
bool secondPhaseReusesScratch = false;
for (size_t i = secondScan + 1u; i < instructions.size(); ++i) {
if (IsStoreToRoot(irContext, instructions[i], scratch)) {
secondPhaseReusesScratch = true;
break;
}
}
if (!secondPhaseReusesScratch) continue;
auto* constantMgr = irContext->get_constant_mgr();
const uint32_t scopeId = constantMgr->GetUIntConstId(static_cast<uint32_t>(spv::Scope::Workgroup));
const uint32_t semanticsId =
constantMgr->GetUIntConstId(static_cast<uint32_t>(spv::MemorySemanticsMask::AcquireRelease) |
static_cast<uint32_t>(spv::MemorySemanticsMask::WorkgroupMemory));
if (scopeId == 0u || semanticsId == 0u) return Status::Failure;
instructions[secondScan]->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpControlBarrier, 0u, 0u,
Instruction::OperandList{Operand{SPV_OPERAND_TYPE_ID, {scopeId}},
Operand{SPV_OPERAND_TYPE_ID, {scopeId}},
Operand{SPV_OPERAND_TYPE_ID, {semanticsId}}}));
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
return Status::SuccessWithoutChange;
}
spvtools::Optimizer::PassToken FixIterationRPBarrierPass::CreateFixIterationRPBarrierPass() {
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<FixIterationRPBarrierPass>());
}
} // namespace MobileGL::MG_Util::ShaderTranspiler
@@ -0,0 +1,28 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.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 "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
namespace MobileGL::MG_Util::ShaderTranspiler {
// Repairs iterationRP Program 203's missing workgroup rendezvous between two
// reductions that reuse prefixSumCache. The first phase broadcasts its result
// through prefixSumCache[0], but the second phase may overwrite that element before
// every invocation has read it. The pass fingerprints that exact two-scan,
// 512-invocation shape and inserts one Workgroup control barrier immediately before
// the second scan. Unrelated modules and already-repaired modules are byte-identical.
class FixIterationRPBarrierPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "fix-iterationrp-barrier"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateFixIterationRPBarrierPass();
};
} // namespace MobileGL::MG_Util::ShaderTranspiler
@@ -0,0 +1,558 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "FixIterationRPSubgroupScratchPass.h"
#include "spirv.hpp"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.h"
#include "source/opt/ir_context.h"
#include "source/opt/module.h"
#include "source/util/make_unique.h"
#include <map>
#include <unordered_map>
#include <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
// The Vulkan minimum for maxComputeSharedMemorySize, used when the caller
// could not tell us the device's real limit.
constexpr uint32_t kMinimumSharedMemoryBytes = 16384u;
// The narrowest subgroup width iterationRP's declarations are sized for.
// At or above it both shipped shapes fit and nothing may be rewritten.
constexpr uint32_t kPackAssumedSubgroupWidth = 16u;
Instruction* FindBuiltinDefinition(IRContext* context, spv::BuiltIn builtin) {
auto* defUseMgr = context->get_def_use_mgr();
for (auto& annotation : context->annotations()) {
if (annotation.opcode() != spv::Op::OpDecorate || annotation.NumInOperands() < 3) {
continue;
}
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
spv::Decoration::BuiltIn) {
continue;
}
if (static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2)) != builtin) {
continue;
}
return defUseMgr->GetDef(annotation.GetSingleWordInOperand(0));
}
return nullptr;
}
// Walks an access-chain pointer expression back to the variable it is
// rooted at; returns nullptr for anything that is not a plain chain.
const Instruction* RootVariable(IRContext* context, uint32_t pointerId) {
auto* defUseMgr = context->get_def_use_mgr();
const Instruction* def = defUseMgr->GetDef(pointerId);
while (def != nullptr) {
switch (def->opcode()) {
case spv::Op::OpVariable:
return def;
case spv::Op::OpAccessChain:
case spv::Op::OpInBoundsAccessChain:
case spv::Op::OpCopyObject:
def = defUseMgr->GetDef(def->GetSingleWordInOperand(0));
break;
default:
return nullptr;
}
}
return nullptr;
}
// A 32-bit float scalar or vector - the shape of every accumulator the
// pack runs through its scans (float, vec2 and vec4 all appear). Returns
// the component count, or 0 for anything else.
uint32_t Float32ComponentCount(IRContext* context, uint32_t typeId) {
auto* defUseMgr = context->get_def_use_mgr();
const Instruction* type = defUseMgr->GetDef(typeId);
if (type == nullptr) return 0u;
uint32_t components = 1u;
if (type->opcode() == spv::Op::OpTypeVector) {
components = type->GetSingleWordInOperand(1);
if (components < 2u || components > 4u) return 0u;
type = defUseMgr->GetDef(type->GetSingleWordInOperand(0));
if (type == nullptr) return 0u;
}
if (type->opcode() != spv::Op::OpTypeFloat ||
type->GetSingleWordInOperand(0) != 32u) {
return 0u;
}
return components;
}
uint32_t RoundUp(uint32_t value, uint32_t alignment) {
return alignment == 0u ? value : ((value + alignment - 1u) / alignment) * alignment;
}
// Size AND alignment of a workgroup-storage type. Drivers lay shared
// memory out at natural alignment and the limit
// (VUID-RuntimeSpirv-Workgroup-06530) counts the padding that produces,
// so a model that sums unpadded sizes would under-count exactly where the
// budget check matters. Returns false for anything not modelled here,
// which the caller answers by declining to grow at all rather than by
// certifying growth against a total it knows is an underestimate.
bool WorkgroupTypeLayout(IRContext* context, uint32_t typeId, uint32_t* size,
uint32_t* alignment, uint32_t depth = 0u) {
if (depth > 8u) return false;
auto* defUseMgr = context->get_def_use_mgr();
const Instruction* type = defUseMgr->GetDef(typeId);
if (type == nullptr) return false;
switch (type->opcode()) {
case spv::Op::OpTypeBool:
*size = 4u;
*alignment = 4u;
return true;
case spv::Op::OpTypeInt:
case spv::Op::OpTypeFloat: {
const uint32_t width = type->GetSingleWordInOperand(0) / 8u;
if (width == 0u) return false;
*size = width;
*alignment = width;
return true;
}
case spv::Op::OpTypeVector: {
uint32_t componentSize = 0u;
uint32_t componentAlignment = 0u;
if (!WorkgroupTypeLayout(context, type->GetSingleWordInOperand(0),
&componentSize, &componentAlignment, depth + 1u)) {
return false;
}
const uint32_t components = type->GetSingleWordInOperand(1);
if (components < 2u || components > 4u) return false;
*size = componentSize * components;
// A three-component vector aligns like a four-component one.
*alignment = componentSize * (components == 3u ? 4u : components);
return true;
}
case spv::Op::OpTypeMatrix:
case spv::Op::OpTypeArray: {
uint32_t elementSize = 0u;
uint32_t elementAlignment = 0u;
if (!WorkgroupTypeLayout(context, type->GetSingleWordInOperand(0), &elementSize,
&elementAlignment, depth + 1u)) {
return false;
}
uint32_t count = 0u;
if (type->opcode() == spv::Op::OpTypeMatrix) {
count = type->GetSingleWordInOperand(1);
} else {
const Instruction* length =
defUseMgr->GetDef(type->GetSingleWordInOperand(1));
if (length == nullptr || length->opcode() != spv::Op::OpConstant) {
return false; // spec-constant length: not sizeable here
}
count = length->GetSingleWordInOperand(0);
}
*size = RoundUp(elementSize, elementAlignment) * count;
*alignment = elementAlignment;
return true;
}
case spv::Op::OpTypeStruct: {
uint32_t offset = 0u;
uint32_t structAlignment = 1u;
for (uint32_t i = 0; i < type->NumInOperands(); ++i) {
uint32_t memberSize = 0u;
uint32_t memberAlignment = 0u;
if (!WorkgroupTypeLayout(context, type->GetSingleWordInOperand(i),
&memberSize, &memberAlignment, depth + 1u)) {
return false;
}
offset = RoundUp(offset, memberAlignment) + memberSize;
if (memberAlignment > structAlignment) structAlignment = memberAlignment;
}
*size = RoundUp(offset, structAlignment);
*alignment = structAlignment;
return true;
}
default:
return false;
}
}
// The group operations the pack's prefix scans use.
bool IsScanOrReduce(spv::GroupOperation operation) {
return operation == spv::GroupOperation::Reduce ||
operation == spv::GroupOperation::InclusiveScan ||
operation == spv::GroupOperation::ExclusiveScan;
}
} // namespace
spvtools::opt::Pass::Status FixIterationRPSubgroupScratchPass::Process() {
auto* irContext = context();
auto* defUseMgr = irContext->get_def_use_mgr();
// Without a known device width there is no topology to compare against;
// and a width the pack already assumed needs no patch at all. Both of
// iterationRP's shapes are sized for >= 16 lanes (512/16 = 32 entries,
// 1024/16 = 64), so every module on such a device - the pack's or anyone
// else's - must pass through byte-identical. The per-array length test
// further down is the second gate, not a replacement for this one.
if (m_nativeSubgroupSize == 0u || m_nativeSubgroupSize >= kPackAssumedSubgroupWidth) {
return Status::SuccessWithoutChange;
}
for (const Instruction& entryPoint : irContext->module()->entry_points()) {
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) !=
spv::ExecutionModel::GLCompute) {
return Status::SuccessWithoutChange;
}
}
// Fingerprint 1: a literal workgroup size, so the subgroup count the
// dispatch actually partitions into is known here.
const auto resolveUintConstant = [&](uint32_t id, uint32_t* value) {
const Instruction* def = defUseMgr->GetDef(id);
if (def == nullptr || def->opcode() != spv::Op::OpConstant) return false;
*value = def->GetSingleWordInOperand(0);
return true;
};
uint32_t localSize[3] = {0, 0, 0};
bool haveLocalSize = false;
if (Instruction* workgroupSize =
FindBuiltinDefinition(irContext, spv::BuiltIn::WorkgroupSize)) {
if (workgroupSize->opcode() == spv::Op::OpConstantComposite &&
workgroupSize->NumInOperands() == 3) {
haveLocalSize =
resolveUintConstant(workgroupSize->GetSingleWordInOperand(0), &localSize[0]) &&
resolveUintConstant(workgroupSize->GetSingleWordInOperand(1), &localSize[1]) &&
resolveUintConstant(workgroupSize->GetSingleWordInOperand(2), &localSize[2]);
}
}
if (!haveLocalSize) {
for (const Instruction& mode : irContext->module()->execution_modes()) {
if (mode.opcode() == spv::Op::OpExecutionMode &&
static_cast<spv::ExecutionMode>(mode.GetSingleWordInOperand(1)) ==
spv::ExecutionMode::LocalSize) {
localSize[0] = mode.GetSingleWordInOperand(2);
localSize[1] = mode.GetSingleWordInOperand(3);
localSize[2] = mode.GetSingleWordInOperand(4);
haveLocalSize = true;
break;
}
}
}
if (!haveLocalSize || localSize[0] == 0u || localSize[1] == 0u || localSize[2] == 0u) {
return Status::SuccessWithoutChange;
}
const uint64_t totalInvocations =
static_cast<uint64_t>(localSize[0]) * localSize[1] * localSize[2];
if (totalInvocations == 0u || totalInvocations > (1u << 20)) {
return Status::SuccessWithoutChange;
}
const uint32_t requiredLength = static_cast<uint32_t>(
(totalInvocations + m_nativeSubgroupSize - 1u) / m_nativeSubgroupSize);
// Fingerprint 2: a subgroup scan over a 32-bit float value - the pack's
// prefix-sum reduction, and the reason its scratch is indexed per subgroup.
bool sawFloatSubgroupScan = false;
for (auto& function : *irContext->module()) {
for (auto& block : function) {
for (auto& inst : block) {
if (inst.opcode() != spv::Op::OpGroupNonUniformFAdd &&
inst.opcode() != spv::Op::OpGroupNonUniformFMin &&
inst.opcode() != spv::Op::OpGroupNonUniformFMax) {
continue;
}
if (inst.NumInOperands() < 2) continue;
if (!IsScanOrReduce(static_cast<spv::GroupOperation>(
inst.GetSingleWordInOperand(1)))) {
continue;
}
if (Float32ComponentCount(irContext, inst.type_id()) != 0u) {
sawFloatSubgroupScan = true;
}
}
}
}
if (!sawFloatSubgroupScan) {
return Status::SuccessWithoutChange;
}
// gl_SubgroupID, whose value range the pack's scratch size bakes in.
const Instruction* subgroupIdVariable =
FindBuiltinDefinition(irContext, spv::BuiltIn::SubgroupId);
if (subgroupIdVariable == nullptr ||
subgroupIdVariable->opcode() != spv::Op::OpVariable) {
return Status::SuccessWithoutChange;
}
const uint32_t subgroupIdVariableId = subgroupIdVariable->result_id();
// The pack indexes its scratch with gl_SubgroupID ITSELF, so only values
// that ARE that id qualify - not everything computed from it. An index
// that is masked or clamped (cache[gl_SubgroupID & 3u]) is bounded by
// construction and is none of this pass's business; accepting it would
// turn a targeted repair into a general array resizer. Identity survives
// OpCopyObject, a signedness OpBitcast, and the Function/Private spill
// glslang emits for a builtin load - and nothing else. A spill variable
// counts only when EVERY store into it is the id.
std::unordered_map<uint32_t, bool> subgroupIdValues; // result id IS the id
std::unordered_map<uint32_t, bool> subgroupIdVariables; // spill holding only it
bool changedIdentity = true;
while (changedIdentity) {
changedIdentity = false;
std::unordered_map<uint32_t, uint32_t> totalStores;
std::unordered_map<uint32_t, uint32_t> idStores;
for (auto& function : *irContext->module()) {
for (auto& block : function) {
for (auto& inst : block) {
if (inst.opcode() != spv::Op::OpStore) continue;
const uint32_t pointerId = inst.GetSingleWordInOperand(0);
const Instruction* target = defUseMgr->GetDef(pointerId);
if (target == nullptr || target->opcode() != spv::Op::OpVariable) {
continue;
}
const auto storageClass = static_cast<spv::StorageClass>(
target->GetSingleWordInOperand(0));
if (storageClass != spv::StorageClass::Function &&
storageClass != spv::StorageClass::Private) {
continue;
}
totalStores[pointerId] += 1u;
if (subgroupIdValues.count(inst.GetSingleWordInOperand(1))) {
idStores[pointerId] += 1u;
}
}
}
}
for (const auto& entry : totalStores) {
if (entry.second != 0u && idStores[entry.first] == entry.second &&
!subgroupIdVariables.count(entry.first)) {
subgroupIdVariables[entry.first] = true;
changedIdentity = true;
}
}
for (auto& function : *irContext->module()) {
for (auto& block : function) {
for (auto& inst : block) {
if (inst.result_id() == 0 ||
subgroupIdValues.count(inst.result_id())) {
continue;
}
bool isSubgroupId = false;
switch (inst.opcode()) {
case spv::Op::OpLoad: {
const uint32_t pointerId = inst.GetSingleWordInOperand(0);
isSubgroupId = pointerId == subgroupIdVariableId ||
subgroupIdVariables.count(pointerId) != 0u;
break;
}
case spv::Op::OpCopyObject:
case spv::Op::OpBitcast:
isSubgroupId =
subgroupIdValues.count(inst.GetSingleWordInOperand(0)) != 0u;
break;
default:
break;
}
if (isSubgroupId) {
subgroupIdValues[inst.result_id()] = true;
changedIdentity = true;
}
}
}
}
}
if (subgroupIdValues.empty()) {
return Status::SuccessWithoutChange;
}
// Fingerprint 3: workgroup-shared float arrays indexed by gl_SubgroupID
// itself - the under-declared prefixSumCache.
std::map<uint32_t, Instruction*> candidates;
for (auto& function : *irContext->module()) {
for (auto& block : function) {
for (auto& inst : block) {
if (inst.opcode() != spv::Op::OpAccessChain &&
inst.opcode() != spv::Op::OpInBoundsAccessChain) {
continue;
}
if (inst.NumInOperands() < 2) continue;
if (!subgroupIdValues.count(inst.GetSingleWordInOperand(1))) continue;
Instruction* baseVariable =
defUseMgr->GetDef(inst.GetSingleWordInOperand(0));
if (baseVariable == nullptr ||
baseVariable->opcode() != spv::Op::OpVariable ||
static_cast<spv::StorageClass>(
baseVariable->GetSingleWordInOperand(0)) !=
spv::StorageClass::Workgroup) {
continue;
}
candidates.emplace(baseVariable->result_id(), baseVariable);
}
}
}
if (candidates.empty()) {
return Status::SuccessWithoutChange;
}
// Everything that survives the filter, with the bytes each grown array
// will need. Nothing is mutated until the whole set fits the device's
// shared-memory budget, so a module is never left half-grown.
struct Growth {
Instruction* variable = nullptr;
uint32_t elementTypeId = 0;
uint32_t lengthTypeId = 0;
uint32_t addedBytes = 0;
};
std::vector<Growth> growths;
for (auto& entry : candidates) {
Instruction* variable = entry.second;
// The variable must be reached exclusively through access chains (plus
// debug/decoration instructions): a whole-array load, store, or copy
// would change type with the array and is left alone.
bool onlyAccessChains = true;
const uint32_t variableId = variable->result_id();
defUseMgr->ForEachUser(variable, [&](Instruction* user) {
switch (user->opcode()) {
case spv::Op::OpAccessChain:
case spv::Op::OpInBoundsAccessChain:
if (user->GetSingleWordInOperand(0) != variableId) {
onlyAccessChains = false;
}
return;
case spv::Op::OpName:
case spv::Op::OpDecorate:
return;
default:
onlyAccessChains = false;
return;
}
});
if (!onlyAccessChains) continue;
if (variable->NumInOperands() > 1) continue; // initializer: leave alone
const Instruction* pointerType = defUseMgr->GetDef(variable->type_id());
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) {
continue;
}
const Instruction* arrayType =
defUseMgr->GetDef(pointerType->GetSingleWordInOperand(1));
if (arrayType == nullptr || arrayType->opcode() != spv::Op::OpTypeArray) {
continue;
}
const uint32_t elementTypeId = arrayType->GetSingleWordInOperand(0);
const uint32_t components = Float32ComponentCount(irContext, elementTypeId);
if (components == 0u) continue;
const Instruction* lengthConstant =
defUseMgr->GetDef(arrayType->GetSingleWordInOperand(1));
if (lengthConstant == nullptr || lengthConstant->opcode() != spv::Op::OpConstant) {
continue;
}
const uint32_t currentLength = lengthConstant->GetSingleWordInOperand(0);
// The pack's own assumption holds on this device: the declared array
// already covers every subgroup the workgroup partitions into. That is
// every >= 16-lane device for the shapes iterationRP ships, and those
// modules must pass through byte-identical.
if (currentLength >= requiredLength) continue;
// vec3 strides at its 16-byte alignment, so charge the padded stride.
const uint32_t elementStride = (components == 3u ? 4u : components) * 4u;
growths.push_back(Growth{variable, elementTypeId, lengthConstant->type_id(),
(requiredLength - currentLength) * elementStride});
}
if (growths.empty()) {
return Status::SuccessWithoutChange;
}
// Growing must not push the module past what the device can launch: a
// pipeline that fails to create is worse than the pack's own overrun.
{
uint64_t declaredBytes = 0;
bool sawUnsizeable = false;
for (auto& global : irContext->module()->types_values()) {
if (global.opcode() != spv::Op::OpVariable ||
static_cast<spv::StorageClass>(global.GetSingleWordInOperand(0)) !=
spv::StorageClass::Workgroup) {
continue;
}
const Instruction* pointerType = defUseMgr->GetDef(global.type_id());
uint32_t bytes = 0u;
uint32_t alignment = 0u;
if (pointerType == nullptr ||
pointerType->opcode() != spv::Op::OpTypePointer ||
!WorkgroupTypeLayout(irContext, pointerType->GetSingleWordInOperand(1),
&bytes, &alignment)) {
sawUnsizeable = true;
break;
}
declaredBytes = RoundUp(static_cast<uint32_t>(declaredBytes), alignment) + bytes;
}
// A declaration this pass cannot size leaves the total an
// underestimate, so the growth cannot be certified against the device
// limit at all - decline rather than guess.
if (sawUnsizeable) {
return Status::SuccessWithoutChange;
}
for (const Growth& growth : growths) declaredBytes += growth.addedBytes;
const uint32_t deviceBudget = m_maxWorkgroupScratchBytes != 0u
? m_maxWorkgroupScratchBytes
: kMinimumSharedMemoryBytes;
if (declaredBytes > deviceBudget) {
return Status::SuccessWithoutChange;
}
}
for (const Growth& growth : growths) {
// Build the grown array type. All three new instructions are inserted
// immediately BEFORE the variable so definition-before-use holds in the
// module's global section (manager-created instructions append to its
// end, after the variable). The new length constant reuses the old
// one's integer type, whatever signedness glslang gave it (a duplicate
// scalar constant is legal SPIR-V); the fresh array type makes the
// pointer type unique by construction, so neither collides with an
// existing declaration.
Instruction* variable = growth.variable;
const uint32_t newLengthId = irContext->TakeNextId();
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpConstant, growth.lengthTypeId, newLengthId,
Instruction::OperandList{{SPV_OPERAND_TYPE_TYPED_LITERAL_NUMBER,
{requiredLength}}}));
const uint32_t newArrayTypeId = irContext->TakeNextId();
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpTypeArray, 0, newArrayTypeId,
Instruction::OperandList{
{SPV_OPERAND_TYPE_ID, {growth.elementTypeId}},
{SPV_OPERAND_TYPE_ID, {newLengthId}}}));
const uint32_t newPointerTypeId = irContext->TakeNextId();
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpTypePointer, 0, newPointerTypeId,
Instruction::OperandList{
{SPV_OPERAND_TYPE_STORAGE_CLASS,
{static_cast<uint32_t>(spv::StorageClass::Workgroup)}},
{SPV_OPERAND_TYPE_ID, {newArrayTypeId}}}));
variable->SetResultType(newPointerTypeId);
}
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken
FixIterationRPSubgroupScratchPass::CreateFixIterationRPSubgroupScratchPass(
const Uint32 nativeSubgroupSize, const Uint32 maxWorkgroupScratchBytes) {
return spvtools::Optimizer::PassToken(MakeUnique<FixIterationRPSubgroupScratchPass>(
nativeSubgroupSize, maxWorkgroupScratchBytes));
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,73 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.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 "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// Patches ONE known shader-pack defect: iterationRP hard-sizes the scratch
// its subgroup prefix scans write through prefixSumCache[gl_SubgroupID].
// The pack ships that idiom twice, sized for the >= 16-lane subgroups
// desktop GL drivers give it:
// - the auto-exposure reduction: 32x16 (512 invocations), vec2[32];
// - the RTW importance warp: 1024 invocations, float[64].
// On a narrower Vulkan device (lavapipe's 8 lanes -> 64 and 128 subgroups)
// every subgroup past the last declared entry indexes shared memory out of
// bounds - on a CPU rasterizer that is literal heap corruption. Both
// reduction ALGORITHMS are width-agnostic (their combine loops are sized by
// gl_NumSubgroups), so the faithful repair is to grow the under-declared
// arrays to ceil(invocations / native width) and change nothing else.
//
// This is the pack author's bug, not MobileGL's, so the patch is
// deliberately NOT a general "resize shared arrays" mechanism. It rewrites
// an array only when the module positively matches the pack's reduction
// idiom AND the device's own topology proves the declaration too small:
// - GLCompute entry point with a literal workgroup size;
// - a subgroup scan/reduce over a 32-bit float scalar or vector
// (OpGroupNonUniformF{Add,Min,Max}), the pack's accumulator signature;
// - a workgroup-shared array of 32-bit float scalars/vectors whose
// access-chain index is data-dependent on gl_SubgroupID;
// - a declared length strictly below ceil(invocations / native width).
// That last clause is what keeps the patch inert wherever the pack is
// correct: on any device whose width satisfies the pack's assumption
// (>= 16 lanes: desktop GL, Adreno) both shapes already fit and every
// module passes through byte-identical. Matching at the SPIR-V level keeps
// recognition robust against the whitespace/identifier drift that made the
// old source-text template rewrite (removed in 7769156) so brittle.
//
// The pass never fails a module: anything it cannot prove is this pattern -
// or cannot grow safely (a whole-array use, a spec-constant length, an
// initializer, or growth that would not fit maxWorkgroupScratchBytes) - is
// left exactly as it was. Pass the device's maxComputeSharedMemorySize as
// maxWorkgroupScratchBytes; 0 falls back to the 16384-byte Vulkan minimum.
class FixIterationRPSubgroupScratchPass : public spvtools::opt::Pass {
public:
FixIterationRPSubgroupScratchPass(Uint32 nativeSubgroupSize,
Uint32 maxWorkgroupScratchBytes)
: m_nativeSubgroupSize(nativeSubgroupSize),
m_maxWorkgroupScratchBytes(maxWorkgroupScratchBytes) {}
const char* name() const override { return "fix-iterationrp-subgroup-scratch"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateFixIterationRPSubgroupScratchPass(
Uint32 nativeSubgroupSize, Uint32 maxWorkgroupScratchBytes);
private:
Uint32 m_nativeSubgroupSize;
Uint32 m_maxWorkgroupScratchBytes;
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -19,23 +19,27 @@ namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
constexpr const char* kConflictingName = "sampler";
constexpr const char* kCompatName = "MGL_COMPAT_sampler";
const char* GetCompatName(StringView name) {
if (name == "sampler") return "MGL_COMPAT_sampler";
if (name == "new") return "MGL_COMPAT_new";
return nullptr;
}
Bool IsNamedSamplerFunctionParameter(spvtools::opt::IRContext* context,
spvtools::opt::Instruction& nameInst) {
const char* GetConflictingFunctionParameterCompatName(spvtools::opt::IRContext* context,
spvtools::opt::Instruction& nameInst) {
if (nameInst.opcode() != spv::Op::OpName || nameInst.NumInOperands() < 2) {
return false;
return nullptr;
}
if (nameInst.GetInOperand(1).AsString() != kConflictingName) {
return false;
const char* compatName = GetCompatName(nameInst.GetInOperand(1).AsString());
if (compatName == nullptr) {
return nullptr;
}
auto* defUseMgr = context->get_def_use_mgr();
const Uint32 targetId = nameInst.GetSingleWordInOperand(0);
const auto* target = defUseMgr->GetDef(targetId);
return target != nullptr && target->opcode() == spv::Op::OpFunctionParameter;
return target != nullptr && target->opcode() == spv::Op::OpFunctionParameter ? compatName : nullptr;
}
} // namespace
@@ -44,12 +48,13 @@ namespace MobileGL {
auto* irContext = context();
for (auto& debugInst : irContext->debugs2()) {
if (!IsNamedSamplerFunctionParameter(irContext, debugInst)) {
const char* compatName = GetConflictingFunctionParameterCompatName(irContext, debugInst);
if (compatName == nullptr) {
continue;
}
debugInst.SetInOperand(
1, spvtools::utils::MakeVector<spvtools::opt::Operand::OperandData>(kCompatName));
1, spvtools::utils::MakeVector<spvtools::opt::Operand::OperandData>(compatName));
modified = true;
}
@@ -423,6 +423,26 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
InternalPackedLayout internalPacked;
};
Bool IsValidUnpackPixelPair(TextureInputFormat format, TexturePixelDataType type) {
UnpackChannelMapping mapping{};
if (!GetUnpackChannelMapping(format, mapping)) return false;
PackedTypeLayout packed{};
if (GetPackedTypeLayout(type, packed)) {
return packed.fieldCount == mapping.channelCount;
}
switch (type) {
case TexturePixelDataType::UnsignedInt5999Rev:
case TexturePixelDataType::UnsignedInt101111Rev:
return !mapping.isInteger && mapping.channelCount == 3;
default: {
ShadowComponent component{};
return GetDirectShadowComponentForType(type, mapping.isInteger, component);
}
}
}
// Returns true when the (format, type) -> internal-format upload needs a per-texel conversion;
// returns false both for layouts that already match the shadow bytes (memcpy fast path) and for
// combinations the converter does not support (legacy copy behavior).
@@ -964,6 +984,32 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
return outputPixels;
}
Bool ConvertOnePixelToInternal(TextureInternalFormat targetInternalFormat,
TextureInputFormat textureInputFormat,
TexturePixelDataType inputDataType,
const void* inputPixel,
Vector<Uint8>& outputPixel) {
outputPixel.clear();
if (inputPixel == nullptr || !IsValidUnpackPixelPair(textureInputFormat, inputDataType)) return false;
PixelStoreParameters params{};
params.Alignment = 1;
SizeT convertedSize = 0;
void* converted = ProcessTexturePixelsDataUnpack(
inputPixel, params, targetInternalFormat, textureInputFormat, inputDataType, {1, 1, 1}, false,
convertedSize);
const SizeT expectedSize = MG_Util::GetSizedInternalFormatSizeInBytes(targetInternalFormat);
if (converted == nullptr || convertedSize != expectedSize || expectedSize == 0) {
if (converted != nullptr) free(converted);
return false;
}
outputPixel.resize(convertedSize);
Memcpy(outputPixel.data(), converted, convertedSize);
free(converted);
return true;
}
void* ProcessTexturePixelsDataPack(const void* inputPixels, const PixelStoreParameters& params,
TextureInternalFormat srcInternalFormat, TexturePixelDataType srcDataType,
TextureInputFormat dstInputFormat, TexturePixelDataType dstDataType,
@@ -21,6 +21,12 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
TextureInternalFormat srcInternalFormat, TexturePixelDataType srcDataType,
TextureInputFormat dstInputFormat, TexturePixelDataType dstDataType,
IntVec3 dimension, Bool isBitmap, SizeT& outSize);
Bool ConvertOnePixelToInternal(TextureInternalFormat targetInternalFormat,
TextureInputFormat textureInputFormat,
TexturePixelDataType inputDataType,
const void* inputPixel,
Vector<Uint8>& outputPixel);
void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector<TextureSwizzleParam>& swizzle);
// True when a packed internal format's 32-bit storage word IS the client (format, type) word,

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