[Feat] (Subgroups): use native subgroups or emulate them on both backends

This commit is contained in:
2026-10-13 06:03:57 -04:00
parent cfbaffd757
commit d6b0eba8fe
50 changed files with 909 additions and 360 deletions
+1 -1
View File
@@ -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'
+1 -3
View File
@@ -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}"
+6 -18
View File
@@ -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 }})
+28 -35
View File
@@ -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
+11 -6
View File
@@ -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<char>(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");
@@ -12,6 +12,7 @@
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/Utils.h>
#include <MG_Backend/DirectGLES/SubgroupPolicy.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Classifiers/TextureEnumClassifier.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
@@ -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;
@@ -7,6 +7,7 @@
// End of Source File Header
#include "Managers.h"
#include "SubgroupPolicy.h"
#include <MG_Backend/MGPipe/PipeInputs.h>
// 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<unsigned int> barrierFixedSpirv;
Vector<unsigned int> 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<Uint32>(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 =
@@ -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 <Includes.h>
#include <MG_Backend/SubgroupImplementation.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
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
@@ -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;
@@ -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<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported,
Bool nonZeroIndirectBaseInstanceSupported,
@@ -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
@@ -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
@@ -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;
}
@@ -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;
@@ -10,6 +10,7 @@
#include <Config.h>
#include <Includes.h>
#include <MG_Backend/SubgroupImplementation.h>
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
@@ -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 <Config.h>
#include <Includes.h>
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
+1 -3
View File
@@ -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()
@@ -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 {
+43 -10
View File
@@ -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<MG_Backend::DirectVulkan::BackendObject_DirectVulkan>();
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) {
+17 -2
View File
@@ -6,6 +6,7 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <MG_Backend/SubgroupImplementation.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <cstdlib>
@@ -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;
@@ -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<Uint32> input = CompileStage(GL_COMPUTE_SHADER, kEveryCategorySource);
ASSERT_FALSE(input.empty());
Vector<Uint32> plain;
ASSERT_TRUE(ShaderCompiler::EmulateSubgroupsForVulkan(input, plain, 16384u, true, false));
EXPECT_EQ(CountOpcode(plain, spv::Op::OpAtomicLoad), 0u);
Vector<Uint32> 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<Uint32> input = CompileStage(GL_COMPUTE_SHADER, kEveryCategorySource);
ASSERT_FALSE(input.empty());
@@ -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;
@@ -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<Uint32>(size) : 0u;
caps.SubgroupSupportedStages = static_cast<Uint32>(stages);
caps.SubgroupSupportedFeatures = static_cast<Uint32>(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;
@@ -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.
@@ -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<SizeT>(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;
}
@@ -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<GLuint> 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<GLuint> 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<GLuint> plain = RunStaleSharedVariant(gl, "s_slots[0u]", buffer);
const Optional<GLuint> 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<DriverBugFinding> 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<DriverBugFinding> ProbeR32FMultisampleSwizzleBug(const GLESFunctionsTable& gl) {
if (!R32FMultisampleSwizzleCorrupted(gl)) return std::nullopt;
return DriverBugFinding{
@@ -2466,6 +2593,7 @@ namespace MobileGL::MG_Util::SelfTest {
using DriverBugProbeFn = Optional<DriverBugFinding> (*)(const GLESFunctionsTable&);
constexpr DriverBugProbeFn kGlesDriverBugProbes[] = {
&ProbeGeometryWriteAfterEmitBug,
&ProbeStaleSharedMemoryLoadBug,
&ProbeR32FMultisampleSwizzleBug,
&ProbeImageLocationPerNameBug,
&ProbeCrossStageImageQualifierMergeBug,
@@ -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.
+61 -22
View File
@@ -20,6 +20,8 @@
// MG_State code: it runs standalone, before MG_State::Init().
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
#include <MG_Backend/DirectGLES/Utils.h>
#include <MG_Backend/DirectGLES/SubgroupPolicy.h>
#include <MG_Backend/SubgroupImplementation.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToStr/GLExtensionConverter.h>
@@ -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<GLExtension> 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,
@@ -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
@@ -2049,11 +2049,12 @@ namespace MobileGL {
bool ShaderCompiler::EmulateSubgroupsForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& 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);
@@ -383,17 +383,21 @@ namespace MobileGL {
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Lowers every GL_KHR_shader_subgroup construct in a compute module onto a
// 32-lane virtual subgroup built from workgroup-shared memory. Last-resort
// path for devices with NO native subgroup support, opt-in via
// MOBILEGL_MAGMA_EMULATE_SUBGROUP=1; a device with native subgroup
// operations always uses them. maxWorkgroupScratchBytes bounds the shared
// 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<Uint32>& inputBinary,
Vector<uint32_t>& 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
@@ -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<const char*, GLbitfield> 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<DirectiveEdit> edits;
Vector<MobileGL::String> 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<unsigned char>(line[probe]))) {
probe++;
}
const auto skipSpace = [&] {
while (probe < line.size() && std::isspace(static_cast<unsigned char>(line[probe]))) probe++;
};
skipSpace();
if (probe < line.size() && line[probe] == '#') {
probe++;
while (probe < line.size() && std::isspace(static_cast<unsigned char>(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<unsigned char>(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<unsigned char>(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
@@ -637,13 +637,22 @@ namespace MobileGL {
std::map<uint32_t, Scratch> 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<Instruction>(
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<uint32_t>(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<uint32_t>(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<EmulateSubgroupsPass>(maxWorkgroupScratchBytes));
MakeUnique<EmulateSubgroupsPass>(maxWorkgroupScratchBytes, atomicScratchReads));
}
} // namespace ShaderTranspiler
} // namespace MG_Util
@@ -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
@@ -182,6 +182,10 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
builder.Value(static_cast<Uint32>(inputs.shaderType));
builder.Value(static_cast<Uint8>(inputs.viewportIndexLoweringArmed));
builder.Value(static_cast<Uint8>(inputs.deferGeometryEmitsArmed));
builder.Value(static_cast<Uint8>(inputs.subgroupEmulationArmed));
builder.Value(inputs.subgroupEmulationScratchBytes);
builder.Value(static_cast<Uint8>(inputs.iterationRPBarrierFixArmed));
builder.Value(static_cast<Uint8>(inputs.subgroupScratchAtomicReads));
builder.Value(static_cast<Uint8>(inputs.supportsNoperspectiveInterpolation));
builder.Value(static_cast<Uint8>(inputs.supportsExtendedImageFormats));
builder.Value(inputs.maxColorTextureSamples);
@@ -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
+2 -1
View File
@@ -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` |
+5 -1
View File
@@ -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"))
@@ -96,8 +96,8 @@
android:name="mobilegl_disable_timerquery_title"
android:resource="@string/mobilegl_disable_timerquery_title" />
<meta-data
android:name="mobilegl_disable_subgroup_title"
android:resource="@string/mobilegl_disable_subgroup_title" />
android:name="mobilegl_subgroup_emulation_title"
android:resource="@string/mobilegl_subgroup_emulation_title" />
<meta-data
android:name="mobilegl_magma_r11g11b10f_fallback_title"
android:resource="@string/mobilegl_magma_r11g11b10f_fallback_title" />
@@ -3,7 +3,7 @@
<string name="app_name">MobileGL</string>
<string name="mobilegl_backend_type_title">Rendering backend</string>
<string name="mobilegl_disable_timerquery_title">Disable GPU timer queries</string>
<string name="mobilegl_disable_subgroup_title">Disable Vulkan shader subgroups</string>
<string name="mobilegl_subgroup_emulation_title">Shader subgroup emulation</string>
<string name="mobilegl_magma_r11g11b10f_fallback_title">Use Magma R11G11B10F fallback</string>
<string name="mobilegl_frames_inflight_title">Frames in flight (1-64)</string>
<string name="mobilegl_avoid_sampler_mipmap_min_filter_title">Avoid sampler mipmap minification filters</string>
@@ -19,7 +19,7 @@
android:name="mobilegl_disable_timerquery_title"
tools:node="remove" />
<meta-data
android:name="mobilegl_disable_subgroup_title"
android:name="mobilegl_subgroup_emulation_title"
tools:node="remove" />
<meta-data
android:name="mobilegl_magma_r11g11b10f_fallback_title"
@@ -194,20 +194,10 @@ bool LoadMobileGL(const Request& request, std::string& error) {
} else {
unsetenv("MOBILEGL_COHERENT_AS_FLUSH");
}
if (request.fixIterationRPSubgroupScratch) {
setenv("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH", "1", 1);
if (request.iterationRPFix) {
setenv("MOBILEGL_ITRP_FIX", "ForceOn", 1);
} else {
unsetenv("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
}
if (request.deriveNumSubgroups) {
setenv("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS", "1", 1);
} else {
unsetenv("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
}
if (request.iterationRPFixBarrier) {
setenv("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER", "1", 1);
} else {
unsetenv("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
unsetenv("MOBILEGL_ITRP_FIX");
}
if (request.fboAttachmentDumps.empty()) {
unsetenv("MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS");
@@ -991,10 +981,7 @@ bool WriteResultJson(const Request& request, const Result& result) {
<< (request.avoidAngleLlvmpipeSamplerMipmapMinFilter ? "true" : "false") << ",\n";
file << " \"avoidAngleLlvmpipeExplicitLodBias\": "
<< (request.avoidAngleLlvmpipeExplicitLodBias ? "true" : "false") << ",\n";
file << " \"fixIterationRPSubgroupScratch\": " << (request.fixIterationRPSubgroupScratch ? "true" : "false")
<< ",\n";
file << " \"deriveNumSubgroups\": " << (request.deriveNumSubgroups ? "true" : "false") << ",\n";
file << " \"iterationRPFixBarrier\": " << (request.iterationRPFixBarrier ? "true" : "false") << ",\n";
file << " \"iterationRPFix\": " << (request.iterationRPFix ? "true" : "false") << ",\n";
file << " \"holdMs\": " << request.holdMs << ",\n";
file << " \"mismatchPixels\": " << result.mismatchPixels;
if (request.benchmark) {
@@ -59,9 +59,7 @@ struct Request {
bool avoidAngleLlvmpipeSamplerMipmapMinFilter = false;
bool avoidAngleLlvmpipeExplicitLodBias = false;
bool coherentAsFlush = false;
bool fixIterationRPSubgroupScratch = false;
bool deriveNumSubgroups = false;
bool iterationRPFixBarrier = false;
bool iterationRPFix = false;
int holdMs = 0;
// Generic environment passthrough, each entry `KEY=VALUE` (an entry with no '='
// unsets KEY). Applied last, right before libMobileGL.so is loaded, so a knob that
@@ -111,9 +111,7 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
jboolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
jboolean avoidAngleLlvmpipeExplicitLodBias,
jboolean coherentAsFlush,
jboolean fixIterationRPSubgroupScratch,
jboolean deriveNumSubgroups,
jboolean iterationRPFixBarrier,
jboolean iterationRPFix,
jstring texture2dDumps,
jboolean benchmarkMode,
jint benchmarkTailFrames,
@@ -159,9 +157,7 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
avoidAngleLlvmpipeSamplerMipmapMinFilter == JNI_TRUE;
request.avoidAngleLlvmpipeExplicitLodBias = avoidAngleLlvmpipeExplicitLodBias == JNI_TRUE;
request.coherentAsFlush = coherentAsFlush == JNI_TRUE;
request.fixIterationRPSubgroupScratch = fixIterationRPSubgroupScratch == JNI_TRUE;
request.deriveNumSubgroups = deriveNumSubgroups == JNI_TRUE;
request.iterationRPFixBarrier = iterationRPFixBarrier == JNI_TRUE;
request.iterationRPFix = iterationRPFix == JNI_TRUE;
request.benchmark = benchmarkMode == JNI_TRUE;
request.benchmarkTailFrames = benchmarkTailFrames > 0
? benchmarkTailFrames
@@ -173,9 +173,7 @@ public final class TraceReplayActivity extends Activity {
request.avoidAngleLlvmpipeSamplerMipmapMinFilter,
request.avoidAngleLlvmpipeExplicitLodBias,
request.coherentAsFlush,
request.fixIterationRPSubgroupScratch,
request.deriveNumSubgroups,
request.iterationRPFixBarrier,
request.iterationRPFix,
request.texture2dDumps,
request.benchmark,
request.benchmarkTailFrames,
@@ -210,9 +208,7 @@ public final class TraceReplayActivity extends Activity {
boolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
boolean avoidAngleLlvmpipeExplicitLodBias,
boolean coherentAsFlush,
boolean fixIterationRPSubgroupScratch,
boolean deriveNumSubgroups,
boolean iterationRPFixBarrier,
boolean iterationRPFix,
String texture2dDumps,
boolean benchmark,
int benchmarkTailFrames,
@@ -340,9 +336,7 @@ public final class TraceReplayActivity extends Activity {
final boolean avoidAngleLlvmpipeSamplerMipmapMinFilter;
final boolean avoidAngleLlvmpipeExplicitLodBias;
final boolean coherentAsFlush;
final boolean fixIterationRPSubgroupScratch;
final boolean deriveNumSubgroups;
final boolean iterationRPFixBarrier;
final boolean iterationRPFix;
final String texture2dDumps;
// Benchmark (frame-timing) mode. Replays the whole trace, times every frame
// boundary, and skips the snapshot and the SSIM comparison; "passed" then only
@@ -377,9 +371,7 @@ public final class TraceReplayActivity extends Activity {
boolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
boolean avoidAngleLlvmpipeExplicitLodBias,
boolean coherentAsFlush,
boolean fixIterationRPSubgroupScratch,
boolean deriveNumSubgroups,
boolean iterationRPFixBarrier,
boolean iterationRPFix,
String texture2dDumps,
boolean benchmark,
int benchmarkTailFrames,
@@ -408,9 +400,7 @@ public final class TraceReplayActivity extends Activity {
this.avoidAngleLlvmpipeSamplerMipmapMinFilter = avoidAngleLlvmpipeSamplerMipmapMinFilter;
this.avoidAngleLlvmpipeExplicitLodBias = avoidAngleLlvmpipeExplicitLodBias;
this.coherentAsFlush = coherentAsFlush;
this.fixIterationRPSubgroupScratch = fixIterationRPSubgroupScratch;
this.deriveNumSubgroups = deriveNumSubgroups;
this.iterationRPFixBarrier = iterationRPFixBarrier;
this.iterationRPFix = iterationRPFix;
this.texture2dDumps = texture2dDumps;
this.benchmark = benchmark;
this.benchmarkTailFrames = benchmarkTailFrames;
@@ -463,9 +453,7 @@ public final class TraceReplayActivity extends Activity {
intent.getBooleanExtra("avoid_angle_llvmpipe_sampler_mipmap_min_filter", false),
intent.getBooleanExtra("avoid_angle_llvmpipe_explicit_lod_bias", false),
intent.getBooleanExtra("coherent_as_flush", false),
intent.getBooleanExtra("fix_iterationrp_subgroup_scratch", false),
intent.getBooleanExtra("derive_num_subgroups", false),
intent.getBooleanExtra("iterationrp_fix_barrier", false),
intent.getBooleanExtra("itrp_fix", false),
readString(intent, "texture_2d_dumps", ""),
intent.getBooleanExtra("benchmark", false),
intent.getIntExtra("benchmark_tail_frames", 200),
+4 -11
View File
@@ -47,8 +47,7 @@ Set MOBILEGL_TRACE_ANGLE_VARIANT to the packaged ANGLE short hash used by
DirectGLES replay.
Set MOBILEGL_RETRACE_USE_PBUFFER=1 or pass --use-pbuffer to run
against an offscreen EGL pbuffer instead of the Activity surface.
Set MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1,
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1, and MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1 to
Set MOBILEGL_ITRP_FIX=ForceOn to
forward the corresponding iterationRP SPIR-V repairs into the APK process.
Pass --avoid-angle-llvmpipe-sampler-mipmap-min-filter for DirectGLES traces that
need ANGLE llvmpipe sampler mipmap filters downgraded to avoid driver stalls.
@@ -468,15 +467,9 @@ run_retrace() {
if [ "${coherent_as_flush}" -eq 1 ]; then
set -- "$@" --ez coherent_as_flush true
fi
if [ "${MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH:-}" = "1" ]; then
set -- "$@" --ez fix_iterationrp_subgroup_scratch true
fi
if [ "${MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS:-}" = "1" ]; then
set -- "$@" --ez derive_num_subgroups true
fi
if [ "${MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER:-}" = "1" ]; then
set -- "$@" --ez iterationrp_fix_barrier true
fi
case "${MOBILEGL_ITRP_FIX:-}" in
1|true|on|forceon|ForceOn) set -- "$@" --ez itrp_fix true ;;
esac
if [ -n "${texture_2d_dumps}" ]; then
set -- "$@" --es texture_2d_dumps "${texture_2d_dumps}"
fi
+7 -8
View File
@@ -1,5 +1,6 @@
#include "trace_replay_core.hpp"
#include <cctype>
#include <cstdlib>
#include <iostream>
#include <string>
@@ -81,11 +82,6 @@ bool ReadDouble(int argc, char **argv, int &index, double &out) {
return true;
}
bool ReadEnvFlag(const char *name) {
const char *value = std::getenv(name);
return value != nullptr && std::string(value) == "1";
}
// The benchmark options are documented as --name=VALUE. The space-separated spelling every
// other option here uses is accepted too, so --benchmark-tail-frames=300 and
// --benchmark-tail-frames 300 both work; hasInlineValue says which one was given.
@@ -105,9 +101,12 @@ bool MatchOption(const std::string &arg, const char *name, std::string &inlineVa
bool ParseArgs(int argc, char **argv, mobilegl_trace::Request &request) {
request.backend = "DirectGLES";
request.fixIterationRPSubgroupScratch = ReadEnvFlag("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
request.deriveNumSubgroups = ReadEnvFlag("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
request.iterationRPFixBarrier = ReadEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
// MOBILEGL_ITRP_FIX is a tri-state (Auto/ForceOn/ForceOff); the request only carries ForceOn.
if (const char *itrpFix = std::getenv("MOBILEGL_ITRP_FIX")) {
std::string value = itrpFix;
for (char &ch : value) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
request.iterationRPFix = value == "1" || value == "true" || value == "on" || value == "forceon";
}
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
+1 -1
View File
@@ -52,7 +52,7 @@ The plugin advertises itself through manifest metadata. The launcher lists it as
| Metadata | Read by | Contents |
|---|---|---|
| `fclPlugin`, `renderer`, `boatEnv`, `pojavEnv` | launchers using the original FCL plugin format | Fixed environment: `LIBGL_ES=3`, `POJAV_RENDERER=opengles3`, `MOBILEGL_BACKEND_TYPE=DirectGLES`, plus `MOBILEGL_TRANSPORT=inproc` in an `inproc` build (see below) |
| `fclPlugin_V2` | launchers that read the v2 renderer-plugin format | The same environment plus user options: backend (`DirectGLES`/`DirectVulkan`), frames in flight, and toggles for `MOBILEGL_DISABLE_TIMERQUERY`, `MOBILEGL_MAGMA_DISABLE_SUBGROUP`, `MOBILEGL_MAGMA_R11G11B10F_FALLBACK`, `MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER`, `MOBILEGL_COHERENT_AS_FLUSH`, `MOBILEGL_RELAXED_SEMANTICS`, `MOBILEGL_ESPRYT_USE_ANGLE` |
| `fclPlugin_V2` | launchers that read the v2 renderer-plugin format | The same environment plus user options: backend (`DirectGLES`/`DirectVulkan`), frames in flight, and toggles for `MOBILEGL_DISABLE_TIMERQUERY`, `MOBILEGL_SUBGROUP_EMULATION`, `MOBILEGL_MAGMA_R11G11B10F_FALLBACK`, `MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER`, `MOBILEGL_COHERENT_AS_FLUSH`, `MOBILEGL_RELAXED_SEMANTICS`, `MOBILEGL_ESPRYT_USE_ANGLE` |
The FCL build this guide was checked against reads only the original format, so it shows no plugin
options and always starts the plugin with Espryt. To change anything there, use FCL's
+2 -1
View File
@@ -70,7 +70,8 @@ environment too. The backend itself is the exception: the client asks for it (se
| Variable | Values | Default | Meaning |
|---|---|---|---|
| `MOBILEGL_MAGMA_DISABLE_SUBGROUP` | flag | off | Do not use Vulkan subgroup operations in shaders |
| `MOBILEGL_SUBGROUP_EMULATION` | Auto / ForceOn / ForceOff | Auto | Subgroup emulation: Auto emulates only where the driver lacks subgroups, ForceOn always emulates, ForceOff never does (native subgroups stay) |
| `MOBILEGL_ITRP_FIX` | Auto / ForceOn / ForceOff | Auto | iterationRP shaderpack repairs; ForceOff replays the pack's own bugs |
| `MOBILEGL_MAGMA_R11G11B10F_FALLBACK` | flag | off | Use a fallback format for `GL_R11F_G11F_B10F` |
| `MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER` | 0-16777216 | 16384 | Submit a command buffer mid-frame after this many draws/dispatches (0 = never). Bounds driver memory in huge loading frames. |
| `MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES` | 0-1048576 | 120 | Free grown descriptor pools after this many quiet frames (0 = never) |
+1 -1
View File
@@ -116,7 +116,7 @@ In the plugin's **Render Server** screen:
- Older or non-conformant programs: `MOBILEGL_RELAXED_SEMANTICS=1`.
- Engines that write persistent buffer maps without flushing them: `MOBILEGL_COHERENT_AS_FLUSH=1`.
- Suspect shader translation: `MOBILEGL_SHADER_CACHE=0` rules out the translation cache.
- Magma on a driver with broken subgroup support: `MOBILEGL_MAGMA_DISABLE_SUBGROUP=1`.
- A driver with broken subgroup operations: `MOBILEGL_SUBGROUP_EMULATION=ForceOn` replaces them with MobileGL's compute-stage emulation.
## Platform-specific