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

525 lines
26 KiB
C++

// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ViewportArrayScenario.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
//
// Scenario - gl_ViewportIndex ACTUALLY ROUTES, AND THE PER-INDEX STATE IT SELECTS IS REAL.
//
// The state half of ARB_viewport_array is asserted in MG_Test/State/RenderStateTest.cpp, which
// is a pure set/get exercise and would pass just as green against a backend that stores all 16
// rectangles and rasterizes only the first. This file is the other half: every case here routes
// primitives to a viewport OTHER than 0 and then looks at where the pixels landed.
//
// Three claims, one per case:
// 1. gl_ViewportIndex selects the viewport RECTANGLE - a 4x4 grid of 32x32 viewports, one
// geometry-shader invocation per cell, and every cell must hold its own index.
// 2. gl_ViewportIndex selects the DEPTH RANGE - 16 one-pixel-wide viewports whose ranges are
// (i/16, 1 - i/16), a quad at each end of clip space, and gl_FragCoord.z read back.
// This is the claim that fails loudest against a single-viewport backend, because the
// geometry is still in the right place while every depth comes back as viewport 0's.
// 3. The per-index SCISSOR TEST ENABLE is honoured. Vulkan has no per-viewport scissor-test
// toggle, so a disabled index has to be given the whole framebuffer as its rectangle; the
// case draws the same primitive into the same index twice, once with the test off and once
// with it on, and requires the two results to differ in the documented direction.
//
// Case 1 runs a second time against the DEFAULT framebuffer. MobileGL Y-flips (and pre-transform
// rotates) the default framebuffer's rectangles and does not touch an FBO's, so a port that
// applies the flip to viewport 0 and forgets the other fifteen renders a correct-looking FBO and
// an upside-down window - the classic multi-viewport bug, and invisible to every FBO-only case.
//
// HONEST LIMIT OF THIS FILE. DirectGLES SKIPS every case: GLES has one viewport, one scissor
// rectangle and no gl_ViewportIndex, so routing to index > 0 is an emulation feature that has
// not been built (the Espryt half of KHR-GL43.viewport_array's rendering group is deliberately
// still red). The skip is explicit rather than silent so a future emulation lands here as a
// failing test and not as a test that was quietly never running. DirectVulkan additionally
// skips when the device lacks the multiViewport feature - Vulkan then forbids a pipeline from
// declaring more than one viewport at all, which is a device limit and not a MobileGL bug;
// lavapipe (every CI lane) and both Mali/Adreno devices support it, so the cases do run where
// it matters.
#include <cmath>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kViewportCount = 16;
constexpr int kGridSide = 4; // 4x4 grid of viewports
constexpr int kCellSize = 32; // ... each 32x32
constexpr int kSurfaceSide = kGridSide * kCellSize;
constexpr GLint kUnwritten = -1;
// A geometry shader is the only stage GL 4.1 lets write gl_ViewportIndex, and
// `invocations` runs it once per viewport off a single input point - the same shape
// KHR-GL43.viewport_array.draw_to_single_layer_with_multiple_viewports uses.
const char* const kVertexSource = R"(#version 410 core
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
)";
const char* const kGridGeometrySource = R"(#version 410 core
layout(points, invocations = 16) in;
layout(triangle_strip, max_vertices = 4) out;
flat out int gsIndex;
void main() {
gsIndex = gl_InvocationID;
gl_ViewportIndex = gl_InvocationID;
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
EndPrimitive();
}
)";
// One invocation, viewport chosen by a uniform: lets a case draw the SAME primitive into
// the SAME index twice under two different scissor-enable states.
const char* const kSingleGeometrySource = R"(#version 410 core
layout(points, invocations = 1) in;
layout(triangle_strip, max_vertices = 4) out;
uniform int uViewport;
flat out int gsIndex;
void main() {
gsIndex = uViewport;
gl_ViewportIndex = uViewport;
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
EndPrimitive();
}
)";
const char* const kIntFragmentSource = R"(#version 410 core
flat in int gsIndex;
layout(location = 0) out int fragColor;
void main() { fragColor = gsIndex; }
)";
// Two quads, one at each end of clip space, so the fragment stage can report the depth
// the viewport's range mapped them to. gl_FragCoord.z IS the post-range window depth, so
// it reads back the per-viewport minDepth/maxDepth directly.
const char* const kDepthGeometrySource = R"(#version 410 core
layout(points, invocations = 16) in;
layout(triangle_strip, max_vertices = 8) out;
void main() {
gl_ViewportIndex = gl_InvocationID;
gl_Position = vec4(-1.0, -1.0, -1.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, -1.0, -1.0, 1.0); EmitVertex();
gl_Position = vec4(-1.0, 0.0, -1.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, 0.0, -1.0, 1.0); EmitVertex();
EndPrimitive();
gl_Position = vec4(-1.0, 0.0, 1.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, 0.0, 1.0, 1.0); EmitVertex();
gl_Position = vec4(-1.0, 1.0, 1.0, 1.0); EmitVertex();
gl_Position = vec4( 1.0, 1.0, 1.0, 1.0); EmitVertex();
EndPrimitive();
}
)";
const char* const kDepthFragmentSource = R"(#version 410 core
layout(location = 0) out float fragColor;
void main() { fragColor = gl_FragCoord.z; }
)";
class ViewportArrayScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
if (Gl().BackendName() == "DirectGLES") {
GTEST_SKIP() << "gl_ViewportIndex routing is not emulated on DirectGLES: GLES has one viewport "
"and one scissor rectangle, so every index rasterizes as index 0. The indexed "
"STATE is still asserted (MG_Test RenderStateTest); this is the deferred "
"rendering half of KHR-GL43.viewport_array.";
}
GLint maxViewports = 0;
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
ASSERT_GE(maxViewports, kViewportCount) << "GL 4.3 core requires GL_MAX_VIEWPORTS >= 16";
m_program = BuildProgram(kGridGeometrySource, kIntFragmentSource);
ASSERT_NE(m_program, 0u) << "grid program failed to build: " << m_buildLog;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
ResetViewportArrayState();
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "setup left a GL error behind";
}
void TearDown() override {
if (!Ready() || IsSkipped()) return;
ResetViewportArrayState();
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_program != 0) glDeleteProgram(m_program);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
while (glGetError() != GL_NO_ERROR) {
}
}
// Every case starts from the same slate: this fixture shares its context with every
// other scenario in the process, and a leftover per-index scissor enable is exactly
// the kind of state that would make a later case pass or fail for the wrong reason.
static void ResetViewportArrayState() {
for (int i = 0; i < kViewportCount; ++i) {
glDisablei(GL_SCISSOR_TEST, static_cast<GLuint>(i));
}
glDisable(GL_SCISSOR_TEST);
glViewport(0, 0, kSurfaceSide, kSurfaceSide);
glScissor(0, 0, kSurfaceSide, kSurfaceSide);
glDepthRange(0.0, 1.0);
glDisable(GL_DEPTH_TEST);
}
// The 4x4 grid: viewport y*4+x covers the cell whose lower-left corner is
// (x*cellW, y*cellH), in GL's bottom-left-origin window coordinates. Parameterized on
// the cell size because the default framebuffer this scenario also renders into is
// deliberately non-square (HeadlessGL is 128x96, so a transposing bug cannot hide).
static void SetupGridViewports(int cellW, int cellH) {
std::vector<GLfloat> data(static_cast<size_t>(kViewportCount) * 4);
for (int y = 0; y < kGridSide; ++y) {
for (int x = 0; x < kGridSide; ++x) {
const size_t base = static_cast<size_t>(y * kGridSide + x) * 4;
data[base + 0] = static_cast<GLfloat>(x * cellW);
data[base + 1] = static_cast<GLfloat>(y * cellH);
data[base + 2] = static_cast<GLfloat>(cellW);
data[base + 3] = static_cast<GLfloat>(cellH);
}
}
glViewportArrayv(0, kViewportCount, data.data());
}
GLuint BuildProgram(const char* geometrySource, const char* fragmentSource) {
const GLuint vs = CompileStage(GL_VERTEX_SHADER, kVertexSource);
if (vs == 0) return 0;
const GLuint gs = CompileStage(GL_GEOMETRY_SHADER, geometrySource);
if (gs == 0) {
glDeleteShader(vs);
return 0;
}
const GLuint fs = CompileStage(GL_FRAGMENT_SHADER, fragmentSource);
if (fs == 0) {
glDeleteShader(vs);
glDeleteShader(gs);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, gs);
glAttachShader(program, fs);
glLinkProgram(program);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
glDeleteShader(vs);
glDeleteShader(gs);
glDeleteShader(fs);
if (!linked) {
GLint length = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), nullptr, log.data());
m_buildLog = log.data();
glDeleteProgram(program);
return 0;
}
return program;
}
GLuint CompileStage(GLenum stage, const char* source) {
const GLuint shader = glCreateShader(stage);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled) return shader;
GLint length = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
m_buildLog = log.data();
glDeleteShader(shader);
return 0;
}
// An R32I colour target, pre-filled with kUnwritten so "nothing was drawn here" is
// distinguishable from "index 0 was drawn here".
struct IntTarget {
GLuint fbo = 0;
GLuint texture = 0;
};
// The "nothing drawn here" value is UPLOADED, not cleared: the CTS fills its R32I
// targets the same way (fillTexture), and an upload cannot be confused with a clear
// that a backend defers, reorders or drops - which is exactly the ambiguity a case
// asserting "this cell must be untouched" cannot afford.
static void FillIntTarget(const IntTarget& target, int width, int height) {
const std::vector<GLint> unwritten(static_cast<size_t>(width) * height, kUnwritten);
glBindTexture(GL_TEXTURE_2D, target.texture);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RED_INTEGER, GL_INT, unwritten.data());
}
static IntTarget MakeIntTarget(int width, int height) {
IntTarget target;
glGenTextures(1, &target.texture);
glBindTexture(GL_TEXTURE_2D, target.texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32I, width, height, 0, GL_RED_INTEGER, GL_INT, nullptr);
glGenFramebuffers(1, &target.fbo);
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
FillIntTarget(target, width, height);
return target;
}
static void DestroyIntTarget(IntTarget& target) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (target.fbo != 0) glDeleteFramebuffers(1, &target.fbo);
if (target.texture != 0) glDeleteTextures(1, &target.texture);
}
static std::vector<GLint> ReadInts(int width, int height) {
std::vector<GLint> pixels(static_cast<size_t>(width) * height, 0);
glReadPixels(0, 0, width, height, GL_RED_INTEGER, GL_INT, pixels.data());
return pixels;
}
// The centre of grid cell (x, y), in the bottom-left-origin coordinates glReadPixels
// returns. Sampling the centre rather than a corner keeps the assertion about WHICH
// viewport was selected rather than about edge rounding.
static GLint CellCentre(const std::vector<GLint>& pixels, int stride, int x, int y) {
const int px = x * kCellSize + kCellSize / 2;
const int py = y * kCellSize + kCellSize / 2;
return pixels[static_cast<size_t>(py) * stride + px];
}
std::string m_buildLog;
GLuint m_program = 0;
GLuint m_vao = 0;
};
// --- 1. the viewport rectangle -------------------------------------------------------
TEST_F(ViewportArrayScenario, EachViewportIndexRasterizesIntoItsOwnRectangle) {
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
SetupGridViewports(kCellSize, kCellSize);
glUseProgram(m_program);
glBindVertexArray(m_vao);
glDrawArrays(GL_POINTS, 0, 1);
ASSERT_EQ(glGetError(), GL_NO_ERROR);
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
for (int y = 0; y < kGridSide; ++y) {
for (int x = 0; x < kGridSide; ++x) {
const GLint expected = y * kGridSide + x;
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), expected)
<< "cell (" << x << ", " << y << ") should hold viewport index " << expected
<< "; a single-viewport backend paints the whole image with 15 (the last invocation)";
}
}
DestroyIntTarget(target);
}
// The same claim against the DEFAULT framebuffer, where MobileGL applies its Y-flip and
// pre-transform rotation. Index 0 alone getting the mapping is the classic bug.
TEST_F(ViewportArrayScenario, TheDefaultFramebufferAppliesTheSameFlipToEveryViewport) {
const int surfaceW = Gl().Width();
const int surfaceH = Gl().Height();
ASSERT_GE(surfaceW, kGridSide);
ASSERT_GE(surfaceH, kGridSide);
const int cellW = surfaceW / kGridSide;
const int cellH = surfaceH / kGridSide;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// Paint a value no viewport index can produce, so an unwritten cell is obvious.
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// The default framebuffer is 8-bit RGBA, so the index travels as a colour: cell i is
// painted with red = i * 16, which is exact in 8 bits for i in [0, 16).
const char* const kColorFragmentSource = R"(#version 410 core
flat in int gsIndex;
layout(location = 0) out vec4 fragColor;
void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
)";
const GLuint colorProgram = BuildProgram(kGridGeometrySource, kColorFragmentSource);
ASSERT_NE(colorProgram, 0u) << "colour program failed to build: " << m_buildLog;
SetupGridViewports(cellW, cellH);
glUseProgram(colorProgram);
glBindVertexArray(m_vao);
glDrawArrays(GL_POINTS, 0, 1);
ASSERT_EQ(glGetError(), GL_NO_ERROR);
std::vector<unsigned char> pixels(static_cast<size_t>(surfaceW) * surfaceH * 4, 0);
glReadPixels(0, 0, surfaceW, surfaceH, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
for (int y = 0; y < kGridSide; ++y) {
for (int x = 0; x < kGridSide; ++x) {
const int px = x * cellW + cellW / 2;
const int py = y * cellH + cellH / 2;
const int red = pixels[(static_cast<size_t>(py) * surfaceW + px) * 4];
const int expected = (y * kGridSide + x) * 16;
// One LSB of slack for an 8-bit round trip; the values are 16 apart, so this
// cannot confuse two neighbouring indices.
EXPECT_LE(std::abs(red - expected), 1)
<< "default-framebuffer cell (" << x << ", " << y << ") holds red=" << red << ", expected "
<< expected << ". A vertically mirrored grid means the Y-flip was applied to viewport 0 "
<< "only";
}
}
glDeleteProgram(colorProgram);
}
// --- 2. the depth range --------------------------------------------------------------
TEST_F(ViewportArrayScenario, EachViewportIndexUsesItsOwnDepthRange) {
// 16 columns one pixel wide and two rows tall: row 0 gets the near-plane quad, row 1
// the far-plane one, so both ends of viewport i's range land in the same column.
constexpr int kWidth = kViewportCount;
constexpr int kHeight = 2;
GLuint texture = 0;
GLuint fbo = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, kWidth, kHeight, 0, GL_RED, GL_FLOAT, nullptr);
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
const GLfloat clearValue[4] = {-1.0f, 0.0f, 0.0f, 0.0f};
glClearBufferfv(GL_COLOR, 0, clearValue);
std::vector<GLfloat> viewports(static_cast<size_t>(kViewportCount) * 4);
std::vector<GLdouble> ranges(static_cast<size_t>(kViewportCount) * 2);
for (int i = 0; i < kViewportCount; ++i) {
viewports[static_cast<size_t>(i) * 4 + 0] = static_cast<GLfloat>(i);
viewports[static_cast<size_t>(i) * 4 + 1] = 0.0f;
viewports[static_cast<size_t>(i) * 4 + 2] = 1.0f;
viewports[static_cast<size_t>(i) * 4 + 3] = 2.0f;
ranges[static_cast<size_t>(i) * 2 + 0] = static_cast<GLdouble>(i) / 16.0;
ranges[static_cast<size_t>(i) * 2 + 1] = 1.0 - static_cast<GLdouble>(i) / 16.0;
}
glViewportArrayv(0, kViewportCount, viewports.data());
glDepthRangeArrayv(0, kViewportCount, ranges.data());
const GLuint depthProgram = BuildProgram(kDepthGeometrySource, kDepthFragmentSource);
ASSERT_NE(depthProgram, 0u) << "depth program failed to build: " << m_buildLog;
glUseProgram(depthProgram);
glBindVertexArray(m_vao);
glDrawArrays(GL_POINTS, 0, 1);
ASSERT_EQ(glGetError(), GL_NO_ERROR);
std::vector<GLfloat> pixels(static_cast<size_t>(kWidth) * kHeight, 0.0f);
glReadPixels(0, 0, kWidth, kHeight, GL_RED, GL_FLOAT, pixels.data());
for (int i = 0; i < kViewportCount; ++i) {
const float nearDepth = static_cast<float>(i) / 16.0f;
const float farDepth = 1.0f - static_cast<float>(i) / 16.0f;
// The tolerance covers depth-buffer-free rasterization of gl_FragCoord.z on a
// software rasterizer; the per-index values are 1/16 apart, so it cannot let a
// neighbouring viewport's range through, and viewport 0's range (0, 1) differs
// from every other index by at least 1/16.
EXPECT_NEAR(pixels[i], nearDepth, 1.0e-3f)
<< "viewport " << i << " near-plane depth; got viewport 0's range if this is 0";
EXPECT_NEAR(pixels[static_cast<size_t>(kWidth) + i], farDepth, 1.0e-3f)
<< "viewport " << i << " far-plane depth; got viewport 0's range if this is 1";
}
glDeleteProgram(depthProgram);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &texture);
}
// --- 3. the per-index scissor-test enable --------------------------------------------
TEST_F(ViewportArrayScenario, AnIndexedScissorEnableClipsOnlyThatIndex) {
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
// One full-size viewport per index so the scissor rectangle is the ONLY thing that
// can shrink the quad - the same separation KHR-GL43.viewport_array.scissor uses.
glViewport(0, 0, kSurfaceSide, kSurfaceSide);
std::vector<GLint> boxes(static_cast<size_t>(kViewportCount) * 4);
for (int y = 0; y < kGridSide; ++y) {
for (int x = 0; x < kGridSide; ++x) {
const size_t base = static_cast<size_t>(y * kGridSide + x) * 4;
boxes[base + 0] = x * kCellSize;
boxes[base + 1] = y * kCellSize;
boxes[base + 2] = kCellSize;
boxes[base + 3] = kCellSize;
}
}
glScissorArrayv(0, kViewportCount, boxes.data());
const GLuint singleProgram = BuildProgram(kSingleGeometrySource, kIntFragmentSource);
ASSERT_NE(singleProgram, 0u) << "single-viewport program failed to build: " << m_buildLog;
glUseProgram(singleProgram);
glBindVertexArray(m_vao);
const GLint uViewport = glGetUniformLocation(singleProgram, "uViewport");
ASSERT_NE(uViewport, -1);
constexpr GLint kProbeIndex = 6; // grid cell (2, 1)
constexpr int kProbeX = kProbeIndex % kGridSide;
constexpr int kProbeY = kProbeIndex / kGridSide;
// (a) scissor test ENABLED for this index: the quad is clipped to its 32x32 box.
glUniform1i(uViewport, kProbeIndex);
glEnablei(GL_SCISSOR_TEST, kProbeIndex);
glDrawArrays(GL_POINTS, 0, 1);
ASSERT_EQ(glGetError(), GL_NO_ERROR);
{
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, kProbeX, kProbeY), kProbeIndex)
<< "the scissored index must still paint inside its own box";
for (int y = 0; y < kGridSide; ++y) {
for (int x = 0; x < kGridSide; ++x) {
if (x == kProbeX && y == kProbeY) continue;
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kUnwritten)
<< "cell (" << x << ", " << y << ") is outside scissor rectangle " << kProbeIndex
<< " and must be untouched";
}
}
}
// (b) scissor test DISABLED for the same index, everything else identical: with no
// per-viewport toggle in Vulkan this is the case that needs the disabled index to be
// given the full framebuffer rectangle, and it is exactly where "leave the last
// rectangle bound" would show up as a still-clipped quad.
FillIntTarget(target, kSurfaceSide, kSurfaceSide);
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
glDisablei(GL_SCISSOR_TEST, kProbeIndex);
glDrawArrays(GL_POINTS, 0, 1);
ASSERT_EQ(glGetError(), GL_NO_ERROR);
{
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
for (int y = 0; y < kGridSide; ++y) {
for (int x = 0; x < kGridSide; ++x) {
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kProbeIndex)
<< "with the scissor test off for index " << kProbeIndex
<< ", its full-viewport quad must cover cell (" << x << ", " << y << ")";
}
}
}
glDeleteProgram(singleProgram);
DestroyIntTarget(target);
}
} // namespace
} // namespace MGITest