Compare commits

..
Author SHA1 Message Date
Claude 37b20f2fca [Fix] (DirectGLES): repair the emulation-guard mask restore and scissor the resolve fallback's staging blit correctly
Final audit round over the DirectGLES scratch/shadow mechanisms; three verified
defects fixed:

- ~ScopedEmulationDrawState restored the APPLICATION's per-buffer colour masks,
  not what SyncRenderState actually pushed: a widened attachment's alpha-off
  doctoring (g_syncedColorMaskAlphaWidenMask) was dropped while the memo still
  claimed it applied, so the next sync early-outed and draws wrote fragment
  alpha into the widened buffer - breaking the stored-alpha==1.0 invariant the
  widen discipline exists to protect. The restore now re-applies the doctoring.
- The same restore loop gated on the core glColorMaski name only, while the sync
  push falls back to glColorMaskiEXT/OES: EXT/OES-only devices were left holding
  buffer 0's mask broadcast across every draw buffer with the shadow recording
  the divergent set (never repaired). The restore now uses the same three-way
  pointer fallback.
- ResolveThenBlit ran its resolve-into-scratch staging blit under the
  application's scissor: a box not covering the scratch-origin rect clipped the
  resolve silently (no GL error), and the second blit then copied stale scratch
  renderbuffer texels into the destination. The staging blit now runs scissor-off
  (shadow-tracked, like ScopedScissorDisable); the caller-visible blit keeps its
  native scissor semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsqQQF7ugn7MqZXcmnmz1z
2026-08-19 17:05:05 +00:00
Claude ee908aab46 [Fix] (DirectVulkan, GLImpl, MG_State, ShaderTranspiler): second audit round over the remaining memo sites
Six more verified defects from the residual memo/cache mechanisms:

- Program resource cache (DirectVulkan reflection): glShaderStorageBlockBinding
  deliberately does not bump the backend state version, and the SSO pipeline
  composite is unnamed so the by-name in-place patch can never reach its slot -
  the composite kept serving pre-rebind SSBO bindings. The cache now keys on the
  program's block-binding version; a binding-only change re-applies the overrides
  by name instead of re-running spirv-reflect. SetShaderStorageBlockBinding also
  gains the equality bail-out its uniform-block sibling has, so the composite
  mirror's replay stops churning the version every draw.
- LinkProgram's allowVSOnlyPrograms function-static latch never set its own
  initialized flag (dead memo, re-read every call) - and completing it would have
  frozen a per-backend capability across re-initialization. Replaced with a fresh
  per-link read from the null-checked active backend.
- Query object registry: drained at full library teardown (DestroyAllQueryObjects,
  mirroring DestroyAllSyncObjects) - undeleted queries and their backend wrappers
  leaked across Destroy/Initialize cycles, stale ids stayed IsQuery == GL_TRUE in
  the re-initialized library, and a later delete could hand the old backend's
  wrapper to a different backend's DeleteBackendQuery.
- Converted vertex streams and the host-side EBO max-index scan now SyncGpuWrites
  before reading the coherent mapping: XFB/SSBO/image writes are merely recorded
  at that point, so the conversion read pre-write bytes (the restart-index
  rewrite already synced; these two host reads did not).
- Zero-stride converted bindings: both converters rejected stride 0, making the
  factory's documented single-element conversion unreachable and silently
  dropping every draw using such a binding; the stride is substituted with the
  element size for the one-element case.
- DemoteFloat64Pass block relayout: measurement queued into the module eagerly,
  so a mid-struct failure left a half-relaid-out block (compacted offsets before
  the failing member, 64-bit offsets after) while claiming the block was left
  alone. Decoration writes are now collected and committed only when the whole
  block measures successfully.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsqQQF7ugn7MqZXcmnmz1z
2026-08-19 16:48:15 +00:00
Claude a4ccf3a837 [Fix] (DirectGLES, DirectVulkan, MG_State): close stale-cache, A-B-A and state-leak holes across the memo layers
Audit of every memoization implementation; sixteen verified defects fixed:

DirectGLES backend:
- Broadcast draw-buffer memo: cleared at MakeCurrent/DestroyEGLContext like its
  sibling shadows; its identity+version key is only monotonic within one GLContext,
  so a library teardown + re-init could false-hit on a recycled FBO address.
- Backend texture id re-mint (RecreateBackendTexture) now bumps an attachment
  generation that the SyncCurrentFBO gate and every FBO twin compare, so driver
  FBOs re-attach instead of keeping the deleted texture name; the attachment walk
  re-enters until the generation is quiescent (a walk itself can re-mint).
- Buffer id re-mint (persistent-map adoption, immutable-store retire) now bumps a
  generation the VAO twin sync compares, forcing a full re-emit of the baked
  glVertexAttribPointer / element-array bindings that frontend versions cannot see.
- VAO element-array sync memo: bound-object identity joins the wrapping Uint16
  slot version (same pairing the ResolvedDrawBuffers IBO memo already uses).

DirectVulkan backend:
- EBO slice memo gains the mapped-buffer guard its vertex-binding sibling has: a
  shadow-backed persistent map mutates with no epoch bump, so a hit must decline.
- VkClearManager::MergeClearPayload keeps colorEncoding/colorInt/colorUint with
  the color, so deferred glClearBufferiv/uiv no longer degrade to all-zero float.
- GetOrCreateComputePipeline no longer memoizes a failed creation (same contract
  as PipelineFactory): a transient driver failure was permanently disabling every
  dispatch of that program.
- Explicit-LOD-0 verdict memo keys on the sampling-resolution generation; sampler
  filter/aniso/LOD setters bump only that counter, so the old key served a stale
  verdict (wrong SPIR-V variant) after glTexParameter/glSamplerParameter changes.
- SetupDraw fast path declines instead of re-arming on a moved sampling-resolution
  generation (the snapshot bakes the LOD verdict into its pipeline), and
  recomputes the XfbCapture bit so the first draw after glBeginTransformFeedback
  cannot bind the undecorated variant and silently capture nothing.
- VertexInputStateFactory eviction epoch is drawn from a process-wide source: VAO
  state-pointer memos outlive the factory across renderer recreation, and a fresh
  factory restarting at epoch 1 would dereference a dead factory's entry.
- Cached render passes re-read the live renderbuffer clear payload at begin (the
  clear VALUE is not in the pass hash; the entry's inline snapshot replayed the
  creation-time color and dropped the newly queued one).
- FramebufferObject gains a never-reused lifetime id, keyed into the render-pass
  fast-path memo and the SetupDraw snapshot beside the raw pointer + Uint16
  version pair, which address reuse plus fresh version counts could equal.
- SyncTextureResource's preserved-content image goes through the deferred-release
  ring on both failure paths instead of a synchronous destructor under the GPU.

MG_State frontend:
- Layer-1 compile memo is env-disciplined like layers 2/3: a node computed against
  a dead CompileEnv (e.g. pre-capability fallback limits) no longer answers
  glCompileShader forever once the environment's content changes.
- Pipeline composite cache rebuilds from each stage program's last-link shader
  snapshot (new LinkedShaderRef list + pinned link inputs) instead of the live
  attach list and current compile nodes: post-link glAttachShader/glCompileShader
  must not leak into the composite while the (lifetimeId, linkVersion) signature
  still hits - GL's "as last linked" rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsqQQF7ugn7MqZXcmnmz1z
