[MG_IntegrationTest] (P7): the streamed subdata-then-draw pair is a case on all three Magma arms

F1WireScenario.StreamedBufferSubDataBeforeEachDrawIsOrdered is OpenRA's frame reduced to four
quads: one GL_DYNAMIC_DRAW VBO that is never orphaned, four batches written over the SAME
offset-0 range and each drawn immediately, each in its own two-pixel column and its own colour.
The case asserts PIXELS, because this path has no GL error and no log line to assert - a draw
that read another batch's bytes still executes and still reports success.

Two entries per arm. The plain Fm. entry is a regression gate that runs on every driver. The
StaleSerial. entry carries MGITEST_MAGMA_FORCE_STALE_BUFFER_SERIAL=1 and is the rule-J red-once:
on integration-magma-spawn, at the package base, batches 0..2 lose their own vertices and batch 3
- the last write - keeps them, which is the mechanism spelled out in pixels. Its own ENVIRONMENT
rather than a lane-wide flip, for the reason the ShaderMip pair gives: the knob changes how every
streamed write in the process is answered.
This commit is contained in:
2026-09-22 18:44:07 -04:00
parent c9164f25d3
commit a2e244c036
2 changed files with 134 additions and 0 deletions
@@ -3066,6 +3066,33 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
${mglItestArmProperties}
ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MOBILEGL_TEST_PIPELINE_CREATE_DELAY_MS=25\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-pipelinedelay${mglItestArmLogTag}-${pipelineDelayCase}.log")
endforeach()
# ---- P7 wave 2 package B3, EXIT GATE 3: the streamed `glBufferSubData -> draw` pair.
#
# TWO ENTRIES OVER ONE CASE, for the reason the ShaderMip pair gives. The plain entry is
# a regression gate that runs on every driver. The forced entry is the RED-ONCE: it makes
# the frame-counting half of VkBufferManager's busy predicate answer "idle", which is the
# state a mid-frame idle drain puts the wire arm into on the device and which no host
# lane reaches on its own - measured, zero mid-frame frame-boundary drains across the
# whole OpenRA replay, which is why the divergence is device-only and why the lane could
# not arm against it before this knob existed.
#
# ITS OWN ENTRY RATHER THAN A LANE-WIDE FLIP: the knob changes how EVERY streamed buffer
# write in the process is answered, so putting it in the shared Fm environment would
# retarget every other draw case in this lane.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.${mglItestArm}.Fm."
TEST_FILTER "F1WireScenario.StreamedBufferSubDataBeforeEachDrawIsOrdered"
DISCOVERY_TIMEOUT 30
PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT}
${mglItestArmProperties}
ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-streamedsubdata${mglItestArmLogTag}.log")
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.${mglItestArm}.StaleSerial."
TEST_FILTER "F1WireScenario.StreamedBufferSubDataBeforeEachDrawIsOrdered"
DISCOVERY_TIMEOUT 30
PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT}
${mglItestArmProperties}
ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MGITEST_MAGMA_FORCE_STALE_BUFFER_SERIAL=1\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-streamedsubdata-stale${mglItestArmLogTag}.log")
# `default-color-blit-shape@P7`'s retirement. TWO ENTRIES OVER ONE CASE, and the second
# is the only way its arm is reachable here: every surface this lane creates reports
# VK_SURFACE_TRANSFORM_IDENTITY, so the scratch path that exists FOR rotation would
@@ -2064,4 +2064,111 @@ TEST_F(F1WireScenario, CopyImageInPlaceOverlapDeclinesAndLeavesTheLevelAlone) {
glBindTexture(GL_TEXTURE_2D, texture);
}
// ---- P7 wave 2 package B3, EXIT GATE 3: a streamed glBufferSubData -> draw pair is ordered --
//
// THE SHAPE OF THE OpenRA DEVICE DIVERGENCE, reduced to four quads. The application writes a
// batch of vertices over the SAME range of ONE dynamic buffer and immediately draws it, then
// writes the next batch over the same range and draws that, and so on - OpenRA does this ~24
// times per frame into GL buffer 1, and the picture that reached the Redmi was missing exactly
// the quads of one of those batches with the terrain under them intact.
//
// The server may answer such a write in two shapes: an ordered vkCmdCopyBuffer, or an immediate
// host memcpy when it believes the GPU is finished with the bytes. Believing that wrongly is
// silent and produces no log line and no GL error - the draw still executes, it just reads
// somebody else's vertices. So the case cannot assert "no error"; it has to assert the pixels.
//
// Registered TWICE per arm. The plain entry is a regression gate on every driver. The
// `StaleSerial.` entry carries MGITEST_MAGMA_FORCE_STALE_BUFFER_SERIAL=1, which forces the
// frame-counting half of the busy predicate to answer "idle" - the state a mid-frame idle drain
// puts the wire arm into on the device, and one no host lane reaches by itself (measured: zero
// mid-frame frame-boundary drains across the whole OpenRA replay). With the counting half
// forced to lie, only the submission-fence half can still order the copy, which is exactly what
// this package added and exactly what the entry pins.
TEST_F(F1WireScenario, StreamedBufferSubDataBeforeEachDrawIsOrdered) {
if (!Ready()) return;
constexpr int kSize = 8, kBatches = 4, kColumn = kSize / kBatches;
Attach(GL_RGBA8);
const GLuint program = BuildWireProgram({
{GL_VERTEX_SHADER, R"(#version 430 core
layout(location = 0) in vec2 aPos;
layout(location = 1) in vec4 aColor;
out vec4 vColor;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); vColor = aColor; }
)"},
{GL_FRAGMENT_SHADER, R"(#version 430 core
in vec4 vColor;
out vec4 fragColor;
void main() { fragColor = vColor; }
)"}});
ASSERT_NE(program, 0u);
// One vertical bar per batch, each in its own two-pixel column and its own colour, so a
// draw that read another batch's bytes lands in the wrong column AND the wrong colour.
constexpr GLfloat kColors[kBatches][4] = {{1, 0, 0, 1}, {0, 1, 0, 1}, {0, 0, 1, 1}, {1, 1, 0, 1}};
const auto batchVertices = [&](int batch) {
const GLfloat x0 = -1.0f + static_cast<GLfloat>(batch) * 0.5f;
const GLfloat x1 = x0 + 0.5f;
const GLfloat* c = kColors[batch];
return std::array<GLfloat, 36>{
x0, -1, c[0], c[1], c[2], c[3], x1, -1, c[0], c[1], c[2], c[3], x1, 1, c[0], c[1], c[2], c[3],
x0, -1, c[0], c[1], c[2], c[3], x1, 1, c[0], c[1], c[2], c[3], x0, 1, c[0], c[1], c[2], c[3]};
};
GLuint vao = 0, vbo = 0;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
// GL_DYNAMIC_DRAW and no orphaning: OpenRA's buffer 1 exactly.
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 36, nullptr, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), reinterpret_cast<const void*>(0));
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat),
reinterpret_cast<const void*>(2 * sizeof(GLfloat)));
glUseProgram(program);
glViewport(0, 0, kSize, kSize);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
for (int batch = 0; batch < kBatches; ++batch) {
const auto vertices = batchVertices(batch);
glBufferSubData(GL_ARRAY_BUFFER, 0, static_cast<GLsizeiptr>(sizeof(vertices)), vertices.data());
glDrawArrays(GL_TRIANGLES, 0, 6);
}
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.StreamedSubData.draw";
std::vector<GLubyte> pixels(static_cast<size_t>(kSize) * kSize * 4, 0);
glReadPixels(0, 0, kSize, kSize, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.StreamedSubData.readback";
for (int batch = 0; batch < kBatches; ++batch) {
const GLubyte* want = nullptr;
const GLubyte expected[4] = {static_cast<GLubyte>(kColors[batch][0] * 255.0f),
static_cast<GLubyte>(kColors[batch][1] * 255.0f),
static_cast<GLubyte>(kColors[batch][2] * 255.0f),
static_cast<GLubyte>(kColors[batch][3] * 255.0f)};
want = expected;
for (int dx = 0; dx < kColumn; ++dx) {
const int x = batch * kColumn + dx;
const GLubyte* got = pixels.data() + (static_cast<size_t>(kSize / 2) * kSize + x) * 4;
EXPECT_EQ(got[0], want[0]) << "F1.StreamedSubData: batch " << batch << " column " << x
<< " lost its own vertices (r)";
EXPECT_EQ(got[1], want[1]) << "F1.StreamedSubData: batch " << batch << " column " << x
<< " lost its own vertices (g)";
EXPECT_EQ(got[2], want[2]) << "F1.StreamedSubData: batch " << batch << " column " << x
<< " lost its own vertices (b)";
}
}
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDeleteBuffers(1, &vbo);
glBindVertexArray(0);
glDeleteVertexArrays(1, &vao);
glUseProgram(0);
glDeleteProgram(program);
}
} // namespace MGITest