mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Feat] (Subgroups): use native subgroups or emulate them on both backends
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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
|
||||
|
||||
+6
-18
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user