[Fix] (Getter): answer the GL 4.6 limit surface honestly - tess/cull/subroutine pnames, TBuiltInResource drift, 84 UBO binding points, 64-bit GL_MAX_ELEMENT_INDEX, per-category sample truth

This commit is contained in:
2026-08-27 03:47:16 -04:00
parent 07669aacd4
commit 7168f2ef77
24 changed files with 905 additions and 120 deletions
+10 -3
View File
@@ -1577,8 +1577,13 @@ namespace MobileGL::MG_Impl::GLImpl {
std::to_string(id) + " is not a transform feedback object name."));
return;
}
// GL_MAX_VERTEX_STREAMS is 1, so stream 0 is the only one that exists.
if (stream != 0) {
// GL 4.6 core 10.3.7 bounds `stream` by GL_MAX_VERTEX_STREAMS, which is 4. Only stream 0
// can ever have been written - nothing in the shader pipeline supports
// layout(stream = N) - so a higher stream captured zero vertices and the draw is a legal
// no-op rather than an error. The bound is read from the getter so the two cannot drift.
GLint maxVertexStreams = 1;
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
if (stream >= static_cast<GLuint>(std::max(maxVertexStreams, 1))) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
@@ -1596,7 +1601,9 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
const Uint64 vertices = MG_State::pGLContext->GetTransformFeedbackRecordedVertices(id);
// Only stream 0 ever records anything (see the stream bound above), so a higher stream
// replays nothing.
const Uint64 vertices = stream == 0 ? MG_State::pGLContext->GetTransformFeedbackRecordedVertices(id) : 0;
if (vertices == 0) return;
const auto count = static_cast<GLsizei>(vertices);
AccountTransformFeedbackPrimitives(mode, count);
@@ -688,16 +688,15 @@ namespace MobileGL::MG_Impl::GLImpl {
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own
// (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
// The multisample TEXTURE path resolves the limit per format the same way
// (GL_Texture.cpp, GetMaxSupportedTextureSamples). Both are floored to the value MobileGL
// advertises: on a driver where the two differ - Adreno reports GL_MAX_SAMPLES 4 and
// GL_MAX_INTEGER_SAMPLES 1 - rejecting the advertised count here only moves the failure
// from the driver into MobileGL, so the frontend accepts it and the backend clamps the
// count it actually hands the driver.
// (GL_Texture.cpp, GetMaxSupportedTextureSamples), and both now enforce exactly what their
// pname advertises. The integer ceiling used to be floored at GL_MAX_SAMPLES so that the
// frontend would accept a count it had advertised globally - but on Adreno and Mali the
// integer path is genuinely one sample, and accepting four only moved the failure from an
// honest INVALID_OPERATION here to a silently under-allocated renderbuffer.
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::max();
}
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format);
GLenum normalizedFormat = GL_RGBA;
@@ -711,10 +710,8 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!isIntegerFormat) {
return GetMaxRenderbufferSamples_State();
}
// Per-format still, but never below the ceiling glGetIntegerv(GL_MAX_SAMPLES) promised:
// the driver's raw GL_MAX_INTEGER_SAMPLES stays the *backend* limit and the backend
// clamps to it, while the frontend honours what it advertised.
return std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
// Exactly what glGetIntegerv(GL_MAX_INTEGER_SAMPLES) reports.
return GetAdvertisedIntegerMaxSamples();
}
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
+209 -21
View File
@@ -93,8 +93,15 @@ namespace MobileGL::MG_Impl::GLImpl {
// limits they advertise still have to be legal.
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
constexpr GLint kFrontendMaxDebugLoggedMessages = 1;
constexpr GLint kFrontendMaxVertexUniformComponents = 4096;
constexpr GLint kFrontendMaxVertexUniformVectors = 128;
// The *_VECTORS answers are the *_COMPONENTS ones divided by four, never a second
// literal: they used to be independent (4096 components against 128 vectors, 64 varying
// components against 8 varying vectors) and could not both be describing the same
// capacity. Both are shared with BuildTBuiltInResource through Types.h, because
// gl_MaxVertexUniformVectors and gl_MaxVaryingVectors expand from the same numbers.
constexpr GLint kFrontendMaxVertexUniformComponents =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_COMPONENTS);
constexpr GLint kFrontendMaxVertexUniformVectors =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_VECTORS);
constexpr GLint kFrontendMaxVertexUniformBlocks = 14;
constexpr GLint kFrontendMaxVertexOutputComponents = 64;
constexpr GLint kFrontendMaxFragmentInputComponents = 128;
@@ -106,21 +113,50 @@ namespace MobileGL::MG_Impl::GLImpl {
constexpr GLint kFrontendMaxGeometryTextureImageUnits = 16;
constexpr GLint kFrontendMaxGeometryUniformComponents = 1024;
constexpr GLint kFrontendMaxGeometryUniformBlocks = 14;
constexpr GLint kFrontendMaxCombinedUniformBlocks = kFrontendMaxVertexUniformBlocks +
kFrontendMaxGeometryUniformBlocks +
kFrontendMaxFragmentUniformBlocks;
constexpr GLint kFrontendMaxVaryingComponents = 64;
constexpr GLint kFrontendMaxVaryingVectors = 8;
// ARB_geometry_shader4's per-invocation count. No TBuiltInResource field and no
// gl_MaxGeometryShaderInvocations built-in exists to keep in step, so this is a getter
// answer only; 32 is the GL 4.6 core minimum (table 23.57).
constexpr GLint kFrontendMaxGeometryShaderInvocations = 32;
constexpr GLint kFrontendMaxTessControlUniformBlocks = 14;
constexpr GLint kFrontendMaxTessEvaluationUniformBlocks = 14;
// GL 4.6's minimum is 14 uniform blocks on each of the FIVE graphics stages (70), not
// three: the two tessellation stages were simply missing from this sum, so even a
// frontend with enough binding points advertised 42.
constexpr GLint kFrontendMaxCombinedUniformBlocks =
kFrontendMaxVertexUniformBlocks + kFrontendMaxTessControlUniformBlocks +
kFrontendMaxTessEvaluationUniformBlocks + kFrontendMaxGeometryUniformBlocks +
kFrontendMaxFragmentUniformBlocks;
constexpr GLint kFrontendMaxVaryingComponents =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VARYING_COMPONENTS);
constexpr GLint kFrontendMaxVaryingVectors =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VARYING_VECTORS);
constexpr GLint kFrontendMaxProgramTexelOffset = 7;
constexpr GLint kFrontendMinProgramTexelOffset = -8;
constexpr GLint kFrontendMaxTransformFeedbackInterleavedComponents = 64;
constexpr GLint kFrontendMaxTransformFeedbackSeparateAttribs = 4;
constexpr GLint kFrontendMaxTransformFeedbackSeparateComponents = 4;
// ARB_transform_feedback3's vertex-stream count; see the GL_MAX_VERTEX_STREAMS case for
// what streams 1..3 mean in an implementation that can only emit to stream 0.
constexpr GLint kFrontendMaxVertexStreams = 4;
constexpr GLint kFrontendMaxGeometryOutputVertices = 256;
constexpr GLint kFrontendMaxGeometryTotalOutputComponents = 1024;
constexpr GLint kFrontendMinUniformBufferBindings = 36;
// GL 4.5 core table 23.64 requires 84 indexed uniform binding points, and that is exactly
// how wide the state layer's array is (BufferState::BufferBindingPointCount) - see the
// GL_MAX_UNIFORM_BUFFER_BINDINGS case for why the ES driver's own, smaller count is not
// the ceiling here.
constexpr GLint kFrontendMinUniformBufferBindings = 84;
constexpr GLint kFrontendSubpixelBits = 4;
constexpr GLint kFrontendMaxSamples = 4;
constexpr GLint kFrontendMaxSamples =
static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_ADVERTISED_MAX_SAMPLES);
// ARB_shader_subroutine's two limits. NOTHING IMPLEMENTS SUBROUTINES: there is no
// glGetSubroutineIndex / glUniformSubroutinesuiv, only the program-interface enum
// plumbing. These are answered - with the GL 4.5 core minimums - because the conformance
// suite queries them before it checks for the feature and an INVALID_ENUM both leaves the
// caller reading its own uninitialised stack slot and strands an error for the next
// unrelated call to trip over. The extension is deliberately NOT advertised, so the
// numbers are a table entry, not a capability claim.
constexpr GLint kFrontendMaxSubroutines = 256;
constexpr GLint kFrontendMaxSubroutineUniformLocations = 1024;
// The floors under GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE. Shared with the compile
// pipeline (CaptureCompileEnv floors the same driver answers at them, and
@@ -455,10 +491,18 @@ namespace MobileGL::MG_Impl::GLImpl {
} // namespace
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so the driver's value is floored
// before it is advertised. Every other multisample ceiling MobileGL advertises has to be
// floored the same way: promising 4 samples globally while answering GL_MAX_INTEGER_SAMPLES
// 1 - which is exactly what Adreno reports - makes the frontend reject the very count it
// just told the application to use. The backends clamp the realised count instead.
// before it is advertised. gl_MaxSamples expands from the same floored number
// (BuildTBuiltInResource), which is also what sizes gl_SampleMask[].
//
// THE FLOOR STOPS HERE, and that is the point. It used to be applied to
// GL_MAX_INTEGER_SAMPLES, GL_MAX_COLOR_TEXTURE_SAMPLES and GL_MAX_DEPTH_TEXTURE_SAMPLES too,
// on the reasoning that an application reads GL_MAX_SAMPLES once and hands that count to
// every glTexStorage*Multisample. Table 23.53 gives those three a minimum of ONE, and the
// reasoning had it backwards: Adreno and Mali back an integer multisample texture with a
// single sample, so flooring the query at 4 did not make four samples exist - it made the
// backend silently under-allocate (ClampSamplesToBackendSupport) while the application wrote
// per-sample data it could never read back. Reporting what was probed turns that into an
// honest "unsupported" the application can branch on.
GLint GetAdvertisedMaxSamples() {
if (MG_Backend::pActiveBackendObject == nullptr) {
return kFrontendMaxSamples;
@@ -466,6 +510,30 @@ namespace MobileGL::MG_Impl::GLImpl {
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, kFrontendMaxSamples);
}
// GL 4.6 core table 23.53 minimum for the per-category multisample ceilings. One, not four:
// see the note on GetAdvertisedMaxSamples. A zero would be a probe that never ran, so it is
// floored rather than trusted.
namespace {
GLint AdvertisedCategoryMaxSamples(Int MG_Backend::DynamicBackendParameters::*categoryLimit) {
if (MG_Backend::pActiveBackendObject == nullptr) {
return 1;
}
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().*categoryLimit, 1);
}
} // namespace
GLint GetAdvertisedColorTextureMaxSamples() {
return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxColorTextureSamples);
}
GLint GetAdvertisedDepthTextureMaxSamples() {
return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxDepthTextureSamples);
}
GLint GetAdvertisedIntegerMaxSamples() {
return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxIntegerSamples);
}
// Declared in GL_Getter.h, so that the draw path can feed the same number to the reserved
// gl_NumSamples stand-in that glGetIntegerv(GL_SAMPLES) reports.
GLint ResolveDrawFramebufferSampleCount() {
@@ -1227,6 +1295,13 @@ namespace MobileGL::MG_Impl::GLImpl {
}
switch (pname) {
case GL_MAX_ELEMENT_INDEX:
// The largest value a GL_UNSIGNED_INT index may take. It has to be answered HERE and
// not left to the 32-bit fallback below: the conformance suite reads it with
// glGetInteger64v, and widening the saturated GLint would report INT32_MAX where the
// spec requires 2^32-1.
params[0] = 0xFFFFFFFFLL;
return;
case GL_MAX_SHADER_STORAGE_BLOCK_SIZE:
if (MG_Backend::pActiveBackendObject) {
params[0] = static_cast<GLint64>(
@@ -1706,6 +1781,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_GEOMETRY_UNIFORM_COMPONENTS:
*params = kFrontendMaxGeometryUniformComponents;
return;
case GL_MAX_GEOMETRY_SHADER_INVOCATIONS:
*params = kFrontendMaxGeometryShaderInvocations;
return;
case GL_MAX_IMAGE_SAMPLES:
*params = 0; // multisampled image load/store is not exposed by the DirectGLES frontend
return;
@@ -1759,6 +1837,59 @@ namespace MobileGL::MG_Impl::GLImpl {
*params =
StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessEvaluationShaderStorageBlocks);
return;
// The tessellation per-stage resource limits. Every one of these is ALSO a GLSL built-in
// constant that BuildTBuiltInResource expands, and the two must report the same number
// (KHR-GL45.limits.max_tess_* compares them directly) - which is why the values come from
// the shared block in MG_Util/ShaderTranspiler/Types.h rather than from literals here.
// They were the whole per-stage tess family: the table had been filled in only where the
// honest answer was zero (the atomic counters, the image uniforms) or where a driver
// query existed (GL_MAX_PATCH_VERTICES, GL_MAX_TESS_GEN_LEVEL), so every pname whose
// answer is a real resource count fell through to GL_INVALID_ENUM.
case GL_MAX_TESS_CONTROL_INPUT_COMPONENTS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_INPUT_COMPONENTS);
return;
case GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_OUTPUT_COMPONENTS);
return;
case GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS);
return;
case GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS);
return;
case GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_UNIFORM_COMPONENTS);
return;
case GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_INPUT_COMPONENTS);
return;
case GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_OUTPUT_COMPONENTS);
return;
case GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS);
return;
case GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_UNIFORM_COMPONENTS);
return;
case GL_MAX_TESS_PATCH_COMPONENTS:
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_PATCH_COMPONENTS);
return;
// Routed through the same clamp as every other per-stage block count so the
// MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >= per-stage ordering of
// GL 4.6 table 23.64 cannot be broken by the two families moving independently.
case GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS:
*params = ClampUniformBlockCount(kFrontendMaxTessControlUniformBlocks);
return;
case GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS:
*params = ClampUniformBlockCount(kFrontendMaxTessEvaluationUniformBlocks);
return;
case GL_MAX_SUBROUTINES:
*params = kFrontendMaxSubroutines;
return;
case GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS:
*params = kFrontendMaxSubroutineUniformLocations;
return;
case GL_MAX_TEXTURE_LOD_BIAS:
*params = 15; // TODO
return;
@@ -2174,7 +2305,12 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
case GL_MAX_ELEMENT_INDEX:
*params = 1024 * 1024; // TODO
// 64-bit state (see GetInteger64v); the 32-bit query saturates, per the GL
// state-query conversion rules - the same shape GL_MAX_SHADER_STORAGE_BLOCK_SIZE
// uses. The real answer is 2^32-1 because both backends draw with GL_UNSIGNED_INT
// indices and neither bounds an index value; the old `1024 * 1024` was a placeholder
// that no draw path ever consulted.
*params = INT32_MAX;
return;
case GL_CONTEXT_PROFILE_MASK:
// Reports the requested context profile (EGL defaults 3.x contexts to core);
@@ -2275,7 +2411,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = static_cast<GLint>(dynamicParameters.ViewportIndexProvokingVertex);
break;
case GL_MAX_COLOR_TEXTURE_SAMPLES:
*params = std::max(dynamicParameters.MaxColorTextureSamples, GetAdvertisedMaxSamples());
*params = GetAdvertisedColorTextureMaxSamples();
break;
case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS:
*params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents,
@@ -2305,7 +2441,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = dynamicParameters.MaxCubeMapTextureSize;
break;
case GL_MAX_DEPTH_TEXTURE_SAMPLES:
*params = std::max(dynamicParameters.MaxDepthTextureSamples, GetAdvertisedMaxSamples());
*params = GetAdvertisedDepthTextureMaxSamples();
break;
case GL_MAX_FRAMEBUFFER_WIDTH:
*params = dynamicParameters.MaxFramebufferWidth;
@@ -2332,7 +2468,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = dynamicParameters.MaxComputeImageUniforms;
break;
case GL_MAX_INTEGER_SAMPLES:
*params = std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
*params = GetAdvertisedIntegerMaxSamples();
break;
case GL_MAX_RENDERBUFFER_SIZE:
*params = dynamicParameters.MaxRenderbufferSize;
@@ -2356,12 +2492,43 @@ namespace MobileGL::MG_Impl::GLImpl {
for (Uint i = 0; i < 2; ++i) params[i] = static_cast<GLint>(std::lround(inner[i]));
break;
}
// GL 4.6 core table 23.66: whether the primitive-restart index terminates a patch.
// GL_FALSE is a legal answer and the true one - neither backend cuts a patch short, and
// the DirectVulkan draw path relies on this staying false (it resolves primitive restart
// to "never" for a PATCH_LIST topology on the strength of it).
case GL_PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED:
*params = GL_FALSE;
break;
case GL_MAX_PATCH_VERTICES:
*params = dynamicParameters.MaxPatchVertices;
break;
case GL_MAX_TESS_GEN_LEVEL:
*params = dynamicParameters.MaxTessGenLevel;
break;
// Same helper, and so the same arithmetic, as every other GL_MAX_COMBINED_*_UNIFORM_
// COMPONENTS: default-block components + blocks * (block size / 4). It reproduces the
// conformance suite's own formula exactly, so the two cannot drift.
case GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS:
*params = GetMaxCombinedUniformComponents(
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_UNIFORM_COMPONENTS),
kFrontendMaxTessControlUniformBlocks, dynamicParameters.MaxUniformBlockSize);
break;
case GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS:
*params = GetMaxCombinedUniformComponents(
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_UNIFORM_COMPONENTS),
kFrontendMaxTessEvaluationUniformBlocks, dynamicParameters.MaxUniformBlockSize);
break;
// ARB_cull_distance. Backend-derived exactly like GL_MAX_CLIP_DISTANCES beside it, and
// for a stronger reason: a cull distance discards the whole primitive, so advertising
// eight the rasterizer cannot serve turns every culling draw into a silent no-op. Zero is
// the honest answer on a host with no cull-distance route, and the conformance suite then
// skips the functional cases instead of failing them deep inside a pixel comparison.
case GL_MAX_CULL_DISTANCES:
*params = dynamicParameters.MaxCullDistances;
break;
case GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES:
*params = dynamicParameters.MaxCombinedClipAndCullDistances;
break;
case GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET:
*params = dynamicParameters.MinProgramTextureGatherOffset;
break;
@@ -2412,7 +2579,15 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxTransformFeedbackSeparateAttribs;
break;
case GL_MAX_VERTEX_STREAMS:
*params = 1;
// GL 4.5 core table 23.62 requires four. MobileGL can only ever EMIT to stream 0 -
// nothing in the shader pipeline supports layout(stream = N), EmitStreamVertex or a
// per-stream capture layout - but that is a statement about what a geometry shader
// may produce, not about which stream indices exist. Streams 1..3 exist and are
// permanently empty, and the two entry points that address a stream say so: an
// indexed primitive query on one answers zero (GL_Query's emptyVertexStream) and
// glDrawTransformFeedbackStream on one draws nothing. Answering 1 instead used to
// make both of them GL_INVALID_VALUE.
*params = kFrontendMaxVertexStreams;
break;
case GL_TRANSFORM_FEEDBACK_ACTIVE:
*params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0;
@@ -2429,15 +2604,28 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_TEXTURE_SIZE:
*params = dynamicParameters.MaxTextureSize;
break;
case GL_MAX_UNIFORM_BUFFER_BINDINGS:
case GL_MAX_UNIFORM_BUFFER_BINDINGS: {
// Never advertise more bindings than the state layer's indexed-binding array can track
// (BufferState::BufferBindingPointCount): glBindBufferBase rejects indices past that
// capacity, and the GL CTS per-case state reset calls glBindBufferBase on every
// advertised index and expects no error. The floor equals the GL 3.3 core minimum
// (36), so the clamp never under-advertises.
// advertised index and expects no error. The floor is the GL 4.5 core minimum, and
// the array was widened to exactly it, so the two coincide by construction.
//
// WHY THE BACKEND'S OWN COUNT IS NOT THE CEILING HERE, unlike the shader-storage
// family. A GL uniform binding point is where an APPLICATION parks a buffer; it is
// not a driver binding point. Neither backend forwards it as one on the draw path:
// DirectGLES rebinds the blocks a program declares onto COMPACTED ES points
// (BindCurrentProgramWithResources maps block i to ES point i+1) and DirectVulkan
// resolves each block to a descriptor. So what the host driver's count bounds is how
// many blocks ONE PROGRAM may use, which is GL_MAX_COMBINED_UNIFORM_BLOCKS (70) -
// inside the ES 3.2 minimum of 72 - and not how many points an application may bind.
static_assert(static_cast<GLint>(MG_State::GLState::BufferBindingPointCount) >=
kFrontendMinUniformBufferBindings,
"the indexed-binding array must be able to hold every advertised uniform binding point");
*params = std::clamp(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings,
static_cast<GLint>(MG_State::GLState::BufferBindingPointCount));
break;
}
case GL_MAX_UNIFORM_BLOCK_SIZE:
*params = dynamicParameters.MaxUniformBlockSize;
break;
+10 -2
View File
@@ -25,9 +25,17 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum GetError();
GLenum GetGraphicsResetStatus();
// The GL_MAX_SAMPLES value MobileGL advertises, i.e. the driver's value floored to the GL
// core minimum. Frontend multisample validators have to honour this ceiling for every
// format, otherwise MobileGL rejects a sample count it advertised itself.
// core minimum of 4. This is the RENDERBUFFER ceiling; the three per-category texture
// ceilings below have a minimum of one and are reported as probed.
GLint GetAdvertisedMaxSamples();
// Exactly what GL_MAX_COLOR_TEXTURE_SAMPLES / GL_MAX_DEPTH_TEXTURE_SAMPLES /
// GL_MAX_INTEGER_SAMPLES report: the probed backend limit floored at the GL 4.6 core minimum
// of ONE (table 23.53). Exported so the frontend's storage validation enforces exactly what
// the query promised - it used to floor both at 4 and then let the backend quietly
// under-allocate whatever the driver could not actually provide.
GLint GetAdvertisedColorTextureMaxSamples();
GLint GetAdvertisedDepthTextureMaxSamples();
GLint GetAdvertisedIntegerMaxSamples();
// What glGetIntegerv(GL_SAMPLES) answers for the CURRENT draw framebuffer: the largest sample
// count over its attachments, and 0 for a single-sample or default framebuffer (GL 4.6 core
// 9.2.3 / 22.2 - GL_SAMPLE_BUFFERS is 1 exactly when this is non-zero).
+48 -5
View File
@@ -40,6 +40,12 @@ namespace MobileGL::MG_Impl::GLImpl {
// stand in for the backend's.
Uint64 accountedCaptureDrawSnapshot = 0;
Uint64 geometryCaptureDrawSnapshot = 0;
// Set when glBeginQueryIndexed named a vertex stream above 0. MobileGL advertises
// GL_MAX_VERTEX_STREAMS = 4 because GL 4.5 requires it, and nothing in the shader
// pipeline can emit to a stream other than 0 - so the primitive count on any other
// stream is provably zero, and this makes the object report that instead of
// aliasing stream 0's backend counter.
Bool emptyVertexStream = false;
};
// Query calls may arrive from any thread (launchers migrate the context
@@ -116,6 +122,7 @@ namespace MobileGL::MG_Impl::GLImpl {
queryObject->ended = false;
queryObject->resultCached = false;
queryObject->cachedResult = 0;
queryObject->emptyVertexStream = false;
}
// Callers must hold g_queryObjectsMutex.
@@ -188,6 +195,23 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
// A span begun on a vertex stream this implementation can never emit to. The answer
// is zero, and it is available immediately - the backend query that ran alongside it
// counted stream 0 and must not be reported here.
if (queryObject->emptyVertexStream) {
switch (pname) {
case GL_QUERY_RESULT:
case GL_QUERY_RESULT_NO_WAIT:
outValue = 0;
return true;
case GL_QUERY_RESULT_AVAILABLE:
outValue = GL_TRUE;
return true;
default:
break;
}
}
switch (pname) {
case GL_QUERY_TARGET:
// The target a query was begun with (or created with, for glCreateQueries) - state
@@ -741,14 +765,15 @@ namespace MobileGL::MG_Impl::GLImpl {
}
namespace {
Bool IsPerVertexStreamQueryTarget(GLenum target) {
return target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
}
// The indexed query entry points differ from the plain ones only in the vertex
// stream they address (GL 4.6 core 4.2.1): index must be below GL_MAX_VERTEX_STREAMS
// for the two transform feedback targets and zero for every other target. With a
// single vertex stream both bounds are 1, so a valid call is always index 0 and
// forwards to the unindexed implementation.
// for the two transform feedback targets and zero for every other target.
Bool ValidateQueryStreamIndex(const char* function, GLenum target, GLuint index) {
const Bool perStreamTarget =
target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
const Bool perStreamTarget = IsPerVertexStreamQueryTarget(target);
GLint maxVertexStreams = 1;
if (perStreamTarget) {
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
@@ -761,11 +786,29 @@ namespace MobileGL::MG_Impl::GLImpl {
: "index must be zero for this query target.");
return false;
}
// Streams 1..GL_MAX_VERTEX_STREAMS-1 exist but nothing can emit to them, so a span begun
// on one counts zero primitives. Flagging the object is what keeps that answer honest:
// BeginQuery below still opens a real backend query (it is the only way to reuse the
// whole target/object state machine), and that query counts STREAM 0.
//
// Known simplification, spelled out rather than hidden: because the backend query is
// shared, only ONE query may be active per target here, while GL allows one per
// (target, stream) pair. A program running a stream-0 and a stream-2
// GL_PRIMITIVES_GENERATED query at the same time gets GL_INVALID_OPERATION on the
// second. Nothing can produce a non-zero stream-2 result to be worth more than that
// until the shader pipeline grows layout(stream = N).
void MarkQueryEmptyVertexStream(GLuint id) {
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
auto* queryObject = FindQueryObjectLocked(id);
if (queryObject && queryObject->active) queryObject->emptyVertexStream = true;
}
} // namespace
void BeginQueryIndexed(GLenum target, GLuint index, GLuint id) {
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
BeginQuery(target, id);
if (index != 0 && IsPerVertexStreamQueryTarget(target)) MarkQueryEmptyVertexStream(id);
}
void EndQueryIndexed(GLenum target, GLuint index) {
+30 -28
View File
@@ -522,44 +522,46 @@ namespace MobileGL::MG_Impl::GLImpl {
return sampleCounts.empty() ? 0 : sampleCounts.front();
}
// The ceiling the frontend enforces, which must never be lower than the one MobileGL
// advertises: the CTS - and real applications - read GL_MAX_SAMPLES once and hand that
// exact count to glTexImage*Multisample for every format. Answering 4 there and then
// rejecting 4 here because the ES driver reports GL_MAX_INTEGER_SAMPLES 1 (Adreno) is a
// self-inconsistency, not a spec-mandated error. The backends clamp the count they hand
// the driver; the shadow state keeps reporting what the application asked for.
// The ceiling the frontend enforces, which is EXACTLY the one MobileGL advertises for
// this format's category - GL_MAX_DEPTH_TEXTURE_SAMPLES, GL_MAX_INTEGER_SAMPLES or
// GL_MAX_COLOR_TEXTURE_SAMPLES, all three of which have a GL 4.6 minimum of one and are
// reported as probed. It used to floor all three at GL_MAX_SAMPLES (4) on the reasoning
// that an application reads GL_MAX_SAMPLES once and hands that count to every
// glTexStorage*Multisample. That reasoning had it backwards: on Adreno and on Mali an
// integer multisample texture is backed by ONE sample, so accepting four here did not
// make four samples exist - ClampSamplesToBackendSupport quietly allocated one and the
// application wrote per-sample data it could never read back. Raising INVALID_OPERATION
// is what a real driver does, and it is what makes that silent squeeze unreachable for
// application-visible storage.
Int GetMaxSupportedTextureSamples(TextureTarget textureTarget,
TextureInternalFormat textureInternalFormat) {
if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::max();
}
const Int advertisedMaxSamples = GetAdvertisedMaxSamples();
const Bool isDepthOrStencil = MG_Util::IsDepthFormatInternalFormat(textureInternalFormat) ||
MG_Util::IsStencilFormatInternalFormat(textureInternalFormat);
Bool isIntegerFormat = false;
if (!isDepthOrStencil) {
GLenum normalizedInternalFormat =
MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat);
GLenum normalizedFormat = GL_RGBA;
GLenum normalizedType = GL_UNSIGNED_BYTE;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
normalizedInternalFormat, PixelFormatNormalizeOptionBit::None, &normalizedInternalFormat,
&normalizedFormat, &normalizedType);
isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
}
const Int categoryMaxSamples = isDepthOrStencil ? GetAdvertisedDepthTextureMaxSamples()
: isIntegerFormat ? GetAdvertisedIntegerMaxSamples()
: GetAdvertisedColorTextureMaxSamples();
// glGetInternalformativ(GL_SAMPLES) is answered from this very list (GetInternalformativ
// below), and GL 4.6 core 8.8 makes that query the definition of the per-format
// maximum - validating against anything else is how the two answers drifted apart.
const Int probedMaxSamples = GetProbedMaxTextureSamples(textureTarget, textureInternalFormat);
if (probedMaxSamples > 0) {
return std::max(probedMaxSamples, advertisedMaxSamples);
}
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
if (MG_Util::IsDepthFormatInternalFormat(textureInternalFormat) ||
MG_Util::IsStencilFormatInternalFormat(textureInternalFormat)) {
return std::max(dynamicParameters.MaxDepthTextureSamples, advertisedMaxSamples);
}
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat);
GLenum normalizedFormat = GL_RGBA;
GLenum normalizedType = GL_UNSIGNED_BYTE;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
normalizedInternalFormat, PixelFormatNormalizeOptionBit::None, &normalizedInternalFormat,
&normalizedFormat, &normalizedType);
const Bool isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
return std::max(isIntegerFormat ? dynamicParameters.MaxIntegerSamples
: dynamicParameters.MaxColorTextureSamples,
advertisedMaxSamples);
return probedMaxSamples > 0 ? std::max(probedMaxSamples, categoryMaxSamples) : categoryMaxSamples;
}
Bool ValidateTextureMultisampleStorage(TextureTarget textureTarget, GLsizei samples, GLsizei width,
@@ -12,15 +12,15 @@
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Util/Converters/MGToGL/DataTypeConverter.h>
#include <MG_Util/Converters/MGToStr/DataTypeConverter.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
Uint GetMaxVertexAttribs() {
constexpr Uint capacity = static_cast<Uint>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
if (!MG_Backend::pActiveBackendObject) return capacity;
const Int backendLimit = MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs;
if (backendLimit <= 0) return capacity;
return std::min(static_cast<Uint>(backendLimit), capacity);
// Shared with reflection's limit and with gl_MaxVertexAttribs; see ResolveMaxVertexAttribs.
const Bool hasBackend = MG_Backend::pActiveBackendObject != nullptr;
const Int backendLimit =
hasBackend ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs : 0;
return static_cast<Uint>(MG_Util::ShaderTranspiler::ResolveMaxVertexAttribs(hasBackend, backendLimit));
}
Uint GetMaxVertexAttribBindings() {