Files
MobileGL/MobileGL/MG_Test/State/RenderStateTest.cpp
T

1029 lines
47 KiB
C++

// MobileGL - MobileGL/MG_Test/State/RenderStateTest.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
//
// Indexed capability state (glEnablei/glDisablei/glIsEnabledi) exists for exactly two
// capabilities: GL_BLEND, indexed by draw buffer, and GL_SCISSOR_TEST, indexed by viewport
// (ARB_viewport_array). Every other capability must come back as GL_INVALID_ENUM per GL 4.6
// sec. 17.3.3 - and, far more importantly, must come back at all: RenderState::SetCapabilityIndexed
// and IsCapabilityEnabledIndexed used to answer a non-blend capability with THROW_UNIMPL_EXCEPTION,
// which unwinds a C++ exception through the C GL ABI and terminates the process.
//
// The second half of this file is the ARB_viewport_array indexed rectangle state. Every one of
// glViewportArrayv/glViewportIndexedf(v)/glScissorArrayv/glScissorIndexed(v)/glDepthRangeArrayv/
// glDepthRangeIndexed was a MGLOG_W_ONCE stub that raised no error and stored nothing, and the
// indexed getters answered EVERY index with viewport 0's value, so a set/get round trip silently
// reported the initial state. The assertions below are deliberately state-shaped rather than
// render-shaped: this IS the state machine, and the rendering half (gl_ViewportIndex routing) is
// asserted separately in MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp.
#include <gtest/gtest.h>
#include "Includes.h"
#include "Init.h"
#include <limits>
#include <MG_Impl/GLImpl/Drawing/GL_Drawing.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_State/GLState/RenderState/RenderState.h>
using namespace MobileGL;
namespace {
class RenderStateTest: public ::testing::Test {
protected:
// GL error flags are sticky per code and the context outlives an individual test in this
// binary, so a pending error from an earlier case would be handed to the next GetError().
static void DrainPendingGlErrors() {
for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) {
}
}
static void ExpectSingleGlError(GLenum expected) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error";
}
void SetUp() override {
MobileGL::Initialize();
DrainPendingGlErrors();
}
void TearDown() override {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
}
};
} // namespace
TEST_F(RenderStateTest, IndexedCapabilityTogglesRejectNonIndexedCapabilities) {
// GL_CLIP_DISTANCE0 is a real capability, just not an indexed one - the shape an application or
// a CTS negative test would hit. GL_SCISSOR_TEST used to be in this list and is not any more:
// ARB_viewport_array makes it the second indexed capability (see the tests below).
for (const GLenum cap : {GL_CLIP_DISTANCE0, GL_DEPTH_TEST, GL_STENCIL_TEST}) {
MG_Impl::GLImpl::Enablei(cap, 0);
ExpectSingleGlError(GL_INVALID_ENUM);
MG_Impl::GLImpl::Disablei(cap, 0);
ExpectSingleGlError(GL_INVALID_ENUM);
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(cap, 0), GL_FALSE);
ExpectSingleGlError(GL_INVALID_ENUM);
}
}
TEST_F(RenderStateTest, IndexedCapabilityTogglesRejectAnOutOfRangeBufferIndex) {
const GLuint outOfRange = MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS;
MG_Impl::GLImpl::Enablei(GL_BLEND, outOfRange);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::Disablei(GL_BLEND, outOfRange);
ExpectSingleGlError(GL_INVALID_VALUE);
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, outOfRange), GL_FALSE);
ExpectSingleGlError(GL_INVALID_VALUE);
}
TEST_F(RenderStateTest, IndexedBlendTogglesStillWork) {
// The rejection path must not have cost the one capability that is genuinely indexed.
MG_Impl::GLImpl::Enablei(GL_BLEND, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, 1), GL_TRUE);
MG_Impl::GLImpl::Disablei(GL_BLEND, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, 1), GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// ---------------------------------------------------------------------------------------------
// ARB_viewport_array: indexed viewport / scissor / depth-range state
// ---------------------------------------------------------------------------------------------
namespace {
constexpr GLuint kMaxViewports = RenderStateParameters::MAX_VIEWPORTS;
Array<Array<GLfloat, 4>, kMaxViewports> ReadAllViewports() {
Array<Array<GLfloat, 4>, kMaxViewports> out{};
for (GLuint i = 0; i < kMaxViewports; ++i) {
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, i, out[i].data());
}
return out;
}
Array<Array<GLdouble, 2>, kMaxViewports> ReadAllDepthRanges() {
Array<Array<GLdouble, 2>, kMaxViewports> out{};
for (GLuint i = 0; i < kMaxViewports; ++i) {
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, i, out[i].data());
}
return out;
}
} // namespace
TEST_F(RenderStateTest, ScissorTestIsIndexedByViewport) {
// The exact shape of KHR-GL43.viewport_array.scissor_test_state_api's toggle loop: one index
// is flipped and EVERY index is read back, so a broadcast masquerading as an indexed write
// cannot pass.
MG_Impl::GLImpl::Disable(GL_SCISSOR_TEST);
ExpectSingleGlError(GL_NO_ERROR);
for (GLuint toggled = 0; toggled < kMaxViewports; ++toggled) {
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, toggled);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "index " << toggled;
for (GLuint i = 0; i < kMaxViewports; ++i) {
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, i), i == toggled ? GL_TRUE : GL_FALSE)
<< "enabled index " << toggled << ", read index " << i;
}
MG_Impl::GLImpl::Disablei(GL_SCISSOR_TEST, toggled);
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, toggled), GL_FALSE);
}
ExpectSingleGlError(GL_NO_ERROR);
}
TEST_F(RenderStateTest, NonIndexedScissorTestEnableWritesEveryViewport) {
// GL 4.6 core 17.3.2: Enable/Disable(SCISSOR_TEST) is "for all viewports". Reading only
// index 0 back would let a broadcast-less implementation through, so every index is checked.
MG_Impl::GLImpl::Enable(GL_SCISSOR_TEST);
for (GLuint i = 0; i < kMaxViewports; ++i) {
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, i), GL_TRUE) << "index " << i;
}
// ... and the non-indexed query answers for viewport 0 (GL 4.6 core 22.1).
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_TRUE);
MG_Impl::GLImpl::Disable(GL_SCISSOR_TEST);
for (GLuint i = 0; i < kMaxViewports; ++i) {
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, i), GL_FALSE) << "index " << i;
}
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_FALSE);
// An indexed enable on a NON-zero index must not move the non-indexed answer.
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, 3);
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_FALSE);
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, 0);
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_TRUE);
MG_Impl::GLImpl::Disable(GL_SCISSOR_TEST);
ExpectSingleGlError(GL_NO_ERROR);
}
TEST_F(RenderStateTest, ScissorTestEnableRejectsAnOutOfRangeViewportIndex) {
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, kMaxViewports);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::Disablei(GL_SCISSOR_TEST, kMaxViewports);
ExpectSingleGlError(GL_INVALID_VALUE);
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, kMaxViewports), GL_FALSE);
ExpectSingleGlError(GL_INVALID_VALUE);
// MAX_VIEWPORTS - 1 is the last LEGAL index and must stay silent.
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, kMaxViewports - 1);
ExpectSingleGlError(GL_NO_ERROR);
MG_Impl::GLImpl::Disablei(GL_SCISSOR_TEST, kMaxViewports - 1);
ExpectSingleGlError(GL_NO_ERROR);
}
TEST_F(RenderStateTest, MaxViewportsMatchesTheIndexedStateWidth) {
// The advertised limit and the width of the state arrays are the same number by
// construction; a divergence would make some index simultaneously legal to the CTS and
// out of range to the setters.
GLint maxViewports = 0;
MG_Impl::GLImpl::GetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
ExpectSingleGlError(GL_NO_ERROR);
EXPECT_EQ(maxViewports, static_cast<GLint>(kMaxViewports));
EXPECT_GE(maxViewports, 16) << "GL 4.3 core requires MAX_VIEWPORTS >= 16";
}
TEST_F(RenderStateTest, ViewportArrayvRoundTripsThroughEveryGetterWidth) {
Array<GLfloat, kMaxViewports * 4> written{};
for (GLuint i = 0; i < kMaxViewports; ++i) {
written[i * 4 + 0] = static_cast<GLfloat>(i) + 0.125f;
written[i * 4 + 1] = static_cast<GLfloat>(i) + 0.25f;
written[i * 4 + 2] = static_cast<GLfloat>(64 + i);
written[i * 4 + 3] = static_cast<GLfloat>(32 + i);
}
MG_Impl::GLImpl::ViewportArrayv(0, kMaxViewports, written.data());
ExpectSingleGlError(GL_NO_ERROR);
for (GLuint i = 0; i < kMaxViewports; ++i) {
GLfloat asFloat[4] = {};
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, i, asFloat);
// Bit-exact: the fractional origin is the whole point of float viewport state, and the
// CTS compares with == (0.125 and 0.25 are exact binary fractions, so this is fair).
EXPECT_EQ(asFloat[0], written[i * 4 + 0]) << "index " << i << " must round-trip verbatim";
EXPECT_EQ(asFloat[1], written[i * 4 + 1]) << "index " << i;
EXPECT_EQ(asFloat[2], written[i * 4 + 2]) << "index " << i;
EXPECT_EQ(asFloat[3], written[i * 4 + 3]) << "index " << i;
GLdouble asDouble[4] = {};
MG_Impl::GLImpl::GetDoublei_v(GL_VIEWPORT, i, asDouble);
for (int c = 0; c < 4; ++c) {
EXPECT_EQ(asDouble[c], static_cast<GLdouble>(written[i * 4 + c])) << "index " << i << " component " << c;
}
// The integer widths round to nearest rather than truncate; the .5+ case is pinned by
// ViewportRoundsRatherThanTruncatesForIntegerQueries below.
GLint asInt[4] = {};
MG_Impl::GLImpl::GetIntegeri_v(GL_VIEWPORT, i, asInt);
EXPECT_EQ(asInt[2], static_cast<GLint>(64 + i)) << "index " << i;
EXPECT_EQ(asInt[3], static_cast<GLint>(32 + i)) << "index " << i;
GLint64 asInt64[4] = {};
MG_Impl::GLImpl::GetInteger64i_v(GL_VIEWPORT, i, asInt64);
for (int c = 0; c < 4; ++c) {
EXPECT_EQ(asInt64[c], static_cast<GLint64>(asInt[c])) << "index " << i << " component " << c;
}
GLboolean asBool[4] = {};
MG_Impl::GLImpl::GetBooleani_v(GL_VIEWPORT, i, asBool);
EXPECT_EQ(asBool[2], GL_TRUE) << "index " << i << ": a non-zero width is GL_TRUE";
}
ExpectSingleGlError(GL_NO_ERROR);
}
TEST_F(RenderStateTest, ViewportRoundsRatherThanTruncatesForIntegerQueries) {
MG_Impl::GLImpl::ViewportIndexedf(2, 0.0f, 0.0f, 255.875f, 63.5f);
ExpectSingleGlError(GL_NO_ERROR);
GLint asInt[4] = {};
MG_Impl::GLImpl::GetIntegeri_v(GL_VIEWPORT, 2, asInt);
EXPECT_EQ(asInt[2], 256);
EXPECT_EQ(asInt[3], 64);
GLfloat asFloat[4] = {};
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, 2, asFloat);
EXPECT_EQ(asFloat[2], 255.875f) << "the integer query must not disturb the stored float";
ExpectSingleGlError(GL_NO_ERROR);
}
TEST_F(RenderStateTest, ViewportIndexedWritesTouchExactlyOneIndex) {
MG_Impl::GLImpl::Viewport(0, 0, 8, 8);
const auto before = ReadAllViewports();
for (GLuint target = 0; target < kMaxViewports; ++target) {
const GLfloat value[4] = {0.375f, 0.375f, 0.625f, 0.625f};
// Alternate the two indexed entry points so both are covered by the isolation claim.
if (target % 2 == 0) {
MG_Impl::GLImpl::ViewportIndexedf(target, value[0], value[1], value[2], value[3]);
} else {
MG_Impl::GLImpl::ViewportIndexedfv(target, value);
}
ExpectSingleGlError(GL_NO_ERROR);
const auto after = ReadAllViewports();
for (GLuint i = 0; i < kMaxViewports; ++i) {
if (i == target) {
EXPECT_EQ(after[i][0], value[0]) << "index " << i;
EXPECT_EQ(after[i][2], value[2]) << "index " << i;
} else {
EXPECT_EQ(after[i], before[i]) << "write to " << target << " disturbed index " << i;
}
}
MG_Impl::GLImpl::ViewportIndexedf(target, before[target][0], before[target][1], before[target][2],
before[target][3]);
}
ExpectSingleGlError(GL_NO_ERROR);
}
TEST_F(RenderStateTest, ClassicViewportWritesEveryIndexAndIsVisibleThroughIndexZero) {
// Both directions of the aliasing. ARB_viewport_array defines glViewport as ViewportIndexedf
// on every index, and glGetIntegerv(GL_VIEWPORT) as viewport 0.
MG_Impl::GLImpl::ViewportIndexedf(5, 1.0f, 2.0f, 3.0f, 4.0f);
MG_Impl::GLImpl::Viewport(0, 0, 1, 1);
ExpectSingleGlError(GL_NO_ERROR);
for (GLuint i = 0; i < kMaxViewports; ++i) {
GLfloat data[4] = {};
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, i, data);
EXPECT_EQ(data[0], 0.0f) << "index " << i;
EXPECT_EQ(data[2], 1.0f) << "glViewport must overwrite index " << i;
}
MG_Impl::GLImpl::ViewportIndexedf(0, 4.0f, 5.0f, 6.0f, 7.0f);
GLint classic[4] = {};
MG_Impl::GLImpl::GetIntegerv(GL_VIEWPORT, classic);
EXPECT_EQ(classic[0], 4);
EXPECT_EQ(classic[2], 6);
GLfloat classicFloat[4] = {};
MG_Impl::GLImpl::GetFloatv(GL_VIEWPORT, classicFloat);
EXPECT_EQ(classicFloat[2], 6.0f);
// Index 5 keeps its own value: writing index 0 is not a broadcast.
GLfloat other[4] = {};
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, 5, other);
EXPECT_EQ(other[2], 1.0f);
ExpectSingleGlError(GL_NO_ERROR);
}
TEST_F(RenderStateTest, ScissorBoxRoundTripsPerIndexAndAliasesIndexZero) {
Array<GLint, kMaxViewports * 4> written{};
for (GLuint i = 0; i < kMaxViewports; ++i) {
written[i * 4 + 0] = static_cast<GLint>(i);
written[i * 4 + 1] = static_cast<GLint>(i * 2);
written[i * 4 + 2] = static_cast<GLint>(16 + i);
written[i * 4 + 3] = static_cast<GLint>(8 + i);
}
MG_Impl::GLImpl::ScissorArrayv(0, kMaxViewports, written.data());
ExpectSingleGlError(GL_NO_ERROR);
for (GLuint i = 0; i < kMaxViewports; ++i) {
GLint readBack[4] = {};
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, i, readBack);
for (int c = 0; c < 4; ++c) {
EXPECT_EQ(readBack[c], written[i * 4 + c]) << "index " << i << " component " << c;
}
}
// Indexed writes stay indexed; both spellings.
MG_Impl::GLImpl::ScissorIndexed(4, 4, 4, 8, 8);
const GLint indexedV[4] = {9, 9, 12, 12};
MG_Impl::GLImpl::ScissorIndexedv(7, indexedV);
ExpectSingleGlError(GL_NO_ERROR);
GLint probe[4] = {};
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 4, probe);
EXPECT_EQ(probe[2], 8);
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 7, probe);
EXPECT_EQ(probe[2], 12);
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 5, probe);
EXPECT_EQ(probe[2], static_cast<GLint>(16 + 5)) << "index 5 must be untouched";
// glScissor writes every rectangle, and glGetIntegerv(GL_SCISSOR_BOX) reports rectangle 0.
MG_Impl::GLImpl::Scissor(2, 3, 5, 6);
for (GLuint i = 0; i < kMaxViewports; ++i) {
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, i, probe);
EXPECT_EQ(probe[0], 2) << "index " << i;
EXPECT_EQ(probe[2], 5) << "index " << i;
}
GLint classic[4] = {};
MG_Impl::GLImpl::GetIntegerv(GL_SCISSOR_BOX, classic);
EXPECT_EQ(classic[2], 5);
ExpectSingleGlError(GL_NO_ERROR);
}
TEST_F(RenderStateTest, DepthRangeRoundTripsPerIndexAndAliasesIndexZero) {
Array<GLdouble, kMaxViewports * 2> written{};
for (GLuint i = 0; i < kMaxViewports; ++i) {
// Exact binary fractions, like the CTS uses: a float-backed store round-trips them.
written[i * 2 + 0] = static_cast<GLdouble>(i) / 16.0;
written[i * 2 + 1] = 1.0 - static_cast<GLdouble>(i) / 16.0;
}
MG_Impl::GLImpl::DepthRangeArrayv(0, kMaxViewports, written.data());
ExpectSingleGlError(GL_NO_ERROR);
const auto readBack = ReadAllDepthRanges();
for (GLuint i = 0; i < kMaxViewports; ++i) {
EXPECT_EQ(readBack[i][0], written[i * 2 + 0]) << "index " << i;
EXPECT_EQ(readBack[i][1], written[i * 2 + 1]) << "index " << i;
}
MG_Impl::GLImpl::DepthRangeIndexed(9, 0.25, 0.75);
ExpectSingleGlError(GL_NO_ERROR);
GLdouble probe[2] = {};
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, 9, probe);
EXPECT_EQ(probe[0], 0.25);
EXPECT_EQ(probe[1], 0.75);
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, 8, probe);
EXPECT_EQ(probe[0], 8.0 / 16.0) << "index 8 must be untouched";
GLfloat asFloat[2] = {};
MG_Impl::GLImpl::GetFloati_v(GL_DEPTH_RANGE, 9, asFloat);
EXPECT_EQ(asFloat[0], 0.25f);
EXPECT_EQ(asFloat[1], 0.75f);
// glDepthRange writes every range; glGetDoublev(GL_DEPTH_RANGE) reports range 0.
MG_Impl::GLImpl::DepthRange(0.0, 1.0);
for (GLuint i = 0; i < kMaxViewports; ++i) {
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, i, probe);
EXPECT_EQ(probe[0], 0.0) << "index " << i;
EXPECT_EQ(probe[1], 1.0) << "index " << i;
}
MG_Impl::GLImpl::DepthRangeIndexed(0, 0.125, 0.875);
GLdouble classic[2] = {};
MG_Impl::GLImpl::GetDoublev(GL_DEPTH_RANGE, classic);
EXPECT_EQ(classic[0], 0.125);
EXPECT_EQ(classic[1], 0.875);
MG_Impl::GLImpl::DepthRange(0.0, 1.0);
ExpectSingleGlError(GL_NO_ERROR);
}
TEST_F(RenderStateTest, IndexedRectangleSettersRejectAnOutOfRangeIndex) {
const GLfloat viewport[4] = {0.0f, 0.0f, 1.0f, 1.0f};
const GLint scissor[4] = {0, 0, 1, 1};
for (const GLuint index : {kMaxViewports, kMaxViewports + 1}) {
MG_Impl::GLImpl::ViewportIndexedf(index, 0.0f, 0.0f, 1.0f, 1.0f);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::ViewportIndexedfv(index, viewport);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::ScissorIndexed(index, 0, 0, 1, 1);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::ScissorIndexedv(index, scissor);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::DepthRangeIndexed(index, 0.0, 1.0);
ExpectSingleGlError(GL_INVALID_VALUE);
}
// The last legal index must stay silent - api_errors checks both sides of the boundary.
MG_Impl::GLImpl::ViewportIndexedf(kMaxViewports - 1, 0.0f, 0.0f, 1.0f, 1.0f);
ExpectSingleGlError(GL_NO_ERROR);
MG_Impl::GLImpl::ScissorIndexed(kMaxViewports - 1, 0, 0, 1, 1);
ExpectSingleGlError(GL_NO_ERROR);
MG_Impl::GLImpl::DepthRangeIndexed(kMaxViewports - 1, 0.0, 1.0);
ExpectSingleGlError(GL_NO_ERROR);
}
TEST_F(RenderStateTest, ArraySettersRejectAnOutOfRangeRangeButAcceptAnExactlyFullOne) {
Array<GLfloat, kMaxViewports * 4> viewports{};
Array<GLint, kMaxViewports * 4> scissors{};
Array<GLdouble, kMaxViewports * 2> depths{};
for (GLuint i = 0; i < kMaxViewports; ++i) {
viewports[i * 4 + 2] = 1.0f;
viewports[i * 4 + 3] = 1.0f;
scissors[i * 4 + 2] = 1;
scissors[i * 4 + 3] = 1;
depths[i * 2 + 1] = 1.0;
}
// first == MAX_VIEWPORTS, and first + count > MAX_VIEWPORTS.
MG_Impl::GLImpl::ViewportArrayv(kMaxViewports, 1, viewports.data());
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::ViewportArrayv(1, kMaxViewports, viewports.data());
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::ScissorArrayv(kMaxViewports, 1, scissors.data());
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::ScissorArrayv(1, kMaxViewports, scissors.data());
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::DepthRangeArrayv(kMaxViewports, 1, depths.data());
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::DepthRangeArrayv(1, kMaxViewports, depths.data());
ExpectSingleGlError(GL_INVALID_VALUE);
// first + count == MAX_VIEWPORTS is LEGAL - the off-by-one an ">=" bound would get wrong,
// and one KHR-GL43.viewport_array.api_errors asserts explicitly.
MG_Impl::GLImpl::ViewportArrayv(1, kMaxViewports - 1, viewports.data());
ExpectSingleGlError(GL_NO_ERROR);
MG_Impl::GLImpl::ScissorArrayv(1, kMaxViewports - 1, scissors.data());
ExpectSingleGlError(GL_NO_ERROR);
MG_Impl::GLImpl::DepthRangeArrayv(1, kMaxViewports - 1, depths.data());
ExpectSingleGlError(GL_NO_ERROR);
// A negative count is GL_INVALID_VALUE and must not be read as a huge unsigned length.
MG_Impl::GLImpl::ViewportArrayv(0, -1, viewports.data());
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::ScissorArrayv(0, -1, scissors.data());
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::DepthRangeArrayv(0, -1, depths.data());
ExpectSingleGlError(GL_INVALID_VALUE);
}
TEST_F(RenderStateTest, NegativeExtentsAreRejectedWithoutDisturbingState) {
MG_Impl::GLImpl::Viewport(0, 0, 4, 4);
MG_Impl::GLImpl::Scissor(0, 0, 4, 4);
ExpectSingleGlError(GL_NO_ERROR);
MG_Impl::GLImpl::Viewport(0, 0, -1, 1);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::Viewport(0, 0, 1, -1);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::Scissor(0, 0, -1, 1);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::Scissor(0, 0, 1, -1);
ExpectSingleGlError(GL_INVALID_VALUE);
for (GLuint index = 0; index < kMaxViewports; ++index) {
MG_Impl::GLImpl::ViewportIndexedf(index, 0.0f, 0.0f, -1.0f, 1.0f);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::ViewportIndexedf(index, 0.0f, 0.0f, 1.0f, -1.0f);
ExpectSingleGlError(GL_INVALID_VALUE);
const GLfloat badW[4] = {0.0f, 0.0f, -1.0f, 1.0f};
MG_Impl::GLImpl::ViewportIndexedfv(index, badW);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::ScissorIndexed(index, 0, 0, -1, 1);
ExpectSingleGlError(GL_INVALID_VALUE);
const GLint badH[4] = {0, 0, 1, -1};
MG_Impl::GLImpl::ScissorIndexedv(index, badH);
ExpectSingleGlError(GL_INVALID_VALUE);
// The array form must reject the WHOLE call for one bad element, exactly once, and
// leave every rectangle alone - api_errors submits a full 16-element array with a
// single negative extent and then requires the error queue to hold one entry.
Array<GLfloat, kMaxViewports * 4> viewports{};
Array<GLint, kMaxViewports * 4> scissors{};
for (GLuint i = 0; i < kMaxViewports; ++i) {
viewports[i * 4 + 2] = 1.0f;
viewports[i * 4 + 3] = 1.0f;
scissors[i * 4 + 2] = 1;
scissors[i * 4 + 3] = 1;
}
viewports[index * 4 + 2] = -1.0f;
scissors[index * 4 + 3] = -1;
MG_Impl::GLImpl::ViewportArrayv(0, kMaxViewports, viewports.data());
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::ScissorArrayv(0, kMaxViewports, scissors.data());
ExpectSingleGlError(GL_INVALID_VALUE);
}
// Nothing above may have landed.
GLint viewport[4] = {};
MG_Impl::GLImpl::GetIntegeri_v(GL_VIEWPORT, 0, viewport);
EXPECT_EQ(viewport[2], 4);
EXPECT_EQ(viewport[3], 4);
GLint scissor[4] = {};
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 0, scissor);
EXPECT_EQ(scissor[2], 4);
EXPECT_EQ(scissor[3], 4);
ExpectSingleGlError(GL_NO_ERROR);
}
TEST_F(RenderStateTest, IndexedRectangleQueriesRejectAnOutOfRangeIndex) {
GLint ints[4] = {};
GLfloat floats[4] = {};
GLdouble doubles[4] = {};
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, kMaxViewports, ints);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, kMaxViewports, floats);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, kMaxViewports, doubles);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, kMaxViewports - 1, ints);
ExpectSingleGlError(GL_NO_ERROR);
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, kMaxViewports - 1, floats);
ExpectSingleGlError(GL_NO_ERROR);
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, kMaxViewports - 1, doubles);
ExpectSingleGlError(GL_NO_ERROR);
}
// ---------------------------------------------------------------------------------------------
// "Has the application written this scissor rectangle?" - the flag, not the extent
// ---------------------------------------------------------------------------------------------
// glScissor(0, 0, 0, 0) is legal GL and means "the scissor test rejects every fragment", but it
// is byte-identical to the never-written default, whose meaning is the opposite ("the whole
// window", which the frontend cannot spell before a surface exists). DirectGLES resolved the two
// by looking at the EXTENT and so inverted every deliberately empty box into the full surface -
// KHR-GL43.viewport_array.scissor_zero_dimension is exactly that draw, and it came back holding
// the drawn colour where the untouched fill was required.
//
// These drive RenderState directly instead of the GL entry points on purpose: the flag's whole
// content is what it says BEFORE the first scissor call of a context, and this binary shares one
// context across every case in the file, so a pristine object is the only place that state
// still exists by the time these run.
namespace {
constexpr Uint32 kAllViewportsWritten =
RenderStateParameters::MAX_VIEWPORTS >= 32 ? ~0u : (1u << RenderStateParameters::MAX_VIEWPORTS) - 1u;
} // namespace
TEST_F(RenderStateTest, AnEmptyScissorBoxIsDistinguishableFromNeverHavingBeenWritten) {
MG_State::GLState::RenderState state;
EXPECT_EQ(state.GetAllParameters().ScissorBoxWrittenMask, 0u)
<< "a fresh context has never been given a scissor box, and the all-zero rectangle it "
"starts with must not be mistaken for one";
// Not one stored byte moves here - every box already held (0,0,0,0) - and yet this is the
// call that turns "the frontend does not know the window size" into "reject every fragment".
state.SetScissorBox(IntVec4(0, 0, 0, 0));
EXPECT_EQ(state.GetAllParameters().ScissorBoxWrittenMask, kAllViewportsWritten);
for (GLuint index = 0; index < kMaxViewports; ++index) {
EXPECT_EQ(state.GetScissorBoxIndexed(index), IntVec4(0, 0, 0, 0)) << "index " << index;
}
}
TEST_F(RenderStateTest, AnIndexedScissorWriteClaimsOnlyItsOwnIndex) {
MG_State::GLState::RenderState state;
state.SetScissorBoxIndexed(5, IntVec4(0, 0, 0, 0));
EXPECT_EQ(state.GetAllParameters().ScissorBoxWrittenMask, 1u << 5);
state.SetScissorBoxIndexed(0, IntVec4(0, 0, 0, 0));
EXPECT_EQ(state.GetAllParameters().ScissorBoxWrittenMask, (1u << 5) | 1u);
// ARB_viewport_array makes the non-indexed setter a write to every index, so it claims all 16.
state.SetScissorBox(IntVec4(1, 2, 3, 4));
EXPECT_EQ(state.GetAllParameters().ScissorBoxWrittenMask, kAllViewportsWritten);
}
TEST_F(RenderStateTest, TheFirstScissorWriteBumpsTheVersionEvenWhenTheValueDoesNotMove) {
// Load-bearing, and not merely tidy: DirectGLES' SyncRenderState early-outs on an unchanged
// render-state version BEFORE it reaches the span memcmp that would otherwise notice the
// flag. A version-less transition would sit in the parameter block, never be pushed, and the
// empty box would go on rendering as the whole surface.
MG_State::GLState::RenderState state;
const Uint initial = state.GetVersion();
state.SetScissorBox(IntVec4(0, 0, 0, 0));
EXPECT_GT(state.GetVersion(), initial) << "claiming the rectangle is itself a state change";
// Once claimed, a genuinely redundant write stays free - the flag costs one transition, not
// a version bump per call.
const Uint settled = state.GetVersion();
state.SetScissorBox(IntVec4(0, 0, 0, 0));
EXPECT_EQ(state.GetVersion(), settled);
MG_State::GLState::RenderState indexed;
const Uint indexedInitial = indexed.GetVersion();
indexed.SetScissorBoxIndexed(3, IntVec4(0, 0, 0, 0));
EXPECT_GT(indexed.GetVersion(), indexedInitial);
const Uint indexedSettled = indexed.GetVersion();
indexed.SetScissorBoxIndexed(3, IntVec4(0, 0, 0, 0));
EXPECT_EQ(indexed.GetVersion(), indexedSettled);
}
// --- glPatchParameterfv (GL 4.6 core 11.2.2) ---------------------------------------------------
//
// GL_PATCH_DEFAULT_OUTER_LEVEL / GL_PATCH_DEFAULT_INNER_LEVEL are the tessellation levels a
// program with an evaluation stage and NO control stage runs at. glPatchParameterfv was a stub
// that stored nothing and raised nothing, so the state could never move off its 1.0 default and
// both backends hardcoded 1.0 into the pass-through control stage they synthesize. The getters
// were absent too, which is what KHR-GL4x.tessellation_shader.single.
// default_values_of_context_wide_properties dies on.
TEST_F(RenderStateTest, PatchDefaultLevelsStartAtTheGLDefault) {
GLfloat outer[4] = {-1.0f, -1.0f, -1.0f, -1.0f};
MG_Impl::GLImpl::GetFloatv(GL_PATCH_DEFAULT_OUTER_LEVEL, outer);
ExpectSingleGlError(GL_NO_ERROR);
for (const GLfloat level : outer) EXPECT_FLOAT_EQ(level, 1.0f);
GLfloat inner[2] = {-1.0f, -1.0f};
MG_Impl::GLImpl::GetFloatv(GL_PATCH_DEFAULT_INNER_LEVEL, inner);
ExpectSingleGlError(GL_NO_ERROR);
for (const GLfloat level : inner) EXPECT_FLOAT_EQ(level, 1.0f);
}
TEST_F(RenderStateTest, PatchDefaultLevelsRoundTripThroughEveryGetter) {
const GLfloat outerIn[4] = {2.0f, 3.5f, 4.0f, 5.25f};
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_OUTER_LEVEL, outerIn);
ExpectSingleGlError(GL_NO_ERROR);
const GLfloat innerIn[2] = {6.5f, 7.0f};
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_INNER_LEVEL, innerIn);
ExpectSingleGlError(GL_NO_ERROR);
GLfloat outer[4] = {};
MG_Impl::GLImpl::GetFloatv(GL_PATCH_DEFAULT_OUTER_LEVEL, outer);
EXPECT_FLOAT_EQ(outer[0], 2.0f);
EXPECT_FLOAT_EQ(outer[1], 3.5f);
EXPECT_FLOAT_EQ(outer[2], 4.0f);
EXPECT_FLOAT_EQ(outer[3], 5.25f);
GLfloat inner[2] = {};
MG_Impl::GLImpl::GetFloatv(GL_PATCH_DEFAULT_INNER_LEVEL, inner);
EXPECT_FLOAT_EQ(inner[0], 6.5f);
EXPECT_FLOAT_EQ(inner[1], 7.0f);
ExpectSingleGlError(GL_NO_ERROR);
// Float state read through the integer and boolean getters: glGetIntegerv rounds (GL 4.6 core
// 2.2.2) and glGetBooleanv delegates to it, so both must ANSWER rather than report
// INVALID_ENUM - which is exactly what the conformance suite asks them first.
GLint outerInts[4] = {};
MG_Impl::GLImpl::GetIntegerv(GL_PATCH_DEFAULT_OUTER_LEVEL, outerInts);
EXPECT_EQ(outerInts[0], 2);
EXPECT_EQ(outerInts[1], 4) << "3.5 rounds away from zero";
EXPECT_EQ(outerInts[3], 5);
ExpectSingleGlError(GL_NO_ERROR);
GLboolean outerBools[4] = {};
MG_Impl::GLImpl::GetBooleanv(GL_PATCH_DEFAULT_OUTER_LEVEL, outerBools);
EXPECT_EQ(outerBools[0], GL_TRUE);
ExpectSingleGlError(GL_NO_ERROR);
GLdouble outerDoubles[4] = {};
MG_Impl::GLImpl::GetDoublev(GL_PATCH_DEFAULT_OUTER_LEVEL, outerDoubles);
EXPECT_DOUBLE_EQ(outerDoubles[3], 5.25) << "glGetDoublev must widen all four, not just the first";
ExpectSingleGlError(GL_NO_ERROR);
// glGetInteger64v shares glGetIntegerv's accepted-pname set (GL 4.6 core 22.1), so it owes the
// same component count. Its own table listed neither pname, so three of the four words were
// left holding whatever the caller's buffer held - and no error said so.
GLint64 outerLongs[4] = {9, 9, 9, 9};
MG_Impl::GLImpl::GetInteger64v(GL_PATCH_DEFAULT_OUTER_LEVEL, outerLongs);
EXPECT_EQ(outerLongs[0], 2);
EXPECT_EQ(outerLongs[3], 5) << "glGetInteger64v must write all four, not just the first";
GLint64 innerLongs[2] = {9, 9};
MG_Impl::GLImpl::GetInteger64v(GL_PATCH_DEFAULT_INNER_LEVEL, innerLongs);
EXPECT_EQ(innerLongs[1], 7);
ExpectSingleGlError(GL_NO_ERROR);
// Put the context back where the rest of the binary expects it.
const GLfloat defaults4[4] = {1.0f, 1.0f, 1.0f, 1.0f};
const GLfloat defaults2[2] = {1.0f, 1.0f};
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_OUTER_LEVEL, defaults4);
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_INNER_LEVEL, defaults2);
DrainPendingGlErrors();
}
// GL 4.6 core 2.2.2: a float state comes back through glGetBooleanv as GL_FALSE only when it is
// zero. Deriving the answer from glGetIntegerv - which rounds - reported GL_FALSE for a level of
// 0.25, which is neither zero nor anything the application asked to be rounded.
TEST_F(RenderStateTest, PatchDefaultLevelsBelowHalfAreStillTrueAsBooleans) {
const GLfloat fractional[4] = {0.25f, 0.0f, 0.4f, 0.25f};
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_OUTER_LEVEL, fractional);
const GLfloat fractionalInner[2] = {0.25f, 0.0f};
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_INNER_LEVEL, fractionalInner);
ExpectSingleGlError(GL_NO_ERROR);
GLboolean outer[4] = {};
MG_Impl::GLImpl::GetBooleanv(GL_PATCH_DEFAULT_OUTER_LEVEL, outer);
EXPECT_EQ(outer[0], GL_TRUE) << "0.25 is not zero";
EXPECT_EQ(outer[1], GL_FALSE) << "0.0 is the one value that is false";
EXPECT_EQ(outer[2], GL_TRUE);
GLboolean inner[2] = {};
MG_Impl::GLImpl::GetBooleanv(GL_PATCH_DEFAULT_INNER_LEVEL, inner);
EXPECT_EQ(inner[0], GL_TRUE);
EXPECT_EQ(inner[1], GL_FALSE);
ExpectSingleGlError(GL_NO_ERROR);
const GLfloat defaults4[4] = {1.0f, 1.0f, 1.0f, 1.0f};
const GLfloat defaults2[2] = {1.0f, 1.0f};
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_OUTER_LEVEL, defaults4);
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_INNER_LEVEL, defaults2);
DrainPendingGlErrors();
}
TEST_F(RenderStateTest, PatchParameterfvRejectsEveryOtherPname) {
const GLfloat levels[4] = {1.0f, 1.0f, 1.0f, 1.0f};
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_VERTICES, levels);
ExpectSingleGlError(GL_INVALID_ENUM);
MG_Impl::GLImpl::PatchParameterfv(GL_MAX_PATCH_VERTICES, levels);
ExpectSingleGlError(GL_INVALID_ENUM);
// The integer setter keeps its own, disjoint, accepted pname.
MG_Impl::GLImpl::PatchParameteri(GL_PATCH_DEFAULT_OUTER_LEVEL, 4);
ExpectSingleGlError(GL_INVALID_ENUM);
}
TEST_F(RenderStateTest, PatchDefaultLevelsAreTreatedAsPipelineState) {
// Load-bearing: both backends compile these numbers into the pass-through tessellation control
// stage they synthesize, so a change has to invalidate an already-built program the same way a
// glPatchParameteri does. Bumping only the all-state version would leave DirectVulkan's
// pipeline memo - which keys on the PIPELINE-state version - handing back a pipeline built
// with the old levels.
MG_State::GLState::RenderState state;
const Uint initialPipelineVersion = state.GetPipelineStateVersion();
state.SetPatchDefaultOuterLevel(FloatVec4(2.0f, 2.0f, 2.0f, 2.0f));
EXPECT_GT(state.GetPipelineStateVersion(), initialPipelineVersion);
const Uint settled = state.GetPipelineStateVersion();
state.SetPatchDefaultOuterLevel(FloatVec4(2.0f, 2.0f, 2.0f, 2.0f));
EXPECT_EQ(state.GetPipelineStateVersion(), settled) << "a redundant write is free";
state.SetPatchDefaultInnerLevel(FloatVec2(3.0f, 3.0f));
EXPECT_GT(state.GetPipelineStateVersion(), settled);
}
// glPatchParameterfv accepts NaN by design, and NaN is never equal to itself under IEEE `==`. A
// value-compared redundant-write guard therefore never settles: every re-set of the identical
// tuple bumps the pipeline-state version, and - one level down - DirectGLES's staleness clause
// re-transpiles, re-compiles and re-links the synthesized pass-through stage on every draw. Both
// compare BIT PATTERNS instead, which is what DirectVulkan's module key already hashes.
TEST_F(RenderStateTest, ARedundantNaNPatchLevelWriteSettlesInsteadOfBumpingForever) {
const Float notANumber = std::numeric_limits<Float>::quiet_NaN();
MG_State::GLState::RenderState state;
state.SetPatchDefaultOuterLevel(FloatVec4(notANumber, 1.0f, 1.0f, 1.0f));
const Uint afterFirst = state.GetPipelineStateVersion();
state.SetPatchDefaultOuterLevel(FloatVec4(notANumber, 1.0f, 1.0f, 1.0f));
EXPECT_EQ(state.GetPipelineStateVersion(), afterFirst)
<< "the identical NaN tuple is not a state change";
state.SetPatchDefaultInnerLevel(FloatVec2(notANumber, 1.0f));
const Uint afterInner = state.GetPipelineStateVersion();
state.SetPatchDefaultInnerLevel(FloatVec2(notANumber, 1.0f));
EXPECT_EQ(state.GetPipelineStateVersion(), afterInner);
// A genuinely different tuple still moves, so the guard has not simply gone blind.
state.SetPatchDefaultOuterLevel(FloatVec4(notANumber, 2.0f, 1.0f, 1.0f));
EXPECT_GT(state.GetPipelineStateVersion(), afterInner);
}
// --- desktop GL_PRIMITIVE_RESTART state --------------------------------------------------------
//
// The cap and its index are what a desktop application enables instead of ES's
// GL_PRIMITIVE_RESTART_FIXED_INDEX. Both halves have to be answerable, because the backends read
// them on every indexed draw to decide whether the index data needs rewriting.
TEST_F(RenderStateTest, PrimitiveRestartCapAndIndexAreBothQueryable) {
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_PRIMITIVE_RESTART), GL_FALSE);
ExpectSingleGlError(GL_NO_ERROR);
MG_Impl::GLImpl::Enable(GL_PRIMITIVE_RESTART);
MG_Impl::GLImpl::PrimitiveRestartIndex(1026u);
ExpectSingleGlError(GL_NO_ERROR);
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_PRIMITIVE_RESTART), GL_TRUE);
GLint index = 0;
MG_Impl::GLImpl::GetIntegerv(GL_PRIMITIVE_RESTART_INDEX, &index);
EXPECT_EQ(index, 1026);
// The fixed-index cap is a separate piece of state and must not have moved.
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_PRIMITIVE_RESTART_FIXED_INDEX), GL_FALSE);
ExpectSingleGlError(GL_NO_ERROR);
MG_Impl::GLImpl::Disable(GL_PRIMITIVE_RESTART);
MG_Impl::GLImpl::PrimitiveRestartIndex(0u);
DrainPendingGlErrors();
}
// glMinSampleShading was a logging no-op while ARB_sample_shading was advertised and
// glEnable(GL_SAMPLE_SHADING) fell through RenderState::SetCapability's default arm, so an
// application could turn sample shading on, ask for a rate, and get neither - with every query
// agreeing that nothing had happened.
TEST_F(RenderStateTest, MinSampleShadingRoundTripsAndClamps) {
DrainPendingGlErrors();
// GL 4.6 core table 23.10: the initial value is 0.
GLfloat initial = -1.0f;
MG_Impl::GLImpl::GetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &initial);
EXPECT_FLOAT_EQ(initial, 0.0f);
MG_Impl::GLImpl::MinSampleShading(0.25f);
GLfloat value = -1.0f;
MG_Impl::GLImpl::GetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &value);
EXPECT_FLOAT_EQ(value, 0.25f);
// The fraction survives the double query too, and rounds - not truncates - for the integer one.
GLdouble asDouble = -1.0;
MG_Impl::GLImpl::GetDoublev(GL_MIN_SAMPLE_SHADING_VALUE, &asDouble);
EXPECT_NEAR(asDouble, 0.25, 1e-6);
GLint asInt = -1;
MG_Impl::GLImpl::GetIntegerv(GL_MIN_SAMPLE_SHADING_VALUE, &asInt);
EXPECT_EQ(asInt, 0);
// A non-zero fraction is GL_TRUE, which the integer path would have rounded away first.
GLboolean asBoolean = GL_FALSE;
MG_Impl::GLImpl::GetBooleanv(GL_MIN_SAMPLE_SHADING_VALUE, &asBoolean);
EXPECT_EQ(asBoolean, GL_TRUE);
// "value is clamped to [0, 1]" - not an error, a clamp.
MG_Impl::GLImpl::MinSampleShading(2.0f);
MG_Impl::GLImpl::GetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &value);
EXPECT_FLOAT_EQ(value, 1.0f);
MG_Impl::GLImpl::MinSampleShading(-3.0f);
MG_Impl::GLImpl::GetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &value);
EXPECT_FLOAT_EQ(value, 0.0f);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::MinSampleShading(0.0f);
}
TEST_F(RenderStateTest, SampleShadingEnableIsStoredAndQueryable) {
DrainPendingGlErrors();
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SAMPLE_SHADING), GL_FALSE);
MG_Impl::GLImpl::Enable(GL_SAMPLE_SHADING);
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SAMPLE_SHADING), GL_TRUE);
GLboolean asBoolean = GL_FALSE;
MG_Impl::GLImpl::GetBooleanv(GL_SAMPLE_SHADING, &asBoolean);
EXPECT_EQ(asBoolean, GL_TRUE);
GLint asInt = 0;
MG_Impl::GLImpl::GetIntegerv(GL_SAMPLE_SHADING, &asInt);
EXPECT_EQ(asInt, GL_TRUE);
MG_Impl::GLImpl::Disable(GL_SAMPLE_SHADING);
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SAMPLE_SHADING), GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// glClipControl and glPolygonOffsetClamp were DECLARE_GL_FUNCTION_STUB_HEAD entry points: they
// took their arguments, recorded nothing and raised no error, and the state variables they own
// (GL_CLIP_ORIGIN, GL_CLIP_DEPTH_MODE, GL_POLYGON_OFFSET_CLAMP) had no arm in any getter, so the
// very first query of a conformance case raised GL_INVALID_ENUM and killed it. These assertions
// are state-shaped on purpose - the rasterization half of clip control is a backend question, but
// the state machine has to round-trip regardless of what a backend does with it.
TEST_F(RenderStateTest, ClipControlStateRoundTripsAndDefaultsToLowerLeftNegativeOneToOne) {
DrainPendingGlErrors();
GLint origin = 0;
GLint depthMode = 0;
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_ORIGIN, &origin);
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_DEPTH_MODE, &depthMode);
EXPECT_EQ(origin, GL_LOWER_LEFT);
EXPECT_EQ(depthMode, GL_NEGATIVE_ONE_TO_ONE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::ClipControl(GL_UPPER_LEFT, GL_ZERO_TO_ONE);
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_ORIGIN, &origin);
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_DEPTH_MODE, &depthMode);
EXPECT_EQ(origin, GL_UPPER_LEFT);
EXPECT_EQ(depthMode, GL_ZERO_TO_ONE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// Every getter flavour has to answer, not just the integer one - the conformance suite reads
// this state through all of them.
GLfloat asFloat = 0.0f;
MG_Impl::GLImpl::GetFloatv(GL_CLIP_ORIGIN, &asFloat);
EXPECT_EQ(static_cast<GLint>(asFloat), GL_UPPER_LEFT);
GLint64 asInt64 = 0;
MG_Impl::GLImpl::GetInteger64v(GL_CLIP_DEPTH_MODE, &asInt64);
EXPECT_EQ(static_cast<GLint>(asInt64), GL_ZERO_TO_ONE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::ClipControl(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(RenderStateTest, ClipControlRejectsBadEnumsAndLeavesTheStateAlone) {
DrainPendingGlErrors();
MG_Impl::GLImpl::ClipControl(GL_UPPER_LEFT, GL_ZERO_TO_ONE);
DrainPendingGlErrors();
MG_Impl::GLImpl::ClipControl(GL_FRONT, GL_ZERO_TO_ONE);
ExpectSingleGlError(GL_INVALID_ENUM);
MG_Impl::GLImpl::ClipControl(GL_UPPER_LEFT, GL_FRONT);
ExpectSingleGlError(GL_INVALID_ENUM);
GLint origin = 0;
GLint depthMode = 0;
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_ORIGIN, &origin);
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_DEPTH_MODE, &depthMode);
EXPECT_EQ(origin, GL_UPPER_LEFT) << "a rejected glClipControl must not change the state";
EXPECT_EQ(depthMode, GL_ZERO_TO_ONE) << "a rejected glClipControl must not change the state";
MG_Impl::GLImpl::ClipControl(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE);
DrainPendingGlErrors();
}
TEST_F(RenderStateTest, PolygonOffsetClampStoresTheClampAndTheFactorUnitsPair) {
DrainPendingGlErrors();
GLfloat clamp = -1.0f;
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
EXPECT_FLOAT_EQ(clamp, 0.0f) << "the default clamp is zero, i.e. no clamping";
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::PolygonOffsetClamp(1.5f, 2.5f, 0.5f);
GLfloat factor = 0.0f;
GLfloat units = 0.0f;
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_FACTOR, &factor);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_UNITS, &units);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
EXPECT_FLOAT_EQ(factor, 1.5f);
EXPECT_FLOAT_EQ(units, 2.5f);
EXPECT_FLOAT_EQ(clamp, 0.5f) << "the fractional clamp must survive - the integer path rounds it away";
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// glcPolygonOffsetClampTests reads GL_POLYGON_OFFSET_CLAMP through all five getters and
// requires no error from any of them; that is what used to kill the availability case.
GLboolean asBoolean = GL_FALSE;
MG_Impl::GLImpl::GetBooleanv(GL_POLYGON_OFFSET_CLAMP, &asBoolean);
EXPECT_EQ(asBoolean, GL_TRUE);
GLint asInt = -1;
MG_Impl::GLImpl::GetIntegerv(GL_POLYGON_OFFSET_CLAMP, &asInt);
EXPECT_EQ(asInt, 1) << "0.5 rounds to nearest for the integer query";
GLint64 asInt64 = -1;
MG_Impl::GLImpl::GetInteger64v(GL_POLYGON_OFFSET_CLAMP, &asInt64);
EXPECT_EQ(asInt64, 1);
GLdouble asDouble = -1.0;
MG_Impl::GLImpl::GetDoublev(GL_POLYGON_OFFSET_CLAMP, &asDouble);
EXPECT_NEAR(asDouble, 0.5, 1e-6);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// GL 4.6 core 14.6.5 defines glPolygonOffset(factor, units) as EQUIVALENT to
// glPolygonOffsetClamp(factor, units, 0) - totally, not "except for the clamp". So it writes
// all three, and a clamp left over from an earlier glPolygonOffsetClamp must be gone.
MG_Impl::GLImpl::PolygonOffset(3.0f, 4.0f);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_FACTOR, &factor);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_UNITS, &units);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
EXPECT_FLOAT_EQ(factor, 3.0f);
EXPECT_FLOAT_EQ(units, 4.0f);
EXPECT_FLOAT_EQ(clamp, 0.0f) << "glPolygonOffset IS PolygonOffsetClamp(factor, units, 0)";
// The same rule when factor and units do NOT change: the clamp still has to be cleared, which
// an early-out keyed on the factor/units pair alone would skip.
MG_Impl::GLImpl::PolygonOffsetClamp(3.0f, 4.0f, 0.75f);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
ASSERT_FLOAT_EQ(clamp, 0.75f);
MG_Impl::GLImpl::PolygonOffset(3.0f, 4.0f);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
EXPECT_FLOAT_EQ(clamp, 0.0f) << "a no-op factor/units write must still clear the clamp";
MG_Impl::GLImpl::PolygonOffsetClamp(0.0f, 0.0f, 0.0f);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL_TEXTURE_BUFFER_BINDING (0x8C2A) is the same token as GL_TEXTURE_BUFFER; as a glGetIntegerv
// pname it asks which BUFFER object is bound there, and it had no arm at all, so
// esextcTextureBufferParameters died on its first query.
TEST_F(RenderStateTest, TextureBufferBindingAnswersTheBoundBufferName) {
DrainPendingGlErrors();
GLint binding = -1;
MG_Impl::GLImpl::GetIntegerv(GL_TEXTURE_BUFFER_BINDING, &binding);
EXPECT_EQ(binding, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL_ARB_spirv_extensions. Zero is legal and true: MobileGL relies on no SPIR-V extension, so
// glGetStringi(GL_SPIR_V_EXTENSIONS, i) is never legally reached.
TEST_F(RenderStateTest, NumSpirVExtensionsIsQueryableAndZero) {
DrainPendingGlErrors();
GLint count = -1;
MG_Impl::GLImpl::GetIntegerv(GL_NUM_SPIR_V_EXTENSIONS, &count);
EXPECT_EQ(count, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}