[Test] (P9 W2): TextureRemintPullScenario - fourteen re-mint cases on both backends x monolith/Split/Spawn/Tcp, and the forced-readback-failure fallback entries

This commit is contained in:
2026-09-29 03:23:54 -04:00
parent 6c2f8a560c
commit aa0c242b51
4 changed files with 701 additions and 9 deletions
@@ -108,6 +108,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/ImageLoadStoreSsoScenario.cpp
Scenarios/ImageTargetKindScenario.cpp
Scenarios/ImageFormatQualifierScenario.cpp
Scenarios/TextureRemintPullScenario.cpp
Scenarios/NonCoreImageFormatScenario.cpp
Scenarios/ImageSizeAfterRespecScenario.cpp
Scenarios/EmptyTextureLevelScenario.cpp
@@ -2688,6 +2689,40 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1, so it does not green-skip on these arms.
mgl_itest_register_split_arms("InvalidImageUnitScenario.*" "")
# ---- P9 W2: A TEXTURE RE-MINT NEEDS NO BYTE FROM THE CLIENT ------------------------------
#
# The ROADMAP's `TextureRemintPullScenario`, and it proves the opposite of its name: the
# server re-mints from the GPU level and its own staged store, so no pull protocol exists
# (notes/p9/W2-REMINT.md). Espryt's three arms here; the Magma arms are laid by hand in the
# `DirectVulkan.<arm>.Remint.` block, where the gated Magma flavour lives; both backends' monolith
# entries come from the whole-binary discovery.
mgl_itest_register_split_arms("TextureRemintPullScenario.*" "")
# W2-b's control. MGITEST_ESPRYT_FORCE_REMINT_READBACK_FAILURE is read by the SERVER (the
# promotion in RequireImageBindableStorageByHandle), so it reaches inproc and spawn - the spawned
# server copies this process's environment - and never the tcp lane's shared fixture: split and
# spawn only, the MAGMA_SERVER_ENV_KNOB_NO_TCP reason. The entries carry their own label rather
# than integration-{split,spawn} so the Espryt spawn-parity sets stay knob-free; integration-gpu
# is what runs them in the gate. Only the uploaded-only cases: their levels are covered by the
# store, so the fallback must still produce the right texels and the server log must name it.
foreach(mglItestArm IN ITEMS Split Spawn)
string(TOUPPER "${mglItestArm}" mglItestArmUpper)
string(TOLOWER "${mglItestArm}" mglItestArmLower)
foreach(remintFallbackCase AnUploadedMutableTextureKeepsItsTexelsAcrossTheImageBind
AWidenedFormatsUploadSurvivesTheCarrierRemint
AnRgba8SnormUploadSurvivesTheImageBind
AnRgba16SnormImmutableUploadSurvivesTheCarrierRemint)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.${mglItestArm}.RemintFallback."
TEST_FILTER "TextureRemintPullScenario.${remintFallbackCase}"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\\;integration-remint-fallback-${mglItestArmLower}"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_${mglItestArmUpper}_ENVIRONMENT}\;MGITEST_ESPRYT_FORCE_REMINT_READBACK_FAILURE=1\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p9-remint-fallback-${mglItestArmLower}-${remintFallbackCase}.log")
endforeach()
endforeach()
# ---- P5b t2: the transform-feedback spans, the XFB object bind, the patch parameter -------
#
# MG_Remote/CONTRACT-P5B.md §2 t2. The three P5 lanes above are the REDUCED PATH's targets
@@ -3612,6 +3647,40 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
endforeach()
endif()
# P9 W2: THE TEXTURE RE-MINT ON MAGMA, all three arms, GATED. A storage-usage upgrade recreates
# the VkImage and copies the old one into it GPU to GPU (PreserveTextureContentsOnRecreate), so
# no byte comes from the client here either (notes/p9/W2-REMINT.md). The gated band flavour
# (role-split state, strict errors, run-ahead), one entry per case for a private log each.
if (MOBILEGL_BUILD_DISAGGREGATED)
foreach(mglItestArm IN LISTS MGL_ITEST_MAGMA_ARMS)
mgl_itest_magma_arm_shape(${mglItestArm})
mgl_itest_join_environment(mglItestMagmaRemintEnvironment
${MGL_ITEST_MAGMA_BAND_FLAVOUR_ENV} ${mglItestArmTransportEnv})
foreach(remintCase AnUploadedMutableTextureKeepsItsTexelsAcrossTheImageBind
AGpuClearAfterTheUploadSurvivesTheImageBind
AnImmutableTexturesGpuClearSurvivesTheImageBind
ANullDefinedTextureWrittenOnlyByTheGpuSurvivesTheImageBind
APendingPartialUploadMergesOverGpuWrittenTexels
AGeneratedMipLevelSurvivesTheImageBind
AGpuClearedMipLevelSurvivesTheImageBind
ALevelDefinedWithoutDataAfterTheLastSyncDoesNotEndTheSession
ALevelRedefinedLargerWithoutDataAfterTheLastSyncDoesNotEndTheSession
APartialUploadIntoALevelDefinedAfterTheLastSyncReachesTheImage
AWidenedFormatsGpuClearSurvivesTheCarrierRemint
AWidenedFormatsUploadSurvivesTheCarrierRemint
AnRgba8SnormUploadSurvivesTheImageBind
AnRgba16SnormImmutableUploadSurvivesTheCarrierRemint)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.${mglItestArm}.Remint."
TEST_FILTER "TextureRemintPullScenario.${remintCase}"
DISCOVERY_TIMEOUT 30
PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT}
${mglItestArmProperties}
ENVIRONMENT "${mglItestMagmaRemintEnvironment}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p9-remint${mglItestArmLogTag}-${remintCase}.log")
endforeach()
endforeach()
endif()
# Magma application buffers: public GL pixels/bytes, with the two role blocks and
# strict field checks forced on. Existing public scenarios supply offset, mutation,
# persistent-map, growth and indirect coverage; no copied backend implementation.
@@ -29,9 +29,11 @@ namespace MGITest {
};
bool PeekPipeTextureResourceRecord(unsigned glTextureName, PipeTextureResourceRecordPeek* out);
// The process-wide texture-remint pull count (PipeStats "tex-remint-pulls", `trp=` on the
// The process-wide texture re-mint count (PipeStats "tex-remint-pulls", `trp=` on the
// summary line; ROADMAP open question 2). Arms the PipeStats counters for this process on
// the first call, which is what lets a case read the number without a stats-enabled lane.
// Process-wide means it sees the SERVER's count only where the server is this process:
// monolith and inproc, never spawn or tcp.
bool PeekPipeStatsTextureRemintPulls(unsigned long long* out);
// Espryt's count of texture uploads it actually issued (PipeStats "tex-upload-emissions"):
// what tells a CONSUMED pending upload apart from a DROPPED one, since the record's set is
@@ -33,7 +33,7 @@
// must see it too.
// M-A Nothing produced kMGPipeBindSampler / kMGPipeBindShaderImage, so ImageBindableHint was
// dead: the applier never saw a texture become image-bound, the metadata respecify
// (ID-18 M4) had no live trigger, and the remint pull the hint exists to prevent was
// (ID-18 M4) had no live trigger, and the re-mint the hint exists to prevent was
// neither prevented nor counted. The case here reads the applier's record around a
// glBindImageTexture: the hint arrives as a metadata update that keeps the pending upload
// standing beside it, and the picture after the transition is the texels that upload
@@ -538,9 +538,10 @@ void main() { oColor = texture(uTex, vUv); }
}
// THE NUMBER ROADMAP OPEN QUESTION 2 ASKS FOR: a texture Espryt allocated BEFORE the
// hint reached it is re-minted image-bindable at the bind and its levels replayed
// from the client's shadow - one remint pull, counted. Arming the counter here is
// what makes it readable without a stats-enabled lane.
// hint reached it is re-minted image-bindable at the bind - its levels replayed from
// the client's shadow in monolith - one re-mint, counted. Under inproc the server
// keeps this immutable RGBA8 allocation instead and counts nothing (P9 W2). Arming
// the counter here is what makes it readable without a stats-enabled lane.
unsigned long long pullsBefore = 0;
const bool pullsReadable = PeekPipeStatsTextureRemintPulls(&pullsBefore);
@@ -581,10 +582,10 @@ void main() { oColor = texture(uTex, vUv); }
// THE TRANSITION. An immutable texture has no storage-defining respecify left, so the
// hint can only arrive as a metadata update (ID-18 M4). Espryt syncs the texture
// eagerly inside glBindImageTexture and the widening re-mints its storage, replaying
// every defined level from the shadow (the remint pull the counter below counts), so
// every defined level from the shadow (the re-mint the counter below counts), so
// the standing upload is consumed by that regeneration here and the picture that
// follows is blue whatever the metadata respecify did to the record - the KEPT
// property is proved further down, on a texture no remint stands in front of.
// property is proved further down, on a texture no re-mint stands in front of.
(void)uploadsBeforeBind;
(void)uploadsReadable;
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
@@ -610,11 +611,11 @@ void main() { oColor = texture(uTex, vUv); }
ASSERT_TRUE(runtime.sessionActive);
EXPECT_EQ(pullsAfter, pullsBefore)
<< "the server must preserve this already immutable RGBA8 allocation; "
"image binding needs no remint pull";
"image binding needs no re-mint";
} else {
EXPECT_EQ(pullsAfter, pullsBefore + 1)
<< "the monolith re-mint of a texture allocated before its hint was not counted "
"as a remint pull (trp= on the stats line is ROADMAP open question 2's number)";
"(trp= on the stats line is ROADMAP open question 2's number)";
}
}
@@ -0,0 +1,620 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureRemintPullScenario.cpp
// Copyright (c) 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 TEXTURE RE-MINT NEEDS NO BYTE FROM THE CLIENT (P9 W2, notes/p9/W2-REMINT.md).
//
// The name is the ROADMAP's: P4a named `texture-remint-pull` the one new stall class of the
// split - a texture first bound to an image unit AFTER the backend allocated it has to be
// re-minted in image-bindable (possibly channel-widened) storage, and design/04 §8.4 assumed the
// server held no texels to fill it with, so the levels would be PULLED back from the client. P9
// W2 measured the opposite on both backends: Espryt's server reads each level back from the old
// GPU texture and replays its own staged store for what is still pending
// (RequireImageBindableStorageByHandle), Magma copies the old image into the new one GPU to GPU
// (PreserveTextureContentsOnRecreate). No pull protocol exists, and this scenario is what says
// the re-mint is right without one.
//
// EVERY CASE HAS ONE SHAPE. Give a texture its content by one means, sync it to the backend with
// a sampling draw (so the backend really holds storage when the bind arrives - a texture whose
// first sync comes after the bind is allocated image-bindable up front and re-mints nothing),
// optionally write it on the GPU, then glBindImageTexture it and copy it with imageLoad into a
// destination that was image-bound BEFORE its own first sync (so the destination never re-mints),
// and read the destination back through a framebuffer. The expectation is the GL answer, never
// "what monolith does": the cases exist exactly where the arms can differ.
//
// A mutable, uploaded, synced the store and the GPU agree
// B mutable, uploaded, then cleared on the GPU the store is STALE, the GPU holds it
// C immutable RGBA8, uploaded, then cleared Espryt's split keeps the allocation
// D null-defined, only ever cleared on the GPU the store has NO bytes
// E cleared, then an unsynced partial upload GPU texels merged with the pending box
// F glGenerateMipmap's level 1 GPU-generated, no bytes ever crossed
// F2 an uploaded level 1, then cleared on the GPU as B, one level down
// G level 1 null-defined AFTER the last sync the driver does not hold the level
// G2 level 0 redefined larger, null, after the sync the driver holds the OLD extent
// G3 G, plus a partial upload into that level the store holds the whole level
// H widened rg8 immutable, uploaded, then cleared the carrier re-mint (Espryt promotes)
// H2 widened rg8, uploaded only
// I RGBA8_SNORM mutable, uploaded only
// J RGBA16_SNORM immutable (widened), uploaded only
//
// G and G3 were Fatal{ResourceUnavailable, "image-promotion-readback"} on Espryt's split arms
// until W2-a: the promotion read a level the driver never allocated.
//
// ARMS. Both backends on the monolith arm and on Split / Spawn / Tcp. ONE NAMED SKIP: the
// monolith DirectGLES arm replays the client's shadow over texels the GPU wrote (B, C, D, E, F,
// F2, H) - a known dev defect, not P9's (W2-REMINT.md §6), whose fix moves the pull build's .text
// (G1). Its other cases run and pass there.
//
// W2-d's COUNTER. `trp=` (TextureRemintPulls) counts a re-mint of storage the backend already
// held on BOTH Espryt arms now. The peek is process-wide, so it sees the server's count only
// where the server is this process: monolith and inproc. A/H assert +1 there, C asserts +0 on
// inproc (the kept allocation is not a re-mint).
//
// W2-b's CONTROL. MGITEST_ESPRYT_FORCE_REMINT_READBACK_FAILURE=1 (read by the SERVER) makes every
// promotion readback fail, which a host driver cannot be made to do for real. The
// `DirectGLES.{Split,Spawn}.RemintFallback.` entries run the uploaded-only cases under it: the
// store covers those levels, so the pixels stay right and the server log names the fallback.
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/P4aFinalFixPeek.h"
#include "../Harness/PipeStatsWindow.h"
#include "../Harness/ScenarioFixture.h"
#include "../Harness/SplitRuntimePeek.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 const char* kForceReadbackFailure = "MGITEST_ESPRYT_FORCE_REMINT_READBACK_FAILURE";
constexpr const char* kFallbackMarker = "remint-readback-fallback";
constexpr const char* kVertexSource = 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);
}
)";
// The sync: any draw that samples the texture makes the backend allocate and upload it.
constexpr const char* kSampleSource = R"(#version 430 core
uniform sampler2D u_texture;
out vec4 o_color;
void main() { o_color = texelFetch(u_texture, ivec2(gl_FragCoord.xy) % 4, 0); }
)";
std::string CopyComputeSource(const char* sourceFormat, bool signedNormalized) {
std::string source = "#version 430 core\nlayout(local_size_x = 8, local_size_y = 8) in;\n";
source += std::string("layout(") + sourceFormat + ", binding = 0) readonly uniform image2D u_source;\n";
source += "layout(rgba8, binding = 1) writeonly uniform image2D u_destination;\n";
source += "void main() {\n ivec2 p = ivec2(gl_GlobalInvocationID.xy);\n";
source += " vec4 v = imageLoad(u_source, p);\n";
if (signedNormalized) source += " v = v * 0.5 + 0.5;\n";
source += " imageStore(u_destination, p, v);\n}\n";
return source;
}
std::vector<GLubyte> Pattern(int edge, GLubyte magic) {
std::vector<GLubyte> texels(static_cast<std::size_t>(edge * edge) * 4u);
for (int y = 0; y < edge; ++y) {
for (int x = 0; x < edge; ++x) {
const std::size_t at = static_cast<std::size_t>((y * edge + x) * 4);
texels[at + 0] = magic;
texels[at + 1] = static_cast<GLubyte>(16 * (x + 1));
texels[at + 2] = static_cast<GLubyte>(16 * (y + 1));
texels[at + 3] = 0xFF;
}
}
return texels;
}
std::vector<GLubyte> Solid(int edge, GLubyte r, GLubyte g, GLubyte b, GLubyte a) {
std::vector<GLubyte> texels(static_cast<std::size_t>(edge * edge) * 4u);
for (std::size_t i = 0; i < texels.size(); i += 4) {
texels[i] = r;
texels[i + 1] = g;
texels[i + 2] = b;
texels[i + 3] = a;
}
return texels;
}
const GLfloat kGreen[4] = {0.0f, 1.0f, 0.0f, 1.0f};
class TextureRemintPullScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
GLint computeImages = 0;
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &computeImages);
FirstGLError();
if (computeImages < 2) {
GTEST_SKIP() << "the compute stage has fewer than two image uniforms";
}
m_wire = SplitRuntimeSkipReason().empty();
m_inproc = m_wire && PeekSplitRuntime().transportName == "inproc";
m_espryt = Gl().BackendName() == "DirectGLES";
m_forced = std::getenv(kForceReadbackFailure) != nullptr;
std::string error;
m_sampleProgram = CompileProgram(kVertexSource, kSampleSource, &error);
ASSERT_NE(m_sampleProgram, 0u) << error;
m_scratch = MakeColorFbo(4, 4);
ASSERT_NE(m_scratch.fbo, 0u) << "could not create the sampling target";
glGenVertexArrays(1, &m_vao);
glGenFramebuffers(1, &m_fbo);
ASSERT_EQ(FirstGLError(), 0u);
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glBindTexture(GL_TEXTURE_2D, 0);
if (m_sampleProgram != 0) glDeleteProgram(m_sampleProgram);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
if (!m_textures.empty()) {
glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
}
BindDefaultFramebuffer();
if (m_scratch.fbo != 0) DestroyColorFbo(m_scratch);
glViewport(0, 0, Gl().Width(), Gl().Height());
FirstGLError();
}
// The one named skip (see the header): the monolith DirectGLES re-mint replays the
// client's shadow, which never saw what the GPU wrote. Empty when the arm is a subject.
std::string MonolithEsprytShadowReplaySkip() const {
if (!m_espryt || m_wire) return {};
return "the monolith DirectGLES re-mint (RequireImageBindableStorage) replays the client's "
"shadow over texels the GPU wrote - a known dev defect filed separately "
"(notes/p9/W2-REMINT.md §6), whose fix moves the pull build's .text (G1). The split "
"arms and both Magma arms are this case's subject.";
}
GLuint NewTexture() {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
return texture;
}
static void NearestWithMaxLevel(GLuint texture, int maxLevel) {
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);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, maxLevel);
glBindTexture(GL_TEXTURE_2D, 0);
}
// A draw that samples the texture: from here on the backend holds storage for it.
void SyncBySampling(GLuint texture) {
BindFbo(m_scratch);
glUseProgram(m_sampleProgram);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glUniform1i(glGetUniformLocation(m_sampleProgram, "u_texture"), 0);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
glUseProgram(0);
BindDefaultFramebuffer();
glFinish();
}
// A GPU write the client's shadow never sees.
void ClearOnTheGpu(GLuint texture, int level, const GLfloat* color) {
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, level);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
glClearBufferfv(GL_COLOR, 0, color);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
BindDefaultFramebuffer();
}
// THE TRANSITION AND THE READ. The destination is image-bound before its first sync,
// so only the source can re-mint; the copy goes through imageLoad, which reads the
// re-minted storage and nothing else.
std::vector<GLubyte> ImageCopy(GLuint source, int level, GLenum unitFormat, const char* glslFormat,
bool signedNormalized = false) {
const GLuint destination = NewTexture();
glBindTexture(GL_TEXTURE_2D, destination);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge, kEdge);
glBindTexture(GL_TEXTURE_2D, 0);
glBindImageTexture(1, destination, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8);
glBindImageTexture(0, source, level, GL_FALSE, 0, GL_READ_ONLY, unitFormat);
EXPECT_EQ(FirstGLError(), 0u) << "glBindImageTexture raised a GL error";
const std::string computeSource = CopyComputeSource(glslFormat, signedNormalized);
const char* text = computeSource.c_str();
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
EXPECT_EQ(linked, GL_TRUE) << "the imageLoad copy did not link";
glUseProgram(program);
// A BARRIER THE SPEC DOES NOT ASK FOR, AND IT IS HERE ON PURPOSE. GL orders a
// framebuffer write (the GPU clears above) before a later shader image load without
// one, yet on the Espryt split arm the kept-allocation case (C, no re-mint to
// serialize anything) read the pre-clear texels in 3 of 30 runs without it and 0 of
// 30 with it, while every server-side decision was identical in passing and failing
// runs (W2-REMINT.md §7). That ordering gap is not the re-mint's question; this case
// asks what the re-minted storage HOLDS, so the ordering is pinned here.
glMemoryBarrier(GL_ALL_BARRIER_BITS);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
glUseProgram(0);
glDeleteProgram(program);
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, destination, 0);
std::vector<GLubyte> read(static_cast<std::size_t>(kEdge * kEdge) * 4u, 0xA5);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, kEdge, kEdge, GL_RGBA, GL_UNSIGNED_BYTE, read.data());
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
BindDefaultFramebuffer();
EXPECT_EQ(FirstGLError(), 0u) << "the copy or its readback raised a GL error";
return read;
}
// The `edge` x `edge` corner of the read against `want`, `channels` channels, within 2.
void ExpectTexels(const char* what, const std::vector<GLubyte>& read, int edge,
const std::vector<GLubyte>& want, int channels = 4) {
int wrong = 0;
int firstX = -1, firstY = -1;
for (int y = 0; y < edge; ++y) {
for (int x = 0; x < edge; ++x) {
for (int c = 0; c < channels; ++c) {
const int got = read[static_cast<std::size_t>((y * kEdge + x) * 4 + c)];
const int expected = want[static_cast<std::size_t>((y * edge + x) * 4 + c)];
if (std::abs(got - expected) > 2 && wrong++ == 0) {
firstX = x;
firstY = y;
}
}
}
}
const std::size_t readAt = static_cast<std::size_t>((std::max(firstY, 0) * kEdge + std::max(firstX, 0)) * 4);
const std::size_t wantAt = static_cast<std::size_t>((std::max(firstY, 0) * edge + std::max(firstX, 0)) * 4);
EXPECT_EQ(wrong, 0) << what << ": " << wrong << " channel(s) differ; first at (" << firstX << ", "
<< firstY << ") read " << int(read[readAt]) << "," << int(read[readAt + 1]) << ","
<< int(read[readAt + 2]) << "," << int(read[readAt + 3]) << " want "
<< int(want[wantAt]) << "," << int(want[wantAt + 1]) << ","
<< int(want[wantAt + 2]) << "," << int(want[wantAt + 3]) << " (backend "
<< Gl().BackendName() << ", " << (m_wire ? "split" : "monolith") << ")";
}
// W2-d: where the peek can see the server's count (monolith, inproc).
bool RemintCountVisible() const { return m_espryt && (!m_wire || m_inproc); }
unsigned long long RemintCount() {
unsigned long long count = 0;
EXPECT_TRUE(PeekPipeStatsTextureRemintPulls(&count));
return count;
}
// W2-b: under the forced-failure knob the server must have named its fallback.
void ExpectFallbackNamedSince(const PipeStatsWindow::LogMark& mark) {
if (!m_forced) return;
const std::string log = PipeStatsWindow::ReadServerLogSince(mark);
EXPECT_NE(log.find(kFallbackMarker), std::string::npos)
<< kForceReadbackFailure << " is set but the server log after the image bind carries no '"
<< kFallbackMarker << "' line: either the knob did not reach the server (the entry "
<< "proves nothing) or the promotion took no readback at all";
}
bool m_wire = false;
bool m_inproc = false;
bool m_espryt = false;
bool m_forced = false;
GLuint m_sampleProgram = 0;
GLuint m_vao = 0;
GLuint m_fbo = 0;
ColorFbo m_scratch{};
std::vector<GLuint> m_textures;
};
TEST_F(TextureRemintPullScenario, AnUploadedMutableTextureKeepsItsTexelsAcrossTheImageBind) {
if (!Ready()) return;
const bool counted = RemintCountVisible() && !m_forced;
const unsigned long long before = counted ? RemintCount() : 0;
const GLuint texture = NewTexture();
const auto pattern = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
const auto mark = PipeStatsWindow::MarkLaneLog();
ExpectTexels("an uploaded mutable texture", ImageCopy(texture, 0, GL_RGBA8, "rgba8"), kEdge, pattern);
ExpectFallbackNamedSince(mark);
if (counted) {
EXPECT_EQ(RemintCount(), before + 1)
<< "the re-mint of a mutable texture the backend already held was not counted (trp=)";
}
}
TEST_F(TextureRemintPullScenario, AGpuClearAfterTheUploadSurvivesTheImageBind) {
if (!Ready()) return;
if (const std::string skip = MonolithEsprytShadowReplaySkip(); !skip.empty()) GTEST_SKIP() << skip;
const GLuint texture = NewTexture();
const auto pattern = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
ClearOnTheGpu(texture, 0, kGreen);
SyncBySampling(texture);
ExpectTexels("a GPU clear after the upload", ImageCopy(texture, 0, GL_RGBA8, "rgba8"), kEdge,
Solid(kEdge, 0, 255, 0, 255));
}
TEST_F(TextureRemintPullScenario, AnImmutableTexturesGpuClearSurvivesTheImageBind) {
if (!Ready()) return;
if (const std::string skip = MonolithEsprytShadowReplaySkip(); !skip.empty()) GTEST_SKIP() << skip;
const bool counted = RemintCountVisible() && m_inproc;
const unsigned long long before = counted ? RemintCount() : 0;
const GLuint texture = NewTexture();
const auto pattern = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge, kEdge);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
ClearOnTheGpu(texture, 0, kGreen);
SyncBySampling(texture);
ExpectTexels("an immutable texture's GPU clear", ImageCopy(texture, 0, GL_RGBA8, "rgba8"), kEdge,
Solid(kEdge, 0, 255, 0, 255));
if (counted) {
EXPECT_EQ(RemintCount(), before)
<< "an immutable core-format allocation is KEPT by the server, not re-minted, and must "
"not count (trp=)";
}
}
TEST_F(TextureRemintPullScenario, ANullDefinedTextureWrittenOnlyByTheGpuSurvivesTheImageBind) {
if (!Ready()) return;
if (const std::string skip = MonolithEsprytShadowReplaySkip(); !skip.empty()) GTEST_SKIP() << skip;
const GLuint texture = NewTexture();
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
ClearOnTheGpu(texture, 0, kGreen);
SyncBySampling(texture);
ExpectTexels("a null-defined texture only the GPU wrote", ImageCopy(texture, 0, GL_RGBA8, "rgba8"),
kEdge, Solid(kEdge, 0, 255, 0, 255));
}
TEST_F(TextureRemintPullScenario, APendingPartialUploadMergesOverGpuWrittenTexels) {
if (!Ready()) return;
if (const std::string skip = MonolithEsprytShadowReplaySkip(); !skip.empty()) GTEST_SKIP() << skip;
const GLuint texture = NewTexture();
const auto pattern = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
ClearOnTheGpu(texture, 0, kGreen);
SyncBySampling(texture);
const auto blue = Solid(2, 0, 0, 255, 255);
glBindTexture(GL_TEXTURE_2D, texture);
glTexSubImage2D(GL_TEXTURE_2D, 0, 2, 2, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
glBindTexture(GL_TEXTURE_2D, 0);
auto want = Solid(kEdge, 0, 255, 0, 255);
for (int y = 2; y < 4; ++y) {
for (int x = 2; x < 4; ++x) {
const std::size_t at = static_cast<std::size_t>((y * kEdge + x) * 4);
want[at] = 0;
want[at + 1] = 0;
want[at + 2] = 255;
}
}
ExpectTexels("a pending partial upload over GPU texels", ImageCopy(texture, 0, GL_RGBA8, "rgba8"),
kEdge, want);
}
TEST_F(TextureRemintPullScenario, AGeneratedMipLevelSurvivesTheImageBind) {
if (!Ready()) return;
if (const std::string skip = MonolithEsprytShadowReplaySkip(); !skip.empty()) GTEST_SKIP() << skip;
const GLuint texture = NewTexture();
const auto solid = Solid(kEdge, 200, 100, 50, 255);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, solid.data());
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
SyncBySampling(texture);
ExpectTexels("glGenerateMipmap's level 1", ImageCopy(texture, 1, GL_RGBA8, "rgba8"), kEdge / 2,
Solid(kEdge / 2, 200, 100, 50, 255));
}
TEST_F(TextureRemintPullScenario, AGpuClearedMipLevelSurvivesTheImageBind) {
if (!Ready()) return;
if (const std::string skip = MonolithEsprytShadowReplaySkip(); !skip.empty()) GTEST_SKIP() << skip;
const GLuint texture = NewTexture();
const auto level0 = Solid(kEdge, 200, 100, 50, 255);
const auto level1 = Solid(kEdge / 2, 10, 20, 30, 255);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, level0.data());
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, level1.data());
NearestWithMaxLevel(texture, 1);
SyncBySampling(texture);
ClearOnTheGpu(texture, 1, kGreen);
SyncBySampling(texture);
ExpectTexels("a GPU-cleared level 1", ImageCopy(texture, 1, GL_RGBA8, "rgba8"), kEdge / 2,
Solid(kEdge / 2, 0, 255, 0, 255));
}
// W2-a. Level 1 exists in the store (a null-data respecify) and on no driver: the texture's
// last sync predates it. The promotion must not read it back.
TEST_F(TextureRemintPullScenario, ALevelDefinedWithoutDataAfterTheLastSyncDoesNotEndTheSession) {
if (!Ready()) return;
const GLuint texture = NewTexture();
const auto pattern = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glBindTexture(GL_TEXTURE_2D, 0);
ExpectTexels("level 0 beside a level defined after the last sync",
ImageCopy(texture, 0, GL_RGBA8, "rgba8"), kEdge, pattern);
}
// W2-a's extent half: the driver holds level 0 at its OLD extent. The new contents are
// undefined (GL 4.6 core 8.5), so only "no Fatal, no GL error" is asserted.
TEST_F(TextureRemintPullScenario, ALevelRedefinedLargerWithoutDataAfterTheLastSyncDoesNotEndTheSession) {
if (!Ready()) return;
const GLuint texture = NewTexture();
const auto pattern = Pattern(kEdge / 2, 0x40);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
pattern.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glBindTexture(GL_TEXTURE_2D, 0);
(void)ImageCopy(texture, 0, GL_RGBA8, "rgba8");
EXPECT_EQ(FirstGLError(), 0u);
}
// W2-a's store half: the level is on no driver, but its whole staged run is in the store
// (the client's level shadow, the partial upload inside it), and that is what replays.
TEST_F(TextureRemintPullScenario, APartialUploadIntoALevelDefinedAfterTheLastSyncReachesTheImage) {
if (!Ready()) return;
const GLuint texture = NewTexture();
const auto pattern = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
const auto blue = Solid(2, 0, 0, 255, 255);
glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
// Level 1 inside [BASE_LEVEL, MAX_LEVEL], or the image access is invalid (8.26).
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
glBindTexture(GL_TEXTURE_2D, 0);
ExpectTexels("a partial upload into a level defined after the last sync",
ImageCopy(texture, 1, GL_RGBA8, "rgba8"), 2, blue);
}
TEST_F(TextureRemintPullScenario, AWidenedFormatsGpuClearSurvivesTheCarrierRemint) {
if (!Ready()) return;
if (const std::string skip = MonolithEsprytShadowReplaySkip(); !skip.empty()) GTEST_SKIP() << skip;
const bool counted = RemintCountVisible();
const unsigned long long before = counted ? RemintCount() : 0;
const GLuint texture = NewTexture();
std::vector<GLubyte> rg(static_cast<std::size_t>(kEdge * kEdge) * 2u, 0x33);
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RG8, kEdge, kEdge);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RG, GL_UNSIGNED_BYTE, rg.data());
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
const GLfloat halfQuarter[4] = {0.5f, 0.25f, 0.0f, 1.0f};
ClearOnTheGpu(texture, 0, halfQuarter);
SyncBySampling(texture);
ExpectTexels("a widened rg8 texture's GPU clear", ImageCopy(texture, 0, GL_RG8, "rg8"), kEdge,
Solid(kEdge, 128, 64, 0, 255), 2);
if (counted) {
EXPECT_EQ(RemintCount(), before + 1)
<< "the carrier re-mint of a widened texture the backend already held was not counted "
"(trp=)";
}
}
TEST_F(TextureRemintPullScenario, AWidenedFormatsUploadSurvivesTheCarrierRemint) {
if (!Ready()) return;
const GLuint texture = NewTexture();
std::vector<GLubyte> rg(static_cast<std::size_t>(kEdge * kEdge) * 2u);
for (std::size_t i = 0; i < rg.size(); i += 2) {
rg[i] = 0x33;
rg[i + 1] = 0x99;
}
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RG8, kEdge, kEdge);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RG, GL_UNSIGNED_BYTE, rg.data());
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
const auto mark = PipeStatsWindow::MarkLaneLog();
ExpectTexels("a widened rg8 upload", ImageCopy(texture, 0, GL_RG8, "rg8"), kEdge,
Solid(kEdge, 0x33, 0x99, 0, 255), 2);
ExpectFallbackNamedSince(mark);
}
TEST_F(TextureRemintPullScenario, AnRgba8SnormUploadSurvivesTheImageBind) {
if (!Ready()) return;
const GLuint texture = NewTexture();
std::vector<GLbyte> texels(static_cast<std::size_t>(kEdge * kEdge) * 4u);
for (std::size_t i = 0; i < texels.size(); i += 4) {
texels[i] = 127;
texels[i + 1] = 0;
texels[i + 2] = -127;
texels[i + 3] = 127;
}
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8_SNORM, kEdge, kEdge, 0, GL_RGBA, GL_BYTE, texels.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
const auto mark = PipeStatsWindow::MarkLaneLog();
ExpectTexels("an RGBA8_SNORM upload", ImageCopy(texture, 0, GL_RGBA8_SNORM, "rgba8_snorm", true), kEdge,
Solid(kEdge, 255, 128, 0, 255), 3);
ExpectFallbackNamedSince(mark);
}
TEST_F(TextureRemintPullScenario, AnRgba16SnormImmutableUploadSurvivesTheCarrierRemint) {
if (!Ready()) return;
const GLuint texture = NewTexture();
std::vector<GLshort> texels(static_cast<std::size_t>(kEdge * kEdge) * 4u);
for (std::size_t i = 0; i < texels.size(); i += 4) {
texels[i] = 32767;
texels[i + 1] = 0;
texels[i + 2] = -32767;
texels[i + 3] = 32767;
}
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA16_SNORM, kEdge, kEdge);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RGBA, GL_SHORT, texels.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
const auto mark = PipeStatsWindow::MarkLaneLog();
ExpectTexels("an RGBA16_SNORM immutable upload",
ImageCopy(texture, 0, GL_RGBA16_SNORM, "rgba16_snorm", true), kEdge,
Solid(kEdge, 255, 128, 0, 255), 3);
ExpectFallbackNamedSince(mark);
}
} // namespace
} // namespace MGITest