[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)

This commit is contained in:
2026-09-16 12:10:38 -04:00
parent 37fc4fdb0f
commit e564939615
11 changed files with 1465 additions and 95 deletions
@@ -62,6 +62,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/CrossFrameBufferScenario.cpp Scenarios/CrossFrameBufferScenario.cpp
Scenarios/ResidentIndexScenario.cpp Scenarios/ResidentIndexScenario.cpp
Scenarios/MultiDrawScenario.cpp Scenarios/MultiDrawScenario.cpp
Scenarios/IndexedDrawFamilyScenario.cpp
Scenarios/DrawParametersScenario.cpp Scenarios/DrawParametersScenario.cpp
Scenarios/AsyncCompileScenario.cpp Scenarios/AsyncCompileScenario.cpp
Scenarios/XfbAfterClipDistanceScenario.cpp Scenarios/XfbAfterClipDistanceScenario.cpp
@@ -1935,6 +1936,41 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}" 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 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 # 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 # pushes the mapping's dirty blocks - so pmap must be non-zero and mpr must be the monolith
@@ -11,4 +11,11 @@ foreach(scenario @MGL_SPLIT_SMALL_RING_SCENARIOS@)
"MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log") "MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log")
endforeach() endforeach()
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. # PersistentMapArm retains its existing private path and RESOURCE_LOCK: b1 reads it.
@@ -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 <cstdint>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// 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 <class Draw>
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<const void*>(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<const void*>(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<const void*>(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
+16 -2
View File
@@ -689,6 +689,21 @@ namespace MobileGL::MG_Remote::Client {
Uint64 ClientSession::EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes, Uint64 ClientSession::EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
const void* varTail, Uint64 varTailBytes, void* replyOut, const void* varTail, Uint64 varTailBytes, void* replyOut,
Uint64 replyBytes, Int32* statusOut, Uint64* replySizeOut) { 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 (statusOut != nullptr) *statusOut = Wire::ReplySink::kStatusError;
if (replySizeOut != nullptr) *replySizeOut = 0; if (replySizeOut != nullptr) *replySizeOut = 0;
if (!m_started) { if (!m_started) {
@@ -713,8 +728,7 @@ namespace MobileGL::MG_Remote::Client {
std::abort(); std::abort();
} }
const Uint64 seq = const Uint64 seq = m_encoder.EncodeRecord(op, payload, payloadBytes, tails, tailCount);
m_encoder.EncodeRecord(op, payload, payloadBytes, varTail, varTailBytes);
if (seq == Wire::kInvalidSeq) { if (seq == Wire::kInvalidSeq) {
// The ring refused it. NOT a silent drop and not a retry loop: R-10 says P5 does no // 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. // chunking and must prove it needs none, so a refusal is the proof failing.
@@ -108,6 +108,15 @@ namespace MobileGL::MG_Remote::Client {
const void* varTail, Uint64 varTailBytes, void* replyOut, const void* varTail, Uint64 varTailBytes, void* replyOut,
Uint64 replyBytes, Int32* statusOut, Uint64* replySizeOut = nullptr); 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 // 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. // red; it must be run once and the way it goes red recorded.
Bool BarrierArmed() const; Bool BarrierArmed() const;
+435 -29
View File
@@ -10,8 +10,8 @@
// //
// THE PARTITION IS CONTRACT-P5.md §7's AND IS NOT RE-DERIVED HERE (R-15, ID-12): // 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 A 2 slots answered locally from the caps mirror, never emitted, never Fatal
// class B 5 slots emitted // class B 24 slots emitted (P5's five; P5b d1's nineteen draw slots, all on draw_vbo)
// class C 64 slots Fatal{UnmigratedVerb, "<slot>"} // class C 45 slots Fatal{UnmigratedVerb, "<slot>"}
// The three counts are static_asserted to sum to kRemoteEmitSlotCount below, so a slot that // 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 // changes class without changing the arithmetic is a build break rather than a behaviour
// change nobody reviewed. // change nobody reviewed.
@@ -35,7 +35,14 @@
#include <MG_State/GLState/BufferState/BufferObject.h> #include <MG_State/GLState/BufferState/BufferObject.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
// 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 <MG_Impl/Pipe/ResourceTracker.h>
#include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
@@ -272,6 +279,303 @@ namespace MobileGL::MG_Remote::Client {
sizeof(range), nullptr, 0, nullptr); 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, "<slot>+<QUALIFIER>"}),
// 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<Uint64>(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<Uint64>(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<const void*>(static_cast<std::intptr_t>(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<MG_Pipe::MGPDrawRange>& MultiDrawScratch(Uint32 count) {
static Vector<MG_Pipe::MGPDrawRange>& scratch = *new Vector<MG_Pipe::MGPDrawRange>();
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<Uint32>(drawcount);
MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, 0, 1, 0, n, bindings);
Vector<MG_Pipe::MGPDrawRange>& ranges = MultiDrawScratch(n);
for (Uint32 i = 0; i < n; ++i) {
PlanDrawRange(bindings, 0,
reinterpret_cast<const void*>(static_cast<std::intptr_t>(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<Uint32>(drawcount);
MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, indexSize, 1, 0, n, bindings);
Vector<MG_Pipe::MGPDrawRange>& 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, void EmitBlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0,
GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask,
GLenum filter) { 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, // 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, // tail the wave-3 remainder nothing measured: queries, syncs, the texture readbacks,
// the DSA blit, the swap interval (census-classC.md "static cross") // the DSA blit, the swap interval (census-classC.md "static cross")
#define MGR_UNMIGRATED_D1_SLOTS(X) \ // d1 v1 flipped all nineteen (DrawElements, DrawElementsBaseVertex, MultiDrawArrays,
X(DrawElements, void, (GLenum, GLsizei, GLenum, const void*)) \ // MultiDrawElements, MultiDrawElementsBaseVertex, MultiDrawElementsIndirect,
X(DrawElementsBaseVertex, void, (GLenum, GLsizei, GLenum, const void*, GLint)) \ // MultiDrawArraysIndirect, MultiDrawElementsIndirectCount, MultiDrawArraysIndirectCount,
X(MultiDrawArrays, void, (GLenum, const GLint*, const GLsizei*, GLsizei)) \ // DrawRangeElementsBaseVertex, DrawRangeElements, the four DrawElementsInstanced*,
X(MultiDrawElements, void, (GLenum, const GLsizei*, GLenum, const GLvoid* const*, GLsizei)) \ // DrawArraysInstancedBaseInstance, DrawArraysInstanced, DrawElementsIndirect,
X(MultiDrawElementsBaseVertex, void, \ // DrawArraysIndirect) to class B; the list is kept, empty, so the ownership assertion
(GLenum, const GLsizei*, GLenum, const GLvoid* const*, GLsizei, const GLint*)) \ // below still reads 0 + 19 = 19 and a slot that fell back in would have to be added here.
X(MultiDrawElementsIndirect, void, (GLenum, GLenum, const void*, GLsizei, GLsizei)) \ #define MGR_UNMIGRATED_D1_SLOTS(X)
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*))
// DispatchCompute and DispatchComputeIndirect are i1's too; they are hand-written below // DispatchCompute and DispatchComputeIndirect are i1's too; they are hand-written below
// because they carry b1's dispatch hook before the Fatal. // 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. // 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 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 kEmittedSlotsI1 = 0;
constexpr Uint32 kEmittedSlotsT2 = 0; constexpr Uint32 kEmittedSlotsT2 = 0;
constexpr Uint32 kEmittedSlotsF1 = 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(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(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(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, static_assert(kLocallyAnsweredSlots + kEmittedSlots + kUnmigratedSlots == kRemoteEmitSlotCount,
"the three classes no longer partition the 71 slots"); "the three classes no longer partition the 71 slots");
@@ -812,6 +1106,27 @@ namespace MobileGL::MG_Remote::Client {
// ---- class B // ---- class B
table.GL.Clear = &EmitClear; table.GL.Clear = &EmitClear;
table.GL.DrawArrays = &EmitDrawArrays; 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.ReadPixels = &EmitReadPixels;
table.GL.BlitFramebuffer = &EmitBlitFramebuffer; table.GL.BlitFramebuffer = &EmitBlitFramebuffer;
table.Present = &EmitPresent; 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<Uint32>(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<Uint8>(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<Uint32>(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<Uint32>(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<std::intptr_t>(reinterpret_cast<std::uintptr_t>(indicesOrFirst));
out.Start = first > 0 ? static_cast<Uint32>(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<std::uintptr_t>(indicesOrFirst);
if (offset % indexSize != 0) return false;
const std::uintptr_t start = offset / indexSize;
if (start > 0xFFFFFFFFull) return false;
out.Start = static_cast<Uint32>(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<Uint64>(reinterpret_cast<std::uintptr_t>(indirect));
block.ParameterOffset = hasParameterBuffer ? static_cast<Uint64>(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<Uint32>(stride) : 0u;
block.DrawCount = drawCount > 0 ? static_cast<Uint32>(drawCount) : 0u;
return block;
}
} // namespace MobileGL::MG_Remote::Client } // namespace MobileGL::MG_Remote::Client
+55
View File
@@ -161,4 +161,59 @@ namespace MobileGL::MG_Remote::Client {
// 0=OK / 1=DECLINED / 2=ERROR as `status`. // 0=OK / 1=DECLINED / 2=ERROR as `status`.
Bool ReadbackReplyIsComplete(Int32 status, Uint64 replySize, Uint64 expected); 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 } // namespace MobileGL::MG_Remote::Client
+221 -50
View File
@@ -281,65 +281,236 @@ namespace MobileGL::MG_Remote::Server {
std::abort(); 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, Bool ServerVerbSink::OnDrawVbo(const MG_Pipe::MGPDrawInfo& info,
const MG_Pipe::MGPDrawRange* ranges, const MG_Pipe::MGPDrawRange* ranges,
const MG_Pipe::MGHostSpan* userIndices, const MG_Pipe::MGHostSpan* userIndices,
const MG_Pipe::MGPDrawIndirect* indirect) { 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"); const MG_Backend::GlobalBackendFunctionsTable* table = Table("draw_vbo");
if (table == nullptr) return false; 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 MG_Backend::GLFunctionsTable& gl = table->GL;
const Bool instanced = info.InstanceCount > 1 || info.StartInstance != 0; const auto mode = static_cast<GLenum>(info.Mode);
if (info.NumDraws != 1) {
ServerUnmigratedVerbFatal(info.IndexSize == 0 ? "MultiDrawArrays" : "MultiDrawElements"); 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" // ---- the indirect family: the block is the whole description --------------------
: "DrawElementsInstanced"); if (indirect != nullptr) {
} // The layout already refused a record that sets both flags or declares ranges
const MG_Pipe::MGPDrawRange& range = ranges[0]; // beside the block, so NumDraws is 0 and there is no span here.
if (info.IndexSize == 0) { const auto offset = reinterpret_cast<const void*>(static_cast<std::uintptr_t>(indirect->Offset));
if (gl.DrawArrays == nullptr) return false; const auto drawCount = static_cast<GLsizei>(indirect->DrawCount);
gl.DrawArrays(static_cast<GLenum>(info.Mode), static_cast<GLint>(range.Start), const auto stride = static_cast<GLsizei>(indirect->Stride);
static_cast<GLsizei>(range.Count)); const Bool counted = !MG_Pipe::MGPipeHandleIsNull(indirect->ParameterBuffer);
} else { const auto parameterOffset = static_cast<GLintptr>(indirect->ParameterOffset);
if (gl.DrawElementsBaseVertex == nullptr) return false; // glMultiDraw*Indirect with drawcount 1 and stride 0 IS glDraw*Indirect by GL's own
GLenum indexType = GL_UNSIGNED_INT; // definition, so the single-draw entry point is the one the monolith reaches for
switch (info.IndexSize) { // the single-draw call and nothing is lost for the multi-draw spelling of it.
case 1: indexType = GL_UNSIGNED_BYTE; break; const Bool single = !counted && drawCount == 1 && stride == 0;
case 2: indexType = GL_UNSIGNED_SHORT; break; if (info.IndexSize == 0) {
case 4: indexType = GL_UNSIGNED_INT; break; if (counted) {
default: if (gl.MultiDrawArraysIndirectCount == nullptr) return false;
// IndexSize is "0 = arrays, else 1 / 2 / 4" (MGPipeTypes.h:1323) and nothing gl.MultiDrawArraysIndirectCount(mode, offset, parameterOffset, drawCount, stride);
// else is a legal width; defaulting to 4 would read past the element buffer. } else if (single) {
Wire::WireProtocolFatalAt("MGPDrawInfo::IndexSize", info.IndexSize, 4); 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 ++m_draws;
// Start * IndexSize - the same arithmetic PipeFill's emitter inverted. return true;
const auto offset = static_cast<std::uintptr_t>(range.Start) * info.IndexSize; }
gl.DrawElementsBaseVertex(static_cast<GLenum>(info.Mode),
static_cast<GLsizei>(range.Count), indexType, if (ranges == nullptr || info.NumDraws == 0) return false;
reinterpret_cast<const void*>(offset), range.IndexBias); const Bool instanced = info.InstanceCount != 1 || info.StartInstance != 0;
const auto instanceCount = static_cast<GLsizei>(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<SizeT>(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<GLint>(ranges[i].Start);
m_multiCounts[i] = static_cast<GLsizei>(ranges[i].Count);
}
gl.MultiDrawArrays(mode, m_multiFirsts.data(), m_multiCounts.data(),
static_cast<GLsizei>(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<const void*>(
static_cast<std::uintptr_t>(ranges[i].Start) * info.IndexSize);
m_multiCounts[i] = static_cast<GLsizei>(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<GLsizei>(n),
m_multiBaseVertices.data());
}
++m_draws;
return true;
}
// ---- one range ----------------------------------------------------------------------
const MG_Pipe::MGPDrawRange& range = ranges[0];
const auto count = static_cast<GLsizei>(range.Count);
if (info.IndexSize == 0) {
if (!instanced) {
if (gl.DrawArrays == nullptr) return false;
gl.DrawArrays(mode, static_cast<GLint>(range.Start), count);
} else if (baseInstance != 0) {
if (gl.DrawArraysInstancedBaseInstance == nullptr) return false;
gl.DrawArraysInstancedBaseInstance(mode, static_cast<GLint>(range.Start), count,
instanceCount, baseInstance);
} else {
if (gl.DrawArraysInstanced == nullptr) return false;
gl.DrawArraysInstanced(mode, static_cast<GLint>(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<const void*>(static_cast<std::uintptr_t>(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; ++m_draws;
return true; return true;
+30 -1
View File
@@ -127,7 +127,8 @@ namespace MobileGL::MG_Remote::Server {
// f1 OnGenerateMipmap, OnCopyFramebufferToTexture ("CopyTexImage2D" / // f1 OnGenerateMipmap, OnCopyFramebufferToTexture ("CopyTexImage2D" /
// "CopyTexSubImage2D"), and OnClear's four non-Whole kinds (live already) // "CopyTexSubImage2D"), and OnClear's four non-Whole kinds (live already)
// d1 OnDrawVbo above: the indirect tail, the user-index span, NumDraws > 1 and the // 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 OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) override;
Bool OnMemoryBarrier(const MG_Pipe::MGPMemoryBarrier& barrier) override; Bool OnMemoryBarrier(const MG_Pipe::MGPMemoryBarrier& barrier) override;
Bool OnResourceCopyRegion(const MG_Pipe::MGPCopyRegion& copy) 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. // DstSize is exactly the heap overflow codex 1 found, one field over.
Uint64 ReadbackScratchBytes() const { return static_cast<Uint64>(m_readbackScratch.size()); } Uint64 ReadbackScratchBytes() const { return static_cast<Uint64>(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: private:
const MG_Backend::GlobalBackendFunctionsTable* Table(const char* verb) const; 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 // 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. // between them. Grown, never shrunk, and never handed out past the call.
Vector<Uint8> m_readbackScratch; Vector<Uint8> 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<GLsizei> m_multiCounts;
Vector<GLint> m_multiFirsts;
Vector<const void*> m_multiOffsets;
Vector<GLint> m_multiBaseVertices;
LastDrawRecord m_lastDraw{};
Uint64 m_drawRecords = 0;
}; };
class PipeApplier { class PipeApplier {
+208 -13
View File
@@ -175,8 +175,11 @@ TEST(RemoteEmitTable, TheThreeClassesPartitionAllSeventyOneSlots) {
// table itself reports with - which is also what t1's arming condition reads - rather than // 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. // recomputed here, so a table that lost an emitter cannot look like one that never had it.
EXPECT_EQ(LocallyAnsweredSlotCount(), 2u); EXPECT_EQ(LocallyAnsweredSlotCount(), 2u);
EXPECT_EQ(ImplementedVerbCount(), 5u); // P5b d1 moved the nineteen draw slots from class C to class B (CONTRACT-P5B.md §7: B = 5
EXPECT_EQ(UnmigratedSlotCount(), 64u); // + 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(), EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(),
kRemoteEmitSlotCount); kRemoteEmitSlotCount);
} }
@@ -229,22 +232,42 @@ TEST(RemoteEmitTable, AnUnmigratedSlotAbortsAndNamesItself) {
// THE DEATH TEST ON THE UnmigratedVerbFatal ARM. It asserts the exact wording, not merely // 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, // 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. // 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, "<slot>"} - 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([] { const ChildResult r = RunInChild([] {
RemoteEmitTable().GL.DrawElements(0x0004 /*GL_TRIANGLES*/, 3, 0x1405 /*GL_UNSIGNED_INT*/, RemoteEmitTable().GL.DrawElements(0x0004 /*GL_TRIANGLES*/, 3, 0x1405 /*GL_UNSIGNED_INT*/,
nullptr); nullptr);
}); });
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"DrawElements\"}"), std::string::npos) << r.Log; 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"
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"
<< r.Log; << 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 // 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<const void*>(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<const void*>(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<const void*>(13), 3, 0, range))
<< "13 bytes is not a whole number of 4-byte indices; the emitter refuses "
"\"<slot>+INDEX_OFFSET\" rather than draw from index 3";
EXPECT_TRUE(PlanDrawRange(b, 4, reinterpret_cast<const void*>(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<const void*>(static_cast<std::intptr_t>(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<const void*>(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<const void*>(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 "
"\"<slot>+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<const void*>(t.GL.GetTexImage); // a class-C thunk
const void* draws[19] = {
reinterpret_cast<const void*>(t.GL.DrawElements),
reinterpret_cast<const void*>(t.GL.DrawElementsBaseVertex),
reinterpret_cast<const void*>(t.GL.DrawRangeElements),
reinterpret_cast<const void*>(t.GL.DrawRangeElementsBaseVertex),
reinterpret_cast<const void*>(t.GL.DrawElementsInstanced),
reinterpret_cast<const void*>(t.GL.DrawElementsInstancedBaseVertex),
reinterpret_cast<const void*>(t.GL.DrawElementsInstancedBaseInstance),
reinterpret_cast<const void*>(t.GL.DrawElementsInstancedBaseVertexBaseInstance),
reinterpret_cast<const void*>(t.GL.DrawArraysInstanced),
reinterpret_cast<const void*>(t.GL.DrawArraysInstancedBaseInstance),
reinterpret_cast<const void*>(t.GL.MultiDrawArrays),
reinterpret_cast<const void*>(t.GL.MultiDrawElements),
reinterpret_cast<const void*>(t.GL.MultiDrawElementsBaseVertex),
reinterpret_cast<const void*>(t.GL.DrawArraysIndirect),
reinterpret_cast<const void*>(t.GL.DrawElementsIndirect),
reinterpret_cast<const void*>(t.GL.MultiDrawArraysIndirect),
reinterpret_cast<const void*>(t.GL.MultiDrawElementsIndirect),
reinterpret_cast<const void*>(t.GL.MultiDrawArraysIndirectCount),
reinterpret_cast<const void*>(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) { TEST(RemoteEmitTable, TheE2DropSwitchStartsDisarmed) {
// The half a unit case can state. E2's statement - "drop one Clear emission and OpenRA's // 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; // SSIM falls below 0.99" - is a TRACE LANE's, because the picture is the thing it is about;
+135
View File
@@ -1778,6 +1778,141 @@ TEST(StagedShadowProductionTest, TheStreamingIdiomThroughAPoolReuseIsUploadedNot
} }
#endif // !_WIN32 #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) { int main(int argc, char** argv) {
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first // 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 // write, and caches the FILE*. The name carries this process's pid, because