[Fix, Test] (MG_State, MG_Util, DirectVulkan, MG_Test): replay narrow-subgroup reductions correctly

This commit is contained in:
2026-08-16 13:33:01 -04:00
parent 6df5a6137f
commit 0ecfdff4e7
8 changed files with 479 additions and 36 deletions
+4 -3
View File
@@ -128,9 +128,10 @@ namespace MobileGL::MG_Config {
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
// version request.
Bool RelaxedSemantics = false;
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that rewrites
// recognized subgroup reductions when narrow Vulkan subgroups overflow fixed scratch
// arrays, or when Qualcomm subgroups are wider than the captured 32-lane model (see
// ShaderSourceProcessor's quirk registry).
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
@@ -10,6 +10,7 @@
#include "MG_Backend/BackendObject.h"
#include "DirectVulkan.h"
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include "MG_State/GLState/Core.h"
#include "MG_State/GLState/TextureState/TextureState.h"
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
@@ -383,6 +384,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MG_State::pGLContext) {
MG_State::pGLContext->InvalidateCompileEnv();
}
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
MutableFormatCapabilities());
PrintFormatCapabilities(GetFormatCapabilities());
@@ -687,6 +691,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_vulkanCaps = capabilities;
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MG_State::pGLContext) {
MG_State::pGLContext->InvalidateCompileEnv();
}
MutableFormatCapabilities().Clear();
}
+5
View File
@@ -39,6 +39,11 @@ namespace MobileGL::MG_State {
return m_compileEnv;
}
void GLContext::InvalidateCompileEnv() {
m_compileEnv.reset();
m_compileEnvBackend = nullptr;
}
// Error
void GLContext::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
// Invariant I1, mechanically enforced: the GL error state is GL-thread-owned.
+4 -1
View File
@@ -413,9 +413,12 @@ namespace MobileGL {
// cannot be captured in MG_State::Init() - that runs BEFORE MG_Backend::Init(),
// so there is no backend to query yet. Re-captured whenever the active backend
// object changes, which also rolls the fingerprint and therefore invalidates
// every P0b preprocess memo keyed against the old one.
// every P0b preprocess memo keyed against the old one. A backend whose dynamic
// capabilities become available without changing object identity must call
// InvalidateCompileEnv() after publishing them.
// GL thread only.
const SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv>& GetCompileEnv();
void InvalidateCompileEnv();
private:
// State Components
+176 -4
View File
@@ -2104,15 +2104,92 @@ void main() {
float warp = (prefixSum - importance) / sum - float(gl_LocalInvocationID.x + 1u) / float(1024);
outputValues[gl_GlobalInvocationID.x] = warp;
}
)";
}
String MakeWeightedExposureSubgroupReductionShader() {
return R"(#version 460 core
#extension GL_KHR_shader_subgroup_arithmetic : enable
uniform int frameCounter;
uniform float frameTime;
uniform float aspectRatio;
uniform vec2 pixelSize;
uniform sampler2D colortex2;
uniform sampler2D pixelData2D;
layout(local_size_x = 32, local_size_y = 16) in;
layout(rg16f) uniform image2D img_pixelData2D;
shared vec2 prefixSumCache[32];
float remapSaturate(float value, float edge0, float edge1) {
return clamp((value - edge0) / (edge1 - edge0), 0.0f, 1.0f);
}
float GetExposureValue(float luminance) {
return max(luminance, 1.0E-5f);
}
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 = 0; 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 = 0; 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 / 1.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));
}
}
)";
}
} // namespace
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanUsesSharedMemoryAndProducesValidSpirv) {
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanForNarrowSubgroupsProducesValidSpirv) {
using namespace MG_Util::ShaderTranspiler;
String source = MakeLinearSubgroupPrefixScanShader();
ASSERT_TRUE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
ASSERT_TRUE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 8, source));
EXPECT_NE(source.find("shared float prefixSumCache[1024]"), String::npos) << source;
EXPECT_NE(source.find("mglVirtualSubgroupInvocation"), String::npos) << source;
@@ -2121,7 +2198,7 @@ TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanUsesSharedMemoryAndProduc
EXPECT_EQ(source.find("gl_Subgroup"), String::npos) << source;
const String onceRewritten = source;
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 8, source));
EXPECT_EQ(source, onceRewritten);
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
@@ -2150,12 +2227,21 @@ TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanUsesSharedMemoryAndProduc
EXPECT_EQ(spirvText.find("OpGroupNonUniform"), String::npos) << spirvText;
}
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanStillSupportsWideQualcommSubgroups) {
using namespace MG_Util::ShaderTranspiler;
String source = MakeLinearSubgroupPrefixScanShader();
EXPECT_TRUE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
EXPECT_EQ(source.find("subgroupInclusiveAdd"), String::npos) << source;
}
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanRejectsOtherStagesAndSubgroupWidths) {
using namespace MG_Util::ShaderTranspiler;
const String original = MakeLinearSubgroupPrefixScanShader();
for (const auto& [stage, subgroupSize] :
{std::pair{ShaderStage::Compute, Uint32{32}}, std::pair{ShaderStage::Fragment, Uint32{64}},
{std::pair{ShaderStage::Compute, Uint32{0}}, std::pair{ShaderStage::Compute, Uint32{16}},
std::pair{ShaderStage::Compute, Uint32{32}}, std::pair{ShaderStage::Fragment, Uint32{64}},
std::pair{ShaderStage::Compute, Uint32{96}}}) {
String source = original;
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(stage, subgroupSize, source));
@@ -2220,6 +2306,92 @@ TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanRejectsPartialOrUnsafeTem
expectUnchanged(std::move(nvShuffleCall));
}
TEST_F(ProgramUtilTest, RewriteWeightedExposureReductionForEightLaneSubgroupsProducesValidSpirv) {
using namespace MG_Util::ShaderTranspiler;
String source = MakeWeightedExposureSubgroupReductionShader();
ASSERT_TRUE(RewriteWeightedExposureSubgroupReductionForVulkan(ShaderStage::Compute, 8, source));
EXPECT_NE(source.find("mglExposureWeightedSum"), String::npos) << source;
EXPECT_EQ(source.find("subgroupInclusiveAdd"), String::npos) << source;
EXPECT_EQ(source.find("gl_Subgroup"), String::npos) << source;
EXPECT_EQ(source.find("barrier()"), String::npos) << source;
const String onceRewritten = source;
EXPECT_FALSE(RewriteWeightedExposureSubgroupReductionForVulkan(ShaderStage::Compute, 8, source));
EXPECT_EQ(source, onceRewritten);
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
ASSERT_TRUE(shaderResult) << shaderResult.error().log << "\nsource:\n" << source;
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
ASSERT_TRUE(programResult) << programResult.error().log;
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
ASSERT_TRUE(binaryResult) << binaryResult.error().log;
ASSERT_EQ(binaryResult->size(), 1u);
String validationDiagnostics;
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
tools.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) {
validationDiagnostics += message;
validationDiagnostics += '\n';
});
EXPECT_TRUE(tools.Validate(binaryResult->front())) << validationDiagnostics;
String spirvText;
ASSERT_TRUE(tools.Disassemble(binaryResult->front(), &spirvText));
EXPECT_EQ(spirvText.find("OpGroupNonUniform"), String::npos) << spirvText;
}
TEST_F(ProgramUtilTest, RewriteWeightedExposureReductionRejectsOtherWidthsAndUnsafeTemplates) {
using namespace MG_Util::ShaderTranspiler;
const String original = MakeWeightedExposureSubgroupReductionShader();
const auto expectUnchanged = [&](ShaderStage stage, Uint32 subgroupSize, String source) {
const String before = source;
EXPECT_FALSE(RewriteWeightedExposureSubgroupReductionForVulkan(stage, subgroupSize, source));
EXPECT_EQ(source, before);
};
expectUnchanged(ShaderStage::Compute, 0, original);
expectUnchanged(ShaderStage::Compute, 16, original);
expectUnchanged(ShaderStage::Compute, 32, original);
expectUnchanged(ShaderStage::Fragment, 8, original);
String wrongScratchSize = original;
wrongScratchSize.replace(wrongScratchSize.find("prefixSumCache[32]"), std::strlen("prefixSumCache[32]"),
"prefixSumCache[64]");
expectUnchanged(ShaderStage::Compute, 8, std::move(wrongScratchSize));
String changedWeighting = original;
changedWeighting.replace(changedWeighting.find("29.3f"), std::strlen("29.3f"), "30.0f");
expectUnchanged(ShaderStage::Compute, 8, std::move(changedWeighting));
String extraSubgroupUse = original;
extraSubgroupUse.insert(extraSubgroupUse.find("void main()"),
"float extraSubgroupUse(float value) { return subgroupAdd(value); }\n");
expectUnchanged(ShaderStage::Compute, 8, std::move(extraSubgroupUse));
}
TEST_F(ProgramUtilTest, NarrowSubgroupQuirkRunsForDirectVulkanWithoutVendorSpoofing) {
using namespace MG_Util::ShaderTranspiler;
CompileEnv env;
env.backend = BackendType::DirectVulkan;
env.params.SubgroupSize = 8;
env.params.GpuVendor = MG_Backend::GpuVendorKind::Unknown;
env.subgroupPrefixScanQuirk = MG_Config::QuirkOverride::Auto;
String source = MakeWeightedExposureSubgroupReductionShader();
PreprocessShaderSource(ShaderStage::Compute, source, env);
EXPECT_NE(source.find("mglExposureWeightedSum"), String::npos) << source;
EXPECT_EQ(source.find("subgroupInclusiveAdd"), String::npos) << source;
}
// The LEXICAL half must fire at the source level (before the parse) for the
// preempt-list names - the end-to-end ESSL tests cannot tell which half did the
// rename, and for these names the parse would fail without the source rewrite.
+32
View File
@@ -31,6 +31,7 @@
#include <MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h>
#include <MG_Util/Math/HalfFloat.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
#include <MG_Util/Debug/Log.h>
#include <MG_Util/Types.h>
@@ -710,6 +711,37 @@ TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) {
EXPECT_TRUE(backend.GetDynamicParameters().SubgroupQuadOperationsInAllStages);
}
TEST(DirectVulkanSanity, CapabilityRefreshInvalidatesTheCachedCompileEnvironment) {
using namespace MobileGL;
auto previousContext = Move(MG_State::pGLContext);
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
auto backend = MakeUnique<MG_Backend::DirectVulkan::BackendObject_DirectVulkan>();
auto* backendPtr = backend.get();
MG_Backend::pActiveBackendObject = Move(backend);
const auto before = MG_State::pGLContext->GetCompileEnv();
EXPECT_EQ(before->params.SubgroupSize, 0u);
MG_External::VulkanCapabilities caps;
caps.SupportsShaderSubgroup = true;
caps.SubgroupSize = 8;
caps.SubgroupSupportedStages = VK_SHADER_STAGE_COMPUTE_BIT;
caps.SubgroupSupportedOperations = VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_ARITHMETIC_BIT;
backendPtr->ApplyVulkanCapabilitiesForTesting(caps);
const auto after = MG_State::pGLContext->GetCompileEnv();
EXPECT_NE(after.get(), before.get());
EXPECT_NE(after->fingerprint, before->fingerprint);
EXPECT_EQ(after->backend, BackendType::DirectVulkan);
EXPECT_EQ(after->params.SubgroupSize, 8u);
MG_Backend::pActiveBackendObject = Move(previousBackend);
MG_State::pGLContext = Move(previousContext);
}
TEST(DirectVulkanSanity, KeepsOptionalGpuShaderInt64BranchForVoxyQuadDecode) {
using namespace MobileGL;
@@ -23,6 +23,7 @@
namespace {
using MobileGL::SizeT;
using MobileGL::String;
using MobileGL::Uint32;
using MobileGL::Vector;
bool IsIdentifierChar(char ch) {
@@ -254,6 +255,46 @@ namespace {
return true;
}
SizeT CountTokenSequence(const Vector<CodeToken>& tokens,
std::initializer_list<const char*> expected) {
SizeT count = 0;
for (SizeT position = 0; position < tokens.size(); ++position) {
if (MatchTokenSequence(tokens, position, expected)) {
++count;
}
}
return count;
}
bool FindUniqueTokenSequence(const Vector<CodeToken>& tokens, const Vector<CodeToken>& expected,
SizeT& sourceBegin, SizeT& sourceEnd) {
if (expected.empty() || expected.size() > tokens.size()) {
return false;
}
SizeT matchCount = 0;
for (SizeT position = 0; position + expected.size() <= tokens.size(); ++position) {
bool matches = true;
for (SizeT expectedIndex = 0; expectedIndex < expected.size(); ++expectedIndex) {
if (tokens[position + expectedIndex].text != expected[expectedIndex].text) {
matches = false;
break;
}
}
if (!matches) {
continue;
}
++matchCount;
sourceBegin = tokens[position].begin;
sourceEnd = tokens[position + expected.size() - 1].end;
}
return matchCount == 1;
}
bool IsPowerOfTwo(Uint32 value) {
return value != 0u && (value & (value - 1u)) == 0u;
}
struct LinearPrefixScanMatch {
SizeT sharedArraySizeBegin = 0;
SizeT sharedArraySizeEnd = 0;
@@ -506,6 +547,164 @@ namespace {
return replacement;
}
struct WeightedExposureReductionMatch {
SizeT mainBegin = 0;
SizeT mainEnd = 0;
};
bool ParseWeightedExposureReductionTemplate(const Vector<CodeToken>& tokens,
WeightedExposureReductionMatch& match) {
// IterationRP's exposure pass is a complete, stable shader-pack template. Match the
// whole main body before replacing it: a partial match would be unsafe because the
// replacement deliberately replays the 32x16 sample grid from one invocation.
static const Vector<CodeToken> expectedMain = TokenizeCode(R"glsl(
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 = 0; 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 = 0; 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 / 1.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");
if (!FindUniqueTokenSequence(tokens, expectedMain, match.mainBegin, match.mainEnd) ||
CountTokenSequence(tokens,
{"layout", "(", "local_size_x", "=", "32", ",", "local_size_y", "=", "16",
")", "in", ";"}) != 1 ||
CountTokenSequence(tokens, {"shared", "vec2", "prefixSumCache", "[", "32", "]", ";"}) != 1 ||
CountTokenSequence(tokens,
{"float", "GetExposureValue", "(", "float", "luminance", ")", "{"}) != 1 ||
CountTokenSequence(tokens, {"uniform", "int", "frameCounter", ";"}) != 1 ||
CountTokenSequence(tokens, {"uniform", "float", "frameTime", ";"}) != 1 ||
CountTokenSequence(tokens, {"uniform", "float", "aspectRatio", ";"}) != 1 ||
CountTokenSequence(tokens, {"uniform", "vec2", "pixelSize", ";"}) != 1 ||
CountTokenSequence(tokens, {"uniform", "sampler2D", "colortex2", ";"}) != 1 ||
CountTokenSequence(tokens, {"uniform", "sampler2D", "pixelData2D", ";"}) != 1 ||
CountTokenSequence(tokens,
{"layout", "(", "rg16f", ")", "uniform", "image2D", "img_pixelData2D", ";"}) !=
1) {
return false;
}
// No second user of the scratch array or lane-width-sensitive builtin may survive the
// rewrite. These counts describe the fully matched main body plus its one declaration.
if (CountToken(tokens, "prefixSumCache") != 9 || CountToken(tokens, "GetExposureValue") != 2 ||
CountToken(tokens, "subgroupInclusiveAdd") != 2 ||
CountToken(tokens, "gl_SubgroupInvocationID") != 4 || CountToken(tokens, "gl_SubgroupSize") != 4 ||
CountToken(tokens, "gl_SubgroupID") != 8 || CountToken(tokens, "gl_NumSubgroups") != 2 ||
CountToken(tokens, "gl_LocalInvocationIndex") != 2 || CountToken(tokens, "barrier") != 5 ||
CountToken(tokens, "findMSB") != 1 ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "subgroup", {"subgroupInclusiveAdd"}) ||
HasIdentifierWithPrefixOutsideAllowed(
tokens, "gl_Subgroup",
{"gl_SubgroupInvocationID", "gl_SubgroupSize", "gl_SubgroupID", "gl_NumSubgroups"}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_SubGroup", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_Warp", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_Thread", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_SMID", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "ballot", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "shuffle", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "readInvocation", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "readFirstInvocation", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "anyInvocation", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "allInvocations", {}) ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "mglExposure", {})) {
return false;
}
return true;
}
String BuildWeightedExposureReductionReplacement() {
return R"glsl(void main() {
if (gl_LocalInvocationIndex != 0u) return;
float mglExposureAverage = 0.0f;
for (uint mglExposureY = 0u; mglExposureY < 16u; ++mglExposureY) {
for (uint mglExposureX = 0u; mglExposureX < 32u; ++mglExposureX) {
vec2 mglExposureTexCoord = (vec2(mglExposureX, mglExposureY) + 0.5f) * vec2(1.0f / 32.0f, 1.0f / 16.0f);
vec2 mglExposureSampleCoord = mglExposureTexCoord * (1.0f / 64.0f);
mglExposureSampleCoord.x += (15.0f / 32.0f) + pixelSize.x * 12.0f;
mglExposureAverage += dot(textureLod(colortex2, mglExposureSampleCoord, 0.0f).rgb,
vec3(0.2125f, 0.7154f, 0.0721f));
}
}
mglExposureAverage /= 512.0f;
vec2 mglExposureWeightedSum = vec2(0.0f);
for (uint mglExposureY = 0u; mglExposureY < 16u; ++mglExposureY) {
for (uint mglExposureX = 0u; mglExposureX < 32u; ++mglExposureX) {
vec2 mglExposureTexCoord = (vec2(mglExposureX, mglExposureY) + 0.5f) * vec2(1.0f / 32.0f, 1.0f / 16.0f);
vec2 mglExposureSampleCoord = mglExposureTexCoord * (1.0f / 64.0f);
mglExposureSampleCoord.x += (15.0f / 32.0f) + pixelSize.x * 12.0f;
float mglExposureTile = dot(textureLod(colortex2, mglExposureSampleCoord, 0.0f).rgb,
vec3(0.2125f, 0.7154f, 0.0721f));
vec2 mglExposureDistance = mglExposureTexCoord * 2.0f - 1.0f;
mglExposureDistance.y /= aspectRatio;
float mglExposureWeight = remapSaturate(length(mglExposureDistance), 0.6f, 0.4f);
mglExposureTile = max(7.0E-7f, mglExposureTile);
float mglExposureLumaWeight = mglExposureAverage / mglExposureTile;
mglExposureLumaWeight = pow(mglExposureLumaWeight,
remapSaturate(mglExposureAverage, 0.02f, 0.001f) * 0.4f + 0.2f);
mglExposureWeight *= mglExposureLumaWeight;
mglExposureWeightedSum += vec2(mglExposureTile * mglExposureWeight, mglExposureWeight);
}
}
float avgExposure = max(mglExposureWeightedSum.x / mglExposureWeightedSum.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 / 1.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";
}
void SkipDirectiveWhitespace(const MobileGL::String& source, SizeT& pos, SizeT lineEnd) {
while (pos < lineEnd && std::isspace(static_cast<unsigned char>(source[pos]))) {
pos++;
@@ -1256,15 +1455,12 @@ namespace MobileGL {
Bool RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage stage, Uint32 nativeSubgroupSize,
String& source) {
constexpr Uint32 capturedSubgroupSize = 32;
if (stage != ShaderStage::Compute || nativeSubgroupSize <= capturedSubgroupSize ||
nativeSubgroupSize % capturedSubgroupSize != 0) {
return false;
}
// Vulkan subgroup widths are powers of two. Keep the workaround restricted to
// wider widths which are a power-of-two multiple of the captured 32-lane model.
const Uint32 subgroupScale = nativeSubgroupSize / capturedSubgroupSize;
if ((subgroupScale & (subgroupScale - 1u)) != 0u) {
const Bool narrowSubgroup = nativeSubgroupSize != 0u && nativeSubgroupSize < 16u &&
capturedSubgroupSize % nativeSubgroupSize == 0u;
const Bool wideSubgroup = nativeSubgroupSize > capturedSubgroupSize &&
nativeSubgroupSize % capturedSubgroupSize == 0u;
if (stage != ShaderStage::Compute || !IsPowerOfTwo(nativeSubgroupSize) ||
(!narrowSubgroup && !wideSubgroup)) {
return false;
}
@@ -1276,8 +1472,8 @@ namespace MobileGL {
// silently falls back to the driver's miscompiled path. Make that visible.
if (CountToken(tokens, "subgroupInclusiveAdd") > 0) {
MGLOG_W_ONCE("%s: subgroupInclusiveAdd present but the linear prefix-scan template "
"did not match; the wide-subgroup rewrite was NOT applied",
__func__);
"did not match; the subgroup-compatibility rewrite was NOT applied",
__func__);
}
return false;
}
@@ -1291,6 +1487,30 @@ namespace MobileGL {
return true;
}
Bool RewriteWeightedExposureSubgroupReductionForVulkan(ShaderStage stage, Uint32 nativeSubgroupSize,
String& source) {
if (stage != ShaderStage::Compute || !IsPowerOfTwo(nativeSubgroupSize) ||
nativeSubgroupSize >= 16u) {
return false;
}
const Vector<CodeToken> tokens = TokenizeCode(source);
WeightedExposureReductionMatch match;
if (!ParseWeightedExposureReductionTemplate(tokens, match)) {
if (CountToken(tokens, "subgroupInclusiveAdd") == 2 &&
CountToken(tokens, "GetExposureValue") > 0) {
MGLOG_W_ONCE("%s: weighted exposure subgroup reductions were present but the complete "
"template did not match; the narrow-subgroup rewrite was NOT applied",
__func__);
}
return false;
}
source.replace(match.mainBegin, match.mainEnd - match.mainBegin,
BuildWeightedExposureReductionReplacement());
return true;
}
namespace {
struct ShaderSourceQuirkContext {
ShaderStage stage = ShaderStage::Unknown;
@@ -1320,16 +1540,20 @@ namespace MobileGL {
"subgroup-prefix-scan-rewrite",
[](const CompileEnv& env) { return env.subgroupPrefixScanQuirk; },
[](const ShaderSourceQuirkContext& ctx) {
// Qualcomm's Vulkan driver miscompiles the recognized float
// InclusiveScan pattern for native subgroups wider than the
// captured 32 lanes; other vendors compile it correctly and
// should keep their native scan.
// Narrow subgroups overflow the pack's fixed subgroup-result scratch
// arrays. Qualcomm also miscompiles the recognized float InclusiveScan
// pattern when its native subgroup is wider than the captured 32 lanes.
return ctx.backend == BackendType::DirectVulkan &&
ctx.vendor == MG_Backend::GpuVendorKind::Qualcomm;
((ctx.subgroupSize != 0u && ctx.subgroupSize < 16u) ||
(ctx.vendor == MG_Backend::GpuVendorKind::Qualcomm &&
ctx.subgroupSize > 32u));
},
[](const ShaderSourceQuirkContext& ctx, String& source) {
return RewriteLinearSubgroupPrefixScanForVulkan(ctx.stage, ctx.subgroupSize,
source);
const Bool exposureRewritten = RewriteWeightedExposureSubgroupReductionForVulkan(
ctx.stage, ctx.subgroupSize, source);
const Bool prefixScanRewritten = RewriteLinearSubgroupPrefixScanForVulkan(
ctx.stage, ctx.subgroupSize, source);
return exposureRewritten || prefixScanRewritten;
},
},
};
@@ -30,17 +30,16 @@ namespace MobileGL {
// tests and diagnostics that drive the preprocessor standalone.
void PreprocessShaderSource(ShaderStage stage, String& source);
// Some desktop-captured compute shaders build a workgroup-wide linear prefix scan
// from subgroupInclusiveAdd plus a shared array of subgroup totals. Qualcomm's
// Vulkan driver miscompiles that exact float InclusiveScan path for native subgroups
// wider than the capture's 32 lanes. For the narrowly recognized, uniform-control-
// flow template, replace the subgroup-local scan with a shared-memory, strict
// left-fold over virtual 32-lane segments. Returns true only when the complete safe
// template was recognized and rewritten. PreprocessShaderSource reaches this through
// its device-quirk registry: by default only on detected Qualcomm Vulkan devices,
// overridable either way with MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN=1/0. The explicit
// entry point exists for deterministic tests.
// Some desktop-captured compute shaders size shared scratch for a 32-lane subgroup
// model. Narrow Vulkan subgroups can produce more subgroup totals than that storage
// holds; Qualcomm also miscompiles one recognized scan when its subgroup is wider.
// These entry points replace only complete, known-safe templates with lane-independent
// algorithms. PreprocessShaderSource reaches them through its device-quirk registry;
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN=1/0 overrides the automatic device gate. The
// explicit entry points exist for deterministic tests.
Bool RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage stage, Uint32 nativeSubgroupSize, String& source);
Bool RewriteWeightedExposureSubgroupReductionForVulkan(ShaderStage stage, Uint32 nativeSubgroupSize,
String& source);
// Rewrites a "#version 330 core" directive that PreprocessShaderSource normalized down
// from a legacy desktop version back up to "#version 460 core". Returns false (leaving