mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Fix, Test] (ShaderTranspiler, DirectGLES): clamp multisample fetches to the backend's real sample count
This commit is contained in:
@@ -292,6 +292,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||
|
||||
@@ -4923,6 +4923,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str());
|
||||
}
|
||||
|
||||
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so every multisample
|
||||
// ceiling MobileGL advertises is floored to 4 no matter what the ES driver
|
||||
// reports - but the realised allocation cannot be, and
|
||||
// ClampSamplesToBackendSupport quietly gives an integer or depth multisample
|
||||
// texture the ONE sample Adreno and Mali actually support for it. A shader
|
||||
// written against the advertised ceiling then fetches a sample that storage does
|
||||
// not have and reads garbage; KHR-GL33/40/41.texture_swizzle.functional_* and
|
||||
// KHR-GLxx.texture_size_promotion.functional bake exactly that literal in. Clamp
|
||||
// the Sample operand to the backend-real per-category maximum so the fetch lands
|
||||
// inside the allocation. Gated on some category actually being squeezed AND the
|
||||
// module actually declaring a multisampled image, so no other stage pays an
|
||||
// optimizer round trip for it. DirectVulkan is deliberately not given this: it
|
||||
// allocates the sample count it was asked for, so its modules are already right.
|
||||
Vector<unsigned int> clampedSampleSpirv;
|
||||
{
|
||||
// Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples():
|
||||
// this is backend code and must not reach into the GL frontend. 4 is that
|
||||
// translation unit's kFrontendMaxSamples, which is the source of truth -
|
||||
// keep the two in step.
|
||||
constexpr Int kFrontendMaxSamples = 4;
|
||||
const Int advertisedMaxSamples =
|
||||
std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
|
||||
if ((g_GLESCapabilities.MaxColorTextureSamples < advertisedMaxSamples ||
|
||||
g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples ||
|
||||
g_GLESCapabilities.MaxDepthTextureSamples < advertisedMaxSamples) &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresMultisampledImage(
|
||||
*effectiveSpirv) &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::ClampMultisampleFetchesForEssl(
|
||||
*effectiveSpirv, clampedSampleSpirv,
|
||||
g_GLESCapabilities.MaxColorTextureSamples,
|
||||
g_GLESCapabilities.MaxIntegerSamples,
|
||||
g_GLESCapabilities.MaxDepthTextureSamples, advertisedMaxSamples,
|
||||
enableSpirvValidation) &&
|
||||
!clampedSampleSpirv.empty()) {
|
||||
effectiveSpirv = &clampedSampleSpirv;
|
||||
}
|
||||
}
|
||||
|
||||
// GLSL ES has no ARRAY vertex inputs, and SPIRV-Cross refuses the whole module
|
||||
// rather than emulating them, so this has to happen before it sees the binary.
|
||||
Vector<unsigned int> splitArrayInputSpirv;
|
||||
|
||||
@@ -10,6 +10,7 @@ add_executable(
|
||||
DemoteFloat64Test.cpp
|
||||
FlattenXfbInterfaceBlocksTest.cpp
|
||||
LowerViewportIndexTest.cpp
|
||||
ClampMultisampleFetchTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(SpirvPassTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/ClampMultisampleFetchTest.cpp
|
||||
// Copyright (c) 2025-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
|
||||
//
|
||||
// ClampMultisampleFetchPass exists because MobileGL advertises one multisample ceiling and the ES
|
||||
// driver underneath delivers another. GL 4.6 core table 23.53 forces GL_MAX_SAMPLES and
|
||||
// GL_MAX_INTEGER_SAMPLES up to 4; Adreno and Mali back an integer multisample texture with ONE
|
||||
// sample, and DirectGLES quietly allocates that (ClampSamplesToBackendSupport). A CTS shader that
|
||||
// bakes in `texelFetch(usampler2DMS, coord, 3)` - which is what
|
||||
// KHR-GL33/40/41.texture_swizzle.functional_* and KHR-GLxx.texture_size_promotion.functional do -
|
||||
// then reads a sample the storage does not have.
|
||||
//
|
||||
// So what has to hold is per-fetch and per-category at once: the squeezed category's Sample
|
||||
// operand must come back in range, a category that is not squeezed must be untouched, a module
|
||||
// with no multisampled image at all must come out byte for byte as it went in, and every result
|
||||
// must still be a valid module. Real GLSL through the same glslang path the backends use, for the
|
||||
// same reason LowerViewportIndexTest.cpp does it: what matters is what glslang actually emits.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#define SPV_ENABLE_UTILITY_CODE
|
||||
#include "glslang/SPIRV/spirv.hpp11"
|
||||
#undef SPV_ENABLE_UTILITY_CODE
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
|
||||
namespace {
|
||||
// GLSL.std.450 instruction number (see 3rdparty/glslang/SPIRV/GLSL.std.450.h). The signed
|
||||
// minimum, which is what a GLSL `int` sample index asks for.
|
||||
constexpr Uint32 kGlslStd450SMin = 39u;
|
||||
|
||||
// What MobileGL tells the application GL_MAX_SAMPLES / GL_MAX_INTEGER_SAMPLES are, i.e.
|
||||
// GL_Getter's kFrontendMaxSamples floor. Each test supplies its own backend-real ceilings
|
||||
// against it; Adreno and Mali's Immortalis-G925 both really answer 1 for integer formats.
|
||||
constexpr Int32 kAdvertisedMaxSamples = 4;
|
||||
|
||||
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> CompileFragment(const String& source) {
|
||||
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_FRAGMENT_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_FRAGMENT_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();
|
||||
}
|
||||
|
||||
String Disassemble(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String text;
|
||||
tools.Disassemble(spirv, &text);
|
||||
return text;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// OpImageFetch words: 0 opcode/count, 1 result type, 2 result id, 3 image, 4 coordinate,
|
||||
// 5 the optional image-operands mask, 6.. the ids that mask asks for.
|
||||
struct ImageFetch {
|
||||
Uint32 resultId = 0u;
|
||||
Uint32 imageId = 0u;
|
||||
Uint32 mask = 0u;
|
||||
Vector<Uint32> maskOperandIds;
|
||||
};
|
||||
|
||||
Vector<ImageFetch> CollectImageFetches(const Vector<Uint32>& spirv) {
|
||||
Vector<ImageFetch> fetches;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != spv::Op::OpImageFetch || wordCount < 5u) return;
|
||||
ImageFetch fetch{};
|
||||
fetch.resultId = words[2];
|
||||
fetch.imageId = words[3];
|
||||
if (wordCount > 5u) {
|
||||
fetch.mask = words[5];
|
||||
for (Uint32 word = 6u; word < wordCount; ++word) {
|
||||
fetch.maskOperandIds.push_back(words[word]);
|
||||
}
|
||||
}
|
||||
fetches.push_back(fetch);
|
||||
});
|
||||
return fetches;
|
||||
}
|
||||
|
||||
// OpExtInst words: 0 opcode/count, 1 result type, 2 result id, 3 set, 4 instruction number,
|
||||
// 5.. the operand ids.
|
||||
struct ExtInst {
|
||||
Uint32 resultId = 0u;
|
||||
Uint32 instructionNumber = 0u;
|
||||
Vector<Uint32> operandIds;
|
||||
};
|
||||
|
||||
Vector<ExtInst> CollectExtInsts(const Vector<Uint32>& spirv) {
|
||||
Vector<ExtInst> extInsts;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != spv::Op::OpExtInst || wordCount < 5u) return;
|
||||
ExtInst extInst{};
|
||||
extInst.resultId = words[2];
|
||||
extInst.instructionNumber = words[4];
|
||||
for (Uint32 word = 5u; word < wordCount; ++word) {
|
||||
extInst.operandIds.push_back(words[word]);
|
||||
}
|
||||
extInsts.push_back(extInst);
|
||||
});
|
||||
return extInsts;
|
||||
}
|
||||
|
||||
std::map<Uint32, Uint32> CollectScalarConstants(const Vector<Uint32>& spirv) {
|
||||
std::map<Uint32, Uint32> values;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == spv::Op::OpConstant && wordCount == 4u) values[words[2]] = words[3];
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
// The one fetch carrying an explicit Sample operand. glslang emits Sample on its own for a
|
||||
// multisample texelFetch - there is no texelFetchOffset for a multisampled sampler - so the
|
||||
// sample id is the mask's first and only operand.
|
||||
const ImageFetch* FindSampleCarryingFetch(const Vector<ImageFetch>& fetches) {
|
||||
for (const ImageFetch& fetch : fetches) {
|
||||
if ((fetch.mask & static_cast<Uint32>(spv::ImageOperandsMask::Sample)) != 0u) {
|
||||
return &fetch;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ImageFetch* FindLodCarryingFetch(const Vector<ImageFetch>& fetches) {
|
||||
for (const ImageFetch& fetch : fetches) {
|
||||
if ((fetch.mask & static_cast<Uint32>(spv::ImageOperandsMask::Lod)) != 0u) {
|
||||
return &fetch;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// KHR-GL4x.texture_swizzle.functional's integer multisample read in miniature: the sample
|
||||
// index is the advertised GL_MAX_INTEGER_SAMPLES - 1, baked in as a literal, which is exactly
|
||||
// the value the one-sample allocation underneath cannot answer. The plain sampler2D fetch is
|
||||
// the negative control - a NON-multisampled image whose Lod operand this pass must not touch.
|
||||
const char* const kIntegerMultisampleFetch = R"(#version 410 core
|
||||
uniform usampler2DMS uintMs;
|
||||
uniform sampler2D plain;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
uvec4 texel = texelFetch(uintMs, ivec2(gl_FragCoord.xy), 3);
|
||||
vec4 other = texelFetch(plain, ivec2(gl_FragCoord.xy), 0);
|
||||
fragColor = vec4(texel) * 0.5 + other;
|
||||
}
|
||||
)";
|
||||
|
||||
// The colour class, which real devices squeeze to something above 1 rather than to 1.
|
||||
const char* const kColorMultisampleFetch = R"(#version 410 core
|
||||
uniform sampler2DMS colorMs;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = texelFetch(colorMs, ivec2(gl_FragCoord.xy), 3);
|
||||
}
|
||||
)";
|
||||
|
||||
// Every stage on a squeezed device goes through the probe, so the one that declares no
|
||||
// multisampled image has to come back untouched.
|
||||
const char* const kNoMultisampleFetch = R"(#version 410 core
|
||||
uniform sampler2D plain;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = texelFetch(plain, ivec2(gl_FragCoord.xy), 0);
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
class ClampMultisampleFetchTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
|
||||
<< "the clamped module did not survive spirv-val";
|
||||
}
|
||||
|
||||
Uint64 m_validationFailuresAtStart = 0;
|
||||
};
|
||||
|
||||
// The probe is the gate that keeps every ordinary stage off an optimizer round trip, so it has to
|
||||
// answer no for a shader that never reads a multisample texture - and yes for the ones that do.
|
||||
TEST_F(ClampMultisampleFetchTest, TheProbeAnswersOnlyForAMultisampledImage) {
|
||||
const Vector<Uint32> plain = CompileFragment(kNoMultisampleFetch);
|
||||
ASSERT_FALSE(plain.empty());
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresMultisampledImage(plain));
|
||||
|
||||
const Vector<Uint32> integerMs = CompileFragment(kIntegerMultisampleFetch);
|
||||
ASSERT_FALSE(integerMs.empty());
|
||||
EXPECT_TRUE(ShaderCompiler::DeclaresMultisampledImage(integerMs));
|
||||
|
||||
const Vector<Uint32> colorMs = CompileFragment(kColorMultisampleFetch);
|
||||
ASSERT_FALSE(colorMs.empty());
|
||||
EXPECT_TRUE(ShaderCompiler::DeclaresMultisampledImage(colorMs));
|
||||
|
||||
// Runs on every stage of every program on a squeezed device, so it must survive a stage that
|
||||
// produced no SPIR-V rather than pushing a parse diagnostic for it.
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresMultisampledImage({}));
|
||||
}
|
||||
|
||||
// The overwhelming majority of modules. Behind the probe they never reach the pass at all, but the
|
||||
// pass has to be inert for them on its own, or a future caller that forgets the gate silently
|
||||
// re-serialises every shader in the program.
|
||||
TEST_F(ClampMultisampleFetchTest, LeavesAModuleWithoutAMultisampledImageUntouched) {
|
||||
const Vector<Uint32> input = CompileFragment(kNoMultisampleFetch);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(
|
||||
input, output, /*maxColorSamples=*/4, /*maxIntegerSamples=*/1, /*maxDepthSamples=*/4,
|
||||
kAdvertisedMaxSamples, true));
|
||||
EXPECT_EQ(output, input) << Disassemble(output);
|
||||
}
|
||||
|
||||
// The bug itself. GL_MAX_INTEGER_SAMPLES says 4, the texture has one sample, and the shader asks
|
||||
// for sample 3.
|
||||
TEST_F(ClampMultisampleFetchTest, ReplacesAnOutOfRangeIntegerSampleWithZero) {
|
||||
const Vector<Uint32> input = CompileFragment(kIntegerMultisampleFetch);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
const Vector<ImageFetch> before = CollectImageFetches(input);
|
||||
ASSERT_EQ(before.size(), 2u) << Disassemble(input);
|
||||
const ImageFetch* sampleBefore = FindSampleCarryingFetch(before);
|
||||
const ImageFetch* lodBefore = FindLodCarryingFetch(before);
|
||||
ASSERT_NE(sampleBefore, nullptr) << Disassemble(input);
|
||||
ASSERT_NE(lodBefore, nullptr) << Disassemble(input);
|
||||
ASSERT_EQ(sampleBefore->maskOperandIds.size(), 1u);
|
||||
const std::map<Uint32, Uint32> constantsBefore = CollectScalarConstants(input);
|
||||
ASSERT_EQ(constantsBefore.count(sampleBefore->maskOperandIds.front()), 1u);
|
||||
EXPECT_EQ(constantsBefore.at(sampleBefore->maskOperandIds.front()), 3u);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(
|
||||
input, output, /*maxColorSamples=*/4, /*maxIntegerSamples=*/1, /*maxDepthSamples=*/4,
|
||||
kAdvertisedMaxSamples, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
const String dis = Disassemble(output);
|
||||
ASSERT_TRUE(Validates(output)) << dis;
|
||||
|
||||
const Vector<ImageFetch> after = CollectImageFetches(output);
|
||||
ASSERT_EQ(after.size(), 2u) << dis;
|
||||
const ImageFetch* sampleAfter = FindSampleCarryingFetch(after);
|
||||
ASSERT_NE(sampleAfter, nullptr) << dis;
|
||||
ASSERT_EQ(sampleAfter->maskOperandIds.size(), 1u) << dis;
|
||||
|
||||
// Sample 0 is the only one a one-sample allocation has - and it is a CONSTANT, not a computed
|
||||
// minimum: at K == 1 there is nothing to compare against. An id that resolves in the constant
|
||||
// table cannot also be some OpExtInst's result.
|
||||
const std::map<Uint32, Uint32> constantsAfter = CollectScalarConstants(output);
|
||||
ASSERT_EQ(constantsAfter.count(sampleAfter->maskOperandIds.front()), 1u) << dis;
|
||||
EXPECT_EQ(constantsAfter.at(sampleAfter->maskOperandIds.front()), 0u) << dis;
|
||||
|
||||
// The float sampler2D in the same module is not multisampled, so its Lod fetch has to come
|
||||
// through with the same image, the same mask and the same operand.
|
||||
const ImageFetch* lodAfter = FindLodCarryingFetch(after);
|
||||
ASSERT_NE(lodAfter, nullptr) << dis;
|
||||
EXPECT_EQ(lodAfter->imageId, lodBefore->imageId) << dis;
|
||||
EXPECT_EQ(lodAfter->mask, lodBefore->mask) << dis;
|
||||
EXPECT_EQ(lodAfter->maskOperandIds, lodBefore->maskOperandIds) << dis;
|
||||
}
|
||||
|
||||
// The same shader on a device whose integer ceiling really is what MobileGL advertises. Nothing is
|
||||
// out of range, so nothing may be rewritten - and the module must not even be re-serialised.
|
||||
TEST_F(ClampMultisampleFetchTest, LeavesTheFetchAloneWhenTheCategoryReachesTheAdvertisedMaximum) {
|
||||
const Vector<Uint32> input = CompileFragment(kIntegerMultisampleFetch);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(
|
||||
input, output, /*maxColorSamples=*/4, /*maxIntegerSamples=*/4, /*maxDepthSamples=*/4,
|
||||
kAdvertisedMaxSamples, true));
|
||||
EXPECT_EQ(output, input) << Disassemble(output);
|
||||
}
|
||||
|
||||
// A category squeezed to something above 1 cannot be answered with a constant: an index the
|
||||
// allocation does have must survive, so only the upper bound moves.
|
||||
TEST_F(ClampMultisampleFetchTest, ClampsAColorSampleWithAMinimum) {
|
||||
const Vector<Uint32> input = CompileFragment(kColorMultisampleFetch);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
const Vector<ImageFetch> before = CollectImageFetches(input);
|
||||
ASSERT_EQ(before.size(), 1u) << Disassemble(input);
|
||||
ASSERT_EQ(before.front().maskOperandIds.size(), 1u);
|
||||
const Uint32 originalSampleId = before.front().maskOperandIds.front();
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(
|
||||
input, output, /*maxColorSamples=*/2, /*maxIntegerSamples=*/4, /*maxDepthSamples=*/4,
|
||||
kAdvertisedMaxSamples, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
const String dis = Disassemble(output);
|
||||
ASSERT_TRUE(Validates(output)) << dis;
|
||||
|
||||
const Vector<ImageFetch> after = CollectImageFetches(output);
|
||||
ASSERT_EQ(after.size(), 1u) << dis;
|
||||
ASSERT_EQ(after.front().maskOperandIds.size(), 1u) << dis;
|
||||
const Uint32 clampedSampleId = after.front().maskOperandIds.front();
|
||||
EXPECT_NE(clampedSampleId, originalSampleId) << dis;
|
||||
|
||||
const Vector<ExtInst> extInsts = CollectExtInsts(output);
|
||||
const ExtInst* minimum = nullptr;
|
||||
for (const ExtInst& extInst : extInsts) {
|
||||
if (extInst.resultId == clampedSampleId) minimum = &extInst;
|
||||
}
|
||||
ASSERT_NE(minimum, nullptr) << dis;
|
||||
EXPECT_EQ(minimum->instructionNumber, kGlslStd450SMin) << dis;
|
||||
ASSERT_EQ(minimum->operandIds.size(), 2u) << dis;
|
||||
EXPECT_EQ(minimum->operandIds[0], originalSampleId) << dis;
|
||||
|
||||
// min(sample, K - 1), i.e. the last sample a two-sample allocation has.
|
||||
const std::map<Uint32, Uint32> constants = CollectScalarConstants(output);
|
||||
ASSERT_EQ(constants.count(minimum->operandIds[1]), 1u) << dis;
|
||||
EXPECT_EQ(constants.at(minimum->operandIds[1]), 1u) << dis;
|
||||
}
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/Lower1DArrayImagesPass.h"
|
||||
#include "SpirvPasses/BakeImageFormatsPass.h"
|
||||
#include "SpirvPasses/ClampMultisampleFetchPass.h"
|
||||
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
@@ -651,6 +652,26 @@ namespace MobileGL {
|
||||
return LowerViewportIndexPass::DeclaresViewportIndexBuiltin(binary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::ClampMultisampleFetchesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const Int32 maxColorSamples,
|
||||
const Int32 maxIntegerSamples,
|
||||
const Int32 maxDepthSamples,
|
||||
const Int32 advertisedMaxSamples,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(ClampMultisampleFetchPass::CreateClampMultisampleFetchPass(
|
||||
maxColorSamples, maxIntegerSamples, maxDepthSamples, advertisedMaxSamples));
|
||||
|
||||
return RunOptimizerChecked("ClampMultisampleFetchesForEssl", optimizer, inputBinary,
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DeclaresMultisampledImage(const Vector<Uint32>& binary) {
|
||||
return ClampMultisampleFetchPass::DeclaresMultisampledImage(binary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::SplitArrayVertexInputsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
|
||||
@@ -46,6 +46,25 @@ namespace MobileGL {
|
||||
// whether the pass above has anything to do. The gate that keeps every other
|
||||
// stage off an optimizer round trip it does not need.
|
||||
static bool DeclaresViewportIndexBuiltin(const Vector<Uint32>& binary);
|
||||
// Clamps the Sample image-operand of every multisample fetch to the sample count
|
||||
// the BACKEND can really deliver for that image's category, which on Adreno and
|
||||
// Mali is 1 for integer formats while the frontend advertises the GL-mandated
|
||||
// floor of 4. Without it a `texelFetch(usampler2DMS, coord, 3)` reads past the
|
||||
// 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.
|
||||
static bool ClampMultisampleFetchesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
Int32 maxColorSamples,
|
||||
Int32 maxIntegerSamples,
|
||||
Int32 maxDepthSamples,
|
||||
Int32 advertisedMaxSamples,
|
||||
bool enableSpirvValidation = 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.
|
||||
static bool DeclaresMultisampledImage(const Vector<Uint32>& binary);
|
||||
// Replaces an ARRAY vertex input with one input per element at consecutive
|
||||
// locations, seeding a Private copy of the array so indexed reads still work.
|
||||
// GLSL ES has no array vertex inputs and SPIRV-Cross refuses the whole module
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp
|
||||
// Copyright (c) 2025-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 "ClampMultisampleFetchPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/build_module.h"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_builder.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/opt/types.h"
|
||||
#include "source/util/make_unique.h"
|
||||
#include "source/util/string_utils.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::InstructionBuilder;
|
||||
using spvtools::opt::IRContext;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
// GLSL.std.450 instruction numbers (see 3rdparty/glslang/SPIRV/GLSL.std.450.h).
|
||||
// UMin is not interchangeable with SMin here: an unsigned operand large enough to
|
||||
// read as negative would come back OUT of range from a signed minimum, which is
|
||||
// the very thing this pass exists to prevent.
|
||||
constexpr uint32_t kGlslUMin = 38u;
|
||||
constexpr uint32_t kGlslSMin = 39u;
|
||||
|
||||
// OpTypeImage in-operands: 0 sampled type, 1 Dim, 2 Depth, 3 Arrayed, 4 MS,
|
||||
// 5 Sampled, 6 Format.
|
||||
constexpr uint32_t kSampledTypeOperand = 0;
|
||||
constexpr uint32_t kDepthOperand = 2;
|
||||
constexpr uint32_t kMultisampledOperand = 4;
|
||||
|
||||
// OpImageFetch / OpImageRead in-operands: 0 image, 1 coordinate, 2 the optional
|
||||
// image-operands mask, 3.. the ids that mask asks for.
|
||||
constexpr uint32_t kImageOperand = 0;
|
||||
constexpr uint32_t kImageOperandsMaskOperand = 2;
|
||||
|
||||
// The categories GL keeps a separate GL_MAX_*_SAMPLES ceiling for.
|
||||
enum class SampleCategory { Color, Depth, Integer };
|
||||
|
||||
// The two operations that can carry a Sample image-operand and take their
|
||||
// coordinate in in-operand 1. OpImageWrite can carry one too, but its operand
|
||||
// layout differs (image, coordinate, TEXEL, mask) and writing a multisample
|
||||
// texel is not expressible in the ESSL this backend emits, so it is left out
|
||||
// rather than given an untested second index arithmetic. The sparse forms are
|
||||
// out of scope for the same reason: ESSL has no sparse texturing at all, so a
|
||||
// module containing one cannot reach a driver through this path anyway.
|
||||
bool CarriesSampleImageOperand(spv::Op opcode) {
|
||||
return opcode == spv::Op::OpImageFetch || opcode == spv::Op::OpImageRead;
|
||||
}
|
||||
|
||||
bool IsMultisampledImageType(const Instruction* imageType) {
|
||||
return imageType != nullptr && imageType->opcode() == spv::Op::OpTypeImage &&
|
||||
imageType->NumInOperands() > kMultisampledOperand &&
|
||||
imageType->GetSingleWordInOperand(kMultisampledOperand) == 1u;
|
||||
}
|
||||
|
||||
// The OpTypeImage behind whatever an image operation was handed - a sampled
|
||||
// image, a bare image, or a pointer to (or array of) either. Same unwrapping as
|
||||
// Lower1DArrayImagesPass.
|
||||
Instruction* ResolveImageType(IRContext* context, uint32_t objectId) {
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
Instruction* object = defUseMgr->GetDef(objectId);
|
||||
if (object == nullptr) return nullptr;
|
||||
Instruction* type = defUseMgr->GetDef(object->type_id());
|
||||
while (type != nullptr) {
|
||||
switch (type->opcode()) {
|
||||
case spv::Op::OpTypeImage:
|
||||
return type;
|
||||
case spv::Op::OpTypeSampledImage:
|
||||
case spv::Op::OpTypePointer:
|
||||
case spv::Op::OpTypeArray:
|
||||
case spv::Op::OpTypeRuntimeArray:
|
||||
// Each names its element type in its last in-operand, except arrays,
|
||||
// whose element type is the FIRST.
|
||||
type = defUseMgr->GetDef(type->opcode() == spv::Op::OpTypeArray ||
|
||||
type->opcode() == spv::Op::OpTypeRuntimeArray
|
||||
? type->GetSingleWordInOperand(0)
|
||||
: type->GetSingleWordInOperand(type->NumInOperands() - 1));
|
||||
continue;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SampleCategory CategoryOf(IRContext* context, const Instruction* imageType) {
|
||||
const Instruction* sampledType =
|
||||
context->get_def_use_mgr()->GetDef(imageType->GetSingleWordInOperand(kSampledTypeOperand));
|
||||
if (sampledType != nullptr && sampledType->opcode() == spv::Op::OpTypeInt) {
|
||||
return SampleCategory::Integer;
|
||||
}
|
||||
// Depth == 1 is the ONLY spelling that positively means a depth image.
|
||||
// glslang writes 0 for a plain sampler and 2 ("no indication") wherever it
|
||||
// cannot tell, and GLSL has no multisampled shadow sampler for it to write 1
|
||||
// for, so everything but an explicit 1 falls to the colour ceiling - which is
|
||||
// also the safer of the two to guess at, being the one GL_MAX_SAMPLES itself
|
||||
// describes. Guarded because Depth is only readable on a well-formed type.
|
||||
if (imageType->NumInOperands() > kDepthOperand &&
|
||||
imageType->GetSingleWordInOperand(kDepthOperand) == 1u) {
|
||||
return SampleCategory::Depth;
|
||||
}
|
||||
return SampleCategory::Color;
|
||||
}
|
||||
|
||||
// Where the Sample id sits among an image operation's in-operands, or false when
|
||||
// the operation carries no Sample at all.
|
||||
//
|
||||
// The position is NOT fixed. The mask's ids follow it in ASCENDING BIT ORDER, so
|
||||
// every lower bit that is set pushes Sample along by the number of ids that bit
|
||||
// asks for: Bias/Lod/ConstOffset/Offset/ConstOffsets one each, Grad two (dx and
|
||||
// dy). Bits at or above Sample cannot move it and are irrelevant here. glslang
|
||||
// only ever emits Sample on its own for a GLSL texelFetch - there is no
|
||||
// texelFetchOffset for a multisampled sampler - so in practice this always
|
||||
// answers 3; the walk is what keeps that from being an assumption.
|
||||
bool TryGetSampleOperandIndex(const Instruction& instruction, uint32_t* sampleOperandIndex) {
|
||||
if (instruction.NumInOperands() <= kImageOperandsMaskOperand) {
|
||||
// No image-operands mask at all, so no explicit sample: SPIR-V reads
|
||||
// sample 0, which is in range of any allocation. Nothing to clamp.
|
||||
return false;
|
||||
}
|
||||
const uint32_t mask = instruction.GetSingleWordInOperand(kImageOperandsMaskOperand);
|
||||
const auto has = [mask](spv::ImageOperandsMask bit) {
|
||||
return (mask & static_cast<uint32_t>(bit)) != 0u;
|
||||
};
|
||||
if (!has(spv::ImageOperandsMask::Sample)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t index = kImageOperandsMaskOperand + 1;
|
||||
if (has(spv::ImageOperandsMask::Bias)) ++index;
|
||||
if (has(spv::ImageOperandsMask::Lod)) ++index;
|
||||
if (has(spv::ImageOperandsMask::Grad)) index += 2;
|
||||
if (has(spv::ImageOperandsMask::ConstOffset)) ++index;
|
||||
if (has(spv::ImageOperandsMask::Offset)) ++index;
|
||||
if (has(spv::ImageOperandsMask::ConstOffsets)) ++index;
|
||||
if (instruction.NumInOperands() <= index) {
|
||||
// A mask promising more operands than the instruction carries is a
|
||||
// malformed module; leave it to the validator rather than indexing past
|
||||
// the end of it.
|
||||
return false;
|
||||
}
|
||||
*sampleOperandIndex = index;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The module's GLSL.std.450 import, creating it when the module has none.
|
||||
// glslang emits one for all but the most trivial shaders, but a module that
|
||||
// reached here without one must still be clampable. 0 means no id was available,
|
||||
// and in that case NOTHING was added - the caller can still leave the module
|
||||
// untouched. IRContext::AddExtInstImport rather than Module's: it is the one that
|
||||
// keeps the def-use and feature managers in step with the new import.
|
||||
uint32_t EnsureGlslStd450Import(IRContext* context) {
|
||||
for (const Instruction& import : context->module()->ext_inst_imports()) {
|
||||
if (spvtools::utils::MakeString(import.GetInOperand(0).words) == "GLSL.std.450") {
|
||||
return import.result_id();
|
||||
}
|
||||
}
|
||||
const uint32_t importId = context->TakeNextId();
|
||||
if (importId == 0u) return 0u;
|
||||
context->AddExtInstImport(spvtools::MakeUnique<Instruction>(
|
||||
context, spv::Op::OpExtInstImport, 0, importId,
|
||||
Instruction::OperandList{
|
||||
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector("GLSL.std.450")}}));
|
||||
return importId;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool ClampMultisampleFetchPass::DeclaresMultisampledImage(const Vector<Uint32>& binary) {
|
||||
if (binary.empty()) {
|
||||
// An empty module is a stage that produced no SPIR-V, which is not a verdict
|
||||
// about multisample fetches; letting BuildModule reject it would push a
|
||||
// spurious diagnostic through the message consumer first.
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1, [](spv_message_level_t, const char*, const spv_position_t&, const char*) {},
|
||||
binary.data(), binary.size());
|
||||
if (!context) {
|
||||
// Unparseable here means unusable downstream too; let the ordinary transpile
|
||||
// path produce the error rather than inventing a verdict from it.
|
||||
return false;
|
||||
}
|
||||
for (const Instruction& type : context->module()->types_values()) {
|
||||
if (IsMultisampledImageType(&type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status ClampMultisampleFetchPass::Process() {
|
||||
// No category is squeezed, so no fetch can be out of range. This is the whole
|
||||
// answer on a driver whose per-format ceilings all reach what MobileGL
|
||||
// advertises, and it costs nothing.
|
||||
if (m_maxColorSamples >= m_advertisedMaxSamples &&
|
||||
m_maxIntegerSamples >= m_advertisedMaxSamples &&
|
||||
m_maxDepthSamples >= m_advertisedMaxSamples) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
auto* irContext = context();
|
||||
|
||||
// The type table settles it for almost every shader: no multisampled image
|
||||
// declared, nothing any fetch in the body could be reading.
|
||||
bool hasMultisampledImageType = false;
|
||||
for (const Instruction& type : irContext->types_values()) {
|
||||
if (IsMultisampledImageType(&type)) {
|
||||
hasMultisampledImageType = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasMultisampledImageType) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
auto* constantMgr = irContext->get_constant_mgr();
|
||||
|
||||
bool clampedAnything = false;
|
||||
for (auto& function : *irContext->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& instruction : block) {
|
||||
if (!CarriesSampleImageOperand(instruction.opcode()) ||
|
||||
instruction.NumInOperands() <= kImageOperandsMaskOperand) {
|
||||
continue;
|
||||
}
|
||||
const Instruction* imageType =
|
||||
ResolveImageType(irContext, instruction.GetSingleWordInOperand(kImageOperand));
|
||||
if (!IsMultisampledImageType(imageType)) {
|
||||
continue;
|
||||
}
|
||||
uint32_t sampleOperandIndex = 0;
|
||||
if (!TryGetSampleOperandIndex(instruction, &sampleOperandIndex)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Int32 categoryMaxSamples = m_maxColorSamples;
|
||||
switch (CategoryOf(irContext, imageType)) {
|
||||
case SampleCategory::Integer:
|
||||
categoryMaxSamples = m_maxIntegerSamples;
|
||||
break;
|
||||
case SampleCategory::Depth:
|
||||
categoryMaxSamples = m_maxDepthSamples;
|
||||
break;
|
||||
case SampleCategory::Color:
|
||||
break;
|
||||
}
|
||||
if (categoryMaxSamples >= m_advertisedMaxSamples) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The replacement has to carry the ORIGINAL operand's type: SPIR-V
|
||||
// permits either signedness for Sample, and handing OpImageFetch an
|
||||
// int where it had a uint is an invalid module rather than a wrong
|
||||
// answer - the kind of defect that reaches a driver as "compiles
|
||||
// here, not there".
|
||||
const uint32_t sampleOperandId = instruction.GetSingleWordInOperand(sampleOperandIndex);
|
||||
const Instruction* sampleOperandDef = defUseMgr->GetDef(sampleOperandId);
|
||||
if (sampleOperandDef == nullptr) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t sampleTypeId = sampleOperandDef->type_id();
|
||||
const analysis::Type* sampleType =
|
||||
sampleTypeId != 0u ? typeMgr->GetType(sampleTypeId) : nullptr;
|
||||
const analysis::Integer* sampleInteger =
|
||||
sampleType != nullptr ? sampleType->AsInteger() : nullptr;
|
||||
if (sampleInteger == nullptr || sampleInteger->width() != 32u) {
|
||||
// GLSL spells the sample index `int` and SPIR-V requires an
|
||||
// integer scalar, so this is unreachable from any shader this
|
||||
// backend compiles. Declining beats minting a constant of a
|
||||
// width the operand never had.
|
||||
MGLOG_D("ClampMultisampleFetchPass: sample operand %%%u of a "
|
||||
"multisample fetch is not a 32-bit integer scalar; left "
|
||||
"unclamped.",
|
||||
sampleOperandId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (categoryMaxSamples <= 1) {
|
||||
// One sample exists, and its index is 0.
|
||||
const analysis::Constant* zero = constantMgr->GetConstant(sampleType, {0u});
|
||||
const Instruction* zeroInst =
|
||||
zero != nullptr ? constantMgr->GetDefiningInstruction(zero, sampleTypeId)
|
||||
: nullptr;
|
||||
if (zeroInst == nullptr) {
|
||||
return Status::Failure;
|
||||
}
|
||||
instruction.SetInOperand(sampleOperandIndex, {zeroInst->result_id()});
|
||||
irContext->UpdateDefUse(&instruction);
|
||||
clampedAnything = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// min(operand, K-1). Only the upper bound: an index already inside
|
||||
// the allocation comes through untouched, which is what makes this
|
||||
// safe to apply to a shader that was already correct.
|
||||
//
|
||||
// Everything from here on either completes or fails the module.
|
||||
// Anything that gives up half way - after the import or the bound
|
||||
// constant has been added - would leave a MUTATED module reported as
|
||||
// SuccessWithoutChange, which spvtools::Optimizer asserts against
|
||||
// (it re-serialises and compares byte for byte in that case).
|
||||
const uint32_t glslStd450Id = EnsureGlslStd450Import(irContext);
|
||||
if (glslStd450Id == 0u) {
|
||||
// Id space exhausted, and the import was NOT added. Nothing has
|
||||
// changed yet, but nothing further can be built either.
|
||||
return Status::Failure;
|
||||
}
|
||||
const uint32_t resultId = irContext->TakeNextId();
|
||||
const analysis::Constant* bound = constantMgr->GetConstant(
|
||||
sampleType, {static_cast<uint32_t>(categoryMaxSamples - 1)});
|
||||
const Instruction* boundInst =
|
||||
bound != nullptr ? constantMgr->GetDefiningInstruction(bound, sampleTypeId)
|
||||
: nullptr;
|
||||
if (resultId == 0u || boundInst == nullptr) {
|
||||
return Status::Failure;
|
||||
}
|
||||
|
||||
InstructionBuilder builder(
|
||||
irContext, &instruction,
|
||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
Instruction* clamped = builder.AddInstruction(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpExtInst, sampleTypeId, resultId,
|
||||
Instruction::OperandList{
|
||||
{SPV_OPERAND_TYPE_ID, {glslStd450Id}},
|
||||
{SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER,
|
||||
{sampleInteger->IsSigned() ? kGlslSMin : kGlslUMin}},
|
||||
{SPV_OPERAND_TYPE_ID, {sampleOperandId}},
|
||||
{SPV_OPERAND_TYPE_ID, {boundInst->result_id()}}}));
|
||||
if (clamped == nullptr) {
|
||||
return Status::Failure;
|
||||
}
|
||||
instruction.SetInOperand(sampleOperandIndex, {clamped->result_id()});
|
||||
irContext->UpdateDefUse(&instruction);
|
||||
clampedAnything = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!clampedAnything) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken ClampMultisampleFetchPass::CreateClampMultisampleFetchPass(
|
||||
const Int32 maxColorSamples, const Int32 maxIntegerSamples, const Int32 maxDepthSamples,
|
||||
const Int32 advertisedMaxSamples) {
|
||||
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<ClampMultisampleFetchPass>(
|
||||
maxColorSamples, maxIntegerSamples, maxDepthSamples, advertisedMaxSamples));
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,89 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.h
|
||||
// Copyright (c) 2025-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"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so MobileGL floors every
|
||||
// multisample ceiling it advertises to 4 (GL_Getter's kFrontendMaxSamples) no matter
|
||||
// what the ES driver reports. The realised allocation cannot be floored the same way -
|
||||
// the driver would simply reject it - so DirectGLES clamps the count it passes to
|
||||
// glTexStorage*Multisample down to what the format really supports
|
||||
// (ClampSamplesToBackendSupport). On Adreno and on Mali's Immortalis-G925 that is ONE
|
||||
// sample for every integer format, while the frontend keeps telling the application
|
||||
// GL_MAX_INTEGER_SAMPLES is 4.
|
||||
//
|
||||
// A shader written against the advertised ceiling therefore fetches a sample the
|
||||
// backing storage does not have. KHR-GL33/40/41.texture_swizzle.functional_* and
|
||||
// KHR-GLxx.texture_size_promotion.functional bake `texelFetch(usampler2DMS, coord, 3)`
|
||||
// in as a literal, and the fetch comes back as 0/garbage ("Found pixel with wrong
|
||||
// value", "read value = 0") on a texture the backend quietly allocated with one
|
||||
// sample.
|
||||
//
|
||||
// This pass closes that gap from the shader side: for every fetch of a multisampled
|
||||
// image it clamps the Sample image-operand to the backend's REAL maximum for that
|
||||
// image's category, so the lookup lands inside the allocation the backend made.
|
||||
// - K >= advertisedMaxSamples: the category is not squeezed, nothing is rewritten.
|
||||
// - K <= 1: the Sample operand becomes a constant 0 of its own type - the only
|
||||
// sample that exists.
|
||||
// - 1 < K < advertisedMaxSamples: the operand is wrapped in min(operand, K-1),
|
||||
// which leaves an in-range index exactly as it was.
|
||||
// Only the UPPER bound is clamped. A negative index is out of range in GL before this
|
||||
// pass and after it alike, and MobileGL is not the component that should be inventing
|
||||
// a value for it.
|
||||
//
|
||||
// Category comes from the OpTypeImage: an OpTypeInt sampled type is the integer
|
||||
// class (GL_MAX_INTEGER_SAMPLES), a float one is depth when the image's Depth operand
|
||||
// is exactly 1 and colour otherwise. That last clause is deliberate: glslang writes
|
||||
// Depth 0 for a plain sampler and 2 ("unknown") wherever it cannot tell, and GLSL has
|
||||
// no multisampled shadow sampler at all, so only an explicit 1 is treated as a depth
|
||||
// image and everything else falls to the colour limit - which is the one a
|
||||
// mis-classified image would want anyway.
|
||||
//
|
||||
// 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 {
|
||||
public:
|
||||
// The three backend-REAL per-category ceilings, plus the count the GL frontend
|
||||
// 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)
|
||||
: m_maxColorSamples(maxColorSamples),
|
||||
m_maxIntegerSamples(maxIntegerSamples),
|
||||
m_maxDepthSamples(maxDepthSamples),
|
||||
m_advertisedMaxSamples(advertisedMaxSamples) {}
|
||||
|
||||
const char* name() const override { return "clamp-multisample-fetch"; }
|
||||
Status Process() override;
|
||||
|
||||
// Whether the module declares any multisampled image type, i.e. whether running
|
||||
// this pass could change anything. Answered from a single parse so the caller can
|
||||
// skip the optimizer round trip entirely - which is every shader but the handful
|
||||
// that read a multisample texture directly.
|
||||
static bool DeclaresMultisampledImage(const Vector<Uint32>& binary);
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateClampMultisampleFetchPass(
|
||||
Int32 maxColorSamples, Int32 maxIntegerSamples, Int32 maxDepthSamples,
|
||||
Int32 advertisedMaxSamples);
|
||||
|
||||
private:
|
||||
Int32 m_maxColorSamples;
|
||||
Int32 m_maxIntegerSamples;
|
||||
Int32 m_maxDepthSamples;
|
||||
Int32 m_advertisedMaxSamples;
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
Reference in New Issue
Block a user