[Fix] (Espryt): bound usampler2DMS fetches by the bound texture's real sample count

This commit is contained in:
2026-10-12 08:39:26 -04:00
parent c9bf43b710
commit 09b315197b
11 changed files with 371 additions and 10 deletions
@@ -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 &&
+41 -1
View File
@@ -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<TextureInternalFormat>(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<Int>(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);
}
}
+11
View File
@@ -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
+68
View File
@@ -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<String, String> clampNames; // sampler name -> sample-max uniform name
Vector<std::pair<SizeT, String>> 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<SizeT>(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<String> 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<SizeT> 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 {
+15
View File
@@ -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_<sampler>;` 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 {
@@ -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<SharedPtr<glslang::TShader>> shaders;
const Vector<Pair<GLenum, const char*>> 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<uint32_t> deferred;
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(modules.value()[1], deferred, 4, 1, 4, 4, true,
/*deferDirectUnsignedFetches=*/true));
const Vector<uint32_t>& 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;
}
@@ -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<Uint32>& spirv, Uint32 value) {
const std::map<Uint32, Uint32> constants = CollectScalarConstants(spirv);
SizeT count = 0;
for (const ImageFetch& fetch : CollectImageFetches(spirv)) {
if ((fetch.mask & static_cast<Uint32>(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<Uint32> input = CompileFragment(kStencilMultisampleFetch);
ASSERT_FALSE(input.empty());
Vector<Uint32> 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<Uint32> input = CompileFragment(kStencilMultisampleFetch);
ASSERT_FALSE(input.empty());
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);
EXPECT_EQ(CountSampleOperandsEqualTo(output, 3u), 0u) << dis;
EXPECT_EQ(CountSampleOperandsEqualTo(output, 0u), 3u) << dis;
}
@@ -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);
@@ -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<Uint32>& inputBinary,
Vector<uint32_t>& 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.
@@ -21,6 +21,7 @@
#include "source/util/make_unique.h"
#include "source/util/string_utils.h"
#include <algorithm>
#include <memory>
#include <vector>
@@ -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<uint32_t>(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<ClampMultisampleFetchPass>(
maxColorSamples, maxIntegerSamples, maxDepthSamples, advertisedMaxSamples));
maxColorSamples, maxIntegerSamples, maxDepthSamples, advertisedMaxSamples,
deferDirectUnsignedFetches));
}
} // namespace ShaderTranspiler
} // namespace MG_Util
@@ -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(<uniform name>, ...)` 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