diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 22d5fc7b..4be84f38 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -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(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(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(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_SIZE[index]) : 0; } GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks, diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index dff11265..8e89afa1 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -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_Backend::pActiveBackendObject = MakeUnique(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; diff --git a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp index cb8e3a46..0ef054ae 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp @@ -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(std::max(backendValue, 0)), kFrontendMinComputeWorkGroupSizes[index]); + std::max(static_cast(std::max(backendSize, 0)), MIN_COMPUTE_WORK_GROUP_SIZE[index]); + env->maxComputeWorkGroupCount[index] = + std::max(static_cast(std::max(backendCount, 0)), MIN_COMPUTE_WORK_GROUP_COUNT[index]); } constexpr Uint64 kFrontendMaxComputeWorkGroupInvocations = 1024; diff --git a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h index 2eadd65b..8daf3ce8 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h +++ b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h @@ -12,6 +12,18 @@ #include 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; diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 375ed43b..d96e36de 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -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(maxWorkGroupSize[0]); + Resources.maxComputeWorkGroupSizeY = static_cast(maxWorkGroupSize[1]); + Resources.maxComputeWorkGroupSizeZ = static_cast(maxWorkGroupSize[2]); + Resources.maxComputeWorkGroupCountX = static_cast(maxWorkGroupCount[0]); + Resources.maxComputeWorkGroupCountY = static_cast(maxWorkGroupCount[1]); + Resources.maxComputeWorkGroupCountZ = static_cast(maxWorkGroupCount[2]); Resources.limits.nonInductiveForLoops = true; Resources.limits.whileLoops = true;