From 09b315197b0b534b9511a4cf204d54fcad96d7b3 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 12 Oct 2026 08:19:17 -0400 Subject: [PATCH] [Fix] (Espryt): bound usampler2DMS fetches by the bound texture's real sample count --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 13 +++ MobileGL/MG_Backend/DirectGLES/Managers.cpp | 42 ++++++++- MobileGL/MG_Backend/DirectGLES/Managers.h | 11 +++ MobileGL/MG_Backend/DirectGLES/Utils.cpp | 68 +++++++++++++++ MobileGL/MG_Backend/DirectGLES/Utils.h | 15 ++++ .../Backend/DirectGLES/EsslShaderPassTest.cpp | 85 +++++++++++++++++++ .../ClampMultisampleFetchTest.cpp | 64 ++++++++++++++ .../ShaderTranspiler/ShaderCompiler.cpp | 6 +- .../MG_Util/ShaderTranspiler/ShaderCompiler.h | 6 +- .../SpirvPasses/ClampMultisampleFetchPass.cpp | 48 ++++++++++- .../SpirvPasses/ClampMultisampleFetchPass.h | 23 ++++- 11 files changed, 371 insertions(+), 10 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index a0ed4ab6..4e7aa868 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -5757,6 +5757,19 @@ namespace MobileGL::MG_Backend::DirectGLES { } } + // The last sample a usampler2DMS fetch may name, from the texture + // actually bound: a stencil texture's real count is not an integer + // colour texture's. Keyed like the bias above - a texture's storage or + // a unit's binding moving is what invalidates this pass's memo. + if (samplerBinding.msSampleMaxLocation >= 0) { + const Int sampleMax = + std::max(PipeTextureBackendSampleCountForHandle(view.Texture), 1) - 1; + if (sampleMax != samplerBinding.lastAssignedMsSampleMax) { + g_GLESFuncs.glUniform1i(samplerBinding.msSampleMaxLocation, sampleMax); + samplerBinding.lastAssignedMsSampleMax = sampleMax; + } + } + const auto* viewRecord = PipeSamplerViewRecordForHandle(view.View); const Bool sampledIsTexture2D = viewRecord != nullptr && diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 35abb59f..84c5a794 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -146,6 +146,20 @@ namespace MobileGL::MG_Backend::DirectGLES { // kFrontendMaxSamples, which is the source of truth - keep the two in step. constexpr Int kFrontendMaxSamples = 4; + // Whether a usampler2DMS fetch's sample index is bounded at RUNTIME by the bound texture's + // real sample count rather than at compile time by the integer ceiling: armed when that + // ceiling is squeezed below what MobileGL advertises AND the depth ceiling - which the + // stencil aspect of a multisample depth/stencil texture is allocated under, and which a + // usampler reads too - exceeds it, so no one static bound is right for both. Read on both + // sides of the translation-memo boundary (the SPIR-V pass defers, the ESSL pass takes + // over); it is a function of three values the memo key already carries, so the two halves + // cannot disagree about one program. + static Bool ClampsUnsignedMultisampleFetchesAtRuntime() { + const Int advertisedMaxSamples = std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples); + return g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples && + g_GLESCapabilities.MaxDepthTextureSamples > g_GLESCapabilities.MaxIntegerSamples; + } + static Uint ResolveBackendEsslVersion() { const auto& version = g_GLESCapabilities.GLESVersion; if (version.Major > 3 || (version.Major == 3 && version.Minor >= 2)) { @@ -4502,6 +4516,21 @@ namespace MobileGL::MG_Backend::DirectGLES { return MobileGL::TextureTarget::Unknown; } + Int PipeTextureBackendSampleCountForHandle(MG_Pipe::MGPipeHandle res) { + const auto* record = PipeTextureRecordForHandle(res); + if (record == nullptr) return 0; + const MobileGL::TextureTarget target = PipeTextureTargetForHandle(res); + if (!TextureImpl::IsMultisampleTextureTarget(target)) return 0; + // The same three inputs the storage allocation clamps with (SyncToBackend's multisample + // arm): the record's format and requested count, and the format's ES transfer format, + // which only decides the integer-vs-colour fallback when the probe has no answer. + const auto internalFormat = static_cast(record->Desc.InternalFormat); + GLenum glInternalFormat = 0, glFormat = 0, glType = 0; + TextureImpl::GenerateTextureFormatInfo(internalFormat, &glInternalFormat, &glFormat, &glType, target); + return ClampSamplesToBackendSupport(GetFormatCapabilityTargetIndex(target), internalFormat, glFormat, + static_cast(record->Desc.Samples)); + } + const MG_Pipe::MGPipeSamplerCsoRecord* PipeSamplerCsoRecordForHandle(MG_Pipe::MGPipeHandle cso) { return PipeRecordAt(MG_Pipe::MGPipeApplier().SamplerCsos, cso); } @@ -11694,7 +11723,7 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESCapabilities.MaxColorTextureSamples, g_GLESCapabilities.MaxIntegerSamples, g_GLESCapabilities.MaxDepthTextureSamples, advertisedMaxSamples, - enableSpirvValidation) && + enableSpirvValidation, ClampsUnsignedMultisampleFetchesAtRuntime()) && !clampedSampleSpirv.empty()) { effectiveSpirv = &clampedSampleSpirv; } @@ -12915,6 +12944,9 @@ namespace MobileGL::MG_Backend::DirectGLES { source = ForceFlatIntegerVaryings(source, glShaderType); source = BroadcastLegacyFragColor(std::move(source), glShaderType, m_fragColorBroadcastCount); source = EmulateTextureLodBias(source, ShouldAvoidExplicitLodBiasOnAngleLlvmpipe()); + if (ClampsUnsignedMultisampleFetchesAtRuntime()) { + source = ClampUnsignedMultisampleFetches(source); + } source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType); source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType); // The two halves of the gl_ViewportIndex routing emulation, next to the draw- @@ -13440,6 +13472,14 @@ namespace MobileGL::MG_Backend::DirectGLES { binding.lodBiasLocation = g_GLESFuncs.glGetUniformLocation( m_backendProgramId, (String(LOD_BIAS_UNIFORM_PREFIX) + elementName).c_str()); binding.lastAssignedLodBias = 0.0f; + // Present only for the unsigned multisample samplers ClampUnsignedMultisampleFetches + // rewrote, named the same way. + if (uniformType == GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE || + uniformType == GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY) { + binding.msSampleMaxLocation = g_GLESFuncs.glGetUniformLocation( + m_backendProgramId, (String(MS_SAMPLE_MAX_UNIFORM_PREFIX) + elementName).c_str()); + } + binding.lastAssignedMsSampleMax = 0; m_samplerUniformBindings.push_back(binding); } } diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index c80815fb..5fa36ce9 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -644,6 +644,12 @@ namespace MobileGL::MG_Backend::DirectGLES { // every caller treats as "decline", never as a default. MobileGL::TextureTarget PipeTextureTargetForHandle(MG_Pipe::MGPipeHandle res); + // The sample count the backend really allocates for the multisample texture this handle + // names - the requested count clamped by ClampSamplesToBackendSupport, exactly as the + // storage allocation clamps it - or 0 when the handle names no live multisample texture. + // What a usampler2DMS fetch's runtime bound (ClampUnsignedMultisampleFetches) is set from. + Int PipeTextureBackendSampleCountForHandle(MG_Pipe::MGPipeHandle res); + // The pending-upload entry the applier accumulated for this (uploadTarget, level) of this // texture record, or null (D-D5). SERVER-SIDE STATE, and that is the whole point: the // client clears its own dirty flags at EMISSION for the levels the applier accepted, while @@ -2774,6 +2780,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // so an unbiased shader issues no per-draw glUniform1f at all. Int lodBiasLocation = -1; Float lastAssignedLodBias = 0.0f; + // Location of this sampler's runtime sample-index bound + // (PrgramImpl::ClampUnsignedMultisampleFetches), -1 when the shader has none; + // lastAssignedMsSampleMax mirrors the value the program holds (0 after link). + Int msSampleMaxLocation = -1; + Int lastAssignedMsSampleMax = 0; }; // Memo of the whole per-draw sampler-uniform pass (glUniform1i unit diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index d378b961..e73a27c8 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -2057,6 +2057,74 @@ namespace MobileGL::MG_Backend::DirectGLES { return result; } + String ClampUnsignedMultisampleFetches(const String& glslCode) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + if (glslCode.find("usampler2DMS") == String::npos || glslCode.find("texelFetch") == String::npos) { + return glslCode; + } + + // The non-arrayed unsigned multisample samplers - the ones ClampMultisampleFetchPass + // deferred, and the only ones a lookup can name directly as its first argument. + static const std::regex declRegex( + R"(uniform\s+(?:(?:highp|mediump|lowp)\s+)?usampler2DMS(?:Array)?\s+([A-Za-z_][A-Za-z0-9_]*)\s*;)"); + UnorderedMap clampNames; // sampler name -> sample-max uniform name + Vector> declEnds; // where each declaration ends, and its uniform + for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), declRegex), end; it != end; ++it) { + const String name = (*it)[1].str(); + const String clampName = String(MS_SAMPLE_MAX_UNIFORM_PREFIX) + name; + if (clampNames.emplace(name, clampName).second) { + declEnds.emplace_back(static_cast(it->position(0) + it->length(0)), clampName); + } + } + if (clampNames.empty()) { + return glslCode; + } + + // Right-to-left, so an earlier call's offsets survive the insertions behind it. + constexpr const char* kFetch = "texelFetch"; + const SizeT fetchLength = std::strlen(kFetch); + String result = glslCode; + Vector usedClamps; + for (SizeT scan = result.rfind(kFetch); scan != String::npos; + scan = scan == 0 ? String::npos : result.rfind(kFetch, scan - 1)) { + if (scan > 0 && IsIdentifierChar(result[scan - 1])) continue; + const SizeT openParen = result.find_first_not_of(" \t", scan + fetchLength); + if (openParen == String::npos || result[openParen] != '(') continue; // texelFetchOffset etc. + const Vector marks = SplitCallArguments(result, openParen); + if (marks.size() != 3) continue; // (sampler, coord, sample) is the multisample form + const SizeT firstStart = result.find_first_not_of(" \t", openParen + 1); + SizeT firstEnd = marks[0]; + while (firstEnd > firstStart && (result[firstEnd - 1] == ' ' || result[firstEnd - 1] == '\t')) { + --firstEnd; + } + if (firstStart == String::npos || firstEnd <= firstStart) continue; + const auto found = clampNames.find(result.substr(firstStart, firstEnd - firstStart)); + if (found == clampNames.end()) continue; + const SizeT sampleStart = result.find_first_not_of(" \t\n", marks[1] + 1); + if (sampleStart == String::npos || sampleStart >= marks[2]) continue; + const SizeT sampleEnd = marks[2]; + result.insert(sampleEnd, String(", ") + found->second + ")"); + result.insert(sampleStart, "min("); + if (std::find(usedClamps.begin(), usedClamps.end(), found->second) == usedClamps.end()) { + usedClamps.push_back(found->second); + } + } + if (usedClamps.empty()) { + return glslCode; + } + + // Declared right after the sampler they bound, back to front so the recorded + // offsets (taken from the unedited text) stay valid: every fetch sits after the + // declarations in the emitted ESSL, so no insertion above has moved them. + for (auto it = declEnds.rbegin(); it != declEnds.rend(); ++it) { + if (std::find(usedClamps.begin(), usedClamps.end(), it->second) == usedClamps.end()) continue; + result.insert(it->first, String("\nuniform highp int ") + it->second + ";"); + } + return result; + } + } // namespace PrgramImpl namespace Utils { diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index 50887312..6248c4e3 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -576,6 +576,21 @@ namespace MobileGL::MG_Backend::DirectGLES { // Off by default and only ever set on ANGLE + llvmpipe, where injecting the uniform // into a constant LOD crashes the driver (MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS). String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false); + // Prefix of the per-sampler int uniform that carries the bound multisample texture's + // last real sample index into the shader (see ClampUnsignedMultisampleFetches); the + // suffix is the sampler's own name. + constexpr const char* MS_SAMPLE_MAX_UNIFORM_PREFIX = "mg_msSampleMax_"; + // The runtime half of ClampMultisampleFetchPass's deferDirectUnsignedFetches. A + // usampler2DMS[Array] reads integer colour textures, which the backend may allocate with + // fewer samples than MobileGL advertises, AND the stencil aspect of multisample + // depth/stencil textures, which it allocates under the (larger) depth ceiling - so no + // single compile-time bound is right for both. Declares one + // `uniform highp int mg_msSampleMax_;` per non-arrayed unsigned multisample + // sampler and wraps the sample argument of every texelFetch that names it in + // min(sample, that uniform). Draws push the bound texture's real sample count - 1 into + // it (0, the program's initial value, when nothing multisampled is bound). Returns the + // source unchanged when there is nothing to rewrite. + String ClampUnsignedMultisampleFetches(const String& glslCode); } // namespace PrgramImpl namespace Utils { diff --git a/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp b/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp index 6fe0c24b..938fce30 100644 --- a/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp +++ b/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp @@ -25,6 +25,7 @@ using namespace MobileGL; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BakeImageFormatQualifiers; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BuildPassthroughTessControlEssl; +using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ClampUnsignedMultisampleFetches; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ExtractPerVertexBlockMembers; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ForceFlatIntegerVaryings; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_ARRAY_ELEMENT_PREFIX; @@ -1763,3 +1764,87 @@ void main() { gl_Position = gl_in[0].gl_Position; EmitVertex(); } EXPECT_EQ(RedeclareClipCullDistanceInputsInPerVertexBlock(readsPointSize, GL_VERTEX_SHADER, producer), readsPointSize); } + +// The runtime half of the deferred usampler2DMS clamp: the sample argument of every fetch that +// names an unsigned multisample uniform is bounded by that sampler's own uniform, declared right +// after it. A float multisample sampler and a non-multisample one are none of its business. +TEST(ClampUnsignedMultisampleFetchesTest, BoundsTheSampleOfEveryUnsignedMultisampleFetch) { + const String essl = "#version 320 es\n" + "precision highp float;\n" + "layout(binding = 0) uniform highp usampler2DMS stencilTex;\n" + "uniform highp sampler2DMS colorMs;\n" + "uniform highp usampler2D plain;\n" + "layout(location = 0) out highp vec4 color;\n" + "void main() {\n" + " uint a = texelFetch(stencilTex, ivec2(0), 3).x;\n" + " uint b = texelFetch(stencilTex, ivec2(0), int(gl_FragCoord.x)).x;\n" + " vec4 c = texelFetch(colorMs, ivec2(0), 3);\n" + " uint d = texelFetch(plain, ivec2(0), 0).x;\n" + " color = vec4(float(a), float(b), c.x, float(d));\n" + "}\n"; + const String out = ClampUnsignedMultisampleFetches(essl); + EXPECT_TRUE(Contains(out, "texelFetch(stencilTex, ivec2(0), min(3, mg_msSampleMax_stencilTex))")) << out; + EXPECT_TRUE(Contains(out, "texelFetch(stencilTex, ivec2(0), min(int(gl_FragCoord.x), mg_msSampleMax_stencilTex))")) + << out; + EXPECT_TRUE(Contains(out, "uniform highp usampler2DMS stencilTex;\nuniform highp int mg_msSampleMax_stencilTex;")) + << out; + EXPECT_EQ(CountOf(out, "uniform highp int mg_msSampleMax_"), 1u) << out; + EXPECT_EQ(CountOf(out, "min("), 2u) << out; + + const String untouched = "#version 320 es\nuniform highp sampler2DMS colorMs;\n" + "void main() { highp vec4 c = texelFetch(colorMs, ivec2(0), 3); }\n"; + EXPECT_EQ(ClampUnsignedMultisampleFetches(untouched), untouched); +} + +// End to end, the way the program build runs it: the SPIR-V pass defers the direct usampler2DMS +// fetches of KHR-GL4x.texture_stencil8.multisample's checker, SPIRV-Cross prints them with their +// literal samples intact, and the text pass bounds each one by the runtime uniform. +TEST(ClampUnsignedMultisampleFetchesTest, BoundsTheStencilCheckerAfterTheDeferredSpirvClamp) { + using namespace MG_Util::ShaderTranspiler; + MobileGL::Initialize(); + const char* vertexSource = R"(#version 330 core +void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); } +)"; + const char* fragmentSource = R"(#version 330 core +uniform usampler2DMS stencilTex; +out vec4 color; +void main() { + uint s0 = texelFetch(stencilTex, ivec2(0), 0).r; + uint s1 = texelFetch(stencilTex, ivec2(0), 1).r; + uint s2 = texelFetch(stencilTex, ivec2(0), 2).r; + uint s3 = texelFetch(stencilTex, ivec2(0), 3).r; + color = vec4(float(s0), float(s1), float(s2), float(s3)) / 255.0; +} +)"; + Vector> shaders; + const Vector> stages{{GL_VERTEX_SHADER, vertexSource}, + {GL_FRAGMENT_SHADER, fragmentSource}}; + for (const auto& [stage, text] : stages) { + ShaderAttrib attrib{.shaderType = stage, .sourceStr = text}; + auto compiled = ShaderCompiler::CompileShader(attrib); + ASSERT_TRUE(compiled) << compiled.error().log; + shaders.push_back(compiled.value()); + } + ProgramAttrib programAttrib{.shaders = shaders}; + auto program = ShaderCompiler::LinkProgram(programAttrib); + ASSERT_TRUE(program) << program.error().log; + ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_VERTEX_SHADER, GL_FRAGMENT_SHADER}, + .program = *program.value()}; + auto modules = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib); + ASSERT_TRUE(modules) << modules.error().log; + ASSERT_EQ(modules.value().size(), 2u); + + Vector deferred; + ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(modules.value()[1], deferred, 4, 1, 4, 4, true, + /*deferDirectUnsignedFetches=*/true)); + const Vector& fragmentModule = deferred.empty() ? modules.value()[1] : deferred; + SpvcSession session(fragmentModule, SessionUsageBit::Transpile); + auto essl = ShaderCompiler::DecompileShader(session); + ASSERT_TRUE(essl) << essl.error().log; + + const String out = ClampUnsignedMultisampleFetches(essl.value()); + for (Int sample = 1; sample <= 3; ++sample) { + EXPECT_TRUE(Contains(out, "min(" + std::to_string(sample) + ", mg_msSampleMax_stencilTex)")) << out; + } + EXPECT_EQ(CountOf(out, "uniform highp int mg_msSampleMax_stencilTex;"), 1u) << out; +} diff --git a/MobileGL/MG_Test/ShaderTranspiler/ClampMultisampleFetchTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/ClampMultisampleFetchTest.cpp index 4c538317..544066a8 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/ClampMultisampleFetchTest.cpp +++ b/MobileGL/MG_Test/ShaderTranspiler/ClampMultisampleFetchTest.cpp @@ -381,3 +381,67 @@ TEST_F(ClampMultisampleFetchTest, ClampsAColorSampleWithAMinimum) { ASSERT_EQ(constants.count(minimum->operandIds[1]), 1u) << dis; EXPECT_EQ(constants.at(minimum->operandIds[1]), 1u) << dis; } + +namespace { + // KHR-GL4x.texture_stencil8.multisample in miniature: a four-sample STENCIL_INDEX8 texture + // read through a usampler2DMS with literal sample indices. Beside it, the same sampler + // reached through a function parameter and a signed integer sampler - neither of which the + // runtime bound can name, so both must keep the static integer clamp. + const char* const kStencilMultisampleFetch = R"(#version 410 core +uniform usampler2DMS stencilTex; +uniform isampler2DMS signedMs; +out vec4 fragColor; +uint fetchThrough(usampler2DMS s, int i) { return texelFetch(s, ivec2(0), i).r; } +void main() { + uint direct = texelFetch(stencilTex, ivec2(0), 3).r; + uint viaParameter = fetchThrough(stencilTex, 3); + int signedValue = texelFetch(signedMs, ivec2(0), 3).r; + fragColor = vec4(float(direct), float(viaParameter), float(signedValue), 1.0); +} +)"; + + // How many Sample operands of the module's multisample fetches are the constant `value`. + SizeT CountSampleOperandsEqualTo(const Vector& spirv, Uint32 value) { + const std::map constants = CollectScalarConstants(spirv); + SizeT count = 0; + for (const ImageFetch& fetch : CollectImageFetches(spirv)) { + if ((fetch.mask & static_cast(spv::ImageOperandsMask::Sample)) == 0u) continue; + const auto constant = constants.find(fetch.maskOperandIds.back()); + if (constant != constants.end() && constant->second == value) ++count; + } + return count; + } +} // namespace + +// Integer colour squeezed to one sample but depth/stencil at four (Adreno): a usampler2DMS fetch +// straight from its uniform is handed to the runtime bound and keeps sample 3 - a stencil texture +// really has it - while the fetch through a parameter and the signed one still read sample 0. +TEST_F(ClampMultisampleFetchTest, DefersADirectUnsignedFetchToTheRuntimeBound) { + const Vector input = CompileFragment(kStencilMultisampleFetch); + ASSERT_FALSE(input.empty()); + + Vector output; + ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl( + input, output, /*maxColorSamples=*/4, /*maxIntegerSamples=*/1, /*maxDepthSamples=*/4, + kAdvertisedMaxSamples, true, /*deferDirectUnsignedFetches=*/true)); + ASSERT_FALSE(output.empty()); + const String dis = Disassemble(output); + ASSERT_TRUE(Validates(output)) << dis; + EXPECT_EQ(CountSampleOperandsEqualTo(output, 3u), 1u) << dis; + EXPECT_EQ(CountSampleOperandsEqualTo(output, 0u), 2u) << dis; +} + +// Without the runtime bound nothing is deferred: the pre-existing static clamp, all three to 0. +TEST_F(ClampMultisampleFetchTest, WithoutTheRuntimeBoundEveryIntegerFetchKeepsTheStaticClamp) { + const Vector input = CompileFragment(kStencilMultisampleFetch); + ASSERT_FALSE(input.empty()); + + Vector output; + ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl( + input, output, /*maxColorSamples=*/4, /*maxIntegerSamples=*/1, /*maxDepthSamples=*/4, + kAdvertisedMaxSamples, true)); + ASSERT_FALSE(output.empty()); + const String dis = Disassemble(output); + EXPECT_EQ(CountSampleOperandsEqualTo(output, 3u), 0u) << dis; + EXPECT_EQ(CountSampleOperandsEqualTo(output, 0u), 3u) << dis; +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index b3c0f85f..ae2579f9 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -1461,11 +1461,13 @@ namespace MobileGL { const Int32 maxIntegerSamples, const Int32 maxDepthSamples, const Int32 advertisedMaxSamples, - const bool enableSpirvValidation) { + const bool enableSpirvValidation, + const bool deferDirectUnsignedFetches) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(ClampMultisampleFetchPass::CreateClampMultisampleFetchPass( - maxColorSamples, maxIntegerSamples, maxDepthSamples, advertisedMaxSamples)); + maxColorSamples, maxIntegerSamples, maxDepthSamples, advertisedMaxSamples, + deferDirectUnsignedFetches)); return RunOptimizerChecked("ClampMultisampleFetchesForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index f8c06643..3ff5de0e 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -78,14 +78,16 @@ namespace MobileGL { // end of a one-sample allocation. Pass the backend-real per-category ceilings and // the advertised maximum (GL_Getter's GetAdvertisedMaxSamples); a category that // already reaches the advertised value is left alone. DirectGLES transpile path - // only. See ClampMultisampleFetchPass. + // only. See ClampMultisampleFetchPass, which also says what + // deferDirectUnsignedFetches hands over to the caller's runtime clamp. static bool ClampMultisampleFetchesForEssl(const Vector& inputBinary, Vector& outputBinary, Int32 maxColorSamples, Int32 maxIntegerSamples, Int32 maxDepthSamples, Int32 advertisedMaxSamples, - bool enableSpirvValidation = false); + bool enableSpirvValidation = false, + bool deferDirectUnsignedFetches = false); // Whether the module declares any multisampled image type, i.e. whether the pass // above has anything to do. The gate that keeps every other stage off an // optimizer round trip it does not need. diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp index 21bb30bb..eea3b38d 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp @@ -21,6 +21,7 @@ #include "source/util/make_unique.h" #include "source/util/string_utils.h" +#include #include #include @@ -120,6 +121,39 @@ namespace MobileGL { return SampleCategory::Color; } + // An unsigned integer sampled type: the usampler* family, which is what reads the + // stencil aspect of a multisample texture as well as integer colour. + bool HasUnsignedIntegerSampledType(IRContext* context, const Instruction* imageType) { + const Instruction* sampledType = + context->get_def_use_mgr()->GetDef(imageType->GetSingleWordInOperand(kSampledTypeOperand)); + return sampledType != nullptr && sampledType->opcode() == spv::Op::OpTypeInt && + sampledType->NumInOperands() > 1 && sampledType->GetSingleWordInOperand(1) == 0u; + } + + // Whether the image an operation reads is loaded straight from a non-arrayed + // UniformConstant global - through at most an OpImage of the loaded sampled image + // - i.e. whether the ESSL will name that global's uniform as the lookup's first + // argument. + bool ReadsDirectlyFromGlobal(IRContext* context, uint32_t imageId) { + auto* defUseMgr = context->get_def_use_mgr(); + const Instruction* object = defUseMgr->GetDef(imageId); + if (object != nullptr && object->opcode() == spv::Op::OpImage) { + object = defUseMgr->GetDef(object->GetSingleWordInOperand(0)); + } + if (object == nullptr || object->opcode() != spv::Op::OpLoad) return false; + const Instruction* variable = defUseMgr->GetDef(object->GetSingleWordInOperand(0)); + if (variable == nullptr || variable->opcode() != spv::Op::OpVariable || + variable->GetSingleWordInOperand(0) != + static_cast(spv::StorageClass::UniformConstant)) { + return false; + } + const Instruction* pointerType = defUseMgr->GetDef(variable->type_id()); + if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) return false; + const Instruction* pointee = defUseMgr->GetDef(pointerType->GetSingleWordInOperand(1)); + return pointee != nullptr && (pointee->opcode() == spv::Op::OpTypeImage || + pointee->opcode() == spv::Op::OpTypeSampledImage); + } + // Where the Sample id sits among an image operation's in-operands, or false when // the operation carries no Sample at all. // @@ -261,6 +295,15 @@ namespace MobileGL { switch (CategoryOf(irContext, imageType)) { case SampleCategory::Integer: categoryMaxSamples = m_maxIntegerSamples; + // Taken over at runtime by the bound texture's real count (see + // the class comment); here only the larger static ceiling, so a + // stencil texture's samples above the integer one stay readable. + if (m_deferDirectUnsignedFetches && + HasUnsignedIntegerSampledType(irContext, imageType) && + ReadsDirectlyFromGlobal(irContext, + instruction.GetSingleWordInOperand(kImageOperand))) { + categoryMaxSamples = std::max(m_maxIntegerSamples, m_maxDepthSamples); + } break; case SampleCategory::Depth: categoryMaxSamples = m_maxDepthSamples; @@ -369,9 +412,10 @@ namespace MobileGL { spvtools::Optimizer::PassToken ClampMultisampleFetchPass::CreateClampMultisampleFetchPass( const Int32 maxColorSamples, const Int32 maxIntegerSamples, const Int32 maxDepthSamples, - const Int32 advertisedMaxSamples) { + const Int32 advertisedMaxSamples, const bool deferDirectUnsignedFetches) { return spvtools::Optimizer::PassToken(spvtools::MakeUnique( - maxColorSamples, maxIntegerSamples, maxDepthSamples, advertisedMaxSamples)); + maxColorSamples, maxIntegerSamples, maxDepthSamples, advertisedMaxSamples, + deferDirectUnsignedFetches)); } } // namespace ShaderTranspiler } // namespace MG_Util diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.h index 8d4fb722..18b07092 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.h +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.h @@ -51,6 +51,20 @@ namespace MobileGL { // image and everything else falls to the colour limit - which is the one a // mis-classified image would want anyway. // + // An UNSIGNED integer image is the one category whose static ceiling can be wrong + // in both directions: a usampler2DMS reads integer colour formats AND the stencil + // aspect of a multisample STENCIL_INDEX8 / depth-stencil texture, and the backend + // allocates the latter under the DEPTH ceiling (Adreno: 4 samples where integer + // colour gets 1). Clamping such a fetch to the integer ceiling reads sample 0 for + // every index of a four-sample stencil texture. With deferDirectUnsignedFetches the + // caller takes those fetches over at runtime - a per-sampler uniform holding the + // bound texture's real last sample index (DirectGLES ClampUnsignedMultisampleFetches) + // - and this pass only bounds them by the larger of the two ceilings. "Direct" is a + // fetch whose image is loaded straight from a non-arrayed global, which is exactly + // what reaches the ESSL as `texelFetch(, ...)` for the text pass to + // find; a sampler reached through a function parameter or an array keeps the + // integer clamp. + // // DirectGLES transpile path only. DirectVulkan allocates the sample count it was // asked for and must see the module unchanged. class ClampMultisampleFetchPass : public spvtools::opt::Pass { @@ -59,11 +73,13 @@ namespace MobileGL { // advertises (GL_Getter's GetAdvertisedMaxSamples). A category whose real ceiling // already reaches the advertised one is left completely alone. ClampMultisampleFetchPass(Int32 maxColorSamples, Int32 maxIntegerSamples, - Int32 maxDepthSamples, Int32 advertisedMaxSamples) + Int32 maxDepthSamples, Int32 advertisedMaxSamples, + bool deferDirectUnsignedFetches = false) : m_maxColorSamples(maxColorSamples), m_maxIntegerSamples(maxIntegerSamples), m_maxDepthSamples(maxDepthSamples), - m_advertisedMaxSamples(advertisedMaxSamples) {} + m_advertisedMaxSamples(advertisedMaxSamples), + m_deferDirectUnsignedFetches(deferDirectUnsignedFetches) {} const char* name() const override { return "clamp-multisample-fetch"; } Status Process() override; @@ -80,13 +96,14 @@ namespace MobileGL { static spvtools::Optimizer::PassToken CreateClampMultisampleFetchPass( Int32 maxColorSamples, Int32 maxIntegerSamples, Int32 maxDepthSamples, - Int32 advertisedMaxSamples); + Int32 advertisedMaxSamples, bool deferDirectUnsignedFetches = false); private: Int32 m_maxColorSamples; Int32 m_maxIntegerSamples; Int32 m_maxDepthSamples; Int32 m_advertisedMaxSamples; + bool m_deferDirectUnsignedFetches; }; } // namespace ShaderTranspiler } // namespace MG_Util