From e8ee7b1a88a7de1d36c890e67416714033809597 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 5 Sep 2026 21:03:22 -0400 Subject: [PATCH] [Feat] (Backend, MGPipe): carry the six per-axis compute limits in DynamicBackendParameters so MGPCaps has every backend-owned indexed answer, and pin them against glGetIntegeri_v on both backends - P-1: MGPCaps is DynamicBackendParameters by inclusion (plan B section 4.4.1), but that struct carried MaxComputeWorkGroupInvocations and no per-axis GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE - the six numbers that ARE the backend-owned indexed answers surviving the getter retirement (GL_Getter.cpp and CompileEnv.cpp ask GLFunctionsTable::GetIntegeri_v for exactly these, DirectVulkan answers them from VkPhysicalDeviceLimits), so the interface had a hole where its only genuine indexed carrier should be. DynamicBackendParameters now has MaxComputeWorkGroupCount[3] / MaxComputeWorkGroupSize[3] with the GL 4.3 minimums as the no-backend defaults; DirectGLES fills them from glGetIntegeri_v inside the loader's bracketed probe run (GLESCapabilities carries them, logged with the other limits) and DirectVulkan from maxComputeWorkGroupCount / maxComputeWorkGroupSize through the loader's SaturateToInt like every other limit. Raw driver answers, as the invocations limit is: the frontend floors them at the shared MIN_COMPUTE_WORK_GROUP_* minimums itself. - The GetIntegeri_v table path is untouched, as is GL_Getter and CompileEnv behaviour: retiring the getter in favour of the caps is P0.5, and this only makes sure the caps have what P0.5 needs. - PipeCalls.def's footer no longer claims that "only GL_COMPUTE_WORK_GROUP_SIZE is a real backend answer and it lives in MGPCaps": the six limits live in MGPCaps, and GL_COMPUTE_WORK_GROUP_SIZE is a frontend link artifact (ProgramObject::GetComputeLocalSize, what GL_Program.cpp answers from), which AdvertisedLimitsScenario.ComputeLocalSizeComesFromTheLinkedProgram already pins. The MGPCaps size assertion is a composition of sizeof(DynamicBackendParameters) and follows the struct. - AdvertisedLimitsScenario.ComputeWorkGroupLimitsAreTheCapsBlocksAnswer pins, on both lanes: answerability, the GL 4.3 floors, vector/indexed agreement, INVALID_VALUE past axis 2, and - through the new Harness/BackendCapsPeek translation unit, which is the one place the module looks past the GL API - that max(caps, minimum) equals the live glGetIntegeri_v answer axis by axis. Shown live by halving each backend's caps copy: both lanes fail with "MGPCaps carries 512 but glGetIntegeri_v answers 1024". On Android the module links the shipping .so (hidden visibility), so the peek returns false there and only the GL-visible half runs. ComputeWorkGroupCapabilities.TakesEveryAxisFromTheIndexedQuery in BackendLoaderTest pins the DirectGLES loader half against the fake driver, per axis and above the initialisers. - Verified: AdvertisedLimitsScenario 20/20 on DirectGLES and DirectVulkan (llvmpipe / lavapipe), BackendLoaderTest green. --- MobileGL/MG_Backend/BackendObject.h | 13 ++++ .../DirectGLES/BackendObject_DirectGLES.cpp | 7 +++ .../BackendObject_DirectVulkan.cpp | 9 +++ .../MG_Backend/DirectVulkan/DirectVulkan.cpp | 5 +- MobileGL/MG_IntegrationTest/CMakeLists.txt | 1 + .../Harness/BackendCapsPeek.cpp | 42 +++++++++++++ .../Harness/BackendCapsPeek.h | 29 +++++++++ .../Scenarios/AdvertisedLimitsScenario.cpp | 61 +++++++++++++++++++ MobileGL/MG_Pipe/MGPipeTypes.h | 5 +- MobileGL/MG_Pipe/PipeCalls.def | 21 +++++-- .../BackendLoader/BackendLoaderTest.cpp | 41 ++++++++++++- .../MG_Util/BackendLoaders/OpenGL/Loader.cpp | 21 +++++++ .../MG_Util/BackendLoaders/OpenGL/Loader.h | 5 ++ .../MG_Util/BackendLoaders/Vulkan/Loader.cpp | 8 +++ .../MG_Util/BackendLoaders/Vulkan/Loader.h | 5 ++ 15 files changed, 263 insertions(+), 10 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/Harness/BackendCapsPeek.cpp create mode 100644 MobileGL/MG_IntegrationTest/Harness/BackendCapsPeek.h diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 6b2c0231..70cd3c78 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -378,6 +378,19 @@ namespace MobileGL { Int MaxFragmentShaderStorageBlocks = 8; Int MaxComputeUniformBlocks = 12; Int MaxComputeWorkGroupInvocations = 128; + // GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE, one value per + // axis. These six, with the invocations limit above, are the only indexed limits a + // backend genuinely OWNS - the device answers them (glGetIntegeri_v on DirectGLES, + // VkPhysicalDeviceLimits::maxComputeWorkGroupCount/Size on DirectVulkan) - and so + // the only ones that survive the retirement of the GetIntegeri_v table entry: they + // cross the MGPipe boundary inside MGPCaps, by inclusion of this struct (plan B + // section 4.4.1). Every other indexed pname names frontend state. RAW driver + // answers, like the invocations limit: GL_Getter and the compile environment floor + // them at the shared MIN_COMPUTE_WORK_GROUP_* minimums themselves. The defaults are + // the GL 4.3 core minimums (table 23.60) and describe the no-backend case, as + // MaxClipDistances' does. + Int MaxComputeWorkGroupCount[3] = {65535, 65535, 65535}; + Int MaxComputeWorkGroupSize[3] = {1024, 1024, 64}; Int MaxShaderStorageBufferBindings = 8; Int MaxTextureBufferSize = 65536; // GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained. diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index c8d7c5a1..b36af187 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -1415,6 +1415,13 @@ namespace MobileGL::MG_Backend::DirectGLES { clampStageStorageBlocks(m_GLESCapabilities.MaxFragmentShaderStorageBlocks); m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks; m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations; + // The six per-axis compute limits: the driver's raw glGetIntegeri_v answers, the same + // numbers GLFunctionsTable::GetIntegeri_v forwards live. Carried here so that MGPCaps has + // them once the table entry retires (plan B section 4.4.1); GL_Getter floors them. + for (SizeT axis = 0; axis < 3; ++axis) { + m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_GLESCapabilities.MaxComputeWorkGroupCount[axis]; + m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_GLESCapabilities.MaxComputeWorkGroupSize[axis]; + } // (MaxShaderStorageBufferBindings is assigned above, before the per-stage clamp reads it.) // This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and // on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index e113d81a..3a3bc0d1 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -937,6 +937,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { clampLimit("GL_MAX_COMPUTE_UNIFORM_BLOCKS", m_vulkanCaps.MaxComputeUniformBlocks, kMaxAdvertisedBufferBlocks); m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations; + // The six per-axis compute limits, from the same VkPhysicalDeviceLimits fields + // GLFunctionsTable::GetIntegeri_v (DirectVulkan.cpp) reads live. Carried here so that + // MGPCaps has them once the table entry retires (plan B section 4.4.1); GL_Getter floors + // them. Not clamped: unlike the block counts these are not amounts an application + // allocates, and the frontend already raises them to the GL minimum. + for (SizeT axis = 0; axis < 3; ++axis) { + m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_vulkanCaps.MaxComputeWorkGroupCount[axis]; + m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_vulkanCaps.MaxComputeWorkGroupSize[axis]; + } m_dynamicParameters.MaxShaderStorageBufferBindings = clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings, kMaxAdvertisedBufferBlocks); diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index 33789e61..bb778cc2 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -674,7 +674,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // The two compute limits are the only indexed pnames a backend genuinely owns: they come // from the physical device, and MG_Impl/GLImpl/Getter/GL_Getter.cpp asks for them here so it - // can raise the answer to the GL required minimum. Every other indexed pname names FRONTEND + // can raise the answer to the GL required minimum. The same six numbers are carried in + // DynamicBackendParameters::MaxComputeWorkGroupCount/Size (filled at capability init from + // the same limits), which is their MGPCaps carrier once this entry retires - the + // AdvertisedLimitsScenario pins the two against each other. Every other indexed pname names FRONTEND // state (the indexed buffer bindings, the per-unit texture/sampler bindings, the image-unit // bindings, the viewport rectangles, the indexed capabilities) and is answered there before // the table is consulted, so the arms this function used to carry for diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 7dad1f1d..83e93597 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -51,6 +51,7 @@ endif() add_executable(MobileGLIntegrationTest Main.cpp Harness/HeadlessGL.cpp + Harness/BackendCapsPeek.cpp Scenarios/OrientationScenario.cpp Scenarios/CrossFrameBufferScenario.cpp Scenarios/ResidentIndexScenario.cpp diff --git a/MobileGL/MG_IntegrationTest/Harness/BackendCapsPeek.cpp b/MobileGL/MG_IntegrationTest/Harness/BackendCapsPeek.cpp new file mode 100644 index 00000000..03e3e95e --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Harness/BackendCapsPeek.cpp @@ -0,0 +1,42 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Harness/BackendCapsPeek.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "BackendCapsPeek.h" + +#if !defined(__ANDROID__) +#include + +namespace MobileGL::MG_Backend { + // Declared in MG_Backend/BackendObjects.h, which also pulls in both backends' headers + // and, through them, their loaders; the reference alone is all that is needed here. + extern UniquePtr& pActiveBackendObject; +} // namespace MobileGL::MG_Backend +#endif + +namespace MGITest { + + bool PeekComputeWorkGroupCaps(int outCount[3], int outSize[3]) { +#if defined(__ANDROID__) + (void)outCount; + (void)outSize; + return false; +#else + const auto& backend = MobileGL::MG_Backend::pActiveBackendObject; + if (!backend) { + return false; + } + const MobileGL::MG_Backend::DynamicBackendParameters& caps = backend->GetDynamicParameters(); + for (int axis = 0; axis < 3; ++axis) { + outCount[axis] = caps.MaxComputeWorkGroupCount[axis]; + outSize[axis] = caps.MaxComputeWorkGroupSize[axis]; + } + return true; +#endif + } + +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Harness/BackendCapsPeek.h b/MobileGL/MG_IntegrationTest/Harness/BackendCapsPeek.h new file mode 100644 index 00000000..773b5dba --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Harness/BackendCapsPeek.h @@ -0,0 +1,29 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Harness/BackendCapsPeek.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// The one place this module looks past the GL API into the active backend's caps block. +// +// It exists for exactly one assertion: that the six per-axis compute limits the MGPipe +// caps block carries (DynamicBackendParameters::MaxComputeWorkGroupCount/Size, plan B +// section 4.4.1) are the same numbers glGetIntegeri_v answers today, since P0.5 retires +// the getter in favour of the caps. A separate translation unit, because the scenario +// sources include the GL headers with prototypes and MobileGL's umbrella header is not +// meant to meet them in one file. + +#pragma once + +namespace MGITest { + + // Copies the active backend's MaxComputeWorkGroupCount / MaxComputeWorkGroupSize into the + // two arrays and returns true. Returns false, touching nothing, where the caps block is + // out of reach: on Android this module links the SHIPPING libMobileGL.so, built + // -fvisibility=hidden, so no internal symbol resolves; on desktop it links MobileGL_s and + // the read is direct. + bool PeekComputeWorkGroupCaps(int outCount[3], int outSize[3]); + +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp index 0e929618..479683a3 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp @@ -26,9 +26,11 @@ // quantities, so an entry that only fails on DirectVulkan is a translation bug and one that // fails on both is a table bug. +#include #include #include +#include "../Harness/BackendCapsPeek.h" #include "../Harness/HeadlessGL.h" #include "../Harness/ScenarioFixture.h" @@ -564,5 +566,64 @@ void main() { g_data[gl_LocalInvocationIndex] = 1u; } (void)FirstGLError(); } + // THE SIX COMPUTE LIMITS THAT OUTLIVE THE GETTER. GL_MAX_COMPUTE_WORK_GROUP_COUNT and + // GL_MAX_COMPUTE_WORK_GROUP_SIZE, three axes each, are the only indexed pnames the + // DEVICE answers rather than the frontend (glGetIntegeri_v on Espryt, VkPhysicalDevice- + // Limits on Magma), and therefore the only ones that have to cross the MGPipe boundary + // once GetIntegeri_v is retired (plan B section 4.4.6 / P0.5). They ride in MGPCaps by + // inclusion, as DynamicBackendParameters::MaxComputeWorkGroupCount/Size, filled by both + // backends at capability init. This case pins that the caps copy and the live getter + // answer are one number - the getter floors the backend's raw answer at the GL 4.3 + // minimum, so the comparison is against the floored caps value - and pins the + // GL-visible half on every lane: answerability, the floors, vector/indexed agreement + // and the index bound. On a lane where the caps block is out of reach (Android links + // the shipping .so) only the GL-visible half runs. + TEST_F(AdvertisedLimitsScenario, ComputeWorkGroupLimitsAreTheCapsBlocksAnswer) { + struct Axis { + GLenum pname; + const char* name; + GLint minimum[3]; // GL 4.3 core table 23.60 + }; + const Axis axes[] = { + {GL_MAX_COMPUTE_WORK_GROUP_COUNT, "GL_MAX_COMPUTE_WORK_GROUP_COUNT", {65535, 65535, 65535}}, + {GL_MAX_COMPUTE_WORK_GROUP_SIZE, "GL_MAX_COMPUTE_WORK_GROUP_SIZE", {1024, 1024, 64}}, + }; + int capsCount[3] = {0, 0, 0}; + int capsSize[3] = {0, 0, 0}; + const bool capsVisible = PeekComputeWorkGroupCaps(capsCount, capsSize); + + for (const Axis& axis : axes) { + GLint indexed[3] = {-1, -1, -1}; + for (GLuint i = 0; i < 3; ++i) { + glGetIntegeri_v(axis.pname, i, &indexed[i]); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << axis.name << "[" << i << "]"; + EXPECT_GE(indexed[i], axis.minimum[i]) + << axis.name << "[" << i << "] = " << indexed[i] + << " is below the GL 4.3 core table 23.60 minimum " << axis.minimum[i]; + } + GLint vector[3] = {-1, -1, -1}; + glGetIntegerv(axis.pname, vector); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << axis.name; + for (int i = 0; i < 3; ++i) { + EXPECT_EQ(vector[i], indexed[i]) + << axis.name << "[" << i << "]: the vector query and the indexed query disagree"; + } + GLint outOfRange = -424242; + glGetIntegeri_v(axis.pname, 3, &outOfRange); + EXPECT_EQ(FirstGLError(), GLenum(GL_INVALID_VALUE)) + << axis.name << "[3]: an index past the three axes is INVALID_VALUE (GL 4.6 core 22.1)"; + + if (!capsVisible) continue; + const int* capsAxis = axis.pname == GL_MAX_COMPUTE_WORK_GROUP_COUNT ? capsCount : capsSize; + for (int i = 0; i < 3; ++i) { + EXPECT_EQ(std::max(capsAxis[i], axis.minimum[i]), indexed[i]) + << axis.name << "[" << i << "]: MGPCaps carries " << capsAxis[i] + << " but glGetIntegeri_v answers " << indexed[i] + << " - the caps block and the getter path must be one number, because P0.5 retires " + "the getter in favour of the caps"; + } + } + } + } // namespace } // namespace MGITest diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index 227338d3..133fff8f 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -122,7 +122,10 @@ namespace MobileGL::MG_Pipe { struct MGPCaps { // The ~90 flat scalars the backends already publish, by inclusion rather than by - // restatement: a caps field added there must not need a second edit here. + // restatement: a caps field added there must not need a second edit here. This is + // also where the six per-axis compute limits (MaxComputeWorkGroupCount/Size) ride - + // the only indexed answers the device owns, and therefore the only ones that outlive + // the GetIntegeri_v table entry (see the PipeCalls.def footer). DynamicBackendParameters Dynamic; Uint64 CallMask; // MGPCapBit // The two halves that are not flat PODs travel as blobs: the format capability diff --git a/MobileGL/MG_Pipe/PipeCalls.def b/MobileGL/MG_Pipe/PipeCalls.def index 799fdea6..05db1f18 100644 --- a/MobileGL/MG_Pipe/PipeCalls.def +++ b/MobileGL/MG_Pipe/PipeCalls.def @@ -140,9 +140,18 @@ X(SetSwapInterval, MGPSwapInterval, kCtxVerb, kOptional) // clang-format on -// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"): GetIntegeri_v, -// GetInteger64i_v, GetProgramiv (only GL_COMPUTE_WORK_GROUP_SIZE is a real backend answer -// and it lives in MGPCaps), ShaderStorageBlockBinding (folded into MGPProgramDesc's -// reflection archive), set_pixel_unpack_state (no such state crosses the line - plan 4.6 -// D5), a compressed-format concept, pipe_transfer, and the stage dimension of -// set_sampler_views (MobileGL's texture unit space is merged, not per stage - plan 4.4.3). +// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"): +// - GetIntegeri_v / GetInteger64i_v. The six backend-owned answers they carry - +// GL_MAX_COMPUTE_WORK_GROUP_COUNT and GL_MAX_COMPUTE_WORK_GROUP_SIZE, three axes each, +// the only indexed pnames the device rather than the frontend answers - live in MGPCaps +// as DynamicBackendParameters::MaxComputeWorkGroupCount / MaxComputeWorkGroupSize, filled +// by both backends at capability init (DirectGLES from glGetIntegeri_v, DirectVulkan from +// VkPhysicalDeviceLimits) and floored by the frontend. Every other indexed pname names +// frontend state and is answered before any table is consulted. +// - GetProgramiv. GL_COMPUTE_WORK_GROUP_SIZE is a FRONTEND link artifact +// (ProgramObject::GetComputeLocalSize, what GL_Program.cpp has always answered from), not +// a backend answer at all; nothing a backend knows about a program crosses this way. +// - ShaderStorageBlockBinding (folded into MGPProgramDesc's reflection archive), +// set_pixel_unpack_state (no such state crosses the line - plan 4.6 D5), a +// compressed-format concept, pipe_transfer, and the stage dimension of set_sampler_views +// (MobileGL's texture unit space is merged, not per stage - plan 4.4.3). diff --git a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp index 48ac8797..ed10ea49 100644 --- a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp +++ b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp @@ -37,6 +37,11 @@ namespace { std::size_t ioBlockDraws = 0; // Behavior knobs, configured per test before running the probe. GLint maxVertexSsboBlocks = 4; + // GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE per axis, answered through glGetIntegeri_v. + // Above the GL minimums and distinct per axis, so a loader that left an initialiser in + // place or copied one axis into another is caught. + GLint maxComputeWorkGroupCount[3] = {70001, 70002, 70003}; + GLint maxComputeWorkGroupSize[3] = {1500, 1501, 100}; GLint glesMajorVersion = 3; GLint glesMinorVersion = 1; GLint maxVertexImageUniforms = 2; @@ -460,8 +465,20 @@ namespace { if (data == nullptr) return; for (int i = 0; i < 4; ++i) data[i] = GL_TRUE; }; - funcs.glGetIntegeri_v = [](GLenum, GLuint, GLint* data) { - if (data != nullptr) *data = 0; + funcs.glGetIntegeri_v = [](GLenum pname, GLuint index, GLint* data) { + if (data == nullptr) return; + *data = 0; + if (index >= 3) return; + switch (pname) { + case GL_MAX_COMPUTE_WORK_GROUP_COUNT: + *data = g_fake.maxComputeWorkGroupCount[index]; + break; + case GL_MAX_COMPUTE_WORK_GROUP_SIZE: + *data = g_fake.maxComputeWorkGroupSize[index]; + break; + default: + break; + } }; funcs.glGetProgramInfoLog = [](GLuint, GLsizei bufSize, GLsizei* length, GLchar* infoLog) { if (infoLog != nullptr && bufSize > 0) infoLog[0] = '\0'; @@ -1551,3 +1568,23 @@ TEST(LocatedIoBlockProbe, ReportsTheDefectOnlyWhenTheUnlocatedControlCarriesTheP EXPECT_FALSE(ProbeLocatedIoBlocksLosePayload(crippled).detected); EXPECT_EQ(g_fake.ioBlockDraws, 0u) << "an entry-point-gated probe must not draw at all"; } + +// The six per-axis compute limits are the backend-owned answers that cross the MGPipe boundary +// inside MGPCaps (DynamicBackendParameters::MaxComputeWorkGroupCount/Size), so the loader has +// to take EACH axis from glGetIntegeri_v rather than leave an initialiser - or one axis's +// answer - in the other slots. The integration side (AdvertisedLimitsScenario) pins the copy +// against the live getter on both backends; this pins the driver-to-caps step on its own. +TEST(ComputeWorkGroupCapabilities, TakesEveryAxisFromTheIndexedQuery) { + const auto funcs = MakeFakeGLESFunctions(); + ResetFakeDriver(); + MobileGL::MG_External::GLESCapabilities caps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs)); + for (int axis = 0; axis < 3; ++axis) { + EXPECT_EQ(caps.MaxComputeWorkGroupCount[axis], g_fake.maxComputeWorkGroupCount[axis]) << "axis " << axis; + EXPECT_EQ(caps.MaxComputeWorkGroupSize[axis], g_fake.maxComputeWorkGroupSize[axis]) << "axis " << axis; + } + // The initialisers are the GL 4.3 minimums and every fake answer is above them, so a + // value equal to its initialiser here would mean the query never ran. + EXPECT_GT(caps.MaxComputeWorkGroupCount[0], 65535); + EXPECT_GT(caps.MaxComputeWorkGroupSize[2], 64); +} diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index eb143d0d..4584572e 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -1145,6 +1145,8 @@ namespace MobileGL::MG_Util::BackendLoader { GLint maxFragmentShaderStorageBlocks = 4; GLint maxComputeUniformBlocks = 12; GLint maxComputeWorkGroupInvocations = 128; + GLint maxComputeWorkGroupCount[3] = {65535, 65535, 65535}; + GLint maxComputeWorkGroupSize[3] = {1024, 1024, 64}; GLint maxShaderStorageBufferBindings = 8; GLint maxTextureBufferSize = 65536; GLint maxUniformBufferBindings = 24; @@ -1276,6 +1278,17 @@ namespace MobileGL::MG_Util::BackendLoader { glesFuncs.glGetIntegerv(GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, &maxCombinedShaderStorageBlocks); glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_UNIFORM_BLOCKS, &maxComputeUniformBlocks); glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &maxComputeWorkGroupInvocations); + // The per-axis pair beside it, through the indexed query. ES 3.1 core like the + // invocations limit, so it sits inside the same bracketed run: a 3.0 context rejects + // it, the drain below swallows the error and the locals keep the GL 4.3 minimums. + // These are the six backend-owned indexed answers that cross the MGPipe boundary in + // MGPCaps (DynamicBackendParameters::MaxComputeWorkGroupCount/Size). + if (glesFuncs.glGetIntegeri_v) { + for (GLuint axis = 0; axis < 3; ++axis) { + glesFuncs.glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, axis, &maxComputeWorkGroupCount[axis]); + glesFuncs.glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, axis, &maxComputeWorkGroupSize[axis]); + } + } glesFuncs.glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &maxShaderStorageBufferBindings); // GL_MAX_TEXTURE_BUFFER_SIZE is deliberately NOT batched here: like // GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT below, the pname only exists once buffer textures do, @@ -1584,6 +1597,10 @@ namespace MobileGL::MG_Util::BackendLoader { caps.MaxFragmentShaderStorageBlocks = maxFragmentShaderStorageBlocks; caps.MaxComputeUniformBlocks = maxComputeUniformBlocks; caps.MaxComputeWorkGroupInvocations = maxComputeWorkGroupInvocations; + for (SizeT axis = 0; axis < 3; ++axis) { + caps.MaxComputeWorkGroupCount[axis] = maxComputeWorkGroupCount[axis]; + caps.MaxComputeWorkGroupSize[axis] = maxComputeWorkGroupSize[axis]; + } caps.MaxShaderStorageBufferBindings = maxShaderStorageBufferBindings; caps.MaxTextureBufferSize = maxTextureBufferSize; // Through glesFuncs, like every other capability query here: a bare glGetIntegerv resolves @@ -1681,6 +1698,10 @@ namespace MobileGL::MG_Util::BackendLoader { MGLOG_I(" GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS: %d", caps.MaxFragmentShaderStorageBlocks); MGLOG_I(" GL_MAX_COMPUTE_UNIFORM_BLOCKS: %d", caps.MaxComputeUniformBlocks); MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS: %d", caps.MaxComputeWorkGroupInvocations); + MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_COUNT: %d %d %d", caps.MaxComputeWorkGroupCount[0], + caps.MaxComputeWorkGroupCount[1], caps.MaxComputeWorkGroupCount[2]); + MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_SIZE: %d %d %d", caps.MaxComputeWorkGroupSize[0], + caps.MaxComputeWorkGroupSize[1], caps.MaxComputeWorkGroupSize[2]); MGLOG_I(" GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS: %d", caps.MaxShaderStorageBufferBindings); // Three distinct states, and the suffix must not conflate them: a driver answer, a floor // kept because there are no buffer textures to ask about, and a floor kept because the diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index d8b3b02f..e0d41fd3 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -1309,6 +1309,11 @@ namespace MobileGL { Int MaxFragmentShaderStorageBlocks = 4; Int MaxComputeUniformBlocks = 12; Int MaxComputeWorkGroupInvocations = 128; + // GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE per axis, as the driver answers + // glGetIntegeri_v. Raw: the frontend floors them at the GL minimums itself. The + // initialisers are those minimums, for a context that rejects the query. + Int MaxComputeWorkGroupCount[3] = {65535, 65535, 65535}; + Int MaxComputeWorkGroupSize[3] = {1024, 1024, 64}; Int MaxShaderStorageBufferBindings = 8; Int MaxTextureBufferSize = 65536; // GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained. diff --git a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp index e2d9a364..2a7126c2 100644 --- a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp @@ -196,6 +196,10 @@ namespace MobileGL::MG_Util::BackendLoader { caps.MaxCombinedShaderStorageBlocks = SaturateToInt(p.limits.maxDescriptorSetStorageBuffers); caps.MaxComputeUniformBlocks = SaturateToInt(p.limits.maxPerStageDescriptorUniformBuffers); caps.MaxComputeWorkGroupInvocations = SaturateToInt(p.limits.maxComputeWorkGroupInvocations); + for (SizeT axis = 0; axis < 3; ++axis) { + caps.MaxComputeWorkGroupCount[axis] = SaturateToInt(p.limits.maxComputeWorkGroupCount[axis]); + caps.MaxComputeWorkGroupSize[axis] = SaturateToInt(p.limits.maxComputeWorkGroupSize[axis]); + } caps.MaxShaderStorageBufferBindings = SaturateToInt(p.limits.maxDescriptorSetStorageBuffers); caps.MaxTextureBufferSize = SaturateToInt(p.limits.maxTexelBufferElements); caps.TextureBufferOffsetAlignment = @@ -333,6 +337,10 @@ namespace MobileGL::MG_Util::BackendLoader { caps.MaxCombinedShaderStorageBlocks = SaturateToInt(properties.limits.maxDescriptorSetStorageBuffers); caps.MaxComputeUniformBlocks = SaturateToInt(properties.limits.maxPerStageDescriptorUniformBuffers); caps.MaxComputeWorkGroupInvocations = SaturateToInt(properties.limits.maxComputeWorkGroupInvocations); + for (SizeT axis = 0; axis < 3; ++axis) { + caps.MaxComputeWorkGroupCount[axis] = SaturateToInt(properties.limits.maxComputeWorkGroupCount[axis]); + caps.MaxComputeWorkGroupSize[axis] = SaturateToInt(properties.limits.maxComputeWorkGroupSize[axis]); + } caps.MaxShaderStorageBufferBindings = SaturateToInt(properties.limits.maxDescriptorSetStorageBuffers); caps.MaxTextureBufferSize = SaturateToInt(properties.limits.maxTexelBufferElements); caps.TextureBufferOffsetAlignment = diff --git a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h index 33907086..2e370cf8 100644 --- a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h @@ -56,6 +56,11 @@ namespace MobileGL { Int MaxCombinedShaderStorageBlocks = 32; Int MaxComputeUniformBlocks = 12; Int MaxComputeWorkGroupInvocations = 128; + // VkPhysicalDeviceLimits::maxComputeWorkGroupCount / maxComputeWorkGroupSize per + // axis, saturated to Int like every other limit here. Raw: the frontend floors them + // at the GL minimums itself. + Int MaxComputeWorkGroupCount[3] = {65535, 65535, 65535}; + Int MaxComputeWorkGroupSize[3] = {1024, 1024, 64}; Int MaxShaderStorageBufferBindings = 8; Int MaxTextureBufferSize = 65536; // GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.