Files
MobileGL/MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp
T

383 lines
25 KiB
C++

// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.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
//
// "The limit we advertise is a promise, and an application will hold us to it."
//
// DirectVulkan copied Vulkan descriptor limits straight into the GL limit table. Those are not
// the same quantity: Adreno answers maxPerStageDescriptorUniformBuffers at descriptor-indexing
// scale, and GL_MAX_COMPUTE_UNIFORM_BLOCKS is a count an app will allocate. KHR-GL44.multi_bind
// .dispatch_bind_buffers_base does exactly that - createsO(limit) buffers and splices O(limit)
// UBO declarations into one compute shader - and spent ~14 s allocating before dying on
// std::bad_alloc. Its sibling dispatch_bind_buffers_range hard-codes 4 buffers and passes.
//
// Two failure modes, one table:
// - too LARGE: an unusable promise (the OOM above).
// - too SMALL or negative: a uint32 limit that lost its top bit on the way to a signed Int -
// UINT32_MAX arrived as -1, which every downstream std::min then accepted as "small enough".
// A conformant GL 4.x implementation may never advertise below the spec minimum either.
//
// Every bound below is checked on BOTH backends, because the loader casts are shared and the
// DirectGLES lane is the control: it takes its limits from a driver that already reports GL
// quantities, so an entry that only fails on DirectVulkan is a translation bug and one that
// fails on both is a table bug.
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
struct LimitBound {
GLenum pname;
const char* name;
// The GL 4.x required minimum. A value below this is a conformance failure in its own
// right, and is what a sign-flipped uint32 looks like.
int minimum;
// The largest value this implementation is willing to promise. Chosen well above every
// desktop driver's answer, so it can only catch a descriptor-scale number.
int ceiling;
};
const std::vector<LimitBound>& BufferLimitTable() {
static const std::vector<LimitBound> table = {
// 84 is the GL 4.5 core table 23.64 minimum, and also the width of the state
// layer's indexed-binding array - the two were made to coincide when the array
// was widened from 36, which had made the clamp in GL_Getter degenerate.
{GL_MAX_UNIFORM_BUFFER_BINDINGS, "GL_MAX_UNIFORM_BUFFER_BINDINGS", 84, 256},
// 14 uniform blocks on each of the FIVE graphics stages. The sum used to count
// three, and the two tessellation stages were simply missing from it.
{GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS", 70, 256},
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", 12, 256},
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", 8, 256},
{GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", 8, 256},
{GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", 8, 256},
{GL_MAX_TEXTURE_BUFFER_SIZE, "GL_MAX_TEXTURE_BUFFER_SIZE", 65536, 1 << 27},
{GL_MAX_UNIFORM_BLOCK_SIZE, "GL_MAX_UNIFORM_BLOCK_SIZE", 16384, 1 << 30},
// Already clamped before this campaign; in the table so a regression there is
// caught by the same case.
{GL_MAX_SHADER_STORAGE_BLOCK_SIZE, "GL_MAX_SHADER_STORAGE_BLOCK_SIZE", 1 << 24, 512 * 1024 * 1024},
{GL_MAX_TEXTURE_IMAGE_UNITS, "GL_MAX_TEXTURE_IMAGE_UNITS", 16, 32},
{GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, "GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS", 48, 192},
};
return table;
}
class AdvertisedLimitsScenario : public ScenarioTest {};
TEST_F(AdvertisedLimitsScenario, EveryBufferLimitIsWithinItsAdvertisedRange) {
for (const LimitBound& bound : BufferLimitTable()) {
GLint value = -424242;
glGetIntegerv(bound.pname, &value);
const unsigned int error = FirstGLError();
EXPECT_EQ(error, GLenum(GL_NO_ERROR))
<< bound.name << " is not answerable: " << GLErrorName(error);
if (error != GL_NO_ERROR) continue;
EXPECT_GE(value, bound.minimum)
<< bound.name << " = " << value << " is below the GL required minimum "
<< bound.minimum << " (a negative or tiny value here is a uint32 limit that lost "
"its top bit on the way to a signed Int)";
EXPECT_LE(value, bound.ceiling)
<< bound.name << " = " << value << " exceeds the ceiling " << bound.ceiling
<< " this implementation is willing to promise - an application that allocates "
"what we advertise will run out of memory";
}
}
// A per-stage block count is an amount of BINDING POINTS an application will use, so it
// can never exceed the number of binding points that exist. GL 4.6 Table 23.64 states the
// relation the other way round (MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS
// >= every per-stage count), and DirectVulkan broke it by clamping the two families
// independently: a device reporting 256 compute uniform blocks and 84 uniform binding
// points passes both ceilings and still cannot serve
// KHR-GL44.multi_bind.dispatch_bind_buffers_base, which reads the block count and binds
// that many buffers in one glBindBuffersBase - INVALID_OPERATION before a single bind.
TEST_F(AdvertisedLimitsScenario, PerStageBlockCountsFitInTheirBindingPoints) {
struct Relation {
GLenum blocks;
const char* blocksName;
GLenum bindings;
const char* bindingsName;
};
const Relation relations[] = {
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS,
"GL_MAX_UNIFORM_BUFFER_BINDINGS"},
{GL_MAX_VERTEX_UNIFORM_BLOCKS, "GL_MAX_VERTEX_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS,
"GL_MAX_UNIFORM_BUFFER_BINDINGS"},
{GL_MAX_FRAGMENT_UNIFORM_BLOCKS, "GL_MAX_FRAGMENT_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS,
"GL_MAX_UNIFORM_BUFFER_BINDINGS"},
{GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS,
"GL_MAX_UNIFORM_BUFFER_BINDINGS"},
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS",
GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS"},
{GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS",
GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS"},
};
for (const Relation& relation : relations) {
GLint blocks = -1;
GLint bindings = -1;
glGetIntegerv(relation.blocks, &blocks);
glGetIntegerv(relation.bindings, &bindings);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << relation.blocksName;
EXPECT_LE(blocks, bindings)
<< relation.blocksName << " = " << blocks << " exceeds " << relation.bindingsName << " = "
<< bindings << "; a shader may declare more blocks than there are binding points to bind them to";
}
// THE MIDDLE TERM, which the relation quoted above always had and this case never
// checked. It is the one that actually broke: widening the binding-point array to 84
// raised what every PER-STAGE count clamps to, while the combined value was a
// five-stage sum of 70 - so a device reporting descriptor-indexing-scale uniform
// buffers (Adreno: maxPerStageDescriptorUniformBuffers = 16777216) advertised 84
// compute uniform blocks inside a combined limit of 70. Per-stage <= combined is
// exactly the assertion that says so, and it costs one glGetIntegerv per row.
struct StageAgainstCombined {
GLenum stage;
const char* stageName;
GLenum combined;
const char* combinedName;
};
const StageAgainstCombined stageRelations[] = {
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_VERTEX_UNIFORM_BLOCKS, "GL_MAX_VERTEX_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS, "GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS",
GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS, "GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS",
GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_GEOMETRY_UNIFORM_BLOCKS, "GL_MAX_GEOMETRY_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_FRAGMENT_UNIFORM_BLOCKS, "GL_MAX_FRAGMENT_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS",
GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS"},
{GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS, "GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS",
GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS"},
};
for (const StageAgainstCombined& relation : stageRelations) {
GLint stage = -1;
GLint combined = -1;
glGetIntegerv(relation.stage, &stage);
glGetIntegerv(relation.combined, &combined);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << relation.stageName;
EXPECT_LE(stage, combined)
<< relation.stageName << " = " << stage << " exceeds " << relation.combinedName << " = "
<< combined << "; GL 4.6 table 23.64 orders MAX_*_BUFFER_BINDINGS >= MAX_COMBINED_*_BLOCKS >= "
"every per-stage count, and a single-stage program may use its whole per-stage allowance";
}
}
// KHR-GL44.multi_bind.functional_bind_buffers_range sizes each of an indexed target's
// binding points at MAX_<target>_SIZE / MAX_<target>_BINDINGS and binds all of them in
// one glBindBuffersRange. That quotient has to be a legal BindBufferRange size, which
// makes the two limits of every indexed family a PAIR: advertise a size that does not
// survive division by the binding count and the call fails with INVALID_VALUE before any
// of it binds.
TEST_F(AdvertisedLimitsScenario, IndexedTargetSizeSurvivesDivisionByItsBindingCount) {
struct IndexedFamily {
GLenum maxSize;
const char* maxSizeName;
GLenum maxBindings;
const char* maxBindingsName;
GLint sizeGranularity; // BindBufferRange's size rule for the target
};
const IndexedFamily families[] = {
{GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, "GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE",
GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, "GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS", 1},
{GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS, "GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS",
GL_MAX_TRANSFORM_FEEDBACK_BUFFERS, "GL_MAX_TRANSFORM_FEEDBACK_BUFFERS", 4},
{GL_MAX_UNIFORM_BLOCK_SIZE, "GL_MAX_UNIFORM_BLOCK_SIZE", GL_MAX_UNIFORM_BUFFER_BINDINGS,
"GL_MAX_UNIFORM_BUFFER_BINDINGS", 1},
{GL_MAX_SHADER_STORAGE_BLOCK_SIZE, "GL_MAX_SHADER_STORAGE_BLOCK_SIZE",
GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", 1},
};
for (const IndexedFamily& family : families) {
GLint maxSize = -1;
GLint maxBindings = -1;
glGetIntegerv(family.maxSize, &maxSize);
glGetIntegerv(family.maxBindings, &maxBindings);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << family.maxSizeName;
ASSERT_GT(maxBindings, 0) << family.maxBindingsName;
const GLint perBinding = maxSize / maxBindings;
EXPECT_GT(perBinding, 0)
<< family.maxSizeName << " (" << maxSize << ") / " << family.maxBindingsName << " ("
<< maxBindings << ") is zero, and BindBufferRange rejects a zero size";
EXPECT_EQ(perBinding % family.sizeGranularity, 0)
<< family.maxSizeName << " (" << maxSize << ") / " << family.maxBindingsName << " ("
<< maxBindings << ") = " << perBinding << " is not a multiple of the "
<< family.sizeGranularity << "-byte size granularity BindBufferRange requires for it";
}
}
// The OOM case in isolation, because it is the one with a known CTS victim and the one a
// future refactor is most likely to reintroduce by copying the Vulkan limit back.
TEST_F(AdvertisedLimitsScenario, ComputeUniformBlocksIsAnAmountAnApplicationCouldActuallyAllocate) {
GLint blocks = -1;
glGetIntegerv(GL_MAX_COMPUTE_UNIFORM_BLOCKS, &blocks);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_GE(blocks, 12);
EXPECT_LE(blocks, 256) << "KHR-GL44.multi_bind.dispatch_bind_buffers_base creates one GL buffer "
"and one UBO declaration per advertised block";
GLint blockSize = -1;
glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &blockSize);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_GT(blockSize, 0);
// GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS is derived from the product of these two,
// so their product has to stay representable.
EXPECT_LE(static_cast<long long>(blocks) * blockSize,
static_cast<long long>(2147483647))
<< "blocks(" << blocks << ") * blockSize(" << blockSize << ") overflows the GLint the "
"derived component limits are computed in";
}
// The GL 4.5 core minimums that had no case in the getter at all, or that were still
// carrying an ES/GL3.3-tier number. Every one of these answered GL_INVALID_ENUM or a
// too-small value against a context advertising 4.6, and each is the FIRST call its
// conformance case makes - so the case died before it could measure anything.
//
// The cull pair is deliberately absent: zero is a legal answer there (a backend with no
// cull-distance route MUST report it), so it is checked for answerability only, below.
TEST_F(AdvertisedLimitsScenario, EveryGL45CoreMinimumIsMet) {
const std::vector<LimitBound> table = {
{GL_MAX_VARYING_VECTORS, "GL_MAX_VARYING_VECTORS", 15, 256},
{GL_MAX_VERTEX_UNIFORM_VECTORS, "GL_MAX_VERTEX_UNIFORM_VECTORS", 256, 1 << 20},
{GL_MAX_VARYING_COMPONENTS, "GL_MAX_VARYING_COMPONENTS", 60, 1 << 20},
// GL_MAX_VERTEX_STREAMS is deliberately absent. GL 4.5 requires 4 and MobileGL
// answers 1, which is a KNOWN non-conformance rather than an oversight: raising
// the number un-gates two transform-feedback CTS cases per package across
// KHR-GL40..GL46 that then fail, because no part of the shader pipeline supports
// layout(stream = N). See the GL_MAX_VERTEX_STREAMS case in GL_Getter.cpp. Adding
// a row here would pin a number the implementation cannot back.
{GL_MAX_GEOMETRY_SHADER_INVOCATIONS, "GL_MAX_GEOMETRY_SHADER_INVOCATIONS", 32, 256},
{GL_MAX_SUBROUTINES, "GL_MAX_SUBROUTINES", 256, 1 << 20},
{GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS, "GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS", 1024, 1 << 20},
{GL_MAX_TESS_CONTROL_INPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_INPUT_COMPONENTS", 128, 1 << 16},
{GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS", 128, 1 << 16},
{GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS", 4096,
1 << 20},
{GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS, "GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS", 16, 256},
{GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS, "GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS", 1024, 1 << 20},
{GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS, "GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS", 14, 256},
{GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS, "GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS", 128, 1 << 16},
{GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS, "GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS", 128, 1 << 16},
{GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS, "GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS", 16, 256},
{GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS, "GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS", 1024,
1 << 20},
{GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS, "GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS", 14, 256},
{GL_MAX_TESS_PATCH_COMPONENTS, "GL_MAX_TESS_PATCH_COMPONENTS", 120, 1 << 16},
{GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS, "GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS",
58368, 1 << 30},
{GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS,
"GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS", 58368, 1 << 30},
};
for (const LimitBound& bound : table) {
GLint value = -424242;
glGetIntegerv(bound.pname, &value);
const unsigned int error = FirstGLError();
EXPECT_EQ(error, GLenum(GL_NO_ERROR)) << bound.name << " is not answerable: " << GLErrorName(error);
if (error != GL_NO_ERROR) continue;
EXPECT_GE(value, bound.minimum) << bound.name << " = " << value << " is below the GL 4.5 minimum "
<< bound.minimum;
EXPECT_LE(value, bound.ceiling) << bound.name << " = " << value << " exceeds the ceiling "
<< bound.ceiling;
}
// ARB_cull_distance's pair. Zero is honest on a backend with no cull-distance route,
// so only answerability and the combined-limit ordering are checked here.
GLint cull = -1;
GLint clip = -1;
GLint combined = -1;
glGetIntegerv(GL_MAX_CULL_DISTANCES, &cull);
glGetIntegerv(GL_MAX_CLIP_DISTANCES, &clip);
glGetIntegerv(GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, &combined);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the ARB_cull_distance queries must not error";
EXPECT_GE(cull, 0);
EXPECT_GE(combined, cull) << "GL 4.6 core 11.1.3.10: the combined limit is at least the cull one";
EXPECT_GE(combined, clip) << "GL 4.6 core 11.1.3.10: the combined limit is at least the clip one";
// GL_MAX_ELEMENT_INDEX is 64-bit state: the required 2^32-1 does not fit a GLint, so
// the wide query must answer it and the narrow one must saturate rather than wrap.
GLint64 elementIndex = -1;
glGetInteger64v(GL_MAX_ELEMENT_INDEX, &elementIndex);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_GE(elementIndex, static_cast<GLint64>(4294967295LL))
<< "GL 4.5 core table 23.55 sets the GL_MAX_ELEMENT_INDEX minimum at 2^32-1";
}
// ARB_viewport_array's own limits. They are advertised from three different places -
// GL_MAX_VIEWPORTS from the frontend's indexed state width, the bounds range and the
// subpixel bits from the backend caps table - and each backend fills that table from a
// different source, so all three are checked on both lanes.
//
// GL_VIEWPORT_BOUNDS_RANGE is the one that shipped wrong: GLES has no such query, the
// DirectGLES loader's glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE) therefore raised
// GL_INVALID_ENUM and left the probe's zero-initialized array in place, and MobileGL
// advertised [0, 0] - a range that admits no viewport origin at all, and the check that
// kept KHR-GL43.viewport_array.queries red on Espryt after the indexed-state work.
TEST_F(AdvertisedLimitsScenario, ViewportArrayLimitsMeetTheirGL43Floors) {
GLint maxViewports = -1;
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_GE(maxViewports, 16) << "GL 4.3 core table 23.53 sets the MAX_VIEWPORTS minimum at 16";
EXPECT_LE(maxViewports, 256) << "one viewport rectangle of indexed state is allocated per advertised "
"viewport, and the CTS sizes its arrays off this number";
GLfloat boundsRange[2] = {1.0f, -1.0f};
glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE, boundsRange);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_LE(boundsRange[0], -32768.0f)
<< "GL 4.6 core table 23.60 sets the VIEWPORT_BOUNDS_RANGE minimum at [-32768, 32767]; got ["
<< boundsRange[0] << ", " << boundsRange[1] << "]";
EXPECT_GE(boundsRange[1], 32767.0f)
<< "GL 4.6 core table 23.60 sets the VIEWPORT_BOUNDS_RANGE minimum at [-32768, 32767]; got ["
<< boundsRange[0] << ", " << boundsRange[1] << "]";
// KNOWN INFIDELITY, pinned here rather than hidden. MobileGL reports the driver's own
// VIEWPORT_SUBPIXEL_BITS (4 on llvmpipe, i.e. 1/16-pixel viewport precision), but the
// float viewport rectangle glViewportIndexedf stores is snapped to integers on its
// way to both backends (ComputeGLViewport, DirectGLES SyncRenderState). The STATE
// round trip is exact - which is all KHR-GL43.viewport_array.viewport_api checks, and
// all this cluster set out to fix - so the gap is in rasterization only: a fractional
// viewport origin rasterizes as if it had been rounded. Nothing in the suite or in
// Minecraft sets one. Only the spec floor is asserted; tightening this to EQ(0) would
// mean advertising no subpixel precision at all, which is a separate decision about a
// limit MobileGL currently passes through from the driver.
GLint subpixelBits = -1;
glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &subpixelBits);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_GE(subpixelBits, 0) << "GL 4.6 core table 23.60: VIEWPORT_SUBPIXEL_BITS has a minimum of 0, and "
"a negative value is what a sign-flipped uint32 looks like";
GLint viewportDims[2] = {-1, -1};
glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewportDims);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
GLint maxRenderbufferSize = -1;
glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maxRenderbufferSize);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
// GL 4.6 core 13.6.1: MAX_VIEWPORT_DIMS must be at least as large as the largest
// renderable surface, or a full-size framebuffer could not be fully viewported.
EXPECT_GE(viewportDims[0], maxRenderbufferSize);
EXPECT_GE(viewportDims[1], maxRenderbufferSize);
}
} // namespace
} // namespace MGITest