From a2e244c0364a8029928aeaa7e2e892744fcf4566 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 22 Sep 2026 18:25:31 -0400 Subject: [PATCH] [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. --- MobileGL/MG_IntegrationTest/CMakeLists.txt | 27 +++++ .../Scenarios/F1WireScenario.cpp | 107 ++++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index b17e7579..e639bf7e 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -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 diff --git a/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp index 25e6e051..f03c94b8 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp @@ -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(batch) * 0.5f; + const GLfloat x1 = x0 + 0.5f; + const GLfloat* c = kColors[batch]; + return std::array{ + 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(0)); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), + reinterpret_cast(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(sizeof(vertices)), vertices.data()); + glDrawArrays(GL_TRIANGLES, 0, 6); + } + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.StreamedSubData.draw"; + + std::vector pixels(static_cast(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(kColors[batch][0] * 255.0f), + static_cast(kColors[batch][1] * 255.0f), + static_cast(kColors[batch][2] * 255.0f), + static_cast(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(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