[Test] (Magma): OversizedRecordingSplitScenario carries state across the split - a depth occluder, a stencil mark, an additive blend destination, a 4x multisample target resolved after it, occlusion and primitives-generated queries and a transform-feedback capture left open across it, each checked on both backends and in the budget-64 lane

This commit is contained in:
2026-09-29 09:44:58 -04:00
parent 4d3d677a7f
commit 9c32d9fa68
2 changed files with 630 additions and 75 deletions
+6 -4
View File
@@ -596,10 +596,12 @@ gtest_discover_tests(MobileGLIntegrationTest
)
# OversizedRecordingSplitScenario with the command-buffer draw budget PINNED to 64. The ambient
# registrations run its 1024-draw frame under the default budget, which never splits it; this
# entry splits it 15 times, so the pixels prove a split loses nothing, and the arming assertion
# (the renderer's latched MGLOG_I) proves the split happened at all - on a renderer that records
# the whole frame into one command buffer, it is the assertion that goes red. DirectVulkan only:
# registrations run its cases under the default budget, which never splits them; this entry
# splits every case at least once between the draws that leave state (depth, stencil, a blend
# destination, multisample samples, an open query, an active capture) and the draws that
# consume it, so the results prove a split carries all of it, and the arming assertion (the
# renderer's latched MGLOG_I) proves the split happened at all - on a renderer that records the
# whole frame into one command buffer, it is the assertion that goes red. DirectVulkan only:
# the budget steers nothing on DirectGLES, whose driver flushes on its own.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.SplitRecording."
@@ -16,23 +16,29 @@
// now submits the open buffer after MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER draws and goes
// on recording into a fresh one (VulkanRenderer::SplitOversizedRecording).
//
// Each draw here writes exactly one pixel of a 32x32 target, in its own colour, selected by a
// per-draw uniform, so every draw carries its own uniform state, like Minecraft's chunk draws.
// After 1024 draws every pixel must hold its own draw's colour. Each of these split bugs leaves
// a pixel wrong: a re-begun render pass that does not LOAD, a draw recorded into the retired
// buffer, or state that is not re-emitted on the fresh one.
// The first case writes each pixel of a 32x32 target with its own draw and its own uniform
// value, so it pins that no draw is lost and no per-draw state is left behind on either side of
// a split. The rest put GL state that one draw leaves for a later one ACROSS the split: a depth
// occluder, a stencil mark, a blend destination, the samples of a multisample target, an
// occlusion or primitives-generated query that is still open, and a transform-feedback capture
// that is still active. Each draws its "before" part, then kFillers draws, then its "after"
// part. kFillers is more than twice the SplitRecording lane's budget of 64, so at least one
// split lands between the two parts wherever the recording's count stood.
//
// The ambient registrations run the same draws unsplit, because the default budget is far above
// 1024; that is the control. The DirectVulkan.SplitRecording. entry pins the budget to 64, so
// the frame is split 15 times. There the case also asserts the renderer's latched MGLOG_I, the
// one assertion that is red on a renderer that never splits.
// any of these frames; that is the control. The DirectVulkan.SplitRecording. entry pins the
// budget to 64, and there every case also asserts the renderer's latched MGLOG_I - the one
// assertion that is red on a renderer that never splits.
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <sstream>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
@@ -50,6 +56,11 @@ namespace MGITest {
constexpr int kSide = 32;
constexpr int kDraws = kSide * kSide;
// More than twice the SplitRecording lane's budget (64): wherever the count stands when
// they start, at least one split lands among them.
constexpr int kFillers = 150;
// Draws inside the query and capture spans: also more than twice the budget.
constexpr int kSpanDraws = 200;
// aPos spans the unit square; the uniform moves it onto cell (i % 32, i / 32) of a
// 32x32 grid, which is exactly one pixel of a 32x32 target.
@@ -72,6 +83,37 @@ void main() {
}
)";
// The state cases: the unit-square vertices (or a triangle of them) stretched over an
// NDC rectangle at a given depth, in a uniform colour. A zero rectangle makes a
// zero-area primitive - a draw that reaches the renderer and rasterizes nothing.
constexpr const char* kRectVS = R"(#version 330 core
in vec2 aPos;
uniform vec4 uRect;
uniform float uDepth;
void main() { gl_Position = vec4(mix(uRect.xy, uRect.zw, aPos), uDepth, 1.0); }
)";
constexpr const char* kRectFS = R"(#version 330 core
uniform vec4 uColor;
out vec4 o_color;
void main() { o_color = uColor; }
)";
// The capture case: one point per draw, carrying 2 * uIndex + 1.
constexpr const char* kCaptureVS = R"(#version 330 core
uniform int uIndex;
out float vs_value;
void main() {
vs_value = float(uIndex) * 2.0 + 1.0;
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
constexpr const char* kCaptureFS = R"(#version 330 core
out vec4 o_color;
void main() { o_color = vec4(1.0); }
)";
constexpr float kCapturePoison = -12345.0f;
// The budget the process was launched with, or 0 when it is unset or not a number.
unsigned long PinnedDrawBudget() {
const char* value = std::getenv("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER");
@@ -106,6 +148,13 @@ void main() {
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
}
std::string Describe(const Rgba8& c) {
std::ostringstream os;
os << "rgba(" << static_cast<int>(c.r) << ", " << static_cast<int>(c.g) << ", " << static_cast<int>(c.b)
<< ", " << static_cast<int>(c.a) << ")";
return os.str();
}
::testing::AssertionResult EveryPixelHoldsItsOwnDraw(const Image& image) {
int wrong = 0;
int firstX = -1, firstY = -1;
@@ -133,87 +182,591 @@ void main() {
<< " (a pixel no draw reached still holds the clear colour 0,0,0,0)";
}
// Every pixel of columns [x0, x1] (all rows) is `expected`, each channel within `tolerance`.
::testing::AssertionResult ColumnsAre(const Image& image, int x0, int x1, const Rgba8& expected,
int tolerance, const char* what) {
int wrong = 0;
int firstX = -1, firstY = -1;
Rgba8 firstSeen{};
for (int y = 0; y < kSide; ++y) {
for (int x = x0; x <= x1; ++x) {
const Rgba8 seen = image.At(x, y);
const bool close = std::abs(seen.r - expected.r) <= tolerance &&
std::abs(seen.g - expected.g) <= tolerance &&
std::abs(seen.b - expected.b) <= tolerance &&
std::abs(seen.a - expected.a) <= tolerance;
if (!close) {
if (wrong == 0) {
firstX = x;
firstY = y;
firstSeen = seen;
}
++wrong;
}
}
}
if (wrong == 0) return ::testing::AssertionSuccess();
return ::testing::AssertionFailure()
<< what << ": " << wrong << " pixels of columns " << x0 << ".." << x1 << " are not "
<< Describe(expected) << "; first (" << firstX << ", " << firstY << ") = " << Describe(firstSeen);
}
class OversizedRecordingSplitScenario : public ScenarioTest {
protected:
// kDraws single-pixel quads into a fresh target, one uniform value per draw, with no
// readback, flush or rebind in between, so the renderer sees one uninterrupted
// recording. Then the whole target is read back.
Image DrawEveryPixelWithItsOwnDraw(std::string* error) {
const unsigned int program = CompileProgram(kVS, kFS, error);
if (program == 0) return Image();
ColorFbo target = MakeColorFbo(kSide, kSide);
if (target.fbo == 0) {
*error = "MakeColorFbo failed";
glDeleteProgram(program);
return Image();
}
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
// Taken before the case draws anything, so the line the arming check looks for
// can only have been written by this case. The renderer latches it at its first
// split, and ctest gives every case a process of its own.
m_logBefore = LibraryLogSize();
static const float kUnitQuad[] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f};
GLuint vao = 0, vbo = 0;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(kUnitQuad), kUnitQuad, GL_STATIC_DRAW);
// 0..3: the unit square as a strip. 4..6: its lower-left triangle. 7..9: its
// upper-right triangle - the two share the diagonal, so together they cover every
// sample of the square exactly once.
static const float kVertices[] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(kVertices), kVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
BindFbo(target);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
glUseProgram(program);
const GLint indexLocation = glGetUniformLocation(program, "uIndex");
for (int i = 0; i < kDraws; ++i) {
glUniform1i(indexLocation, i);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
Image image = ReadPixels(kSide, kSide);
glUseProgram(0);
glBindVertexArray(0);
glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);
BindDefaultFramebuffer();
DestroyColorFbo(target);
glDeleteProgram(program);
return image;
}
void TearDown() override {
if (Ready()) {
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glDisable(GL_BLEND);
glDisable(GL_RASTERIZER_DISCARD);
glDepthMask(GL_TRUE);
glStencilMask(0xFF);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glUseProgram(0);
glBindVertexArray(0);
BindDefaultFramebuffer();
if (m_rectProgram != 0) glDeleteProgram(m_rectProgram);
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
for (GLuint rbo : m_renderbuffers) glDeleteRenderbuffers(1, &rbo);
for (GLuint fbo : m_framebuffers) glDeleteFramebuffers(1, &fbo);
for (ColorFbo& target : m_colorTargets) DestroyColorFbo(target);
}
ScenarioTest::TearDown();
}
// In the SplitRecording lane (budget pinned below what every case draws, DirectVulkan,
// a log to read), a split must have happened during this case. Elsewhere this case is
// the unsplit control and there is nothing to arm.
void ExpectTheRecordingWasSplitWhereTheBudgetIsPinned() {
const unsigned long budget = PinnedDrawBudget();
const bool splitExpected = budget > 0 && budget * 2 < static_cast<unsigned long>(kFillers) &&
Gl().BackendName() == std::string("DirectVulkan") &&
!LibraryLogPath().empty();
RecordProperty("arming_checked", splitExpected ? "yes" : "no");
if (!splitExpected) return;
const std::string appended = LibraryLogSince(m_logBefore);
EXPECT_NE(appended.find("draws into one command buffer; submitting it"), std::string::npos)
<< "MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER=" << budget << " and this case recorded more "
<< "than " << kFillers << " draws in one frame, but DirectVulkan never reported splitting the "
"recording. A renderer that does not split grows a single command buffer without bound "
"(rd12 on Adreno: ~40k driver mappings, then SIGABRT). See "
"VulkanRenderer::SplitOversizedRecording. Log appended by this case:\n"
<< appended;
}
bool BuildRectProgram(std::string* error) {
m_rectProgram = CompileProgram(kRectVS, kRectFS, error);
if (m_rectProgram == 0) return false;
m_rectLoc = glGetUniformLocation(m_rectProgram, "uRect");
m_depthLoc = glGetUniformLocation(m_rectProgram, "uDepth");
m_colorLoc = glGetUniformLocation(m_rectProgram, "uColor");
glUseProgram(m_rectProgram);
glBindVertexArray(m_vao);
return true;
}
// The rectangle [x0, x1] x [y0, y1] in NDC; `first`/`count` pick the strip or a triangle.
void DrawRect(float x0, float y0, float x1, float y1, float depth, float r, float g, float b, float a,
GLenum mode = GL_TRIANGLE_STRIP, GLint first = 0, GLsizei count = 4) {
glUniform4f(m_rectLoc, x0, y0, x1, y1);
glUniform1f(m_depthLoc, depth);
glUniform4f(m_colorLoc, r, g, b, a);
glDrawArrays(mode, first, count);
}
// One pixel of the 32x32 grid, by index.
void DrawPixel(int index, float r, float g, float b, float a) {
const float x = static_cast<float>(index % kSide);
const float y = static_cast<float>(index / kSide);
DrawRect(x / 16.0f - 1.0f, y / 16.0f - 1.0f, (x + 1.0f) / 16.0f - 1.0f, (y + 1.0f) / 16.0f - 1.0f, 0.0f,
r, g, b, a);
}
// Draws that reach the renderer and rasterize nothing: zero-area strips.
void DrawFillers(int count) {
for (int i = 0; i < count; ++i) {
DrawRect(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
}
}
// A kSide x kSide RGBA8 target, optionally with a DEPTH24_STENCIL8 renderbuffer.
// Returns fbo 0 when the driver will not complete it.
ColorFbo MakeTarget(bool withDepthStencil) {
ColorFbo target = MakeColorFbo(kSide, kSide);
if (target.fbo == 0) return target;
m_colorTargets.push_back(target);
if (withDepthStencil) {
GLuint rbo = 0;
glGenRenderbuffers(1, &rbo);
m_renderbuffers.push_back(rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, kSide, kSide);
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
target.fbo = 0;
}
}
return target;
}
Image ReadTarget(const ColorFbo& target) {
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
return ReadPixels(kSide, kSide);
}
std::uintmax_t m_logBefore = 0;
GLuint m_vao = 0;
GLuint m_vbo = 0;
GLuint m_rectProgram = 0;
GLint m_rectLoc = -1;
GLint m_depthLoc = -1;
GLint m_colorLoc = -1;
std::vector<ColorFbo> m_colorTargets;
std::vector<GLuint> m_renderbuffers;
std::vector<GLuint> m_framebuffers;
};
} // namespace
// kDraws single-pixel quads into a fresh target, one uniform value per draw, with no
// readback, flush or rebind in between. Every pixel must hold its own draw's colour.
TEST_F(OversizedRecordingSplitScenario, EveryDrawOfTheFrameLandsAcrossCommandBufferSplits) {
if (!Ready()) return;
// Taken before the draws, so the line the arming check looks for can only have been
// written by them. The renderer latches it at its first split.
const std::uintmax_t logBefore = LibraryLogSize();
std::string error;
const Image image = DrawEveryPixelWithItsOwnDraw(&error);
ASSERT_FALSE(image.Empty()) << error;
const unsigned int program = CompileProgram(kVS, kFS, &error);
ASSERT_NE(program, 0u) << error;
ColorFbo target = MakeTarget(false);
ASSERT_NE(target.fbo, 0u) << "MakeColorFbo failed";
BindFbo(target);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
glUseProgram(program);
glBindVertexArray(m_vao);
const GLint indexLocation = glGetUniformLocation(program, "uIndex");
for (int i = 0; i < kDraws; ++i) {
glUniform1i(indexLocation, i);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
const Image image = ReadPixels(kSide, kSide);
glUseProgram(0);
glDeleteProgram(program);
EXPECT_TRUE(EveryPixelHoldsItsOwnDraw(image));
EXPECT_EQ(FirstGLError(), 0u);
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
}
// Arming: only where the budget is pinned below this frame's draw count, on the backend
// that has the budget, and with a log to read. Everywhere else this case is the unsplit
// control and stops here.
const unsigned long budget = PinnedDrawBudget();
const bool splitExpected = budget > 0 && budget < static_cast<unsigned long>(kDraws) &&
Gl().BackendName() == std::string("DirectVulkan") && !LibraryLogPath().empty();
RecordProperty("arming_checked", splitExpected ? "yes" : "no");
if (!splitExpected) {
return;
// (a) An occluder drawn before the split must still fail the depth test of a draw after it:
// the depth attachment's contents have to survive the render pass ending and re-beginning.
TEST_F(OversizedRecordingSplitScenario, ADepthOccluderDrawnBeforeTheSplitHidesADrawAfterIt) {
if (!Ready()) return;
std::string error;
ASSERT_TRUE(BuildRectProgram(&error)) << error;
ColorFbo target = MakeTarget(true);
if (target.fbo == 0) GTEST_SKIP() << "no complete RGBA8 + DEPTH24_STENCIL8 target on this driver";
BindFbo(target);
glClearDepth(1.0);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glDepthMask(GL_TRUE);
DrawRect(-1.0f, -1.0f, 0.0f, 1.0f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f); // left half, near, red
DrawFillers(kFillers);
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f); // everything, far, green
const Image image = ReadTarget(target);
EXPECT_TRUE(ColumnsAre(image, 0, kSide / 2 - 1, Rgba8{255, 0, 0, 255}, 0,
"the near occluder drawn before the split must hide the far draw after it"));
EXPECT_TRUE(ColumnsAre(image, kSide / 2, kSide - 1, Rgba8{0, 255, 0, 255}, 0,
"where nothing occludes, the far draw after the split must land"));
EXPECT_EQ(FirstGLError(), 0u);
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
}
// (b) A stencil mark made before the split must gate a draw after it.
TEST_F(OversizedRecordingSplitScenario, AStencilMarkMadeBeforeTheSplitGatesADrawAfterIt) {
if (!Ready()) return;
std::string error;
ASSERT_TRUE(BuildRectProgram(&error)) << error;
ColorFbo target = MakeTarget(true);
if (target.fbo == 0) GTEST_SKIP() << "no complete RGBA8 + DEPTH24_STENCIL8 target on this driver";
BindFbo(target);
glDisable(GL_DEPTH_TEST);
glStencilMask(0xFF);
glClearStencil(0);
glClear(GL_STENCIL_BUFFER_BIT);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
DrawRect(-1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f); // left half: stencil 1, blue
glStencilFunc(GL_EQUAL, 1, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
DrawFillers(kFillers);
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f); // everything, green, where stencil == 1
const Image image = ReadTarget(target);
EXPECT_TRUE(ColumnsAre(image, 0, kSide / 2 - 1, Rgba8{0, 255, 0, 255}, 0,
"where the mark was made before the split, the draw after it must pass"));
EXPECT_TRUE(ColumnsAre(image, kSide / 2, kSide - 1, Rgba8{0, 0, 0, 255}, 0,
"where no mark was made, the draw after the split must be stencilled out"));
EXPECT_EQ(FirstGLError(), 0u);
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
}
// (c) Additive blending onto pixels written before the split: every one of kSpanDraws
// full-target draws adds exactly 1/255 of red onto a target whose green was written first,
// so the result counts every draw on both sides of every split.
TEST_F(OversizedRecordingSplitScenario, BlendingAccumulatesOntoPixelsWrittenBeforeTheSplit) {
if (!Ready()) return;
std::string error;
ASSERT_TRUE(BuildRectProgram(&error)) << error;
ColorFbo target = MakeTarget(false);
ASSERT_NE(target.fbo, 0u) << "MakeColorFbo failed";
BindFbo(target);
glDisable(GL_DEPTH_TEST);
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ONE, GL_ONE);
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 55.0f / 255.0f, 0.0f, 0.0f);
for (int i = 0; i < kSpanDraws; ++i) {
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 1.0f / 255.0f, 0.0f, 0.0f, 0.0f);
}
const std::string appended = LibraryLogSince(logBefore);
EXPECT_NE(appended.find("draws into one command buffer; submitting it"), std::string::npos)
<< "MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER=" << budget << " and one frame recorded " << kDraws
<< " draws, but DirectVulkan never reported splitting the recording. A renderer that does not "
"split grows a single command buffer without bound (rd12 on Adreno: ~40k driver mappings, "
"then SIGABRT). See VulkanRenderer::SplitOversizedRecording. Log appended by this test:\n"
<< appended;
const Image image = ReadTarget(target);
EXPECT_TRUE(ColumnsAre(image, 0, kSide - 1, Rgba8{static_cast<std::uint8_t>(kSpanDraws), 55, 0, 0}, 1,
"every additive draw, and the green written before any split, must be in the sum"));
EXPECT_EQ(FirstGLError(), 0u);
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
}
// (d) A multisample target drawn on both sides of the split, then resolved. Before the
// split a triangle covers the lower-left half of the target; after it, the complementary
// triangle covers the upper-right half. On the diagonal each pixel's samples are shared
// between the two, so only a target that kept every SAMPLE across the split resolves them to
// red + green = 255. One that was resolved or re-broadcast at the split gives about 190
// there, and one whose samples were dropped gives black or garbage.
TEST_F(OversizedRecordingSplitScenario, AMultisampleTargetKeepsItsSamplesAcrossTheSplit) {
if (!Ready()) return;
std::string error;
ASSERT_TRUE(BuildRectProgram(&error)) << error;
GLint maxSamples = 0;
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
if (maxSamples < 2) {
GTEST_SKIP() << "GL_MAX_SAMPLES is " << maxSamples << "; this needs a multisample renderbuffer";
}
const GLsizei samples = std::min<GLint>(4, maxSamples);
GLuint msFbo = 0, msRbo = 0;
glGenFramebuffers(1, &msFbo);
m_framebuffers.push_back(msFbo);
glGenRenderbuffers(1, &msRbo);
m_renderbuffers.push_back(msRbo);
glBindRenderbuffer(GL_RENDERBUFFER, msRbo);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, kSide, kSide);
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msRbo);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
GTEST_SKIP() << "no complete " << samples << "x multisample RGBA8 renderbuffer on this driver";
}
ColorFbo resolved = MakeTarget(false);
ASSERT_NE(resolved.fbo, 0u) << "MakeColorFbo failed";
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
glViewport(0, 0, kSide, kSide);
glDisable(GL_DEPTH_TEST);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, GL_TRIANGLES, 4, 3); // lower-left, red
DrawFillers(kFillers);
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, GL_TRIANGLES, 7, 3); // upper-right, green
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolved.fbo);
glBlitFramebuffer(0, 0, kSide, kSide, 0, 0, kSide, kSide, GL_COLOR_BUFFER_BIT, GL_NEAREST);
const Image image = ReadTarget(resolved);
int wrong = 0;
std::string first;
for (int y = 0; y < kSide && wrong < 1000; ++y) {
for (int x = 0; x < kSide; ++x) {
const Rgba8 seen = image.At(x, y);
bool ok = false;
// The diagonal x + y = kSide in pixel units passes through the pixels whose
// x + y == kSide - 1; every other pixel lies wholly on one side of it.
if (x + y < kSide - 1) {
ok = seen == Rgba8{255, 0, 0, 255};
} else if (x + y > kSide - 1) {
ok = seen == Rgba8{0, 255, 0, 255};
} else {
ok = seen.b == 0 && seen.a == 255 && seen.r > 0 && seen.g > 0 &&
std::abs(static_cast<int>(seen.r) + static_cast<int>(seen.g) - 255) <= 3;
}
if (!ok) {
if (wrong == 0) {
std::ostringstream os;
os << "(" << x << ", " << y << ") = " << Describe(seen);
first = os.str();
}
++wrong;
}
}
}
EXPECT_EQ(wrong, 0) << wrong << " resolved pixels are wrong; first " << first
<< ". Lower-left of the diagonal must be red, upper-right green, and a pixel on it "
"red + green = 255 with both present (its samples were split between a draw before "
"the command-buffer split and one after it)";
EXPECT_EQ(FirstGLError(), 0u);
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
}
// (e) Occlusion queries open across the split. GL_SAMPLES_PASSED counts kSpanDraws
// single-pixel draws on both sides of it; GL_ANY_SAMPLES_PASSED is answered by the one draw
// made before the split, the fillers after it pass nothing - with a fillers-only control
// that must answer false.
TEST_F(OversizedRecordingSplitScenario, OcclusionQueriesOpenAcrossTheSplitCountBothSides) {
if (!Ready()) return;
std::string error;
ASSERT_TRUE(BuildRectProgram(&error)) << error;
ColorFbo target = MakeTarget(false);
ASSERT_NE(target.fbo, 0u) << "MakeColorFbo failed";
BindFbo(target);
glDisable(GL_DEPTH_TEST);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
GLuint queries[3] = {};
glGenQueries(3, queries);
FirstGLError();
// Any samples: control first (fillers only), then one real pixel before the split.
glBeginQuery(GL_ANY_SAMPLES_PASSED, queries[0]);
const unsigned int anyBeginError = FirstGLError();
if (anyBeginError != 0u) {
glDeleteQueries(3, queries);
GTEST_SKIP() << Gl().BackendName() << " does not accept GL_ANY_SAMPLES_PASSED ("
<< GLErrorName(anyBeginError) << ")";
}
DrawFillers(kFillers);
glEndQuery(GL_ANY_SAMPLES_PASSED);
glBeginQuery(GL_ANY_SAMPLES_PASSED, queries[1]);
DrawPixel(0, 1.0f, 1.0f, 1.0f, 1.0f);
DrawFillers(kFillers);
glEndQuery(GL_ANY_SAMPLES_PASSED);
GLuint anyControl = 7, anyAcross = 7;
glGetQueryObjectuiv(queries[0], GL_QUERY_RESULT, &anyControl);
glGetQueryObjectuiv(queries[1], GL_QUERY_RESULT, &anyAcross);
EXPECT_EQ(anyControl, static_cast<GLuint>(GL_FALSE)) << "fillers alone rasterize nothing";
EXPECT_EQ(anyAcross, static_cast<GLuint>(GL_TRUE))
<< "the only passing draw was made before the split; the query that spans it lost that answer";
// Samples passed: kSpanDraws distinct single-sample pixels, spanning at least one split.
glBeginQuery(GL_SAMPLES_PASSED, queries[2]);
const unsigned int samplesBeginError = FirstGLError();
if (samplesBeginError == 0u) {
for (int i = 0; i < kSpanDraws; ++i) {
DrawPixel(i, 1.0f, 1.0f, 1.0f, 1.0f);
}
glEndQuery(GL_SAMPLES_PASSED);
GLuint samplesPassed = 0;
glGetQueryObjectuiv(queries[2], GL_QUERY_RESULT, &samplesPassed);
if (Gl().BackendName() == std::string("DirectGLES")) {
// OpenGL ES has no sample-counting occlusion query, only ANY_SAMPLES_PASSED*, so
// DirectGLES answers GL_SAMPLES_PASSED with a 0/1 approximation (DirectGLES.cpp,
// the occlusion-query result path). The exact count is not this backend's to give.
RecordProperty("samples_passed", "0/1 approximation on OpenGL ES; exact count not checked");
EXPECT_EQ(samplesPassed, 1u) << "samples passed, so the approximation must read 1";
} else {
EXPECT_EQ(samplesPassed, static_cast<GLuint>(kSpanDraws))
<< "each of the " << kSpanDraws << " draws covers exactly one single-sample pixel";
}
} else {
RecordProperty("samples_passed", std::string("not accepted: ") + GLErrorName(samplesBeginError));
}
glDeleteQueries(3, queries);
EXPECT_EQ(FirstGLError(), 0u);
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
}
// (e) GL_PRIMITIVES_GENERATED open across the split: kSpanDraws strips of two triangles.
// A one-draw query first establishes that this stack counts at all.
TEST_F(OversizedRecordingSplitScenario, APrimitivesGeneratedQueryOpenAcrossTheSplitCountsBothSides) {
if (!Ready()) return;
std::string error;
ASSERT_TRUE(BuildRectProgram(&error)) << error;
ColorFbo target = MakeTarget(false);
ASSERT_NE(target.fbo, 0u) << "MakeColorFbo failed";
BindFbo(target);
glDisable(GL_DEPTH_TEST);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
GLuint queries[2] = {};
glGenQueries(2, queries);
FirstGLError();
glBeginQuery(GL_PRIMITIVES_GENERATED, queries[0]);
const unsigned int beginError = FirstGLError();
if (beginError != 0u) {
glDeleteQueries(2, queries);
GTEST_SKIP() << Gl().BackendName() << " does not accept GL_PRIMITIVES_GENERATED ("
<< GLErrorName(beginError) << ")";
}
DrawPixel(0, 1.0f, 1.0f, 1.0f, 1.0f);
glEndQuery(GL_PRIMITIVES_GENERATED);
GLuint control = 0;
glGetQueryObjectuiv(queries[0], GL_QUERY_RESULT, &control);
if (control == 0u) {
glDeleteQueries(2, queries);
GTEST_SKIP() << "GL_PRIMITIVES_GENERATED reads 0 for a single unsplit strip on " << Gl().BackendName()
<< " (" << Gl().RendererString() << "): nothing this stack counts, so nothing to carry";
}
ASSERT_EQ(control, 2u) << "one strip of four vertices is two triangles";
glBeginQuery(GL_PRIMITIVES_GENERATED, queries[1]);
for (int i = 0; i < kSpanDraws; ++i) {
DrawPixel(i, 1.0f, 1.0f, 1.0f, 1.0f);
}
glEndQuery(GL_PRIMITIVES_GENERATED);
GLuint generated = 0;
glGetQueryObjectuiv(queries[1], GL_QUERY_RESULT, &generated);
EXPECT_EQ(generated, static_cast<GLuint>(2 * kSpanDraws))
<< kSpanDraws << " strips of two triangles each, drawn across at least one split";
glDeleteQueries(2, queries);
EXPECT_EQ(FirstGLError(), 0u);
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
}
// (f) Transform feedback active across the split: kSpanDraws one-point draws, draw i
// capturing 2i + 1, must append in order into one buffer with nothing past the end, and
// GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN must count every one.
TEST_F(OversizedRecordingSplitScenario, ATransformFeedbackCaptureActiveAcrossTheSplitAppendsEveryDraw) {
if (!Ready()) return;
auto compile = [](GLenum stage, const char* source, std::string* log) -> GLuint {
const GLuint shader = glCreateShader(stage);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint ok = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
if (!ok) {
char buffer[2048] = {};
glGetShaderInfoLog(shader, sizeof(buffer) - 1, nullptr, buffer);
*log = buffer;
glDeleteShader(shader);
return 0;
}
return shader;
};
std::string log;
const GLuint vs = compile(GL_VERTEX_SHADER, kCaptureVS, &log);
ASSERT_NE(vs, 0u) << log;
const GLuint fs = compile(GL_FRAGMENT_SHADER, kCaptureFS, &log);
ASSERT_NE(fs, 0u) << log;
const GLuint program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
const char* varyings[] = {"vs_value"};
glTransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
glLinkProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (!linked) {
char buffer[2048] = {};
glGetProgramInfoLog(program, sizeof(buffer) - 1, nullptr, buffer);
glDeleteProgram(program);
FAIL() << "capture program failed to link: " << buffer;
}
constexpr int kSlack = 4;
std::vector<float> poison(kSpanDraws + kSlack, kCapturePoison);
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, buffer);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(poison.size() * sizeof(float)),
poison.data(), GL_STATIC_COPY);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer);
GLuint written = 0;
glGenQueries(1, &written);
glUseProgram(program);
glBindVertexArray(m_vao);
const GLint indexLocation = glGetUniformLocation(program, "uIndex");
glEnable(GL_RASTERIZER_DISCARD);
glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, written);
glBeginTransformFeedback(GL_POINTS);
for (int i = 0; i < kSpanDraws; ++i) {
glUniform1i(indexLocation, i);
glDrawArrays(GL_POINTS, 0, 1);
}
glEndTransformFeedback();
glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
glDisable(GL_RASTERIZER_DISCARD);
glUseProgram(0);
GLuint primitivesWritten = 0;
glGetQueryObjectuiv(written, GL_QUERY_RESULT, &primitivesWritten);
std::vector<float> captured(poison.size(), 0.0f);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, buffer);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast<GLsizeiptr>(captured.size() * sizeof(float)),
captured.data());
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
glDeleteQueries(1, &written);
glDeleteBuffers(1, &buffer);
glDeleteProgram(program);
EXPECT_EQ(primitivesWritten, static_cast<GLuint>(kSpanDraws));
int wrong = 0;
std::string first;
for (int i = 0; i < static_cast<int>(captured.size()); ++i) {
const float expected = i < kSpanDraws ? static_cast<float>(2 * i + 1) : kCapturePoison;
if (captured[i] != expected) {
if (wrong == 0) {
std::ostringstream os;
os << "record " << i << " = " << captured[i] << ", expected " << expected;
first = os.str();
}
++wrong;
}
}
EXPECT_EQ(wrong, 0) << wrong << " of " << captured.size() << " records are wrong; first " << first
<< " (records past " << kSpanDraws << " must keep the poison)";
EXPECT_EQ(FirstGLError(), 0u);
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
}
} // namespace MGITest