[Test] (P11 M): dev's ImageBindableRemint and PartialUploadOverGpuContent scenarios on the Split/Spawn/Tcp arms of both backends, three scattered-upload cases for the handle arm's carried layout, and TextureRemintPullScenario's monolith Espryt skip retired

This commit is contained in:
2026-09-29 06:30:54 -04:00
parent 48a306025d
commit 9cde08064c
3 changed files with 129 additions and 23 deletions
@@ -2788,6 +2788,18 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
endforeach()
endforeach()
# ---- P11 M: DEV'S TWO GPU-CONTENT SCENARIOS ON THE SPLIT ARMS ----------------------------
#
# Merged from dev@c60639db with their monolith entries. ImageBindableRemintScenario is the
# monolith twin of TextureRemintPullScenario above (568090f0 made Espryt's frontend re-mint
# read the GPU level back as the server's already did); PartialUploadOverGpuContentScenario is
# 8966d5c2's sub-rect upload over converted/reshaped levels (the server's handle arm cuts the
# region from the conversion's buffer too) plus b1d60523's Magma ordering cases, which are
# plain GL and run here on Espryt as well. The Magma arms are laid by hand in the
# `DirectVulkan.<arm>.GpuContent.` block, where the gated Magma flavour lives.
mgl_itest_register_split_arms("ImageBindableRemintScenario.*" "")
mgl_itest_register_split_arms("PartialUploadOverGpuContentScenario.*" "")
# ---- 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
@@ -3778,6 +3790,31 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
endforeach()
endif()
# P11 M: DEV'S TWO GPU-CONTENT SCENARIOS ON MAGMA, all three arms, GATED, in the band flavour -
# ImageBindableRemintScenario (Magma is its control backend: a storage upgrade copies image to
# image) and PartialUploadOverGpuContentScenario (8966d5c2's RGB->RGBA region cut and b1d60523's
# upload-after-clear/draw ordering, both of which run in the server's renderer on these arms).
# One entry per scenario and arm; every entry gets a private log through MGL_SPLIT_ARM_TEST_LISTS.
if (MOBILEGL_BUILD_DISAGGREGATED)
foreach(mglItestArm IN LISTS MGL_ITEST_MAGMA_ARMS)
mgl_itest_magma_arm_shape(${mglItestArm})
mgl_itest_join_environment(mglItestMagmaGpuContentEnvironment
${MGL_ITEST_MAGMA_BAND_FLAVOUR_ENV} ${mglItestArmTransportEnv})
foreach(mglItestGpuContentScenario ImageBindableRemintScenario PartialUploadOverGpuContentScenario)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.${mglItestArm}.GpuContent."
TEST_LIST "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}"
TEST_FILTER "${mglItestGpuContentScenario}.*"
DISCOVERY_TIMEOUT 30
PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT}
${mglItestArmProperties}
ENVIRONMENT "${mglItestMagmaGpuContentEnvironment}")
list(APPEND MGL_SPLIT_ARM_TEST_LISTS "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}")
math(EXPR MGL_SPLIT_ARM_INDEX "${MGL_SPLIT_ARM_INDEX} + 1")
endforeach()
endforeach()
endif()
# P10 B: ASYNC COMPILATION, THE XFB QUERY PAIR AND GL_PRIMITIVES_GENERATED ON MAGMA, all three
# arms, GATED, in the band flavour (role-split state, strict errors, run-ahead). The DirectGLES
# half of the first two is registered beside the other Espryt arms; the third is Magma's alone.
@@ -259,6 +259,67 @@ void main() {
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}; }
@@ -348,6 +409,24 @@ void main() {
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) {
@@ -44,10 +44,12 @@
// 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.
// ARMS. Both backends on the monolith arm and on Split / Spawn / Tcp, every case. The monolith
// DirectGLES arm used to be skipped for B, C, D, E, F, F2 and H: its re-mint replayed the client's
// shadow over texels the GPU wrote (W2-REMINT.md §6, the half of ID-P9-8 left to dev). Dev's
// 568090f0 made it read the GPU level back first (AdoptDriverLevelIntoShadow), P11 M merged it,
// and the skip is retired; E is also what holds the push builds' monolith arm to merging the
// applier's pending boxes, which the frontend's dirty model no longer names 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
@@ -181,16 +183,6 @@ void main() { o_color = texelFetch(u_texture, ivec2(gl_FragCoord.xy) % 4, 0); }
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);
@@ -310,8 +302,13 @@ void main() { o_color = texelFetch(u_texture, ivec2(gl_FragCoord.xy) % 4, 0); }
<< 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); }
// W2-d: where the peek can see the server's count (monolith, inproc) - and only on a
// push build, which is where PipeStats exists; the pull build's peek answers false (P11
// M: the skip retired above now runs H there too, and A always did).
bool RemintCountVisible() const {
unsigned long long ignored = 0;
return m_espryt && (!m_wire || m_inproc) && PeekPipeStatsTextureRemintPulls(&ignored);
}
unsigned long long RemintCount() {
unsigned long long count = 0;
EXPECT_TRUE(PeekPipeStatsTextureRemintPulls(&count));
@@ -360,7 +357,6 @@ void main() { o_color = texelFetch(u_texture, ivec2(gl_FragCoord.xy) % 4, 0); }
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);
@@ -375,7 +371,6 @@ void main() { o_color = texelFetch(u_texture, ivec2(gl_FragCoord.xy) % 4, 0); }
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();
@@ -398,7 +393,6 @@ void main() { o_color = texelFetch(u_texture, ivec2(gl_FragCoord.xy) % 4, 0); }
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);
@@ -412,7 +406,6 @@ void main() { o_color = texelFetch(u_texture, ivec2(gl_FragCoord.xy) % 4, 0); }
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);
@@ -440,7 +433,6 @@ void main() { o_color = texelFetch(u_texture, ivec2(gl_FragCoord.xy) % 4, 0); }
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);
@@ -456,7 +448,6 @@ void main() { o_color = texelFetch(u_texture, ivec2(gl_FragCoord.xy) % 4, 0); }
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);
@@ -529,7 +520,6 @@ void main() { o_color = texelFetch(u_texture, ivec2(gl_FragCoord.xy) % 4, 0); }
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();