mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
[Fix, Test] (GLState, ShaderTranspiler): reject a storage-block binding at or past GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "ShaderCompileTask.h"
|
#include "ShaderCompileTask.h"
|
||||||
|
|
||||||
|
#include <MG_State/GLState/BufferState/BufferState.h>
|
||||||
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
|
|
||||||
#include <glslang/Include/PoolAlloc.h>
|
#include <glslang/Include/PoolAlloc.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <charconv>
|
#include <charconv>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -137,8 +139,21 @@ namespace {
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS) answers, recomputed rather than
|
||||||
|
// queried: the compile runs on a worker with no context, and the pname is not a plain backend
|
||||||
|
// parameter - the getter caps the backend's count by the state layer's fixed binding-point
|
||||||
|
// array (GL_Getter's GetIndexedBufferQueryPointCount). A shader must be judged against the
|
||||||
|
// number the application was told, not against either half of it.
|
||||||
|
static MobileGL::Int MaxShaderStorageBufferBindings(
|
||||||
|
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
|
||||||
|
const MobileGL::Int frontendPoints =
|
||||||
|
static_cast<MobileGL::Int>(MobileGL::MG_State::GLState::BufferBindingPointCount);
|
||||||
|
if (!env.HasBackend()) return frontendPoints;
|
||||||
|
return std::min(frontendPoints, std::max(env.params.MaxShaderStorageBufferBindings, 0));
|
||||||
|
}
|
||||||
|
|
||||||
// The half of a compile that depends on nothing but the source text, the stage and the
|
// The half of a compile that depends on nothing but the source text, the stage and the
|
||||||
// environment snapshot: preprocessing, the two lexical rejections, and the two lexical
|
// environment snapshot: preprocessing, the three lexical rejections, and the two lexical
|
||||||
// side-channel extractions. Split out so P0b layer 2 can memoize exactly this and
|
// side-channel extractions. Split out so P0b layer 2 can memoize exactly this and
|
||||||
// nothing else - the glslang parse stays per-object because its TShader is consume-once.
|
// nothing else - the glslang parse stays per-object because its TShader is consume-once.
|
||||||
// Deliberately free of any per-object state so the memo is sound.
|
// Deliberately free of any per-object state so the memo is sound.
|
||||||
@@ -172,6 +187,13 @@ namespace {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (const std::optional<String> bindingError = FindShaderStorageBindingViolation(
|
||||||
|
result.preprocessedSource, MaxShaderStorageBufferBindings(env))) {
|
||||||
|
result.outcome = ShaderPreprocessOutcome::ResourceBindingRejected;
|
||||||
|
result.infoLog = *bindingError;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// The parse this feeds runs in the link-compatible configuration (Vulkan-client
|
// The parse this feeds runs in the link-compatible configuration (Vulkan-client
|
||||||
// env with relaxed rules): the TShader it produces is what glLinkProgram links and
|
// env with relaxed rules): the TShader it produces is what glLinkProgram links and
|
||||||
// what the backends' SPIR-V is generated from - there is no second, GL-client
|
// what the backends' SPIR-V is generated from - there is no second, GL-client
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
ComputeLocalSizeRejected,
|
ComputeLocalSizeRejected,
|
||||||
// FindReservedIdentifierViolation rejected it.
|
// FindReservedIdentifierViolation rejected it.
|
||||||
ReservedIdentifierRejected,
|
ReservedIdentifierRejected,
|
||||||
|
// FindShaderStorageBindingViolation rejected it: a storage block declared a binding at or
|
||||||
|
// past GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS.
|
||||||
|
ResourceBindingRejected,
|
||||||
// The source-only half was clean but glslang rejected the preprocessed source.
|
// The source-only half was clean but glslang rejected the preprocessed source.
|
||||||
// Memoizing this saves the parse itself on every later object with that source.
|
// Memoizing this saves the parse itself on every later object with that source.
|
||||||
ParseFailed,
|
ParseFailed,
|
||||||
|
|||||||
@@ -3840,3 +3840,41 @@ TEST_F(ProgramUtilTest, EsslCoreImageFormatSetIsTheThirteenTheSpecLists) {
|
|||||||
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8051 /*GL_RGB8*/));
|
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8051 /*GL_RGB8*/));
|
||||||
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0 /*GL_NONE*/));
|
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0 /*GL_NONE*/));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KHR-GL43.shader_storage_buffer_object.negative-glsl-compileTime: a storage block declared at
|
||||||
|
// GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS must fail to compile, and so must an arrayed one whose
|
||||||
|
// LAST element passes the ceiling. The relaxed Vulkan-rules parse enforces neither.
|
||||||
|
TEST_F(ProgramUtilTest, StorageBlockBindingCeilingIsCheckedAtItsExactBoundary) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
constexpr Int kMaxBindings = 36;
|
||||||
|
const auto violation = [](const String& body) {
|
||||||
|
return FindShaderStorageBindingViolation("#version 430 core\n" + body + "void main() {}\n", kMaxBindings);
|
||||||
|
};
|
||||||
|
|
||||||
|
// The boundary itself: max - 1 is the last legal point, max is one past it.
|
||||||
|
EXPECT_FALSE(violation("layout(binding = 35) buffer Buffer { int x; };\n").has_value());
|
||||||
|
EXPECT_TRUE(violation("layout(binding = 36) buffer Buffer { int x; };\n").has_value());
|
||||||
|
|
||||||
|
// An instance array takes CONSECUTIVE points, so what has to fit is base + count - 1.
|
||||||
|
EXPECT_FALSE(violation("layout(binding = 32) buffer Buffer { int x; } g_array[4];\n").has_value());
|
||||||
|
EXPECT_TRUE(violation("layout(binding = 34) buffer Buffer { int x; } g_array[4];\n").has_value());
|
||||||
|
|
||||||
|
// Qualifiers and a second layout list may sit between the binding and the keyword.
|
||||||
|
EXPECT_TRUE(violation("layout(std430) layout(binding = 36) coherent restrict buffer B { int x; };\n")
|
||||||
|
.has_value());
|
||||||
|
|
||||||
|
// Things the scanner must NOT judge: a uniform block (a different ceiling), a storage block
|
||||||
|
// with no explicit binding, the bare default-qualifier form, and an instance array whose size
|
||||||
|
// is not a literal.
|
||||||
|
EXPECT_FALSE(violation("layout(binding = 40) uniform Block { int x; };\n"
|
||||||
|
"layout(binding = 0) buffer Buffer { int y; };\n")
|
||||||
|
.has_value());
|
||||||
|
EXPECT_FALSE(violation("buffer Buffer { int x; };\nconst int binding = 40;\n").has_value());
|
||||||
|
EXPECT_FALSE(violation("layout(binding = 1) buffer;\nbuffer Buffer { int x; };\n").has_value());
|
||||||
|
EXPECT_FALSE(violation("const int kCount = 4;\nlayout(binding = 34) buffer B { int x; } g[kCount];\n")
|
||||||
|
.has_value());
|
||||||
|
|
||||||
|
// A backend that advertises no binding points has no ceiling to enforce.
|
||||||
|
EXPECT_FALSE(FindShaderStorageBindingViolation("layout(binding = 36) buffer B { int x; };\n", 0).has_value());
|
||||||
|
}
|
||||||
|
|||||||
@@ -1332,6 +1332,97 @@ namespace MobileGL {
|
|||||||
return bindings;
|
return bindings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Binding points a storage-block declaration starting at `bufferPos` occupies.
|
||||||
|
// One for a scalar instance (and for the "layout(...) buffer;" default-qualifier
|
||||||
|
// form, which declares no block at all); the element count for an instance array,
|
||||||
|
// whose elements take base, base+1, ... (GLSL 4.30 4.4.5). -1 means "the grammar
|
||||||
|
// here is outside this scanner's narrow subset", i.e. do not judge this one.
|
||||||
|
long long StorageBlockBindingPointCount(const Vector<CodeToken>& tokens, SizeT bufferPos,
|
||||||
|
SizeT count) {
|
||||||
|
SizeT k = bufferPos + 1;
|
||||||
|
if (k < count && IsIdentifierToken(tokens[k])) ++k; // block type name
|
||||||
|
if (k >= count || tokens[k].text != "{") return 1;
|
||||||
|
|
||||||
|
MobileGL::Int braceDepth = 0;
|
||||||
|
while (k < count) {
|
||||||
|
if (tokens[k].text == "{") {
|
||||||
|
++braceDepth;
|
||||||
|
} else if (tokens[k].text == "}") {
|
||||||
|
--braceDepth;
|
||||||
|
if (braceDepth == 0) {
|
||||||
|
++k;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++k;
|
||||||
|
}
|
||||||
|
if (braceDepth != 0) return -1; // unterminated block: not this scanner's business
|
||||||
|
|
||||||
|
if (k < count && IsIdentifierToken(tokens[k])) ++k; // instance name
|
||||||
|
if (k >= count || tokens[k].text != "[") return 1;
|
||||||
|
if (k + 2 < count && IsDecimalIntegerToken(tokens[k + 1].text) && tokens[k + 2].text == "]") {
|
||||||
|
return std::max<long long>(1, std::strtoll(tokens[k + 1].text.c_str(), nullptr, 10));
|
||||||
|
}
|
||||||
|
return -1; // sized by an expression, or unsized
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::optional<String> FindShaderStorageBindingViolation(const String& source, Int maxBindings) {
|
||||||
|
// A backend that advertises nothing has no ceiling to enforce.
|
||||||
|
if (maxBindings <= 0) return std::nullopt;
|
||||||
|
// Fast path: no storage block, nothing to check. Both keywords are required for a
|
||||||
|
// violation to exist, and the pair is absent from almost every shader-pack source.
|
||||||
|
if (source.find("buffer") == String::npos || source.find("binding") == String::npos) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Vector<CodeToken> tokens = TokenizeCode(source);
|
||||||
|
const SizeT count = tokens.size();
|
||||||
|
// The binding the qualifier run currently being scanned declared, -1 for none.
|
||||||
|
// Several layout(...) lists may precede one declaration and the later one wins,
|
||||||
|
// which is the same accumulate-then-consume shape the extractors above use.
|
||||||
|
long long binding = -1;
|
||||||
|
for (SizeT pos = 0; pos < count; ++pos) {
|
||||||
|
const String& text = tokens[pos].text;
|
||||||
|
if (text == "layout" && pos + 1 < count && tokens[pos + 1].text == "(") {
|
||||||
|
SizeT j = pos + 2;
|
||||||
|
Int parenDepth = 1;
|
||||||
|
while (j < count && parenDepth > 0) {
|
||||||
|
const String& layoutToken = tokens[j].text;
|
||||||
|
if (layoutToken == "(") {
|
||||||
|
++parenDepth;
|
||||||
|
} else if (layoutToken == ")") {
|
||||||
|
--parenDepth;
|
||||||
|
} else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < count &&
|
||||||
|
tokens[j + 1].text == "=" && IsDecimalIntegerToken(tokens[j + 2].text)) {
|
||||||
|
binding = std::min(std::strtoll(tokens[j + 2].text.c_str(), nullptr, 10),
|
||||||
|
static_cast<long long>(INT_MAX / 2));
|
||||||
|
j += 2;
|
||||||
|
}
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
pos = j - 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (text == "buffer") {
|
||||||
|
const long long points = binding >= 0 ? StorageBlockBindingPointCount(tokens, pos, count) : -1;
|
||||||
|
if (points > 0 && binding + points > static_cast<long long>(maxBindings)) {
|
||||||
|
return "ERROR: invalid value " + std::to_string(binding) +
|
||||||
|
" for layout specifier 'binding': a shader storage block occupying " +
|
||||||
|
std::to_string(points) + " binding point(s) from there passes " +
|
||||||
|
"GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS (" + std::to_string(maxBindings) + ").";
|
||||||
|
}
|
||||||
|
binding = -1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Qualifiers may sit between the layout list and the `buffer` keyword; anything
|
||||||
|
// else ends the run, so a binding never leaks onto an unrelated declaration.
|
||||||
|
if (!IsNonLayoutQualifierKeyword(text)) binding = -1;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
UnorderedMap<String, Int> ExtractExplicitUniformLocations(const String& source) {
|
UnorderedMap<String, Int> ExtractExplicitUniformLocations(const String& source) {
|
||||||
UnorderedMap<String, Int> locations;
|
UnorderedMap<String, Int> locations;
|
||||||
// Fast path: without the qualifier keyword there is nothing to extract.
|
// Fast path: without the qualifier keyword there is nothing to extract.
|
||||||
|
|||||||
@@ -64,6 +64,17 @@ namespace MobileGL {
|
|||||||
// mapIO can capture them, so they are recovered lexically (same narrow
|
// mapIO can capture them, so they are recovered lexically (same narrow
|
||||||
// grammar discipline as ExtractExplicitUniformLocations).
|
// grammar discipline as ExtractExplicitUniformLocations).
|
||||||
UnorderedMap<String, Uint> ExtractExplicitOpaqueBindings(const String& source);
|
UnorderedMap<String, Uint> ExtractExplicitOpaqueBindings(const String& source);
|
||||||
|
|
||||||
|
// A shader storage block whose layout(binding = N) reaches or passes
|
||||||
|
// GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS is a compile-time error in GL 4.3 core 4.4.5,
|
||||||
|
// and an arrayed block instance takes CONSECUTIVE points, so the last element is what
|
||||||
|
// has to fit. glslang cannot raise it for MobileGL: every shader is parsed as a Vulkan
|
||||||
|
// client under relaxed rules, where the GL ceilings do not apply, and TBuiltInResource
|
||||||
|
// has no storage-buffer binding field to check against in the first place. Returns the
|
||||||
|
// compile-error text for the first violation, or nullopt for a clean source.
|
||||||
|
// `maxBindings` is what glGetIntegerv answers for that pname; a non-positive value
|
||||||
|
// means "nothing to check against" and every declaration passes.
|
||||||
|
std::optional<String> FindShaderStorageBindingViolation(const String& source, Int maxBindings);
|
||||||
} // namespace ShaderTranspiler
|
} // namespace ShaderTranspiler
|
||||||
} // namespace MG_Util
|
} // namespace MG_Util
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
Reference in New Issue
Block a user