diff --git a/.github/scripts/validate-plugin-apks.sh b/.github/scripts/validate-plugin-apks.sh index 7c495cd0..354b605c 100644 --- a/.github/scripts/validate-plugin-apks.sh +++ b/.github/scripts/validate-plugin-apks.sh @@ -44,7 +44,7 @@ require 'key:MOBILEGL_BACKEND_TYPE' "$plugin_resource_text" 'V2 backend variable require 'defaultValue:DirectGLES' "$plugin_resource_text" 'V2 DirectGLES default' require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option' require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle' -require 'key:MOBILEGL_MAGMA_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle' +require 'key:MOBILEGL_SUBGROUP_EMULATION' "$plugin_resource_text" 'V2 subgroup emulation setting' require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle' require 'key:MOBILEGL_FRAMES_IN_FLIGHT' "$plugin_resource_text" 'V2 frames-in-flight setting' require 'key:MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle' diff --git a/.github/workflows/apk.yml b/.github/workflows/apk.yml index 05b9f874..ae319fe7 100644 --- a/.github/workflows/apk.yml +++ b/.github/workflows/apk.yml @@ -512,9 +512,7 @@ jobs: MOBILEGL_ESPRYT_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }} MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }} MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }} - MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }} - MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }} - MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }} + MOBILEGL_ITRP_FIX: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && 'ForceOn' || 'Auto' }} run: | apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk" test -f "${apk_file}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a74431e5..0638c586 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -444,9 +444,7 @@ jobs: # crash stack without burning a CI round on an in-workflow debugger. env: MOBILEGL_ITEST_REQUIRE_GPU: "1" - MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1" - MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: "1" - MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: "1" + MOBILEGL_ITRP_FIX: "ForceOn" run: | ulimit -c unlimited if [ "$MOBILEGL_CI_NATIVE_GPU" != 1 ] && [ ! -f /.dockerenv ]; then sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'; fi @@ -771,9 +769,7 @@ jobs: # `integration`, unverified, and P2 can take it once the comparator's cost is known. env: MOBILEGL_ITEST_REQUIRE_GPU: "1" - MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1" - MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: "1" - MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: "1" + MOBILEGL_ITRP_FIX: "ForceOn" run: | ulimit -c unlimited if [ "$MOBILEGL_CI_NATIVE_GPU" != 1 ] && [ ! -f /.dockerenv ]; then sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'; fi @@ -859,9 +855,7 @@ jobs: working-directory: build-verify env: MOBILEGL_ITEST_REQUIRE_GPU: "1" - MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1" - MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: "1" - MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: "1" + MOBILEGL_ITRP_FIX: "ForceOn" run: | ulimit -c unlimited if [ "$MOBILEGL_CI_NATIVE_GPU" != 1 ] && [ ! -f /.dockerenv ]; then sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'; fi @@ -1635,9 +1629,7 @@ jobs: working-directory: build-split env: MOBILEGL_ITEST_REQUIRE_GPU: "1" - MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1" - MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: "1" - MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: "1" + MOBILEGL_ITRP_FIX: "ForceOn" run: | ulimit -c unlimited if [ "$MOBILEGL_CI_NATIVE_GPU" != 1 ] && [ ! -f /.dockerenv ]; then sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'; fi @@ -2913,9 +2905,7 @@ jobs: fi if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \ && [ '${{ matrix.case }}' = 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' ]; then - export MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1 - export MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1 - export MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1 + export MOBILEGL_ITRP_FIX=ForceOn 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 @@ -3388,9 +3378,7 @@ jobs: # and two runs that differ in a quirk knob are not. if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \ && [ '${{ matrix.case }}' = 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' ]; then - echo "MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1" >> "${GITHUB_ENV}" - echo "MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1" >> "${GITHUB_ENV}" - echo "MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1" >> "${GITHUB_ENV}" + echo "MOBILEGL_ITRP_FIX=ForceOn" >> "${GITHUB_ENV}" fi - name: Retrace and validate (${{ matrix.transport }}) diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 8b8a99e1..e5ff8398 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -85,41 +85,26 @@ namespace MobileGL::MG_Config { // MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash. String TraceAngleVariant; #endif - // MOBILEGL_MAGMA_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support, - // including the opt-in emulated compute path below. - Bool MagmaDisableSubgroup = false; - // MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute - // stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory - // (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever - // engages when this flag is set AND the device has no native subgroup support at - // all - a device with real subgroup operations always uses them natively, - // whatever their width (the known iterationRP defect is patched by - // MagmaFixIterationRPSubgroupScratch below instead). Off by default. - Bool MagmaEmulateSubgroup = false; - // MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the - // pack declares `shared vec2 prefixSumCache[32]` for a 512-invocation exposure - // reduction and indexes it by gl_SubgroupID, so any device with sub-16-lane - // subgroups (8-lane lavapipe -> 64 subgroups) writes shared memory out of - // bounds. The pass grows that one array to what the device's topology needs and - // touches nothing else; it only rewrites modules positively matching the pack's - // reduction fingerprint (ShaderTranspiler::FixIterationRPSubgroupScratchPass), - // so every other shader passes through byte-identical - as does iterationRP - // itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug - // verbatim. - QuirkOverride MagmaFixIterationRPSubgroupScratch = QuirkOverride::Auto; - // MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup - // rendezvous between its two reductions over prefixSumCache. Off by default and - // fingerprint-gated by FixIterationRPBarrierPass when enabled. - Bool MagmaIterationRPFixBarrier = false; - // MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with - // ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path - // (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires - // gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same - // dispatch emits IDs 0..7, and the derived value is the one Vulkan guarantees - // whenever the pipeline can request REQUIRE_FULL_SUBGROUPS (which the renderer - // does whenever local_size_x is a multiple of the native width). ForceOff returns - // to the raw driver builtin. - QuirkOverride MagmaDeriveNumSubgroups = QuirkOverride::Auto; + // MOBILEGL_SUBGROUP_EMULATION: how either backend implements GL_KHR_shader_subgroup + // (MG_Backend/SubgroupImplementation.h). Auto (the default) uses the driver's own subgroup + // operations natively whenever it has them, at whatever width (8/16/32/64/128 lanes), and + // otherwise emulates the compute stage on a 32-lane VIRTUAL subgroup in workgroup-shared + // memory (ShaderTranspiler::EmulateSubgroupsPass). ForceOn emulates even over native + // support, so the emulation can be exercised on any device; ForceOff never emulates, + // leaving a driver without subgroups without the extension. + QuirkOverride SubgroupEmulation = QuirkOverride::Auto; + // MOBILEGL_ITRP_FIX: the iterationRP shaderpack's two compute-shader bugs, repaired by + // fingerprint-gated passes that leave every other module byte-identical + // (MG_Backend/SubgroupImplementation.h): + // * FixIterationRPSubgroupScratchPass - the pack declares `shared vec2 + // prefixSumCache[32]` for a 512-invocation reduction indexed by gl_SubgroupID, so a + // sub-16-lane device (8-lane lavapipe -> 64 subgroups) writes shared memory out of + // bounds; the array grows to what the native width needs (inert at >= 16 lanes, and + // under the 32-lane emulation); + // * FixIterationRPBarrierPass - Program 203's second reduction overwrites that scratch + // without a workgroup rendezvous after the first one's readers. + // Auto and ForceOn apply both; ForceOff replays the pack's bugs verbatim. + QuirkOverride IterationRPFix = QuirkOverride::Auto; // MOBILEGL_MAGMA_INPASS_CLEAR: a wire-arm glClear / glClearBuffer* whose targets are the // attachments of the draw pass for the bound framebuffer is recorded inside that pass // (vkCmdClearAttachments) rather than in a render pass of its own built for the clear. @@ -221,6 +206,14 @@ namespace MobileGL::MG_Config { // everywhere. ForceOn exercises the rewrite on a healthy driver; ForceOff is the // negative control that replays the lost writes. QuirkOverride EsprytDeferGeometryEmits = QuirkOverride::Auto; + // MOBILEGL_ESPRYT_ATOMIC_SUBGROUP_SCRATCH: DirectGLES's subgroup emulation reads its + // workgroup scratch back through atomic loads (EmulateSubgroupsPass). The Adreno ES + // compiler treats ordinary shared-memory loads as pure and merges or reorders them + // across stores and barrier(), so an emulated subgroup operation would see the lanes of + // an earlier one; the driver POST measures that and arms this by itself, so Auto is + // right everywhere. ForceOn exercises the atomic reads on a healthy driver; ForceOff is + // the negative control. + QuirkOverride EsprytAtomicSubgroupScratch = QuirkOverride::Auto; // MOBILEGL_POINT_SIZE_DEMOTION: demote gl_PointSize out of tessellation/geometry // stages into an ordinary varying (ShaderCompiler:: // DemoteTessellationGeometryPointSizeForProgram) instead of declining such programs diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 2b529975..10737c99 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -249,11 +249,19 @@ namespace MobileGL::MG_ConfigLoader { // Quirk overrides are tri-state: an unset variable keeps device auto-detection, a truthy // value forces the quirk on, anything else set ("0", "false", "") forces it off. + // Unset, empty or "auto" keeps Auto; "forceon"/"on" and "forceoff"/"off"/"no" name the two + // overrides; anything else is read as a flag (1/true -> ForceOn, 0/false -> ForceOff). inline MG_Config::QuirkOverride QueryEnvQuirkOverride(const String& key) { auto it = acceptedEnvVariablesMap->find(key); if (it == acceptedEnvVariablesMap->end()) { return MG_Config::QuirkOverride::Auto; } + String lowered = it->second; + std::transform(lowered.begin(), lowered.end(), lowered.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (lowered.empty() || lowered == "auto") return MG_Config::QuirkOverride::Auto; + if (lowered == "forceon" || lowered == "on") return MG_Config::QuirkOverride::ForceOn; + if (lowered == "forceoff" || lowered == "off" || lowered == "no") return MG_Config::QuirkOverride::ForceOff; return IsTruthyValue(it->second) ? MG_Config::QuirkOverride::ForceOn : MG_Config::QuirkOverride::ForceOff; } @@ -361,12 +369,8 @@ namespace MobileGL::MG_ConfigLoader { #if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, ""); #endif - features.MagmaDisableSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_SUBGROUP"); - features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP"); - features.MagmaFixIterationRPSubgroupScratch = - QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH"); - features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER"); - features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS"); + features.SubgroupEmulation = QueryEnvQuirkOverride("MOBILEGL_SUBGROUP_EMULATION"); + features.IterationRPFix = QueryEnvQuirkOverride("MOBILEGL_ITRP_FIX"); features.MagmaInPassClear = QueryEnvQuirkOverride("MOBILEGL_MAGMA_INPASS_CLEAR"); features.MagmaInPassClearDefault = QueryEnvQuirkOverride("MOBILEGL_MAGMA_INPASS_CLEAR_DEFAULT"); features.MagmaPassCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PASS_CACHE"); @@ -395,6 +399,7 @@ namespace MobileGL::MG_ConfigLoader { features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS"); features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS"); features.EsprytDeferGeometryEmits = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_DEFER_GEOMETRY_EMITS"); + features.EsprytAtomicSubgroupScratch = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_ATOMIC_SUBGROUP_SCRATCH"); features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION"); features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH"); features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY"); diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 04a596ae..27edca22 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -907,6 +908,11 @@ namespace MobileGL::MG_Backend::DirectGLES { extensions.push_back(E_GL_EXT_fragment_shading_rate_primitive); } } + // Native where the ES driver has subgroups, emulated in the compute stage where it + // has none (SubgroupPolicy.h); the dynamic parameters below say which. + if (ResolveEsprytSubgroupImplementation(capabilities) != SubgroupImplementation::None) { + extensions.push_back(E_GL_KHR_shader_subgroup); + } } } // namespace @@ -1949,6 +1955,16 @@ namespace MobileGL::MG_Backend::DirectGLES { // mathematically mandatory here, on every device, forever - which is why this stays false // regardless of what the driver underneath happens to support. m_dynamicParameters.SupportsShaderFloat64 = false; + { + const EsprytSubgroupParameters subgroups = ResolveEsprytSubgroupParameters(m_GLESCapabilities); + m_dynamicParameters.SubgroupSize = subgroups.size; + m_dynamicParameters.SubgroupSupportedStages = subgroups.stages; + m_dynamicParameters.SubgroupSupportedFeatures = subgroups.features; + m_dynamicParameters.SubgroupQuadOperationsInAllStages = subgroups.quadInAllStages; + MGLOG_I("DirectGLES: GL_KHR_shader_subgroup %s (size %u, stages 0x%x, features 0x%x)", + SubgroupImplementationName(ResolveEsprytSubgroupImplementation(m_GLESCapabilities)), + subgroups.size, subgroups.stages, subgroups.features); + } // Follows the line above, and must: OpenGL ES has no double-precision vertex format and no // fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here. m_dynamicParameters.SupportsFloat64VertexAttributes = false; diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 27921f4d..333852ea 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include "Managers.h" +#include "SubgroupPolicy.h" #include // P3a: the handle-shaped resource op table, the applier's records and the reverse channel. // Both headers are compiled into the library only under push, so they are included here @@ -11647,6 +11648,37 @@ namespace MobileGL::MG_Backend::DirectGLES { } } + // GL_KHR_shader_subgroup where the ES driver has none (SubgroupPolicy.h): the compute + // stage's subgroup operations are lowered onto a 32-lane virtual subgroup in + // workgroup-shared memory, so the ESSL needs nothing from the driver but barrier() + // and shared memory. Where the driver has subgroups, the operations stay and + // SPIRV-Cross spells them as GL_KHR_shader_subgroup_* built-ins. iterationRP's + // scratch race is patched first unless MOBILEGL_ITRP_FIX is ForceOff, as + // on DirectVulkan. On a driver whose shared-memory loads go stale across barrier() + // the scratch is read through atomic loads. Keyed by subgroupEmulationArmed / + // iterationRPBarrierFixArmed / subgroupScratchAtomicReads. + Vector barrierFixedSpirv; + Vector emulatedSubgroupSpirv; + if (glShaderType == GL_COMPUTE_SHADER && + ResolveEsprytSubgroupImplementation(g_GLESCapabilities) == SubgroupImplementation::Emulated) { + if (ShouldApplyIterationRPFixes() && + MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPBarrierForVulkan( + *effectiveSpirv, barrierFixedSpirv, enableSpirvValidation) && + !barrierFixedSpirv.empty()) { + effectiveSpirv = &barrierFixedSpirv; + } + if (MG_Util::ShaderTranspiler::ShaderCompiler::EmulateSubgroupsForVulkan( + *effectiveSpirv, emulatedSubgroupSpirv, + static_cast(g_GLESCapabilities.MaxComputeSharedMemorySize), enableSpirvValidation, + g_GLESCapabilities.SharedMemoryLoadsGoStale) && + !emulatedSubgroupSpirv.empty()) { + effectiveSpirv = &emulatedSubgroupSpirv; + } else { + MGLOG_E("DirectGLES: subgroup emulation declined a compute stage; it keeps subgroup " + "operations this ES driver cannot compile"); + } + } + // ESSL cannot express gl_ViewportIndex either, but unlike the draw parameters // there IS an extension that provides it - so this runs only when the driver does // NOT advertise GL_OES_viewport_array. A driver that does keeps the builtin and @@ -12661,6 +12693,15 @@ namespace MobileGL::MG_Backend::DirectGLES { (ViewportArrayEmulationEnabled() || !g_GLESCapabilities.SupportsViewportArray); esslKeyInputs.deferGeometryEmitsArmed = glShaderType == GL_GEOMETRY_SHADER && g_GLESCapabilities.DropsGeometryWritesAfterEmit; + esslKeyInputs.subgroupEmulationArmed = + glShaderType == GL_COMPUTE_SHADER && + ResolveEsprytSubgroupImplementation(g_GLESCapabilities) == SubgroupImplementation::Emulated; + esslKeyInputs.subgroupEmulationScratchBytes = + esslKeyInputs.subgroupEmulationArmed ? g_GLESCapabilities.MaxComputeSharedMemorySize : 0; + esslKeyInputs.iterationRPBarrierFixArmed = + esslKeyInputs.subgroupEmulationArmed && ShouldApplyIterationRPFixes(); + esslKeyInputs.subgroupScratchAtomicReads = + esslKeyInputs.subgroupEmulationArmed && g_GLESCapabilities.SharedMemoryLoadsGoStale; esslKeyInputs.supportsNoperspectiveInterpolation = g_GLESCapabilities.SupportsNoperspectiveInterpolation; esslKeyInputs.supportsExtendedImageFormats = diff --git a/MobileGL/MG_Backend/DirectGLES/SubgroupPolicy.h b/MobileGL/MG_Backend/DirectGLES/SubgroupPolicy.h new file mode 100644 index 00000000..81645bbb --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/SubgroupPolicy.h @@ -0,0 +1,55 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/SubgroupPolicy.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 +#include +#include + +namespace MobileGL::MG_Backend::DirectGLES { + // How DirectGLES implements GL_KHR_shader_subgroup (MG_Backend/SubgroupImplementation.h), + // shared by the extension list, the dynamic parameters and the ESSL transpile. + // + // NATIVE where the ES driver itself has GL_KHR_shader_subgroup: the stage is transpiled + // with its GroupNonUniform operations intact and SPIRV-Cross spells them as the + // GL_KHR_shader_subgroup_* built-ins. Without Vulkan semantics SPIRV-Cross can only print + // the basic, vote and ballot operations (plus add/mul reductions, which do not make up the + // arithmetic feature), so those three features are all the native path advertises. + // + // EMULATED where it has none (Adreno's ES driver among them): the compute stage is lowered + // onto a 32-lane virtual subgroup in workgroup-shared memory before the transpile + // (EmulateSubgroupsPass), which needs nothing from the driver beyond barrier() and shared + // memory, and advertises every feature. + inline SubgroupImplementation ResolveEsprytSubgroupImplementation(const MG_External::GLESCapabilities& caps) { + return ResolveSubgroupImplementation(caps.SupportsShaderSubgroup); + } + + inline constexpr Uint32 kEsslPrintableSubgroupFeatures = + GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_VOTE_BIT_KHR | GL_SUBGROUP_FEATURE_BALLOT_BIT_KHR; + + struct EsprytSubgroupParameters { + Uint32 size = 0; + Uint32 stages = 0; + Uint32 features = 0; + Bool quadInAllStages = false; + }; + + inline EsprytSubgroupParameters ResolveEsprytSubgroupParameters(const MG_External::GLESCapabilities& caps) { + switch (ResolveEsprytSubgroupImplementation(caps)) { + case SubgroupImplementation::Native: + return {caps.SubgroupSize, caps.SubgroupSupportedStages, + caps.SubgroupSupportedFeatures & kEsslPrintableSubgroupFeatures, false}; + case SubgroupImplementation::Emulated: + return {kEmulatedSubgroupSize, kEmulatedSubgroupStages, kEmulatedSubgroupFeatures, false}; + case SubgroupImplementation::None: + break; + } + return {}; + } +} // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 91a92174..895980cf 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -940,7 +940,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (nonZeroIndirectBaseInstanceSupported) { extensions.push_back(E_GL_ARB_base_instance); } - if (shaderSubgroupSupported && !MG_Config::Features.MagmaDisableSubgroup) { + if (shaderSubgroupSupported) { extensions.push_back(E_GL_KHR_shader_subgroup); } // GL_KHR_parallel_shader_compile is MobileGL's own capability, not the Vulkan @@ -1103,12 +1103,13 @@ 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. + // The emulated compute path (SubgroupSupportPolicy.h) carries the extension by + // itself on devices with no native subgroup support at all; a device with native + // subgroups advertises - and uses - those unless MOBILEGL_SUBGROUP_EMULATION forces + // the emulation. const Bool subgroupSupportAdvertised = - m_vulkanCaps.SupportsShaderSubgroup || - ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup); + ResolveVulkanSubgroupImplementation(m_vulkanCaps.SupportsShaderSubgroup) != + MG_Backend::SubgroupImplementation::None; m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions( subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(), pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(), @@ -1480,14 +1481,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_dynamicParameters.SupportsFloat64VertexAttributes = false; m_dynamicParameters.MaxShaderStorageBlockSize = std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize); - if (m_vulkanCaps.SupportsShaderSubgroup) { + const MG_Backend::SubgroupImplementation subgroupImplementation = + ResolveVulkanSubgroupImplementation(m_vulkanCaps.SupportsShaderSubgroup); + if (subgroupImplementation == MG_Backend::SubgroupImplementation::Native) { m_dynamicParameters.SubgroupSize = m_vulkanCaps.SubgroupSize; m_dynamicParameters.SubgroupSupportedStages = mapShaderStages(m_vulkanCaps.SubgroupSupportedStages); 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 + } else if (subgroupImplementation == MG_Backend::SubgroupImplementation::Emulated) { + // A device with no native subgroups (or MOBILEGL_SUBGROUP_EMULATION forcing it): 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 @@ -1496,8 +1499,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { 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)"); + MGLOG_I("DirectVulkan: emulating 32-lane compute subgroups (%s)", + m_vulkanCaps.SupportsShaderSubgroup ? "forced by MOBILEGL_SUBGROUP_EMULATION" + : "no native subgroup support"); } else { m_dynamicParameters.SubgroupSize = 0; m_dynamicParameters.SubgroupSupportedStages = 0; diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h index 19e48a61..3af6ce40 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h @@ -109,9 +109,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { const RendererInfo& GetRendererIdentity(); // The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for - // a device with the given raw capabilities. The MOBILEGL_MAGMA_DISABLE_SUBGROUP and - // MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass - // the detected device support (passing an already-gated value is harmless). + // a device with the given raw capabilities. The MOBILEGL_DISABLE_TIMERQUERY escape hatch + // is applied inside, so callers pass the detected device support (passing an + // already-gated value is harmless). `shaderSubgroupSupported` is whether + // GL_KHR_shader_subgroup is offered at all, natively or emulated. Vector BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported, Bool anisotropicFilteringSupported, Bool nonZeroIndirectBaseInstanceSupported, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index 873ece7d..87f109ed 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -3698,8 +3698,8 @@ 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). + // around them. The emulation path replaces them on devices with no subgroup + // support at all, or when MOBILEGL_SUBGROUP_EMULATION forces it. if (stages[i] == ShaderStage::Compute) { // Program 203 broadcasts the first reduction through // prefixSumCache[0], then lets the second reduction overwrite that diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h index b5986403..710272f7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h @@ -489,7 +489,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // 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 emulateSubgroups = false; // no-native-support devices, or MOBILEGL_SUBGROUP_EMULATION 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 diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index a4296d9c..4d5a3432 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2329,10 +2329,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { ProgramFactory::SubgroupLoweringPolicy subgroupPolicy{}; subgroupPolicy.emulateSubgroups = ShouldEmulateSubgroups(m_nativeSubgroupSupported); subgroupPolicy.fixIterationRPSubgroupScratch = - m_nativeSubgroupSupported && ShouldFixIterationRPSubgroupScratch(); - subgroupPolicy.fixIterationRPBarrier = ShouldFixIterationRPBarrier(); - subgroupPolicy.deriveNumSubgroups = - m_nativeSubgroupSupported && ShouldDeriveNumSubgroups(); + UsesNativeSubgroups(m_nativeSubgroupSupported) && ShouldApplyIterationRPFixes(); + subgroupPolicy.fixIterationRPBarrier = ShouldApplyIterationRPFixes(); + subgroupPolicy.deriveNumSubgroups = UsesNativeSubgroups(m_nativeSubgroupSupported); subgroupPolicy.requireFullSubgroups = m_computeFullSubgroupsFeatureEnabled; subgroupPolicy.nativeSubgroupSize = m_nativeSubgroupSize; subgroupPolicy.maxComputeWorkgroupSubgroups = m_maxComputeWorkgroupSubgroups; @@ -7915,14 +7914,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { 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_MAGMA_DISABLE_SUBGROUP escape hatch, so the module lowerings can never - // disagree with the advertised capabilities. + // Mirrors the loader's HasUsableShaderSubgroupSupport gate, 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.MagmaDisableSubgroup) { + if (usableSubgroups) { m_nativeSubgroupSize = subgroupProperties.subgroupSize; m_nativeSubgroupSupported = true; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 09cff6e9..014f8c04 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -1166,8 +1166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool m_shaderDrawParametersFeatureEnabled = false; // Native subgroup topology, queried at device creation for the compute-module // subgroup repairs (SubgroupSupportPolicy.h) and the REQUIRE_FULL_SUBGROUPS - // stage flag; 0 / false when the device has no usable compute subgroups or - // MOBILEGL_MAGMA_DISABLE_SUBGROUP forced them off. + // stage flag; 0 / false when the device has no usable compute subgroups. Uint32 m_nativeSubgroupSize = 0; Bool m_nativeSubgroupSupported = false; Bool m_computeFullSubgroupsFeatureEnabled = false; diff --git a/MobileGL/MG_Backend/DirectVulkan/SubgroupSupportPolicy.h b/MobileGL/MG_Backend/DirectVulkan/SubgroupSupportPolicy.h index 0ef22070..c809bb9f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/SubgroupSupportPolicy.h +++ b/MobileGL/MG_Backend/DirectVulkan/SubgroupSupportPolicy.h @@ -10,6 +10,7 @@ #include #include +#include namespace MobileGL::MG_Backend::DirectVulkan { // The single decision point for how DirectVulkan implements GL_KHR_shader_subgroup, @@ -22,42 +23,35 @@ namespace MobileGL::MG_Backend::DirectVulkan { // - 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; + // reusing that scratch (both iterationRP repairs: MOBILEGL_ITRP_FIX); // - 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. + // The 32-lane shared-memory emulation (EmulateSubgroupsPass) is what a device with no + // subgroup support at all gets (MG_Backend::ResolveSubgroupImplementation; forced with + // MOBILEGL_SUBGROUP_EMULATION); it never replaces available native operations by default. - 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; + using MG_Backend::kEmulatedSubgroupFeatures; + using MG_Backend::kEmulatedSubgroupSize; + using MG_Backend::kEmulatedSubgroupStages; + + inline MG_Backend::SubgroupImplementation ResolveVulkanSubgroupImplementation(const Bool nativeSubgroupSupported) { + return MG_Backend::ResolveSubgroupImplementation(nativeSubgroupSupported); + } inline Bool ShouldEmulateSubgroups(const Bool nativeSubgroupSupported) { - return MG_Config::Features.MagmaEmulateSubgroup && !nativeSubgroupSupported && - !MG_Config::Features.MagmaDisableSubgroup; + return ResolveVulkanSubgroupImplementation(nativeSubgroupSupported) == + MG_Backend::SubgroupImplementation::Emulated; } - inline Bool ShouldFixIterationRPSubgroupScratch() { - // Auto is ON: the patch is fingerprint-gated to iterationRP's reduction and - // grows one under-declared array; every other module passes through untouched. - return MG_Config::Features.MagmaFixIterationRPSubgroupScratch != - MG_Config::QuirkOverride::ForceOff; + // Native subgroup operations in use - the device has them and they are not overridden. + // gl_NumSubgroups is derived on this path unconditionally (DeriveNumSubgroupsPass): GL + // requires gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same + // dispatch emits IDs 0..7, and the derived ceil() value is the one Vulkan guarantees + // whenever the pipeline can request REQUIRE_FULL_SUBGROUPS. + inline Bool UsesNativeSubgroups(const Bool nativeSubgroupSupported) { + return ResolveVulkanSubgroupImplementation(nativeSubgroupSupported) == + MG_Backend::SubgroupImplementation::Native; } - inline Bool ShouldFixIterationRPBarrier() { - return MG_Config::Features.MagmaIterationRPFixBarrier; - } - - inline Bool ShouldDeriveNumSubgroups() { - // Auto is ON: gl_NumSubgroups must agree with the gl_SubgroupID range for the GL - // contract to hold, and the derived ceil() value is the one the renderer can pin - // with REQUIRE_FULL_SUBGROUPS - the driver builtin is the value with no - // cross-driver guarantee (Adreno returns 1 for an 8-subgroup dispatch). - return MG_Config::Features.MagmaDeriveNumSubgroups != MG_Config::QuirkOverride::ForceOff; - } + using MG_Backend::ShouldApplyIterationRPFixes; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/SubgroupImplementation.h b/MobileGL/MG_Backend/SubgroupImplementation.h new file mode 100644 index 00000000..ec6abbaf --- /dev/null +++ b/MobileGL/MG_Backend/SubgroupImplementation.h @@ -0,0 +1,63 @@ +// MobileGL - MobileGL/MG_Backend/SubgroupImplementation.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 +#include + +namespace MobileGL::MG_Backend { + // How a backend implements GL_KHR_shader_subgroup - decided once, from what its driver + // offers, and shared by capability advertisement and module lowering so the two can never + // disagree. + // Native - the driver's own subgroup operations, at whatever width it runs (8, 16, 32, + // 64, 128 lanes); MobileGL advertises the driver's size, stages and features. + // Emulated - a 32-lane VIRTUAL subgroup in workgroup-shared memory + // (ShaderTranspiler::EmulateSubgroupsPass): compute stage only, every feature + // bit. What a driver with no subgroup operations at all gets. + // None - GL_KHR_shader_subgroup is not advertised. + // MOBILEGL_SUBGROUP_EMULATION: Auto emulates exactly where nothing native exists; ForceOn + // emulates even over native support (to exercise the emulation on any device); ForceOff + // never emulates. + enum class SubgroupImplementation : Uint8 { None, Native, Emulated }; + + 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 SubgroupImplementation ResolveSubgroupImplementation(const Bool nativeSupported) { + switch (MG_Config::Features.SubgroupEmulation) { + case MG_Config::QuirkOverride::ForceOn: + return SubgroupImplementation::Emulated; + case MG_Config::QuirkOverride::ForceOff: + return nativeSupported ? SubgroupImplementation::Native : SubgroupImplementation::None; + case MG_Config::QuirkOverride::Auto: + default: + return nativeSupported ? SubgroupImplementation::Native : SubgroupImplementation::Emulated; + } + } + + // MOBILEGL_ITRP_FIX: whether iterationRP's two compute-shader repairs run (Config.h). Both + // passes are fingerprint-gated, so Auto applies them; only ForceOff replays the pack's bugs. + inline Bool ShouldApplyIterationRPFixes() { + return MG_Config::Features.IterationRPFix != MG_Config::QuirkOverride::ForceOff; + } + + inline const char* SubgroupImplementationName(const SubgroupImplementation implementation) { + switch (implementation) { + case SubgroupImplementation::Native: return "native"; + case SubgroupImplementation::Emulated: return "emulated"; + case SubgroupImplementation::None: break; + } + return "none"; + } +} // namespace MobileGL::MG_Backend diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 0481b349..8eef9107 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -368,9 +368,7 @@ if (MOBILEGL_ITEST_VK_ICD) if (MOBILEGL_ITEST_VK_ICD MATCHES "lvp_icd|lavapipe") message(STATUS "Integration tests: lavapipe ICD - forcing the iterationRP repairs on") list(APPEND MGL_ITEST_VULKAN_ENV - "MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1" - "MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1" - "MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1") + "MOBILEGL_ITRP_FIX=ForceOn") endif() endif() diff --git a/MobileGL/MG_IntegrationTest/Scenarios/IterationRPFirstReductionScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/IterationRPFirstReductionScenario.cpp index 28f2a7d8..90f93e4b 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/IterationRPFirstReductionScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/IterationRPFirstReductionScenario.cpp @@ -137,7 +137,7 @@ namespace MGITest { // 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 MagmaFixIterationRPSubgroupScratch patch, which + // the pack itself replays through the MOBILEGL_ITRP_FIX scratch 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 { diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index 3d49528c..b7d04644 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -162,9 +162,8 @@ TEST_F(ProgramTest, CompileFragment) { CompileShader(fs); } -TEST_F(ProgramTest, CompileVoxySubgroupProbeShader) { - char infoLog[1024] = ""; - const char* csSrc = R"(#version 430 +namespace { + const char* const kVoxySubgroupProbe = R"(#version 430 #extension GL_KHR_shader_subgroup_basic : require #extension GL_KHR_shader_subgroup_arithmetic : require layout(local_size_x=32) in; @@ -173,14 +172,48 @@ void main() { } )"; - GLuint cs = CreateShader(GL_COMPUTE_SHADER); - ShaderSource(cs, 1, &csSrc, nullptr); - CompileShader(cs); + GLint CompileStatusOf(GLenum stage, const char* source, String& infoLog) { + GLuint shader = CreateShader(stage); + ShaderSource(shader, 1, &source, nullptr); + CompileShader(shader); + GLint compileStatus = GL_FALSE; + GetShaderiv(shader, GL_COMPILE_STATUS, &compileStatus); + char log[1024] = ""; + GetShaderInfoLog(shader, sizeof(log), nullptr, log); + infoLog = log; + DeleteShader(shader); + return compileStatus; + } +} // namespace - GLint compileStatus = GL_FALSE; - GetShaderiv(cs, GL_COMPILE_STATUS, &compileStatus); - GetShaderInfoLog(cs, sizeof(infoLog), nullptr, infoLog); - EXPECT_EQ(compileStatus, GL_TRUE) << infoLog; +// A `#extension GL_KHR_shader_subgroup_* : require` compiles exactly where the active backend +// offers that feature in that stage: against native compute subgroups with arithmetic it does; +// against a backend advertising no subgroups at all (no native support, emulation forced off) the +// frontend refuses it, rather than accepting a shader the backend would then fail on. +TEST_F(ProgramTest, CompileVoxySubgroupProbeShader) { + auto previousBackend = Move(MG_Backend::pActiveBackendObject); + const auto savedEmulation = MG_Config::Features.SubgroupEmulation; + auto backend = MakeUnique(); + auto* backendPtr = backend.get(); + MG_Backend::pActiveBackendObject = Move(backend); + String infoLog; + + MG_External::VulkanCapabilities nativeCaps; + nativeCaps.SupportsShaderSubgroup = true; + nativeCaps.SubgroupSize = 64; + nativeCaps.SubgroupSupportedStages = VK_SHADER_STAGE_COMPUTE_BIT; + nativeCaps.SubgroupSupportedOperations = VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_ARITHMETIC_BIT; + backendPtr->ApplyVulkanCapabilitiesForTesting(nativeCaps); + EXPECT_EQ(CompileStatusOf(GL_COMPUTE_SHADER, kVoxySubgroupProbe, infoLog), GL_TRUE) << infoLog; + + MG_Config::Features.SubgroupEmulation = MG_Config::QuirkOverride::ForceOff; + MG_External::VulkanCapabilities noCaps; + backendPtr->ApplyVulkanCapabilitiesForTesting(noCaps); + EXPECT_EQ(CompileStatusOf(GL_COMPUTE_SHADER, kVoxySubgroupProbe, infoLog), GL_FALSE); + EXPECT_NE(infoLog.find("is not available to this shader stage"), String::npos) << infoLog; + + MG_Config::Features.SubgroupEmulation = savedEmulation; + MG_Backend::pActiveBackendObject = Move(previousBackend); } TEST_F(ProgramTest, CompileVoxyGpuShaderInt64QuadDecode) { diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index 26481f4f..036dbd08 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -6,6 +6,7 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header +#include #include #include #include @@ -858,18 +859,22 @@ TEST(FragmentInterpolationCapabilities, PlumbsGLESAndBothVulkanPropertyPaths) { EXPECT_EQ(vkBackend.GetDynamicParameters().FragmentInterpolationOffsetBits, 4); } -TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) { +// Native subgroups whenever Vulkan reports usable support; otherwise the 32-lane compute +// emulation by default, and nothing at all when MOBILEGL_SUBGROUP_EMULATION is ForceOff. +TEST(DirectVulkanSanity, AdvertisesNativeSubgroupsOrTheEmulation) { using namespace MobileGL; MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend; + const auto savedEmulation = MG_Config::Features.SubgroupEmulation; MG_External::VulkanCapabilities unsupportedCaps; unsupportedCaps.SupportsShaderSubgroup = false; unsupportedCaps.SubgroupSize = 32; unsupportedCaps.SubgroupSupportedStages = VK_SHADER_STAGE_COMPUTE_BIT; unsupportedCaps.SubgroupSupportedOperations = VK_SUBGROUP_FEATURE_BASIC_BIT; - backend.ApplyVulkanCapabilitiesForTesting(unsupportedCaps); + MG_Config::Features.SubgroupEmulation = MG_Config::QuirkOverride::ForceOff; + backend.ApplyVulkanCapabilitiesForTesting(unsupportedCaps); const auto& unsupportedExtensions = backend.GetRendererInfo().RendererGLInfo.Extensions; EXPECT_EQ(std::find(unsupportedExtensions.begin(), unsupportedExtensions.end(), E_GL_KHR_shader_subgroup), unsupportedExtensions.end()); @@ -877,6 +882,16 @@ TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) { EXPECT_EQ(backend.GetDynamicParameters().SubgroupSupportedStages, 0u); EXPECT_EQ(backend.GetDynamicParameters().SubgroupSupportedFeatures, 0u); + MG_Config::Features.SubgroupEmulation = MG_Config::QuirkOverride::Auto; + backend.ApplyVulkanCapabilitiesForTesting(unsupportedCaps); + const auto& emulatedExtensions = backend.GetRendererInfo().RendererGLInfo.Extensions; + EXPECT_NE(std::find(emulatedExtensions.begin(), emulatedExtensions.end(), E_GL_KHR_shader_subgroup), + emulatedExtensions.end()); + EXPECT_EQ(backend.GetDynamicParameters().SubgroupSize, MG_Backend::kEmulatedSubgroupSize); + EXPECT_EQ(backend.GetDynamicParameters().SubgroupSupportedStages, MG_Backend::kEmulatedSubgroupStages); + EXPECT_EQ(backend.GetDynamicParameters().SubgroupSupportedFeatures, MG_Backend::kEmulatedSubgroupFeatures); + MG_Config::Features.SubgroupEmulation = savedEmulation; + MG_External::VulkanCapabilities supportedCaps; supportedCaps.SupportsShaderSubgroup = true; supportedCaps.SubgroupSize = 32; diff --git a/MobileGL/MG_Test/ShaderTranspiler/EmulateSubgroupsTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/EmulateSubgroupsTest.cpp index cfddb3df..f008b50f 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/EmulateSubgroupsTest.cpp +++ b/MobileGL/MG_Test/ShaderTranspiler/EmulateSubgroupsTest.cpp @@ -209,6 +209,24 @@ TEST(EmulateSubgroupsPass, LowersEveryCategoryToSharedMemory) { EXPECT_GT(CountOpcode(output, spv::Op::OpControlBarrier), CountOpcode(input, spv::Op::OpControlBarrier)); } +// The stale-shared-load repair: every scratch read becomes an OpAtomicLoad of a uint word +// (floats, ints and vectors cross by bitcast), and the module still validates. +TEST(EmulateSubgroupsPass, ReadsScratchThroughAtomicLoadsWhenAsked) { + const Vector input = CompileStage(GL_COMPUTE_SHADER, kEveryCategorySource); + ASSERT_FALSE(input.empty()); + + Vector plain; + ASSERT_TRUE(ShaderCompiler::EmulateSubgroupsForVulkan(input, plain, 16384u, true, false)); + EXPECT_EQ(CountOpcode(plain, spv::Op::OpAtomicLoad), 0u); + + Vector atomic; + ASSERT_TRUE(ShaderCompiler::EmulateSubgroupsForVulkan(input, atomic, 16384u, true, true)); + ASSERT_TRUE(Validates(atomic)); + EXPECT_EQ(CountGroupNonUniform(atomic), 0u); + EXPECT_GT(CountOpcode(atomic, spv::Op::OpAtomicLoad), 0u); + EXPECT_GT(CountOpcode(atomic, spv::Op::OpBitcast), CountOpcode(plain, spv::Op::OpBitcast)); +} + TEST(EmulateSubgroupsPass, IsIdempotent) { const Vector input = CompileStage(GL_COMPUTE_SHADER, kEveryCategorySource); ASSERT_FALSE(input.empty()); diff --git a/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp index 27044448..fa298676 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp +++ b/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp @@ -982,6 +982,26 @@ TEST_F(TranslationCacheTest, L2KeyMovesWithEveryGateThatSteersTheEsslChain) { v.viewportIndexLoweringArmed = true; variants.emplace_back("viewportIndexLoweringArmed", BuildEsslTranslationKey(v)); } + { // arms EmulateSubgroupsPass on the ESSL path + EsslTranslationKeyInputs v = base; + v.subgroupEmulationArmed = true; + variants.emplace_back("subgroupEmulationArmed", BuildEsslTranslationKey(v)); + } + { // the emulation's scratch budget decides whether it declines + EsslTranslationKeyInputs v = base; + v.subgroupEmulationScratchBytes = 32768; + variants.emplace_back("subgroupEmulationScratchBytes", BuildEsslTranslationKey(v)); + } + { // arms FixIterationRPBarrierPass on the ESSL path + EsslTranslationKeyInputs v = base; + v.iterationRPBarrierFixArmed = true; + variants.emplace_back("iterationRPBarrierFixArmed", BuildEsslTranslationKey(v)); + } + { // the subgroup emulation reads its scratch through atomic loads + EsslTranslationKeyInputs v = base; + v.subgroupScratchAtomicReads = true; + variants.emplace_back("subgroupScratchAtomicReads", BuildEsslTranslationKey(v)); + } { // arms DeferGeometryEmitsForEssl EsslTranslationKeyInputs v = base; v.deferGeometryEmitsArmed = true; diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index c8115a23..8d581347 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -1171,6 +1171,9 @@ namespace MobileGL::MG_Util::BackendLoader { if (std::strcmp(extension, "GL_EXT_fragment_shading_rate") == 0) { caps.SupportsFragmentShadingRate = true; } + if (std::strcmp(extension, "GL_KHR_shader_subgroup") == 0) { + caps.SupportsShaderSubgroup = true; + } if (std::strcmp(extension, "GL_EXT_fragment_shading_rate_primitive") == 0) { caps.SupportsFragmentShadingRatePrimitive = true; } @@ -1292,6 +1295,27 @@ namespace MobileGL::MG_Util::BackendLoader { if (caps.SupportsFragmentShadingRate) { FillInFragmentShadingRateCapabilities(caps, glesFuncs); } + if (caps.SupportsShaderSubgroup) { + // Through glesFuncs, never the bare entry point (that is MobileGL's own getter). + GLint size = 0, stages = 0, features = 0; + GLboolean quad = GL_FALSE; + while (glesFuncs.glGetError() != GL_NO_ERROR) {} + glesFuncs.glGetIntegerv(GL_SUBGROUP_SIZE_KHR, &size); + glesFuncs.glGetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &stages); + glesFuncs.glGetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &features); + glesFuncs.glGetBooleanv(GL_SUBGROUP_QUAD_ALL_STAGES_KHR, &quad); + const Bool answered = glesFuncs.glGetError() == GL_NO_ERROR; + caps.SubgroupSize = size > 0 ? static_cast(size) : 0u; + caps.SubgroupSupportedStages = static_cast(stages); + caps.SubgroupSupportedFeatures = static_cast(features); + caps.SubgroupQuadOperationsInAllStages = quad == GL_TRUE; + caps.SupportsShaderSubgroup = answered && caps.SubgroupSize > 0 && + (caps.SubgroupSupportedStages & GL_COMPUTE_SHADER_BIT) != 0 && + (caps.SubgroupSupportedFeatures & GL_SUBGROUP_FEATURE_BASIC_BIT_KHR) != 0; + } + MGLOG_I(" GL_KHR_shader_subgroup (native ES): %s size=%u stages=0x%x features=0x%x", + caps.SupportsShaderSubgroup ? "usable" : "absent", caps.SubgroupSize, caps.SubgroupSupportedStages, + caps.SubgroupSupportedFeatures); // Core from ES 3.2 on, so an extension string is not required there; below 3.2 the // extension is, and the pointer still has to have resolved either way. const Bool esAtLeast32 = caps.GLESVersion.Major > 3 || @@ -1881,6 +1905,12 @@ namespace MobileGL::MG_Util::BackendLoader { } caps.MaxShaderStorageBufferBindings = maxShaderStorageBufferBindings; caps.MaxTextureBufferSize = maxTextureBufferSize; + { + GLint sharedMemory = 0; + while (glesFuncs.glGetError() != GL_NO_ERROR) {} + glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_SHARED_MEMORY_SIZE, &sharedMemory); + if (glesFuncs.glGetError() == GL_NO_ERROR && sharedMemory > 0) caps.MaxComputeSharedMemorySize = sharedMemory; + } // Through glesFuncs, like every other capability query here: a bare glGetIntegerv resolves // to MobileGL's own exported entry point, which answers this pname from the very // capability table being filled in - so the driver's real alignment never arrived and the @@ -2078,6 +2108,23 @@ namespace MobileGL::MG_Util::BackendLoader { ? "true (DirectGLES defers a writing geometry stage's emits behind its writes)" : "false"); + switch (MG_Config::Features.EsprytAtomicSubgroupScratch) { + case MG_Config::QuirkOverride::ForceOn: + caps.SharedMemoryLoadsGoStale = true; + break; + case MG_Config::QuirkOverride::ForceOff: + caps.SharedMemoryLoadsGoStale = false; + break; + case MG_Config::QuirkOverride::Auto: + default: + caps.SharedMemoryLoadsGoStale = SelfTest::SharedMemoryLoadsGoStale(glesFuncs); + break; + } + MGLOG_I(" Shared-memory loads go stale across barrier(): %s", + caps.SharedMemoryLoadsGoStale + ? "true (the subgroup emulation reads its scratch through atomic loads)" + : "false"); + caps.IsAngleRenderer = caps.GLESRendererString.find("ANGLE") != String::npos; caps.IsAngleLlvmpipeRenderer = caps.IsAngleRenderer && caps.GLESRendererString.find("llvmpipe") != String::npos; diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index 2c55251f..db8ac0df 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -1324,6 +1324,11 @@ namespace MobileGL { // then defers every emit of a geometry stage that writes memory behind those writes // (DeferGeometryEmitsPass). Defaults FALSE: never assumed broken on an unmeasured driver. Bool DropsGeometryWritesAfterEmit = false; + // True when two loads of one shared-memory word separated by a store and barrier() + // return the same (first) value (SelfTest::SharedMemoryLoadsGoStale - measured on + // Adreno). The subgroup emulation then reads its scratch through atomic loads. + // Defaults FALSE: never assumed broken on an unmeasured driver. + Bool SharedMemoryLoadsGoStale = false; Int UniformBufferOffsetAlignment = 256; // Its storage-buffer counterpart, queried separately because it is a separate limit: // Adreno 830 answers 32 for GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT and 64 for @@ -1380,6 +1385,18 @@ namespace MobileGL { // initialisers are those minimums, for a context that rejects the query. Int MaxComputeWorkGroupCount[3] = {65535, 65535, 65535}; Int MaxComputeWorkGroupSize[3] = {1024, 1024, 64}; + // GL_MAX_COMPUTE_SHARED_MEMORY_SIZE; the initialiser is the ES 3.1 minimum. The budget + // the subgroup emulation's workgroup scratch has to fit in. + Int MaxComputeSharedMemorySize = 16384; + // GL_KHR_shader_subgroup on the ES driver, with what it answers for it. Extension-only + // in every ES version; the values are meaningful only when the flag is set, and the + // flag is only set when the driver also answers a usable size (> 0) with the compute + // stage and the basic feature - the same rule the Vulkan loader applies. + Bool SupportsShaderSubgroup = false; + Uint32 SubgroupSize = 0; + Uint32 SubgroupSupportedStages = 0; // GL_*_SHADER_BIT, as the ES query answers + Uint32 SubgroupSupportedFeatures = 0; // GL_SUBGROUP_FEATURE_*_BIT_KHR + Bool SubgroupQuadOperationsInAllStages = false; Int MaxShaderStorageBufferBindings = 8; Int MaxTextureBufferSize = 65536; // GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained. diff --git a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp index afc42a12..840954b4 100644 --- a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp @@ -91,9 +91,6 @@ namespace MobileGL::MG_Util::BackendLoader { return loaded; } - Bool IsShaderSubgroupForcedDisabled() { - return MG_Config::Features.MagmaDisableSubgroup; - } } // namespace inline Version DecodeApiVersion(uint32_t version) { @@ -266,8 +263,7 @@ namespace MobileGL::MG_Util::BackendLoader { caps.MaxShaderStorageBlockSize = static_cast(p.limits.maxStorageBufferRange); const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 && HasUsableShaderSubgroupSupport(subgroupProps); - const Bool forceDisableShaderSubgroup = IsShaderSubgroupForcedDisabled(); - caps.SupportsShaderSubgroup = supportsShaderSubgroup && !forceDisableShaderSubgroup; + caps.SupportsShaderSubgroup = supportsShaderSubgroup; if (caps.SupportsShaderSubgroup) { caps.SubgroupSize = subgroupProps.subgroupSize; caps.SubgroupSupportedStages = subgroupProps.supportedStages; @@ -280,12 +276,9 @@ namespace MobileGL::MG_Util::BackendLoader { caps.SubgroupQuadOperationsInAllStages = false; } - MGLOG_I("Vulkan shader subgroup support: detected=%s advertised=%s size=%u stages=0x%x operations=0x%x", - supportsShaderSubgroup ? "true" : "false", caps.SupportsShaderSubgroup ? "true" : "false", - subgroupProps.subgroupSize, subgroupProps.supportedStages, subgroupProps.supportedOperations); - if (supportsShaderSubgroup && forceDisableShaderSubgroup) { - MGLOG_W("Vulkan shader subgroup support forced off by MOBILEGL_MAGMA_DISABLE_SUBGROUP"); - } + MGLOG_I("Vulkan shader subgroup support: native=%s size=%u stages=0x%x operations=0x%x", + supportsShaderSubgroup ? "true" : "false", subgroupProps.subgroupSize, + subgroupProps.supportedStages, subgroupProps.supportedOperations); return true; } diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp index 9beb23fb..3099431d 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp @@ -424,6 +424,112 @@ namespace MobileGL::MG_Util::SelfTest { return dropped; } + namespace { + constexpr const char* kStaleSharedProbeName = "stale shared-memory load"; + // a * 16 + b with a = 1 (first read) and b = 7 (second read). + constexpr GLuint kStaleSharedExpected = 0x17u; + // One result word per invocation of the 32-wide workgroup. + constexpr GLsizei kStaleSharedResultBytes = 32 * sizeof(GLuint); + + // The probe program around `read`, the expression that loads slot 0: a plain load (the + // subject) or an atomic one (the control). Every invocation writes its own result word, + // unconditionally: guarding the write (`if (lane == 0u)`) splits the control flow and the + // compiler then keeps the loads in place - measured, that shape reads 0x17 on the very + // driver where this one reads 0x11. + String StaleSharedComputeSource(const char* read) { + return String("#version 310 es\n" + "layout(local_size_x = 32) in;\n" + "layout(std430, binding = 0) buffer Result { highp uint data[32]; } g_result;\n" + "shared highp uint s_slots[32];\n" + "void main() {\n" + " highp uint lane = gl_LocalInvocationIndex;\n" + " s_slots[lane] = 1u;\n" + " barrier();\n" + " highp uint a = ") + + read + + ";\n" + " barrier();\n" + " s_slots[lane] = 7u;\n" + " barrier();\n" + " highp uint b = " + + read + + ";\n" + " barrier();\n" + " g_result.data[lane] = a * 16u + b;\n" + "}\n"; + } + + // Runs one variant and returns what invocation 0 wrote, or nullopt when the program + // could not be built or run. + Optional RunStaleSharedVariant(const GLESFunctionsTable& gl, const char* readExpression, + GLuint buffer) { + const String source = StaleSharedComputeSource(readExpression); + const GLuint shader = + CompileStage(gl, GL_COMPUTE_SHADER, source.c_str(), "compute", kStaleSharedProbeName); + if (shader == 0) return std::nullopt; + const GLuint program = gl.glCreateProgram(); + Optional result; + if (program != 0) { + gl.glAttachShader(program, shader); + gl.glLinkProgram(program); + GLint linked = 0; + gl.glGetProgramiv(program, GL_LINK_STATUS, &linked); + if (linked != GL_FALSE) { + const GLuint zero[kStaleSharedResultBytes / sizeof(GLuint)] = {}; + gl.glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer); + gl.glBufferData(GL_SHADER_STORAGE_BUFFER, kStaleSharedResultBytes, zero, GL_DYNAMIC_DRAW); + gl.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, buffer); + gl.glUseProgram(program); + Drain(gl); + gl.glDispatchCompute(1, 1, 1); + if (gl.glGetError() == GL_NO_ERROR) { + gl.glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT); + gl.glFinish(); + result = ReadFirstWord(gl, buffer); + } + } + gl.glDeleteProgram(program); + } + gl.glDeleteShader(shader); + return result; + } + } // namespace + + Bool ProbeSharedMemoryLoadsGoStale(const GLESFunctionsTable& gl) { + if (!HasEveryEntryPoint(gl) || gl.glDispatchCompute == nullptr) return false; + + SavedState saved; + Save(gl, saved); + Drain(gl); + + GLuint buffer = 0; + gl.glGenBuffers(1, &buffer); + const Optional plain = RunStaleSharedVariant(gl, "s_slots[0u]", buffer); + const Optional atomic = RunStaleSharedVariant(gl, "atomicAdd(s_slots[0u], 0u)", buffer); + if (buffer != 0) gl.glDeleteBuffers(1, &buffer); + Restore(gl, saved); + + if (!plain || !atomic) { + MGLOG_I("[driver-bug] %s probe reached no verdict (a variant did not build or run)", + kStaleSharedProbeName); + return false; + } + if (*atomic != kStaleSharedExpected) { + MGLOG_I("[driver-bug] %s probe reached no verdict: the atomic control read 0x%x (expected 0x%x)", + kStaleSharedProbeName, *atomic, kStaleSharedExpected); + return false; + } + const Bool stale = *plain != kStaleSharedExpected; + MGLOG_I("[driver-bug] %s probe: plain reads 0x%x, atomic reads 0x%x (expected 0x%x)%s", kStaleSharedProbeName, + *plain, *atomic, kStaleSharedExpected, stale ? " - SHARED LOADS GO STALE ACROSS barrier()" : ""); + return stale; + } + + Bool SharedMemoryLoadsGoStale(const GLESFunctionsTable& gl) { + static const Bool stale = ProbeSharedMemoryLoadsGoStale(gl); + return stale; + } + namespace { // ===================== SHARED PROGRAM BUILDING ===================== // The four probes below all decide their verdict from whether a program LINKS or from @@ -2294,6 +2400,27 @@ namespace MobileGL::MG_Util::SelfTest { Move(detail)}; } + Optional ProbeStaleSharedMemoryLoadBug(const GLESFunctionsTable& gl) { + if (!SharedMemoryLoadsGoStale(gl)) return std::nullopt; + String detail = + "the ES compiler treats ordinary loads from shared variables as pure: two reads of one " + "shared word separated by a store and barrier() (memoryBarrierShared() too) return the " + "first value, and a read can move past a later store; atomicAdd(x, 0u) reads of the same " + "words stay in place. "; + if (MG_Config::Features.EsprytAtomicSubgroupScratch == MG_Config::QuirkOverride::ForceOff) { + detail += "THE REPAIR IS DISABLED in this process (MOBILEGL_ESPRYT_ATOMIC_SUBGROUP_SCRATCH), so " + "an emulated subgroup operation can see the lanes of an earlier one"; + return DriverBugFinding{"Shared-memory loads go stale across barrier()", DriverBugVerdict::Unfixable, + Move(detail)}; + } + detail += "DirectGLES's subgroup emulation reads its workgroup scratch through atomic loads " + "(EmulateSubgroupsPass), so emulated subgroup operations see every lane's current value; " + "an application's own shared-memory code is passed through as written; override with " + "MOBILEGL_ESPRYT_ATOMIC_SUBGROUP_SCRATCH"; + return DriverBugFinding{"Shared-memory loads go stale across barrier()", DriverBugVerdict::Fixed, + Move(detail)}; + } + Optional ProbeR32FMultisampleSwizzleBug(const GLESFunctionsTable& gl) { if (!R32FMultisampleSwizzleCorrupted(gl)) return std::nullopt; return DriverBugFinding{ @@ -2466,6 +2593,7 @@ namespace MobileGL::MG_Util::SelfTest { using DriverBugProbeFn = Optional (*)(const GLESFunctionsTable&); constexpr DriverBugProbeFn kGlesDriverBugProbes[] = { &ProbeGeometryWriteAfterEmitBug, + &ProbeStaleSharedMemoryLoadBug, &ProbeR32FMultisampleSwizzleBug, &ProbeImageLocationPerNameBug, &ProbeCrossStageImageQualifierMergeBug, diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h index b03c7df8..6c82396d 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h @@ -184,6 +184,20 @@ namespace MobileGL::MG_Util::SelfTest { // ProbeGeometryStageSsboWriteAfterEmitDropped(), evaluated at most once per process. Bool GeometryStageSsboWriteAfterEmitDropped(const MG_External::GLESFunctionsTable& gl); + // Every invocation of a 32-wide compute workgroup stores 1 into its shared slot, barrier(), + // reads slot 0, barrier(), stores 7, barrier(), reads slot 0 again, and writes both reads + // to its own result word: the second read must see 7. The Adreno ES compiler treats ordinary shared loads as pure - it hands back the + // first read (and, with other addresses, moves a read past a later store) - while an + // atomicAdd(x, 0u) read of the same words is kept in place. That atomic twin is the + // control: the verdict is "stale" only when the plain program goes wrong while the atomic + // one stays right. Deterministic (a compiler transform), so latched; false when compute + // shaders or an entry point are missing or anything fails to set up. Restores the GL state + // it touches. + Bool ProbeSharedMemoryLoadsGoStale(const MG_External::GLESFunctionsTable& gl); + + // ProbeSharedMemoryLoadsGoStale(), evaluated at most once per process. + Bool SharedMemoryLoadsGoStale(const MG_External::GLESFunctionsTable& gl); + // Samples one R32F GL_TEXTURE_2D_MULTISAMPLE texel through a swizzled alpha channel, twice, // with a separately linked program each time. Returns true only when the swizzled read goes // wrong while every control read stays right. diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index 48b6a885..ea5f1d22 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -20,6 +20,8 @@ // MG_State code: it runs standalone, before MG_State::Init(). #include #include +#include +#include #include #include #include @@ -310,6 +312,37 @@ namespace MobileGL::MG_Util::SelfTest { driverLimitName, deviceLimit, deviceLimit)); } + // "Shader subgroups": how this backend implements GL_KHR_shader_subgroup on this driver + // (MG_Backend/SubgroupImplementation.h) - the driver's own operations at their native width, + // the 32-lane compute emulation, or nothing - and whether the iterationRP repairs are armed. + // `nativeDetail` describes what the driver offers natively, or is empty when it has nothing. + void ReportShaderSubgroups(ReportBuilder& builder, const Bool nativeSupported, const String& nativeDetail) { + using MG_Backend::SubgroupImplementation; + const String itrp = MG_Backend::ShouldApplyIterationRPFixes() + ? "iterationRP repairs armed (MOBILEGL_ITRP_FIX)" + : "iterationRP repairs DISABLED (MOBILEGL_ITRP_FIX=ForceOff)"; + switch (MG_Backend::ResolveSubgroupImplementation(nativeSupported)) { + case SubgroupImplementation::Native: + builder.Pass("Shader subgroups", format("native: {}; {}", nativeDetail, itrp)); + break; + case SubgroupImplementation::Emulated: + builder.Pass("Shader subgroups", + format("emulated: {}-lane virtual subgroups in workgroup-shared memory, compute stage " + "only, every feature ({}); {}", + MG_Backend::kEmulatedSubgroupSize, + nativeSupported ? "forced over native " + nativeDetail + + " by MOBILEGL_SUBGROUP_EMULATION=ForceOn" + : String("the driver has no subgroup operations"), + itrp)); + break; + case SubgroupImplementation::None: + builder.FailOptional("Shader subgroups", + "the driver has no subgroup operations and MOBILEGL_SUBGROUP_EMULATION=ForceOff " + "turned the emulation off, so GL_KHR_shader_subgroup is not advertised"); + break; + } + } + void EvaluateGlesChecklist(ReportBuilder& builder, const MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& glesFuncs) { const Int major = caps.GLESVersion.Major; @@ -1239,6 +1272,14 @@ namespace MobileGL::MG_Util::SelfTest { EvaluateGlesChecklist(builder, caps, glesFuncs); ProbeGlesTimerQuery(builder, caps, glesFuncs); ProbeGlesNoperspective(builder, caps, glesFuncs); + ReportShaderSubgroups( + builder, caps.SupportsShaderSubgroup, + caps.SupportsShaderSubgroup + ? format("GL_KHR_shader_subgroup, subgroup size {}, stages 0x{:x}, features 0x{:x} (advertised: " + "0x{:x}, what ESSL output can spell)", + caps.SubgroupSize, caps.SubgroupSupportedStages, caps.SubgroupSupportedFeatures, + caps.SubgroupSupportedFeatures & MG_Backend::DirectGLES::kEsslPrintableSubgroupFeatures) + : String()); builder.report.formatCapabilities.emplace(); MG_Backend::DirectGLES::PopulateFormatCapabilities( glesFuncs, caps, builder.report.formatCapabilities.value()); @@ -1266,11 +1307,16 @@ namespace MobileGL::MG_Util::SelfTest { if (summary.capsValid) { backendApiVersionString = MG_Backend::DirectGLES::FormatBackendAPIVersionString( summary.caps.GLESRendererString, summary.caps.GLESVersion.Major, summary.caps.GLESVersion.Minor); - advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectGLES::BuildAdvertisedExtensions( + Vector extensions = MG_Backend::DirectGLES::BuildAdvertisedExtensions( summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy, summary.caps.SupportsDrawIndirect, summary.caps.SupportsDrawIndirect && summary.caps.SupportsBaseInstance, - summary.caps.SupportsTextureView, summary.caps.SupportsTextureCubeMapArray)); + summary.caps.SupportsTextureView, summary.caps.SupportsTextureCubeMapArray); + if (MG_Backend::DirectGLES::ResolveEsprytSubgroupImplementation(summary.caps) != + MG_Backend::SubgroupImplementation::None) { + extensions.push_back(E_GL_KHR_shader_subgroup); + } + advertisedExtensions = JoinAdvertisedExtensions(extensions); } AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString, advertisedExtensions); @@ -1929,7 +1975,7 @@ namespace MobileGL::MG_Util::SelfTest { // Native iterationRP compute witness. This deliberately uses a separate // throwaway Vulkan device rather than the real renderer's queues, and it - // treats MOBILEGL_MAGMA_DISABLE_SUBGROUP as irrelevant: the row reports what the + // treats MOBILEGL_SUBGROUP_EMULATION 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, @@ -2999,26 +3045,19 @@ namespace MobileGL::MG_Util::SelfTest { (subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 && (subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0; // Same usability rule as the Vulkan capability loader's - // HasUsableShaderSubgroupSupport, which feeds the GL_KHR_shader_subgroup - // advertisement of the real backend. - summary.shaderSubgroupUsable = subgroupUsable; - if (subgroupUsable) { - builder.Pass("Compute shader subgroup", - format("basic subgroup operations in compute, subgroup size {}", - subgroupProperties.subgroupSize)); - } else { - builder.FailOptional("Compute shader subgroup", - "basic subgroup operations are not usable from compute shaders, so " - "MobileGL withholds GL_KHR_shader_subgroup and the subgroup " - "iteration-render-pass path cannot run; there is no scalar rewrite " - "that stands in for a subgroup reduction"); - } + // HasUsableShaderSubgroupSupport, which decides native vs emulated in the real backend. + ReportShaderSubgroups(builder, subgroupUsable, + subgroupUsable ? format("subgroup size {}, stages 0x{:x}, operations 0x{:x}", + subgroupProperties.subgroupSize, + subgroupProperties.supportedStages, + subgroupProperties.supportedOperations) + : String()); + summary.shaderSubgroupUsable = MG_Backend::ResolveSubgroupImplementation(subgroupUsable) != + MG_Backend::SubgroupImplementation::None; } else { - builder.FailOptional("Compute shader subgroup", - "subgroup properties could not be queried (no " - "vkGetPhysicalDeviceProperties2, or a pre-1.1 device), so MobileGL " - "withholds GL_KHR_shader_subgroup and the subgroup paths are " - "unavailable whatever the hardware can actually do"); + ReportShaderSubgroups(builder, false, String()); + summary.shaderSubgroupUsable = + MG_Backend::ResolveSubgroupImplementation(false) != MG_Backend::SubgroupImplementation::None; } ProbeVulkanIterationRPWitness(builder, getInstanceProcAddr, instance, physicalDevice, computeQueueFamilyIndex, diff --git a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h index ad7fa858..01235d4f 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h +++ b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h @@ -115,10 +115,11 @@ namespace MobileGL::MG_Util::ShaderTranspiler { // GlslangToSpv branches on which backend is active - ShaderAttrib::flags is 0 on // both production parse paths (ShaderCompileTask::RunCompilePipeline and // ClaimParsedShader). Backend identity steers the TRANSPILE, which is L2's key. - // * `advertisedExtensions`. Its only front-end consumer is ShaderSourceProcessor's - // FilterUnsupportedGpuShaderInt64, which REWRITES THE SOURCE TEXT - and the + // * `advertisedExtensions`, and params.SubgroupSupportedStages / SubgroupSupportedFeatures. + // Their only front-end consumer is ShaderSourceProcessor's + // FilterUnavailableShaderExtensions, which REWRITES THE SOURCE TEXT - and the // preprocessed text is in the L1 key verbatim, a strictly finer discriminator - // than the extension list. (E_GL_ARB_gpu_shader_fp64 is never read by the front + // than the extension list or the masks. (E_GL_ARB_gpu_shader_fp64 is never read by the front // end at all: MOBILEGL_ADVERTISE_FP64 only adds it to the extension STRING the // application queries, and glslang parses `double` the same way either way.) // * params.SupportsShaderFloat64, i.e. ConsumesFloat64Natively(). glslang produces diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 8c1728aa..0192a378 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -2049,11 +2049,12 @@ namespace MobileGL { bool ShaderCompiler::EmulateSubgroupsForVulkan(const Vector& inputBinary, Vector& outputBinary, const Uint32 maxWorkgroupScratchBytes, - const bool enableSpirvValidation) { + const bool enableSpirvValidation, + const bool atomicScratchReads) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); - optimizer.RegisterPass( - EmulateSubgroupsPass::CreateEmulateSubgroupsPass(maxWorkgroupScratchBytes)); + optimizer.RegisterPass(EmulateSubgroupsPass::CreateEmulateSubgroupsPass(maxWorkgroupScratchBytes, + atomicScratchReads)); return RunOptimizerChecked("EmulateSubgroupsForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index c96a0761..8b9367b5 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -383,17 +383,21 @@ namespace MobileGL { Vector& 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 + // 32-lane virtual subgroup built from workgroup-shared memory - what either + // backend runs on a driver with NO native subgroup support (or under + // MOBILEGL_SUBGROUP_EMULATION=ForceOn); the module is generic SPIR-V, consumed by + // DirectVulkan and transpiled to ESSL by DirectGLES alike. + // 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. + // minimum). atomicScratchReads reads the scratch back through atomic loads, + // for a driver whose plain shared-memory loads go stale across barrier() + // (SelfTest::SharedMemoryLoadsGoStale). See EmulateSubgroupsPass. static bool EmulateSubgroupsForVulkan(const Vector& inputBinary, Vector& outputBinary, Uint32 maxWorkgroupScratchBytes, - bool enableSpirvValidation = false); + bool enableSpirvValidation = false, + bool atomicScratchReads = 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 diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp index f848c980..187162a4 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp @@ -905,17 +905,68 @@ namespace { return token.substr(start, end - start); } - void FilterUnsupportedGpuShaderInt64(const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env, - MobileGL::String& source) { - if (env.IsExtensionAdvertised(MobileGL::E_GL_ARB_gpu_shader_int64)) { - return; + // GL_SHADER_STAGE bit of a stage, as GL_SUBGROUP_SUPPORTED_STAGES_KHR spells it. + GLbitfield SubgroupStageBit(MobileGL::ShaderStage stage) { + switch (MobileGL::MG_Util::ConvertShaderStageToGLEnum(stage)) { + case GL_VERTEX_SHADER: return GL_VERTEX_SHADER_BIT; + case GL_TESS_CONTROL_SHADER: return GL_TESS_CONTROL_SHADER_BIT; + case GL_TESS_EVALUATION_SHADER: return GL_TESS_EVALUATION_SHADER_BIT; + case GL_GEOMETRY_SHADER: return GL_GEOMETRY_SHADER_BIT; + case GL_FRAGMENT_SHADER: return GL_FRAGMENT_SHADER_BIT; + case GL_COMPUTE_SHADER: return GL_COMPUTE_SHADER_BIT; + default: return 0; } + } + // The GL_SUBGROUP_FEATURE_*_BIT_KHR an extension name stands for, or 0 for a name that is + // not one of the eight GL_KHR_shader_subgroup_* extensions. + GLbitfield SubgroupFeatureBit(const MobileGL::String& name) { + static const std::pair kFeatures[] = { + {"GL_KHR_shader_subgroup_basic", GL_SUBGROUP_FEATURE_BASIC_BIT_KHR}, + {"GL_KHR_shader_subgroup_vote", GL_SUBGROUP_FEATURE_VOTE_BIT_KHR}, + {"GL_KHR_shader_subgroup_arithmetic", GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR}, + {"GL_KHR_shader_subgroup_ballot", GL_SUBGROUP_FEATURE_BALLOT_BIT_KHR}, + {"GL_KHR_shader_subgroup_shuffle", GL_SUBGROUP_FEATURE_SHUFFLE_BIT_KHR}, + {"GL_KHR_shader_subgroup_shuffle_relative", GL_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT_KHR}, + {"GL_KHR_shader_subgroup_clustered", GL_SUBGROUP_FEATURE_CLUSTERED_BIT_KHR}, + {"GL_KHR_shader_subgroup_quad", GL_SUBGROUP_FEATURE_QUAD_BIT_KHR}, + }; + for (const auto& [featureName, bit] : kFeatures) { + if (name == featureName) return bit; + } + return 0; + } + + // Whether the shader-language extension `name` is one this backend cannot honour in this + // stage - which is what a `#extension name : require` must then fail on, and what an + // `enable` must not pretend to have turned on. glslang parses desktop GLSL 4.60, where + // these are always available, so without this a shader would compile here and then fail + // (or worse, silently do nothing) at the backend: + // * GL_ARB_gpu_shader_int64 when the extension is not advertised; + // * any GL_KHR_shader_subgroup_* when GL_KHR_shader_subgroup is not advertised, when + // this stage is not in GL_SUBGROUP_SUPPORTED_STAGES_KHR, or when that extension's + // feature is not in GL_SUBGROUP_SUPPORTED_FEATURES_KHR - the KHR spec's own terms for + // where each of the eight may be used. The stage and feature tests need a backend: + // with none, every extension reads as available (CompileEnv::IsExtensionAdvertised). + bool IsUnavailableShaderExtension(const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env, + MobileGL::ShaderStage stage, const MobileGL::String& name) { + if (name == "GL_ARB_gpu_shader_int64") return !env.IsExtensionAdvertised(MobileGL::E_GL_ARB_gpu_shader_int64); + const GLbitfield feature = SubgroupFeatureBit(name); + if (feature == 0) return false; + if (!env.IsExtensionAdvertised(MobileGL::E_GL_KHR_shader_subgroup)) return true; + if (!env.HasBackend()) return false; + return (env.params.SubgroupSupportedStages & SubgroupStageBit(stage)) == 0 || + (env.params.SubgroupSupportedFeatures & feature) == 0; + } + + void FilterUnavailableShaderExtensions(const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env, + MobileGL::ShaderStage stage, MobileGL::String& source) { // Detect the directive on a comment/string-masked copy so a commented-out - // "#extension GL_ARB_gpu_shader_int64" is never turned into a synthesized #error. Comments are - // no longer blanked in the delivered source (glslang handles them), so this pass must mask - // locally like its siblings. Masking preserves offsets, so edits collected against the scan - // apply verbatim to `source`; they are applied back-to-front to keep earlier offsets valid. + // "#extension" is never turned into a synthesized #error. Comments are no longer + // blanked in the delivered source (glslang handles them), so this pass must mask + // locally like its siblings. Masking preserves offsets, so edits collected against the + // scan apply verbatim to `source`; they are applied back-to-front to keep earlier + // offsets valid. const MobileGL::String scan = MaskCommentsAndQuotedText(source); struct DirectiveEdit { SizeT pos; @@ -923,6 +974,7 @@ namespace { MobileGL::String replacement; }; Vector edits; + Vector hiddenNames; SizeT lineStart = 0; while (lineStart < scan.size()) { @@ -931,69 +983,56 @@ namespace { if (!hasLineBreak) { lineEnd = scan.size(); } - const MobileGL::String line = scan.substr(lineStart, lineEnd - lineStart); + const SizeT replaceLen = lineEnd - lineStart + (hasLineBreak ? 1 : 0); SizeT probe = 0; - while (probe < line.size() && std::isspace(static_cast(line[probe]))) { - probe++; - } - + const auto skipSpace = [&] { + while (probe < line.size() && std::isspace(static_cast(line[probe]))) probe++; + }; + skipSpace(); if (probe < line.size() && line[probe] == '#') { probe++; - while (probe < line.size() && std::isspace(static_cast(line[probe]))) { - probe++; - } - + skipSpace(); constexpr const char* extensionToken = "extension"; constexpr SizeT extensionLen = 9; - const bool hasExtensionDirective = - probe + extensionLen <= line.size() && - line.compare(probe, extensionLen, extensionToken) == 0 && - (probe + extensionLen == line.size() || !IsIdentifierChar(line[probe + extensionLen])); - if (hasExtensionDirective) { + if (probe + extensionLen <= line.size() && line.compare(probe, extensionLen, extensionToken) == 0 && + (probe + extensionLen == line.size() || !IsIdentifierChar(line[probe + extensionLen]))) { probe += extensionLen; - while (probe < line.size() && std::isspace(static_cast(line[probe]))) { - probe++; - } - - constexpr const char* int64Extension = "GL_ARB_gpu_shader_int64"; - constexpr SizeT int64ExtensionLen = 23; - const bool hasInt64Extension = - probe + int64ExtensionLen <= line.size() && - line.compare(probe, int64ExtensionLen, int64Extension) == 0 && - (probe + int64ExtensionLen == line.size() || - !IsIdentifierChar(line[probe + int64ExtensionLen])); - if (hasInt64Extension) { - probe += int64ExtensionLen; - while (probe < line.size() && std::isspace(static_cast(line[probe]))) { - probe++; + skipSpace(); + const SizeT nameStart = probe; + while (probe < line.size() && IsIdentifierChar(line[probe])) probe++; + const MobileGL::String name = line.substr(nameStart, probe - nameStart); + skipSpace(); + if (!name.empty() && probe < line.size() && line[probe] == ':' && + IsUnavailableShaderExtension(env, stage, name)) { + const MobileGL::String behavior = TrimDirectiveToken(line.substr(probe + 1)); + if (behavior == "require") { + edits.push_back({lineStart, replaceLen, + "#error " + name + " is not available to this shader stage in MobileGL\n"}); + } else if (behavior == "enable" || behavior == "warn") { + edits.push_back({lineStart, replaceLen, "\n"}); } - - if (probe < line.size() && line[probe] == ':') { - probe++; - const MobileGL::String behavior = TrimDirectiveToken(line.substr(probe)); - const SizeT replaceLen = lineEnd - lineStart + (hasLineBreak ? 1 : 0); - if (behavior == "require") { - edits.push_back({lineStart, replaceLen, - "#error GL_ARB_gpu_shader_int64 is not advertised by MobileGL\n"}); - } else if (behavior == "enable" || behavior == "warn") { - edits.push_back({lineStart, replaceLen, "\n"}); - } - lineStart = lineEnd + (hasLineBreak ? 1 : 0); - continue; + if (std::find(hiddenNames.begin(), hiddenNames.end(), name) == hiddenNames.end()) { + hiddenNames.push_back(name); } } } } - lineStart = lineEnd + (hasLineBreak ? 1 : 0); } for (auto it = edits.rbegin(); it != edits.rend(); ++it) { source.replace(it->pos, it->len, it->replacement); } - - ReplaceIdentifier(source, "GL_ARB_gpu_shader_int64", "MG_DISABLED_GL_ARB_gpu_shader_int64"); + // The extension's own feature-test macro, so an `#ifdef` guard takes the fallback branch. + // int64 keeps its historical treatment: hidden whether or not the source names it. + if (IsUnavailableShaderExtension(env, stage, "GL_ARB_gpu_shader_int64") && + std::find(hiddenNames.begin(), hiddenNames.end(), "GL_ARB_gpu_shader_int64") == hiddenNames.end()) { + hiddenNames.push_back("GL_ARB_gpu_shader_int64"); + } + for (const auto& name : hiddenNames) { + ReplaceIdentifier(source, name, "MG_DISABLED_" + name); + } } // GLSL 4.30 4.1.9 allows an interface-block member array to be left unsized when it is NOT the @@ -1945,7 +1984,7 @@ namespace MobileGL { // identifier that merely contained the word. The GLES fallback for devices without // the extension lives in the backend, where device capabilities are known. - FilterUnsupportedGpuShaderInt64(env, source); + FilterUnavailableShaderExtensions(env, stage, source); CoerceUniformBlockPackingToStd140(source); // After the packing coercion: that one rewrites `packed`/`shared` in place and so // cannot move an offset this pass depends on, and reading the block declarations diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp index 7ef6df64..33692a64 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp @@ -637,13 +637,22 @@ namespace MobileGL { std::map scratchByType; const uint32_t cPadded = uintConst(paddedSlots); if (cPadded == 0u) return Status::Failure; - const auto scratchFor = [&](uint32_t elementTypeId) -> const Scratch* { - auto it = scratchByType.find(elementTypeId); + // `elementTypeId` is the scratch element type; with atomic scratch reads its + // array is instead a flat run of uint words, `wordsPerSlot` per slot (an atomic + // load needs a scalar integer pointer). Still one array per element type, so + // the types never share slots - the same aliasing as without atomic reads. + const auto scratchFor = [&](uint32_t elementTypeId, uint32_t wordsPerSlot) -> const Scratch* { + const uint32_t key = elementTypeId; + if (m_atomicScratchReads) elementTypeId = uintTypeId; + auto it = scratchByType.find(key); if (it != scratchByType.end()) return &it->second; + const uint32_t lengthId = + m_atomicScratchReads ? uintConst(paddedSlots * wordsPerSlot) : cPadded; + if (lengthId == 0u) return nullptr; const uint32_t arrayTypeId = irContext->TakeNextId(); irContext->AddType(spvtools::MakeUnique( irContext, spv::Op::OpTypeArray, 0, arrayTypeId, - Instruction::OperandList{IdOp(elementTypeId), IdOp(cPadded)})); + Instruction::OperandList{IdOp(elementTypeId), IdOp(lengthId)})); const uint32_t arrayPointerTypeId = typeMgr->FindPointerToType(arrayTypeId, spv::StorageClass::Workgroup); const uint32_t elementPointerTypeId = @@ -656,7 +665,7 @@ namespace MobileGL { {SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast(spv::StorageClass::Workgroup)}}})); Scratch scratch{variableId, elementTypeId, elementPointerTypeId}; - return &scratchByType.emplace(elementTypeId, scratch).first->second; + return &scratchByType.emplace(key, scratch).first->second; }; // The uint-domain twin of a participating type: bools travel as 0/1 words. @@ -691,25 +700,15 @@ namespace MobileGL { if (numVirtualConstId == 0u) return Status::Failure; // The x word of one of the five ballot-mask builtins for the calling - // invocation's virtual subgroup, bounded to lanes that exist (words y..w - // are zero at 32 lanes). + // invocation's virtual subgroup (words y..w are zero at 32 lanes). Defined + // over gl_SubgroupSize bits, NOT clipped to the lanes a partial last subgroup + // actually has: the masks name lane positions, and GL/Vulkan define them for + // every position below the subgroup size. const auto emitMaskWord = [&](Emitter& em, spv::BuiltIn builtin) -> uint32_t { const uint32_t index = em.Emit(spv::Op::OpLoad, uintTypeId, {IdOp(liiVariableId)}); const uint32_t lane = em.Emit(spv::Op::OpBitwiseAnd, uintTypeId, {IdOp(index), IdOp(c31)}); - const uint32_t base = em.Emit(spv::Op::OpBitwiseAnd, uintTypeId, - {IdOp(index), IdOp(cNotLane)}); - const uint32_t tailBits = - em.Emit(spv::Op::OpISub, uintTypeId, {IdOp(cTotal), IdOp(base)}); - const uint32_t full = em.Emit(spv::Op::OpUGreaterThanEqual, boolTypeId, - {IdOp(tailBits), IdOp(c32)}); - const uint32_t tailShift = em.Emit(spv::Op::OpShiftLeftLogical, uintTypeId, - {IdOp(c1), IdOp(tailBits)}); - const uint32_t tailMask = - em.Emit(spv::Op::OpISub, uintTypeId, {IdOp(tailShift), IdOp(c1)}); - const uint32_t exists = em.Emit(spv::Op::OpSelect, uintTypeId, - {IdOp(full), IdOp(cAllOnes), IdOp(tailMask)}); const auto lowMaskThroughLane = [&](bool inclusive) -> uint32_t { if (!inclusive) { const uint32_t bit = em.Emit(spv::Op::OpShiftLeftLogical, uintTypeId, @@ -748,7 +747,7 @@ namespace MobileGL { default: return 0u; } - return em.Emit(spv::Op::OpBitwiseAnd, uintTypeId, {IdOp(word), IdOp(exists)}); + return word; }; for (const BuiltinUse& use : builtinUses) { @@ -840,22 +839,73 @@ namespace MobileGL { {IdOp(valueId), IdOp(zeros)}); }; + // Atomic scratch reads keep each slot as componentCount uint words; the + // value crosses into that domain by bitcast (a no-op for uint data). + const uint32_t wordsPerSlot = site.valueType.componentCount; + const uint32_t wordTypeId = [&]() -> uint32_t { + if (!m_atomicScratchReads || wordsPerSlot == 1u) return uintTypeId; + const analysis::Type* registeredUint = typeMgr->GetType(uintTypeId); + analysis::Vector vectorType(registeredUint, wordsPerSlot); + return typeMgr->GetTypeInstruction(&vectorType); + }(); + const bool wordsNeedBitcast = scratchType.scalarTypeId != uintTypeId; + const auto wordIndex = [&](uint32_t slotId, uint32_t component) -> uint32_t { + if (wordsPerSlot == 1u) return slotId; + const uint32_t scaled = em.Emit(spv::Op::OpIMul, uintTypeId, + {IdOp(slotId), IdOp(uintConst(wordsPerSlot))}); + return em.Emit(spv::Op::OpIAdd, uintTypeId, {IdOp(scaled), IdOp(uintConst(component))}); + }; + const Scratch* scratch = nullptr; const auto storeOwnSlot = [&](uint32_t valueId) -> bool { - scratch = scratchFor(scratchType.typeId); + scratch = scratchFor(scratchType.typeId, wordsPerSlot); if (scratch == nullptr) return false; - const uint32_t pointer = - em.Emit(spv::Op::OpAccessChain, scratch->pointerTypeId, - {IdOp(scratch->variableId), IdOp(lii)}); - em.EmitNoResult(spv::Op::OpStore, {IdOp(pointer), IdOp(valueId)}); + if (!m_atomicScratchReads) { + const uint32_t pointer = + em.Emit(spv::Op::OpAccessChain, scratch->pointerTypeId, + {IdOp(scratch->variableId), IdOp(lii)}); + em.EmitNoResult(spv::Op::OpStore, {IdOp(pointer), IdOp(valueId)}); + } else { + const uint32_t words = + wordsNeedBitcast ? em.Emit(spv::Op::OpBitcast, wordTypeId, {IdOp(valueId)}) : valueId; + for (uint32_t c = 0; c < wordsPerSlot; ++c) { + const uint32_t word = + wordsPerSlot == 1u + ? words + : em.Emit(spv::Op::OpCompositeExtract, uintTypeId, + {IdOp(words), {SPV_OPERAND_TYPE_LITERAL_INTEGER, {c}}}); + const uint32_t pointer = + em.Emit(spv::Op::OpAccessChain, scratch->pointerTypeId, + {IdOp(scratch->variableId), IdOp(wordIndex(lii, c))}); + em.EmitNoResult(spv::Op::OpStore, {IdOp(pointer), IdOp(word)}); + } + } barrier(); return true; }; const auto readSlot = [&](uint32_t indexId) -> uint32_t { - const uint32_t pointer = - em.Emit(spv::Op::OpAccessChain, scratch->pointerTypeId, - {IdOp(scratch->variableId), IdOp(indexId)}); - return em.Emit(spv::Op::OpLoad, scratchType.typeId, {IdOp(pointer)}); + if (!m_atomicScratchReads) { + const uint32_t pointer = + em.Emit(spv::Op::OpAccessChain, scratch->pointerTypeId, + {IdOp(scratch->variableId), IdOp(indexId)}); + return em.Emit(spv::Op::OpLoad, scratchType.typeId, {IdOp(pointer)}); + } + const uint32_t scopeId = uintConst(static_cast(spv::Scope::Workgroup)); + Instruction::OperandList words; + uint32_t packed = 0; + for (uint32_t c = 0; c < wordsPerSlot; ++c) { + const uint32_t pointer = + em.Emit(spv::Op::OpAccessChain, scratch->pointerTypeId, + {IdOp(scratch->variableId), IdOp(wordIndex(indexId, c))}); + packed = em.Emit(spv::Op::OpAtomicLoad, uintTypeId, + {IdOp(pointer), IdOp(scopeId), IdOp(c0)}); + words.push_back(IdOp(packed)); + } + if (wordsPerSlot > 1u) { + packed = em.Emit(spv::Op::OpCompositeConstruct, wordTypeId, std::move(words)); + } + return wordsNeedBitcast ? em.Emit(spv::Op::OpBitcast, scratchType.typeId, {IdOp(packed)}) + : packed; }; // A select whose condition is a scalar bool but whose values may be // vectors; pre-SPIR-V-1.4 needs the condition splat to a bvec. @@ -966,20 +1016,6 @@ namespace MobileGL { } }; - // The lanes-that-exist word for this virtual subgroup (tail handling). - const auto existsWord = [&]() -> uint32_t { - const uint32_t tailBits = - em.Emit(spv::Op::OpISub, uintTypeId, {IdOp(cTotal), IdOp(base)}); - const uint32_t full = em.Emit(spv::Op::OpUGreaterThanEqual, boolTypeId, - {IdOp(tailBits), IdOp(c32)}); - const uint32_t shifted = em.Emit(spv::Op::OpShiftLeftLogical, uintTypeId, - {IdOp(c1), IdOp(tailBits)}); - const uint32_t mask = - em.Emit(spv::Op::OpISub, uintTypeId, {IdOp(shifted), IdOp(c1)}); - return em.Emit(spv::Op::OpSelect, uintTypeId, - {IdOp(full), IdOp(cAllOnes), IdOp(mask)}); - }; - const auto finish = [&](uint32_t finalId) { inst->SetOpcode(spv::Op::OpCopyObject); inst->SetInOperands({IdOp(finalId)}); @@ -1190,9 +1226,11 @@ namespace MobileGL { {IdOp(site.valueId), {SPV_OPERAND_TYPE_LITERAL_INTEGER, {0u}}}); uint32_t masked = 0; + // Counted over the gl_SubgroupSize (32) bits of the x word - the value is + // an arbitrary uvec4, and bits of lanes a partial subgroup lacks still + // count (subgroupBallotBitCount(uvec4(~0u)) == gl_SubgroupSize). if (site.groupOperation == spv::GroupOperation::Reduce) { - masked = em.Emit(spv::Op::OpBitwiseAnd, uintTypeId, - {IdOp(word), IdOp(existsWord())}); + masked = word; } else { const bool inclusive = site.groupOperation == spv::GroupOperation::InclusiveScan; @@ -1227,8 +1265,8 @@ namespace MobileGL { const uint32_t word = em.Emit(spv::Op::OpCompositeExtract, uintTypeId, {IdOp(site.valueId), {SPV_OPERAND_TYPE_LITERAL_INTEGER, {0u}}}); - const uint32_t masked = em.Emit(spv::Op::OpBitwiseAnd, uintTypeId, - {IdOp(word), IdOp(existsWord())}); + // Over the gl_SubgroupSize (32) bits of the x word, like the bit count. + const uint32_t masked = word; const uint32_t instNumber = site.opcode == spv::Op::OpGroupNonUniformBallotFindLSB ? kGlslFindILsb : kGlslFindUMsb; @@ -1404,9 +1442,9 @@ namespace MobileGL { } spvtools::Optimizer::PassToken EmulateSubgroupsPass::CreateEmulateSubgroupsPass( - const Uint32 maxWorkgroupScratchBytes) { + const Uint32 maxWorkgroupScratchBytes, const bool atomicScratchReads) { return spvtools::Optimizer::PassToken( - MakeUnique(maxWorkgroupScratchBytes)); + MakeUnique(maxWorkgroupScratchBytes, atomicScratchReads)); } } // namespace ShaderTranspiler } // namespace MG_Util diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.h index d2868bec..cb3b0ad7 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.h +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.h @@ -21,9 +21,9 @@ namespace MobileGL { // 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 + // This is never a substitute for real subgroups: by default it only runs on a + // driver with no native subgroup support at all (MG_Backend/SubgroupImplementation.h; + // MOBILEGL_SUBGROUP_EMULATION=ForceOn runs it everywhere, for testing). A device // with native subgroup operations - however narrow - uses them natively, with // FixIterationRPSubgroupScratchPass patching the known pack bug instead. // @@ -54,19 +54,28 @@ namespace MobileGL { // module whose lowering would add more workgroup scratch than // maxWorkgroupScratchBytes (pass the device's maxComputeSharedMemorySize; // 0 falls back to the 16384-byte Vulkan minimum). + // + // atomicScratchReads stores the scratch as plain 32-bit words and reads every + // slot back with OpAtomicLoad (atomicAdd(x, 0u) in ESSL), bit-casting to and from + // the participating type. It is for drivers whose compiler treats ordinary + // shared-memory loads as pure and so merges or reorders them across stores and + // barrier() (SelfTest::SharedMemoryLoadsGoStale, measured on Adreno's ES driver); + // an atomic load is a memory operation such a compiler keeps in place. class EmulateSubgroupsPass : public spvtools::opt::Pass { public: - explicit EmulateSubgroupsPass(Uint32 maxWorkgroupScratchBytes) - : m_maxWorkgroupScratchBytes(maxWorkgroupScratchBytes) {} + EmulateSubgroupsPass(Uint32 maxWorkgroupScratchBytes, bool atomicScratchReads) + : m_maxWorkgroupScratchBytes(maxWorkgroupScratchBytes), + m_atomicScratchReads(atomicScratchReads) {} const char* name() const override { return "emulate-subgroups"; } Status Process() override; static spvtools::Optimizer::PassToken CreateEmulateSubgroupsPass( - Uint32 maxWorkgroupScratchBytes); + Uint32 maxWorkgroupScratchBytes, bool atomicScratchReads = false); private: Uint32 m_maxWorkgroupScratchBytes; + bool m_atomicScratchReads; }; } // namespace ShaderTranspiler } // namespace MG_Util diff --git a/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp b/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp index f18ed561..de61f3ee 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp @@ -182,6 +182,10 @@ namespace MobileGL::MG_Util::ShaderTranspiler { builder.Value(static_cast(inputs.shaderType)); builder.Value(static_cast(inputs.viewportIndexLoweringArmed)); builder.Value(static_cast(inputs.deferGeometryEmitsArmed)); + builder.Value(static_cast(inputs.subgroupEmulationArmed)); + builder.Value(inputs.subgroupEmulationScratchBytes); + builder.Value(static_cast(inputs.iterationRPBarrierFixArmed)); + builder.Value(static_cast(inputs.subgroupScratchAtomicReads)); builder.Value(static_cast(inputs.supportsNoperspectiveInterpolation)); builder.Value(static_cast(inputs.supportsExtendedImageFormats)); builder.Value(inputs.maxColorTextureSamples); diff --git a/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h b/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h index 33199f03..803a7b73 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h +++ b/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h @@ -647,6 +647,15 @@ namespace MobileGL::MG_Util::ShaderTranspiler { // measured dropped storage writes after EmitVertex, or MOBILEGL_ESPRYT_DEFER_GEOMETRY_EMITS // forced it. Bool deferGeometryEmitsArmed = false; + // Whether EmulateSubgroupsPass lowers this (compute) module, and the workgroup-scratch + // budget it may spend (GL_MAX_COMPUTE_SHARED_MEMORY_SIZE, which decides whether it + // declines); plus whether FixIterationRPBarrierPass runs ahead of it. + Bool subgroupEmulationArmed = false; + Int subgroupEmulationScratchBytes = 0; + Bool iterationRPBarrierFixArmed = false; + // Whether that lowering reads its scratch through atomic loads (the driver POST measured + // stale shared-memory loads, or MOBILEGL_ESPRYT_ATOMIC_SUBGROUP_SCRATCH forced it). + Bool subgroupScratchAtomicReads = false; Bool supportsNoperspectiveInterpolation = false; // GL_NV_image_formats. Arms WidenImageFormatsForEssl, which re-declares every storage // image whose format GLSL ES core cannot spell in the core format that carries it and diff --git a/README.md b/README.md index 1cce3826..f36c1453 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,8 @@ MobileGL supports runtime configuration via environment variables. | `MOBILEGL_BACKEND_TYPE` | Select active backend implementation at startup. | `DirectGLES`, `DirectVulkan` | `DirectGLES` | | `MOBILEGL_DISABLE_TIMERQUERY` | Disable GPU timer-query exposure and use. | `0`, `1` | `0` | | `MOBILEGL_ESPRYT_USE_ANGLE` | Load ANGLE EGL/GLES libraries. | `0`, `1` | `0` | -| `MOBILEGL_MAGMA_DISABLE_SUBGROUP` | Disable Vulkan shader subgroup support. | `0`, `1` | `0` | +| `MOBILEGL_SUBGROUP_EMULATION` | `GL_KHR_shader_subgroup` emulation (32-lane, compute stage). `Auto` emulates only where the driver has no subgroup operations; `ForceOn` emulates even over native support; `ForceOff` never emulates (native support is still used). | `Auto`, `ForceOn`, `ForceOff` | `Auto` | +| `MOBILEGL_ITRP_FIX` | The iterationRP shaderpack repairs (scratch-array size, missing workgroup barrier); fingerprint-gated, other shaders are untouched. | `Auto`, `ForceOn`, `ForceOff` | `Auto` | | `MOBILEGL_ADVERTISE_FP64` | Advertise `GL_ARB_gpu_shader_fp64`. GLSL `double`/`dvec`/`dmat` compile and run either way - they are narrowed to 32 bits - so this only changes whether an application is told it has 64-bit precision, which it does not. | `0`, `1` | `0` | | `MOBILEGL_MAGMA_R11G11B10F_FALLBACK` | Use Magma's R11G11B10F format fallback. | `0`, `1` | `0` | | `MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV` | Hand a shader module that fails SPIR-V validation to the driver anyway. By default Magma declines such a program (no shader module is created, its draws and dispatches are refused and logged by name), because a driver that accepts invalid SPIR-V can fault the GPU into device loss. Diagnostic use only. | `0`, `1` | `0` | diff --git a/android-plugin/app/build.gradle.kts b/android-plugin/app/build.gradle.kts index 86c577a3..3478d655 100644 --- a/android-plugin/app/build.gradle.kts +++ b/android-plugin/app/build.gradle.kts @@ -109,7 +109,11 @@ val pluginRendererConfig = buildJsonValue { items = RendererConfig.EnvItems("DirectGLES", listOf("DirectVulkan")), ) toggleable("MOBILEGL_DISABLE_TIMERQUERY", "1", false, RendererConfig.MetaString("mobilegl_disable_timerquery_title")) - toggleable("MOBILEGL_MAGMA_DISABLE_SUBGROUP", "1", false, RendererConfig.MetaString("mobilegl_disable_subgroup_title")) + selectable( + key = "MOBILEGL_SUBGROUP_EMULATION", + title = RendererConfig.MetaString("mobilegl_subgroup_emulation_title"), + items = RendererConfig.EnvItems("Auto", listOf("ForceOn", "ForceOff")), + ) toggleable("MOBILEGL_MAGMA_R11G11B10F_FALLBACK", "1", false, RendererConfig.MetaString("mobilegl_magma_r11g11b10f_fallback_title")) customizable("MOBILEGL_FRAMES_IN_FLIGHT", "3", RendererConfig.MetaString("mobilegl_frames_inflight_title")) toggleable("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER", "1", false, RendererConfig.MetaString("mobilegl_avoid_sampler_mipmap_min_filter_title")) diff --git a/android-plugin/app/src/main/AndroidManifest.xml b/android-plugin/app/src/main/AndroidManifest.xml index f1cfa592..12d19606 100644 --- a/android-plugin/app/src/main/AndroidManifest.xml +++ b/android-plugin/app/src/main/AndroidManifest.xml @@ -96,8 +96,8 @@ android:name="mobilegl_disable_timerquery_title" android:resource="@string/mobilegl_disable_timerquery_title" /> + android:name="mobilegl_subgroup_emulation_title" + android:resource="@string/mobilegl_subgroup_emulation_title" /> diff --git a/android-plugin/app/src/main/res/values/strings.xml b/android-plugin/app/src/main/res/values/strings.xml index 7e4b4dfd..6e8ef137 100644 --- a/android-plugin/app/src/main/res/values/strings.xml +++ b/android-plugin/app/src/main/res/values/strings.xml @@ -3,7 +3,7 @@ MobileGL Rendering backend Disable GPU timer queries - Disable Vulkan shader subgroups + Shader subgroup emulation Use Magma R11G11B10F fallback Frames in flight (1-64) Avoid sampler mipmap minification filters diff --git a/android-plugin/app/src/trace/AndroidManifest.xml b/android-plugin/app/src/trace/AndroidManifest.xml index e1ebe1e9..332bc293 100644 --- a/android-plugin/app/src/trace/AndroidManifest.xml +++ b/android-plugin/app/src/trace/AndroidManifest.xml @@ -19,7 +19,7 @@ android:name="mobilegl_disable_timerquery_title" tools:node="remove" />