From 6dfadeb7d22e219d74dc28cfed94e1f6af0d1826 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 15:30:11 -0400 Subject: [PATCH] [Fix, Test] (BackendLoader): drain and gate every capability probe whose pname is not ES core --- .../BackendLoader/BackendLoaderTest.cpp | 108 +++++++++++++++- .../MG_Util/BackendLoaders/OpenGL/Loader.cpp | 117 +++++++++++++----- 2 files changed, 193 insertions(+), 32 deletions(-) diff --git a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp index 77c981af..63ba1755 100644 --- a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp +++ b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp @@ -52,6 +52,18 @@ namespace { GLint maxClipDistances = 8; bool maxClipDistancesQueried = false; bool clipDistanceQueryRaisesError = false; + // GL_MAX_VIEWPORTS / GL_VIEWPORT_SUBPIXEL_BITS / GL_VIEWPORT_BOUNDS_RANGE are + // GL_OES_viewport_array state and, like the clip-distance pname, exist nowhere in ES core. + GLint maxViewports = 32; + GLint viewportSubpixelBits = 8; + bool viewportArrayLimitsQueried = false; + // A driver rejecting one of the UNCONDITIONAL probes. GL_SMOOTH_LINE_WIDTH_RANGE is the + // realistic one - it is desktop-only state that every GLES driver refuses - and it stands + // in for the whole run: whatever it leaves behind must not reach the application. + bool smoothLineWidthQueryRaisesError = false; + // What the driver answers for the four multisample ceilings. Zero is the value that has + // to be floored away: the frontend would otherwise advertise a sample count it rejects. + GLint multisampleCeiling = 4; GLfloat minFragmentInterpolationOffset = -0.75f; GLfloat maxFragmentInterpolationOffset = 0.625f; GLint fragmentInterpolationOffsetBits = 6; @@ -175,6 +187,21 @@ namespace { *data = g_fake.maxClipDistances; } break; + case GL_MAX_VIEWPORTS: + g_fake.viewportArrayLimitsQueried = true; + *data = g_fake.maxViewports; + break; + case GL_VIEWPORT_SUBPIXEL_BITS: + g_fake.viewportArrayLimitsQueried = true; + *data = g_fake.viewportSubpixelBits; + break; + case GL_MAX_COLOR_TEXTURE_SAMPLES: + case GL_MAX_DEPTH_TEXTURE_SAMPLES: + case GL_MAX_FRAMEBUFFER_SAMPLES: + case GL_MAX_INTEGER_SAMPLES: + case GL_MAX_SAMPLES: + *data = g_fake.multisampleCeiling; + break; case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: g_fake.fragmentInterpolationLimitsQueried = true; if (g_fake.fragmentInterpolationQueryRaisesError) { @@ -254,11 +281,22 @@ namespace { data[0] = g_fake.maxFragmentInterpolationOffset; } break; + case GL_SMOOTH_LINE_WIDTH_RANGE: + if (g_fake.smoothLineWidthQueryRaisesError) { + g_fake.pendingError = GL_INVALID_ENUM; + } else { + data[0] = 0.0f; + data[1] = 0.0f; + } + break; + case GL_VIEWPORT_BOUNDS_RANGE: + g_fake.viewportArrayLimitsQueried = true; + data[0] = 0.0f; + data[1] = 0.0f; + break; // Two-component range queries. case GL_ALIASED_LINE_WIDTH_RANGE: - case GL_SMOOTH_LINE_WIDTH_RANGE: case GL_ALIASED_POINT_SIZE_RANGE: - case GL_VIEWPORT_BOUNDS_RANGE: data[0] = 0.0f; data[1] = 0.0f; break; @@ -715,6 +753,72 @@ TEST(ClipDistanceCapabilities, ARejectedQueryIsDrainedAndReportsZero) { EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR) << "the failed query must not leave an error behind"; } +// The same defect one more time, for the three GL_OES_viewport_array pnames. Their advertised +// values do not come from the driver (GL_Getter answers GL_MAX_VIEWPORTS from the frontend state +// width and floors GL_SUBPIXEL_BITS at its own constant), so what this pins is the other half of +// the class defect: a pname that does not exist must not be asked for, because the GL_INVALID_ENUM +// it raises is then attributed to whatever the application calls next. +TEST(ViewportArrayCapabilities, TheLimitsAreOnlyAskedForWhenTheExtensionIsPresent) { + const auto funcs = MakeFakeGLESFunctions(); + + ResetFakeDriver(); + g_fake.maxVertexSsboBlocks = 0; + MobileGL::MG_External::GLESCapabilities withoutCaps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(withoutCaps, funcs)); + EXPECT_FALSE(withoutCaps.SupportsViewportArray); + EXPECT_FALSE(g_fake.viewportArrayLimitsQueried); + EXPECT_EQ(withoutCaps.MaxViewports, 16) << "the OpenGL core minimum, not a driver answer"; + EXPECT_FLOAT_EQ(withoutCaps.ViewportBoundsRangeMin, -32768.0f); + EXPECT_FLOAT_EQ(withoutCaps.ViewportBoundsRangeMax, 32767.0f); + + ResetFakeDriver(); + g_fake.maxVertexSsboBlocks = 0; + g_fake.extensions.emplace_back("GL_OES_viewport_array"); + MobileGL::MG_External::GLESCapabilities withCaps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(withCaps, funcs)); + EXPECT_TRUE(withCaps.SupportsViewportArray); + EXPECT_TRUE(g_fake.viewportArrayLimitsQueried); + EXPECT_EQ(withCaps.MaxViewports, g_fake.maxViewports); + EXPECT_EQ(withCaps.ViewportSubpixelBits, g_fake.viewportSubpixelBits); +} + +// The multisample ceilings are ES 3.1 state; a driver that answers zero - or an older context +// that answers nothing - must not have that reach GL_Getter, which would then reject the sample +// count it just advertised. +TEST(MultisampleCapabilities, TheAdvertisedSampleCountsNeverFallBelowOne) { + ResetFakeDriver(); + g_fake.maxVertexSsboBlocks = 0; + g_fake.multisampleCeiling = 0; + const auto funcs = MakeFakeGLESFunctions(); + + MobileGL::MG_External::GLESCapabilities caps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs)); + + EXPECT_EQ(caps.MaxColorTextureSamples, 1); + EXPECT_EQ(caps.MaxDepthTextureSamples, 1); + EXPECT_EQ(caps.MaxFramebufferSamples, 1); + EXPECT_EQ(caps.MaxIntegerSamples, 1); + EXPECT_EQ(caps.MaxSamples, 1); + EXPECT_EQ(caps.MaxSampleMaskWords, 1); +} + +// The whole point of the drain, stated once at the level that matters: capability init is the +// first thing that ever touches the driver, so an error it leaves behind surfaces at the +// APPLICATION's first glGetError and is blamed on an unrelated call. GL_SMOOTH_LINE_WIDTH_RANGE +// is the stand-in because it is desktop-only state that every real GLES driver refuses. +TEST(CapabilityProbeHygiene, ARejectedUnconditionalProbeLeavesNoErrorBehind) { + ResetFakeDriver(); + g_fake.maxVertexSsboBlocks = 0; + g_fake.smoothLineWidthQueryRaisesError = true; + const auto funcs = MakeFakeGLESFunctions(); + + MobileGL::MG_External::GLESCapabilities caps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs)); + + EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR) + << "capability init must not hand the application an error it never caused"; +} + TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) { const auto funcs = MakeFakeGLESFunctions(); diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index f2da5d89..38cd1d24 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -1082,11 +1082,39 @@ namespace MobileGL::MG_Util::BackendLoader { GLint maxProgramTextureGatherOffset = 7; GLint maxPatchVertices = 32; GLint maxTessGenLevel = 64; + // Function-scope, and used by every probe group below rather than redeclared inside each + // one. Returns whether anything was drained, which is what lets a group tell "the driver + // answered" from "the driver rejected the pname and left my local alone". + const auto drainErrors = [&glesFuncs]() { + Bool hadError = false; + if (glesFuncs.glGetError) { + while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true; + } + return hadError; + }; + + // THE GENERATOR OF THIS WHOLE BUG FAMILY, closed here. A bare glGetIntegerv/glGetFloatv + // of a pname the driver does not have does two damaging things at once: it leaves the + // local at whatever the declaration initialised it to - an optimistic number the frontend + // then advertises as a capability - and it leaves a GL_INVALID_ENUM in the queue where + // the next unrelated probe's caller, or the application's first glGetError, gets blamed + // for it. The per-stage storage block, fragment interpolation and buffer texture probes + // below already drain and fall back; this unconditional run did neither, which is how + // GL_MAX_CLIP_DISTANCES came to be advertised as 8 on a driver with no clip distances at + // all. Every pname here that is not ES core is now either gated on the capability that + // makes it exist or floored at the value a rejected probe would have left, and the whole + // run is bracketed by a drain. + drainErrors(); glesFuncs.glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, aliasedLineWidthRange); + // GL_SMOOTH_LINE_WIDTH_RANGE / GL_SMOOTH_LINE_WIDTH_GRANULARITY (0x0B22 / 0x0B23) are + // desktop-only - ES has never had an antialiased line width query - so on a real GLES + // driver these two raise GL_INVALID_ENUM. Kept as probes rather than dropped because the + // ANGLE and desktop-GL hosts MobileGL also runs on do answer them; the initialisers are + // the GL 4.6 table 23.55 minimum of [1, 1], which is both the honest answer for a driver + // that cannot say and what an untouched out-param already holds. glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_RANGE, smoothLineWidthRange); glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_GRANULARITY, &smoothLineWidthGranularity); glesFuncs.glGetFloatv(GL_ALIASED_POINT_SIZE_RANGE, aliasedPointSizeRange); - glesFuncs.glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE, viewportBoundsRange); glesFuncs.glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE, &max3DTextureSize); glesFuncs.glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &maxArrayTextureLayers); glesFuncs.glGetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE, &maxCubeMapTextureSize); @@ -1110,8 +1138,25 @@ namespace MobileGL::MG_Util::BackendLoader { // single test case. 1 is a spec-legal value (the minimum required), so cap // to what is actually implemented instead of forwarding the raw driver limit. maxSampleMaskWords = std::min(maxSampleMaskWords, 1); + // The multisample ceilings above are ES 3.1 state apart from GL_MAX_SAMPLES, which is ES + // 3.0, so a 3.0 context rejects five of the six and leaves whatever the out-param held. + // One sample is what a rejected probe leaves behind and is also the smallest legal + // answer, so clamp rather than trust: a zero reaching GL_Getter would have the frontend + // reject the very sample count it just advertised (see GetAdvertisedMaxSamples). + maxColorTextureSamples = std::max(maxColorTextureSamples, 1); + maxDepthTextureSamples = std::max(maxDepthTextureSamples, 1); + maxFramebufferSamples = std::max(maxFramebufferSamples, 1); + maxIntegerSamples = std::max(maxIntegerSamples, 1); + maxSamples = std::max(maxSamples, 1); + maxSampleMaskWords = std::max(maxSampleMaskWords, 1); + // ES 3.2 core, or EXT_tessellation_shader on 3.1. Probed rather than version-gated so a + // 3.1 driver that HAS the extension still gets to answer; the clamp below is what makes a + // rejected query safe, since GL 4.6 table 23.66 and ES 3.2 table 21.45 set the same + // minimums the initialisers carry and neither API permits less. glesFuncs.glGetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices); glesFuncs.glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel); + maxPatchVertices = std::max(maxPatchVertices, 32); + maxTessGenLevel = std::max(maxTessGenLevel, 64); glesFuncs.glGetIntegerv(GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET, &minProgramTextureGatherOffset); glesFuncs.glGetIntegerv(GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET, &maxProgramTextureGatherOffset); // A driver that leaves the probe untouched (pre-ES 3.1, or an ignored enum) must not @@ -1148,6 +1193,13 @@ namespace MobileGL::MG_Util::BackendLoader { (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) { glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &maxGeometryImageUniforms); } + // Closes the bracket opened before the run: every local above now holds either the + // driver's answer or a floor, and nothing this function asked for is left in the error + // queue for a later probe - or the application - to be blamed for. + if (drainErrors()) { + MGLOG_W("One or more capability queries were rejected by this driver; the affected " + "limits keep MobileGL's spec-minimum floors"); + } // Per-stage storage-block counts. Deliberately NOT batched with the unconditional probes // above, for the reason GL_MAX_TEXTURE_BUFFER_SIZE is not: the vertex and fragment pnames // are ES 3.1, but the tessellation and geometry ones only exist from ES 3.2 on (or under @@ -1160,14 +1212,6 @@ namespace MobileGL::MG_Util::BackendLoader { // stages is 0. That is the honest answer: DirectGLES emits ESSL 3.10 on an ES 3.1 context, // where those stages do not exist at all. { - const auto drainErrors = [&glesFuncs]() { - Bool hadError = false; - if (glesFuncs.glGetError) { - while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true; - } - return hadError; - }; - // Isolate from errors raised by the preceding probes so the drain below reports on // these queries only. drainErrors(); @@ -1210,35 +1254,40 @@ namespace MobileGL::MG_Util::BackendLoader { // exists; everywhere else the honest 0 stands and no GL_INVALID_ENUM is left behind for an // unrelated query - or the application's first glGetError - to trip over. if (caps.SupportsClipDistance) { - if (glesFuncs.glGetError) { - while (glesFuncs.glGetError() != GL_NO_ERROR) { - } - } + drainErrors(); glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances); - Bool queryFailed = false; - if (glesFuncs.glGetError) { - while (glesFuncs.glGetError() != GL_NO_ERROR) { - queryFailed = true; - } - } - if (queryFailed) { + if (drainErrors()) { MGLOG_W("GL_EXT_clip_cull_distance is advertised but GL_MAX_CLIP_DISTANCES was " "rejected; reporting no clip distances"); maxClipDistances = 0; } } - glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports); glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims); - glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits); + // GL_MAX_VIEWPORTS (0x825B), GL_VIEWPORT_SUBPIXEL_BITS (0x825C) and GL_VIEWPORT_BOUNDS_RANGE + // (0x825D) all arrive with GL_OES_viewport_array and exist nowhere in ES core, so on the + // drivers DirectGLES actually runs on all three raise GL_INVALID_ENUM. The values MobileGL + // advertises do not change by asking: GL_Getter answers GL_MAX_VIEWPORTS from the frontend + // state width (indexed viewport entry points validate against RenderStateParameters:: + // MAX_VIEWPORTS, so a device answer of 1 would reject indices the state can legitimately + // hold), floors GL_SUBPIXEL_BITS at its own 4, and the bounds range is clamped to the core + // minimum below. What changes is that the errors stop being manufactured. + if (caps.SupportsViewportArray) { + drainErrors(); + glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports); + glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits); + if (glesFuncs.glGetFloatv) { + glesFuncs.glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE, viewportBoundsRange); + } + if (drainErrors()) { + MGLOG_W("GL_OES_viewport_array is advertised but its viewport limit queries were " + "rejected; keeping the OpenGL core minimums"); + maxViewports = 16; + viewportSubpixelBits = 0; + viewportBoundsRange[0] = -32768.0f; + viewportBoundsRange[1] = 32767.0f; + } + } if (caps.SupportsShaderMultisampleInterpolation && glesFuncs.glGetFloatv) { - const auto drainErrors = [&glesFuncs]() { - Bool hadError = false; - if (glesFuncs.glGetError) { - while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true; - } - return hadError; - }; - // Isolate these optional queries from errors raised by preceding capability // probes, then consume any query error so initialization never leaks it into // the application's first glGetError call. @@ -1517,6 +1566,14 @@ namespace MobileGL::MG_Util::BackendLoader { caps.AvoidSamplerMipmapMinFilter ? "true" : "false"); MGLOG_I(" Avoid explicit LOD bias: %s", caps.AvoidExplicitLodBias ? "true" : "false"); + // Last line of defence. Capability init is the very first thing that touches the driver, + // so anything it leaves in the error queue surfaces at the APPLICATION's first + // glGetError and gets attributed to whatever call the app happened to make. Every group + // above drains its own, but a probe added later must not be able to reintroduce the leak. + if (drainErrors()) { + MGLOG_W("Capability initialization left a GL error behind; it has been consumed so it " + "cannot surface at the application's first glGetError"); + } return true; } } // namespace MobileGL::MG_Util::BackendLoader