2026-08-19 16:35:39 +00:00
31 changed files with 196 additions and 1677 deletions
-3
View File
@@ -420,9 +420,6 @@ jobs:
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
run: |
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
test -f "${apk_file}"
-9
View File
@@ -265,9 +265,6 @@ jobs:
# crash stack without burning a CI round on an in-workflow debugger.
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
MOBILEGL_DERIVE_NUM_SUBGROUPS: "1"
MOBILEGL_ITERATIONRP_FIX_BARRIER: "1"
run: |
ulimit -c unlimited
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
@@ -642,12 +639,6 @@ jobs:
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
fi
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
&& [ '${{ matrix.case }}' = 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' ]; then
export MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1
export MOBILEGL_DERIVE_NUM_SUBGROUPS=1
export MOBILEGL_ITERATIONRP_FIX_BARRIER=1
fi
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
# runner has, so force it on for the OIT case it exists to fix. ForceOn
# bypasses only the vendor gate, so this exercises the real strip on
-1
View File
@@ -27,4 +27,3 @@ MobileGL/MG*/cmake-build*
tools/trace_replay/work/
__pycache__/
*.py[cod]
/.gradle
+1 -9
View File
@@ -285,7 +285,6 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
@@ -463,7 +462,7 @@ set(MOBILEGL_INCLUDE_DIR
# Header-only submodule: no add_subdirectory, no link target. Only
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
# pimpl so no consumer target needs this path.
${CMAKE_SOURCE_DIR}/3rdparty/asio/include
${CMAKE_SOURCE_DIR}/3rdparty/asio/asio/include
)
add_library(${CMAKE_PROJECT_NAME} SHARED
@@ -675,10 +674,3 @@ if (NOT ANDROID)
add_subdirectory(tools/trace_replay)
endif()
endif()
# The integration binary is also useful as a standalone adb-shell executable.
# Android cannot use the desktop-only MobileGL_s target, so its CMake module
# links libMobileGL.so and creates an AImageReader-backed window instead.
if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
add_subdirectory(MobileGL/MG_IntegrationTest)
endif()
-4
View File
@@ -100,10 +100,6 @@ namespace MobileGL::MG_Config {
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
// verbatim.
QuirkOverride FixIterationRPSubgroupScratch = QuirkOverride::Auto;
// MOBILEGL_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
// rendezvous between its two reductions over prefixSumCache. Off by default and
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
Bool IterationRPFixBarrier = false;
// MOBILEGL_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
-1
View File
@@ -171,7 +171,6 @@ namespace MobileGL::MG_ConfigLoader {
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
features.FixIterationRPSubgroupScratch =
QueryEnvQuirkOverride("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
features.IterationRPFixBarrier = QueryEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER");
features.DeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_DERIVE_NUM_SUBGROUPS");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
@@ -3190,25 +3190,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
// GL_KHR_shader_subgroup handling (SubgroupSupportPolicy.h). Native subgroup
// operations execute natively; module repairs keep the GL contract intact
// operations execute natively; two module repairs keep the GL contract intact
// around them. The opt-in emulation path replaces them only on devices with no
// subgroup support at all (MOBILEGL_MAGMA_EMULATE_SUBGROUP).
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Compute) {
// Program 203 broadcasts the first reduction through
// prefixSumCache[0], then lets the second reduction overwrite that
// scratch without first rendezvousing all readers. Patch that exact
// fingerprint before either native or emulated subgroup lowering.
if (m_subgroupPolicy.fixIterationRPBarrier) {
Vector<Uint> patchedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPBarrierForVulkan(
moduleSpirvs[i], patchedSpirv, enableSpirvValidation)) {
moduleSpirvs[i] = std::move(patchedSpirv);
} else {
MGLOG_E("ProgramFactory: iterationRP barrier patch failed for program %u; "
"Program 203 keeps its shared-scratch race",
program.GetExternalIndex());
}
}
if (m_subgroupPolicy.emulateSubgroups) {
Vector<Uint> emulatedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::EmulateSubgroupsForVulkan(
@@ -3228,7 +3213,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Uint> patchedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
moduleSpirvs[i], patchedSpirv, m_subgroupPolicy.nativeSubgroupSize,
m_subgroupPolicy.maxComputeSharedMemoryBytes,
enableSpirvValidation)) {
moduleSpirvs[i] = std::move(patchedSpirv);
} else {
@@ -381,7 +381,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
struct SubgroupLoweringPolicy {
Bool emulateSubgroups = false; // MOBILEGL_MAGMA_EMULATE_SUBGROUP, no-native-support devices
Bool fixIterationRPSubgroupScratch = false; // patch iterationRP's under-declared scratch
Bool fixIterationRPBarrier = false; // repair Program 203's shared-scratch race
Bool deriveNumSubgroups = false; // repair the NumSubgroups builtin
Bool requireFullSubgroups = false; // computeFullSubgroups enabled on the device
Uint32 nativeSubgroupSize = 0;
@@ -3063,7 +3063,6 @@ void main() {
subgroupPolicy.emulateSubgroups = ShouldEmulateSubgroups(m_nativeSubgroupSupported);
subgroupPolicy.fixIterationRPSubgroupScratch =
m_nativeSubgroupSupported && ShouldFixIterationRPSubgroupScratch();
subgroupPolicy.fixIterationRPBarrier = ShouldFixIterationRPBarrier();
subgroupPolicy.deriveNumSubgroups =
m_nativeSubgroupSupported && ShouldDeriveNumSubgroups();
subgroupPolicy.requireFullSubgroups = m_computeFullSubgroupsFeatureEnabled;
@@ -18,11 +18,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
//
// Native subgroups are the implementation whenever the device has them, whatever
// their width - subgroup operations execute on the hardware paths they were made
// for. Module-level repairs keep the GL contract intact around them:
// for. Two module-level repairs keep the GL contract intact around them:
// - FixIterationRPSubgroupScratchPass patches the one known pack bug: iterationRP's
// prefixSumCache[32], under-declared for sub-16-lane devices (8-lane lavapipe);
// - FixIterationRPBarrierPass repairs Program 203's race between two reductions
// reusing that scratch, when explicitly enabled;
// - DeriveNumSubgroupsPass replaces the one builtin drivers get wrong
// (gl_NumSubgroups) with the value the rest of the topology implies.
// The 32-lane shared-memory emulation (EmulateSubgroupsPass) is a LAST RESORT for
@@ -49,10 +47,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_Config::QuirkOverride::ForceOff;
}
inline Bool ShouldFixIterationRPBarrier() {
return MG_Config::Features.IterationRPFixBarrier;
}
inline Bool ShouldDeriveNumSubgroups() {
// Auto is ON: gl_NumSubgroups must agree with the gl_SubgroupID range for the GL
// contract to hold, and the derived ceil() value is the one the renderer can pin
+4 -25
View File
@@ -24,14 +24,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(MGL_ITEST_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..)
# Desktop links the static implementation directly. Android runs the same
# executable from adb shell and links the shipping shared library instead.
if (ANDROID)
set(MGL_ITEST_MOBILEGL_TARGET MobileGL)
elseif (TARGET MobileGL_s)
set(MGL_ITEST_MOBILEGL_TARGET MobileGL_s)
else()
message(STATUS "No MobileGL library target is available; skipping the integration test module")
# Only meaningful where MobileGL_s exists (i.e. not Android).
if (NOT TARGET MobileGL_s)
message(STATUS "MobileGL_s is not available; skipping the integration test module")
return()
endif()
@@ -74,7 +69,6 @@ add_executable(MobileGLIntegrationTest
Scenarios/UniformInitializerScenario.cpp
Scenarios/SwizzleAccessRoutineScenario.cpp
Scenarios/IterationRPFirstReductionScenario.cpp
Scenarios/IterationRPProgram203Scenario.cpp
Scenarios/IterationRPScratchFixScenario.cpp
Scenarios/ProgramPipelineScenario.cpp
Scenarios/ImageLoadStoreSsoScenario.cpp
@@ -100,20 +94,9 @@ target_include_directories(MobileGLIntegrationTest PRIVATE
# gtest, not gtest_main: Main.cpp installs the harness banner itself.
target_link_libraries(MobileGLIntegrationTest PRIVATE
GTest::gtest
${MGL_ITEST_MOBILEGL_TARGET}
MobileGL_s
)
if (ANDROID)
find_library(MGL_ITEST_ANDROID_LIBRARY android REQUIRED)
find_library(MGL_ITEST_LOG_LIBRARY log REQUIRED)
find_library(MGL_ITEST_MEDIANDK_LIBRARY mediandk REQUIRED)
target_link_libraries(MobileGLIntegrationTest PRIVATE
${MGL_ITEST_ANDROID_LIBRARY}
${MGL_ITEST_LOG_LIBRARY}
${MGL_ITEST_MEDIANDK_LIBRARY}
)
endif()
if (MSVC)
# Same reason as MG_Test/Backend/DirectVulkan: the GLES headers declare gl*
# as dllimport on Windows, so the in-library GL entry-point definitions only
@@ -122,10 +105,6 @@ if (MSVC)
endif()
target_compile_definitions(MobileGLIntegrationTest PRIVATE -DNOMINMAX)
if (ANDROID)
return()
endif()
# --- ctest wiring --------------------------------------------------------
# A bare libEGL on a glvnd box resolves to whatever vendor comes first, which is
# usually Mesa/llvmpipe - a software rasteriser silently replacing the GPU under
@@ -18,11 +18,6 @@
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#elif defined(__ANDROID__)
#include <android/hardware_buffer.h>
#include <android/native_window.h>
#include <media/NdkImage.h>
#include <media/NdkImageReader.h>
#endif
// MobileGL's own headers, in the order MobileGL/Includes.h uses them: GL/gl.h
@@ -42,7 +37,7 @@
// the only construction that is actually predictive here: MobileGL ABORTS
// (MOBILEGL_ASSERT -> SIGTRAP) rather than returning an error on an unusable
// platform, so nothing the parent can call in-process is allowed to be wrong.
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && __has_include(<sys/wait.h>)
#if !defined(_WIN32) && !defined(__APPLE__) && __has_include(<sys/wait.h>)
#define MGITEST_HAVE_FORK_PREFLIGHT 1
#include <csignal>
#include <ctime>
@@ -83,58 +78,12 @@ namespace MGITest {
CW_USEDEFAULT, CW_USEDEFAULT, kSurfaceWidth, kSurfaceHeight, nullptr, nullptr,
GetModuleHandleW(nullptr), nullptr);
}
#elif defined(__ANDROID__)
AImageReader* g_imageReader = nullptr;
ANativeWindow* g_imageReaderWindow = nullptr;
void DrainImageReader(void*, AImageReader* reader) {
AImage* image = nullptr;
if (AImageReader_acquireNextImage(reader, &image) == AMEDIA_OK && image != nullptr) {
AImage_delete(image);
}
}
bool CreateImageReaderWindow() {
if (g_imageReaderWindow != nullptr) return true;
constexpr int kMaxImages = 4;
const media_status_t status = AImageReader_newWithUsage(
kSurfaceWidth, kSurfaceHeight, AIMAGE_FORMAT_RGBA_8888,
AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT,
kMaxImages, &g_imageReader);
if (status != AMEDIA_OK || g_imageReader == nullptr) return false;
AImageReader_ImageListener listener = {nullptr, DrainImageReader};
AImageReader_setImageListener(g_imageReader, &listener);
if (AImageReader_getWindow(g_imageReader, &g_imageReaderWindow) != AMEDIA_OK ||
g_imageReaderWindow == nullptr) {
AImageReader_setImageListener(g_imageReader, nullptr);
AImageReader_delete(g_imageReader);
g_imageReader = nullptr;
return false;
}
ANativeWindow_acquire(g_imageReaderWindow);
return true;
}
void DestroyImageReaderWindow() {
if (g_imageReaderWindow != nullptr) {
ANativeWindow_release(g_imageReaderWindow);
g_imageReaderWindow = nullptr;
}
if (g_imageReader != nullptr) {
AImageReader_setImageListener(g_imageReader, nullptr);
AImageReader_delete(g_imageReader);
g_imageReader = nullptr;
}
}
#endif
bool UseWindowSurface() {
#if defined(_WIN32)
const char* value = std::getenv("MOBILEGL_ITEST_WINDOW_SURFACE");
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
#elif defined(__ANDROID__)
return true;
#else
return false;
#endif
@@ -174,10 +123,10 @@ namespace MGITest {
// callers). surfaceless is the platform with no window-system dependency at
// all; the surface this file then creates is still a pbuffer, which every
// platform supports and which the amendment to this rule requires as the
// fallback shape on desktop. Android instead supplies an AImageReader
// ANativeWindow. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
// fallback shape. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
// driver that consults them directly cannot reintroduce the dependency
// behind EGL's back.
// behind EGL's back. Desktop-only file: MG_IntegrationTest never builds
// for Android, so no device path is affected.
void EnsureHeadlessPlatform() {
#if defined(__linux__) && !defined(__ANDROID__)
static bool done = false;
@@ -265,21 +214,12 @@ namespace MGITest {
return 6;
}
surface = eglCreateWindowSurface(display, config, g_testWindow, nullptr);
#elif defined(__ANDROID__)
if (!CreateImageReaderWindow()) {
outReason = "failed to create the Android AImageReader integration-test window";
return 6;
}
surface = eglCreateWindowSurface(display, config, g_imageReaderWindow, nullptr);
#endif
} else {
const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE};
surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
}
if (surface == EGL_NO_SURFACE) {
#if defined(__ANDROID__)
DestroyImageReaderWindow();
#endif
outReason = WithEglError(useWindowSurface ? "eglCreateWindowSurface failed"
: "eglCreatePbufferSurface failed");
return 6;
@@ -608,8 +548,6 @@ namespace MGITest {
DestroyWindow(g_testWindow);
g_testWindow = nullptr;
}
#elif defined(__ANDROID__)
DestroyImageReaderWindow();
#endif
m_context = nullptr;
m_surface = nullptr;
@@ -14,11 +14,11 @@
// inspects backend state - both bugs this module pins were invisible to
// state-level assertions and visible only in pixels.
//
// Headless by construction: desktop uses an EGL pbuffer and Android uses an
// AImageReader-backed ANativeWindow that needs no Activity. No window manager,
// no human. Unlike DriverBench the scenarios do draw to the DEFAULT framebuffer
// (that is where the Y-flip lives) and do call eglSwapBuffers (that is the frame
// boundary the cross-frame scenarios need to be real).
// Headless by construction, following MG_Benchmark/Driver/DriverBench.c: an EGL
// context on a PBUFFER surface. No window, no window manager, no human. Unlike
// DriverBench the scenarios do draw to the DEFAULT framebuffer (that is where
// the Y-flip lives) and do call eglSwapBuffers (that is the frame boundary the
// cross-frame scenarios need to be real).
//
// One process is one backend: MOBILEGL_BACKEND_TYPE is latched at
// initialization, so the CMake wiring runs this binary once per backend rather
+1 -7
View File
@@ -31,14 +31,8 @@ namespace {
// silently bound to a workstation's window system is a different
// run from CI's and must be visible as one in the log.
const char* eglPlatform = std::getenv("EGL_PLATFORM");
#if defined(__ANDROID__)
constexpr const char* surfaceKind = "AImageReader window";
#else
constexpr const char* surfaceKind = "pbuffer";
#endif
std::fprintf(stderr, " renderer: %s\n surface: %dx%d %s (headless, EGL_PLATFORM=%s)\n",
std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless, EGL_PLATFORM=%s)\n",
gl.RendererString().c_str(), gl.Width(), gl.Height(),
surfaceKind,
eglPlatform != nullptr ? eglPlatform : "<unset>");
} else if (MGITest::RequireGpu()) {
std::fprintf(stderr,
@@ -1,379 +0,0 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IterationRPProgram203Scenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Full iterationRP Program 203 golden input/output fixture. The original shader
// consumes deterministic complete textures and uniforms, then its complete
// 512x513 RG16F output image is compared against fixed half-float golden bits.
// This catches both a wrong exposure slot and collateral scratch corruption.
#include <array>
#include <bit>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kSceneWidth = 854;
constexpr int kSceneHeight = 480;
constexpr int kPixelDataWidth = 512;
constexpr int kPixelDataHeight = 513;
constexpr std::size_t kSceneTexelCount =
static_cast<std::size_t>(kSceneWidth) * kSceneHeight;
constexpr std::size_t kPixelDataTexelCount =
static_cast<std::size_t>(kPixelDataWidth) * kPixelDataHeight;
struct Rgba32f {
float r, g, b, a;
};
struct Rg16 {
std::uint16_t r, g;
};
static_assert(sizeof(Rgba32f) == 16);
static_assert(sizeof(Rg16) == 4);
// Captured from the fixed fixture on Adreno 830. These are the exact
// RG16F storage bits for (0.806640625, 8.2578125), not rounded decimal
// comparisons performed by the test.
constexpr Rg16 kGoldenExposure = {0x3a74u, 0x4821u};
constexpr const char* kCommonSource = R"glsl(
#version 430 core
#extension GL_KHR_shader_subgroup_arithmetic : require
uniform int frameCounter;
uniform float frameTime;
uniform float aspectRatio;
uniform vec2 pixelSize;
uniform float nightVision;
uniform float darknessLightFactor;
uniform sampler2D colortex2;
uniform sampler2D pixelData2D;
layout(rg16f) uniform image2D img_pixelData2D;
float remapSaturate(float x, float e0, float e1) {
return clamp((x - e0) / (e1 - e0), 0.0f, 1.0f);
}
float GetExposureValue(float luminance) {
float aeCurve = 0.65f;
aeCurve = mix(aeCurve, clamp(aeCurve * 1.2f, 0.0f, 1.0f), nightVision);
aeCurve *= remapSaturate(luminance, 2.0f, 1.0f) * 0.6f + 0.4f;
float ae = pow(luminance, -aeCurve);
ae *= 1.0f - min(darknessLightFactor * 2.0f, 0.9f);
ae *= 8.5f;
return ae;
}
)glsl";
constexpr const char* kOriginalMain = R"glsl(
layout(local_size_x = 32, local_size_y = 16) in;
shared vec2 prefixSumCache[32];
void main() {
vec2 texCoord = (vec2(gl_GlobalInvocationID.xy) + 0.5f) * vec2(1.0f / 32.0f, 1.0f / 16.0f);
vec2 sampleCoord = texCoord * (1.0f / 64.0f);
sampleCoord.x += (15.0f / 32.0f) + pixelSize.x * 12.0f;
float tileExposure = dot(textureLod(colortex2, sampleCoord, 0.0f).rgb,
vec3(0.2125f, 0.7154f, 0.0721f));
vec2 sampleLuminance = vec2(tileExposure, 0.0f);
sampleLuminance = subgroupInclusiveAdd(sampleLuminance);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
barrier();
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint i = 0u; i < loopLength; ++i) {
if ((gl_SubgroupID & (1u << i)) > 0u) {
sampleLuminance += prefixSumCache[(gl_SubgroupID >> i << i) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
}
barrier();
}
if (gl_LocalInvocationIndex == 511u)
prefixSumCache[0] = sampleLuminance / 512.0f;
barrier();
float avg = prefixSumCache[0].x;
vec2 tileDistance = texCoord * 2.0f - 1.0f;
tileDistance.y /= aspectRatio;
float centerDistance = length(tileDistance);
float tileWeight = remapSaturate(centerDistance, 0.6f, 0.4f);
tileExposure = max(7.0E-7f, tileExposure);
float lumaWeight = avg / tileExposure;
lumaWeight = pow(lumaWeight, remapSaturate(avg, 0.02f, 0.001f) * 0.4f + 0.2f);
tileWeight *= lumaWeight;
vec2 sampleExposure = vec2(tileExposure * tileWeight, tileWeight);
sampleExposure = subgroupInclusiveAdd(sampleExposure);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleExposure;
barrier();
for (uint i = 0u; i < loopLength; ++i) {
if ((gl_SubgroupID & (1u << i)) > 0u) {
sampleExposure += prefixSumCache[(gl_SubgroupID >> i << i) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleExposure;
}
barrier();
}
if (gl_LocalInvocationIndex == 511u) {
float avgExposure = max(sampleExposure.x / sampleExposure.y * 29.3f, 1.0E-10f);
avgExposure = log2(avgExposure);
float prevAvgExposure = log2(texelFetch(pixelData2D, ivec2(0, 0), 0).x);
float frameTimeFixed = frameTime + step(frameCounter, 20) * 100.0f;
float exposureTime = clamp(frameTimeFixed * 2.0f, 0.0f, 1.0f);
avgExposure = mix(prevAvgExposure, avgExposure, exposureTime);
avgExposure = max(exp2(avgExposure), 1.0E-5f);
float exposure = GetExposureValue(avgExposure);
imageStore(img_pixelData2D, ivec2(0, 0), vec4(avgExposure, exposure, 0.0f, 0.0f));
}
}
)glsl";
GLuint CompileCompute(const char* mainSource, std::string* error) {
const std::array<const GLchar*, 2> sources = {kCommonSource, mainSource};
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, static_cast<GLsizei>(sources.size()), sources.data(), nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled != GL_TRUE) {
std::array<char, 8192> log{};
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size() - 1), nullptr, log.data());
*error = log.data();
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked != GL_TRUE) {
std::array<char, 8192> log{};
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size() - 1), nullptr, log.data());
*error = log.data();
glDeleteProgram(program);
return 0;
}
return program;
}
std::vector<Rgba32f> MakeSceneInput() {
std::vector<Rgba32f> texels(kSceneTexelCount);
for (int y = 0; y < kSceneHeight; ++y) {
for (int x = 0; x < kSceneWidth; ++x) {
std::uint32_t h = static_cast<std::uint32_t>(x) * 0x9e3779b9u;
h ^= static_cast<std::uint32_t>(y) * 0x85ebca6bu;
h ^= h >> 16u;
h *= 0x7feb352du;
h ^= h >> 15u;
const float noise = static_cast<float>(h & 0xffffu) / 65535.0f;
float base = 0.0002f + noise * 0.075f;
const float dx = static_cast<float>(x - 420);
const float dy = static_cast<float>(y - 4);
base += 0.65f * std::exp(-(dx * dx + dy * dy) / 18.0f);
if (((x + y * 17) % 113) == 0) base += 1.75f;
texels[static_cast<std::size_t>(y) * kSceneWidth + x] =
{base * 0.83f, base * 1.07f, base * 1.31f, 1.0f};
}
}
return texels;
}
std::uint16_t FloatToHalf(float value) {
const std::uint32_t bits = std::bit_cast<std::uint32_t>(value);
const std::uint32_t sign = (bits >> 16u) & 0x8000u;
const std::uint32_t exponent = (bits >> 23u) & 0xffu;
std::uint32_t mantissa = bits & 0x7fffffu;
if (exponent == 0xffu) {
return static_cast<std::uint16_t>(sign | (mantissa == 0 ? 0x7c00u : 0x7e00u));
}
int halfExponent = static_cast<int>(exponent) - 127 + 15;
if (halfExponent >= 31) return static_cast<std::uint16_t>(sign | 0x7c00u);
if (halfExponent <= 0) {
if (halfExponent < -10) return static_cast<std::uint16_t>(sign);
mantissa |= 0x800000u;
const unsigned shift = static_cast<unsigned>(14 - halfExponent);
const std::uint32_t rounded = mantissa + ((1u << (shift - 1u)) - 1u) +
((mantissa >> shift) & 1u);
return static_cast<std::uint16_t>(sign | (rounded >> shift));
}
mantissa += 0xfffu + ((mantissa >> 13u) & 1u);
if ((mantissa & 0x800000u) != 0) {
mantissa = 0;
if (++halfExponent >= 31) return static_cast<std::uint16_t>(sign | 0x7c00u);
}
return static_cast<std::uint16_t>(sign | (static_cast<std::uint32_t>(halfExponent) << 10u) |
(mantissa >> 13u));
}
std::vector<Rg16> MakePixelDataInput() {
std::vector<Rg16> texels(kPixelDataTexelCount);
for (std::size_t i = 0; i < texels.size(); ++i) {
texels[i] = {FloatToHalf(0.35f + static_cast<float>(i % 97u) * 0.0025f),
FloatToHalf(-0.45f + static_cast<float>(i % 89u) * 0.01f)};
}
texels[0] = {FloatToHalf(0.73f), FloatToHalf(1.25f)};
return texels;
}
std::vector<Rg16> MakeGoldenOutput() {
std::vector<Rg16> golden = MakePixelDataInput();
golden[0] = kGoldenExposure;
return golden;
}
GLuint MakeTexture(GLenum internalFormat, GLenum format, GLenum type, int width, int height,
const void* data) {
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(internalFormat), width, height, 0, format,
type, data);
return texture;
}
void BindAndDispatch(GLuint program, GLuint scene, GLuint pixelData) {
glUseProgram(program);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, scene);
glUniform1i(glGetUniformLocation(program, "colortex2"), 3);
glActiveTexture(GL_TEXTURE4);
glBindTexture(GL_TEXTURE_2D, pixelData);
glUniform1i(glGetUniformLocation(program, "pixelData2D"), 4);
glBindImageTexture(0, pixelData, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RG16F);
glUniform1i(glGetUniformLocation(program, "img_pixelData2D"), 0);
glUniform1i(glGetUniformLocation(program, "frameCounter"), 100);
glUniform1f(glGetUniformLocation(program, "frameTime"), 1.0f / 60.0f);
glUniform1f(glGetUniformLocation(program, "aspectRatio"),
static_cast<float>(kSceneWidth) / kSceneHeight);
glUniform2f(glGetUniformLocation(program, "pixelSize"), 1.0f / kSceneWidth, 1.0f / kSceneHeight);
glUniform1f(glGetUniformLocation(program, "nightVision"), 0.23f);
glUniform1f(glGetUniformLocation(program, "darknessLightFactor"), 0.08f);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
}
std::vector<Rg16> ReadWholeRgTexture(GLuint texture) {
std::vector<Rg16> texels(kPixelDataTexelCount);
glBindTexture(GL_TEXTURE_2D, texture);
glGetTexImage(GL_TEXTURE_2D, 0, GL_RG, GL_HALF_FLOAT, texels.data());
return texels;
}
class IterationRPProgram203Scenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
GLint stages = 0;
GLint features = 0;
GLint invocations = 0;
glGetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &stages);
glGetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &features);
glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &invocations);
const GLbitfield required =
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR;
if ((static_cast<GLbitfield>(stages) & GL_COMPUTE_SHADER_BIT) == 0 ||
(static_cast<GLbitfield>(features) & required) != required || invocations < 512) {
GTEST_SKIP() << "requires 512-invocation basic+arithmetic compute subgroups";
}
std::string error;
m_original = CompileCompute(kOriginalMain, &error);
ASSERT_NE(m_original, 0u) << "original Program 203: " << error;
const std::vector<Rgba32f> scene = MakeSceneInput();
const std::vector<Rg16> pixelData = MakePixelDataInput();
m_scene = MakeTexture(GL_RGBA16F, GL_RGBA, GL_FLOAT, kSceneWidth, kSceneHeight, scene.data());
m_originalOutput =
MakeTexture(GL_RG16F, GL_RG, GL_HALF_FLOAT, kPixelDataWidth, kPixelDataHeight,
pixelData.data());
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
}
void TearDown() override {
if (!Ready()) return;
const std::array<GLuint, 2> textures = {m_scene, m_originalOutput};
glDeleteTextures(static_cast<GLsizei>(textures.size()), textures.data());
if (m_original != 0) glDeleteProgram(m_original);
}
GLuint m_original = 0;
GLuint m_scene = 0;
GLuint m_originalOutput = 0;
};
} // namespace
TEST_F(IterationRPProgram203Scenario, FixedCompleteInputProducesFixedCompleteGoldenOutput) {
if (!Ready()) return;
BindAndDispatch(m_original, m_scene, m_originalOutput);
glFinish();
const std::vector<Rg16> actual = ReadWholeRgTexture(m_originalOutput);
const std::vector<Rg16> expected = MakeGoldenOutput();
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
std::size_t mismatchTexels = 0;
std::size_t firstMismatch = actual.size();
for (std::size_t i = 0; i < actual.size(); ++i) {
if (actual[i].r != expected[i].r || actual[i].g != expected[i].g) {
if (firstMismatch == actual.size()) firstMismatch = i;
++mismatchTexels;
}
}
RecordProperty("program203_output_width", kPixelDataWidth);
RecordProperty("program203_output_height", kPixelDataHeight);
RecordProperty("program203_compared_texels", static_cast<long long>(actual.size()));
RecordProperty("program203_mismatch_texels", static_cast<long long>(mismatchTexels));
std::cout << "IterationRPProgram203Scenario complete-output actualExposureBits=(0x" << std::hex
<< actual[0].r << ", 0x" << actual[0].g << ") goldenExposureBits=(0x" << expected[0].r
<< ", 0x" << expected[0].g << std::dec << ") mismatches=" << mismatchTexels << '/'
<< actual.size() << '\n';
if (firstMismatch != actual.size()) {
const std::size_t x = firstMismatch % kPixelDataWidth;
const std::size_t y = firstMismatch / kPixelDataWidth;
ADD_FAILURE() << "complete Program 203 output differs at " << x << ',' << y
<< ": actual half bits=(0x" << std::hex << actual[firstMismatch].r << ", 0x"
<< actual[firstMismatch].g << ") golden half bits=(0x" << expected[firstMismatch].r
<< ", 0x" << expected[firstMismatch].g << std::dec << "); mismatched "
<< mismatchTexels << " of " << actual.size() << " texels";
}
EXPECT_EQ(mismatchTexels, 0u);
}
} // namespace MGITest
@@ -8,13 +8,11 @@
//
// Scenario - THE FIXTURE-SHAPED SUBGROUP REDUCTION, ON WHATEVER WIDTH THE DEVICE HAS.
//
// iterationRP hard-sizes the scratch its subgroup prefix scans write through
// prefixSumCache[gl_SubgroupID], and ships that idiom twice: the auto-exposure pass
// declares `shared vec2 prefixSumCache[32]` for a 512-invocation workgroup, and the
// RTW importance warp declares `shared float prefixSumCache[64]` for a 1024-invocation
// one. Both algorithms are width-agnostic; only the static lengths bake in "at most 32
// (respectively 64) subgroups", which every desktop capture satisfies and an 8-lane
// device (lavapipe: 64 and 128 subgroups) does not. DirectVulkan patches exactly that with
// iterationRP's auto-exposure pass declares `shared vec2 prefixSumCache[32]` for a
// 512-invocation workgroup and combines per-subgroup subtotals through
// prefixSumCache[gl_SubgroupID]. The algorithm is width-agnostic; only the static 32
// bakes in "at most 32 subgroups", which every desktop capture satisfies and an 8-lane
// device (lavapipe: 64 subgroups) does not. DirectVulkan patches exactly that with
// FixIterationRPSubgroupScratchPass, growing the array to ceil(invocations / native
// width) on the modules that match the pack's reduction fingerprint.
//
@@ -49,10 +47,6 @@ namespace MGITest {
constexpr std::uint32_t kInvocationCount = 512u;
// sum of 0..511, exactly representable and associativity-proof in fp32.
constexpr float kExpectedTotal = 130816.0f;
// The RTW warp's shape: 1024 invocations into a 64-entry float scratch.
constexpr std::uint32_t kWideInvocationCount = 1024u;
// sum of 0..1023, likewise exact in fp32.
constexpr float kWideExpectedTotal = 523776.0f;
constexpr const char* kComputeSource = R"(#version 430 core
#extension GL_KHR_shader_subgroup_basic : require
@@ -93,50 +87,6 @@ void main() {
}
atomicMax(outputData.maxSubgroupId, gl_SubgroupID);
}
)";
// The RTW importance warp's shape: a plain float scan over 1024 invocations
// into a 64-entry scratch. Same idiom, different dimensions - which is exactly
// what a fingerprint pinned to the exposure pass's shape walks past.
constexpr const char* kWideComputeSource = R"(#version 430 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 1024) in;
layout(std430, binding = 0) buffer Output {
float total;
uint numSubgroups;
uint maxSubgroupId;
} outputData;
shared float prefixSumCache[64];
void main() {
float importance = float(gl_LocalInvocationID.x);
float prefixSum = subgroupInclusiveAdd(importance);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = prefixSum;
barrier();
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) {
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
prefixSum += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = prefixSum;
}
barrier();
}
if (gl_LocalInvocationID.x == 1023u) {
outputData.total = prefixSum;
outputData.numSubgroups = gl_NumSubgroups;
}
atomicMax(outputData.maxSubgroupId, gl_SubgroupID);
}
)";
struct OutputBlock {
@@ -180,7 +130,8 @@ void main() {
"512-invocation workgroup";
}
m_maxInvocations = static_cast<std::uint32_t>(invocations);
m_program = CompileComputeProgram(kComputeSource);
ASSERT_NE(m_program, 0u) << m_buildLog;
glGenBuffers(1, &m_output);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
@@ -230,14 +181,7 @@ void main() {
return program;
}
// Re-poisons the block, compiles the shape under test and runs it once.
OutputBlock Dispatch(const char* source) {
const OutputBlock poison{-1.0f, 0xa5a5a5a5u, 0u};
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(OutputBlock), &poison);
m_program = CompileComputeProgram(source);
EXPECT_NE(m_program, 0u) << m_buildLog;
if (m_program == 0u) return OutputBlock{};
OutputBlock Dispatch() {
glUseProgram(m_program);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
@@ -249,13 +193,12 @@ void main() {
GLuint m_program = 0;
GLuint m_output = 0;
std::uint32_t m_maxInvocations = 0;
std::string m_buildLog;
};
} // namespace
TEST_F(IterationRPScratchFixScenario, FixtureShapedReductionSumsEveryInvocation) {
const OutputBlock block = Dispatch(kComputeSource);
const OutputBlock block = Dispatch();
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
// The topology diagnostics catch the failure modes by name before the sum does:
@@ -276,27 +219,4 @@ void main() {
<< "workgroup reduction produced " << block.total << " with gl_NumSubgroups="
<< block.numSubgroups;
}
// The pack's second instance of the same bug, and the one that kept the CI
// retrace red after the exposure pass alone was patched.
TEST_F(IterationRPScratchFixScenario, WideFixtureShapedReductionSumsEveryInvocation) {
if (m_maxInvocations < kWideInvocationCount) {
GTEST_SKIP() << "needs a " << kWideInvocationCount << "-invocation workgroup";
}
const OutputBlock block = Dispatch(kWideComputeSource);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
ASSERT_NE(block.numSubgroups, 0xa5a5a5a5u) << "invocation 1023 never reached its store";
EXPECT_GE(block.numSubgroups, 1u);
EXPECT_LE(block.numSubgroups, kWideInvocationCount);
EXPECT_LT(block.maxSubgroupId, block.numSubgroups)
<< "gl_SubgroupID exceeds gl_NumSubgroups - the inconsistency "
"DeriveNumSubgroupsPass exists to repair";
// Without the patch an 8-lane device writes prefixSumCache[64..127] out of
// bounds and this comparison fails.
EXPECT_EQ(block.total, kWideExpectedTotal)
<< "workgroup reduction produced " << block.total << " with gl_NumSubgroups="
<< block.numSubgroups;
}
} // namespace MGITest
@@ -4,7 +4,6 @@ add_executable(
SpirvPassTest
SpirvPassTest.cpp
DeriveNumSubgroupsTest.cpp
FixIterationRPBarrierTest.cpp
FixIterationRPSubgroupScratchTest.cpp
EmulateSubgroupsTest.cpp
DemoteFloat64Test.cpp
@@ -1,206 +0,0 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/FixIterationRPBarrierTest.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#define SPV_ENABLE_UTILITY_CODE
#include "glslang/SPIRV/spirv.hpp11"
#undef SPV_ENABLE_UTILITY_CODE
#include "Includes.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
#include <map>
#include <vector>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
constexpr SizeT kSpirvHeaderWordCount = 5u;
template <typename Visitor>
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
const Uint32 wordCount = spirv[offset] >> 16u;
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
offset += wordCount;
}
}
Vector<Uint32> CompileCompute(const String& source) {
using namespace MobileGL::MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
bool Validates(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
tools.SetMessageConsumer(
[](spv_message_level_t, const char*, const spv_position_t& position, const char* message) {
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
});
return tools.Validate(spirv);
}
Uint32 CountOpcode(const Vector<Uint32>& spirv, spv::Op wanted) {
Uint32 count = 0u;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
if (opcode == wanted) ++count;
});
return count;
}
bool HasWorkgroupBarrierImmediatelyBeforeSecondScan(const Vector<Uint32>& spirv) {
std::map<Uint32, Uint32> uintConstants;
spv::Op previous = spv::Op::OpNop;
Uint32 scanCount = 0u;
bool found = false;
const Uint32* previousWords = nullptr;
Uint32 previousWordCount = 0u;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == spv::Op::OpConstant && wordCount >= 4u) {
uintConstants[words[2]] = words[3];
}
if (opcode == spv::Op::OpGroupNonUniformFAdd && wordCount >= 6u &&
static_cast<spv::GroupOperation>(words[4]) == spv::GroupOperation::InclusiveScan && ++scanCount == 2u &&
previous == spv::Op::OpControlBarrier && previousWordCount == 4u) {
found =
uintConstants[previousWords[1]] == static_cast<Uint32>(spv::Scope::Workgroup) &&
uintConstants[previousWords[2]] == static_cast<Uint32>(spv::Scope::Workgroup) &&
uintConstants[previousWords[3]] == (static_cast<Uint32>(spv::MemorySemanticsMask::AcquireRelease) |
static_cast<Uint32>(spv::MemorySemanticsMask::WorkgroupMemory));
}
previous = opcode;
previousWords = words;
previousWordCount = wordCount;
});
return found;
}
constexpr const char* kProgram203RaceShape = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { vec2 value; } outputData;
shared vec2 prefixSumCache[32];
void main() {
vec2 sampleLuminance = subgroupInclusiveAdd(
vec2(float(gl_LocalInvocationIndex), 1.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
barrier();
if (gl_LocalInvocationIndex == 511u)
prefixSumCache[0] = sampleLuminance / 512.0;
barrier();
float avg = prefixSumCache[0].x;
float weight = avg > 0.0 ? float(gl_LocalInvocationIndex + 1u) / avg : 0.0;
vec2 sampleExposure = subgroupInclusiveAdd(vec2(weight, 1.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleExposure;
barrier();
if (gl_LocalInvocationIndex == 511u)
outputData.value = sampleExposure;
}
)";
constexpr const char* kAlreadySynchronizedShape = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { vec2 value; } outputData;
shared vec2 prefixSumCache[32];
void main() {
vec2 first = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 1.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = first;
barrier();
if (gl_LocalInvocationIndex == 511u) prefixSumCache[0] = first / 512.0;
barrier();
float avg = prefixSumCache[0].x;
barrier();
vec2 second = subgroupInclusiveAdd(vec2(avg, 1.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = second;
barrier();
if (gl_LocalInvocationIndex == 511u) outputData.value = second;
}
)";
constexpr const char* kForeignSingleScanShape = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { vec2 value; } outputData;
shared vec2 prefixSumCache[32];
void main() {
vec2 value = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 1.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = value;
barrier();
if (gl_LocalInvocationIndex == 0u) outputData.value = prefixSumCache[0];
}
)";
} // namespace
TEST(FixIterationRPBarrierPass, InsertsWorkgroupBarrierBeforeSecondReduction) {
const Vector<Uint32> input = CompileCompute(kProgram203RaceShape);
ASSERT_FALSE(input.empty());
const Uint32 inputBarrierCount = CountOpcode(input, spv::Op::OpControlBarrier);
EXPECT_FALSE(HasWorkgroupBarrierImmediatelyBeforeSecondScan(input));
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, output, true));
EXPECT_EQ(CountOpcode(output, spv::Op::OpControlBarrier), inputBarrierCount + 1u);
EXPECT_TRUE(HasWorkgroupBarrierImmediatelyBeforeSecondScan(output));
EXPECT_TRUE(Validates(output));
}
TEST(FixIterationRPBarrierPass, LeavesOtherShapesByteIdentical) {
const Vector<Uint32> input = CompileCompute(kForeignSingleScanShape);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, output, true));
EXPECT_EQ(output, input);
}
TEST(FixIterationRPBarrierPass, LeavesAnAlreadySynchronizedShaderByteIdentical) {
const Vector<Uint32> input = CompileCompute(kAlreadySynchronizedShape);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, output, true));
EXPECT_EQ(output, input);
}
TEST(FixIterationRPBarrierPass, IsIdempotent) {
const Vector<Uint32> input = CompileCompute(kProgram203RaceShape);
ASSERT_FALSE(input.empty());
Vector<Uint32> once;
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, once, true));
Vector<Uint32> twice;
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(once, twice, true));
EXPECT_EQ(twice, once);
}
@@ -109,11 +109,10 @@ namespace {
return tools.Validate(spirv);
}
// iterationRP's exposure reduction, as the pack ships it: 32x16 (512
// invocations), subgroupInclusiveAdd on a vec2, and a 32-entry
// gl_SubgroupID-indexed scratch. A second, plainly indexed array rides along
// to prove the patch is surgical.
constexpr const char* kExposureShapedSource = R"(#version 450 core
// iterationRP's reduction fingerprint: 32x16x1, subgroupInclusiveAdd on a
// vec2, and the pack's own 32-entry gl_SubgroupID-indexed scratch. A second,
// plainly indexed array rides along to prove the patch is surgical.
constexpr const char* kIterationRPShapedSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
@@ -142,195 +141,85 @@ void main() {
}
)";
// The pack's OTHER instance of the same bug, which a fingerprint pinned to the
// exposure pass's dimensions walks straight past: the RTW importance warp
// scans a plain float across 1024 invocations into a 64-entry scratch.
constexpr const char* kRtwWarpShapedSource = R"(#version 450 core
// Same scratch idiom, different workgroup shape - NOT iterationRP, so the
// fingerprint must refuse it even though it would break identically.
constexpr const char* kWrongWorkgroupShapeSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 1024) in;
layout(local_size_x = 64, local_size_y = 8, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { float value; } outputData;
shared float prefixSumCache[64];
shared vec2 prefixSumCache[32];
void main() {
float importance = float(gl_LocalInvocationID.x) * 0.5;
float prefixSum = subgroupInclusiveAdd(importance);
vec2 v = subgroupInclusiveAdd(vec2(1.0, 0.0));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = prefixSum;
prefixSumCache[gl_SubgroupID] = v;
barrier();
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) {
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
prefixSum += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = prefixSum;
}
barrier();
}
if (gl_LocalInvocationID.x == 1023u) outputData.value = prefixSumCache[0];
if (gl_LocalInvocationIndex == 0u)
outputData.value = prefixSumCache[0].x;
}
)";
// A subgroup scan, but the scratch is indexed per invocation rather than per
// subgroup: its size is not a subgroup-count assumption, so it is not ours.
constexpr const char* kInvocationIndexedSource = R"(#version 450 core
// Right shape, but a float scan and a float[32] scratch - not the pack's
// vec2 accumulator signature.
constexpr const char* kWrongElementTypeSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { float value; } outputData;
shared vec2 perInvocation[32];
shared float cache[32];
void main() {
vec2 v = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 0.0));
perInvocation[gl_LocalInvocationIndex & 31u] = v;
float v = subgroupInclusiveAdd(float(gl_LocalInvocationIndex));
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
cache[gl_SubgroupID] = v;
barrier();
if (gl_LocalInvocationIndex == 0u) outputData.value = perInvocation[0].x;
}
)";
// gl_SubgroupID-indexed, but no subgroup scan feeds it and the element type is
// not the pack's float accumulator.
constexpr const char* kNonFloatScratchSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { uint value; } outputData;
shared uint tally[32];
void main() {
float scan = subgroupInclusiveAdd(float(gl_LocalInvocationIndex));
tally[gl_SubgroupID] = uint(scan);
barrier();
if (gl_LocalInvocationIndex == 0u) outputData.value = tally[0];
}
)";
// gl_SubgroupID-indexed, but masked into range: the declaration is bounded by
// construction, not a subgroup-count assumption, so it is not the pack's bug.
constexpr const char* kMaskedSubgroupIndexSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { float value; } outputData;
shared vec2 bounded[8];
void main() {
vec2 v = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 0.0));
bounded[gl_SubgroupID & 7u] = v;
barrier();
if (gl_LocalInvocationIndex == 0u) outputData.value = bounded[0].x;
}
)";
// Neither of the pack's shapes: a small per-subgroup array in a 256-invocation
// workgroup, used to prove the width gate keeps EVERY module inert at >= 16 lanes.
constexpr const char* kForeignShapeSource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 256) in;
layout(std430, binding = 0) buffer Output { float value; } outputData;
shared float partial[4];
void main() {
float v = subgroupInclusiveAdd(float(gl_LocalInvocationID.x));
if (gl_SubgroupID < 4u) partial[gl_SubgroupID] = v;
barrier();
if (gl_LocalInvocationID.x == 0u) outputData.value = partial[0];
}
)";
// No subgroup construct at all.
constexpr const char* kSubgroupFreeSource = R"(#version 450 core
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { float value; } outputData;
shared vec2 scratch[32];
void main() {
scratch[gl_LocalInvocationIndex & 31u] = vec2(float(gl_LocalInvocationIndex), 0.0);
barrier();
if (gl_LocalInvocationIndex == 0u) outputData.value = scratch[0].x;
if (gl_LocalInvocationIndex == 0u)
outputData.value = cache[0];
}
)";
} // namespace
TEST(FixIterationRPSubgroupScratchPass, GrowsTheExposureScratchForNarrowSubgroups) {
const Vector<Uint32> input = CompileCompute(kExposureShapedSource);
TEST(FixIterationRPSubgroupScratchPass, GrowsThePacksScratchForNarrowSubgroups) {
const Vector<Uint32> input = CompileCompute(kIterationRPShapedSource);
ASSERT_FALSE(input.empty());
ASSERT_EQ(WorkgroupArrayLengths(input), (std::vector<Uint32>{4u, 32u}));
// lavapipe: 8-lane subgroups over 512 invocations need 64 entries; the
// plainly indexed neighbour must keep its 4.
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, 32768u, true));
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, true));
EXPECT_EQ(WorkgroupArrayLengths(output), (std::vector<Uint32>{4u, 64u}));
EXPECT_TRUE(Validates(output));
}
// The regression the CI retrace caught: patching only the exposure pass leaves
// this one writing 128 subgroups into 64 entries, and the frame stays wrong.
TEST(FixIterationRPSubgroupScratchPass, GrowsTheRtwWarpScratchForNarrowSubgroups) {
const Vector<Uint32> input = CompileCompute(kRtwWarpShapedSource);
ASSERT_FALSE(input.empty());
ASSERT_EQ(WorkgroupArrayLengths(input), (std::vector<Uint32>{64u}));
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, 32768u, true));
EXPECT_EQ(WorkgroupArrayLengths(output), (std::vector<Uint32>{128u}));
EXPECT_TRUE(Validates(output));
}
TEST(FixIterationRPSubgroupScratchPass, LeavesPackWidthAssumptionsAloneOnWideDevices) {
// Both shapes are sized for >= 16 lanes (512/16 = 32, 1024/16 = 64), so on
// every such device the modules must pass through byte-identical.
for (const char* source : {kExposureShapedSource, kRtwWarpShapedSource}) {
const Vector<Uint32> input = CompileCompute(source);
ASSERT_FALSE(input.empty());
for (const Uint32 nativeSize : {16u, 32u, 64u, 128u}) {
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
input, output, nativeSize, 32768u, true));
EXPECT_EQ(output, input) << "native width " << nativeSize;
}
}
}
TEST(FixIterationRPSubgroupScratchPass, RefusesAModuleOutsideTheIdiom) {
for (const char* source : {kInvocationIndexedSource, kNonFloatScratchSource,
kSubgroupFreeSource, kMaskedSubgroupIndexSource}) {
const Vector<Uint32> input = CompileCompute(source);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, 32768u, true));
EXPECT_EQ(output, input);
}
}
// A grown array that would not fit the device's shared memory is left alone:
// a pipeline that cannot be created is worse than the pack's own overrun.
// The width gate is what keeps unrelated shaders untouched on the devices the pack
// was written for: at >= 16 lanes nothing is rewritten, whatever its shape.
TEST(FixIterationRPSubgroupScratchPass, LeavesEveryModuleAloneAtThePacksAssumedWidth) {
const Vector<Uint32> input = CompileCompute(kForeignShapeSource);
const Vector<Uint32> input = CompileCompute(kIterationRPShapedSource);
ASSERT_FALSE(input.empty());
for (const Uint32 nativeSize : {16u, 32u, 64u}) {
// >= 16 lanes means at most 32 subgroups: the pack's declared size holds and
// the module must pass through byte-identical.
for (const Uint32 nativeSize : {16u, 32u, 64u, 128u}) {
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
input, output, nativeSize, 32768u, true));
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, nativeSize, true));
EXPECT_EQ(output, input) << "native width " << nativeSize;
}
}
TEST(FixIterationRPSubgroupScratchPass, RefusesGrowthThatWouldNotFitSharedMemory) {
const Vector<Uint32> input = CompileCompute(kRtwWarpShapedSource);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, 256u, true));
EXPECT_EQ(output, input);
TEST(FixIterationRPSubgroupScratchPass, RefusesAModuleOutsideTheFingerprint) {
for (const char* source : {kWrongWorkgroupShapeSource, kWrongElementTypeSource}) {
const Vector<Uint32> input = CompileCompute(source);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, true));
EXPECT_EQ(output, input);
}
}
TEST(FixIterationRPSubgroupScratchPass, IsIdempotent) {
for (const char* source : {kExposureShapedSource, kRtwWarpShapedSource}) {
const Vector<Uint32> input = CompileCompute(source);
ASSERT_FALSE(input.empty());
Vector<Uint32> once;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, once, 8u, 32768u, true));
Vector<Uint32> twice;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(once, twice, 8u, 32768u, true));
EXPECT_EQ(twice, once);
}
const Vector<Uint32> input = CompileCompute(kIterationRPShapedSource);
ASSERT_FALSE(input.empty());
Vector<Uint32> once;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, once, 8u, true));
Vector<Uint32> twice;
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(once, twice, 8u, true));
EXPECT_EQ(twice, once);
}
@@ -27,7 +27,6 @@
#include "SpirvPasses/ZeroBaseVertexPass.h"
#include "SpirvPasses/DeriveNumSubgroupsPass.h"
#include "SpirvPasses/EmulateSubgroupsPass.h"
#include "SpirvPasses/FixIterationRPBarrierPass.h"
#include "SpirvPasses/FixIterationRPSubgroupScratchPass.h"
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
#include "SpirvPasses/Lower1DArrayImagesPass.h"
@@ -913,29 +912,17 @@ namespace MobileGL {
bool ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
const Uint32 nativeSubgroupSize, const Uint32 maxWorkgroupScratchBytes,
const bool enableSpirvValidation) {
const Uint32 nativeSubgroupSize, const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(
FixIterationRPSubgroupScratchPass::CreateFixIterationRPSubgroupScratchPass(
nativeSubgroupSize, maxWorkgroupScratchBytes));
nativeSubgroupSize));
return RunOptimizerChecked("FixIterationRPSubgroupScratchForVulkan", optimizer,
inputBinary, outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::FixIterationRPBarrierForVulkan(
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(FixIterationRPBarrierPass::CreateFixIterationRPBarrierPass());
return RunOptimizerChecked("FixIterationRPBarrierForVulkan", optimizer,
inputBinary, outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
using namespace spvtools;
@@ -167,24 +167,13 @@ namespace MobileGL {
Vector<uint32_t>& outputBinary,
Uint32 maxWorkgroupScratchBytes,
bool enableSpirvValidation = false);
// Grows iterationRP's under-declared gl_SubgroupID-indexed scratch to the
// subgroup count the device actually partitions into, fingerprint-gated to
// that pack's reduction idiom; every other module - and every device whose
// width the pack already assumed - passes through byte-identical.
// maxWorkgroupScratchBytes bounds the growth (pass the device's
// maxComputeSharedMemorySize; 0 falls back to the 16384-byte Vulkan
// minimum). See FixIterationRPSubgroupScratchPass.
// Patches iterationRP's under-declared prefixSumCache[32] on sub-16-lane
// devices, fingerprint-gated to that pack's reduction; every other module
// passes through byte-identical. See FixIterationRPSubgroupScratchPass.
static bool FixIterationRPSubgroupScratchForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
Uint32 nativeSubgroupSize,
Uint32 maxWorkgroupScratchBytes,
bool enableSpirvValidation = false);
// Inserts the missing workgroup rendezvous between Program 203's two
// prefixSumCache reductions. Fingerprint-gated to the iterationRP shape;
// unrelated and already-repaired modules pass through byte-identical.
static bool FixIterationRPBarrierForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Re-declares 64-bit float vertex inputs as their 32-bit unsigned word pair
// (double -> uvec2, dvec2 -> uvec4) and bitcasts them back to double at entry, so no
// VK_FORMAT_R64*_SFLOAT is needed - lavapipe advertises none of them for vertex
@@ -1,232 +0,0 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "FixIterationRPBarrierPass.h"
#include "spirv.hpp"
#include "source/opt/constants.h"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.h"
#include "source/opt/ir_context.h"
#include "source/opt/module.h"
#include "source/util/make_unique.h"
#include <vector>
namespace MobileGL::MG_Util::ShaderTranspiler {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
using spvtools::opt::Operand;
const Instruction* RootVariable(IRContext* context, uint32_t pointerId) {
const Instruction* def = context->get_def_use_mgr()->GetDef(pointerId);
while (def != nullptr) {
switch (def->opcode()) {
case spv::Op::OpVariable:
return def;
case spv::Op::OpAccessChain:
case spv::Op::OpInBoundsAccessChain:
case spv::Op::OpCopyObject:
def = context->get_def_use_mgr()->GetDef(def->GetSingleWordInOperand(0));
break;
default:
return nullptr;
}
}
return nullptr;
}
bool IsUintConstant(IRContext* context, uint32_t id, uint32_t wanted) {
const Instruction* def = context->get_def_use_mgr()->GetDef(id);
return def != nullptr && def->opcode() == spv::Op::OpConstant && def->NumInOperands() == 1u &&
def->GetSingleWordInOperand(0) == wanted;
}
bool IsZeroElementPointer(IRContext* context, uint32_t pointerId, const Instruction** root) {
const Instruction* pointer = context->get_def_use_mgr()->GetDef(pointerId);
if (pointer == nullptr ||
(pointer->opcode() != spv::Op::OpAccessChain && pointer->opcode() != spv::Op::OpInBoundsAccessChain) ||
pointer->NumInOperands() < 2u) {
return false;
}
for (uint32_t i = 1u; i < pointer->NumInOperands(); ++i) {
if (!IsUintConstant(context, pointer->GetSingleWordInOperand(i), 0u)) return false;
}
*root = RootVariable(context, pointerId);
return *root != nullptr;
}
bool IsWorkgroupVec2Array(IRContext* context, const Instruction* variable) {
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable || variable->NumInOperands() < 1u ||
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) != spv::StorageClass::Workgroup) {
return false;
}
auto* defUseMgr = context->get_def_use_mgr();
const Instruction* pointerType = defUseMgr->GetDef(variable->type_id());
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer ||
pointerType->NumInOperands() < 2u) {
return false;
}
const Instruction* arrayType = defUseMgr->GetDef(pointerType->GetSingleWordInOperand(1));
if (arrayType == nullptr || arrayType->opcode() != spv::Op::OpTypeArray ||
arrayType->NumInOperands() < 2u) {
return false;
}
const Instruction* length = defUseMgr->GetDef(arrayType->GetSingleWordInOperand(1));
if (length == nullptr || length->opcode() != spv::Op::OpConstant || length->NumInOperands() != 1u) {
return false;
}
const uint32_t arrayLength = length->GetSingleWordInOperand(0);
if (arrayLength < 32u || arrayLength > 512u) return false;
const Instruction* vectorType = defUseMgr->GetDef(arrayType->GetSingleWordInOperand(0));
if (vectorType == nullptr || vectorType->opcode() != spv::Op::OpTypeVector ||
vectorType->NumInOperands() < 2u || vectorType->GetSingleWordInOperand(1) != 2u) {
return false;
}
const Instruction* scalarType = defUseMgr->GetDef(vectorType->GetSingleWordInOperand(0));
return scalarType != nullptr && scalarType->opcode() == spv::Op::OpTypeFloat &&
scalarType->NumInOperands() == 1u && scalarType->GetSingleWordInOperand(0) == 32u;
}
bool IsVec2FloatInclusiveAdd(IRContext* context, const Instruction* inst) {
if (inst->opcode() != spv::Op::OpGroupNonUniformFAdd || inst->NumInOperands() < 3u ||
static_cast<spv::GroupOperation>(inst->GetSingleWordInOperand(1)) !=
spv::GroupOperation::InclusiveScan) {
return false;
}
const Instruction* vectorType = context->get_def_use_mgr()->GetDef(inst->type_id());
if (vectorType == nullptr || vectorType->opcode() != spv::Op::OpTypeVector ||
vectorType->NumInOperands() < 2u || vectorType->GetSingleWordInOperand(1) != 2u) {
return false;
}
const Instruction* scalarType = context->get_def_use_mgr()->GetDef(vectorType->GetSingleWordInOperand(0));
return scalarType != nullptr && scalarType->opcode() == spv::Op::OpTypeFloat &&
scalarType->NumInOperands() == 1u && scalarType->GetSingleWordInOperand(0) == 32u;
}
bool HasProgram203LocalSize(IRContext* context) {
for (const Instruction& entryPoint : context->module()->entry_points()) {
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) !=
spv::ExecutionModel::GLCompute) {
return false;
}
}
for (const Instruction& mode : context->module()->execution_modes()) {
if (mode.opcode() == spv::Op::OpExecutionMode && mode.NumInOperands() >= 5u &&
static_cast<spv::ExecutionMode>(mode.GetSingleWordInOperand(1)) == spv::ExecutionMode::LocalSize) {
return mode.GetSingleWordInOperand(2) == 32u && mode.GetSingleWordInOperand(3) == 16u &&
mode.GetSingleWordInOperand(4) == 1u;
}
}
return false;
}
bool IsStoreToRoot(IRContext* context, const Instruction* inst, const Instruction* root) {
return inst->opcode() == spv::Op::OpStore && inst->NumInOperands() >= 2u &&
RootVariable(context, inst->GetSingleWordInOperand(0)) == root;
}
} // namespace
spvtools::opt::Pass::Status FixIterationRPBarrierPass::Process() {
auto* irContext = context();
if (!HasProgram203LocalSize(irContext)) return Status::SuccessWithoutChange;
for (auto& function : *irContext->module()) {
std::vector<Instruction*> instructions;
std::vector<size_t> scans;
for (auto& block : function) {
for (auto& inst : block) {
if (IsVec2FloatInclusiveAdd(irContext, &inst)) scans.push_back(instructions.size());
instructions.push_back(&inst);
}
}
// Program 203 has exactly two vec2 inclusive adds: the luminance reduction
// and the weighted-exposure reduction. More or fewer is not our fingerprint.
if (scans.size() != 2u) continue;
const size_t firstScan = scans[0];
const size_t secondScan = scans[1];
const Instruction* scratch = nullptr;
size_t averageLoad = instructions.size();
for (size_t i = firstScan + 1u; i < secondScan; ++i) {
Instruction* inst = instructions[i];
if (inst->opcode() != spv::Op::OpLoad || inst->NumInOperands() < 1u) continue;
const Instruction* root = nullptr;
if (!IsZeroElementPointer(irContext, inst->GetSingleWordInOperand(0), &root) ||
!IsWorkgroupVec2Array(irContext, root)) {
continue;
}
// The broadcast is read as prefixSumCache[0].x, hence a scalar load.
const Instruction* type = irContext->get_def_use_mgr()->GetDef(inst->type_id());
if (type == nullptr || type->opcode() != spv::Op::OpTypeFloat || type->NumInOperands() != 1u ||
type->GetSingleWordInOperand(0) != 32u) {
continue;
}
scratch = root;
averageLoad = i;
break;
}
if (scratch == nullptr) continue;
bool sawZeroBroadcastStore = false;
bool sawPublishBarrier = false;
for (size_t i = firstScan + 1u; i < averageLoad; ++i) {
const Instruction* root = nullptr;
if (instructions[i]->opcode() == spv::Op::OpStore &&
IsZeroElementPointer(irContext, instructions[i]->GetSingleWordInOperand(0), &root) &&
root == scratch) {
sawZeroBroadcastStore = true;
} else if (sawZeroBroadcastStore && instructions[i]->opcode() == spv::Op::OpControlBarrier) {
sawPublishBarrier = true;
}
}
if (!sawZeroBroadcastStore || !sawPublishBarrier) continue;
bool alreadySynchronized = false;
for (size_t i = averageLoad + 1u; i < secondScan; ++i) {
if (instructions[i]->opcode() == spv::Op::OpControlBarrier) {
alreadySynchronized = true;
break;
}
}
if (alreadySynchronized) return Status::SuccessWithoutChange;
bool secondPhaseReusesScratch = false;
for (size_t i = secondScan + 1u; i < instructions.size(); ++i) {
if (IsStoreToRoot(irContext, instructions[i], scratch)) {
secondPhaseReusesScratch = true;
break;
}
}
if (!secondPhaseReusesScratch) continue;
auto* constantMgr = irContext->get_constant_mgr();
const uint32_t scopeId = constantMgr->GetUIntConstId(static_cast<uint32_t>(spv::Scope::Workgroup));
const uint32_t semanticsId =
constantMgr->GetUIntConstId(static_cast<uint32_t>(spv::MemorySemanticsMask::AcquireRelease) |
static_cast<uint32_t>(spv::MemorySemanticsMask::WorkgroupMemory));
if (scopeId == 0u || semanticsId == 0u) return Status::Failure;
instructions[secondScan]->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpControlBarrier, 0u, 0u,
Instruction::OperandList{Operand{SPV_OPERAND_TYPE_ID, {scopeId}},
Operand{SPV_OPERAND_TYPE_ID, {scopeId}},
Operand{SPV_OPERAND_TYPE_ID, {semanticsId}}}));
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
return Status::SuccessWithoutChange;
}
spvtools::Optimizer::PassToken FixIterationRPBarrierPass::CreateFixIterationRPBarrierPass() {
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<FixIterationRPBarrierPass>());
}
} // namespace MobileGL::MG_Util::ShaderTranspiler
@@ -1,28 +0,0 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.h
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
namespace MobileGL::MG_Util::ShaderTranspiler {
// Repairs iterationRP Program 203's missing workgroup rendezvous between two
// reductions that reuse prefixSumCache. The first phase broadcasts its result
// through prefixSumCache[0], but the second phase may overwrite that element before
// every invocation has read it. The pass fingerprints that exact two-scan,
// 512-invocation shape and inserts one Workgroup control barrier immediately before
// the second scan. Unrelated modules and already-repaired modules are byte-identical.
class FixIterationRPBarrierPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "fix-iterationrp-barrier"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateFixIterationRPBarrierPass();
};
} // namespace MobileGL::MG_Util::ShaderTranspiler
@@ -26,13 +26,13 @@ namespace MobileGL {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
// The Vulkan minimum for maxComputeSharedMemorySize, used when the caller
// could not tell us the device's real limit.
constexpr uint32_t kMinimumSharedMemoryBytes = 16384u;
// The narrowest subgroup width iterationRP's declarations are sized for.
// At or above it both shipped shapes fit and nothing may be rewritten.
constexpr uint32_t kPackAssumedSubgroupWidth = 16u;
// iterationRP's reduction fingerprint, spelled out.
constexpr uint32_t kIterationRPLocalSizeX = 32u;
constexpr uint32_t kIterationRPLocalSizeY = 16u;
constexpr uint32_t kIterationRPLocalSizeZ = 1u;
constexpr uint32_t kIterationRPInvocations =
kIterationRPLocalSizeX * kIterationRPLocalSizeY * kIterationRPLocalSizeZ;
constexpr uint32_t kIterationRPScratchLength = 32u;
Instruction* FindBuiltinDefinition(IRContext* context, spv::BuiltIn builtin) {
auto* defUseMgr = context->get_def_use_mgr();
@@ -73,121 +73,18 @@ namespace MobileGL {
return nullptr;
}
// A 32-bit float scalar or vector - the shape of every accumulator the
// pack runs through its scans (float, vec2 and vec4 all appear). Returns
// the component count, or 0 for anything else.
uint32_t Float32ComponentCount(IRContext* context, uint32_t typeId) {
auto* defUseMgr = context->get_def_use_mgr();
const Instruction* type = defUseMgr->GetDef(typeId);
if (type == nullptr) return 0u;
uint32_t components = 1u;
if (type->opcode() == spv::Op::OpTypeVector) {
components = type->GetSingleWordInOperand(1);
if (components < 2u || components > 4u) return 0u;
type = defUseMgr->GetDef(type->GetSingleWordInOperand(0));
if (type == nullptr) return 0u;
}
if (type->opcode() != spv::Op::OpTypeFloat ||
type->GetSingleWordInOperand(0) != 32u) {
return 0u;
}
return components;
}
uint32_t RoundUp(uint32_t value, uint32_t alignment) {
return alignment == 0u ? value : ((value + alignment - 1u) / alignment) * alignment;
}
// Size AND alignment of a workgroup-storage type. Drivers lay shared
// memory out at natural alignment and the limit
// (VUID-RuntimeSpirv-Workgroup-06530) counts the padding that produces,
// so a model that sums unpadded sizes would under-count exactly where the
// budget check matters. Returns false for anything not modelled here,
// which the caller answers by declining to grow at all rather than by
// certifying growth against a total it knows is an underestimate.
bool WorkgroupTypeLayout(IRContext* context, uint32_t typeId, uint32_t* size,
uint32_t* alignment, uint32_t depth = 0u) {
if (depth > 8u) return false;
auto* defUseMgr = context->get_def_use_mgr();
const Instruction* type = defUseMgr->GetDef(typeId);
if (type == nullptr) return false;
switch (type->opcode()) {
case spv::Op::OpTypeBool:
*size = 4u;
*alignment = 4u;
return true;
case spv::Op::OpTypeInt:
case spv::Op::OpTypeFloat: {
const uint32_t width = type->GetSingleWordInOperand(0) / 8u;
if (width == 0u) return false;
*size = width;
*alignment = width;
return true;
}
case spv::Op::OpTypeVector: {
uint32_t componentSize = 0u;
uint32_t componentAlignment = 0u;
if (!WorkgroupTypeLayout(context, type->GetSingleWordInOperand(0),
&componentSize, &componentAlignment, depth + 1u)) {
return false;
}
const uint32_t components = type->GetSingleWordInOperand(1);
if (components < 2u || components > 4u) return false;
*size = componentSize * components;
// A three-component vector aligns like a four-component one.
*alignment = componentSize * (components == 3u ? 4u : components);
return true;
}
case spv::Op::OpTypeMatrix:
case spv::Op::OpTypeArray: {
uint32_t elementSize = 0u;
uint32_t elementAlignment = 0u;
if (!WorkgroupTypeLayout(context, type->GetSingleWordInOperand(0), &elementSize,
&elementAlignment, depth + 1u)) {
return false;
}
uint32_t count = 0u;
if (type->opcode() == spv::Op::OpTypeMatrix) {
count = type->GetSingleWordInOperand(1);
} else {
const Instruction* length =
defUseMgr->GetDef(type->GetSingleWordInOperand(1));
if (length == nullptr || length->opcode() != spv::Op::OpConstant) {
return false; // spec-constant length: not sizeable here
}
count = length->GetSingleWordInOperand(0);
}
*size = RoundUp(elementSize, elementAlignment) * count;
*alignment = elementAlignment;
return true;
}
case spv::Op::OpTypeStruct: {
uint32_t offset = 0u;
uint32_t structAlignment = 1u;
for (uint32_t i = 0; i < type->NumInOperands(); ++i) {
uint32_t memberSize = 0u;
uint32_t memberAlignment = 0u;
if (!WorkgroupTypeLayout(context, type->GetSingleWordInOperand(i),
&memberSize, &memberAlignment, depth + 1u)) {
return false;
}
offset = RoundUp(offset, memberAlignment) + memberSize;
if (memberAlignment > structAlignment) structAlignment = memberAlignment;
}
*size = RoundUp(offset, structAlignment);
*alignment = structAlignment;
return true;
}
default:
// vec2 of 32-bit float - the type of iterationRP's luminance/exposure
// accumulator and of its prefixSumCache entries.
bool IsVec2Float32(IRContext* context, uint32_t typeId) {
const Instruction* type = context->get_def_use_mgr()->GetDef(typeId);
if (type == nullptr || type->opcode() != spv::Op::OpTypeVector ||
type->GetSingleWordInOperand(1) != 2u) {
return false;
}
}
// The group operations the pack's prefix scans use.
bool IsScanOrReduce(spv::GroupOperation operation) {
return operation == spv::GroupOperation::Reduce ||
operation == spv::GroupOperation::InclusiveScan ||
operation == spv::GroupOperation::ExclusiveScan;
const Instruction* component =
context->get_def_use_mgr()->GetDef(type->GetSingleWordInOperand(0));
return component != nullptr && component->opcode() == spv::Op::OpTypeFloat &&
component->GetSingleWordInOperand(0) == 32u;
}
} // namespace
@@ -195,15 +92,13 @@ namespace MobileGL {
auto* irContext = context();
auto* defUseMgr = irContext->get_def_use_mgr();
// Without a known device width there is no topology to compare against;
// and a width the pack already assumed needs no patch at all. Both of
// iterationRP's shapes are sized for >= 16 lanes (512/16 = 32 entries,
// 1024/16 = 64), so every module on such a device - the pack's or anyone
// else's - must pass through byte-identical. The per-array length test
// further down is the second gate, not a replacement for this one.
if (m_nativeSubgroupSize == 0u || m_nativeSubgroupSize >= kPackAssumedSubgroupWidth) {
// A device whose native width already satisfies the pack's assumption
// (>= 16 lanes -> at most 32 subgroups) needs no patch at all.
if (m_nativeSubgroupSize == 0u || m_nativeSubgroupSize >= 16u) {
return Status::SuccessWithoutChange;
}
const uint32_t requiredLength =
(kIterationRPInvocations + m_nativeSubgroupSize - 1u) / m_nativeSubgroupSize;
for (const Instruction& entryPoint : irContext->module()->entry_points()) {
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) !=
@@ -212,8 +107,7 @@ namespace MobileGL {
}
}
// Fingerprint 1: a literal workgroup size, so the subgroup count the
// dispatch actually partitions into is known here.
// Fingerprint 1: the pack's exposure-pass workgroup shape, 32x16x1.
const auto resolveUintConstant = [&](uint32_t id, uint32_t* value) {
const Instruction* def = defUseMgr->GetDef(id);
if (def == nullptr || def->opcode() != spv::Op::OpConstant) return false;
@@ -245,40 +139,26 @@ namespace MobileGL {
}
}
}
if (!haveLocalSize || localSize[0] == 0u || localSize[1] == 0u || localSize[2] == 0u) {
if (!haveLocalSize || localSize[0] != kIterationRPLocalSizeX ||
localSize[1] != kIterationRPLocalSizeY || localSize[2] != kIterationRPLocalSizeZ) {
return Status::SuccessWithoutChange;
}
const uint64_t totalInvocations =
static_cast<uint64_t>(localSize[0]) * localSize[1] * localSize[2];
if (totalInvocations == 0u || totalInvocations > (1u << 20)) {
return Status::SuccessWithoutChange;
}
const uint32_t requiredLength = static_cast<uint32_t>(
(totalInvocations + m_nativeSubgroupSize - 1u) / m_nativeSubgroupSize);
// Fingerprint 2: a subgroup scan over a 32-bit float value - the pack's
// prefix-sum reduction, and the reason its scratch is indexed per subgroup.
bool sawFloatSubgroupScan = false;
// Fingerprint 2: the reduction's subgroupInclusiveAdd on a vec2.
bool sawVec2InclusiveAdd = false;
for (auto& function : *irContext->module()) {
for (auto& block : function) {
for (auto& inst : block) {
if (inst.opcode() != spv::Op::OpGroupNonUniformFAdd &&
inst.opcode() != spv::Op::OpGroupNonUniformFMin &&
inst.opcode() != spv::Op::OpGroupNonUniformFMax) {
continue;
}
if (inst.NumInOperands() < 2) continue;
if (!IsScanOrReduce(static_cast<spv::GroupOperation>(
inst.GetSingleWordInOperand(1)))) {
continue;
}
if (Float32ComponentCount(irContext, inst.type_id()) != 0u) {
sawFloatSubgroupScan = true;
if (inst.opcode() == spv::Op::OpGroupNonUniformFAdd &&
static_cast<spv::GroupOperation>(inst.GetSingleWordInOperand(1)) ==
spv::GroupOperation::InclusiveScan &&
IsVec2Float32(irContext, inst.type_id())) {
sawVec2InclusiveAdd = true;
}
}
}
}
if (!sawFloatSubgroupScan) {
if (!sawVec2InclusiveAdd) {
return Status::SuccessWithoutChange;
}
@@ -291,89 +171,60 @@ namespace MobileGL {
}
const uint32_t subgroupIdVariableId = subgroupIdVariable->result_id();
// The pack indexes its scratch with gl_SubgroupID ITSELF, so only values
// that ARE that id qualify - not everything computed from it. An index
// that is masked or clamped (cache[gl_SubgroupID & 3u]) is bounded by
// construction and is none of this pass's business; accepting it would
// turn a targeted repair into a general array resizer. Identity survives
// OpCopyObject, a signedness OpBitcast, and the Function/Private spill
// glslang emits for a builtin load - and nothing else. A spill variable
// counts only when EVERY store into it is the id.
std::unordered_map<uint32_t, bool> subgroupIdValues; // result id IS the id
std::unordered_map<uint32_t, bool> subgroupIdVariables; // spill holding only it
bool changedIdentity = true;
while (changedIdentity) {
changedIdentity = false;
std::unordered_map<uint32_t, uint32_t> totalStores;
std::unordered_map<uint32_t, uint32_t> idStores;
// Conservative taint walk over values, and through Function/Private
// temporaries by variable (glslang routinely spills builtin loads into
// locals before they reach an index expression). Over-tainting is safe:
// the candidate filter below still demands the exact vec2[32] shape.
std::unordered_map<uint32_t, bool> valueTainted; // result id -> tainted
std::unordered_map<uint32_t, bool> variableTainted; // variable id -> tainted
bool changedTaint = true;
while (changedTaint) {
changedTaint = false;
for (auto& function : *irContext->module()) {
for (auto& block : function) {
for (auto& inst : block) {
if (inst.opcode() != spv::Op::OpStore) continue;
const uint32_t pointerId = inst.GetSingleWordInOperand(0);
const Instruction* target = defUseMgr->GetDef(pointerId);
if (target == nullptr || target->opcode() != spv::Op::OpVariable) {
const spv::Op opcode = inst.opcode();
if (opcode == spv::Op::OpStore) {
if (!valueTainted.count(inst.GetSingleWordInOperand(1))) continue;
const Instruction* root =
RootVariable(irContext, inst.GetSingleWordInOperand(0));
if (root == nullptr) continue;
if (!variableTainted.count(root->result_id())) {
variableTainted[root->result_id()] = true;
changedTaint = true;
}
continue;
}
const auto storageClass = static_cast<spv::StorageClass>(
target->GetSingleWordInOperand(0));
if (storageClass != spv::StorageClass::Function &&
storageClass != spv::StorageClass::Private) {
if (inst.result_id() == 0 || valueTainted.count(inst.result_id())) {
continue;
}
totalStores[pointerId] += 1u;
if (subgroupIdValues.count(inst.GetSingleWordInOperand(1))) {
idStores[pointerId] += 1u;
}
}
}
}
for (const auto& entry : totalStores) {
if (entry.second != 0u && idStores[entry.first] == entry.second &&
!subgroupIdVariables.count(entry.first)) {
subgroupIdVariables[entry.first] = true;
changedIdentity = true;
}
}
for (auto& function : *irContext->module()) {
for (auto& block : function) {
for (auto& inst : block) {
if (inst.result_id() == 0 ||
subgroupIdValues.count(inst.result_id())) {
continue;
}
bool isSubgroupId = false;
switch (inst.opcode()) {
case spv::Op::OpLoad: {
bool tainted = false;
if (opcode == spv::Op::OpLoad) {
const uint32_t pointerId = inst.GetSingleWordInOperand(0);
isSubgroupId = pointerId == subgroupIdVariableId ||
subgroupIdVariables.count(pointerId) != 0u;
break;
if (pointerId == subgroupIdVariableId) tainted = true;
const Instruction* root = RootVariable(irContext, pointerId);
if (root != nullptr && variableTainted.count(root->result_id())) {
tainted = true;
}
} else {
inst.ForEachInId([&](const uint32_t* operandId) {
if (valueTainted.count(*operandId)) tainted = true;
});
}
case spv::Op::OpCopyObject:
case spv::Op::OpBitcast:
isSubgroupId =
subgroupIdValues.count(inst.GetSingleWordInOperand(0)) != 0u;
break;
default:
break;
}
if (isSubgroupId) {
subgroupIdValues[inst.result_id()] = true;
changedIdentity = true;
if (tainted) {
valueTainted[inst.result_id()] = true;
changedTaint = true;
}
}
}
}
}
if (subgroupIdValues.empty()) {
if (valueTainted.empty()) {
return Status::SuccessWithoutChange;
}
// Fingerprint 3: workgroup-shared float arrays indexed by gl_SubgroupID
// itself - the under-declared prefixSumCache.
// Fingerprint 3: workgroup-shared vec2[32] arrays whose access-chain
// index depends on gl_SubgroupID - the under-declared prefixSumCache.
std::map<uint32_t, Instruction*> candidates;
for (auto& function : *irContext->module()) {
for (auto& block : function) {
@@ -383,7 +234,7 @@ namespace MobileGL {
continue;
}
if (inst.NumInOperands() < 2) continue;
if (!subgroupIdValues.count(inst.GetSingleWordInOperand(1))) continue;
if (!valueTainted.count(inst.GetSingleWordInOperand(1))) continue;
Instruction* baseVariable =
defUseMgr->GetDef(inst.GetSingleWordInOperand(0));
if (baseVariable == nullptr ||
@@ -401,16 +252,7 @@ namespace MobileGL {
return Status::SuccessWithoutChange;
}
// Everything that survives the filter, with the bytes each grown array
// will need. Nothing is mutated until the whole set fits the device's
// shared-memory budget, so a module is never left half-grown.
struct Growth {
Instruction* variable = nullptr;
uint32_t elementTypeId = 0;
uint32_t lengthTypeId = 0;
uint32_t addedBytes = 0;
};
std::vector<Growth> growths;
bool changedModule = false;
for (auto& entry : candidates) {
Instruction* variable = entry.second;
@@ -448,70 +290,18 @@ namespace MobileGL {
continue;
}
const uint32_t elementTypeId = arrayType->GetSingleWordInOperand(0);
const uint32_t components = Float32ComponentCount(irContext, elementTypeId);
if (components == 0u) continue;
if (!IsVec2Float32(irContext, elementTypeId)) continue;
const Instruction* lengthConstant =
defUseMgr->GetDef(arrayType->GetSingleWordInOperand(1));
if (lengthConstant == nullptr || lengthConstant->opcode() != spv::Op::OpConstant) {
uint32_t currentLength = 0;
if (lengthConstant == nullptr ||
lengthConstant->opcode() != spv::Op::OpConstant ||
!((currentLength = lengthConstant->GetSingleWordInOperand(0),
currentLength == kIterationRPScratchLength))) {
continue;
}
const uint32_t currentLength = lengthConstant->GetSingleWordInOperand(0);
// The pack's own assumption holds on this device: the declared array
// already covers every subgroup the workgroup partitions into. That is
// every >= 16-lane device for the shapes iterationRP ships, and those
// modules must pass through byte-identical.
if (currentLength >= requiredLength) continue;
// vec3 strides at its 16-byte alignment, so charge the padded stride.
const uint32_t elementStride = (components == 3u ? 4u : components) * 4u;
growths.push_back(Growth{variable, elementTypeId, lengthConstant->type_id(),
(requiredLength - currentLength) * elementStride});
}
if (growths.empty()) {
return Status::SuccessWithoutChange;
}
// Growing must not push the module past what the device can launch: a
// pipeline that fails to create is worse than the pack's own overrun.
{
uint64_t declaredBytes = 0;
bool sawUnsizeable = false;
for (auto& global : irContext->module()->types_values()) {
if (global.opcode() != spv::Op::OpVariable ||
static_cast<spv::StorageClass>(global.GetSingleWordInOperand(0)) !=
spv::StorageClass::Workgroup) {
continue;
}
const Instruction* pointerType = defUseMgr->GetDef(global.type_id());
uint32_t bytes = 0u;
uint32_t alignment = 0u;
if (pointerType == nullptr ||
pointerType->opcode() != spv::Op::OpTypePointer ||
!WorkgroupTypeLayout(irContext, pointerType->GetSingleWordInOperand(1),
&bytes, &alignment)) {
sawUnsizeable = true;
break;
}
declaredBytes = RoundUp(static_cast<uint32_t>(declaredBytes), alignment) + bytes;
}
// A declaration this pass cannot size leaves the total an
// underestimate, so the growth cannot be certified against the device
// limit at all - decline rather than guess.
if (sawUnsizeable) {
return Status::SuccessWithoutChange;
}
for (const Growth& growth : growths) declaredBytes += growth.addedBytes;
const uint32_t deviceBudget = m_maxWorkgroupScratchBytes != 0u
? m_maxWorkgroupScratchBytes
: kMinimumSharedMemoryBytes;
if (declaredBytes > deviceBudget) {
return Status::SuccessWithoutChange;
}
}
for (const Growth& growth : growths) {
// Build the grown array type. All three new instructions are inserted
// immediately BEFORE the variable so definition-before-use holds in the
// module's global section (manager-created instructions append to its
@@ -520,17 +310,17 @@ namespace MobileGL {
// scalar constant is legal SPIR-V); the fresh array type makes the
// pointer type unique by construction, so neither collides with an
// existing declaration.
Instruction* variable = growth.variable;
const uint32_t lengthTypeId = lengthConstant->type_id();
const uint32_t newLengthId = irContext->TakeNextId();
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpConstant, growth.lengthTypeId, newLengthId,
irContext, spv::Op::OpConstant, lengthTypeId, newLengthId,
Instruction::OperandList{{SPV_OPERAND_TYPE_TYPED_LITERAL_NUMBER,
{requiredLength}}}));
const uint32_t newArrayTypeId = irContext->TakeNextId();
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpTypeArray, 0, newArrayTypeId,
Instruction::OperandList{
{SPV_OPERAND_TYPE_ID, {growth.elementTypeId}},
{SPV_OPERAND_TYPE_ID, {elementTypeId}},
{SPV_OPERAND_TYPE_ID, {newLengthId}}}));
const uint32_t newPointerTypeId = irContext->TakeNextId();
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
@@ -541,17 +331,21 @@ namespace MobileGL {
{SPV_OPERAND_TYPE_ID, {newArrayTypeId}}}));
variable->SetResultType(newPointerTypeId);
changedModule = true;
}
if (!changedModule) {
return Status::SuccessWithoutChange;
}
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken
FixIterationRPSubgroupScratchPass::CreateFixIterationRPSubgroupScratchPass(
const Uint32 nativeSubgroupSize, const Uint32 maxWorkgroupScratchBytes) {
return spvtools::Optimizer::PassToken(MakeUnique<FixIterationRPSubgroupScratchPass>(
nativeSubgroupSize, maxWorkgroupScratchBytes));
const Uint32 nativeSubgroupSize) {
return spvtools::Optimizer::PassToken(
MakeUnique<FixIterationRPSubgroupScratchPass>(nativeSubgroupSize));
}
} // namespace ShaderTranspiler
} // namespace MG_Util
@@ -16,57 +16,48 @@
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// Patches ONE known shader-pack defect: iterationRP hard-sizes the scratch
// its subgroup prefix scans write through prefixSumCache[gl_SubgroupID].
// The pack ships that idiom twice, sized for the >= 16-lane subgroups
// desktop GL drivers give it:
// - the auto-exposure reduction: 32x16 (512 invocations), vec2[32];
// - the RTW importance warp: 1024 invocations, float[64].
// On a narrower Vulkan device (lavapipe's 8 lanes -> 64 and 128 subgroups)
// every subgroup past the last declared entry indexes shared memory out of
// bounds - on a CPU rasterizer that is literal heap corruption. Both
// reduction ALGORITHMS are width-agnostic (their combine loops are sized by
// gl_NumSubgroups), so the faithful repair is to grow the under-declared
// arrays to ceil(invocations / native width) and change nothing else.
// Patches ONE known shader-pack defect: iterationRP's auto-exposure reduction
// declares `shared vec2 prefixSumCache[32]` for its 512-invocation workgroup
// and stores per-subgroup subtotals through prefixSumCache[gl_SubgroupID].
// The pack hard-sized that scratch for the >=16-lane subgroups desktop GL
// drivers ship; on a narrower Vulkan device (lavapipe's 8 lanes -> 64
// subgroups) every subgroup past entry 31 indexes shared memory out of
// bounds - on a CPU rasterizer that is literal heap corruption. The
// reduction ALGORITHM is width-agnostic (its combine loop is sized by
// gl_NumSubgroups), so the faithful repair is to grow the one under-declared
// array to ceil(512 / native width) and change nothing else. This is the
// pack author's bug, not MobileGL's; the patch is therefore deliberately
// NOT a general mechanism - it only rewrites modules that positively match
// iterationRP's reduction fingerprint:
// - GLCompute entry point with local size exactly 32x16x1;
// - a subgroupInclusiveAdd on a vec2 (OpGroupNonUniformFAdd InclusiveScan,
// the pack's luminance/exposure accumulator signature);
// - a workgroup-shared array of exactly vec2[32] whose access-chain index
// is data-dependent on gl_SubgroupID.
// Matching at the SPIR-V level keeps the recognition robust against
// whitespace/identifier-level drift that made the old source-text template
// rewrite (removed in 7769156) so brittle, while still refusing to touch
// anything that is not this pack's reduction. On devices whose native width
// already satisfies the pack's assumption (>= 16 lanes: desktop GL, Adreno),
// the grown length equals or undershoots the declared 32 and every module
// passes through byte-identical.
//
// This is the pack author's bug, not MobileGL's, so the patch is
// deliberately NOT a general "resize shared arrays" mechanism. It rewrites
// an array only when the module positively matches the pack's reduction
// idiom AND the device's own topology proves the declaration too small:
// - GLCompute entry point with a literal workgroup size;
// - a subgroup scan/reduce over a 32-bit float scalar or vector
// (OpGroupNonUniformF{Add,Min,Max}), the pack's accumulator signature;
// - a workgroup-shared array of 32-bit float scalars/vectors whose
// access-chain index is data-dependent on gl_SubgroupID;
// - a declared length strictly below ceil(invocations / native width).
// That last clause is what keeps the patch inert wherever the pack is
// correct: on any device whose width satisfies the pack's assumption
// (>= 16 lanes: desktop GL, Adreno) both shapes already fit and every
// module passes through byte-identical. Matching at the SPIR-V level keeps
// recognition robust against the whitespace/identifier drift that made the
// old source-text template rewrite (removed in 7769156) so brittle.
//
// The pass never fails a module: anything it cannot prove is this pattern -
// or cannot grow safely (a whole-array use, a spec-constant length, an
// initializer, or growth that would not fit maxWorkgroupScratchBytes) - is
// left exactly as it was. Pass the device's maxComputeSharedMemorySize as
// maxWorkgroupScratchBytes; 0 falls back to the 16384-byte Vulkan minimum.
// The pass never fails a module: anything it cannot prove is this exact
// pattern - or cannot grow safely (a whole-array use, a spec-constant
// length, an initializer) - is left exactly as it was.
class FixIterationRPSubgroupScratchPass : public spvtools::opt::Pass {
public:
FixIterationRPSubgroupScratchPass(Uint32 nativeSubgroupSize,
Uint32 maxWorkgroupScratchBytes)
: m_nativeSubgroupSize(nativeSubgroupSize),
m_maxWorkgroupScratchBytes(maxWorkgroupScratchBytes) {}
explicit FixIterationRPSubgroupScratchPass(Uint32 nativeSubgroupSize)
: m_nativeSubgroupSize(nativeSubgroupSize) {}
const char* name() const override { return "fix-iterationrp-subgroup-scratch"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateFixIterationRPSubgroupScratchPass(
Uint32 nativeSubgroupSize, Uint32 maxWorkgroupScratchBytes);
Uint32 nativeSubgroupSize);
private:
Uint32 m_nativeSubgroupSize;
Uint32 m_maxWorkgroupScratchBytes;
};
} // namespace ShaderTranspiler
} // namespace MG_Util
@@ -164,21 +164,6 @@ bool LoadMobileGL(const Request& request, std::string& error) {
} else {
unsetenv("MOBILEGL_COHERENT_AS_FLUSH");
}
if (request.fixIterationRPSubgroupScratch) {
setenv("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH", "1", 1);
} else {
unsetenv("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
}
if (request.deriveNumSubgroups) {
setenv("MOBILEGL_DERIVE_NUM_SUBGROUPS", "1", 1);
} else {
unsetenv("MOBILEGL_DERIVE_NUM_SUBGROUPS");
}
if (request.iterationRPFixBarrier) {
setenv("MOBILEGL_ITERATIONRP_FIX_BARRIER", "1", 1);
} else {
unsetenv("MOBILEGL_ITERATIONRP_FIX_BARRIER");
}
if (request.fboAttachmentDumps.empty()) {
unsetenv("MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS");
} else {
@@ -833,10 +818,6 @@ 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 << " \"holdMs\": " << request.holdMs << ",\n";
file << " \"mismatchPixels\": " << result.mismatchPixels << "\n";
file << "}\n";
@@ -44,9 +44,6 @@ struct Request {
bool avoidAngleLlvmpipeSamplerMipmapMinFilter = false;
bool avoidAngleLlvmpipeExplicitLodBias = false;
bool coherentAsFlush = false;
bool fixIterationRPSubgroupScratch = false;
bool deriveNumSubgroups = false;
bool iterationRPFixBarrier = false;
int holdMs = 0;
};
@@ -122,9 +122,6 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
jboolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
jboolean avoidAngleLlvmpipeExplicitLodBias,
jboolean coherentAsFlush,
jboolean fixIterationRPSubgroupScratch,
jboolean deriveNumSubgroups,
jboolean iterationRPFixBarrier,
jstring texture2dDumps) {
mobilegl_trace::Request request;
request.tracePath = ToString(env, tracePath);
@@ -153,9 +150,6 @@ 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;
ScopedTraceReplayState replayState;
mobilegl_trace_set_requested_size(request.width, request.height);
@@ -116,9 +116,6 @@ public final class TraceReplayActivity extends Activity {
request.avoidAngleLlvmpipeSamplerMipmapMinFilter,
request.avoidAngleLlvmpipeExplicitLodBias,
request.coherentAsFlush,
request.fixIterationRPSubgroupScratch,
request.deriveNumSubgroups,
request.iterationRPFixBarrier,
request.texture2dDumps
);
Log.i(TAG, result.toString());
@@ -152,9 +149,6 @@ public final class TraceReplayActivity extends Activity {
boolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
boolean avoidAngleLlvmpipeExplicitLodBias,
boolean coherentAsFlush,
boolean fixIterationRPSubgroupScratch,
boolean deriveNumSubgroups,
boolean iterationRPFixBarrier,
String texture2dDumps
);
@@ -180,9 +174,6 @@ 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 String texture2dDumps;
private TraceReplayRequest(
@@ -207,9 +198,6 @@ public final class TraceReplayActivity extends Activity {
boolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
boolean avoidAngleLlvmpipeExplicitLodBias,
boolean coherentAsFlush,
boolean fixIterationRPSubgroupScratch,
boolean deriveNumSubgroups,
boolean iterationRPFixBarrier,
String texture2dDumps
) {
this.tracePath = tracePath;
@@ -233,9 +221,6 @@ 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.texture2dDumps = texture2dDumps;
}
@@ -264,9 +249,6 @@ 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),
readString(intent, "texture_2d_dumps", "")
);
}
-12
View File
@@ -40,9 +40,6 @@ 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 DirectGLES
against an offscreen EGL pbuffer instead of the Activity surface.
Set MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1,
MOBILEGL_DERIVE_NUM_SUBGROUPS=1, and MOBILEGL_ITERATIONRP_FIX_BARRIER=1 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.
Pass --avoid-angle-llvmpipe-explicit-lod-bias for DirectGLES traces whose shaders
@@ -366,15 +363,6 @@ run_retrace() {
if [ "${coherent_as_flush}" -eq 1 ]; then
set -- "$@" --ez coherent_as_flush true
fi
if [ "${MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH:-}" = "1" ]; then
set -- "$@" --ez fix_iterationrp_subgroup_scratch true
fi
if [ "${MOBILEGL_DERIVE_NUM_SUBGROUPS:-}" = "1" ]; then
set -- "$@" --ez derive_num_subgroups true
fi
if [ "${MOBILEGL_ITERATIONRP_FIX_BARRIER:-}" = "1" ]; then
set -- "$@" --ez iterationrp_fix_barrier true
fi
if [ -n "${texture_2d_dumps}" ]; then
set -- "$@" --es texture_2d_dumps "${texture_2d_dumps}"
fi
-8
View File
@@ -72,16 +72,8 @@ 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";
}
bool ParseArgs(int argc, char **argv, mobilegl_trace::Request &request) {
request.backend = "DirectGLES";
request.fixIterationRPSubgroupScratch = ReadEnvFlag("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
request.deriveNumSubgroups = ReadEnvFlag("MOBILEGL_DERIVE_NUM_SUBGROUPS");
request.iterationRPFixBarrier = ReadEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER");
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];