mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Fix] (GL): derive per-stage atomic-counter limits from the backend's storage-block budget and fail the link past them
This commit is contained in:
@@ -68,38 +68,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// limits below: gl_MaxComputeUniformComponents expands from BuildTBuiltInResource.
|
||||
constexpr GLint kFrontendMaxComputeUniformComponents =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_COMPUTE_UNIFORM_COMPONENTS);
|
||||
// Every atomic-counter limit is shared with the glslang resource table
|
||||
// (BuildTBuiltInResource) through MG_Util/ShaderTranspiler/Types.h: GL 4.6 requires
|
||||
// glGetIntegerv and the gl_MaxAtomicCounter* built-in constants to agree, and the two
|
||||
// used to be independent tables that disagreed on both the binding count and the buffer
|
||||
// size. Never move one of these without the other.
|
||||
constexpr GLint kFrontendMaxComputeAtomicCounters =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
|
||||
constexpr GLint kFrontendMaxComputeAtomicCounterBuffers =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
|
||||
constexpr GLint kFrontendMaxComputeSharedMemorySize = 32768;
|
||||
constexpr GLint kFrontendMaxComputeWorkGroupInvocations = 1024;
|
||||
constexpr GLint kFrontendMaxCombinedAtomicCounters =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
|
||||
constexpr GLint kFrontendMaxCombinedAtomicCounterBuffers =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
|
||||
constexpr GLint kFrontendMaxFragmentAtomicCounters =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
|
||||
constexpr GLint kFrontendMaxFragmentAtomicCounterBuffers =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
|
||||
constexpr GLint kFrontendMaxGeometryAtomicCounters = 0;
|
||||
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
|
||||
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
|
||||
constexpr GLint kFrontendMaxVertexAtomicCounters = 0;
|
||||
// Zero counters means zero buffers to hold them. These have to be ANSWERED rather than
|
||||
// left to the default INVALID_ENUM: a well-behaved application queries the limit exactly
|
||||
// to find out that the stage cannot do this, and an error instead both leaves its output
|
||||
// untouched (so it reads uninitialised memory and may conclude the opposite) and leaves a
|
||||
// GL error pending that surfaces at whatever unrelated call checks next.
|
||||
constexpr GLint kFrontendMaxGeometryAtomicCounterBuffers = 0;
|
||||
constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0;
|
||||
constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0;
|
||||
constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0;
|
||||
// GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE: the byte offset ceiling a counter may be declared
|
||||
// at. The matching binding count is applied in GetIndexedBufferQueryPointCount, so that
|
||||
// the getter, the indexed queries and glBindBufferBase all share one ceiling.
|
||||
@@ -333,6 +303,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// ARM's GLES driver reports 0 for all four (a Mali-G925 does), and advertising 16 there
|
||||
// bought nothing: the program still failed to link inside the backend, the frontend
|
||||
// still reported LINK_STATUS as true, and every draw with it silently rendered nothing.
|
||||
// Every per-stage and combined atomic-counter limit. Shared with the glslang resource
|
||||
// table (BuildTBuiltInResource) through ResolveAtomicCounterLimits: GL 4.6 requires
|
||||
// glGetIntegerv and the gl_Max*AtomicCounter* built-in constants to agree, and the link
|
||||
// fails a program over them. A stage that cannot host a counter answers 0 rather than
|
||||
// INVALID_ENUM - an application queries the limit exactly to find that out.
|
||||
MG_Util::ShaderTranspiler::AtomicCounterLimits AdvertisedAtomicCounterLimits() {
|
||||
static const MG_Backend::DynamicBackendParameters kBackendlessDefaults{};
|
||||
return MG_Util::ShaderTranspiler::ResolveAtomicCounterLimits(
|
||||
MG_Backend::pActiveBackendObject ? MG_Backend::pActiveBackendObject->GetDynamicParameters()
|
||||
: kBackendlessDefaults);
|
||||
}
|
||||
|
||||
GLint StageStorageBlockCount(Int MG_Backend::DynamicBackendParameters::*stageLimit) {
|
||||
static const MG_Backend::DynamicBackendParameters kBackendlessDefaults{};
|
||||
const MG_Backend::DynamicBackendParameters& parameters =
|
||||
@@ -1885,10 +1867,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxComputeUniformComponents;
|
||||
return;
|
||||
case GL_MAX_COMPUTE_ATOMIC_COUNTERS:
|
||||
*params = kFrontendMaxComputeAtomicCounters;
|
||||
*params = AdvertisedAtomicCounterLimits().stageCounters[5];
|
||||
return;
|
||||
case GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS:
|
||||
*params = kFrontendMaxComputeAtomicCounterBuffers;
|
||||
*params = AdvertisedAtomicCounterLimits().stageBuffers[5];
|
||||
return;
|
||||
case GL_MAX_COMPUTE_SHARED_MEMORY_SIZE:
|
||||
*params = kFrontendMaxComputeSharedMemorySize;
|
||||
@@ -2081,10 +2063,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(MG_Util::ConvertLogicOperationToGLEnum(MG_State::pGLContext->GetLogicOp()));
|
||||
return;
|
||||
case GL_MAX_COMBINED_ATOMIC_COUNTERS:
|
||||
*params = kFrontendMaxCombinedAtomicCounters;
|
||||
*params = AdvertisedAtomicCounterLimits().combinedCounters;
|
||||
return;
|
||||
case GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS:
|
||||
*params = kFrontendMaxCombinedAtomicCounterBuffers;
|
||||
*params = AdvertisedAtomicCounterLimits().combinedBuffers;
|
||||
return;
|
||||
case GL_MAX_COMBINED_UNIFORM_BLOCKS:
|
||||
*params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks);
|
||||
@@ -2099,10 +2081,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = 1024 * 1024; // TODO
|
||||
return;
|
||||
case GL_MAX_FRAGMENT_ATOMIC_COUNTERS:
|
||||
*params = kFrontendMaxFragmentAtomicCounters;
|
||||
*params = AdvertisedAtomicCounterLimits().stageCounters[4];
|
||||
return;
|
||||
case GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS:
|
||||
*params = kFrontendMaxFragmentAtomicCounterBuffers;
|
||||
*params = AdvertisedAtomicCounterLimits().stageBuffers[4];
|
||||
return;
|
||||
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
|
||||
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxFragmentShaderStorageBlocks);
|
||||
@@ -2125,10 +2107,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = ClampUniformBlockCount(kFrontendMaxFragmentUniformBlocks);
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_ATOMIC_COUNTERS:
|
||||
*params = kFrontendMaxGeometryAtomicCounters;
|
||||
*params = AdvertisedAtomicCounterLimits().stageCounters[3];
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS:
|
||||
*params = kFrontendMaxGeometryAtomicCounterBuffers;
|
||||
*params = AdvertisedAtomicCounterLimits().stageBuffers[3];
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
||||
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxGeometryShaderStorageBlocks);
|
||||
@@ -2191,16 +2173,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = INT_MAX; // TODO
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_ATOMIC_COUNTERS:
|
||||
*params = kFrontendMaxTessControlAtomicCounters;
|
||||
*params = AdvertisedAtomicCounterLimits().stageCounters[1];
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_ATOMIC_COUNTER_BUFFERS:
|
||||
*params = kFrontendMaxTessControlAtomicCounterBuffers;
|
||||
*params = AdvertisedAtomicCounterLimits().stageBuffers[1];
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_ATOMIC_COUNTERS:
|
||||
*params = kFrontendMaxTessEvaluationAtomicCounters;
|
||||
*params = AdvertisedAtomicCounterLimits().stageCounters[2];
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_ATOMIC_COUNTER_BUFFERS:
|
||||
*params = kFrontendMaxTessEvaluationAtomicCounterBuffers;
|
||||
*params = AdvertisedAtomicCounterLimits().stageBuffers[2];
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS:
|
||||
*params = MG_Backend::pActiveBackendObject
|
||||
@@ -2286,10 +2268,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxVaryingVectors;
|
||||
return;
|
||||
case GL_MAX_VERTEX_ATOMIC_COUNTERS:
|
||||
*params = kFrontendMaxVertexAtomicCounters;
|
||||
*params = AdvertisedAtomicCounterLimits().stageCounters[0];
|
||||
return;
|
||||
case GL_MAX_VERTEX_ATOMIC_COUNTER_BUFFERS:
|
||||
*params = kFrontendMaxVertexAtomicCounterBuffers;
|
||||
*params = AdvertisedAtomicCounterLimits().stageBuffers[0];
|
||||
return;
|
||||
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
||||
*params = MG_Backend::pActiveBackendObject
|
||||
|
||||
@@ -363,9 +363,8 @@ namespace {
|
||||
// default-block uniforms and is not a GL block at all. An instance array costs one block per
|
||||
// element, reconciled per name exactly as image arrays are above.
|
||||
//
|
||||
// Atomic counter buffers are deliberately NOT counted here. Their pre-fragment limits are a
|
||||
// frontend 0 while every backend serves a counter as a lowered storage block in any stage,
|
||||
// so enforcing that number would only start refusing programs that run today.
|
||||
// Atomic counter buffers are lowered storage blocks too, but GL counts them against their
|
||||
// own limits: ValidateAtomicCounterLimits below.
|
||||
static MobileGL::String ValidateBlockLimits(glslang::TProgram& reflection,
|
||||
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
|
||||
using MobileGL::Array;
|
||||
@@ -451,6 +450,95 @@ namespace {
|
||||
return {};
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.7: LinkProgram FAILS when a stage uses more atomic counter buffers or atomic
|
||||
// counters than GL_MAX_<STAGE>_ATOMIC_COUNTER_BUFFERS / _ATOMIC_COUNTERS, or the stages
|
||||
// together more than the GL_MAX_COMBINED_* pair (a buffer or counter two stages use counts in
|
||||
// each). Nothing enforced either, so a geometry stage with more counters than advertised
|
||||
// linked cleanly. The limits are ResolveAtomicCounterLimits', the derivation glGetIntegerv
|
||||
// and the gl_Max*AtomicCounter* constants answer from, and they follow what the backend can
|
||||
// host - a counter buffer is a lowered storage block in every stage - so this refuses nothing
|
||||
// the backend could have run.
|
||||
//
|
||||
// A counter buffer is one synthesized gl_AtomicCounterBlock_<binding> block; a counter is one
|
||||
// element of one of its members. Both are counted only in the stages that reference them.
|
||||
static MobileGL::String ValidateAtomicCounterLimits(glslang::TProgram& reflection,
|
||||
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
|
||||
using MobileGL::Int;
|
||||
using MobileGL::SizeT;
|
||||
using MobileGL::Vector;
|
||||
namespace Transpiler = MobileGL::MG_Util::ShaderTranspiler;
|
||||
constexpr SizeT kStageCount = Transpiler::ATOMIC_COUNTER_STAGE_COUNT;
|
||||
|
||||
static constexpr EShLanguage kStages[kStageCount] = {EShLangVertex, EShLangTessControl, EShLangTessEvaluation,
|
||||
EShLangGeometry, EShLangFragment, EShLangCompute};
|
||||
static constexpr const char* kStageNames[kStageCount] = {"VERTEX", "TESS_CONTROL", "TESS_EVALUATION",
|
||||
"GEOMETRY", "FRAGMENT", "COMPUTE"};
|
||||
const auto inStage = [](EShLanguageMask stages, SizeT stage) {
|
||||
return (static_cast<unsigned>(stages) & (1u << static_cast<unsigned>(kStages[stage]))) != 0;
|
||||
};
|
||||
|
||||
const Int blockCount = reflection.getNumUniformBlocks();
|
||||
if (blockCount <= 0) return {};
|
||||
Vector<bool> isCounterBlock(static_cast<SizeT>(blockCount), false);
|
||||
Int buffers[kStageCount] = {};
|
||||
bool anyCounterBlock = false;
|
||||
for (Int i = 0; i < blockCount; ++i) {
|
||||
const auto& block = reflection.getUniformBlock(i);
|
||||
if (!IsAtomicCounterBlockName(block.name)) continue;
|
||||
isCounterBlock[static_cast<SizeT>(i)] = true;
|
||||
anyCounterBlock = true;
|
||||
for (SizeT stage = 0; stage < kStageCount; ++stage) {
|
||||
if (inStage(block.stages, stage)) ++buffers[stage];
|
||||
}
|
||||
}
|
||||
if (!anyCounterBlock) return {}; // every program that declares no atomic counter
|
||||
|
||||
Int counters[kStageCount] = {};
|
||||
const Int uniformCount = reflection.getNumUniformVariables();
|
||||
for (Int i = 0; i < uniformCount; ++i) {
|
||||
const auto& uniform = reflection.getUniform(i);
|
||||
if (uniform.index < 0 || uniform.index >= blockCount || !isCounterBlock[static_cast<SizeT>(uniform.index)]) {
|
||||
continue;
|
||||
}
|
||||
Int elements = uniform.size > 1 ? uniform.size : 1;
|
||||
if (const glslang::TType* type = uniform.getType(); type != nullptr && type->isSizedArray()) {
|
||||
elements = type->getCumulativeArraySize();
|
||||
}
|
||||
for (SizeT stage = 0; stage < kStageCount; ++stage) {
|
||||
if (inStage(uniform.stages, stage)) counters[stage] += elements;
|
||||
}
|
||||
}
|
||||
|
||||
const Transpiler::AtomicCounterLimits limits = Transpiler::ResolveAtomicCounterLimits(env.params);
|
||||
Int combinedBuffers = 0;
|
||||
Int combinedCounters = 0;
|
||||
for (SizeT stage = 0; stage < kStageCount; ++stage) {
|
||||
if (buffers[stage] > limits.stageBuffers[stage]) {
|
||||
return std::format("This program uses {} atomic counter buffers in one stage, more than the {} "
|
||||
"GL_MAX_{}_ATOMIC_COUNTER_BUFFERS allows.",
|
||||
buffers[stage], limits.stageBuffers[stage], kStageNames[stage]);
|
||||
}
|
||||
if (counters[stage] > limits.stageCounters[stage]) {
|
||||
return std::format("This program uses {} atomic counters in one stage, more than the {} "
|
||||
"GL_MAX_{}_ATOMIC_COUNTERS allows.",
|
||||
counters[stage], limits.stageCounters[stage], kStageNames[stage]);
|
||||
}
|
||||
combinedBuffers += buffers[stage];
|
||||
combinedCounters += counters[stage];
|
||||
}
|
||||
if (combinedBuffers > limits.combinedBuffers) {
|
||||
return std::format("This program uses {} atomic counter buffers across its stages, more than the {} "
|
||||
"GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS allows.",
|
||||
combinedBuffers, limits.combinedBuffers);
|
||||
}
|
||||
if (combinedCounters > limits.combinedCounters) {
|
||||
return std::format("This program uses {} atomic counters across its stages, more than the {} "
|
||||
"GL_MAX_COMBINED_ATOMIC_COUNTERS allows.",
|
||||
combinedCounters, limits.combinedCounters);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static bool IsBuiltInPipelineOutput(const glslang::TObjectReflection& output) {
|
||||
const auto* type = output.getType();
|
||||
return type && type->getQualifier().builtIn != glslang::EbvNone;
|
||||
@@ -1251,6 +1339,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (String counterLimitError = ValidateAtomicCounterLimits(*artifacts.program, env);
|
||||
!counterLimitError.empty()) {
|
||||
artifacts.infoLog = Move(counterLimitError);
|
||||
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
||||
ProgramObject::ResetLinkArtifacts(artifacts);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------- GL-facing index spaces (relaxed-parse cleanup) ----------
|
||||
// Blocks first: global-UBO membership drives the uniform filter below. The
|
||||
// synthesized MGL_GLOBAL_UBO is a transpiler artifact - its members are GL
|
||||
|
||||
@@ -924,6 +924,56 @@ void main() { color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrem
|
||||
ClearErrors();
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.7 fails the link when a stage uses more atomic counter buffers than
|
||||
// GL_MAX_<STAGE>_ATOMIC_COUNTER_BUFFERS. Nothing counted them, so a geometry stage with more
|
||||
// counter buffers than advertised linked cleanly. Sized off the ADVERTISED limits, which
|
||||
// follow the active backend's geometry-stage storage-block budget.
|
||||
TEST_F(ProgramInterfaceTest, AtomicCounterBuffersOverAStageLimitFailToLink) {
|
||||
GLint maxGeometryBuffers = -1;
|
||||
GLint maxBindings = -1;
|
||||
GetIntegerv(GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS, &maxGeometryBuffers);
|
||||
GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, &maxBindings);
|
||||
ClearErrors();
|
||||
ASSERT_GE(maxGeometryBuffers, 0);
|
||||
|
||||
// One counter per binding 0..buffers-1, each its own buffer.
|
||||
const auto link = [](GLint buffers, std::string& log) {
|
||||
std::string gs = R"(#version 430
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
)";
|
||||
std::string body;
|
||||
for (GLint i = 0; i < buffers; ++i) {
|
||||
gs += "layout(binding = " + std::to_string(i) + ") uniform atomic_uint c" + std::to_string(i) + ";\n";
|
||||
body += " atomicCounterIncrement(c" + std::to_string(i) + ");\n";
|
||||
}
|
||||
gs += "void main() {\n" + body + " gl_Position = vec4(0.0);\n EmitVertex();\n}\n";
|
||||
const char* gsSource = gs.c_str();
|
||||
const GLuint p = MakeProgram(kSimpleVs, kSimpleFs);
|
||||
const GLuint sh = CreateShader(GL_GEOMETRY_SHADER);
|
||||
ShaderSource(sh, 1, &gsSource, nullptr);
|
||||
CompileShader(sh);
|
||||
AttachShader(p, sh);
|
||||
LinkProgram(p);
|
||||
GLint status = -1;
|
||||
GetProgramiv(p, GL_LINK_STATUS, &status);
|
||||
char buffer[4096] = "";
|
||||
GetProgramInfoLog(p, sizeof(buffer), nullptr, buffer);
|
||||
log = buffer;
|
||||
return status == GL_TRUE;
|
||||
};
|
||||
|
||||
std::string log;
|
||||
if (maxGeometryBuffers + 1 <= maxBindings) {
|
||||
EXPECT_FALSE(link(maxGeometryBuffers + 1, log)) << "one buffer over the advertised limit must not link";
|
||||
EXPECT_NE(log.find("GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS"), std::string::npos) << "info log was: " << log;
|
||||
}
|
||||
if (maxGeometryBuffers > 0) {
|
||||
EXPECT_TRUE(link(maxGeometryBuffers, log)) << "exactly the advertised limit must link: " << log;
|
||||
}
|
||||
ClearErrors();
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.6 fails the link when a stage's active image uniforms exceed
|
||||
// GL_MAX_*_IMAGE_UNIFORMS, or when their sum exceeds GL_MAX_COMBINED_IMAGE_UNIFORMS. Nothing
|
||||
// counted them - glslang keeps those numbers only so gl_Max*ImageUniforms can expand from
|
||||
|
||||
@@ -1230,6 +1230,84 @@ void main() {
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
// Every per-stage atomic-counter limit follows the backend's per-stage STORAGE-BLOCK budget,
|
||||
// because both backends serve a counter buffer as a lowered storage block in any stage: a stage
|
||||
// with storage blocks gets counter buffers (up to the binding count) and a full buffer's worth of
|
||||
// counters, a stage without gets 0 of both. glGetIntegerv and the gl_Max*AtomicCounter*
|
||||
// constants must say the same thing (KHR-GL43.shader_atomic_counters.basic-glsl-built-in).
|
||||
TEST(GetterSanity, AtomicCounterLimitsFollowTheStageStorageBlockBudget) {
|
||||
using namespace MobileGL;
|
||||
namespace Transpiler = MG_Util::ShaderTranspiler;
|
||||
|
||||
auto previousContext = MG_State::pGLContext.Take();
|
||||
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
MG_Backend::DynamicBackendParameters params{};
|
||||
params.MaxShaderStorageBufferBindings = 24;
|
||||
params.MaxCombinedShaderStorageBlocks = 24;
|
||||
params.MaxVertexShaderStorageBlocks = 4;
|
||||
params.MaxTessControlShaderStorageBlocks = 0;
|
||||
params.MaxTessEvaluationShaderStorageBlocks = 0;
|
||||
params.MaxGeometryShaderStorageBlocks = 2;
|
||||
params.MaxFragmentShaderStorageBlocks = 4;
|
||||
params.MaxComputeShaderStorageBlocks = 24;
|
||||
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
|
||||
|
||||
const GLint fullBuffer = static_cast<GLint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE / 4);
|
||||
const struct {
|
||||
GLenum buffersPname;
|
||||
GLenum countersPname;
|
||||
GLint buffers;
|
||||
GLint counters;
|
||||
const char* buffersConstant;
|
||||
const char* countersConstant;
|
||||
} expected[] = {
|
||||
{GL_MAX_VERTEX_ATOMIC_COUNTER_BUFFERS, GL_MAX_VERTEX_ATOMIC_COUNTERS, 4, fullBuffer,
|
||||
"gl_MaxVertexAtomicCounterBuffers", "gl_MaxVertexAtomicCounters"},
|
||||
{GL_MAX_TESS_CONTROL_ATOMIC_COUNTER_BUFFERS, GL_MAX_TESS_CONTROL_ATOMIC_COUNTERS, 0, 0,
|
||||
"gl_MaxTessControlAtomicCounterBuffers", "gl_MaxTessControlAtomicCounters"},
|
||||
{GL_MAX_TESS_EVALUATION_ATOMIC_COUNTER_BUFFERS, GL_MAX_TESS_EVALUATION_ATOMIC_COUNTERS, 0, 0,
|
||||
"gl_MaxTessEvaluationAtomicCounterBuffers", "gl_MaxTessEvaluationAtomicCounters"},
|
||||
{GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS, GL_MAX_GEOMETRY_ATOMIC_COUNTERS, 2, fullBuffer,
|
||||
"gl_MaxGeometryAtomicCounterBuffers", "gl_MaxGeometryAtomicCounters"},
|
||||
{GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS, GL_MAX_FRAGMENT_ATOMIC_COUNTERS, 4, fullBuffer,
|
||||
"gl_MaxFragmentAtomicCounterBuffers", "gl_MaxFragmentAtomicCounters"},
|
||||
{GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS, GL_MAX_COMPUTE_ATOMIC_COUNTERS,
|
||||
static_cast<GLint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS), fullBuffer,
|
||||
"gl_MaxComputeAtomicCounterBuffers", "gl_MaxComputeAtomicCounters"},
|
||||
// The combined pair: the stage sum (4 + 2 + 4 + 8 buffers), bounded by the combined
|
||||
// storage-block budget the lowered blocks draw from.
|
||||
{GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS, GL_MAX_COMBINED_ATOMIC_COUNTERS, 18, 4 * fullBuffer,
|
||||
"gl_MaxCombinedAtomicCounterBuffers", "gl_MaxCombinedAtomicCounters"},
|
||||
};
|
||||
String checks;
|
||||
for (const auto& limit : expected) {
|
||||
GLint reported = -1;
|
||||
MG_Impl::GLImpl::GetIntegerv(limit.buffersPname, &reported);
|
||||
EXPECT_EQ(reported, limit.buffers) << limit.buffersConstant;
|
||||
reported = -1;
|
||||
MG_Impl::GLImpl::GetIntegerv(limit.countersPname, &reported);
|
||||
EXPECT_EQ(reported, limit.counters) << limit.countersConstant;
|
||||
// A negative array size is a compile error, so each line compiles only while the
|
||||
// constant expands to exactly the number glGetIntegerv answers.
|
||||
checks += "int mg_" + String(limit.buffersConstant) + "[" + limit.buffersConstant +
|
||||
" == " + std::to_string(limit.buffers) + " ? 1 : -1];\n";
|
||||
checks += "int mg_" + String(limit.countersConstant) + "[" + limit.countersConstant +
|
||||
" == " + std::to_string(limit.counters) + " ? 1 : -1];\n";
|
||||
}
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
const String source = "#version 430 core\nout vec4 color;\n" + checks + "void main() { color = vec4(1.0); }\n";
|
||||
auto compiled = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
|
||||
.shaderType = GL_FRAGMENT_SHADER,
|
||||
.sourceStr = source,
|
||||
});
|
||||
EXPECT_TRUE(compiled) << (compiled ? "" : compiled.error().log);
|
||||
|
||||
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
// KHR-GL43.compute_shader.max: the test queries every GL_MAX_COMPUTE_* value through the API and
|
||||
// then makes a compute shader compare the matching gl_MaxCompute* constant against it. The two
|
||||
// used to be independent tables and gl_MaxComputeWorkGroupSize.z disagreed - glslang compiled
|
||||
|
||||
@@ -83,6 +83,16 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// gl_MaxCullDistances / gl_MaxCombinedClipAndCullDistances from the same numbers.
|
||||
HashValue(state, env.params.MaxCullDistances);
|
||||
HashValue(state, env.params.MaxCombinedClipAndCullDistances);
|
||||
// The inputs of ResolveAtomicCounterLimits, which every gl_Max*AtomicCounter* constant
|
||||
// expands from: the per-stage counter-buffer budget is the stage's storage-block budget.
|
||||
HashValue(state, env.params.MaxVertexShaderStorageBlocks);
|
||||
HashValue(state, env.params.MaxTessControlShaderStorageBlocks);
|
||||
HashValue(state, env.params.MaxTessEvaluationShaderStorageBlocks);
|
||||
HashValue(state, env.params.MaxGeometryShaderStorageBlocks);
|
||||
HashValue(state, env.params.MaxFragmentShaderStorageBlocks);
|
||||
HashValue(state, env.params.MaxComputeShaderStorageBlocks);
|
||||
HashValue(state, env.params.MaxCombinedShaderStorageBlocks);
|
||||
HashValue(state, env.params.MaxShaderStorageBufferBindings);
|
||||
// The texture-image-unit family, made env-derived in the same wave. They were stock
|
||||
// glslang defaults (32/32/80) that disagreed with what glGetIntegerv answered, and
|
||||
// gl_MaxTextureImageUnits / gl_MaxVertexTextureImageUnits / gl_MaxCombinedTextureImageUnits
|
||||
|
||||
@@ -118,8 +118,6 @@ namespace MobileGL {
|
||||
Resources.maxComputeUniformComponents = MAX_COMPUTE_UNIFORM_COMPONENTS;
|
||||
Resources.maxComputeTextureImageUnits = 16;
|
||||
Resources.maxComputeImageUniforms = 8;
|
||||
Resources.maxComputeAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
|
||||
Resources.maxComputeAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
|
||||
Resources.maxVaryingComponents = MAX_VARYING_COMPONENTS;
|
||||
Resources.maxVertexOutputComponents = 64;
|
||||
Resources.maxGeometryInputComponents = 64;
|
||||
@@ -153,25 +151,14 @@ namespace MobileGL {
|
||||
Resources.maxPatchVertices = 32;
|
||||
Resources.maxTessGenLevel = 64;
|
||||
Resources.maxViewports = 16;
|
||||
Resources.maxVertexAtomicCounters = 0;
|
||||
Resources.maxTessControlAtomicCounters = 0;
|
||||
Resources.maxTessEvaluationAtomicCounters = 0;
|
||||
Resources.maxGeometryAtomicCounters = 0;
|
||||
Resources.maxFragmentAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
|
||||
Resources.maxCombinedAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
|
||||
// Every atomic-counter limit below is the one glGetIntegerv answers; the shared
|
||||
// constants in Types.h are what keeps the two sides from drifting apart again.
|
||||
// gl_MaxAtomicCounterBindings and gl_MaxAtomicCounterBufferSize expand from these
|
||||
// (Initialize.cpp), and the binding count is also the ceiling glslang checks a
|
||||
// `layout(binding = N) uniform atomic_uint` against - it was 1, so every counter
|
||||
// outside binding 0 failed to compile.
|
||||
// The per-stage and combined counter counts are env-derived and set further down.
|
||||
Resources.maxAtomicCounterBindings = MAX_ATOMIC_COUNTER_BUFFER_BINDINGS;
|
||||
Resources.maxVertexAtomicCounterBuffers = 0;
|
||||
Resources.maxTessControlAtomicCounterBuffers = 0;
|
||||
Resources.maxTessEvaluationAtomicCounterBuffers = 0;
|
||||
Resources.maxGeometryAtomicCounterBuffers = 0;
|
||||
Resources.maxFragmentAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
|
||||
Resources.maxCombinedAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
|
||||
Resources.maxAtomicCounterBufferSize = MAX_ATOMIC_COUNTER_BUFFER_SIZE;
|
||||
Resources.maxTransformFeedbackBuffers = 4;
|
||||
Resources.maxTransformFeedbackInterleavedComponents = 64;
|
||||
@@ -253,6 +240,27 @@ namespace MobileGL {
|
||||
// declaration with a diagnostic the application can read.
|
||||
Resources.maxCullDistances = dynamicParameters.MaxCullDistances;
|
||||
Resources.maxCombinedClipAndCullDistances = dynamicParameters.MaxCombinedClipAndCullDistances;
|
||||
// The per-stage and combined atomic-counter limits, from the same derivation
|
||||
// glGetIntegerv answers with (ResolveAtomicCounterLimits): a stage's counter
|
||||
// buffers are lowered storage blocks, so they follow the backend's per-stage
|
||||
// storage-block budget, and gl_Max*AtomicCounter* has to expand to that number.
|
||||
{
|
||||
const AtomicCounterLimits counterLimits = ResolveAtomicCounterLimits(dynamicParameters);
|
||||
Resources.maxVertexAtomicCounters = counterLimits.stageCounters[0];
|
||||
Resources.maxTessControlAtomicCounters = counterLimits.stageCounters[1];
|
||||
Resources.maxTessEvaluationAtomicCounters = counterLimits.stageCounters[2];
|
||||
Resources.maxGeometryAtomicCounters = counterLimits.stageCounters[3];
|
||||
Resources.maxFragmentAtomicCounters = counterLimits.stageCounters[4];
|
||||
Resources.maxComputeAtomicCounters = counterLimits.stageCounters[5];
|
||||
Resources.maxCombinedAtomicCounters = counterLimits.combinedCounters;
|
||||
Resources.maxVertexAtomicCounterBuffers = counterLimits.stageBuffers[0];
|
||||
Resources.maxTessControlAtomicCounterBuffers = counterLimits.stageBuffers[1];
|
||||
Resources.maxTessEvaluationAtomicCounterBuffers = counterLimits.stageBuffers[2];
|
||||
Resources.maxGeometryAtomicCounterBuffers = counterLimits.stageBuffers[3];
|
||||
Resources.maxFragmentAtomicCounterBuffers = counterLimits.stageBuffers[4];
|
||||
Resources.maxComputeAtomicCounterBuffers = counterLimits.stageBuffers[5];
|
||||
Resources.maxCombinedAtomicCounterBuffers = counterLimits.combinedBuffers;
|
||||
}
|
||||
|
||||
// The compute work-group limits are the env's, not the backend parameters': they
|
||||
// are the only ones that come from a REAL indexed driver query, which
|
||||
|
||||
@@ -74,11 +74,58 @@ namespace MobileGL {
|
||||
// advertised binding point and binds them all in one glBindBuffersRange, so it must
|
||||
// stay a multiple of, and comfortably larger than, four times the binding count.
|
||||
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFER_SIZE = 16384;
|
||||
// GL_MAX_{FRAGMENT,COMPUTE,COMBINED}_ATOMIC_COUNTER_BUFFERS and the matching
|
||||
// _ATOMIC_COUNTERS. Eight is the GL 4.6 core minimum for the compute stage
|
||||
// (table 23.45) and every other stage this implementation serves counters on.
|
||||
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE = 8;
|
||||
inline constexpr Int MAX_ATOMIC_COUNTERS_PER_STAGE = 8;
|
||||
// The ceilings of GL_MAX_<STAGE>_ATOMIC_COUNTER_BUFFERS and _ATOMIC_COUNTERS for a
|
||||
// stage that can hold a counter buffer at all; ResolveAtomicCounterLimits below turns
|
||||
// them into the advertised numbers. A stage can never use more counter buffers than
|
||||
// there are counter bindings, and a counter is one uint of a lowered storage block, so
|
||||
// the only bound on the counter count is how many fit in one buffer.
|
||||
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE = MAX_ATOMIC_COUNTER_BUFFER_BINDINGS;
|
||||
inline constexpr Int MAX_ATOMIC_COUNTERS_PER_STAGE = MAX_ATOMIC_COUNTER_BUFFER_SIZE / 4;
|
||||
|
||||
// Index order of AtomicCounterLimits' per-stage arrays.
|
||||
inline constexpr SizeT ATOMIC_COUNTER_STAGE_COUNT = 6; // VS, TCS, TES, GS, FS, CS
|
||||
|
||||
struct AtomicCounterLimits {
|
||||
Int stageBuffers[ATOMIC_COUNTER_STAGE_COUNT] = {};
|
||||
Int stageCounters[ATOMIC_COUNTER_STAGE_COUNT] = {};
|
||||
Int combinedBuffers = 0;
|
||||
Int combinedCounters = 0;
|
||||
};
|
||||
|
||||
// THE one derivation of every per-stage and combined atomic-counter limit, read by
|
||||
// glGetIntegerv, by BuildTBuiltInResource (the gl_Max*AtomicCounter* constants) and by
|
||||
// the link-time check, so the three cannot disagree. Both backends serve a counter in
|
||||
// ANY stage the same way - glslang lowers it onto a storage block - so a stage's
|
||||
// counter-buffer budget is its storage-block budget (as GL_MAX_<STAGE>_SHADER_STORAGE_
|
||||
// BLOCKS clamps it), and a stage with none has no counters either. That is also why
|
||||
// the pre-fragment stages are no longer a flat 0: they get exactly what the backend
|
||||
// can host, and 0 only where it really cannot (a driver without vertex-stage storage
|
||||
// blocks still answers 0, and the conformance suite skips the stage).
|
||||
inline AtomicCounterLimits ResolveAtomicCounterLimits(const MG_Backend::DynamicBackendParameters& p) {
|
||||
const Int storageBindings = std::max(p.MaxShaderStorageBufferBindings, 0);
|
||||
const auto clampStorage = [storageBindings](Int limit) {
|
||||
return std::min(std::max(limit, 0), storageBindings);
|
||||
};
|
||||
const Int stageStorageBlocks[ATOMIC_COUNTER_STAGE_COUNT] = {
|
||||
p.MaxVertexShaderStorageBlocks, p.MaxTessControlShaderStorageBlocks,
|
||||
p.MaxTessEvaluationShaderStorageBlocks, p.MaxGeometryShaderStorageBlocks,
|
||||
p.MaxFragmentShaderStorageBlocks, p.MaxComputeShaderStorageBlocks};
|
||||
AtomicCounterLimits limits;
|
||||
Int bufferSum = 0;
|
||||
for (SizeT stage = 0; stage < ATOMIC_COUNTER_STAGE_COUNT; ++stage) {
|
||||
const Int buffers =
|
||||
std::min(MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE, clampStorage(stageStorageBlocks[stage]));
|
||||
limits.stageBuffers[stage] = buffers;
|
||||
limits.stageCounters[stage] = buffers > 0 ? MAX_ATOMIC_COUNTERS_PER_STAGE : 0;
|
||||
bufferSum += buffers;
|
||||
limits.combinedCounters += limits.stageCounters[stage];
|
||||
}
|
||||
// A buffer two stages use counts once per stage (GL 4.6 7.7), so the combined
|
||||
// ceiling is the stage sum, bounded by the combined storage-block budget the
|
||||
// lowered blocks really draw from.
|
||||
limits.combinedBuffers = std::min(bufferSum, clampStorage(p.MaxCombinedShaderStorageBlocks));
|
||||
return limits;
|
||||
}
|
||||
|
||||
// GL_MAX_{VERTEX,TESS_CONTROL,TESS_EVALUATION,GEOMETRY,FRAGMENT}_UNIFORM_BLOCKS. No
|
||||
// built-in constant expands from these, but the link fails a program that uses more
|
||||
|
||||
Reference in New Issue
Block a user