mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
525 lines
26 KiB
C++
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
|