From e5649396155198bb51595aba48b57726b1f1345e Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 16 Sep 2026 12:10:38 -0400 Subject: [PATCH] [Feat] (MG_Remote, P5b d1): flip the nineteen indexed / instanced / multi-draw / indirect draw slots to class B on draw_vbo - the emitters plan the record from the GL call and the bound element / indirect / parameter buffers, stage a client index array as the kDrawHasUserIndices span, refuse +CLIENT_INDICES / +CLIENT_COMMANDS / +INDEX_OFFSET by name, and the sink dispatches every shape to the backend slot the monolith calls (a base vertex of 0 is DrawElements, never DrawElementsBaseVertex with 0: ANGLE has no glDrawElementsBaseVertex) --- MobileGL/MG_IntegrationTest/CMakeLists.txt | 36 ++ .../Harness/SplitLogPaths.cmake.in | 7 + .../Scenarios/IndexedDrawFamilyScenario.cpp | 313 ++++++++++++ MobileGL/MG_Remote/Client/ClientSession.cpp | 18 +- MobileGL/MG_Remote/Client/ClientSession.h | 9 + MobileGL/MG_Remote/Client/EmitTables.cpp | 464 ++++++++++++++++-- MobileGL/MG_Remote/Client/EmitTables.h | 55 +++ MobileGL/MG_Remote/Server/PipeApplier.cpp | 271 ++++++++-- MobileGL/MG_Remote/Server/PipeApplier.h | 31 +- MobileGL/MG_Test/Wire/RemoteClientTest.cpp | 221 ++++++++- MobileGL/MG_Test/Wire/ServerLoopTest.cpp | 135 +++++ 11 files changed, 1465 insertions(+), 95 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/IndexedDrawFamilyScenario.cpp diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index af59f21a..acddb303 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -62,6 +62,7 @@ add_executable(MobileGLIntegrationTest Scenarios/CrossFrameBufferScenario.cpp Scenarios/ResidentIndexScenario.cpp Scenarios/MultiDrawScenario.cpp + Scenarios/IndexedDrawFamilyScenario.cpp Scenarios/DrawParametersScenario.cpp Scenarios/AsyncCompileScenario.cpp Scenarios/XfbAfterClipDistanceScenario.cpp @@ -1935,6 +1936,41 @@ if (MOBILEGL_BUILD_DISAGGREGATED) ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}" ) + # ---- P5b d1: the indexed / instanced / multi-draw / indirect draw family under inproc ---- + # + # One block per scenario, the precedent above. IndexedDrawFamilyScenario is d1's own lane + # case: every case's picture depends on a d1 draw (an element-buffer glDrawElements, a + # client index array, a base vertex, the Minecraft trace's DrawElementsInstancedBaseVertex, + # the two multi-draws, an indirect draw), so a record that did not cross is a wrong picture + # and not a green. MultiDrawScenario, DrawParametersScenario, PrimitiveRestartScenario and + # GuiBatchScenario are the census's own DrawElements / MultiDrawElementsBaseVertex / + # DrawArraysInstancedBaseInstance / MultiDraw*Indirect* first-blocker scenarios, now at + # their pictures. The ONE case excluded by filter is MultiDrawScenario's + # ClientSideIndicesBatchMatchesUnrolledDraws: a multi-draw with client-side indices is + # refused BY NAME under split (Fatal{UnmigratedVerb, "MultiDrawElementsBaseVertex+ + # CLIENT_INDICES"}, CONTRACT-P5B.md d1 - P8's HostResolve.cpp flattens it), which is its + # next first blocker and not a lane failure. It is listed in d1-v1.md, not deleted (G14). + set(MGL_SPLIT_D1_SCENARIOS IndexedDrawFamilyScenario MultiDrawScenario DrawParametersScenario + PrimitiveRestartScenario GuiBatchScenario) + set(MGL_SPLIT_D1_FILTER_IndexedDrawFamilyScenario "IndexedDrawFamilyScenario.*") + set(MGL_SPLIT_D1_FILTER_MultiDrawScenario + "MultiDrawScenario.*-MultiDrawScenario.ClientSideIndicesBatchMatchesUnrolledDraws") + set(MGL_SPLIT_D1_FILTER_DrawParametersScenario "DrawParametersScenario.*") + set(MGL_SPLIT_D1_FILTER_PrimitiveRestartScenario "PrimitiveRestartScenario.*") + set(MGL_SPLIT_D1_FILTER_GuiBatchScenario "GuiBatchScenario.*") + foreach(mglItestD1Scenario IN LISTS MGL_SPLIT_D1_SCENARIOS) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.Split." + TEST_LIST "MGL_SPLIT_D1_${mglItestD1Scenario}_TESTS" + TEST_FILTER "${MGL_SPLIT_D1_FILTER_${mglItestD1Scenario}}" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS "integration-gpu\;integration-split" + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}" + ) + endforeach() + # The split half of the counting pair. MGITEST_PERSISTENT_MAP_ARM=emulated is R-6: under split # the adopt tier is pinned at T2, the resource owner declines every acquisition and the client # pushes the mapping's dirty blocks - so pmap must be non-zero and mpr must be the monolith diff --git a/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in b/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in index 532ba453..b6017508 100644 --- a/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in +++ b/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in @@ -11,4 +11,11 @@ foreach(scenario @MGL_SPLIT_SMALL_RING_SCENARIOS@) "MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log") endforeach() endforeach() +# P5b d1's split entries: the same private, distinct log per entry. +foreach(scenario @MGL_SPLIT_D1_SCENARIOS@) + foreach(entry IN LISTS MGL_SPLIT_D1_${scenario}_TESTS) + set_tests_properties("${entry}" PROPERTIES ENVIRONMENT + "MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log") + endforeach() +endforeach() # PersistentMapArm retains its existing private path and RESOURCE_LOCK: b1 reads it. diff --git a/MobileGL/MG_IntegrationTest/Scenarios/IndexedDrawFamilyScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/IndexedDrawFamilyScenario.cpp new file mode 100644 index 00000000..3d4dcb45 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/IndexedDrawFamilyScenario.cpp @@ -0,0 +1,313 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IndexedDrawFamilyScenario.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 - THE DRAW FAMILY P5b's d1 PUTS ON THE WIRE (MG_Remote/CONTRACT-P5B.md §2 d1): one +// VBO, one EBO, one program, one VAO, and one picture per draw entry point whose correctness +// DEPENDS on the fields that entry point adds to draw_vbo. Two quads live in the vertex buffer, +// a left one and a right one, and every case draws exactly one of them or both through a +// different entry point: +// +// glDrawElements the element-buffer offset (Start = offset / IndexSize) +// glDrawElements, no EBO bound a CLIENT index array - the kDrawHasUserIndices span the +// client stages into SEG_STAGE (the P8 resolve-on-client +// rule, applied by d1) +// glDrawElementsBaseVertex IndexBias: the same six indices land on the other quad +// glDrawRangeElements kDrawHasIndexRange with MinIndex / MaxIndex +// glDrawElementsInstancedBaseVertex InstanceCount: the Minecraft trace's own slot +// (improved-transparency-minecraft-26.3 first-stops here) +// glMultiDrawElementsBaseVertex NumDraws = 2 with a per-range base vertex +// glMultiDrawArrays NumDraws = 2, arrays +// glMultiDrawElementsIndirect kDrawIsIndirect, the MGPDrawIndirect second tail +// +// It is an ORDINARY GL scenario and runs in every lane; the DirectGLES.Split. entries run the +// same bodies under MOBILEGL_TRANSPORT=inproc, where a field that did not cross is a wrong +// picture (the other quad, or no quad) rather than a green. The harness destructor's emit-seq +// check (ScenarioFixture.h) is the statement that records crossed at all; the boxes below are +// the statement that the RIGHT fields crossed. +// +// No glFlush anywhere, for TriangleScenario's reason (its header, point 2): glReadPixels is the +// ordering point on both backends and the SEG_REPLY round trip under split. + +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + // gl_InstanceID shifts a quad one full quad-width to the right per instance, so the + // second instance of the LEFT quad lands exactly on the RIGHT quad's box: an instance + // count that did not cross draws one quad, one that did draws two. + constexpr const char* kVertexSource = R"(#version 330 core +layout(location = 0) in vec2 aPos; +void main() { + gl_Position = vec4(aPos.x + float(gl_InstanceID), aPos.y, 0.0, 1.0); +} +)"; + + constexpr const char* kFragmentSource = R"(#version 330 core +out vec4 oColor; +void main() { oColor = vec4(0.0, 1.0, 0.0, 1.0); } +)"; + + struct Vertex { + float x, y; + }; + + // Vertices 0..3: the LEFT quad's corners; 4..7: the RIGHT quad's corners (for the + // indexed draws). Vertices 8..13 and 14..19: the same two quads as six vertices each + // (for the arrays draws). x spans [-0.9, -0.1] and [0.1, 0.9]; y spans [-0.8, 0.8]. + constexpr Vertex kVertices[20] = { + {-0.9f, -0.8f}, {-0.1f, -0.8f}, {-0.1f, 0.8f}, {-0.9f, 0.8f}, // 0..3 left + {0.1f, -0.8f}, {0.9f, -0.8f}, {0.9f, 0.8f}, {0.1f, 0.8f}, // 4..7 right + {-0.9f, -0.8f}, {-0.1f, -0.8f}, {-0.1f, 0.8f}, // 8..13 left, arrays + {-0.1f, 0.8f}, {-0.9f, 0.8f}, {-0.9f, -0.8f}, + {0.1f, -0.8f}, {0.9f, -0.8f}, {0.9f, 0.8f}, // 14..19 right, arrays + {0.9f, 0.8f}, {0.1f, 0.8f}, {0.1f, -0.8f}, + }; + + // Indices 0..5 draw the left quad; 6..11 draw the right one. GL_UNSIGNED_SHORT, so the + // second run starts at BYTE offset 12 and Start = 6 on the wire. + constexpr std::uint16_t kIndices[12] = {0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4}; + constexpr GLsizeiptr kRightRunByteOffset = 6 * sizeof(std::uint16_t); + + struct DrawElementsIndirectCommand { + std::uint32_t count, instanceCount, firstIndex, baseVertex, baseInstance; + }; + + class IndexedDrawFamilyScenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + + std::string error; + m_program = CompileProgram(kVertexSource, kFragmentSource, &error); + ASSERT_NE(m_program, 0u) << error; + + glGenVertexArrays(1, &m_vao); + glBindVertexArray(m_vao); + glGenBuffers(1, &m_vbo); + glBindBuffer(GL_ARRAY_BUFFER, m_vbo); + glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(sizeof(kVertices)), kVertices, GL_STATIC_DRAW); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr); + glEnableVertexAttribArray(0); + glGenBuffers(1, &m_ebo); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, GLsizeiptr(sizeof(kIndices)), kIndices, GL_STATIC_DRAW); + ASSERT_EQ(FirstGLError(), 0u) << "building the VBO, the EBO and the VAO"; + } + + void TearDown() override { + if (!Ready() || IsSkipped()) return; + glBindVertexArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glBindBuffer(GL_DRAW_INDIRECT_BUFFER, 0); + if (m_indirect != 0) glDeleteBuffers(1, &m_indirect); + if (m_ebo != 0) glDeleteBuffers(1, &m_ebo); + if (m_vbo != 0) glDeleteBuffers(1, &m_vbo); + if (m_vao != 0) glDeleteVertexArrays(1, &m_vao); + if (m_program != 0) glDeleteProgram(m_program); + m_indirect = m_ebo = m_vbo = m_vao = m_program = 0; + } + + // Clear to blue, run `draw`, read back. The draw is the ONLY thing that differs + // between the cases. + template + Image ClearThenDrawThenRead(Draw draw) { + HeadlessGL& gl = Gl(); + BindDefaultFramebuffer(); + glViewport(0, 0, gl.Width(), gl.Height()); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_DEPTH_TEST); + ClearTo(0.0f, 0.0f, 1.0f, 1.0f); + glUseProgram(m_program); + glBindVertexArray(m_vao); + draw(); + return ReadPixels(gl.Width(), gl.Height()); + } + + // The interior of the left quad, of the right quad, and the bottom-left corner + // that no quad covers (it carries the clear). + void ExpectLeft(const Image& image, const char* color, const std::string& when) { + const int w = image.Width(); + const int h = image.Height(); + EXPECT_TRUE(RegionIsMostly(image, (w * 20) / 100, (w * 30) / 100, (h * 40) / 100, + (h * 60) / 100, color, 0.0, when + " (left quad)")); + } + void ExpectRight(const Image& image, const char* color, const std::string& when) { + const int w = image.Width(); + const int h = image.Height(); + EXPECT_TRUE(RegionIsMostly(image, (w * 70) / 100, (w * 80) / 100, (h * 40) / 100, + (h * 60) / 100, color, 0.0, when + " (right quad)")); + } + void ExpectCorner(const Image& image, const char* color, const std::string& when) { + const int w = image.Width(); + const int h = image.Height(); + EXPECT_TRUE(RegionIsMostly(image, 0, (w * 3) / 100, 0, (h * 3) / 100, color, 0.0, + when + " (corner, the clear)")); + } + + unsigned int m_program = 0; + unsigned int m_vao = 0; + unsigned int m_vbo = 0; + unsigned int m_ebo = 0; + unsigned int m_indirect = 0; + }; + + } // namespace + + // The census's own first blocker for every Minecraft trace: an element-buffer glDrawElements. + // The byte offset selects the RIGHT quad, so an offset that crossed as 0 (or not at all) + // paints the left one. + TEST_F(IndexedDrawFamilyScenario, AnElementBufferDrawElementsOffsetSelectsTheRightQuad) { + if (!Ready() || IsSkipped()) return; + const Image image = ClearThenDrawThenRead([] { + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, + reinterpret_cast(kRightRunByteOffset)); + }); + EXPECT_EQ(FirstGLError(), 0u); + ExpectRight(image, "green", "glDrawElements at byte offset 12"); + ExpectLeft(image, "blue", "glDrawElements at byte offset 12 must not paint the left quad"); + ExpectCorner(image, "blue", "glDrawElements"); + // The frame boundary, once, so the lane reaches Present as TriangleScenario does. + Gl().EndFrame(); + } + + // No element buffer bound: `indices` is the application's own array. Under split the client + // stages the twelve bytes and the record names the run (kDrawHasUserIndices); the server + // resolves it for the call only. The array names the RIGHT quad's vertices directly. + TEST_F(IndexedDrawFamilyScenario, AClientIndexArrayIsStagedAndDrawsTheQuadItNames) { + if (!Ready() || IsSkipped()) return; + static const std::uint16_t kClientIndices[6] = {4, 5, 6, 6, 7, 4}; + const Image image = ClearThenDrawThenRead([] { + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); // the VAO's element slot, emptied + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, kClientIndices); + }); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo); + EXPECT_EQ(FirstGLError(), 0u); + ExpectRight(image, "green", "glDrawElements from a client index array"); + ExpectLeft(image, "blue", "a client index array naming vertices 4..7 must not paint the left quad"); + ExpectCorner(image, "blue", "client index array"); + } + + // The same six indices as the left quad, plus a base vertex of 4: IndexBias is what moves + // the picture to the right quad. + TEST_F(IndexedDrawFamilyScenario, DrawElementsBaseVertexMovesTheSameIndicesToTheOtherQuad) { + if (!Ready() || IsSkipped()) return; + const Image image = ClearThenDrawThenRead([] { + glDrawElementsBaseVertex(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, nullptr, 4); + }); + EXPECT_EQ(FirstGLError(), 0u); + ExpectRight(image, "green", "glDrawElementsBaseVertex(basevertex = 4)"); + ExpectLeft(image, "blue", "a base vertex of 4 must not paint the left quad"); + } + + // kDrawHasIndexRange: the ranged form with the right quad's index range and byte offset. + TEST_F(IndexedDrawFamilyScenario, DrawRangeElementsDrawsTheRangedRun) { + if (!Ready() || IsSkipped()) return; + const Image image = ClearThenDrawThenRead([] { + glDrawRangeElements(GL_TRIANGLES, 4, 7, 6, GL_UNSIGNED_SHORT, + reinterpret_cast(kRightRunByteOffset)); + }); + EXPECT_EQ(FirstGLError(), 0u); + ExpectRight(image, "green", "glDrawRangeElements(4..7) at byte offset 12"); + ExpectLeft(image, "blue", "glDrawRangeElements must not paint the left quad"); + } + + // The Minecraft trace's own entry point (improved-transparency-minecraft-26.3 first-stops at + // DrawElementsInstancedBaseVertex). Two instances of the LEFT quad: gl_InstanceID shifts the + // second onto the right box, so an InstanceCount that did not cross paints one quad. + TEST_F(IndexedDrawFamilyScenario, DrawElementsInstancedBaseVertexPaintsOneQuadPerInstance) { + if (!Ready() || IsSkipped()) return; + const Image two = ClearThenDrawThenRead([] { + glDrawElementsInstancedBaseVertex(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, nullptr, 2, 0); + }); + EXPECT_EQ(FirstGLError(), 0u); + ExpectLeft(two, "green", "instance 0 of the left quad"); + ExpectRight(two, "green", "instance 1 of the left quad, shifted by gl_InstanceID"); + ExpectCorner(two, "blue", "instanced draw"); + + const Image one = ClearThenDrawThenRead([] { + glDrawElementsInstancedBaseVertex(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, nullptr, 1, 0); + }); + ExpectLeft(one, "green", "one instance"); + ExpectRight(one, "blue", "one instance must not paint the right quad"); + } + + // NumDraws = 2 with a per-range base vertex (the census's MultiDrawElementsBaseVertex, 18 + // entries): both quads from the same six indices. + TEST_F(IndexedDrawFamilyScenario, MultiDrawElementsBaseVertexPaintsEverySubDraw) { + if (!Ready() || IsSkipped()) return; + const Image image = ClearThenDrawThenRead([] { + const GLsizei counts[2] = {6, 6}; + const void* offsets[2] = {nullptr, nullptr}; + const GLint baseVertices[2] = {0, 4}; + glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_SHORT, offsets, 2, baseVertices); + }); + EXPECT_EQ(FirstGLError(), 0u); + ExpectLeft(image, "green", "sub-draw 0 (base vertex 0)"); + ExpectRight(image, "green", "sub-draw 1 (base vertex 4)"); + ExpectCorner(image, "blue", "multi-draw"); + } + + // NumDraws = 2, arrays: the six-vertex copies of both quads. + TEST_F(IndexedDrawFamilyScenario, MultiDrawArraysPaintsEverySubDraw) { + if (!Ready() || IsSkipped()) return; + const Image image = ClearThenDrawThenRead([] { + const GLint firsts[2] = {8, 14}; + const GLsizei counts[2] = {6, 6}; + glMultiDrawArrays(GL_TRIANGLES, firsts, counts, 2); + }); + EXPECT_EQ(FirstGLError(), 0u); + ExpectLeft(image, "green", "sub-draw 0 (first 8)"); + ExpectRight(image, "green", "sub-draw 1 (first 14)"); + ExpectCorner(image, "blue", "multi-draw arrays"); + } + + // kDrawIsIndirect: two DrawElementsIndirectCommands in a GL_DRAW_INDIRECT_BUFFER, the second + // at firstIndex 6. The record carries the buffer's handle, the byte offset and the count; + // the server reads the commands from ITS copy of the buffer and never from a host pointer. + TEST_F(IndexedDrawFamilyScenario, MultiDrawElementsIndirectDrawsFromTheIndirectBuffer) { + if (!Ready() || IsSkipped()) return; + const DrawElementsIndirectCommand commands[2] = {{6, 1, 0, 0, 0}, {6, 1, 6, 0, 0}}; + glGenBuffers(1, &m_indirect); + glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_indirect); + glBufferData(GL_DRAW_INDIRECT_BUFFER, GLsizeiptr(sizeof(commands)), commands, GL_STATIC_DRAW); + ASSERT_EQ(FirstGLError(), 0u) << "building the indirect buffer"; + const Image both = ClearThenDrawThenRead([] { + glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_SHORT, nullptr, 2, 0); + }); + EXPECT_EQ(FirstGLError(), 0u); + ExpectLeft(both, "green", "indirect command 0"); + ExpectRight(both, "green", "indirect command 1 (firstIndex 6)"); + ExpectCorner(both, "blue", "indirect draw"); + + // The second command alone, by byte offset: Offset on the wire is the call's own. + const Image second = ClearThenDrawThenRead([] { + glDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_SHORT, + reinterpret_cast(sizeof(DrawElementsIndirectCommand))); + }); + EXPECT_EQ(FirstGLError(), 0u); + ExpectRight(second, "green", "glDrawElementsIndirect at byte offset 20"); + ExpectLeft(second, "blue", "the second command alone must not paint the left quad"); + } + +} // namespace MGITest diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index bc0be7d4..b9d83787 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -689,6 +689,21 @@ namespace MobileGL::MG_Remote::Client { Uint64 ClientSession::EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes, const void* varTail, Uint64 varTailBytes, void* replyOut, Uint64 replyBytes, Int32* statusOut, Uint64* replySizeOut) { + // One tail is the two-tail form with one entry (P5b d1). Nothing is duplicated: the + // barrier policy below has exactly one body, and the encoder's one-tail EncodeRecord is + // itself defined as the tails form with tailCount <= 1. + const Wire::WireTail tail{varTail, varTailBytes}; + return EmitAndWaitTails(op, payload, payloadBytes, varTail != nullptr ? &tail : nullptr, + varTail != nullptr ? 1u : 0u, replyOut, replyBytes, statusOut, + replySizeOut); + } + + Uint64 ClientSession::EmitAndWaitTails(MG_Pipe::MGPWireOp op, const void* payload, + Uint64 payloadBytes, const Wire::WireTail* tails, + Uint32 tailCount, void* replyOut, Uint64 replyBytes, + Int32* statusOut, Uint64* replySizeOut) { + Uint64 varTailBytes = 0; + for (Uint32 i = 0; i < tailCount; ++i) varTailBytes += tails[i].Size; if (statusOut != nullptr) *statusOut = Wire::ReplySink::kStatusError; if (replySizeOut != nullptr) *replySizeOut = 0; if (!m_started) { @@ -713,8 +728,7 @@ namespace MobileGL::MG_Remote::Client { std::abort(); } - const Uint64 seq = - m_encoder.EncodeRecord(op, payload, payloadBytes, varTail, varTailBytes); + const Uint64 seq = m_encoder.EncodeRecord(op, payload, payloadBytes, tails, tailCount); if (seq == Wire::kInvalidSeq) { // The ring refused it. NOT a silent drop and not a retry loop: R-10 says P5 does no // chunking and must prove it needs none, so a refusal is the proof failing. diff --git a/MobileGL/MG_Remote/Client/ClientSession.h b/MobileGL/MG_Remote/Client/ClientSession.h index be165629..d6539327 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.h +++ b/MobileGL/MG_Remote/Client/ClientSession.h @@ -108,6 +108,15 @@ namespace MobileGL::MG_Remote::Client { const void* varTail, Uint64 varTailBytes, void* replyOut, Uint64 replyBytes, Int32* statusOut, Uint64* replySizeOut = nullptr); + // The same call for a record that carries TWO tails (P5b d1: draw_vbo's user-index span + // or its MGPDrawIndirect block behind the range array). The one-tail form above is this + // one with a single WireTail; the barrier policy lives in exactly one body. A distinct + // name rather than an overload, because `nullptr, 0` would match both. + Uint64 EmitAndWaitTails(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes, + const Wire::WireTail* tails, Uint32 tailCount, void* replyOut, + Uint64 replyBytes, Int32* statusOut, + Uint64* replySizeOut = nullptr); + // MOBILEGL_IPC_VERB_BARRIER. False is the R-1 negative control and is EXPECTED to be // red; it must be run once and the way it goes red recorded. Bool BarrierArmed() const; diff --git a/MobileGL/MG_Remote/Client/EmitTables.cpp b/MobileGL/MG_Remote/Client/EmitTables.cpp index a0cf7b3a..809a7948 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.cpp +++ b/MobileGL/MG_Remote/Client/EmitTables.cpp @@ -10,8 +10,8 @@ // // THE PARTITION IS CONTRACT-P5.md §7's AND IS NOT RE-DERIVED HERE (R-15, ID-12): // class A 2 slots answered locally from the caps mirror, never emitted, never Fatal -// class B 5 slots emitted -// class C 64 slots Fatal{UnmigratedVerb, ""} +// class B 24 slots emitted (P5's five; P5b d1's nineteen draw slots, all on draw_vbo) +// class C 45 slots Fatal{UnmigratedVerb, ""} // The three counts are static_asserted to sum to kRemoteEmitSlotCount below, so a slot that // changes class without changing the arithmetic is a build break rather than a behaviour // change nobody reviewed. @@ -35,7 +35,14 @@ #include #include +#include +// P5b d1: the handle a bound buffer already has (never minted here - the validate-time +// set_index_buffer / the buffer's own constructor did that), for MGPDrawInfo::IndexResource and +// the two indirect-buffer handles. +#include + +#include #include #include @@ -272,6 +279,303 @@ namespace MobileGL::MG_Remote::Client { sizeof(range), nullptr, 0, nullptr); } + // ============================================================================= + // P5b d1 - the nineteen indexed / instanced / multi-draw / indirect draw slots + // (MG_Remote/CONTRACT-P5B.md §2 d1). ONE ROW, draw_vbo (59): the record carries the GL + // call verbatim (rule D) beside the handle the P8 form will dispatch on, and the sink + // reproduces the backend call the monolith makes. + // ============================================================================= + // + // Every entry point below is: read the bindings, plan the head and the ranges (the pure + // functions the unit cases drive), then EmitDrawRecord - which runs the SAME pre-verb + // hooks in the SAME order as EmitDrawArrays (push, then mark walk, then the record; + // ID-18), honours the E2 draw-drop control for every draw record and not only the P5 + // one, stages a client index array into SEG_STAGE when there is one, and emits. + // + // WHAT IS REFUSED BY NAME, ID-57's shape (Fatal{UnmigratedVerb, "+"}), + // never rendered wrong and never fallen through to the driver (R-4): + // +CLIENT_INDICES a glMultiDrawElements* with no element buffer bound: `indices[i]` + // are drawcount separate client pointers and one span names one run; + // P8's HostResolve.cpp flattens it. No measured workload has one. + // +CLIENT_COMMANDS an indirect draw with no GL_DRAW_INDIRECT_BUFFER bound: `indirect` + // would be a host pointer, which rule B forbids on the wire. + // +UNBOUND_PARAMETER an *IndirectCount with no GL_PARAMETER_BUFFER (the frontend has + // already raised INVALID_OPERATION for it; stated so the emitter + // cannot send a null handle where the sink dereferences one). + // +INDEX_OFFSET an element-buffer byte offset that is not a whole number of + // indices (or past 2^32 of them): the record spells Start in + // indices, and rounding would draw from the wrong element. + + [[noreturn]] void RefuseDrawByName(const char* slot, const char* qualifier) { + char name[96]; + std::snprintf(name, sizeof(name), "%s+%s", slot, qualifier); + UnmigratedVerbFatal(name); + } + + // The bindings the plan is made from, read ONCE per draw from the frontend context on + // the GL thread. The element buffer is the VAO's (the same slot EmitIndexBuffer read at + // validate), the two indirect buffers are the context's, and the handles are LOOKED UP, + // never minted: a push build mints in the BufferObject constructor. + RemoteDrawBindings ReadDrawBindings() { + RemoteDrawBindings b{}; + MG_State::GLState::GLContext* ctx = MG_State::pGLContext.get(); + if (ctx == nullptr) return b; + if (const auto& vao = ctx->GetBoundVertexArray()) { + if (const auto& bound = vao->GetIndexBufferBindingSlot().GetBoundObject()) { + b.ElementBufferBound = true; + b.ElementBuffer = MG_Pipe::MGPipeResourceTrackerInstance().Find(*bound); + } + } + if (const auto& di = ctx->GetBufferBindingSlot(::MobileGL::BufferTarget::DrawIndirect).GetBoundObject()) { + b.DrawIndirectBuffer = MG_Pipe::MGPipeResourceTrackerInstance().Find(*di); + } + if (const auto& pb = ctx->GetBufferBindingSlot(::MobileGL::BufferTarget::Parameter).GetBoundObject()) { + b.ParameterBuffer = MG_Pipe::MGPipeResourceTrackerInstance().Find(*pb); + } + b.PrimitiveRestart = ctx->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) || + ctx->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex); + b.RestartIndex = ctx->GetPrimitiveRestartIndex(); + return b; + } + + // The one emission for all nineteen. `clientIndices`/`clientIndexBytes` name a client + // index array to stage (no element buffer bound); `indirect` is the kDrawIsIndirect + // block. The two are exclusive by construction here and by the layout on both sides. + void EmitDrawRecord(const char* slot, MG_Pipe::MGPDrawInfo& info, + const MG_Pipe::MGPDrawRange* ranges, Uint32 numDraws, + const void* clientIndices, Uint64 clientIndexBytes, + const MG_Pipe::MGPDrawIndirect* indirect) { + ClientSession& session = RequireSession(slot); + BeforeDrawVerb(); + + if (g_dropDrawEmission) { + // E2's negative control covers EVERY draw record, not only DrawArrays: the + // Minecraft traces are DrawElements frames, and a control that dropped only the + // one P5 entry point would leave those lanes green with the wire disarmed. + ++g_droppedDrawEmissions; + return; + } + + info.NumDraws = numDraws; + Wire::WireTail tails[2] = {{ranges, static_cast(numDraws) * sizeof(MG_Pipe::MGPDrawRange)}, + {nullptr, 0}}; + Uint32 tailCount = 1; + MG_Pipe::MGHostSpan span{}; + if (clientIndices != nullptr && clientIndexBytes != 0) { + // The P8 resolve-on-client rule, applied: the bytes exist on the client only, + // so the client stages them whole and the record names the run - Ptr = nullptr, + // Seg = SEG_STAGE (rule B). The encoder runs all four honesty arms on the span + // before it is published, and the sink resolves it through MGPipeHostBytes. + const MG_Pipe::MGPBlobRef staged = + session.Encoder().StageBytes(clientIndices, clientIndexBytes); + span.Ptr = nullptr; + span.Seg = staged.Seg; + span.Offset = staged.Offset; + span.Size = staged.Size; + info.Flags |= MG_Pipe::kDrawHasUserIndices; + tails[1] = {&span, sizeof(span)}; + tailCount = 2; + } else if (indirect != nullptr) { + info.Flags |= MG_Pipe::kDrawIsIndirect; + tails[1] = {indirect, sizeof(*indirect)}; + tailCount = 2; + } + session.EmitAndWaitTails(MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info), tails, + tailCount, nullptr, 0, nullptr); + } + + // ---- the single-draw indexed family: DrawElements, DrawElementsBaseVertex, the two + // DrawRangeElements*, the four DrawElementsInstanced* ------------------------------- + void EmitIndexedDraw(const char* slot, GLenum mode, GLsizei count, GLenum type, + const void* indices, GLint baseVertex, GLsizei instanceCount, + GLuint baseInstance, Bool hasRange, GLuint start, GLuint end) { + const RemoteDrawBindings bindings = ReadDrawBindings(); + const Uint8 indexSize = RemoteIndexSizeFor(type); + if (indexSize == 0) RefuseDrawByName(slot, "INDEX_TYPE"); + MG_Pipe::MGPDrawInfo info = + PlanDrawInfo(mode, indexSize, instanceCount, baseInstance, 1, bindings); + if (hasRange) { + info.Flags |= MG_Pipe::kDrawHasIndexRange; + info.MinIndex = start; + info.MaxIndex = end; + } + MG_Pipe::MGPDrawRange range{}; + if (!PlanDrawRange(bindings, indexSize, indices, count, baseVertex, range)) { + RefuseDrawByName(slot, "INDEX_OFFSET"); + } + // No element buffer: `indices` is the application's array and the draw's own + // range is exactly what the driver would read. A null pointer or an empty draw + // stages nothing and crosses as a zero-length range, which is what the monolith + // hands the driver too. + const Bool clientArray = !bindings.ElementBufferBound && indices != nullptr && count > 0; + EmitDrawRecord(slot, info, &range, 1, clientArray ? indices : nullptr, + clientArray ? static_cast(count) * indexSize : 0, nullptr); + } + + void EmitDrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) { + EmitIndexedDraw("DrawElements", mode, count, type, indices, 0, 1, 0, false, 0, 0); + } + void EmitDrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, + GLint basevertex) { + EmitIndexedDraw("DrawElementsBaseVertex", mode, count, type, indices, basevertex, 1, 0, + false, 0, 0); + } + void EmitDrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, + const void* indices) { + EmitIndexedDraw("DrawRangeElements", mode, count, type, indices, 0, 1, 0, true, start, end); + } + void EmitDrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, + GLenum type, const void* indices, GLint basevertex) { + EmitIndexedDraw("DrawRangeElementsBaseVertex", mode, count, type, indices, basevertex, 1, + 0, true, start, end); + } + void EmitDrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, + GLsizei instancecount) { + EmitIndexedDraw("DrawElementsInstanced", mode, count, type, indices, 0, instancecount, 0, + false, 0, 0); + } + void EmitDrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, + const void* indices, GLsizei instancecount, + GLint basevertex) { + EmitIndexedDraw("DrawElementsInstancedBaseVertex", mode, count, type, indices, basevertex, + instancecount, 0, false, 0, 0); + } + void EmitDrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, + const void* indices, GLsizei instancecount, + GLuint baseinstance) { + EmitIndexedDraw("DrawElementsInstancedBaseInstance", mode, count, type, indices, 0, + instancecount, baseinstance, false, 0, 0); + } + void EmitDrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, + const void* indices, GLsizei instancecount, + GLint basevertex, GLuint baseinstance) { + EmitIndexedDraw("DrawElementsInstancedBaseVertexBaseInstance", mode, count, type, indices, + basevertex, instancecount, baseinstance, false, 0, 0); + } + + // ---- the instanced array draws ------------------------------------------------ + void EmitArraysDraw(const char* slot, GLenum mode, GLint first, GLsizei count, + GLsizei instanceCount, GLuint baseInstance) { + const RemoteDrawBindings bindings = ReadDrawBindings(); + MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, 0, instanceCount, baseInstance, 1, bindings); + MG_Pipe::MGPDrawRange range{}; + PlanDrawRange(bindings, 0, reinterpret_cast(static_cast(first)), + count, 0, range); + EmitDrawRecord(slot, info, &range, 1, nullptr, 0, nullptr); + } + void EmitDrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) { + EmitArraysDraw("DrawArraysInstanced", mode, first, count, instancecount, 0); + } + void EmitDrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, + GLsizei instancecount, GLuint baseinstance) { + // The vertex-FETCH base instance already crossed in set_vertex_buffers::BaseInstance + // at validate (D-H1: MGP_SET_BASE_INSTANCE runs before MGP_FILL); StartInstance is + // gl_BaseInstance's value and feeds the GL call. + EmitArraysDraw("DrawArraysInstancedBaseInstance", mode, first, count, instancecount, + baseinstance); + } + + // ---- the multi-draws: MGPDrawRange[drawcount] is exactly their shape ------------- + // + // The range array is built in a scratch vector owned by the GL thread (the emitter is + // single-threaded by the ring's own SPSC contract), sized by drawcount, never held past + // the emission. A drawcount of 0 is a call that draws nothing and emits nothing: the + // frontend has already refused a negative one. + Vector& MultiDrawScratch(Uint32 count) { + static Vector& scratch = *new Vector(); + scratch.resize(count); + return scratch; + } + + void EmitMultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) { + if (drawcount <= 0 || first == nullptr || count == nullptr) return; + const RemoteDrawBindings bindings = ReadDrawBindings(); + const auto n = static_cast(drawcount); + MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, 0, 1, 0, n, bindings); + Vector& ranges = MultiDrawScratch(n); + for (Uint32 i = 0; i < n; ++i) { + PlanDrawRange(bindings, 0, + reinterpret_cast(static_cast(first[i])), + count[i], 0, ranges[i]); + } + EmitDrawRecord("MultiDrawArrays", info, ranges.data(), n, nullptr, 0, nullptr); + } + + void EmitMultiIndexedDraw(const char* slot, GLenum mode, const GLsizei* count, GLenum type, + const GLvoid* const* indices, GLsizei drawcount, + const GLint* basevertex) { + if (drawcount <= 0 || count == nullptr || indices == nullptr) return; + const RemoteDrawBindings bindings = ReadDrawBindings(); + const Uint8 indexSize = RemoteIndexSizeFor(type); + if (indexSize == 0) RefuseDrawByName(slot, "INDEX_TYPE"); + if (!bindings.ElementBufferBound) RefuseDrawByName(slot, "CLIENT_INDICES"); + const auto n = static_cast(drawcount); + MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, indexSize, 1, 0, n, bindings); + Vector& ranges = MultiDrawScratch(n); + for (Uint32 i = 0; i < n; ++i) { + if (!PlanDrawRange(bindings, indexSize, indices[i], count[i], + basevertex != nullptr ? basevertex[i] : 0, ranges[i])) { + RefuseDrawByName(slot, "INDEX_OFFSET"); + } + } + EmitDrawRecord(slot, info, ranges.data(), n, nullptr, 0, nullptr); + } + void EmitMultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, + const GLvoid* const* indices, GLsizei drawcount) { + EmitMultiIndexedDraw("MultiDrawElements", mode, count, type, indices, drawcount, nullptr); + } + void EmitMultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, + const GLvoid* const* indices, GLsizei drawcount, + const GLint* basevertex) { + EmitMultiIndexedDraw("MultiDrawElementsBaseVertex", mode, count, type, indices, drawcount, + basevertex); + } + + // ---- the indirect family: the block is the second tail, NumDraws is 0 ------------- + void EmitIndirectDraw(const char* slot, GLenum mode, GLenum type, const void* indirect, + GLsizei drawcount, GLsizei stride, GLintptr parameterOffset, + Bool hasParameterBuffer) { + const RemoteDrawBindings bindings = ReadDrawBindings(); + const Uint8 indexSize = type != 0 ? RemoteIndexSizeFor(type) : 0; + if (type != 0 && indexSize == 0) RefuseDrawByName(slot, "INDEX_TYPE"); + if (MG_Pipe::MGPipeHandleIsNull(bindings.DrawIndirectBuffer)) { + RefuseDrawByName(slot, "CLIENT_COMMANDS"); + } + if (hasParameterBuffer && MG_Pipe::MGPipeHandleIsNull(bindings.ParameterBuffer)) { + RefuseDrawByName(slot, "UNBOUND_PARAMETER"); + } + MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, indexSize, 1, 0, 0, bindings); + const MG_Pipe::MGPDrawIndirect block = PlanDrawIndirect(bindings, indirect, drawcount, stride, + parameterOffset, hasParameterBuffer); + EmitDrawRecord(slot, info, nullptr, 0, nullptr, 0, &block); + } + void EmitDrawArraysIndirect(GLenum mode, const void* indirect) { + EmitIndirectDraw("DrawArraysIndirect", mode, 0, indirect, 1, 0, 0, false); + } + void EmitDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) { + EmitIndirectDraw("DrawElementsIndirect", mode, type, indirect, 1, 0, 0, false); + } + void EmitMultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, + GLsizei stride) { + EmitIndirectDraw("MultiDrawArraysIndirect", mode, 0, indirect, drawcount, stride, 0, false); + } + void EmitMultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, + GLsizei drawcount, GLsizei stride) { + EmitIndirectDraw("MultiDrawElementsIndirect", mode, type, indirect, drawcount, stride, 0, + false); + } + void EmitMultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, + GLsizei maxdrawcount, GLsizei stride) { + EmitIndirectDraw("MultiDrawArraysIndirectCount", mode, 0, indirect, maxdrawcount, stride, + drawcount, true); + } + void EmitMultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, + GLintptr drawcount, GLsizei maxdrawcount, + GLsizei stride) { + EmitIndirectDraw("MultiDrawElementsIndirectCount", mode, type, indirect, maxdrawcount, + stride, drawcount, true); + } + void EmitBlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { @@ -585,7 +889,8 @@ namespace MobileGL::MG_Remote::Client { } // ============================================================================= - // CLASS C - Fatal{UnmigratedVerb}. 64 slots: 63 in GLFunctionsTable + SetSwapInterval. + // CLASS C - Fatal{UnmigratedVerb}. 45 slots on this head: 44 in GLFunctionsTable + + // SetSwapInterval (64 at the P5b contract commit; d1 v1 moved its 19 to class B). // ============================================================================= // // PARTITIONED BY THE P5b PACKAGE THAT OWNS THE FLIP (MG_Remote/CONTRACT-P5B.md, @@ -611,30 +916,14 @@ namespace MobileGL::MG_Remote::Client { // tail the wave-3 remainder nothing measured: queries, syncs, the texture readbacks, // the DSA blit, the swap interval (census-classC.md "static cross") -#define MGR_UNMIGRATED_D1_SLOTS(X) \ - X(DrawElements, void, (GLenum, GLsizei, GLenum, const void*)) \ - X(DrawElementsBaseVertex, void, (GLenum, GLsizei, GLenum, const void*, GLint)) \ - X(MultiDrawArrays, void, (GLenum, const GLint*, const GLsizei*, GLsizei)) \ - X(MultiDrawElements, void, (GLenum, const GLsizei*, GLenum, const GLvoid* const*, GLsizei)) \ - X(MultiDrawElementsBaseVertex, void, \ - (GLenum, const GLsizei*, GLenum, const GLvoid* const*, GLsizei, const GLint*)) \ - X(MultiDrawElementsIndirect, void, (GLenum, GLenum, const void*, GLsizei, GLsizei)) \ - X(MultiDrawArraysIndirect, void, (GLenum, const void*, GLsizei, GLsizei)) \ - X(MultiDrawElementsIndirectCount, void, (GLenum, GLenum, const void*, GLintptr, GLsizei, GLsizei)) \ - X(MultiDrawArraysIndirectCount, void, (GLenum, const void*, GLintptr, GLsizei, GLsizei)) \ - X(DrawRangeElementsBaseVertex, void, \ - (GLenum, GLuint, GLuint, GLsizei, GLenum, const void*, GLint)) \ - X(DrawRangeElements, void, (GLenum, GLuint, GLuint, GLsizei, GLenum, const void*)) \ - X(DrawElementsInstancedBaseVertexBaseInstance, void, \ - (GLenum, GLsizei, GLenum, const void*, GLsizei, GLint, GLuint)) \ - X(DrawElementsInstancedBaseVertex, void, (GLenum, GLsizei, GLenum, const void*, GLsizei, GLint)) \ - X(DrawElementsInstancedBaseInstance, void, \ - (GLenum, GLsizei, GLenum, const void*, GLsizei, GLuint)) \ - X(DrawElementsInstanced, void, (GLenum, GLsizei, GLenum, const void*, GLsizei)) \ - X(DrawArraysInstancedBaseInstance, void, (GLenum, GLint, GLsizei, GLsizei, GLuint)) \ - X(DrawArraysInstanced, void, (GLenum, GLint, GLsizei, GLsizei)) \ - X(DrawElementsIndirect, void, (GLenum, GLenum, const void*)) \ - X(DrawArraysIndirect, void, (GLenum, const void*)) + // d1 v1 flipped all nineteen (DrawElements, DrawElementsBaseVertex, MultiDrawArrays, + // MultiDrawElements, MultiDrawElementsBaseVertex, MultiDrawElementsIndirect, + // MultiDrawArraysIndirect, MultiDrawElementsIndirectCount, MultiDrawArraysIndirectCount, + // DrawRangeElementsBaseVertex, DrawRangeElements, the four DrawElementsInstanced*, + // DrawArraysInstancedBaseInstance, DrawArraysInstanced, DrawElementsIndirect, + // DrawArraysIndirect) to class B; the list is kept, empty, so the ownership assertion + // below still reads 0 + 19 = 19 and a slot that fell back in would have to be added here. +#define MGR_UNMIGRATED_D1_SLOTS(X) // DispatchCompute and DispatchComputeIndirect are i1's too; they are hand-written below // because they carry b1's dispatch hook before the Fatal. @@ -761,7 +1050,7 @@ namespace MobileGL::MG_Remote::Client { // The emitted counts, PER OWNER. P5's five are c1's; each P5b package raises its own. constexpr Uint32 kEmittedSlotsP5 = 5; // Clear, DrawArrays, ReadPixels, Blit, Present - constexpr Uint32 kEmittedSlotsD1 = 0; + constexpr Uint32 kEmittedSlotsD1 = 19; // the draw family, all on draw_vbo (d1 v1) constexpr Uint32 kEmittedSlotsI1 = 0; constexpr Uint32 kEmittedSlotsT2 = 0; constexpr Uint32 kEmittedSlotsF1 = 0; @@ -778,7 +1067,12 @@ namespace MobileGL::MG_Remote::Client { static_assert(kUnmigratedT2 + kEmittedSlotsT2 == 7, "t2 owns the 7 XFB/tessellation slots"); static_assert(kUnmigratedF1 + kEmittedSlotsF1 == 11, "f1 owns the 11 clear/copy/mip slots"); static_assert(kUnmigratedTail == 20, "the wave-3 tail is 20 slots and no P5b package owns one"); - static_assert(kUnmigratedSlots == 64, "CONTRACT-P5.md §7 class C is 64 slots at the P5b contract commit"); + // 64 at the P5b contract commit; every P5b flip moves exactly one slot from C to B, so + // the SUM is what stays pinned, and a package's flip changes its own count and nothing + // else on this line. + static_assert(kUnmigratedSlots + (kEmittedSlots - kEmittedSlotsP5) == 64, + "CONTRACT-P5.md §7 class C was 64 slots at the P5b contract commit; a P5b flip " + "moves one slot from C to B and the two counts must still sum to 64"); static_assert(kLocallyAnsweredSlots + kEmittedSlots + kUnmigratedSlots == kRemoteEmitSlotCount, "the three classes no longer partition the 71 slots"); @@ -812,6 +1106,27 @@ namespace MobileGL::MG_Remote::Client { // ---- class B table.GL.Clear = &EmitClear; table.GL.DrawArrays = &EmitDrawArrays; + // ---- P5b d1: the nineteen draw slots, all on draw_vbo (CONTRACT-P5B.md §2 d1) + table.GL.DrawElements = &EmitDrawElements; + table.GL.DrawElementsBaseVertex = &EmitDrawElementsBaseVertex; + table.GL.DrawRangeElements = &EmitDrawRangeElements; + table.GL.DrawRangeElementsBaseVertex = &EmitDrawRangeElementsBaseVertex; + table.GL.DrawElementsInstanced = &EmitDrawElementsInstanced; + table.GL.DrawElementsInstancedBaseVertex = &EmitDrawElementsInstancedBaseVertex; + table.GL.DrawElementsInstancedBaseInstance = &EmitDrawElementsInstancedBaseInstance; + table.GL.DrawElementsInstancedBaseVertexBaseInstance = + &EmitDrawElementsInstancedBaseVertexBaseInstance; + table.GL.DrawArraysInstanced = &EmitDrawArraysInstanced; + table.GL.DrawArraysInstancedBaseInstance = &EmitDrawArraysInstancedBaseInstance; + table.GL.MultiDrawArrays = &EmitMultiDrawArrays; + table.GL.MultiDrawElements = &EmitMultiDrawElements; + table.GL.MultiDrawElementsBaseVertex = &EmitMultiDrawElementsBaseVertex; + table.GL.DrawArraysIndirect = &EmitDrawArraysIndirect; + table.GL.DrawElementsIndirect = &EmitDrawElementsIndirect; + table.GL.MultiDrawArraysIndirect = &EmitMultiDrawArraysIndirect; + table.GL.MultiDrawElementsIndirect = &EmitMultiDrawElementsIndirect; + table.GL.MultiDrawArraysIndirectCount = &EmitMultiDrawArraysIndirectCount; + table.GL.MultiDrawElementsIndirectCount = &EmitMultiDrawElementsIndirectCount; table.GL.ReadPixels = &EmitReadPixels; table.GL.BlitFramebuffer = &EmitBlitFramebuffer; table.Present = &EmitPresent; @@ -890,4 +1205,95 @@ namespace MobileGL::MG_Remote::Client { } } + // ============================================================================= + // P5b d1 - the draw family's record plan, at namespace scope so the unit cases drive + // exactly what the emitters above call (R-16). + // ============================================================================= + + Uint8 RemoteIndexSizeFor(GLenum indexType) { + switch (indexType) { + case GL_UNSIGNED_BYTE: return 1; + case GL_UNSIGNED_SHORT: return 2; + case GL_UNSIGNED_INT: return 4; + default: return 0; + } + } + + MG_Pipe::MGPDrawInfo PlanDrawInfo(GLenum mode, Uint8 indexSize, GLsizei instanceCount, + GLuint baseInstance, Uint32 numDraws, + const RemoteDrawBindings& bindings) { + MG_Pipe::MGPDrawInfo info{}; + info.Mode = static_cast(mode); + info.IndexSize = indexSize; + // The restart state rides verbatim (informational in P5b: the backend's + // ScopedRestartIndexSubstitution reads its own barrier-pulled copy and the index bytes + // on its side). kDrawHasUserIndices / kDrawIsIndirect are added by the emission itself, + // kDrawHasIndexRange by the two DrawRangeElements* callers. + info.Flags = bindings.PrimitiveRestart ? static_cast(MG_Pipe::kDrawPrimitiveRestart) : 0; + // A negative count has already been refused by the frontend (INVALID_VALUE); 0 crosses + // as 0 and draws nothing, which is what the driver does with it. + info.InstanceCount = instanceCount > 0 ? static_cast(instanceCount) : 0u; + info.StartInstance = baseInstance; + info.RestartIndex = bindings.RestartIndex; + info.DrawIdOffset = 0; + // The handle beside the call (rule D): the VAO's element buffer for an indexed draw, + // the same handle set_index_buffer (32) carried at validate; null for arrays and for a + // client index array. + info.IndexResource = (indexSize != 0 && bindings.ElementBufferBound) ? bindings.ElementBuffer + : MG_Pipe::kMGPipeNullHandle; + info.MinIndex = ~0u; // "unknown" (MGPipeTypes.h); the DrawRangeElements* callers fill it + info.MaxIndex = ~0u; + info.XfbCpuCapturedVertices = 0; + info.NumDraws = numDraws; + return info; + } + + Bool PlanDrawRange(const RemoteDrawBindings& bindings, Uint8 indexSize, const void* indicesOrFirst, + GLsizei count, GLint baseVertex, MG_Pipe::MGPDrawRange& out) { + out = MG_Pipe::MGPDrawRange{}; + out.Count = count > 0 ? static_cast(count) : 0u; + if (indexSize == 0) { + // Arrays: `first`, spelled through the pointer parameter so one function serves + // both shapes. A negative first has already been refused by the frontend. + const auto first = static_cast(reinterpret_cast(indicesOrFirst)); + out.Start = first > 0 ? static_cast(first) : 0u; + out.IndexBias = 0; + return true; + } + out.IndexBias = baseVertex; + if (!bindings.ElementBufferBound) { + // A client index array: the emitter stages the bytes and the run starts at its + // first index. + out.Start = 0; + return true; + } + // An element buffer: `indices` is a byte offset into it, and Start is that offset in + // INDICES - the same arithmetic OnDrawVbo inverts (offset = Start * IndexSize). An + // offset that is not a whole number of indices cannot be spelled and is the caller's + // refusal, not a rounding. + const auto offset = reinterpret_cast(indicesOrFirst); + if (offset % indexSize != 0) return false; + const std::uintptr_t start = offset / indexSize; + if (start > 0xFFFFFFFFull) return false; + out.Start = static_cast(start); + return true; + } + + MG_Pipe::MGPDrawIndirect PlanDrawIndirect(const RemoteDrawBindings& bindings, const void* indirect, + GLsizei drawCount, GLsizei stride, + GLintptr parameterOffset, Bool hasParameterBuffer) { + MG_Pipe::MGPDrawIndirect block{}; + block.Buffer = bindings.DrawIndirectBuffer; + block.ParameterBuffer = hasParameterBuffer ? bindings.ParameterBuffer : MG_Pipe::kMGPipeNullHandle; + // The command byte offset the call passed as `indirect` (a bound GL_DRAW_INDIRECT_BUFFER + // makes it an offset, never an address - the emitter refused the other case by name). + block.Offset = static_cast(reinterpret_cast(indirect)); + block.ParameterOffset = hasParameterBuffer ? static_cast(parameterOffset) : 0u; + // 0 = tightly packed, as GL spells it; the backend normalises. Negative counts and + // strides have already been refused by the frontend. + block.Stride = stride > 0 ? static_cast(stride) : 0u; + block.DrawCount = drawCount > 0 ? static_cast(drawCount) : 0u; + return block; + } + } // namespace MobileGL::MG_Remote::Client diff --git a/MobileGL/MG_Remote/Client/EmitTables.h b/MobileGL/MG_Remote/Client/EmitTables.h index 5c1fb536..f076c2aa 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.h +++ b/MobileGL/MG_Remote/Client/EmitTables.h @@ -161,4 +161,59 @@ namespace MobileGL::MG_Remote::Client { // 0=OK / 1=DECLINED / 2=ERROR as `status`. Bool ReadbackReplyIsComplete(Int32 status, Uint64 replySize, Uint64 expected); + // ============================================================================= + // P5b d1 - the draw family's record plan (MG_Remote/CONTRACT-P5B.md §2 d1) + // ============================================================================= + // + // The nineteen indexed / instanced / multi-draw / indirect entry points all ride draw_vbo + // (59), and the ONLY thing that differs per entry point is how the GL arguments become the + // record's fields. That derivation is split out of the emitters as three pure functions + // over a snapshot of the bindings the emitter read from the GL context, so a unit case can + // drive the PRODUCTION derivation with synthetic bindings (R-16) while the integration lane + // proves the bindings are read from the right slots. The emitters compose these and add + // nothing but the hooks, the E2 draw-drop control and the staging of a client index array. + + // What the emitter reads from the GL context before it plans a draw. + struct RemoteDrawBindings { + // The VAO's GL_ELEMENT_ARRAY_BUFFER: bound or not, and its handle when bound (the same + // handle set_index_buffer carried at validate). Not bound means `indices` is a client + // pointer and the emitter stages the bytes (kDrawHasUserIndices). + Bool ElementBufferBound = false; + MG_Pipe::MGPipeHandle ElementBuffer = MG_Pipe::kMGPipeNullHandle; + // The bound GL_DRAW_INDIRECT_BUFFER and GL_PARAMETER_BUFFER, null when unbound. + MG_Pipe::MGPipeHandle DrawIndirectBuffer = MG_Pipe::kMGPipeNullHandle; + MG_Pipe::MGPipeHandle ParameterBuffer = MG_Pipe::kMGPipeNullHandle; + // GL_PRIMITIVE_RESTART / GL_PRIMITIVE_RESTART_FIXED_INDEX and the application's index, + // carried verbatim (informational in P5b: the backend reads its own barrier-pulled copy). + Bool PrimitiveRestart = false; + Uint32 RestartIndex = 0; + }; + + // 1 / 2 / 4 for the three GL index types, 0 for anything else (the frontend has already + // refused those with INVALID_ENUM before the slot is reached). + Uint8 RemoteIndexSizeFor(GLenum indexType); + + // The fixed head. `indexSize` 0 = arrays. `instanceCount` is the call's own (1 for a + // non-instanced entry point) and `baseInstance` its gl_BaseInstance value; the sink reads + // "instanced" as InstanceCount != 1 || StartInstance != 0, so an instanced call with a + // count of 1 and no base instance is dispatched as the plain draw it is equivalent to. + MG_Pipe::MGPDrawInfo PlanDrawInfo(GLenum mode, Uint8 indexSize, GLsizei instanceCount, + GLuint baseInstance, Uint32 numDraws, + const RemoteDrawBindings& bindings); + + // One MGPDrawRange from one (first | indices, count, basevertex). Arrays: Start = first. + // Indexed with an element buffer bound: Start = offset / IndexSize, and FALSE when the byte + // offset is not a whole number of indices or does not fit the record's Uint32 Start - the + // caller refuses by name rather than round. Indexed with no element buffer: Start = 0 and + // the bytes are the client's (the emitter stages `count * IndexSize` of them). + Bool PlanDrawRange(const RemoteDrawBindings& bindings, Uint8 indexSize, const void* indicesOrFirst, + GLsizei count, GLint baseVertex, MG_Pipe::MGPDrawRange& out); + + // The kDrawIsIndirect second tail: the bound GL_DRAW_INDIRECT_BUFFER handle, the call's + // `indirect` byte offset, the stride and the draw count; for the *IndirectCount forms the + // bound GL_PARAMETER_BUFFER handle and the byte offset the call spells as `drawcount`. + MG_Pipe::MGPDrawIndirect PlanDrawIndirect(const RemoteDrawBindings& bindings, const void* indirect, + GLsizei drawCount, GLsizei stride, + GLintptr parameterOffset, Bool hasParameterBuffer); + } // namespace MobileGL::MG_Remote::Client diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index cae264b6..8d8ef3fd 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -281,65 +281,236 @@ namespace MobileGL::MG_Remote::Server { std::abort(); } + // P5b d1 (MG_Remote/CONTRACT-P5B.md §2 d1): draw_vbo's whole cross product. The record + // carries the GL call verbatim (rule D) and this body reproduces the backend call the + // monolith makes for that shape, reading only the record and the backend's own + // barrier-pulled state; the twenty GL entry points collapse onto the arms below: + // + // kDrawIsIndirect arrays: DrawArraysIndirect (DrawCount 1, Stride 0) / + // MultiDrawArraysIndirect / MultiDrawArraysIndirectCount (a parameter + // buffer named); indexed: the three Elements twins + // NumDraws != 1 MultiDrawArrays / MultiDrawElementsBaseVertex, the two arrays + // rebuilt from the ranges into bounded locals (rule C) + // arrays, one range DrawArrays / DrawArraysInstanced / DrawArraysInstancedBaseInstance + // indexed, one range DrawElementsBaseVertex (the P5 arm, unchanged, bias 0 for a plain + // DrawElements) / DrawRangeElements[BaseVertex] under + // kDrawHasIndexRange / the four DrawElementsInstanced* by whether a + // base vertex and a base instance are non-zero + // kDrawHasUserIndices the `indices` argument is the resolved SEG_STAGE run instead of an + // element-buffer offset (the client staged a client index array) + // + // "Instanced" is InstanceCount != 1 || StartInstance != 0: an instanced call with a count + // of 1 and no base instance is the plain draw it is equivalent to, and a count of 0 must + // NOT collapse onto the plain draw (it draws nothing, the plain draw would draw once). + // + // What is still refused by name (the census's own grep family): a multi-draw that arrived + // with a span (the client refuses "MultiDrawElements+CLIENT_INDICES" first; P8's + // HostResolve.cpp flattens it) and a multi-draw that claims instancing (no GL entry point + // produces one; the client never sends it). Bool ServerVerbSink::OnDrawVbo(const MG_Pipe::MGPDrawInfo& info, const MG_Pipe::MGPDrawRange* ranges, const MG_Pipe::MGHostSpan* userIndices, const MG_Pipe::MGPDrawIndirect* indirect) { + // The witness first, before the backend is consulted, so a unit process with no + // backend object still sees the wire's fields (PipeApplier.h LastDraw). + m_lastDraw = LastDrawRecord{}; + m_lastDraw.Info = info; + if (ranges != nullptr && info.NumDraws != 0) m_lastDraw.FirstRange = ranges[0]; + if (userIndices != nullptr) { + m_lastDraw.HadUserIndices = true; + m_lastDraw.UserIndexBytes = userIndices->Size; + } + if (indirect != nullptr) { + m_lastDraw.HadIndirect = true; + m_lastDraw.Indirect = *indirect; + } + ++m_drawRecords; + const MG_Backend::GlobalBackendFunctionsTable* table = Table("draw_vbo"); if (table == nullptr) return false; - // P5b d1 (CONTRACT-P5B.md): the three arms the contract gives this sink and P5 did not - // implement are DECLINED BY NAME until d1 lands them - the same names the census greps, - // so the lane's first-blocker table reads the server's gap as the slot it is. - if (indirect != nullptr) { - ServerUnmigratedVerbFatal(info.IndexSize == 0 ? "MultiDrawArraysIndirect" - : "MultiDrawElementsIndirect"); - } - if (userIndices != nullptr) { - // The span is validated and names a SEG_STAGE run the client staged (d1's rule for - // client-side index arrays); resolving it is MG_Pipe::MGPipeHostBytes and passing - // the pointer to gl.DrawElements is d1's body. Declined by name until then. - ServerUnmigratedVerbFatal("DrawElements+CLIENT_INDICES"); - } - if (ranges == nullptr || info.NumDraws == 0) return false; - - // P5 IMPLEMENTS THE TWO SHAPES ITS REDUCED PATH USES AND DECLINES THE REST BY NAME. - // draw_vbo collapses all twenty draw entry points, and picking the right one needs the - // instancing / base-vertex / base-instance / multi-draw cross product. TriangleScenario - // is a single non-instanced array draw and OpenRA's are single indexed draws from a - // bound element buffer; the rest are d1's (CONTRACT-P5B.md d1 says which GL entry each - // shape of the record dispatches to). const MG_Backend::GLFunctionsTable& gl = table->GL; - const Bool instanced = info.InstanceCount > 1 || info.StartInstance != 0; - if (info.NumDraws != 1) { - ServerUnmigratedVerbFatal(info.IndexSize == 0 ? "MultiDrawArrays" : "MultiDrawElements"); + const auto mode = static_cast(info.Mode); + + GLenum indexType = 0; + switch (info.IndexSize) { + case 0: break; // arrays + case 1: indexType = GL_UNSIGNED_BYTE; break; + case 2: indexType = GL_UNSIGNED_SHORT; break; + case 4: indexType = GL_UNSIGNED_INT; break; + default: + // IndexSize is "0 = arrays, else 1 / 2 / 4" (MGPipeTypes.h) and nothing else is a + // legal width; defaulting to 4 would read past the element buffer. + Wire::WireProtocolFatalAt("MGPDrawInfo::IndexSize", info.IndexSize, 4); } - if (instanced) { - ServerUnmigratedVerbFatal(info.IndexSize == 0 ? "DrawArraysInstanced" - : "DrawElementsInstanced"); - } - const MG_Pipe::MGPDrawRange& range = ranges[0]; - if (info.IndexSize == 0) { - if (gl.DrawArrays == nullptr) return false; - gl.DrawArrays(static_cast(info.Mode), static_cast(range.Start), - static_cast(range.Count)); - } else { - if (gl.DrawElementsBaseVertex == nullptr) return false; - GLenum indexType = GL_UNSIGNED_INT; - switch (info.IndexSize) { - case 1: indexType = GL_UNSIGNED_BYTE; break; - case 2: indexType = GL_UNSIGNED_SHORT; break; - case 4: indexType = GL_UNSIGNED_INT; break; - default: - // IndexSize is "0 = arrays, else 1 / 2 / 4" (MGPipeTypes.h:1323) and nothing - // else is a legal width; defaulting to 4 would read past the element buffer. - Wire::WireProtocolFatalAt("MGPDrawInfo::IndexSize", info.IndexSize, 4); + + // ---- the indirect family: the block is the whole description -------------------- + if (indirect != nullptr) { + // The layout already refused a record that sets both flags or declares ranges + // beside the block, so NumDraws is 0 and there is no span here. + const auto offset = reinterpret_cast(static_cast(indirect->Offset)); + const auto drawCount = static_cast(indirect->DrawCount); + const auto stride = static_cast(indirect->Stride); + const Bool counted = !MG_Pipe::MGPipeHandleIsNull(indirect->ParameterBuffer); + const auto parameterOffset = static_cast(indirect->ParameterOffset); + // glMultiDraw*Indirect with drawcount 1 and stride 0 IS glDraw*Indirect by GL's own + // definition, so the single-draw entry point is the one the monolith reaches for + // the single-draw call and nothing is lost for the multi-draw spelling of it. + const Bool single = !counted && drawCount == 1 && stride == 0; + if (info.IndexSize == 0) { + if (counted) { + if (gl.MultiDrawArraysIndirectCount == nullptr) return false; + gl.MultiDrawArraysIndirectCount(mode, offset, parameterOffset, drawCount, stride); + } else if (single) { + if (gl.DrawArraysIndirect == nullptr) return false; + gl.DrawArraysIndirect(mode, offset); + } else { + if (gl.MultiDrawArraysIndirect == nullptr) return false; + gl.MultiDrawArraysIndirect(mode, offset, drawCount, stride); + } + } else { + if (counted) { + if (gl.MultiDrawElementsIndirectCount == nullptr) return false; + gl.MultiDrawElementsIndirectCount(mode, indexType, offset, parameterOffset, + drawCount, stride); + } else if (single) { + if (gl.DrawElementsIndirect == nullptr) return false; + gl.DrawElementsIndirect(mode, indexType, offset); + } else { + if (gl.MultiDrawElementsIndirect == nullptr) return false; + gl.MultiDrawElementsIndirect(mode, indexType, offset, drawCount, stride); + } } - // Start is the FIRST INDEX, so the byte offset into the bound element buffer is - // Start * IndexSize - the same arithmetic PipeFill's emitter inverted. - const auto offset = static_cast(range.Start) * info.IndexSize; - gl.DrawElementsBaseVertex(static_cast(info.Mode), - static_cast(range.Count), indexType, - reinterpret_cast(offset), range.IndexBias); + ++m_draws; + return true; + } + + if (ranges == nullptr || info.NumDraws == 0) return false; + const Bool instanced = info.InstanceCount != 1 || info.StartInstance != 0; + const auto instanceCount = static_cast(info.InstanceCount); + const GLuint baseInstance = info.StartInstance; + + // ---- the multi-draws: the two arrays rebuilt from the ranges (rule C) -------------- + if (info.NumDraws != 1) { + if (userIndices != nullptr) { + ServerUnmigratedVerbFatal(info.IndexSize == 0 ? "MultiDrawArrays+CLIENT_INDICES" + : "MultiDrawElements+CLIENT_INDICES"); + } + if (instanced) { + ServerUnmigratedVerbFatal(info.IndexSize == 0 ? "MultiDrawArrays+INSTANCED" + : "MultiDrawElements+INSTANCED"); + } + const auto n = static_cast(info.NumDraws); + m_multiCounts.resize(n); + if (info.IndexSize == 0) { + if (gl.MultiDrawArrays == nullptr) return false; + m_multiFirsts.resize(n); + for (SizeT i = 0; i < n; ++i) { + m_multiFirsts[i] = static_cast(ranges[i].Start); + m_multiCounts[i] = static_cast(ranges[i].Count); + } + gl.MultiDrawArrays(mode, m_multiFirsts.data(), m_multiCounts.data(), + static_cast(n)); + } else { + if (gl.MultiDrawElementsBaseVertex == nullptr) return false; + m_multiOffsets.resize(n); + m_multiBaseVertices.resize(n); + for (SizeT i = 0; i < n; ++i) { + m_multiOffsets[i] = reinterpret_cast( + static_cast(ranges[i].Start) * info.IndexSize); + m_multiCounts[i] = static_cast(ranges[i].Count); + m_multiBaseVertices[i] = ranges[i].IndexBias; + } + // MultiDrawElements is the same call with every base vertex 0, which is what + // its ranges carry (CONTRACT-P5B.md d1's MultiDrawElements row). + gl.MultiDrawElementsBaseVertex(mode, m_multiCounts.data(), indexType, + m_multiOffsets.data(), static_cast(n), + m_multiBaseVertices.data()); + } + ++m_draws; + return true; + } + + // ---- one range ---------------------------------------------------------------------- + const MG_Pipe::MGPDrawRange& range = ranges[0]; + const auto count = static_cast(range.Count); + if (info.IndexSize == 0) { + if (!instanced) { + if (gl.DrawArrays == nullptr) return false; + gl.DrawArrays(mode, static_cast(range.Start), count); + } else if (baseInstance != 0) { + if (gl.DrawArraysInstancedBaseInstance == nullptr) return false; + gl.DrawArraysInstancedBaseInstance(mode, static_cast(range.Start), count, + instanceCount, baseInstance); + } else { + if (gl.DrawArraysInstanced == nullptr) return false; + gl.DrawArraysInstanced(mode, static_cast(range.Start), count, instanceCount); + } + ++m_draws; + return true; + } + + // Indexed. `indices` is the element-buffer byte offset Start * IndexSize - the same + // arithmetic PipeFill's emitter inverted - or, for a client index array, the staged run + // resolved through the process resolver the server installed (rule C: the pointer is + // used for this call only). The codec proved the span lies inside SEG_STAGE (all four + // honesty arms), so a null here is the resolver missing, which is a wiring fault. + const void* indices = nullptr; + if (userIndices != nullptr) { + indices = MG_Pipe::MGPipeHostBytes(*userIndices); + if (indices == nullptr) { + Wire::WireProtocolFatal("DrawVbo.userIndices", + "the user-index span passed the codec's four honesty arms " + "but MGPipeHostBytes resolved it to null; the server's " + "segment resolver is not installed"); + } + } else { + indices = reinterpret_cast(static_cast(range.Start) * + info.IndexSize); + } + const GLint baseVertex = range.IndexBias; + const Bool ranged = (info.Flags & MG_Pipe::kDrawHasIndexRange) != 0; + // A BASE VERTEX OF 0 IS THE PLAIN ENTRY POINT, NOT THE BaseVertex ONE WITH A 0 - d1's one + // ruling against CONTRACT-P5B.md's "DrawElementsBaseVertex(..., 0), the P5 arm, + // unchanged". Espryt's DrawElementsBaseVertex slot calls glDrawElementsBaseVertex, which + // is ES 3.2 / OES_draw_elements_base_vertex and is NOT loaded on an ES 3.1 provider + // (ANGLE on D3D11: "Failed to load GLES function: glDrawElementsBaseVertex" at bring-up), + // so the P5 arm dereferenced a null function pointer on every plain glDrawElements - the + // one call every Minecraft frame is made of - while the monolith, which calls + // GL.DrawElements for glDrawElements, was fine. Rule D says the sink reproduces the call + // the monolith makes; this is that, for all three indexed families. + if (!instanced) { + if (ranged) { + if (baseVertex != 0) { + if (gl.DrawRangeElementsBaseVertex == nullptr) return false; + gl.DrawRangeElementsBaseVertex(mode, info.MinIndex, info.MaxIndex, count, indexType, + indices, baseVertex); + } else { + if (gl.DrawRangeElements == nullptr) return false; + gl.DrawRangeElements(mode, info.MinIndex, info.MaxIndex, count, indexType, indices); + } + } else if (baseVertex != 0) { + if (gl.DrawElementsBaseVertex == nullptr) return false; + gl.DrawElementsBaseVertex(mode, count, indexType, indices, baseVertex); + } else { + if (gl.DrawElements == nullptr) return false; + gl.DrawElements(mode, count, indexType, indices); + } + } else if (baseInstance != 0) { + if (baseVertex != 0) { + if (gl.DrawElementsInstancedBaseVertexBaseInstance == nullptr) return false; + gl.DrawElementsInstancedBaseVertexBaseInstance(mode, count, indexType, indices, + instanceCount, baseVertex, baseInstance); + } else { + if (gl.DrawElementsInstancedBaseInstance == nullptr) return false; + gl.DrawElementsInstancedBaseInstance(mode, count, indexType, indices, instanceCount, + baseInstance); + } + } else if (baseVertex != 0) { + if (gl.DrawElementsInstancedBaseVertex == nullptr) return false; + gl.DrawElementsInstancedBaseVertex(mode, count, indexType, indices, instanceCount, baseVertex); + } else { + if (gl.DrawElementsInstanced == nullptr) return false; + gl.DrawElementsInstanced(mode, count, indexType, indices, instanceCount); } ++m_draws; return true; diff --git a/MobileGL/MG_Remote/Server/PipeApplier.h b/MobileGL/MG_Remote/Server/PipeApplier.h index 43b09c1b..da2e2a85 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.h +++ b/MobileGL/MG_Remote/Server/PipeApplier.h @@ -127,7 +127,8 @@ namespace MobileGL::MG_Remote::Server { // f1 OnGenerateMipmap, OnCopyFramebufferToTexture ("CopyTexImage2D" / // "CopyTexSubImage2D"), and OnClear's four non-Whole kinds (live already) // d1 OnDrawVbo above: the indirect tail, the user-index span, NumDraws > 1 and the - // instanced arms (declined by name today) + // instanced arms - LIVE since d1 v1 (every shape the client's nineteen draw slots + // produce dispatches to the backend slot CONTRACT-P5B.md §2 d1 names) Bool OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) override; Bool OnMemoryBarrier(const MG_Pipe::MGPMemoryBarrier& barrier) override; Bool OnResourceCopyRegion(const MG_Pipe::MGPCopyRegion& copy) override; @@ -158,6 +159,25 @@ namespace MobileGL::MG_Remote::Server { // DstSize is exactly the heap overflow codex 1 found, one field over. Uint64 ReadbackScratchBytes() const { return static_cast(m_readbackScratch.size()); } + // ---- P5b d1: what the LAST draw_vbo record carried, as the sink saw it --------------- + // + // Recorded BEFORE the backend is consulted, so a process with no backend object (every + // unit case) can still assert the wire's fields rather than only that a draw "was + // declined": the record's head, its first range, its indirect block, and whether a + // user-index span rode with it and how many bytes it named. Nothing here outlives the + // call except these copies (rule C: the pointers the sink was handed are not kept). + struct LastDrawRecord { + MG_Pipe::MGPDrawInfo Info{}; + MG_Pipe::MGPDrawRange FirstRange{}; + MG_Pipe::MGPDrawIndirect Indirect{}; + Uint64 UserIndexBytes = 0; + Bool HadUserIndices = false; + Bool HadIndirect = false; + }; + const LastDrawRecord& LastDraw() const { return m_lastDraw; } + // draw_vbo records seen, applied or declined; Draws() above counts only the applied. + Uint64 DrawRecords() const { return m_drawRecords; } + private: const MG_Backend::GlobalBackendFunctionsTable* Table(const char* verb) const; @@ -173,6 +193,15 @@ namespace MobileGL::MG_Remote::Server { // ReadPixels writes into a caller buffer, so one staging vector per session sits // between them. Grown, never shrunk, and never handed out past the call. Vector m_readbackScratch; + // P5b d1: the multi-draw arrays the glMultiDraw* slots take, rebuilt from the ranges + // per record (rule C: bounded by NumDraws, owned here, never handed out past the call), + // and the last-record witness above. + Vector m_multiCounts; + Vector m_multiFirsts; + Vector m_multiOffsets; + Vector m_multiBaseVertices; + LastDrawRecord m_lastDraw{}; + Uint64 m_drawRecords = 0; }; class PipeApplier { diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index acef2348..5367a78e 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -175,8 +175,11 @@ TEST(RemoteEmitTable, TheThreeClassesPartitionAllSeventyOneSlots) { // table itself reports with - which is also what t1's arming condition reads - rather than // recomputed here, so a table that lost an emitter cannot look like one that never had it. EXPECT_EQ(LocallyAnsweredSlotCount(), 2u); - EXPECT_EQ(ImplementedVerbCount(), 5u); - EXPECT_EQ(UnmigratedSlotCount(), 64u); + // P5b d1 moved the nineteen draw slots from class C to class B (CONTRACT-P5B.md §7: B = 5 + // + the packages' flips, C = 64 - the same). Each P5b package raises B and lowers C by the + // same number, so the two numbers here move together and the sum below never does. + EXPECT_EQ(ImplementedVerbCount(), 5u + 19u); + EXPECT_EQ(UnmigratedSlotCount(), 64u - 19u); EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(), kRemoteEmitSlotCount); } @@ -229,22 +232,42 @@ TEST(RemoteEmitTable, AnUnmigratedSlotAbortsAndNamesItself) { // THE DEATH TEST ON THE UnmigratedVerbFatal ARM. It asserts the exact wording, not merely // that the child died: a control that trips on any abort is satisfied by the wrong abort, // which is one of the three shapes R-16 was written after. + // GetTexImage is the wave-3 tail (CONTRACT-P5B.md §7): no P5b package flips it, so this + // case keeps its subject across the four P5b landings. (It was DrawElements until d1 made + // that a class-B emitter.) + const ChildResult r = RunInChild([] { + RemoteEmitTable().GL.GetTexImage(0x0DE1 /*GL_TEXTURE_2D*/, 0, 0x1908 /*GL_RGBA*/, + 0x1401 /*GL_UNSIGNED_BYTE*/, nullptr); + }); + ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; + EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"GetTexImage\"}"), std::string::npos) << r.Log; +} + +TEST(RemoteEmitTable, EachUnmigratedSlotNamesItsOwnSlot) { + // The half the case above cannot state on its own: that the name in the message is the + // slot's and not a constant. Two different slots, two different names - both from the + // wave-3 tail, for the reason the case above gives. + const ChildResult r = RunInChild([] { RemoteEmitTable().SetSwapInterval(1); }); + ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; + EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"SetSwapInterval\"}"), std::string::npos) << r.Log; + EXPECT_EQ(r.Log.find("GetTexImage"), std::string::npos) + << "the Fatal message names a slot other than the one that was called:\n" + << r.Log; +} + +// P5b d1: a class-B draw slot with no session aborts Fatal{NoClientSession, ""} - the +// class-B shape - and NOT Fatal{UnmigratedVerb}: that is what distinguishes a flipped slot from +// the stub it replaced, by behaviour rather than by pointer. Red once by: moving DrawElements +// back into MGR_UNMIGRATED_D1_SLOTS - the log then reads UnmigratedVerb. +TEST(RemoteEmitTable, AFlippedDrawSlotDemandsASessionRatherThanNamingItselfUnmigrated) { const ChildResult r = RunInChild([] { RemoteEmitTable().GL.DrawElements(0x0004 /*GL_TRIANGLES*/, 3, 0x1405 /*GL_UNSIGNED_INT*/, nullptr); }); ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; - EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"DrawElements\"}"), std::string::npos) << r.Log; -} - -TEST(RemoteEmitTable, EachUnmigratedSlotNamesItsOwnSlot) { - // The half the case above cannot state on its own: that the name in the message is the - // slot's and not a constant. Two different slots, two different names. - const ChildResult r = RunInChild([] { RemoteEmitTable().GL.GenerateMipmap(0x0DE1); }); - ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; - EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"GenerateMipmap\"}"), std::string::npos) << r.Log; - EXPECT_EQ(r.Log.find("DrawElements"), std::string::npos) - << "the Fatal message names a slot other than the one that was called:\n" + EXPECT_NE(r.Log.find("Fatal{NoClientSession, \"DrawElements\"}"), std::string::npos) << r.Log; + EXPECT_EQ(r.Log.find("Fatal{UnmigratedVerb"), std::string::npos) + << "DrawElements is class B since d1 and must not name itself unmigrated:\n" << r.Log; } @@ -405,6 +428,178 @@ TEST(CapsMirrorTest, TheConsumerBlockDoesNotCollideWithTheFeatureBits) { // E2's emitter-drop control: the switch itself, driven through the real emitter's own counter // ===================================================================================== +// ===================================================================================== +// P5b d1: the draw family's record plan (MG_Remote/CONTRACT-P5B.md §2 d1). These drive the +// PRODUCTION derivation the nineteen emitters call - PlanDrawInfo / PlanDrawRange / +// PlanDrawIndirect over a RemoteDrawBindings snapshot - so the fields on the wire are pinned +// here without a GL context (R-16: the production predicate, not a copy). The bindings' READ +// from the real context is the integration lane's (IndexedDrawFamilyScenario under +// DirectGLES.Split.), and the sink's consumption of the same fields is ServerLoopTest's. +// ===================================================================================== + +namespace { + MGPipeHandle D1Handle(Uint32 slot) { + MGPipeHandle h{}; + h.Slot = slot; + h.Gen = 3; + return h; + } + RemoteDrawBindings D1BoundElementBuffer(Uint32 slot) { + RemoteDrawBindings b{}; + b.ElementBufferBound = true; + b.ElementBuffer = D1Handle(slot); + return b; + } +} // namespace + +// Red once by: returning `offset` instead of `offset / indexSize` from PlanDrawRange's +// element-buffer arm - Start reads 24 below. +TEST(RemoteDrawPlan, AnElementBufferDrawElementsCarriesTheHandleAndItsOffsetInIndices) { + const RemoteDrawBindings b = D1BoundElementBuffer(17); + const MGPDrawInfo info = PlanDrawInfo(0x0004 /*GL_TRIANGLES*/, RemoteIndexSizeFor(0x1403 /*USHORT*/), + /*instanceCount=*/1, /*baseInstance=*/0, /*numDraws=*/1, b); + EXPECT_EQ(info.Mode, 4u); + EXPECT_EQ(info.IndexSize, 2u); + EXPECT_EQ(info.IndexResource.Slot, 17u) << "IndexResource must be the VAO's element buffer"; + EXPECT_EQ(info.InstanceCount, 1u); + EXPECT_EQ(info.StartInstance, 0u); + EXPECT_EQ(info.Flags, 0u) << "a plain DrawElements sets no flag"; + EXPECT_EQ(info.MinIndex, ~0u); + EXPECT_EQ(info.NumDraws, 1u); + + MGPDrawRange range{}; + ASSERT_TRUE(PlanDrawRange(b, 2, reinterpret_cast(24), 36, 0, range)); + EXPECT_EQ(range.Start, 12u) << "Start is the byte offset in INDICES (24 / 2)"; + EXPECT_EQ(range.Count, 36u); + EXPECT_EQ(range.IndexBias, 0); +} + +// Red once by: dropping `out.IndexBias = baseVertex` - IndexBias reads 0 below. And the +// instanced head: swap InstanceCount and StartInstance in PlanDrawInfo - 7 and 2 trade places. +TEST(RemoteDrawPlan, TheInstancedBaseVertexBaseInstanceFormCarriesAllThreeNumbers) { + const RemoteDrawBindings b = D1BoundElementBuffer(9); + const MGPDrawInfo info = PlanDrawInfo(0x0004, 4, /*instanceCount=*/7, /*baseInstance=*/2, 1, b); + EXPECT_EQ(info.InstanceCount, 7u); + EXPECT_EQ(info.StartInstance, 2u); + MGPDrawRange range{}; + ASSERT_TRUE(PlanDrawRange(b, 4, reinterpret_cast(0), 6, /*baseVertex=*/5, range)); + EXPECT_EQ(range.Start, 0u); + EXPECT_EQ(range.IndexBias, 5); + // An instanced call with a count of 0 crosses as 0, never as the plain draw's 1: the sink + // reads InstanceCount != 1 as instanced, and 0 instances must draw nothing. + EXPECT_EQ(PlanDrawInfo(0x0004, 4, 0, 0, 1, b).InstanceCount, 0u); +} + +// Red once by: making PlanDrawRange's element-buffer arm `return true` on a remainder - the +// misaligned offset below plans as index 3 instead of being refused. +TEST(RemoteDrawPlan, AMisalignedElementOffsetIsRefusedNotRounded) { + const RemoteDrawBindings b = D1BoundElementBuffer(1); + MGPDrawRange range{}; + EXPECT_FALSE(PlanDrawRange(b, 4, reinterpret_cast(13), 3, 0, range)) + << "13 bytes is not a whole number of 4-byte indices; the emitter refuses " + "\"+INDEX_OFFSET\" rather than draw from index 3"; + EXPECT_TRUE(PlanDrawRange(b, 4, reinterpret_cast(12), 3, 0, range)); + EXPECT_EQ(range.Start, 3u); +} + +// Red once by: setting `out.Start = offset` in the no-element-buffer arm - Start reads the +// pointer's low bits instead of 0 (the staged run starts at its first index). +TEST(RemoteDrawPlan, AClientIndexArrayPlansFromIndexZeroWithNoHandle) { + RemoteDrawBindings b{}; // nothing bound: `indices` is the application's array + const std::uint16_t clientIndices[3] = {0, 1, 2}; + const MGPDrawInfo info = PlanDrawInfo(0x0004, 2, 1, 0, 1, b); + EXPECT_TRUE(MGPipeHandleIsNull(info.IndexResource)) << "no element buffer, no handle"; + MGPDrawRange range{}; + ASSERT_TRUE(PlanDrawRange(b, 2, clientIndices, 3, 0, range)); + EXPECT_EQ(range.Start, 0u); + EXPECT_EQ(range.Count, 3u); + // The span itself is added by the emission (kDrawHasUserIndices, count * IndexSize bytes + // staged); ServerLoopTest's AClientIndexArrayCrossesAsAStagedSpan pins the other side. +} + +// Red once by: returning `first * sizeof(float)` or any scaled first for arrays - Start reads +// something other than 56064 below. (Arrays spell `first` through the pointer parameter.) +TEST(RemoteDrawPlan, AnArraysRangeIsFirstAndCountWithNoBias) { + RemoteDrawBindings b{}; + MGPDrawRange range{}; + ASSERT_TRUE(PlanDrawRange(b, 0, reinterpret_cast(static_cast(56064)), + 16128, 0, range)); + EXPECT_EQ(range.Start, 56064u); + EXPECT_EQ(range.Count, 16128u); + EXPECT_EQ(range.IndexBias, 0); + EXPECT_EQ(PlanDrawInfo(0x0004, 0, 1, 0, 1, b).IndexSize, 0u); +} + +// Red once by: dropping `block.ParameterBuffer = ...` for the counted form - the parameter +// handle reads null below and the sink would dispatch the uncounted call. +TEST(RemoteDrawPlan, TheIndirectBlockNamesBothBuffersAndTheCallsOffsets) { + RemoteDrawBindings b{}; + b.DrawIndirectBuffer = D1Handle(40); + b.ParameterBuffer = D1Handle(41); + const MGPDrawIndirect counted = PlanDrawIndirect(b, reinterpret_cast(64), + /*drawCount=*/3, /*stride=*/20, + /*parameterOffset=*/8, /*hasParameterBuffer=*/true); + EXPECT_EQ(counted.Buffer.Slot, 40u); + EXPECT_EQ(counted.ParameterBuffer.Slot, 41u); + EXPECT_EQ(counted.Offset, 64u); + EXPECT_EQ(counted.ParameterOffset, 8u); + EXPECT_EQ(counted.Stride, 20u); + EXPECT_EQ(counted.DrawCount, 3u); + const MGPDrawIndirect plain = PlanDrawIndirect(b, reinterpret_cast(64), 1, 0, 8, false); + EXPECT_TRUE(MGPipeHandleIsNull(plain.ParameterBuffer)) + << "an uncounted indirect draw names no parameter buffer even when one is bound"; + EXPECT_EQ(plain.ParameterOffset, 0u); + EXPECT_EQ(plain.DrawCount, 1u); + // The head of an indirect record declares no ranges: the server never reads the indirect + // buffer to learn a count (MGPipeTypes.h kDrawIsIndirect). + EXPECT_EQ(PlanDrawInfo(0x0004, 4, 1, 0, 0, b).NumDraws, 0u); +} + +// Red once by: returning 4 for GL_UNSIGNED_SHORT - the second line below reads 4. +TEST(RemoteDrawPlan, TheThreeIndexTypesSizeAndNothingElseDoes) { + EXPECT_EQ(RemoteIndexSizeFor(0x1401 /*GL_UNSIGNED_BYTE*/), 1u); + EXPECT_EQ(RemoteIndexSizeFor(0x1403 /*GL_UNSIGNED_SHORT*/), 2u); + EXPECT_EQ(RemoteIndexSizeFor(0x1405 /*GL_UNSIGNED_INT*/), 4u); + EXPECT_EQ(RemoteIndexSizeFor(0x1406 /*GL_FLOAT*/), 0u) << "not an index type: the emitter refuses " + "\"+INDEX_TYPE\""; +} + +TEST(RemoteEmitTable, TheNineteenDrawSlotsAreEmittersDistinctFromEveryStub) { + // The table half of d1's flip: each of the nineteen draw pointers is non-null and is NOT the + // class-C thunk that stood there at the contract commit. Read from the struct (R-16). + // Red once by: leaving one `table.GL.X = &EmitX;` out of BuildRemoteEmitTable - that slot + // would then be the MGR_UNMIGRATED thunk, which the macro no longer defines, so the build + // breaks first; and by re-adding an X row to MGR_UNMIGRATED_D1_SLOTS, which breaks the + // ownership static_assert. Both are build breaks, which is the point of the arithmetic. + const MG_Backend::GlobalBackendFunctionsTable& t = RemoteEmitTable(); + const void* stub = reinterpret_cast(t.GL.GetTexImage); // a class-C thunk + const void* draws[19] = { + reinterpret_cast(t.GL.DrawElements), + reinterpret_cast(t.GL.DrawElementsBaseVertex), + reinterpret_cast(t.GL.DrawRangeElements), + reinterpret_cast(t.GL.DrawRangeElementsBaseVertex), + reinterpret_cast(t.GL.DrawElementsInstanced), + reinterpret_cast(t.GL.DrawElementsInstancedBaseVertex), + reinterpret_cast(t.GL.DrawElementsInstancedBaseInstance), + reinterpret_cast(t.GL.DrawElementsInstancedBaseVertexBaseInstance), + reinterpret_cast(t.GL.DrawArraysInstanced), + reinterpret_cast(t.GL.DrawArraysInstancedBaseInstance), + reinterpret_cast(t.GL.MultiDrawArrays), + reinterpret_cast(t.GL.MultiDrawElements), + reinterpret_cast(t.GL.MultiDrawElementsBaseVertex), + reinterpret_cast(t.GL.DrawArraysIndirect), + reinterpret_cast(t.GL.DrawElementsIndirect), + reinterpret_cast(t.GL.MultiDrawArraysIndirect), + reinterpret_cast(t.GL.MultiDrawElementsIndirect), + reinterpret_cast(t.GL.MultiDrawArraysIndirectCount), + reinterpret_cast(t.GL.MultiDrawElementsIndirectCount), + }; + for (int i = 0; i < 19; ++i) { + EXPECT_NE(draws[i], nullptr) << "draw slot " << i; + EXPECT_NE(draws[i], stub) << "draw slot " << i << " is a class-C thunk"; + } +} + TEST(RemoteEmitTable, TheE2DropSwitchStartsDisarmed) { // The half a unit case can state. E2's statement - "drop one Clear emission and OpenRA's // SSIM falls below 0.99" - is a TRACE LANE's, because the picture is the thing it is about; diff --git a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp index 10bbe4b9..487b93d7 100644 --- a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp @@ -1778,6 +1778,141 @@ TEST(StagedShadowProductionTest, TheStreamingIdiomThroughAPoolReuseIsUploadedNot } #endif // !_WIN32 +// ===================================================================================== +// P5b d1: a draw_vbo record of every d1 shape crosses to the REAL ServerVerbSink with its +// fields intact (MG_Remote/CONTRACT-P5B.md §2 d1). No backend object lives in this process, +// so each record is DECLINED after the sink has witnessed it - which is exactly the point: +// the witness is taken before the backend is consulted, so these cases pin the wire's fields +// and not a backend call. The emitter half (GL args -> these fields) is RemoteClientTest's +// PlanDraw* cases; the backend half is the DirectGLES.Split. IndexedDrawFamilyScenario lane. +// ===================================================================================== + +namespace { + MG_Pipe::MGPipeHandle D1Handle(Uint32 slot) { + MG_Pipe::MGPipeHandle h{}; + h.Slot = slot; + h.Gen = 1; + return h; + } + MG_Pipe::MGPDrawInfo D1DrawInfo(Uint8 indexSize, Uint32 numDraws) { + MG_Pipe::MGPDrawInfo info{}; + info.Mode = 0x0004; // GL_TRIANGLES + info.IndexSize = indexSize; + info.InstanceCount = 1; + info.MinIndex = ~0u; + info.MaxIndex = ~0u; + info.NumDraws = numDraws; + return info; + } +} // namespace + +// Red once by: dropping `m_lastDraw.Info = info;` from OnDrawVbo - InstanceCount reads 1 and +// IndexBias reads 0 below. +TEST(ServerLoopTest, AnInstancedBaseVertexDrawRecordReachesTheSinkWithItsFieldsIntact) { + ServerFixture fixture; + ASSERT_TRUE(fixture.Handshake()); + ASSERT_TRUE(fixture.StartLoop()); + + MG_Pipe::MGPDrawInfo info = D1DrawInfo(/*indexSize=*/2, /*numDraws=*/1); + info.InstanceCount = 7; // DrawElementsInstancedBaseVertex(..., 7, 5): the Minecraft slot + info.IndexResource = D1Handle(31); + const MG_Pipe::MGPDrawRange range{/*Start=*/12, /*Count=*/36, /*IndexBias=*/5}; + ASSERT_TRUE(fixture.EmitAndWaitWithTail(MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info), &range, + sizeof(range))); + + const Server::ServerVerbSink& verbs = fixture.session->Applier().Verbs(); + EXPECT_EQ(verbs.DrawRecords(), 1u) << "the record did not reach OnDrawVbo"; + EXPECT_EQ(verbs.Draws(), 0u) << "no backend object lives here, so the draw must be DECLINED " + "after the witness, never applied"; + const Server::ServerVerbSink::LastDrawRecord& seen = verbs.LastDraw(); + EXPECT_EQ(seen.Info.IndexSize, 2u); + EXPECT_EQ(seen.Info.InstanceCount, 7u); + EXPECT_EQ(seen.Info.IndexResource.Slot, 31u); + EXPECT_EQ(seen.FirstRange.Start, 12u); + EXPECT_EQ(seen.FirstRange.Count, 36u); + EXPECT_EQ(seen.FirstRange.IndexBias, 5); + EXPECT_FALSE(seen.HadUserIndices); + EXPECT_FALSE(seen.HadIndirect); + EXPECT_FALSE(MG_Pipe::gPipeInputs.ServerStampedVerb()); + + fixture.Stop(); +} + +// Red once by: setting `m_lastDraw.UserIndexBytes = 0` in OnDrawVbo's span arm - the byte +// count below reads 0 while HadUserIndices stays true. +TEST(ServerLoopTest, AClientIndexArrayCrossesAsAStagedSpanAndTheSinkSeesItsByteCount) { + ServerFixture fixture; + ASSERT_TRUE(fixture.Handshake()); + ASSERT_TRUE(fixture.StartLoop()); + + // d1's rule for a client index array: the CLIENT stages count * IndexSize bytes and the + // record names the run (Ptr = nullptr, SEG_STAGE); the sink resolves it for the call only. + const Uint32 indices[6] = {0, 1, 2, 2, 3, 0}; + const MG_Pipe::MGPBlobRef staged = fixture.encoder.StageBytes(indices, sizeof(indices)); + MG_Pipe::MGHostSpan span{}; + span.Ptr = nullptr; + span.Seg = staged.Seg; + span.Offset = staged.Offset; + span.Size = staged.Size; + + MG_Pipe::MGPDrawInfo info = D1DrawInfo(/*indexSize=*/4, /*numDraws=*/1); + info.Flags = MG_Pipe::kDrawHasUserIndices; + const MG_Pipe::MGPDrawRange range{0, 6, 0}; + const Codec::WireTail tails[2] = {{&range, sizeof(range)}, {&span, sizeof(span)}}; + const Uint64 seq = fixture.encoder.EncodeRecord(MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info), + tails, 2); + ASSERT_NE(seq, Codec::kInvalidSeq); + fixture.encoder.Publish(); + fixture.producer.PublishAndNotify(seq); + ASSERT_EQ(fixture.producer.WaitForApplied(seq, 5000), Transport::SessionWait::Reached); + + const Server::ServerVerbSink::LastDrawRecord& seen = fixture.session->Applier().Verbs().LastDraw(); + EXPECT_TRUE(seen.HadUserIndices) << "the span did not reach OnDrawVbo"; + EXPECT_EQ(seen.UserIndexBytes, sizeof(indices)); + EXPECT_EQ(seen.Info.Flags & MG_Pipe::kDrawHasUserIndices, MG_Pipe::kDrawHasUserIndices); + EXPECT_EQ(seen.FirstRange.Count, 6u); + EXPECT_EQ(fixture.session->Applier().Verbs().Draws(), 0u); + + fixture.Stop(); +} + +// Red once by: dropping `m_lastDraw.Indirect = *indirect;` - DrawCount reads 0 below. +TEST(ServerLoopTest, AnIndirectDrawRecordReachesTheSinkWithItsBlockIntact) { + ServerFixture fixture; + ASSERT_TRUE(fixture.Handshake()); + ASSERT_TRUE(fixture.StartLoop()); + + MG_Pipe::MGPDrawInfo info = D1DrawInfo(/*indexSize=*/0, /*numDraws=*/0); + info.Flags = MG_Pipe::kDrawIsIndirect; + MG_Pipe::MGPDrawIndirect block{}; + block.Buffer = D1Handle(40); + block.ParameterBuffer = D1Handle(41); // the *IndirectCount shape + block.Offset = 32; + block.ParameterOffset = 8; + block.Stride = 16; + block.DrawCount = 3; + const Codec::WireTail tails[2] = {{nullptr, 0}, {&block, sizeof(block)}}; + const Uint64 seq = fixture.encoder.EncodeRecord(MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info), + tails, 2); + ASSERT_NE(seq, Codec::kInvalidSeq); + fixture.encoder.Publish(); + fixture.producer.PublishAndNotify(seq); + ASSERT_EQ(fixture.producer.WaitForApplied(seq, 5000), Transport::SessionWait::Reached); + + const Server::ServerVerbSink::LastDrawRecord& seen = fixture.session->Applier().Verbs().LastDraw(); + EXPECT_TRUE(seen.HadIndirect) << "the indirect block did not reach OnDrawVbo"; + EXPECT_FALSE(seen.HadUserIndices); + EXPECT_EQ(seen.Indirect.Buffer.Slot, 40u); + EXPECT_EQ(seen.Indirect.ParameterBuffer.Slot, 41u); + EXPECT_EQ(seen.Indirect.Offset, 32u); + EXPECT_EQ(seen.Indirect.ParameterOffset, 8u); + EXPECT_EQ(seen.Indirect.Stride, 16u); + EXPECT_EQ(seen.Indirect.DrawCount, 3u); + EXPECT_EQ(seen.Info.NumDraws, 0u); + + fixture.Stop(); +} + int main(int argc, char** argv) { // Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first // write, and caches the FILE*. The name carries this process's pid, because