mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[Fix, Test] (BackendLoader): drain and gate every capability probe whose pname is not ES core
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user