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

508 lines
26 KiB
C++

// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PartialUploadOverGpuContentScenario.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 - A SMALL glTexSubImage INTO A LEVEL THE GPU HAS WRITTEN.
//
// Espryt keeps a CPU shadow of every level and syncs a pending client write by uploading the
// region it covers (SyncMipmapsToBackend's sub-rect path). The shadow never sees a GPU write, so
// only the region may go up: anything more puts the shadow's stale texels over what the GPU drew.
// The region upload used to be skipped whenever the shadow's bytes had to be CONVERTED on the way
// to the driver - a three-channel format the driver cannot render is stored four-channel and every
// upload is repacked - and whenever the ES upload is shaped differently from the shadow (a 1D
// array, stored as a 2D array one texel high): those levels went up WHOLE, and a 2x2 write erased
// the rest of the level.
//
// Every case uploads a level, lets the GPU clear it, writes one 2x2 box, and reads the level back:
// the clear outside the box, the write inside. The formats are picked by what they cost the ES
// upload, which is per driver - on Mesa llvmpipe GL_SRGB8, GL_RGB8_SNORM and GL_RGB8UI are stored
// four-channel (see ThreeChannelAttachmentScenario) while GL_RGB16F is native; ES has no renderable
// three-channel format at all, so on a phone all four are converted. GL_RGBA8 is the control that
// never converts. A format a backend cannot attach is skipped, not failed.
//
// The second half is about ORDER, with GL_RGBA8. Magma queues a clear until the texture's next
// use and records texel uploads into a batch submitted ahead of the frame's recorded commands, so
// a write issued after a clear of (or a draw into) the level reached the image FIRST and the
// earlier clear or draw landed over it: the box read back green. Each order is covered - the
// clear already recorded, still queued, synced by a draw or by the readback, the reverse order
// in which the clear must win, and a draw instead of a clear.
#include <cmath>
#include <cstdint>
#include <cstring>
#include <sstream>
#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 kEdge = 8;
constexpr int kBoxAt = 2;
constexpr int kBoxEdge = 2;
constexpr const char* kVS = R"(#version 430 core
void main() {
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
}
)";
// How a value in [0, 1] is spelled in the client data, and how the level is cleared and
// read back. Integer levels spell 1 as kIntegerOne.
enum class Encoding { Unorm8, Snorm8, Float32, Uint8 };
constexpr unsigned kIntegerOne = 9;
struct Format {
GLenum internalFormat;
GLenum clientFormat;
GLenum clientType;
int components;
Encoding encoding;
};
std::vector<std::uint8_t> Fill(const Format& f, int texels, float r, float g, float b) {
const float rgba[4] = {r, g, b, 1.0f};
const std::size_t componentBytes = f.encoding == Encoding::Float32 ? 4u : 1u;
std::vector<std::uint8_t> out(static_cast<std::size_t>(texels) * f.components * componentBytes);
for (int t = 0; t < texels; ++t) {
for (int c = 0; c < f.components; ++c) {
std::uint8_t* dst = out.data() + (static_cast<std::size_t>(t) * f.components + c) * componentBytes;
const float v = rgba[c];
switch (f.encoding) {
case Encoding::Unorm8: *dst = static_cast<std::uint8_t>(std::lround(v * 255.0f)); break;
case Encoding::Snorm8:
*dst = static_cast<std::uint8_t>(static_cast<std::int8_t>(std::lround(v * 127.0f)));
break;
case Encoding::Float32: std::memcpy(dst, &v, sizeof(v)); break;
case Encoding::Uint8: *dst = static_cast<std::uint8_t>(std::lround(v * kIntegerOne)); break;
}
}
}
return out;
}
class PartialUploadOverGpuContentScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_scratch = MakeColorFbo(4, 4);
glGenVertexArrays(1, &m_vao);
glGenFramebuffers(1, &m_fbo);
FirstGLError();
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (GLuint p : m_programs) glDeleteProgram(p);
if (!m_textures.empty()) glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
if (m_vao) glDeleteVertexArrays(1, &m_vao);
if (m_fbo) glDeleteFramebuffers(1, &m_fbo);
BindDefaultFramebuffer();
DestroyColorFbo(m_scratch);
glViewport(0, 0, Gl().Width(), Gl().Height());
FirstGLError();
}
GLuint NewTexture(GLenum target) {
GLuint t = 0;
glGenTextures(1, &t);
m_textures.push_back(t);
glBindTexture(target, t);
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, 0);
glBindTexture(target, 0);
return t;
}
// A draw that samples the texture, which is what syncs its pending writes to the
// driver (an FBO attach alone may find the framebuffer already synced).
void SyncBySampling(GLenum target, GLuint tex, const char* samplerType, const char* coord) {
std::string fs = std::string("#version 430 core\nuniform ") + samplerType +
" u_tex;\nout vec4 o_color;\nvoid main() { o_color = vec4(texelFetch(u_tex, " + coord +
", 0)); }\n";
std::string error;
const GLuint program = CompileProgram(kVS, fs.c_str(), &error);
ASSERT_NE(program, 0u) << error;
m_programs.push_back(program);
BindFbo(m_scratch);
glUseProgram(program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(target, tex);
glUniform1i(glGetUniformLocation(program, "u_tex"), 0);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glBindTexture(target, 0);
glUseProgram(0);
BindDefaultFramebuffer();
glFinish();
EXPECT_EQ(FirstGLError(), 0u) << "sampling sync";
}
// Attaches (tex, layer) to the scratch framebuffer; false when this backend cannot.
bool Attach(GLuint tex, int layer) {
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
if (layer >= 0) {
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex, 0, layer);
} else {
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
}
return glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
}
void Detach() {
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
BindDefaultFramebuffer();
}
// The GPU write the shadow never sees: the whole level (or layer) to green.
void GpuClearGreen(const Format& f) {
if (f.encoding == Encoding::Uint8) {
const GLuint green[4] = {0u, kIntegerOne, 0u, 1u};
glClearBufferuiv(GL_COLOR, 0, green);
} else {
const GLfloat green[4] = {0.0f, 1.0f, 0.0f, 1.0f};
glClearBufferfv(GL_COLOR, 0, green);
}
}
// The attached image's rgb, in units of "one", width x height texels.
std::vector<float> ReadRgb(const Format& f, int width, int height) {
std::vector<float> rgb(static_cast<std::size_t>(width * height) * 3u);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
if (f.encoding == Encoding::Uint8) {
std::vector<GLuint> texels(static_cast<std::size_t>(width * height) * 4u, 0xA5A5A5A5u);
glReadPixels(0, 0, width, height, GL_RGBA_INTEGER, GL_UNSIGNED_INT, texels.data());
for (int i = 0; i < width * height; ++i)
for (int c = 0; c < 3; ++c)
rgb[i * 3 + c] = static_cast<float>(texels[i * 4 + c]) / kIntegerOne;
} else {
std::vector<GLfloat> texels(static_cast<std::size_t>(width * height) * 4u, -7.0f);
glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, texels.data());
for (int i = 0; i < width * height; ++i)
for (int c = 0; c < 3; ++c) rgb[i * 3 + c] = texels[i * 4 + c];
}
EXPECT_EQ(FirstGLError(), 0u) << "readback";
return rgb;
}
// Green everywhere but blue inside [boxX, boxX + boxW) x [boxY, boxY + boxH).
static void ExpectClearAroundBox(const std::vector<float>& rgb, int width, int height, int boxX, int boxY,
int boxW, int boxH) {
int bad = 0;
std::ostringstream first;
for (int y = 0; y < height; ++y)
for (int x = 0; x < width; ++x) {
const bool inBox = x >= boxX && x < boxX + boxW && y >= boxY && y < boxY + boxH;
const float want[3] = {0.0f, inBox ? 0.0f : 1.0f, inBox ? 1.0f : 0.0f};
const float* got = &rgb[static_cast<std::size_t>(y * width + x) * 3u];
bool texelBad = false;
for (int c = 0; c < 3; ++c) texelBad |= std::fabs(got[c] - want[c]) > 0.02f;
if (texelBad && bad++ == 0) {
first << "(" << x << "," << y << ") got " << got[0] << "," << got[1] << "," << got[2]
<< " want " << want[0] << "," << want[1] << "," << want[2];
}
}
EXPECT_EQ(bad, 0) << bad << " of " << width * height << " texels wrong; first " << first.str()
<< " (red outside the box: the stale shadow went back up; green inside it: the "
"write landed before the clear or draw)";
}
// Upload red, sync, clear green on the GPU, sync, write a blue 2x2 box, sync, read.
void Run2D(const Format& f) {
const GLuint t = NewTexture(GL_TEXTURE_2D);
const bool integer = f.encoding == Encoding::Uint8;
const char* sampler = integer ? "usampler2D" : "sampler2D";
const auto red = Fill(f, kEdge * kEdge, 1.0f, 0.0f, 0.0f);
glBindTexture(GL_TEXTURE_2D, t);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(f.internalFormat), kEdge, kEdge, 0, f.clientFormat,
f.clientType, red.data());
glBindTexture(GL_TEXTURE_2D, 0);
ASSERT_EQ(FirstGLError(), 0u) << "definition";
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
if (!Attach(t, -1)) {
Detach();
GTEST_SKIP() << "the format does not attach on backend " << Gl().BackendName();
}
GpuClearGreen(f);
Detach();
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
const auto blue = Fill(f, kBoxEdge * kBoxEdge, 0.0f, 0.0f, 1.0f);
glBindTexture(GL_TEXTURE_2D, t);
glTexSubImage2D(GL_TEXTURE_2D, 0, kBoxAt, kBoxAt, kBoxEdge, kBoxEdge, f.clientFormat, f.clientType,
blue.data());
glBindTexture(GL_TEXTURE_2D, 0);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
ASSERT_TRUE(Attach(t, -1));
const auto rgb = ReadRgb(f, kEdge, kEdge);
Detach();
ExpectClearAroundBox(rgb, kEdge, kEdge, kBoxAt, kBoxAt, kBoxEdge, kBoxEdge);
}
// P11 M: TWO SCATTERED WRITES INTO ONE LEVEL, so the level carries a rect list (their
// area is well under 3/4 of their union box) and not just a box. Under a transport
// (and on the push builds' monolith arm) the list crosses in the client's record,
// whose regions state their source pitch and offset in the CLIENT's layout; a level
// whose upload is converted is cut from the conversion's wider buffer, so those
// numbers must not address it (Managers.cpp, carriedLayoutApplies). The whole level
// is uploaded green first, so every texel the union box spans outside the two boxes
// is green in the shadow too: this case is about WHERE the boxes land, not about GPU
// content between them. Green everywhere but the two blue boxes.
void Run2DScatter(const Format& f) {
constexpr int kFirstAt = 1, kSecondAt = 5;
const GLuint t = NewTexture(GL_TEXTURE_2D);
const bool integer = f.encoding == Encoding::Uint8;
const char* sampler = integer ? "usampler2D" : "sampler2D";
const auto green = Fill(f, kEdge * kEdge, 0.0f, 1.0f, 0.0f);
glBindTexture(GL_TEXTURE_2D, t);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(f.internalFormat), kEdge, kEdge, 0, f.clientFormat,
f.clientType, green.data());
glBindTexture(GL_TEXTURE_2D, 0);
ASSERT_EQ(FirstGLError(), 0u) << "definition";
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
if (!Attach(t, -1)) {
Detach();
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
GTEST_SKIP() << "the format does not attach on backend " << Gl().BackendName();
}
Detach();
const auto blue = Fill(f, kBoxEdge * kBoxEdge, 0.0f, 0.0f, 1.0f);
glBindTexture(GL_TEXTURE_2D, t);
glTexSubImage2D(GL_TEXTURE_2D, 0, kFirstAt, kFirstAt, kBoxEdge, kBoxEdge, f.clientFormat, f.clientType,
blue.data());
glTexSubImage2D(GL_TEXTURE_2D, 0, kSecondAt, kSecondAt, kBoxEdge, kBoxEdge, f.clientFormat,
f.clientType, blue.data());
glBindTexture(GL_TEXTURE_2D, 0);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
ASSERT_TRUE(Attach(t, -1));
const auto rgb = ReadRgb(f, kEdge, kEdge);
Detach();
int bad = 0;
std::ostringstream first;
for (int y = 0; y < kEdge; ++y)
for (int x = 0; x < kEdge; ++x) {
const auto inBox = [&](int at) {
return x >= at && x < at + kBoxEdge && y >= at && y < at + kBoxEdge;
};
const bool blueHere = inBox(kFirstAt) || inBox(kSecondAt);
const float want[3] = {0.0f, blueHere ? 0.0f : 1.0f, blueHere ? 1.0f : 0.0f};
const float* got = &rgb[static_cast<std::size_t>(y * kEdge + x) * 3u];
bool texelBad = false;
for (int c = 0; c < 3; ++c) texelBad |= std::fabs(got[c] - want[c]) > 0.02f;
if (texelBad && bad++ == 0) {
first << "(" << x << "," << y << ") got " << got[0] << "," << got[1] << "," << got[2]
<< " want " << want[0] << "," << want[1] << "," << want[2];
}
}
EXPECT_EQ(bad, 0) << bad << " of " << kEdge * kEdge << " texels wrong; first " << first.str()
<< " (a box read at the wrong pitch or offset lands the wrong bytes)";
}
// Building blocks of the RGBA8 ordering cases: the same texels, issued in the orders
// a backend that defers work may get wrong.
static Format Rgba8() { return {GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, Encoding::Unorm8}; }
GLuint NewRedRgba8() {
const GLuint t = NewTexture(GL_TEXTURE_2D);
const auto red = Fill(Rgba8(), kEdge * kEdge, 1.0f, 0.0f, 0.0f);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
glBindTexture(GL_TEXTURE_2D, 0);
EXPECT_EQ(FirstGLError(), 0u) << "definition";
return t;
}
void SampleRgba8(GLuint t) { SyncBySampling(GL_TEXTURE_2D, t, "sampler2D", "ivec2(0)"); }
void ClearRgba8Green(GLuint t) {
ASSERT_TRUE(Attach(t, -1));
GpuClearGreen(Rgba8());
Detach();
}
// A draw that writes green over the whole level.
void DrawRgba8Green(GLuint t) {
std::string error;
const GLuint program = CompileProgram(
kVS, "#version 430 core\nout vec4 o_color;\nvoid main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }\n",
&error);
ASSERT_NE(program, 0u) << error;
m_programs.push_back(program);
ASSERT_TRUE(Attach(t, -1));
glViewport(0, 0, kEdge, kEdge);
glUseProgram(program);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glUseProgram(0);
Detach();
glViewport(0, 0, Gl().Width(), Gl().Height());
}
void WriteRgba8BlueBox(GLuint t) {
const auto blue = Fill(Rgba8(), kBoxEdge * kBoxEdge, 0.0f, 0.0f, 1.0f);
glBindTexture(GL_TEXTURE_2D, t);
glTexSubImage2D(GL_TEXTURE_2D, 0, kBoxAt, kBoxAt, kBoxEdge, kBoxEdge, GL_RGBA, GL_UNSIGNED_BYTE,
blue.data());
glBindTexture(GL_TEXTURE_2D, 0);
}
// Green everywhere, and blue in the box when `boxWins`.
void ExpectRgba8(GLuint t, bool boxWins) {
ASSERT_TRUE(Attach(t, -1));
const auto rgb = ReadRgb(Rgba8(), kEdge, kEdge);
Detach();
ExpectClearAroundBox(rgb, kEdge, kEdge, kBoxAt, kBoxAt, boxWins ? kBoxEdge : 0, boxWins ? kBoxEdge : 0);
}
GLuint m_vao = 0, m_fbo = 0;
ColorFbo m_scratch{};
std::vector<GLuint> m_programs;
std::vector<GLuint> m_textures;
};
// Control: the shadow's bytes go to the driver as they are.
TEST_F(PartialUploadOverGpuContentScenario, Rgba8SubImageKeepsTheGpuClear) {
if (!Ready()) return;
Run2D({GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, Encoding::Unorm8});
}
TEST_F(PartialUploadOverGpuContentScenario, Srgb8SubImageKeepsTheGpuClear) {
if (!Ready()) return;
Run2D({GL_SRGB8, GL_RGB, GL_UNSIGNED_BYTE, 3, Encoding::Unorm8});
}
TEST_F(PartialUploadOverGpuContentScenario, Rgb8SnormSubImageKeepsTheGpuClear) {
if (!Ready()) return;
Run2D({GL_RGB8_SNORM, GL_RGB, GL_BYTE, 3, Encoding::Snorm8});
}
TEST_F(PartialUploadOverGpuContentScenario, Rgb16fSubImageKeepsTheGpuClear) {
if (!Ready()) return;
Run2D({GL_RGB16F, GL_RGB, GL_FLOAT, 3, Encoding::Float32});
}
TEST_F(PartialUploadOverGpuContentScenario, Rgb8uiSubImageKeepsTheGpuClear) {
if (!Ready()) return;
Run2D({GL_RGB8UI, GL_RGB_INTEGER, GL_UNSIGNED_BYTE, 3, Encoding::Uint8});
}
// P11 M: the rect-list shape of the same upload (Run2DScatter). RGBA8 is the control whose
// record pitch does address the uploaded bytes; the other two are stored four-channel on
// llvmpipe and on every ES driver, so their boxes are cut from the conversion's buffer.
TEST_F(PartialUploadOverGpuContentScenario, Rgba8ScatteredSubImagesLandWhereWritten) {
if (!Ready()) return;
Run2DScatter({GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, Encoding::Unorm8});
}
TEST_F(PartialUploadOverGpuContentScenario, Srgb8ScatteredSubImagesLandWhereWritten) {
if (!Ready()) return;
Run2DScatter({GL_SRGB8, GL_RGB, GL_UNSIGNED_BYTE, 3, Encoding::Unorm8});
}
TEST_F(PartialUploadOverGpuContentScenario, Rgb8uiScatteredSubImagesLandWhereWritten) {
if (!Ready()) return;
Run2DScatter({GL_RGB8UI, GL_RGB_INTEGER, GL_UNSIGNED_BYTE, 3, Encoding::Uint8});
}
// A 1D array's ES image is a 2D array one texel high with the layers in depth, so the
// upload is shaped differently from the shadow (layers as rows) even with no conversion.
TEST_F(PartialUploadOverGpuContentScenario, OneDimensionalArraySubImageKeepsTheGpuClearedLayer) {
if (!Ready()) return;
constexpr int kLayers = 2;
const Format f{GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, Encoding::Unorm8};
const GLuint t = NewTexture(GL_TEXTURE_1D_ARRAY);
const auto red = Fill(f, kEdge * kLayers, 1.0f, 0.0f, 0.0f);
glBindTexture(GL_TEXTURE_1D_ARRAY, t);
glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA8, kEdge, kLayers, 0, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
glBindTexture(GL_TEXTURE_1D_ARRAY, 0);
ASSERT_EQ(FirstGLError(), 0u) << "definition";
SyncBySampling(GL_TEXTURE_1D_ARRAY, t, "sampler1DArray", "ivec2(0)");
if (!Attach(t, 1)) {
Detach();
GTEST_SKIP() << "a 1D array layer does not attach on backend " << Gl().BackendName();
}
GpuClearGreen(f);
Detach();
SyncBySampling(GL_TEXTURE_1D_ARRAY, t, "sampler1DArray", "ivec2(0)");
const auto blue = Fill(f, kBoxEdge, 0.0f, 0.0f, 1.0f);
glBindTexture(GL_TEXTURE_1D_ARRAY, t);
glTexSubImage2D(GL_TEXTURE_1D_ARRAY, 0, kBoxAt, 1, kBoxEdge, 1, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
glBindTexture(GL_TEXTURE_1D_ARRAY, 0);
SyncBySampling(GL_TEXTURE_1D_ARRAY, t, "sampler1DArray", "ivec2(0)");
ASSERT_TRUE(Attach(t, 1));
const auto layer1 = ReadRgb(f, kEdge, 1);
Detach();
ExpectClearAroundBox(layer1, kEdge, 1, kBoxAt, 0, kBoxEdge, 1);
}
// The orders below matter to a backend that defers GPU work (Magma queues clears and runs
// texel uploads in a batch ahead of the frame's recorded commands); every one is plain GL.
// The clear is still queued when the write arrives: it has to reach the level first.
TEST_F(PartialUploadOverGpuContentScenario, SubImageAfterAQueuedClearLandsOnTheClear) {
if (!Ready()) return;
const GLuint t = NewRedRgba8();
SampleRgba8(t);
ClearRgba8Green(t);
WriteRgba8BlueBox(t);
SampleRgba8(t);
ExpectRgba8(t, true);
}
// Same, with the readback as the first thing to sync the write.
TEST_F(PartialUploadOverGpuContentScenario, SubImageAfterAQueuedClearLandsOnTheClearForAReadback) {
if (!Ready()) return;
const GLuint t = NewRedRgba8();
SampleRgba8(t);
ClearRgba8Green(t);
WriteRgba8BlueBox(t);
ExpectRgba8(t, true);
}
// The other order: the write is still pending when the clear arrives, and the clear wins.
TEST_F(PartialUploadOverGpuContentScenario, ClearAfterAPendingSubImageWins) {
if (!Ready()) return;
const GLuint t = NewRedRgba8();
SampleRgba8(t);
WriteRgba8BlueBox(t);
ClearRgba8Green(t);
SampleRgba8(t);
ExpectRgba8(t, false);
}
// A draw into the level instead of a clear.
TEST_F(PartialUploadOverGpuContentScenario, SubImageAfterADrawIntoTheLevelLandsOnTheDraw) {
if (!Ready()) return;
const GLuint t = NewRedRgba8();
SampleRgba8(t);
DrawRgba8Green(t);
WriteRgba8BlueBox(t);
SampleRgba8(t);
ExpectRgba8(t, true);
}
} // namespace
} // namespace MGITest