mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
Compare commits
4
Commits
3181ed2c5a
...
eadf7bc474
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eadf7bc474 | ||
|
|
00a326ef78 | ||
|
|
c09045fe59 | ||
|
|
5bd8ef01e5 |
@@ -420,6 +420,9 @@ jobs:
|
||||
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
|
||||
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
|
||||
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
|
||||
MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
MOBILEGL_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
MOBILEGL_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
run: |
|
||||
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
|
||||
test -f "${apk_file}"
|
||||
|
||||
@@ -265,6 +265,9 @@ jobs:
|
||||
# crash stack without burning a CI round on an in-workflow debugger.
|
||||
env:
|
||||
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||
MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
|
||||
MOBILEGL_DERIVE_NUM_SUBGROUPS: "1"
|
||||
MOBILEGL_ITERATIONRP_FIX_BARRIER: "1"
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||
@@ -639,6 +642,12 @@ jobs:
|
||||
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
|
||||
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
|
||||
fi
|
||||
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
|
||||
&& [ '${{ matrix.case }}' = 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' ]; then
|
||||
export MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1
|
||||
export MOBILEGL_DERIVE_NUM_SUBGROUPS=1
|
||||
export MOBILEGL_ITERATIONRP_FIX_BARRIER=1
|
||||
fi
|
||||
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
|
||||
# runner has, so force it on for the OIT case it exists to fix. ForceOn
|
||||
# bypasses only the vendor gate, so this exercises the real strip on
|
||||
|
||||
@@ -285,6 +285,7 @@ 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
|
||||
@@ -674,3 +675,10 @@ if (NOT ANDROID)
|
||||
add_subdirectory(tools/trace_replay)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# The integration binary is also useful as a standalone adb-shell executable.
|
||||
# Android cannot use the desktop-only MobileGL_s target, so its CMake module
|
||||
# links libMobileGL.so and creates an AImageReader-backed window instead.
|
||||
if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
|
||||
add_subdirectory(MobileGL/MG_IntegrationTest)
|
||||
endif()
|
||||
|
||||
@@ -100,6 +100,10 @@ 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
|
||||
|
||||
@@ -171,6 +171,7 @@ 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,10 +3190,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
// GL_KHR_shader_subgroup handling (SubgroupSupportPolicy.h). Native subgroup
|
||||
// operations execute natively; two module repairs keep the GL contract intact
|
||||
// operations execute natively; module repairs keep the GL contract intact
|
||||
// around them. The opt-in emulation path replaces them only on devices with no
|
||||
// subgroup support at all (MOBILEGL_MAGMA_EMULATE_SUBGROUP).
|
||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Compute) {
|
||||
// Program 203 broadcasts the first reduction through
|
||||
// prefixSumCache[0], then lets the second reduction overwrite that
|
||||
// scratch without first rendezvousing all readers. Patch that exact
|
||||
// fingerprint before either native or emulated subgroup lowering.
|
||||
if (m_subgroupPolicy.fixIterationRPBarrier) {
|
||||
Vector<Uint> patchedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPBarrierForVulkan(
|
||||
moduleSpirvs[i], patchedSpirv, enableSpirvValidation)) {
|
||||
moduleSpirvs[i] = std::move(patchedSpirv);
|
||||
} else {
|
||||
MGLOG_E("ProgramFactory: iterationRP barrier patch failed for program %u; "
|
||||
"Program 203 keeps its shared-scratch race",
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
}
|
||||
if (m_subgroupPolicy.emulateSubgroups) {
|
||||
Vector<Uint> emulatedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::EmulateSubgroupsForVulkan(
|
||||
|
||||
@@ -381,6 +381,7 @@ 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,6 +3063,7 @@ 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,9 +18,11 @@ 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. Two module-level repairs keep the GL contract intact around them:
|
||||
// for. Module-level repairs keep the GL contract intact around them:
|
||||
// - FixIterationRPSubgroupScratchPass patches the one known pack bug: iterationRP's
|
||||
// prefixSumCache[32], under-declared for sub-16-lane devices (8-lane lavapipe);
|
||||
// - FixIterationRPBarrierPass repairs Program 203's race between two reductions
|
||||
// reusing that scratch, when explicitly enabled;
|
||||
// - DeriveNumSubgroupsPass replaces the one builtin drivers get wrong
|
||||
// (gl_NumSubgroups) with the value the rest of the topology implies.
|
||||
// The 32-lane shared-memory emulation (EmulateSubgroupsPass) is a LAST RESORT for
|
||||
@@ -47,6 +49,10 @@ 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
|
||||
|
||||
@@ -24,9 +24,14 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(MGL_ITEST_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..)
|
||||
|
||||
# Only meaningful where MobileGL_s exists (i.e. not Android).
|
||||
if (NOT TARGET MobileGL_s)
|
||||
message(STATUS "MobileGL_s is not available; skipping the integration test module")
|
||||
# Desktop links the static implementation directly. Android runs the same
|
||||
# executable from adb shell and links the shipping shared library instead.
|
||||
if (ANDROID)
|
||||
set(MGL_ITEST_MOBILEGL_TARGET MobileGL)
|
||||
elseif (TARGET MobileGL_s)
|
||||
set(MGL_ITEST_MOBILEGL_TARGET MobileGL_s)
|
||||
else()
|
||||
message(STATUS "No MobileGL library target is available; skipping the integration test module")
|
||||
return()
|
||||
endif()
|
||||
|
||||
@@ -69,6 +74,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/UniformInitializerScenario.cpp
|
||||
Scenarios/SwizzleAccessRoutineScenario.cpp
|
||||
Scenarios/IterationRPFirstReductionScenario.cpp
|
||||
Scenarios/IterationRPProgram203Scenario.cpp
|
||||
Scenarios/IterationRPScratchFixScenario.cpp
|
||||
Scenarios/ProgramPipelineScenario.cpp
|
||||
Scenarios/ImageLoadStoreSsoScenario.cpp
|
||||
@@ -94,9 +100,20 @@ target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
# gtest, not gtest_main: Main.cpp installs the harness banner itself.
|
||||
target_link_libraries(MobileGLIntegrationTest PRIVATE
|
||||
GTest::gtest
|
||||
MobileGL_s
|
||||
${MGL_ITEST_MOBILEGL_TARGET}
|
||||
)
|
||||
|
||||
if (ANDROID)
|
||||
find_library(MGL_ITEST_ANDROID_LIBRARY android REQUIRED)
|
||||
find_library(MGL_ITEST_LOG_LIBRARY log REQUIRED)
|
||||
find_library(MGL_ITEST_MEDIANDK_LIBRARY mediandk REQUIRED)
|
||||
target_link_libraries(MobileGLIntegrationTest PRIVATE
|
||||
${MGL_ITEST_ANDROID_LIBRARY}
|
||||
${MGL_ITEST_LOG_LIBRARY}
|
||||
${MGL_ITEST_MEDIANDK_LIBRARY}
|
||||
)
|
||||
endif()
|
||||
|
||||
if (MSVC)
|
||||
# Same reason as MG_Test/Backend/DirectVulkan: the GLES headers declare gl*
|
||||
# as dllimport on Windows, so the in-library GL entry-point definitions only
|
||||
@@ -105,6 +122,10 @@ if (MSVC)
|
||||
endif()
|
||||
target_compile_definitions(MobileGLIntegrationTest PRIVATE -DNOMINMAX)
|
||||
|
||||
if (ANDROID)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# --- ctest wiring --------------------------------------------------------
|
||||
# A bare libEGL on a glvnd box resolves to whatever vendor comes first, which is
|
||||
# usually Mesa/llvmpipe - a software rasteriser silently replacing the GPU under
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
#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
|
||||
@@ -37,7 +42,7 @@
|
||||
// the only construction that is actually predictive here: MobileGL ABORTS
|
||||
// (MOBILEGL_ASSERT -> SIGTRAP) rather than returning an error on an unusable
|
||||
// platform, so nothing the parent can call in-process is allowed to be wrong.
|
||||
#if !defined(_WIN32) && !defined(__APPLE__) && __has_include(<sys/wait.h>)
|
||||
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && __has_include(<sys/wait.h>)
|
||||
#define MGITEST_HAVE_FORK_PREFLIGHT 1
|
||||
#include <csignal>
|
||||
#include <ctime>
|
||||
@@ -78,12 +83,58 @@ 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
|
||||
@@ -123,10 +174,10 @@ namespace MGITest {
|
||||
// callers). surfaceless is the platform with no window-system dependency at
|
||||
// all; the surface this file then creates is still a pbuffer, which every
|
||||
// platform supports and which the amendment to this rule requires as the
|
||||
// fallback shape. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
|
||||
// fallback shape on desktop. Android instead supplies an AImageReader
|
||||
// ANativeWindow. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
|
||||
// driver that consults them directly cannot reintroduce the dependency
|
||||
// behind EGL's back. Desktop-only file: MG_IntegrationTest never builds
|
||||
// for Android, so no device path is affected.
|
||||
// behind EGL's back.
|
||||
void EnsureHeadlessPlatform() {
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
static bool done = false;
|
||||
@@ -214,12 +265,21 @@ 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;
|
||||
@@ -548,6 +608,8 @@ 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, following MG_Benchmark/Driver/DriverBench.c: an EGL
|
||||
// context on a PBUFFER surface. No window, no window manager, no human. Unlike
|
||||
// DriverBench the scenarios do draw to the DEFAULT framebuffer (that is where
|
||||
// the Y-flip lives) and do call eglSwapBuffers (that is the frame boundary the
|
||||
// cross-frame scenarios need to be real).
|
||||
// Headless by construction: desktop uses an EGL pbuffer and Android uses an
|
||||
// AImageReader-backed ANativeWindow that needs no Activity. No window manager,
|
||||
// no human. Unlike DriverBench the scenarios do draw to the DEFAULT framebuffer
|
||||
// (that is where the Y-flip lives) and do call eglSwapBuffers (that is the frame
|
||||
// boundary the cross-frame scenarios need to be real).
|
||||
//
|
||||
// One process is one backend: MOBILEGL_BACKEND_TYPE is latched at
|
||||
// initialization, so the CMake wiring runs this binary once per backend rather
|
||||
|
||||
@@ -31,8 +31,14 @@ namespace {
|
||||
// silently bound to a workstation's window system is a different
|
||||
// run from CI's and must be visible as one in the log.
|
||||
const char* eglPlatform = std::getenv("EGL_PLATFORM");
|
||||
std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless, EGL_PLATFORM=%s)\n",
|
||||
#if defined(__ANDROID__)
|
||||
constexpr const char* surfaceKind = "AImageReader window";
|
||||
#else
|
||||
constexpr const char* surfaceKind = "pbuffer";
|
||||
#endif
|
||||
std::fprintf(stderr, " renderer: %s\n surface: %dx%d %s (headless, EGL_PLATFORM=%s)\n",
|
||||
gl.RendererString().c_str(), gl.Width(), gl.Height(),
|
||||
surfaceKind,
|
||||
eglPlatform != nullptr ? eglPlatform : "<unset>");
|
||||
} else if (MGITest::RequireGpu()) {
|
||||
std::fprintf(stderr,
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IterationRPProgram203Scenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Full iterationRP Program 203 golden input/output fixture. The original shader
|
||||
// consumes deterministic complete textures and uniforms, then its complete
|
||||
// 512x513 RG16F output image is compared against fixed half-float golden bits.
|
||||
// This catches both a wrong exposure slot and collateral scratch corruption.
|
||||
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
constexpr int kSceneWidth = 854;
|
||||
constexpr int kSceneHeight = 480;
|
||||
constexpr int kPixelDataWidth = 512;
|
||||
constexpr int kPixelDataHeight = 513;
|
||||
constexpr std::size_t kSceneTexelCount =
|
||||
static_cast<std::size_t>(kSceneWidth) * kSceneHeight;
|
||||
constexpr std::size_t kPixelDataTexelCount =
|
||||
static_cast<std::size_t>(kPixelDataWidth) * kPixelDataHeight;
|
||||
|
||||
struct Rgba32f {
|
||||
float r, g, b, a;
|
||||
};
|
||||
|
||||
struct Rg16 {
|
||||
std::uint16_t r, g;
|
||||
};
|
||||
|
||||
static_assert(sizeof(Rgba32f) == 16);
|
||||
static_assert(sizeof(Rg16) == 4);
|
||||
|
||||
// Captured from the fixed fixture on Adreno 830. These are the exact
|
||||
// RG16F storage bits for (0.806640625, 8.2578125), not rounded decimal
|
||||
// comparisons performed by the test.
|
||||
constexpr Rg16 kGoldenExposure = {0x3a74u, 0x4821u};
|
||||
|
||||
constexpr const char* kCommonSource = R"glsl(
|
||||
#version 430 core
|
||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
||||
|
||||
uniform int frameCounter;
|
||||
uniform float frameTime;
|
||||
uniform float aspectRatio;
|
||||
uniform vec2 pixelSize;
|
||||
uniform float nightVision;
|
||||
uniform float darknessLightFactor;
|
||||
uniform sampler2D colortex2;
|
||||
uniform sampler2D pixelData2D;
|
||||
layout(rg16f) uniform image2D img_pixelData2D;
|
||||
|
||||
float remapSaturate(float x, float e0, float e1) {
|
||||
return clamp((x - e0) / (e1 - e0), 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float GetExposureValue(float luminance) {
|
||||
float aeCurve = 0.65f;
|
||||
aeCurve = mix(aeCurve, clamp(aeCurve * 1.2f, 0.0f, 1.0f), nightVision);
|
||||
aeCurve *= remapSaturate(luminance, 2.0f, 1.0f) * 0.6f + 0.4f;
|
||||
float ae = pow(luminance, -aeCurve);
|
||||
ae *= 1.0f - min(darknessLightFactor * 2.0f, 0.9f);
|
||||
ae *= 8.5f;
|
||||
return ae;
|
||||
}
|
||||
)glsl";
|
||||
|
||||
constexpr const char* kOriginalMain = R"glsl(
|
||||
layout(local_size_x = 32, local_size_y = 16) in;
|
||||
shared vec2 prefixSumCache[32];
|
||||
|
||||
void main() {
|
||||
vec2 texCoord = (vec2(gl_GlobalInvocationID.xy) + 0.5f) * vec2(1.0f / 32.0f, 1.0f / 16.0f);
|
||||
vec2 sampleCoord = texCoord * (1.0f / 64.0f);
|
||||
sampleCoord.x += (15.0f / 32.0f) + pixelSize.x * 12.0f;
|
||||
float tileExposure = dot(textureLod(colortex2, sampleCoord, 0.0f).rgb,
|
||||
vec3(0.2125f, 0.7154f, 0.0721f));
|
||||
vec2 sampleLuminance = vec2(tileExposure, 0.0f);
|
||||
sampleLuminance = subgroupInclusiveAdd(sampleLuminance);
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||
prefixSumCache[gl_SubgroupID] = sampleLuminance;
|
||||
barrier();
|
||||
|
||||
uint loopLength = uint(findMSB(gl_NumSubgroups));
|
||||
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
|
||||
for (uint i = 0u; i < loopLength; ++i) {
|
||||
if ((gl_SubgroupID & (1u << i)) > 0u) {
|
||||
sampleLuminance += prefixSumCache[(gl_SubgroupID >> i << i) - 1u];
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||
prefixSumCache[gl_SubgroupID] = sampleLuminance;
|
||||
}
|
||||
barrier();
|
||||
}
|
||||
if (gl_LocalInvocationIndex == 511u)
|
||||
prefixSumCache[0] = sampleLuminance / 512.0f;
|
||||
barrier();
|
||||
|
||||
float avg = prefixSumCache[0].x;
|
||||
vec2 tileDistance = texCoord * 2.0f - 1.0f;
|
||||
tileDistance.y /= aspectRatio;
|
||||
float centerDistance = length(tileDistance);
|
||||
float tileWeight = remapSaturate(centerDistance, 0.6f, 0.4f);
|
||||
tileExposure = max(7.0E-7f, tileExposure);
|
||||
float lumaWeight = avg / tileExposure;
|
||||
lumaWeight = pow(lumaWeight, remapSaturate(avg, 0.02f, 0.001f) * 0.4f + 0.2f);
|
||||
tileWeight *= lumaWeight;
|
||||
|
||||
vec2 sampleExposure = vec2(tileExposure * tileWeight, tileWeight);
|
||||
sampleExposure = subgroupInclusiveAdd(sampleExposure);
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||
prefixSumCache[gl_SubgroupID] = sampleExposure;
|
||||
barrier();
|
||||
for (uint i = 0u; i < loopLength; ++i) {
|
||||
if ((gl_SubgroupID & (1u << i)) > 0u) {
|
||||
sampleExposure += prefixSumCache[(gl_SubgroupID >> i << i) - 1u];
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||
prefixSumCache[gl_SubgroupID] = sampleExposure;
|
||||
}
|
||||
barrier();
|
||||
}
|
||||
|
||||
if (gl_LocalInvocationIndex == 511u) {
|
||||
float avgExposure = max(sampleExposure.x / sampleExposure.y * 29.3f, 1.0E-10f);
|
||||
avgExposure = log2(avgExposure);
|
||||
float prevAvgExposure = log2(texelFetch(pixelData2D, ivec2(0, 0), 0).x);
|
||||
float frameTimeFixed = frameTime + step(frameCounter, 20) * 100.0f;
|
||||
float exposureTime = clamp(frameTimeFixed * 2.0f, 0.0f, 1.0f);
|
||||
avgExposure = mix(prevAvgExposure, avgExposure, exposureTime);
|
||||
avgExposure = max(exp2(avgExposure), 1.0E-5f);
|
||||
float exposure = GetExposureValue(avgExposure);
|
||||
imageStore(img_pixelData2D, ivec2(0, 0), vec4(avgExposure, exposure, 0.0f, 0.0f));
|
||||
}
|
||||
}
|
||||
)glsl";
|
||||
|
||||
GLuint CompileCompute(const char* mainSource, std::string* error) {
|
||||
const std::array<const GLchar*, 2> sources = {kCommonSource, mainSource};
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, static_cast<GLsizei>(sources.size()), sources.data(), nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled != GL_TRUE) {
|
||||
std::array<char, 8192> log{};
|
||||
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size() - 1), nullptr, log.data());
|
||||
*error = log.data();
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked != GL_TRUE) {
|
||||
std::array<char, 8192> log{};
|
||||
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size() - 1), nullptr, log.data());
|
||||
*error = log.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
std::vector<Rgba32f> MakeSceneInput() {
|
||||
std::vector<Rgba32f> texels(kSceneTexelCount);
|
||||
for (int y = 0; y < kSceneHeight; ++y) {
|
||||
for (int x = 0; x < kSceneWidth; ++x) {
|
||||
std::uint32_t h = static_cast<std::uint32_t>(x) * 0x9e3779b9u;
|
||||
h ^= static_cast<std::uint32_t>(y) * 0x85ebca6bu;
|
||||
h ^= h >> 16u;
|
||||
h *= 0x7feb352du;
|
||||
h ^= h >> 15u;
|
||||
const float noise = static_cast<float>(h & 0xffffu) / 65535.0f;
|
||||
float base = 0.0002f + noise * 0.075f;
|
||||
const float dx = static_cast<float>(x - 420);
|
||||
const float dy = static_cast<float>(y - 4);
|
||||
base += 0.65f * std::exp(-(dx * dx + dy * dy) / 18.0f);
|
||||
if (((x + y * 17) % 113) == 0) base += 1.75f;
|
||||
texels[static_cast<std::size_t>(y) * kSceneWidth + x] =
|
||||
{base * 0.83f, base * 1.07f, base * 1.31f, 1.0f};
|
||||
}
|
||||
}
|
||||
return texels;
|
||||
}
|
||||
|
||||
std::uint16_t FloatToHalf(float value) {
|
||||
const std::uint32_t bits = std::bit_cast<std::uint32_t>(value);
|
||||
const std::uint32_t sign = (bits >> 16u) & 0x8000u;
|
||||
const std::uint32_t exponent = (bits >> 23u) & 0xffu;
|
||||
std::uint32_t mantissa = bits & 0x7fffffu;
|
||||
|
||||
if (exponent == 0xffu) {
|
||||
return static_cast<std::uint16_t>(sign | (mantissa == 0 ? 0x7c00u : 0x7e00u));
|
||||
}
|
||||
int halfExponent = static_cast<int>(exponent) - 127 + 15;
|
||||
if (halfExponent >= 31) return static_cast<std::uint16_t>(sign | 0x7c00u);
|
||||
if (halfExponent <= 0) {
|
||||
if (halfExponent < -10) return static_cast<std::uint16_t>(sign);
|
||||
mantissa |= 0x800000u;
|
||||
const unsigned shift = static_cast<unsigned>(14 - halfExponent);
|
||||
const std::uint32_t rounded = mantissa + ((1u << (shift - 1u)) - 1u) +
|
||||
((mantissa >> shift) & 1u);
|
||||
return static_cast<std::uint16_t>(sign | (rounded >> shift));
|
||||
}
|
||||
mantissa += 0xfffu + ((mantissa >> 13u) & 1u);
|
||||
if ((mantissa & 0x800000u) != 0) {
|
||||
mantissa = 0;
|
||||
if (++halfExponent >= 31) return static_cast<std::uint16_t>(sign | 0x7c00u);
|
||||
}
|
||||
return static_cast<std::uint16_t>(sign | (static_cast<std::uint32_t>(halfExponent) << 10u) |
|
||||
(mantissa >> 13u));
|
||||
}
|
||||
|
||||
std::vector<Rg16> MakePixelDataInput() {
|
||||
std::vector<Rg16> texels(kPixelDataTexelCount);
|
||||
for (std::size_t i = 0; i < texels.size(); ++i) {
|
||||
texels[i] = {FloatToHalf(0.35f + static_cast<float>(i % 97u) * 0.0025f),
|
||||
FloatToHalf(-0.45f + static_cast<float>(i % 89u) * 0.01f)};
|
||||
}
|
||||
texels[0] = {FloatToHalf(0.73f), FloatToHalf(1.25f)};
|
||||
return texels;
|
||||
}
|
||||
|
||||
std::vector<Rg16> MakeGoldenOutput() {
|
||||
std::vector<Rg16> golden = MakePixelDataInput();
|
||||
golden[0] = kGoldenExposure;
|
||||
return golden;
|
||||
}
|
||||
|
||||
GLuint MakeTexture(GLenum internalFormat, GLenum format, GLenum type, int width, int height,
|
||||
const void* data) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(internalFormat), width, height, 0, format,
|
||||
type, data);
|
||||
return texture;
|
||||
}
|
||||
|
||||
void BindAndDispatch(GLuint program, GLuint scene, GLuint pixelData) {
|
||||
glUseProgram(program);
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
glBindTexture(GL_TEXTURE_2D, scene);
|
||||
glUniform1i(glGetUniformLocation(program, "colortex2"), 3);
|
||||
glActiveTexture(GL_TEXTURE4);
|
||||
glBindTexture(GL_TEXTURE_2D, pixelData);
|
||||
glUniform1i(glGetUniformLocation(program, "pixelData2D"), 4);
|
||||
glBindImageTexture(0, pixelData, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RG16F);
|
||||
glUniform1i(glGetUniformLocation(program, "img_pixelData2D"), 0);
|
||||
glUniform1i(glGetUniformLocation(program, "frameCounter"), 100);
|
||||
glUniform1f(glGetUniformLocation(program, "frameTime"), 1.0f / 60.0f);
|
||||
glUniform1f(glGetUniformLocation(program, "aspectRatio"),
|
||||
static_cast<float>(kSceneWidth) / kSceneHeight);
|
||||
glUniform2f(glGetUniformLocation(program, "pixelSize"), 1.0f / kSceneWidth, 1.0f / kSceneHeight);
|
||||
glUniform1f(glGetUniformLocation(program, "nightVision"), 0.23f);
|
||||
glUniform1f(glGetUniformLocation(program, "darknessLightFactor"), 0.08f);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
|
||||
}
|
||||
|
||||
std::vector<Rg16> ReadWholeRgTexture(GLuint texture) {
|
||||
std::vector<Rg16> texels(kPixelDataTexelCount);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, GL_RG, GL_HALF_FLOAT, texels.data());
|
||||
return texels;
|
||||
}
|
||||
|
||||
class IterationRPProgram203Scenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
GLint stages = 0;
|
||||
GLint features = 0;
|
||||
GLint invocations = 0;
|
||||
glGetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &stages);
|
||||
glGetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &features);
|
||||
glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &invocations);
|
||||
const GLbitfield required =
|
||||
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR;
|
||||
if ((static_cast<GLbitfield>(stages) & GL_COMPUTE_SHADER_BIT) == 0 ||
|
||||
(static_cast<GLbitfield>(features) & required) != required || invocations < 512) {
|
||||
GTEST_SKIP() << "requires 512-invocation basic+arithmetic compute subgroups";
|
||||
}
|
||||
|
||||
std::string error;
|
||||
m_original = CompileCompute(kOriginalMain, &error);
|
||||
ASSERT_NE(m_original, 0u) << "original Program 203: " << error;
|
||||
|
||||
const std::vector<Rgba32f> scene = MakeSceneInput();
|
||||
const std::vector<Rg16> pixelData = MakePixelDataInput();
|
||||
m_scene = MakeTexture(GL_RGBA16F, GL_RGBA, GL_FLOAT, kSceneWidth, kSceneHeight, scene.data());
|
||||
m_originalOutput =
|
||||
MakeTexture(GL_RG16F, GL_RG, GL_HALF_FLOAT, kPixelDataWidth, kPixelDataHeight,
|
||||
pixelData.data());
|
||||
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
const std::array<GLuint, 2> textures = {m_scene, m_originalOutput};
|
||||
glDeleteTextures(static_cast<GLsizei>(textures.size()), textures.data());
|
||||
if (m_original != 0) glDeleteProgram(m_original);
|
||||
}
|
||||
|
||||
GLuint m_original = 0;
|
||||
GLuint m_scene = 0;
|
||||
GLuint m_originalOutput = 0;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_F(IterationRPProgram203Scenario, FixedCompleteInputProducesFixedCompleteGoldenOutput) {
|
||||
if (!Ready()) return;
|
||||
|
||||
BindAndDispatch(m_original, m_scene, m_originalOutput);
|
||||
glFinish();
|
||||
const std::vector<Rg16> actual = ReadWholeRgTexture(m_originalOutput);
|
||||
const std::vector<Rg16> expected = MakeGoldenOutput();
|
||||
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
std::size_t mismatchTexels = 0;
|
||||
std::size_t firstMismatch = actual.size();
|
||||
for (std::size_t i = 0; i < actual.size(); ++i) {
|
||||
if (actual[i].r != expected[i].r || actual[i].g != expected[i].g) {
|
||||
if (firstMismatch == actual.size()) firstMismatch = i;
|
||||
++mismatchTexels;
|
||||
}
|
||||
}
|
||||
|
||||
RecordProperty("program203_output_width", kPixelDataWidth);
|
||||
RecordProperty("program203_output_height", kPixelDataHeight);
|
||||
RecordProperty("program203_compared_texels", static_cast<long long>(actual.size()));
|
||||
RecordProperty("program203_mismatch_texels", static_cast<long long>(mismatchTexels));
|
||||
std::cout << "IterationRPProgram203Scenario complete-output actualExposureBits=(0x" << std::hex
|
||||
<< actual[0].r << ", 0x" << actual[0].g << ") goldenExposureBits=(0x" << expected[0].r
|
||||
<< ", 0x" << expected[0].g << std::dec << ") mismatches=" << mismatchTexels << '/'
|
||||
<< actual.size() << '\n';
|
||||
|
||||
if (firstMismatch != actual.size()) {
|
||||
const std::size_t x = firstMismatch % kPixelDataWidth;
|
||||
const std::size_t y = firstMismatch / kPixelDataWidth;
|
||||
ADD_FAILURE() << "complete Program 203 output differs at " << x << ',' << y
|
||||
<< ": actual half bits=(0x" << std::hex << actual[firstMismatch].r << ", 0x"
|
||||
<< actual[firstMismatch].g << ") golden half bits=(0x" << expected[firstMismatch].r
|
||||
<< ", 0x" << expected[firstMismatch].g << std::dec << "); mismatched "
|
||||
<< mismatchTexels << " of " << actual.size() << " texels";
|
||||
}
|
||||
EXPECT_EQ(mismatchTexels, 0u);
|
||||
}
|
||||
} // namespace MGITest
|
||||
@@ -4,6 +4,7 @@ add_executable(
|
||||
SpirvPassTest
|
||||
SpirvPassTest.cpp
|
||||
DeriveNumSubgroupsTest.cpp
|
||||
FixIterationRPBarrierTest.cpp
|
||||
FixIterationRPSubgroupScratchTest.cpp
|
||||
EmulateSubgroupsTest.cpp
|
||||
DemoteFloat64Test.cpp
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/FixIterationRPBarrierTest.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#define SPV_ENABLE_UTILITY_CODE
|
||||
#include "glslang/SPIRV/spirv.hpp11"
|
||||
#undef SPV_ENABLE_UTILITY_CODE
|
||||
|
||||
#include "Includes.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
|
||||
namespace {
|
||||
constexpr SizeT kSpirvHeaderWordCount = 5u;
|
||||
|
||||
template <typename Visitor>
|
||||
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
|
||||
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
|
||||
const Uint32 wordCount = spirv[offset] >> 16u;
|
||||
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
|
||||
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
|
||||
offset += wordCount;
|
||||
}
|
||||
}
|
||||
|
||||
Vector<Uint32> CompileCompute(const String& source) {
|
||||
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||
if (!shaderResult) return {};
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
bool Validates(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
tools.SetMessageConsumer(
|
||||
[](spv_message_level_t, const char*, const spv_position_t& position, const char* message) {
|
||||
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
|
||||
});
|
||||
return tools.Validate(spirv);
|
||||
}
|
||||
|
||||
Uint32 CountOpcode(const Vector<Uint32>& spirv, spv::Op wanted) {
|
||||
Uint32 count = 0u;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
|
||||
if (opcode == wanted) ++count;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
bool HasWorkgroupBarrierImmediatelyBeforeSecondScan(const Vector<Uint32>& spirv) {
|
||||
std::map<Uint32, Uint32> uintConstants;
|
||||
spv::Op previous = spv::Op::OpNop;
|
||||
Uint32 scanCount = 0u;
|
||||
bool found = false;
|
||||
const Uint32* previousWords = nullptr;
|
||||
Uint32 previousWordCount = 0u;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == spv::Op::OpConstant && wordCount >= 4u) {
|
||||
uintConstants[words[2]] = words[3];
|
||||
}
|
||||
if (opcode == spv::Op::OpGroupNonUniformFAdd && wordCount >= 6u &&
|
||||
static_cast<spv::GroupOperation>(words[4]) == spv::GroupOperation::InclusiveScan && ++scanCount == 2u &&
|
||||
previous == spv::Op::OpControlBarrier && previousWordCount == 4u) {
|
||||
found =
|
||||
uintConstants[previousWords[1]] == static_cast<Uint32>(spv::Scope::Workgroup) &&
|
||||
uintConstants[previousWords[2]] == static_cast<Uint32>(spv::Scope::Workgroup) &&
|
||||
uintConstants[previousWords[3]] == (static_cast<Uint32>(spv::MemorySemanticsMask::AcquireRelease) |
|
||||
static_cast<Uint32>(spv::MemorySemanticsMask::WorkgroupMemory));
|
||||
}
|
||||
previous = opcode;
|
||||
previousWords = words;
|
||||
previousWordCount = wordCount;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
constexpr const char* kProgram203RaceShape = R"(#version 450 core
|
||||
#extension GL_KHR_shader_subgroup_basic : require
|
||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
||||
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { vec2 value; } outputData;
|
||||
shared vec2 prefixSumCache[32];
|
||||
void main() {
|
||||
vec2 sampleLuminance = subgroupInclusiveAdd(
|
||||
vec2(float(gl_LocalInvocationIndex), 1.0));
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||
prefixSumCache[gl_SubgroupID] = sampleLuminance;
|
||||
barrier();
|
||||
if (gl_LocalInvocationIndex == 511u)
|
||||
prefixSumCache[0] = sampleLuminance / 512.0;
|
||||
barrier();
|
||||
|
||||
float avg = prefixSumCache[0].x;
|
||||
float weight = avg > 0.0 ? float(gl_LocalInvocationIndex + 1u) / avg : 0.0;
|
||||
vec2 sampleExposure = subgroupInclusiveAdd(vec2(weight, 1.0));
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||
prefixSumCache[gl_SubgroupID] = sampleExposure;
|
||||
barrier();
|
||||
if (gl_LocalInvocationIndex == 511u)
|
||||
outputData.value = sampleExposure;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kAlreadySynchronizedShape = R"(#version 450 core
|
||||
#extension GL_KHR_shader_subgroup_basic : require
|
||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
||||
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { vec2 value; } outputData;
|
||||
shared vec2 prefixSumCache[32];
|
||||
void main() {
|
||||
vec2 first = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 1.0));
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||
prefixSumCache[gl_SubgroupID] = first;
|
||||
barrier();
|
||||
if (gl_LocalInvocationIndex == 511u) prefixSumCache[0] = first / 512.0;
|
||||
barrier();
|
||||
float avg = prefixSumCache[0].x;
|
||||
barrier();
|
||||
vec2 second = subgroupInclusiveAdd(vec2(avg, 1.0));
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||
prefixSumCache[gl_SubgroupID] = second;
|
||||
barrier();
|
||||
if (gl_LocalInvocationIndex == 511u) outputData.value = second;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kForeignSingleScanShape = R"(#version 450 core
|
||||
#extension GL_KHR_shader_subgroup_basic : require
|
||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
||||
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { vec2 value; } outputData;
|
||||
shared vec2 prefixSumCache[32];
|
||||
void main() {
|
||||
vec2 value = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 1.0));
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||
prefixSumCache[gl_SubgroupID] = value;
|
||||
barrier();
|
||||
if (gl_LocalInvocationIndex == 0u) outputData.value = prefixSumCache[0];
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
TEST(FixIterationRPBarrierPass, InsertsWorkgroupBarrierBeforeSecondReduction) {
|
||||
const Vector<Uint32> input = CompileCompute(kProgram203RaceShape);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Uint32 inputBarrierCount = CountOpcode(input, spv::Op::OpControlBarrier);
|
||||
EXPECT_FALSE(HasWorkgroupBarrierImmediatelyBeforeSecondScan(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, output, true));
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpControlBarrier), inputBarrierCount + 1u);
|
||||
EXPECT_TRUE(HasWorkgroupBarrierImmediatelyBeforeSecondScan(output));
|
||||
EXPECT_TRUE(Validates(output));
|
||||
}
|
||||
|
||||
TEST(FixIterationRPBarrierPass, LeavesOtherShapesByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kForeignSingleScanShape);
|
||||
ASSERT_FALSE(input.empty());
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
TEST(FixIterationRPBarrierPass, LeavesAnAlreadySynchronizedShaderByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kAlreadySynchronizedShape);
|
||||
ASSERT_FALSE(input.empty());
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
TEST(FixIterationRPBarrierPass, IsIdempotent) {
|
||||
const Vector<Uint32> input = CompileCompute(kProgram203RaceShape);
|
||||
ASSERT_FALSE(input.empty());
|
||||
Vector<Uint32> once;
|
||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, once, true));
|
||||
Vector<Uint32> twice;
|
||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(once, twice, true));
|
||||
EXPECT_EQ(twice, once);
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
#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"
|
||||
@@ -924,6 +925,17 @@ namespace MobileGL {
|
||||
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;
|
||||
|
||||
@@ -179,6 +179,12 @@ namespace MobileGL {
|
||||
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
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "FixIterationRPBarrierPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
const Instruction* RootVariable(IRContext* context, uint32_t pointerId) {
|
||||
const Instruction* def = context->get_def_use_mgr()->GetDef(pointerId);
|
||||
while (def != nullptr) {
|
||||
switch (def->opcode()) {
|
||||
case spv::Op::OpVariable:
|
||||
return def;
|
||||
case spv::Op::OpAccessChain:
|
||||
case spv::Op::OpInBoundsAccessChain:
|
||||
case spv::Op::OpCopyObject:
|
||||
def = context->get_def_use_mgr()->GetDef(def->GetSingleWordInOperand(0));
|
||||
break;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsUintConstant(IRContext* context, uint32_t id, uint32_t wanted) {
|
||||
const Instruction* def = context->get_def_use_mgr()->GetDef(id);
|
||||
return def != nullptr && def->opcode() == spv::Op::OpConstant && def->NumInOperands() == 1u &&
|
||||
def->GetSingleWordInOperand(0) == wanted;
|
||||
}
|
||||
|
||||
bool IsZeroElementPointer(IRContext* context, uint32_t pointerId, const Instruction** root) {
|
||||
const Instruction* pointer = context->get_def_use_mgr()->GetDef(pointerId);
|
||||
if (pointer == nullptr ||
|
||||
(pointer->opcode() != spv::Op::OpAccessChain && pointer->opcode() != spv::Op::OpInBoundsAccessChain) ||
|
||||
pointer->NumInOperands() < 2u) {
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = 1u; i < pointer->NumInOperands(); ++i) {
|
||||
if (!IsUintConstant(context, pointer->GetSingleWordInOperand(i), 0u)) return false;
|
||||
}
|
||||
*root = RootVariable(context, pointerId);
|
||||
return *root != nullptr;
|
||||
}
|
||||
|
||||
bool IsWorkgroupVec2Array(IRContext* context, const Instruction* variable) {
|
||||
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable || variable->NumInOperands() < 1u ||
|
||||
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) != spv::StorageClass::Workgroup) {
|
||||
return false;
|
||||
}
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
const Instruction* pointerType = defUseMgr->GetDef(variable->type_id());
|
||||
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer ||
|
||||
pointerType->NumInOperands() < 2u) {
|
||||
return false;
|
||||
}
|
||||
const Instruction* arrayType = defUseMgr->GetDef(pointerType->GetSingleWordInOperand(1));
|
||||
if (arrayType == nullptr || arrayType->opcode() != spv::Op::OpTypeArray ||
|
||||
arrayType->NumInOperands() < 2u) {
|
||||
return false;
|
||||
}
|
||||
const Instruction* length = defUseMgr->GetDef(arrayType->GetSingleWordInOperand(1));
|
||||
if (length == nullptr || length->opcode() != spv::Op::OpConstant || length->NumInOperands() != 1u) {
|
||||
return false;
|
||||
}
|
||||
const uint32_t arrayLength = length->GetSingleWordInOperand(0);
|
||||
if (arrayLength < 32u || arrayLength > 512u) return false;
|
||||
|
||||
const Instruction* vectorType = defUseMgr->GetDef(arrayType->GetSingleWordInOperand(0));
|
||||
if (vectorType == nullptr || vectorType->opcode() != spv::Op::OpTypeVector ||
|
||||
vectorType->NumInOperands() < 2u || vectorType->GetSingleWordInOperand(1) != 2u) {
|
||||
return false;
|
||||
}
|
||||
const Instruction* scalarType = defUseMgr->GetDef(vectorType->GetSingleWordInOperand(0));
|
||||
return scalarType != nullptr && scalarType->opcode() == spv::Op::OpTypeFloat &&
|
||||
scalarType->NumInOperands() == 1u && scalarType->GetSingleWordInOperand(0) == 32u;
|
||||
}
|
||||
|
||||
bool IsVec2FloatInclusiveAdd(IRContext* context, const Instruction* inst) {
|
||||
if (inst->opcode() != spv::Op::OpGroupNonUniformFAdd || inst->NumInOperands() < 3u ||
|
||||
static_cast<spv::GroupOperation>(inst->GetSingleWordInOperand(1)) !=
|
||||
spv::GroupOperation::InclusiveScan) {
|
||||
return false;
|
||||
}
|
||||
const Instruction* vectorType = context->get_def_use_mgr()->GetDef(inst->type_id());
|
||||
if (vectorType == nullptr || vectorType->opcode() != spv::Op::OpTypeVector ||
|
||||
vectorType->NumInOperands() < 2u || vectorType->GetSingleWordInOperand(1) != 2u) {
|
||||
return false;
|
||||
}
|
||||
const Instruction* scalarType = context->get_def_use_mgr()->GetDef(vectorType->GetSingleWordInOperand(0));
|
||||
return scalarType != nullptr && scalarType->opcode() == spv::Op::OpTypeFloat &&
|
||||
scalarType->NumInOperands() == 1u && scalarType->GetSingleWordInOperand(0) == 32u;
|
||||
}
|
||||
|
||||
bool HasProgram203LocalSize(IRContext* context) {
|
||||
for (const Instruction& entryPoint : context->module()->entry_points()) {
|
||||
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) !=
|
||||
spv::ExecutionModel::GLCompute) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const Instruction& mode : context->module()->execution_modes()) {
|
||||
if (mode.opcode() == spv::Op::OpExecutionMode && mode.NumInOperands() >= 5u &&
|
||||
static_cast<spv::ExecutionMode>(mode.GetSingleWordInOperand(1)) == spv::ExecutionMode::LocalSize) {
|
||||
return mode.GetSingleWordInOperand(2) == 32u && mode.GetSingleWordInOperand(3) == 16u &&
|
||||
mode.GetSingleWordInOperand(4) == 1u;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsStoreToRoot(IRContext* context, const Instruction* inst, const Instruction* root) {
|
||||
return inst->opcode() == spv::Op::OpStore && inst->NumInOperands() >= 2u &&
|
||||
RootVariable(context, inst->GetSingleWordInOperand(0)) == root;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status FixIterationRPBarrierPass::Process() {
|
||||
auto* irContext = context();
|
||||
if (!HasProgram203LocalSize(irContext)) return Status::SuccessWithoutChange;
|
||||
|
||||
for (auto& function : *irContext->module()) {
|
||||
std::vector<Instruction*> instructions;
|
||||
std::vector<size_t> scans;
|
||||
for (auto& block : function) {
|
||||
for (auto& inst : block) {
|
||||
if (IsVec2FloatInclusiveAdd(irContext, &inst)) scans.push_back(instructions.size());
|
||||
instructions.push_back(&inst);
|
||||
}
|
||||
}
|
||||
// Program 203 has exactly two vec2 inclusive adds: the luminance reduction
|
||||
// and the weighted-exposure reduction. More or fewer is not our fingerprint.
|
||||
if (scans.size() != 2u) continue;
|
||||
|
||||
const size_t firstScan = scans[0];
|
||||
const size_t secondScan = scans[1];
|
||||
const Instruction* scratch = nullptr;
|
||||
size_t averageLoad = instructions.size();
|
||||
|
||||
for (size_t i = firstScan + 1u; i < secondScan; ++i) {
|
||||
Instruction* inst = instructions[i];
|
||||
if (inst->opcode() != spv::Op::OpLoad || inst->NumInOperands() < 1u) continue;
|
||||
const Instruction* root = nullptr;
|
||||
if (!IsZeroElementPointer(irContext, inst->GetSingleWordInOperand(0), &root) ||
|
||||
!IsWorkgroupVec2Array(irContext, root)) {
|
||||
continue;
|
||||
}
|
||||
// The broadcast is read as prefixSumCache[0].x, hence a scalar load.
|
||||
const Instruction* type = irContext->get_def_use_mgr()->GetDef(inst->type_id());
|
||||
if (type == nullptr || type->opcode() != spv::Op::OpTypeFloat || type->NumInOperands() != 1u ||
|
||||
type->GetSingleWordInOperand(0) != 32u) {
|
||||
continue;
|
||||
}
|
||||
scratch = root;
|
||||
averageLoad = i;
|
||||
break;
|
||||
}
|
||||
if (scratch == nullptr) continue;
|
||||
|
||||
bool sawZeroBroadcastStore = false;
|
||||
bool sawPublishBarrier = false;
|
||||
for (size_t i = firstScan + 1u; i < averageLoad; ++i) {
|
||||
const Instruction* root = nullptr;
|
||||
if (instructions[i]->opcode() == spv::Op::OpStore &&
|
||||
IsZeroElementPointer(irContext, instructions[i]->GetSingleWordInOperand(0), &root) &&
|
||||
root == scratch) {
|
||||
sawZeroBroadcastStore = true;
|
||||
} else if (sawZeroBroadcastStore && instructions[i]->opcode() == spv::Op::OpControlBarrier) {
|
||||
sawPublishBarrier = true;
|
||||
}
|
||||
}
|
||||
if (!sawZeroBroadcastStore || !sawPublishBarrier) continue;
|
||||
|
||||
bool alreadySynchronized = false;
|
||||
for (size_t i = averageLoad + 1u; i < secondScan; ++i) {
|
||||
if (instructions[i]->opcode() == spv::Op::OpControlBarrier) {
|
||||
alreadySynchronized = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (alreadySynchronized) return Status::SuccessWithoutChange;
|
||||
|
||||
bool secondPhaseReusesScratch = false;
|
||||
for (size_t i = secondScan + 1u; i < instructions.size(); ++i) {
|
||||
if (IsStoreToRoot(irContext, instructions[i], scratch)) {
|
||||
secondPhaseReusesScratch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!secondPhaseReusesScratch) continue;
|
||||
|
||||
auto* constantMgr = irContext->get_constant_mgr();
|
||||
const uint32_t scopeId = constantMgr->GetUIntConstId(static_cast<uint32_t>(spv::Scope::Workgroup));
|
||||
const uint32_t semanticsId =
|
||||
constantMgr->GetUIntConstId(static_cast<uint32_t>(spv::MemorySemanticsMask::AcquireRelease) |
|
||||
static_cast<uint32_t>(spv::MemorySemanticsMask::WorkgroupMemory));
|
||||
if (scopeId == 0u || semanticsId == 0u) return Status::Failure;
|
||||
|
||||
instructions[secondScan]->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpControlBarrier, 0u, 0u,
|
||||
Instruction::OperandList{Operand{SPV_OPERAND_TYPE_ID, {scopeId}},
|
||||
Operand{SPV_OPERAND_TYPE_ID, {scopeId}},
|
||||
Operand{SPV_OPERAND_TYPE_ID, {semanticsId}}}));
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken FixIterationRPBarrierPass::CreateFixIterationRPBarrierPass() {
|
||||
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<FixIterationRPBarrierPass>());
|
||||
}
|
||||
} // namespace MobileGL::MG_Util::ShaderTranspiler
|
||||
@@ -0,0 +1,28 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// Repairs iterationRP Program 203's missing workgroup rendezvous between two
|
||||
// reductions that reuse prefixSumCache. The first phase broadcasts its result
|
||||
// through prefixSumCache[0], but the second phase may overwrite that element before
|
||||
// every invocation has read it. The pass fingerprints that exact two-scan,
|
||||
// 512-invocation shape and inserts one Workgroup control barrier immediately before
|
||||
// the second scan. Unrelated modules and already-repaired modules are byte-identical.
|
||||
class FixIterationRPBarrierPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "fix-iterationrp-barrier"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateFixIterationRPBarrierPass();
|
||||
};
|
||||
} // namespace MobileGL::MG_Util::ShaderTranspiler
|
||||
@@ -164,6 +164,21 @@ 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 {
|
||||
@@ -818,6 +833,10 @@ 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,6 +44,9 @@ struct Request {
|
||||
bool avoidAngleLlvmpipeSamplerMipmapMinFilter = false;
|
||||
bool avoidAngleLlvmpipeExplicitLodBias = false;
|
||||
bool coherentAsFlush = false;
|
||||
bool fixIterationRPSubgroupScratch = false;
|
||||
bool deriveNumSubgroups = false;
|
||||
bool iterationRPFixBarrier = false;
|
||||
int holdMs = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -122,6 +122,9 @@ 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);
|
||||
@@ -150,6 +153,9 @@ 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,6 +116,9 @@ 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());
|
||||
@@ -149,6 +152,9 @@ public final class TraceReplayActivity extends Activity {
|
||||
boolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
|
||||
boolean avoidAngleLlvmpipeExplicitLodBias,
|
||||
boolean coherentAsFlush,
|
||||
boolean fixIterationRPSubgroupScratch,
|
||||
boolean deriveNumSubgroups,
|
||||
boolean iterationRPFixBarrier,
|
||||
String texture2dDumps
|
||||
);
|
||||
|
||||
@@ -174,6 +180,9 @@ 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(
|
||||
@@ -198,6 +207,9 @@ public final class TraceReplayActivity extends Activity {
|
||||
boolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
|
||||
boolean avoidAngleLlvmpipeExplicitLodBias,
|
||||
boolean coherentAsFlush,
|
||||
boolean fixIterationRPSubgroupScratch,
|
||||
boolean deriveNumSubgroups,
|
||||
boolean iterationRPFixBarrier,
|
||||
String texture2dDumps
|
||||
) {
|
||||
this.tracePath = tracePath;
|
||||
@@ -221,6 +233,9 @@ 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;
|
||||
}
|
||||
|
||||
@@ -249,6 +264,9 @@ 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", "")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ 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
|
||||
@@ -363,6 +366,15 @@ 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
|
||||
|
||||
@@ -72,8 +72,16 @@ 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];
|
||||
|
||||
Reference in New Issue
Block a user