[Fix, Test] (GLImpl, ShaderTranspiler): reconcile the compute work-group limits glGetIntegeri_v and glslang advertise

This commit is contained in:
2026-08-20 12:41:01 -04:00
parent 04a06438c5
commit cb155c5b94
5 changed files with 126 additions and 20 deletions
+10 -3
View File
@@ -47,7 +47,10 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
constexpr GLint kFrontendMaxComputeUniformComponents = 1024;
// Shared with the glslang resource table for the same reason as the atomic-counter
// 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
@@ -118,12 +121,16 @@ namespace MobileGL::MG_Impl::GLImpl {
constexpr GLint kFrontendSubpixelBits = 4;
constexpr GLint kFrontendMaxSamples = 4;
// The floors under GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE. Shared with the compile
// pipeline (CaptureCompileEnv floors the same driver answers at them, and
// BuildTBuiltInResource expands gl_MaxComputeWorkGroup* from the result), because a
// shader is allowed to compare the built-in constant against this query.
constexpr GLint GetMinComputeWorkGroupCount(GLuint index) {
return index < 3 ? 65535 : 0;
return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_COUNT[index]) : 0;
}
constexpr GLint GetMinComputeWorkGroupSize(GLuint index) {
return index < 2 ? 1024 : (index == 2 ? 64 : 0);
return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_SIZE[index]) : 0;
}
GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks,
+62
View File
@@ -994,6 +994,68 @@ void main() {
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
// against a permissive 1024 while the context advertises the 64 the GL 4.6 minimum (and every ES
// driver) reports.
TEST(GetterSanity, ComputeWorkGroupQueriesMatchShaderCompilerLimits) {
using namespace MobileGL;
auto previousContext = Move(MG_State::pGLContext);
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(MG_Backend::DynamicBackendParameters{});
GLint size[3] = {0, 0, 0};
GLint count[3] = {0, 0, 0};
for (GLuint index = 0; index < 3; ++index) {
MG_Impl::GLImpl::GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index, &size[index]);
MG_Impl::GLImpl::GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, index, &count[index]);
}
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// A negative array size is a compile error, so the stage only compiles when EVERY component
// of both built-in constants equals what the query above reported. Two-sided by construction:
// a resource table that is too permissive fails it exactly like one that is too tight.
const String source = R"(#version 430 core
layout(local_size_x = 1) in;
const int mgAgree = (gl_MaxComputeWorkGroupSize == ivec3()" +
std::to_string(size[0]) + ", " + std::to_string(size[1]) + ", " +
std::to_string(size[2]) + R"() &&
gl_MaxComputeWorkGroupCount == ivec3()" +
std::to_string(count[0]) + ", " + std::to_string(count[1]) + ", " +
std::to_string(count[2]) + R"()) ? 1 : -1;
int mgProbe[mgAgree];
void main() {
mgProbe[0] = 0;
}
)";
auto compiled = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_COMPUTE_SHADER,
.sourceStr = source,
});
EXPECT_TRUE(compiled) << (compiled ? "" : compiled.error().log);
// The z ceiling is also what glslang checks a declared local_size_z against, so it has to
// reject one invocation past the advertised limit and accept the limit itself.
const String atLimit = "#version 430 core\nlayout(local_size_z = " + std::to_string(size[2]) +
") in;\nvoid main() {}\n";
const String pastLimit = "#version 430 core\nlayout(local_size_z = " + std::to_string(size[2] + 1) +
") in;\nvoid main() {}\n";
EXPECT_TRUE(MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_COMPUTE_SHADER,
.sourceStr = atLimit,
}));
EXPECT_FALSE(MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_COMPUTE_SHADER,
.sourceStr = pastLimit,
}));
MG_Backend::pActiveBackendObject = Move(previousBackend);
MG_State::pGLContext = Move(previousContext);
}
TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) {
using namespace MobileGL;
@@ -28,6 +28,9 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
HashValue(state, env.maxComputeWorkGroupSize[0]);
HashValue(state, env.maxComputeWorkGroupSize[1]);
HashValue(state, env.maxComputeWorkGroupSize[2]);
HashValue(state, env.maxComputeWorkGroupCount[0]);
HashValue(state, env.maxComputeWorkGroupCount[1]);
HashValue(state, env.maxComputeWorkGroupCount[2]);
HashValue(state, env.maxComputeWorkGroupInvocations);
HashValue(state, env.backend);
// DynamicBackendParameters is a plain aggregate of scalars; hashing its object
@@ -51,19 +54,23 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
env->advertisedExtensions = activeBackend->GetRendererInfo().RendererGLInfo.Extensions;
}
// GL_MAX_COMPUTE_WORK_GROUP_SIZE. This is a REAL driver call on DirectGLES; it must
// happen here, on the context thread, and exactly once per context. The frontend
// minimum is the floor, matching what GL_Getter reports.
// TODO: Share these exposed compute limit helpers with GL_Getter.cpp instead of duplicating the frontend minima.
constexpr Uint kFrontendMinComputeWorkGroupSizes[3] = {1024, 1024, 64};
// GL_MAX_COMPUTE_WORK_GROUP_SIZE / _COUNT. These are REAL driver calls on DirectGLES; they
// must happen here, on the context thread, and exactly once per context. The frontend
// minimum is the floor, matching what GL_Getter reports - both sides now floor at the
// shared MIN_COMPUTE_WORK_GROUP_* constants rather than at their own copy of them.
for (Uint index = 0; index < 3; ++index) {
Int backendValue = 0;
Int backendSize = 0;
Int backendCount = 0;
if (MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v) {
MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index,
&backendValue);
&backendSize);
MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, index,
&backendCount);
}
env->maxComputeWorkGroupSize[index] =
std::max(static_cast<Uint>(std::max(backendValue, 0)), kFrontendMinComputeWorkGroupSizes[index]);
std::max(static_cast<Uint>(std::max(backendSize, 0)), MIN_COMPUTE_WORK_GROUP_SIZE[index]);
env->maxComputeWorkGroupCount[index] =
std::max(static_cast<Uint>(std::max(backendCount, 0)), MIN_COMPUTE_WORK_GROUP_COUNT[index]);
}
constexpr Uint64 kFrontendMaxComputeWorkGroupInvocations = 1024;
+19 -1
View File
@@ -12,6 +12,18 @@
#include <MG_Backend/BackendObject.h>
namespace MobileGL::MG_Util::ShaderTranspiler {
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE core minimums (GL 4.6 core table 23.45), in ONE
// place because three separate readers have to agree on them: CaptureCompileEnv (which floors
// the backend's answer at them), GL_Getter (which answers the same query the same way) and
// BuildTBuiltInResource (whose gl_MaxComputeWorkGroup* constants a shader compares against
// the query - KHR-GL43.compute_shader.max does exactly that). They used to be three copies,
// and the z one disagreed: glslang compiled against 1024 while the context advertised 64.
inline constexpr Uint MIN_COMPUTE_WORK_GROUP_COUNT[3] = {65535, 65535, 65535};
inline constexpr Uint MIN_COMPUTE_WORK_GROUP_SIZE[3] = {1024, 1024, 64};
// GL_MAX_COMPUTE_UNIFORM_COMPONENTS, the same invariant with no backend input: the number
// glGetIntegerv answers and the number gl_MaxComputeUniformComponents expands to.
inline constexpr Int MAX_COMPUTE_UNIFORM_COMPONENTS = 1024;
// everything outside (stage, source) this reads - advertised extensions and backend limits -
// so the transformation is a pure function of its three arguments and can run on a worker
// thread.
@@ -34,7 +46,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
struct CompileEnv {
// --- compute limits: the ONLY former real-driver read in the pipeline ---
// GL_MAX_COMPUTE_WORK_GROUP_SIZE, already max()'d with the frontend minimum.
Uint maxComputeWorkGroupSize[3] = {1024, 1024, 64};
Uint maxComputeWorkGroupSize[3] = {MIN_COMPUTE_WORK_GROUP_SIZE[0], MIN_COMPUTE_WORK_GROUP_SIZE[1],
MIN_COMPUTE_WORK_GROUP_SIZE[2]};
// GL_MAX_COMPUTE_WORK_GROUP_COUNT, likewise. Carried for the same reason the size is:
// gl_MaxComputeWorkGroupCount expands from it at parse time, so the compile pipeline
// needs the number the context advertises without reaching back to the live backend.
Uint maxComputeWorkGroupCount[3] = {MIN_COMPUTE_WORK_GROUP_COUNT[0], MIN_COMPUTE_WORK_GROUP_COUNT[1],
MIN_COMPUTE_WORK_GROUP_COUNT[2]};
// GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, likewise.
Uint64 maxComputeWorkGroupInvocations = 1024;
@@ -83,14 +83,7 @@ namespace MobileGL {
Resources.minProgramTexelOffset = -8;
Resources.maxProgramTexelOffset = 7;
Resources.maxClipDistances = 8;
Resources.maxComputeWorkGroupCountX = 65535;
Resources.maxComputeWorkGroupCountY = 65535;
Resources.maxComputeWorkGroupCountZ = 65535;
Resources.maxComputeWorkGroupSizeX = 1024;
Resources.maxComputeWorkGroupSizeY = 1024;
// TODO: Drive glslang compute resource limits from the active backend instead of this permissive cap.
Resources.maxComputeWorkGroupSizeZ = 1024;
Resources.maxComputeUniformComponents = 1024;
Resources.maxComputeUniformComponents = MAX_COMPUTE_UNIFORM_COMPONENTS;
Resources.maxComputeTextureImageUnits = 16;
Resources.maxComputeImageUniforms = 8;
Resources.maxComputeAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
@@ -179,6 +172,25 @@ namespace MobileGL {
Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms;
Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms;
Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms;
Resources.maxComputeTextureImageUnits = dynamicParameters.MaxComputeTextureImageUnits;
// 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
// CaptureCompileEnv already issued once on the GL thread and floored at the core
// minimum exactly as GL_Getter does. Reading the same snapshot here is what makes
// gl_MaxComputeWorkGroupSize and glGetIntegeri_v agree by construction
// (KHR-GL43.compute_shader.max compares them); the z component was 1024 here
// against the 64 every ES driver reports. A null env is the standalone/test entry
// point, which has no context to have queried one - the core minimums stand, which
// is what a default-constructed CompileEnv carries anyway.
const Uint* maxWorkGroupSize = env ? env->maxComputeWorkGroupSize : MIN_COMPUTE_WORK_GROUP_SIZE;
const Uint* maxWorkGroupCount = env ? env->maxComputeWorkGroupCount : MIN_COMPUTE_WORK_GROUP_COUNT;
Resources.maxComputeWorkGroupSizeX = static_cast<int>(maxWorkGroupSize[0]);
Resources.maxComputeWorkGroupSizeY = static_cast<int>(maxWorkGroupSize[1]);
Resources.maxComputeWorkGroupSizeZ = static_cast<int>(maxWorkGroupSize[2]);
Resources.maxComputeWorkGroupCountX = static_cast<int>(maxWorkGroupCount[0]);
Resources.maxComputeWorkGroupCountY = static_cast<int>(maxWorkGroupCount[1]);
Resources.maxComputeWorkGroupCountZ = static_cast<int>(maxWorkGroupCount[2]);
Resources.limits.nonInductiveForLoops = true;
Resources.limits.whileLoops = true;