mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 20:58:31 +09:00
1029 lines
47 KiB
C++
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);
|
|
}
|