[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/ResidentIndexScenario.cpp
Scenarios/MultiDrawScenario.cpp
Scenarios/IndexedDrawFamilyScenario.cpp
Scenarios/DrawParametersScenario.cpp
Scenarios/AsyncCompileScenario.cpp
Scenarios/XfbAfterClipDistanceScenario.cpp
@@ -1935,6 +1936,41 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}"
)
# ---- P5b d1: the indexed / instanced / multi-draw / indirect draw family under inproc ----
#
# One block per scenario, the precedent above. IndexedDrawFamilyScenario is d1's own lane
# case: every case's picture depends on a d1 draw (an element-buffer glDrawElements, a
# client index array, a base vertex, the Minecraft trace's DrawElementsInstancedBaseVertex,
# the two multi-draws, an indirect draw), so a record that did not cross is a wrong picture
# and not a green. MultiDrawScenario, DrawParametersScenario, PrimitiveRestartScenario and
# GuiBatchScenario are the census's own DrawElements / MultiDrawElementsBaseVertex /
# DrawArraysInstancedBaseInstance / MultiDraw*Indirect* first-blocker scenarios, now at
# their pictures. The ONE case excluded by filter is MultiDrawScenario's
# ClientSideIndicesBatchMatchesUnrolledDraws: a multi-draw with client-side indices is
# refused BY NAME under split (Fatal{UnmigratedVerb, "MultiDrawElementsBaseVertex+
# CLIENT_INDICES"}, CONTRACT-P5B.md d1 - P8's HostResolve.cpp flattens it), which is its
# next first blocker and not a lane failure. It is listed in d1-v1.md, not deleted (G14).
set(MGL_SPLIT_D1_SCENARIOS IndexedDrawFamilyScenario MultiDrawScenario DrawParametersScenario
PrimitiveRestartScenario GuiBatchScenario)
set(MGL_SPLIT_D1_FILTER_IndexedDrawFamilyScenario "IndexedDrawFamilyScenario.*")
set(MGL_SPLIT_D1_FILTER_MultiDrawScenario
"MultiDrawScenario.*-MultiDrawScenario.ClientSideIndicesBatchMatchesUnrolledDraws")
set(MGL_SPLIT_D1_FILTER_DrawParametersScenario "DrawParametersScenario.*")
set(MGL_SPLIT_D1_FILTER_PrimitiveRestartScenario "PrimitiveRestartScenario.*")
set(MGL_SPLIT_D1_FILTER_GuiBatchScenario "GuiBatchScenario.*")
foreach(mglItestD1Scenario IN LISTS MGL_SPLIT_D1_SCENARIOS)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.Split."
TEST_LIST "MGL_SPLIT_D1_${mglItestD1Scenario}_TESTS"
TEST_FILTER "${MGL_SPLIT_D1_FILTER_${mglItestD1Scenario}}"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-split"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}"
)
endforeach()
# The split half of the counting pair. MGITEST_PERSISTENT_MAP_ARM=emulated is R-6: under split
# the adopt tier is pinned at T2, the resource owner declines every acquisition and the client
# pushes the mapping's dirty blocks - so pmap must be non-zero and mpr must be the monolith
@@ -11,4 +11,11 @@ foreach(scenario @MGL_SPLIT_SMALL_RING_SCENARIOS@)
"MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log")
endforeach()
endforeach()
# P5b d1's split entries: the same private, distinct log per entry.
foreach(scenario @MGL_SPLIT_D1_SCENARIOS@)
foreach(entry IN LISTS MGL_SPLIT_D1_${scenario}_TESTS)
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
"MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log")
endforeach()
endforeach()
# PersistentMapArm retains its existing private path and RESOURCE_LOCK: b1 reads it.
@@ -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,
const void* varTail, Uint64 varTailBytes, void* replyOut,
Uint64 replyBytes, Int32* statusOut, Uint64* replySizeOut) {
// One tail is the two-tail form with one entry (P5b d1). Nothing is duplicated: the
// barrier policy below has exactly one body, and the encoder's one-tail EncodeRecord is
// itself defined as the tails form with tailCount <= 1.
const Wire::WireTail tail{varTail, varTailBytes};
return EmitAndWaitTails(op, payload, payloadBytes, varTail != nullptr ? &tail : nullptr,
varTail != nullptr ? 1u : 0u, replyOut, replyBytes, statusOut,
replySizeOut);
}
Uint64 ClientSession::EmitAndWaitTails(MG_Pipe::MGPWireOp op, const void* payload,
Uint64 payloadBytes, const Wire::WireTail* tails,
Uint32 tailCount, void* replyOut, Uint64 replyBytes,
Int32* statusOut, Uint64* replySizeOut) {
Uint64 varTailBytes = 0;
for (Uint32 i = 0; i < tailCount; ++i) varTailBytes += tails[i].Size;
if (statusOut != nullptr) *statusOut = Wire::ReplySink::kStatusError;
if (replySizeOut != nullptr) *replySizeOut = 0;
if (!m_started) {
@@ -713,8 +728,7 @@ namespace MobileGL::MG_Remote::Client {
std::abort();
}
const Uint64 seq =
m_encoder.EncodeRecord(op, payload, payloadBytes, varTail, varTailBytes);
const Uint64 seq = m_encoder.EncodeRecord(op, payload, payloadBytes, tails, tailCount);
if (seq == Wire::kInvalidSeq) {
// The ring refused it. NOT a silent drop and not a retry loop: R-10 says P5 does no
// chunking and must prove it needs none, so a refusal is the proof failing.
@@ -108,6 +108,15 @@ namespace MobileGL::MG_Remote::Client {
const void* varTail, Uint64 varTailBytes, void* replyOut,
Uint64 replyBytes, Int32* statusOut, Uint64* replySizeOut = nullptr);
// The same call for a record that carries TWO tails (P5b d1: draw_vbo's user-index span
// or its MGPDrawIndirect block behind the range array). The one-tail form above is this
// one with a single WireTail; the barrier policy lives in exactly one body. A distinct
// name rather than an overload, because `nullptr, 0` would match both.
Uint64 EmitAndWaitTails(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
const Wire::WireTail* tails, Uint32 tailCount, void* replyOut,
Uint64 replyBytes, Int32* statusOut,
Uint64* replySizeOut = nullptr);
// MOBILEGL_IPC_VERB_BARRIER. False is the R-1 negative control and is EXPECTED to be
// red; it must be run once and the way it goes red recorded.
Bool BarrierArmed() const;
+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):
// class A 2 slots answered locally from the caps mirror, never emitted, never Fatal
// class B 5 slots emitted
// class C 64 slots Fatal{UnmigratedVerb, "<slot>"}
// class B 24 slots emitted (P5's five; P5b d1's nineteen draw slots, all on draw_vbo)
// class C 45 slots Fatal{UnmigratedVerb, "<slot>"}
// The three counts are static_asserted to sum to kRemoteEmitSlotCount below, so a slot that
// changes class without changing the arithmetic is a build break rather than a behaviour
// change nobody reviewed.
@@ -35,7 +35,14 @@
#include <MG_State/GLState/BufferState/BufferObject.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 <cstring>
@@ -272,6 +279,303 @@ namespace MobileGL::MG_Remote::Client {
sizeof(range), nullptr, 0, nullptr);
}
// =============================================================================
// P5b d1 - the nineteen indexed / instanced / multi-draw / indirect draw slots
// (MG_Remote/CONTRACT-P5B.md §2 d1). ONE ROW, draw_vbo (59): the record carries the GL
// call verbatim (rule D) beside the handle the P8 form will dispatch on, and the sink
// reproduces the backend call the monolith makes.
// =============================================================================
//
// Every entry point below is: read the bindings, plan the head and the ranges (the pure
// functions the unit cases drive), then EmitDrawRecord - which runs the SAME pre-verb
// hooks in the SAME order as EmitDrawArrays (push, then mark walk, then the record;
// ID-18), honours the E2 draw-drop control for every draw record and not only the P5
// one, stages a client index array into SEG_STAGE when there is one, and emits.
//
// WHAT IS REFUSED BY NAME, ID-57's shape (Fatal{UnmigratedVerb, "<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,
GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask,
GLenum filter) {
@@ -585,7 +889,8 @@ namespace MobileGL::MG_Remote::Client {
}
// =============================================================================
// CLASS C - Fatal{UnmigratedVerb}. 64 slots: 63 in GLFunctionsTable + SetSwapInterval.
// CLASS C - Fatal{UnmigratedVerb}. 45 slots on this head: 44 in GLFunctionsTable +
// SetSwapInterval (64 at the P5b contract commit; d1 v1 moved its 19 to class B).
// =============================================================================
//
// PARTITIONED BY THE P5b PACKAGE THAT OWNS THE FLIP (MG_Remote/CONTRACT-P5B.md,
@@ -611,30 +916,14 @@ namespace MobileGL::MG_Remote::Client {
// tail the wave-3 remainder nothing measured: queries, syncs, the texture readbacks,
// the DSA blit, the swap interval (census-classC.md "static cross")
#define MGR_UNMIGRATED_D1_SLOTS(X) \
X(DrawElements, void, (GLenum, GLsizei, GLenum, const void*)) \
X(DrawElementsBaseVertex, void, (GLenum, GLsizei, GLenum, const void*, GLint)) \
X(MultiDrawArrays, void, (GLenum, const GLint*, const GLsizei*, GLsizei)) \
X(MultiDrawElements, void, (GLenum, const GLsizei*, GLenum, const GLvoid* const*, GLsizei)) \
X(MultiDrawElementsBaseVertex, void, \
(GLenum, const GLsizei*, GLenum, const GLvoid* const*, GLsizei, const GLint*)) \
X(MultiDrawElementsIndirect, void, (GLenum, GLenum, const void*, GLsizei, GLsizei)) \
X(MultiDrawArraysIndirect, void, (GLenum, const void*, GLsizei, GLsizei)) \
X(MultiDrawElementsIndirectCount, void, (GLenum, GLenum, const void*, GLintptr, GLsizei, GLsizei)) \
X(MultiDrawArraysIndirectCount, void, (GLenum, const void*, GLintptr, GLsizei, GLsizei)) \
X(DrawRangeElementsBaseVertex, void, \
(GLenum, GLuint, GLuint, GLsizei, GLenum, const void*, GLint)) \
X(DrawRangeElements, void, (GLenum, GLuint, GLuint, GLsizei, GLenum, const void*)) \
X(DrawElementsInstancedBaseVertexBaseInstance, void, \
(GLenum, GLsizei, GLenum, const void*, GLsizei, GLint, GLuint)) \
X(DrawElementsInstancedBaseVertex, void, (GLenum, GLsizei, GLenum, const void*, GLsizei, GLint)) \
X(DrawElementsInstancedBaseInstance, void, \
(GLenum, GLsizei, GLenum, const void*, GLsizei, GLuint)) \
X(DrawElementsInstanced, void, (GLenum, GLsizei, GLenum, const void*, GLsizei)) \
X(DrawArraysInstancedBaseInstance, void, (GLenum, GLint, GLsizei, GLsizei, GLuint)) \
X(DrawArraysInstanced, void, (GLenum, GLint, GLsizei, GLsizei)) \
X(DrawElementsIndirect, void, (GLenum, GLenum, const void*)) \
X(DrawArraysIndirect, void, (GLenum, const void*))
// d1 v1 flipped all nineteen (DrawElements, DrawElementsBaseVertex, MultiDrawArrays,
// MultiDrawElements, MultiDrawElementsBaseVertex, MultiDrawElementsIndirect,
// MultiDrawArraysIndirect, MultiDrawElementsIndirectCount, MultiDrawArraysIndirectCount,
// DrawRangeElementsBaseVertex, DrawRangeElements, the four DrawElementsInstanced*,
// DrawArraysInstancedBaseInstance, DrawArraysInstanced, DrawElementsIndirect,
// DrawArraysIndirect) to class B; the list is kept, empty, so the ownership assertion
// below still reads 0 + 19 = 19 and a slot that fell back in would have to be added here.
#define MGR_UNMIGRATED_D1_SLOTS(X)
// DispatchCompute and DispatchComputeIndirect are i1's too; they are hand-written below
// because they carry b1's dispatch hook before the Fatal.
@@ -761,7 +1050,7 @@ namespace MobileGL::MG_Remote::Client {
// The emitted counts, PER OWNER. P5's five are c1's; each P5b package raises its own.
constexpr Uint32 kEmittedSlotsP5 = 5; // Clear, DrawArrays, ReadPixels, Blit, Present
constexpr Uint32 kEmittedSlotsD1 = 0;
constexpr Uint32 kEmittedSlotsD1 = 19; // the draw family, all on draw_vbo (d1 v1)
constexpr Uint32 kEmittedSlotsI1 = 0;
constexpr Uint32 kEmittedSlotsT2 = 0;
constexpr Uint32 kEmittedSlotsF1 = 0;
@@ -778,7 +1067,12 @@ namespace MobileGL::MG_Remote::Client {
static_assert(kUnmigratedT2 + kEmittedSlotsT2 == 7, "t2 owns the 7 XFB/tessellation slots");
static_assert(kUnmigratedF1 + kEmittedSlotsF1 == 11, "f1 owns the 11 clear/copy/mip slots");
static_assert(kUnmigratedTail == 20, "the wave-3 tail is 20 slots and no P5b package owns one");
static_assert(kUnmigratedSlots == 64, "CONTRACT-P5.md §7 class C is 64 slots at the P5b contract commit");
// 64 at the P5b contract commit; every P5b flip moves exactly one slot from C to B, so
// the SUM is what stays pinned, and a package's flip changes its own count and nothing
// else on this line.
static_assert(kUnmigratedSlots + (kEmittedSlots - kEmittedSlotsP5) == 64,
"CONTRACT-P5.md §7 class C was 64 slots at the P5b contract commit; a P5b flip "
"moves one slot from C to B and the two counts must still sum to 64");
static_assert(kLocallyAnsweredSlots + kEmittedSlots + kUnmigratedSlots == kRemoteEmitSlotCount,
"the three classes no longer partition the 71 slots");
@@ -812,6 +1106,27 @@ namespace MobileGL::MG_Remote::Client {
// ---- class B
table.GL.Clear = &EmitClear;
table.GL.DrawArrays = &EmitDrawArrays;
// ---- P5b d1: the nineteen draw slots, all on draw_vbo (CONTRACT-P5B.md §2 d1)
table.GL.DrawElements = &EmitDrawElements;
table.GL.DrawElementsBaseVertex = &EmitDrawElementsBaseVertex;
table.GL.DrawRangeElements = &EmitDrawRangeElements;
table.GL.DrawRangeElementsBaseVertex = &EmitDrawRangeElementsBaseVertex;
table.GL.DrawElementsInstanced = &EmitDrawElementsInstanced;
table.GL.DrawElementsInstancedBaseVertex = &EmitDrawElementsInstancedBaseVertex;
table.GL.DrawElementsInstancedBaseInstance = &EmitDrawElementsInstancedBaseInstance;
table.GL.DrawElementsInstancedBaseVertexBaseInstance =
&EmitDrawElementsInstancedBaseVertexBaseInstance;
table.GL.DrawArraysInstanced = &EmitDrawArraysInstanced;
table.GL.DrawArraysInstancedBaseInstance = &EmitDrawArraysInstancedBaseInstance;
table.GL.MultiDrawArrays = &EmitMultiDrawArrays;
table.GL.MultiDrawElements = &EmitMultiDrawElements;
table.GL.MultiDrawElementsBaseVertex = &EmitMultiDrawElementsBaseVertex;
table.GL.DrawArraysIndirect = &EmitDrawArraysIndirect;
table.GL.DrawElementsIndirect = &EmitDrawElementsIndirect;
table.GL.MultiDrawArraysIndirect = &EmitMultiDrawArraysIndirect;
table.GL.MultiDrawElementsIndirect = &EmitMultiDrawElementsIndirect;
table.GL.MultiDrawArraysIndirectCount = &EmitMultiDrawArraysIndirectCount;
table.GL.MultiDrawElementsIndirectCount = &EmitMultiDrawElementsIndirectCount;
table.GL.ReadPixels = &EmitReadPixels;
table.GL.BlitFramebuffer = &EmitBlitFramebuffer;
table.Present = &EmitPresent;
@@ -890,4 +1205,95 @@ namespace MobileGL::MG_Remote::Client {
}
}
// =============================================================================
// P5b d1 - the draw family's record plan, at namespace scope so the unit cases drive
// exactly what the emitters above call (R-16).
// =============================================================================
Uint8 RemoteIndexSizeFor(GLenum indexType) {
switch (indexType) {
case GL_UNSIGNED_BYTE: return 1;
case GL_UNSIGNED_SHORT: return 2;
case GL_UNSIGNED_INT: return 4;
default: return 0;
}
}
MG_Pipe::MGPDrawInfo PlanDrawInfo(GLenum mode, Uint8 indexSize, GLsizei instanceCount,
GLuint baseInstance, Uint32 numDraws,
const RemoteDrawBindings& bindings) {
MG_Pipe::MGPDrawInfo info{};
info.Mode = static_cast<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
+55
View File
@@ -161,4 +161,59 @@ namespace MobileGL::MG_Remote::Client {
// 0=OK / 1=DECLINED / 2=ERROR as `status`.
Bool ReadbackReplyIsComplete(Int32 status, Uint64 replySize, Uint64 expected);
// =============================================================================
// P5b d1 - the draw family's record plan (MG_Remote/CONTRACT-P5B.md §2 d1)
// =============================================================================
//
// The nineteen indexed / instanced / multi-draw / indirect entry points all ride draw_vbo
// (59), and the ONLY thing that differs per entry point is how the GL arguments become the
// record's fields. That derivation is split out of the emitters as three pure functions
// over a snapshot of the bindings the emitter read from the GL context, so a unit case can
// drive the PRODUCTION derivation with synthetic bindings (R-16) while the integration lane
// proves the bindings are read from the right slots. The emitters compose these and add
// nothing but the hooks, the E2 draw-drop control and the staging of a client index array.
// What the emitter reads from the GL context before it plans a draw.
struct RemoteDrawBindings {
// The VAO's GL_ELEMENT_ARRAY_BUFFER: bound or not, and its handle when bound (the same
// handle set_index_buffer carried at validate). Not bound means `indices` is a client
// pointer and the emitter stages the bytes (kDrawHasUserIndices).
Bool ElementBufferBound = false;
MG_Pipe::MGPipeHandle ElementBuffer = MG_Pipe::kMGPipeNullHandle;
// The bound GL_DRAW_INDIRECT_BUFFER and GL_PARAMETER_BUFFER, null when unbound.
MG_Pipe::MGPipeHandle DrawIndirectBuffer = MG_Pipe::kMGPipeNullHandle;
MG_Pipe::MGPipeHandle ParameterBuffer = MG_Pipe::kMGPipeNullHandle;
// GL_PRIMITIVE_RESTART / GL_PRIMITIVE_RESTART_FIXED_INDEX and the application's index,
// carried verbatim (informational in P5b: the backend reads its own barrier-pulled copy).
Bool PrimitiveRestart = false;
Uint32 RestartIndex = 0;
};
// 1 / 2 / 4 for the three GL index types, 0 for anything else (the frontend has already
// refused those with INVALID_ENUM before the slot is reached).
Uint8 RemoteIndexSizeFor(GLenum indexType);
// The fixed head. `indexSize` 0 = arrays. `instanceCount` is the call's own (1 for a
// non-instanced entry point) and `baseInstance` its gl_BaseInstance value; the sink reads
// "instanced" as InstanceCount != 1 || StartInstance != 0, so an instanced call with a
// count of 1 and no base instance is dispatched as the plain draw it is equivalent to.
MG_Pipe::MGPDrawInfo PlanDrawInfo(GLenum mode, Uint8 indexSize, GLsizei instanceCount,
GLuint baseInstance, Uint32 numDraws,
const RemoteDrawBindings& bindings);
// One MGPDrawRange from one (first | indices, count, basevertex). Arrays: Start = first.
// Indexed with an element buffer bound: Start = offset / IndexSize, and FALSE when the byte
// offset is not a whole number of indices or does not fit the record's Uint32 Start - the
// caller refuses by name rather than round. Indexed with no element buffer: Start = 0 and
// the bytes are the client's (the emitter stages `count * IndexSize` of them).
Bool PlanDrawRange(const RemoteDrawBindings& bindings, Uint8 indexSize, const void* indicesOrFirst,
GLsizei count, GLint baseVertex, MG_Pipe::MGPDrawRange& out);
// The kDrawIsIndirect second tail: the bound GL_DRAW_INDIRECT_BUFFER handle, the call's
// `indirect` byte offset, the stride and the draw count; for the *IndirectCount forms the
// bound GL_PARAMETER_BUFFER handle and the byte offset the call spells as `drawcount`.
MG_Pipe::MGPDrawIndirect PlanDrawIndirect(const RemoteDrawBindings& bindings, const void* indirect,
GLsizei drawCount, GLsizei stride,
GLintptr parameterOffset, Bool hasParameterBuffer);
} // namespace MobileGL::MG_Remote::Client
+221 -50
View File
@@ -281,65 +281,236 @@ namespace MobileGL::MG_Remote::Server {
std::abort();
}
// P5b d1 (MG_Remote/CONTRACT-P5B.md §2 d1): draw_vbo's whole cross product. The record
// carries the GL call verbatim (rule D) and this body reproduces the backend call the
// monolith makes for that shape, reading only the record and the backend's own
// barrier-pulled state; the twenty GL entry points collapse onto the arms below:
//
// kDrawIsIndirect arrays: DrawArraysIndirect (DrawCount 1, Stride 0) /
// MultiDrawArraysIndirect / MultiDrawArraysIndirectCount (a parameter
// buffer named); indexed: the three Elements twins
// NumDraws != 1 MultiDrawArrays / MultiDrawElementsBaseVertex, the two arrays
// rebuilt from the ranges into bounded locals (rule C)
// arrays, one range DrawArrays / DrawArraysInstanced / DrawArraysInstancedBaseInstance
// indexed, one range DrawElementsBaseVertex (the P5 arm, unchanged, bias 0 for a plain
// DrawElements) / DrawRangeElements[BaseVertex] under
// kDrawHasIndexRange / the four DrawElementsInstanced* by whether a
// base vertex and a base instance are non-zero
// kDrawHasUserIndices the `indices` argument is the resolved SEG_STAGE run instead of an
// element-buffer offset (the client staged a client index array)
//
// "Instanced" is InstanceCount != 1 || StartInstance != 0: an instanced call with a count
// of 1 and no base instance is the plain draw it is equivalent to, and a count of 0 must
// NOT collapse onto the plain draw (it draws nothing, the plain draw would draw once).
//
// What is still refused by name (the census's own grep family): a multi-draw that arrived
// with a span (the client refuses "MultiDrawElements+CLIENT_INDICES" first; P8's
// HostResolve.cpp flattens it) and a multi-draw that claims instancing (no GL entry point
// produces one; the client never sends it).
Bool ServerVerbSink::OnDrawVbo(const MG_Pipe::MGPDrawInfo& info,
const MG_Pipe::MGPDrawRange* ranges,
const MG_Pipe::MGHostSpan* userIndices,
const MG_Pipe::MGPDrawIndirect* indirect) {
// The witness first, before the backend is consulted, so a unit process with no
// backend object still sees the wire's fields (PipeApplier.h LastDraw).
m_lastDraw = LastDrawRecord{};
m_lastDraw.Info = info;
if (ranges != nullptr && info.NumDraws != 0) m_lastDraw.FirstRange = ranges[0];
if (userIndices != nullptr) {
m_lastDraw.HadUserIndices = true;
m_lastDraw.UserIndexBytes = userIndices->Size;
}
if (indirect != nullptr) {
m_lastDraw.HadIndirect = true;
m_lastDraw.Indirect = *indirect;
}
++m_drawRecords;
const MG_Backend::GlobalBackendFunctionsTable* table = Table("draw_vbo");
if (table == nullptr) return false;
// P5b d1 (CONTRACT-P5B.md): the three arms the contract gives this sink and P5 did not
// implement are DECLINED BY NAME until d1 lands them - the same names the census greps,
// so the lane's first-blocker table reads the server's gap as the slot it is.
if (indirect != nullptr) {
ServerUnmigratedVerbFatal(info.IndexSize == 0 ? "MultiDrawArraysIndirect"
: "MultiDrawElementsIndirect");
}
if (userIndices != nullptr) {
// The span is validated and names a SEG_STAGE run the client staged (d1's rule for
// client-side index arrays); resolving it is MG_Pipe::MGPipeHostBytes and passing
// the pointer to gl.DrawElements is d1's body. Declined by name until then.
ServerUnmigratedVerbFatal("DrawElements+CLIENT_INDICES");
}
if (ranges == nullptr || info.NumDraws == 0) return false;
// P5 IMPLEMENTS THE TWO SHAPES ITS REDUCED PATH USES AND DECLINES THE REST BY NAME.
// draw_vbo collapses all twenty draw entry points, and picking the right one needs the
// instancing / base-vertex / base-instance / multi-draw cross product. TriangleScenario
// is a single non-instanced array draw and OpenRA's are single indexed draws from a
// bound element buffer; the rest are d1's (CONTRACT-P5B.md d1 says which GL entry each
// shape of the record dispatches to).
const MG_Backend::GLFunctionsTable& gl = table->GL;
const Bool instanced = info.InstanceCount > 1 || info.StartInstance != 0;
if (info.NumDraws != 1) {
ServerUnmigratedVerbFatal(info.IndexSize == 0 ? "MultiDrawArrays" : "MultiDrawElements");
const auto mode = static_cast<GLenum>(info.Mode);
GLenum indexType = 0;
switch (info.IndexSize) {
case 0: break; // arrays
case 1: indexType = GL_UNSIGNED_BYTE; break;
case 2: indexType = GL_UNSIGNED_SHORT; break;
case 4: indexType = GL_UNSIGNED_INT; break;
default:
// IndexSize is "0 = arrays, else 1 / 2 / 4" (MGPipeTypes.h) and nothing else is a
// legal width; defaulting to 4 would read past the element buffer.
Wire::WireProtocolFatalAt("MGPDrawInfo::IndexSize", info.IndexSize, 4);
}
if (instanced) {
ServerUnmigratedVerbFatal(info.IndexSize == 0 ? "DrawArraysInstanced"
: "DrawElementsInstanced");
}
const MG_Pipe::MGPDrawRange& range = ranges[0];
if (info.IndexSize == 0) {
if (gl.DrawArrays == nullptr) return false;
gl.DrawArrays(static_cast<GLenum>(info.Mode), static_cast<GLint>(range.Start),
static_cast<GLsizei>(range.Count));
} else {
if (gl.DrawElementsBaseVertex == nullptr) return false;
GLenum indexType = GL_UNSIGNED_INT;
switch (info.IndexSize) {
case 1: indexType = GL_UNSIGNED_BYTE; break;
case 2: indexType = GL_UNSIGNED_SHORT; break;
case 4: indexType = GL_UNSIGNED_INT; break;
default:
// IndexSize is "0 = arrays, else 1 / 2 / 4" (MGPipeTypes.h:1323) and nothing
// else is a legal width; defaulting to 4 would read past the element buffer.
Wire::WireProtocolFatalAt("MGPDrawInfo::IndexSize", info.IndexSize, 4);
// ---- the indirect family: the block is the whole description --------------------
if (indirect != nullptr) {
// The layout already refused a record that sets both flags or declares ranges
// beside the block, so NumDraws is 0 and there is no span here.
const auto offset = reinterpret_cast<const void*>(static_cast<std::uintptr_t>(indirect->Offset));
const auto drawCount = static_cast<GLsizei>(indirect->DrawCount);
const auto stride = static_cast<GLsizei>(indirect->Stride);
const Bool counted = !MG_Pipe::MGPipeHandleIsNull(indirect->ParameterBuffer);
const auto parameterOffset = static_cast<GLintptr>(indirect->ParameterOffset);
// glMultiDraw*Indirect with drawcount 1 and stride 0 IS glDraw*Indirect by GL's own
// definition, so the single-draw entry point is the one the monolith reaches for
// the single-draw call and nothing is lost for the multi-draw spelling of it.
const Bool single = !counted && drawCount == 1 && stride == 0;
if (info.IndexSize == 0) {
if (counted) {
if (gl.MultiDrawArraysIndirectCount == nullptr) return false;
gl.MultiDrawArraysIndirectCount(mode, offset, parameterOffset, drawCount, stride);
} else if (single) {
if (gl.DrawArraysIndirect == nullptr) return false;
gl.DrawArraysIndirect(mode, offset);
} else {
if (gl.MultiDrawArraysIndirect == nullptr) return false;
gl.MultiDrawArraysIndirect(mode, offset, drawCount, stride);
}
} else {
if (counted) {
if (gl.MultiDrawElementsIndirectCount == nullptr) return false;
gl.MultiDrawElementsIndirectCount(mode, indexType, offset, parameterOffset,
drawCount, stride);
} else if (single) {
if (gl.DrawElementsIndirect == nullptr) return false;
gl.DrawElementsIndirect(mode, indexType, offset);
} else {
if (gl.MultiDrawElementsIndirect == nullptr) return false;
gl.MultiDrawElementsIndirect(mode, indexType, offset, drawCount, stride);
}
}
// Start is the FIRST INDEX, so the byte offset into the bound element buffer is
// Start * IndexSize - the same arithmetic PipeFill's emitter inverted.
const auto offset = static_cast<std::uintptr_t>(range.Start) * info.IndexSize;
gl.DrawElementsBaseVertex(static_cast<GLenum>(info.Mode),
static_cast<GLsizei>(range.Count), indexType,
reinterpret_cast<const void*>(offset), range.IndexBias);
++m_draws;
return true;
}
if (ranges == nullptr || info.NumDraws == 0) return false;
const Bool instanced = info.InstanceCount != 1 || info.StartInstance != 0;
const auto instanceCount = static_cast<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;
return true;
+30 -1
View File
@@ -127,7 +127,8 @@ namespace MobileGL::MG_Remote::Server {
// f1 OnGenerateMipmap, OnCopyFramebufferToTexture ("CopyTexImage2D" /
// "CopyTexSubImage2D"), and OnClear's four non-Whole kinds (live already)
// d1 OnDrawVbo above: the indirect tail, the user-index span, NumDraws > 1 and the
// instanced arms (declined by name today)
// instanced arms - LIVE since d1 v1 (every shape the client's nineteen draw slots
// produce dispatches to the backend slot CONTRACT-P5B.md §2 d1 names)
Bool OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) override;
Bool OnMemoryBarrier(const MG_Pipe::MGPMemoryBarrier& barrier) override;
Bool OnResourceCopyRegion(const MG_Pipe::MGPCopyRegion& copy) override;
@@ -158,6 +159,25 @@ namespace MobileGL::MG_Remote::Server {
// DstSize is exactly the heap overflow codex 1 found, one field over.
Uint64 ReadbackScratchBytes() const { return static_cast<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:
const MG_Backend::GlobalBackendFunctionsTable* Table(const char* verb) const;
@@ -173,6 +193,15 @@ namespace MobileGL::MG_Remote::Server {
// ReadPixels writes into a caller buffer, so one staging vector per session sits
// between them. Grown, never shrunk, and never handed out past the call.
Vector<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 {
+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
// recomputed here, so a table that lost an emitter cannot look like one that never had it.
EXPECT_EQ(LocallyAnsweredSlotCount(), 2u);
EXPECT_EQ(ImplementedVerbCount(), 5u);
EXPECT_EQ(UnmigratedSlotCount(), 64u);
// P5b d1 moved the nineteen draw slots from class C to class B (CONTRACT-P5B.md §7: B = 5
// + the packages' flips, C = 64 - the same). Each P5b package raises B and lowers C by the
// same number, so the two numbers here move together and the sum below never does.
EXPECT_EQ(ImplementedVerbCount(), 5u + 19u);
EXPECT_EQ(UnmigratedSlotCount(), 64u - 19u);
EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(),
kRemoteEmitSlotCount);
}
@@ -229,22 +232,42 @@ TEST(RemoteEmitTable, AnUnmigratedSlotAbortsAndNamesItself) {
// THE DEATH TEST ON THE UnmigratedVerbFatal ARM. It asserts the exact wording, not merely
// that the child died: a control that trips on any abort is satisfied by the wrong abort,
// which is one of the three shapes R-16 was written after.
// GetTexImage is the wave-3 tail (CONTRACT-P5B.md §7): no P5b package flips it, so this
// case keeps its subject across the four P5b landings. (It was DrawElements until d1 made
// that a class-B emitter.)
const ChildResult r = RunInChild([] {
RemoteEmitTable().GL.GetTexImage(0x0DE1 /*GL_TEXTURE_2D*/, 0, 0x1908 /*GL_RGBA*/,
0x1401 /*GL_UNSIGNED_BYTE*/, nullptr);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"GetTexImage\"}"), std::string::npos) << r.Log;
}
TEST(RemoteEmitTable, EachUnmigratedSlotNamesItsOwnSlot) {
// The half the case above cannot state on its own: that the name in the message is the
// slot's and not a constant. Two different slots, two different names - both from the
// wave-3 tail, for the reason the case above gives.
const ChildResult r = RunInChild([] { RemoteEmitTable().SetSwapInterval(1); });
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"SetSwapInterval\"}"), std::string::npos) << r.Log;
EXPECT_EQ(r.Log.find("GetTexImage"), std::string::npos)
<< "the Fatal message names a slot other than the one that was called:\n"
<< r.Log;
}
// P5b d1: a class-B draw slot with no session aborts Fatal{NoClientSession, "<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([] {
RemoteEmitTable().GL.DrawElements(0x0004 /*GL_TRIANGLES*/, 3, 0x1405 /*GL_UNSIGNED_INT*/,
nullptr);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"DrawElements\"}"), std::string::npos) << r.Log;
}
TEST(RemoteEmitTable, EachUnmigratedSlotNamesItsOwnSlot) {
// The half the case above cannot state on its own: that the name in the message is the
// slot's and not a constant. Two different slots, two different names.
const ChildResult r = RunInChild([] { RemoteEmitTable().GL.GenerateMipmap(0x0DE1); });
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"GenerateMipmap\"}"), std::string::npos) << r.Log;
EXPECT_EQ(r.Log.find("DrawElements"), std::string::npos)
<< "the Fatal message names a slot other than the one that was called:\n"
EXPECT_NE(r.Log.find("Fatal{NoClientSession, \"DrawElements\"}"), std::string::npos) << r.Log;
EXPECT_EQ(r.Log.find("Fatal{UnmigratedVerb"), std::string::npos)
<< "DrawElements is class B since d1 and must not name itself unmigrated:\n"
<< r.Log;
}
@@ -405,6 +428,178 @@ TEST(CapsMirrorTest, TheConsumerBlockDoesNotCollideWithTheFeatureBits) {
// E2's emitter-drop control: the switch itself, driven through the real emitter's own counter
// =====================================================================================
// =====================================================================================
// P5b d1: the draw family's record plan (MG_Remote/CONTRACT-P5B.md §2 d1). These drive the
// PRODUCTION derivation the nineteen emitters call - PlanDrawInfo / PlanDrawRange /
// PlanDrawIndirect over a RemoteDrawBindings snapshot - so the fields on the wire are pinned
// here without a GL context (R-16: the production predicate, not a copy). The bindings' READ
// from the real context is the integration lane's (IndexedDrawFamilyScenario under
// DirectGLES.Split.), and the sink's consumption of the same fields is ServerLoopTest's.
// =====================================================================================
namespace {
MGPipeHandle D1Handle(Uint32 slot) {
MGPipeHandle h{};
h.Slot = slot;
h.Gen = 3;
return h;
}
RemoteDrawBindings D1BoundElementBuffer(Uint32 slot) {
RemoteDrawBindings b{};
b.ElementBufferBound = true;
b.ElementBuffer = D1Handle(slot);
return b;
}
} // namespace
// Red once by: returning `offset` instead of `offset / indexSize` from PlanDrawRange's
// element-buffer arm - Start reads 24 below.
TEST(RemoteDrawPlan, AnElementBufferDrawElementsCarriesTheHandleAndItsOffsetInIndices) {
const RemoteDrawBindings b = D1BoundElementBuffer(17);
const MGPDrawInfo info = PlanDrawInfo(0x0004 /*GL_TRIANGLES*/, RemoteIndexSizeFor(0x1403 /*USHORT*/),
/*instanceCount=*/1, /*baseInstance=*/0, /*numDraws=*/1, b);
EXPECT_EQ(info.Mode, 4u);
EXPECT_EQ(info.IndexSize, 2u);
EXPECT_EQ(info.IndexResource.Slot, 17u) << "IndexResource must be the VAO's element buffer";
EXPECT_EQ(info.InstanceCount, 1u);
EXPECT_EQ(info.StartInstance, 0u);
EXPECT_EQ(info.Flags, 0u) << "a plain DrawElements sets no flag";
EXPECT_EQ(info.MinIndex, ~0u);
EXPECT_EQ(info.NumDraws, 1u);
MGPDrawRange range{};
ASSERT_TRUE(PlanDrawRange(b, 2, reinterpret_cast<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) {
// 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;
+135
View File
@@ -1778,6 +1778,141 @@ TEST(StagedShadowProductionTest, TheStreamingIdiomThroughAPoolReuseIsUploadedNot
}
#endif // !_WIN32
// =====================================================================================
// P5b d1: a draw_vbo record of every d1 shape crosses to the REAL ServerVerbSink with its
// fields intact (MG_Remote/CONTRACT-P5B.md §2 d1). No backend object lives in this process,
// so each record is DECLINED after the sink has witnessed it - which is exactly the point:
// the witness is taken before the backend is consulted, so these cases pin the wire's fields
// and not a backend call. The emitter half (GL args -> these fields) is RemoteClientTest's
// PlanDraw* cases; the backend half is the DirectGLES.Split. IndexedDrawFamilyScenario lane.
// =====================================================================================
namespace {
MG_Pipe::MGPipeHandle D1Handle(Uint32 slot) {
MG_Pipe::MGPipeHandle h{};
h.Slot = slot;
h.Gen = 1;
return h;
}
MG_Pipe::MGPDrawInfo D1DrawInfo(Uint8 indexSize, Uint32 numDraws) {
MG_Pipe::MGPDrawInfo info{};
info.Mode = 0x0004; // GL_TRIANGLES
info.IndexSize = indexSize;
info.InstanceCount = 1;
info.MinIndex = ~0u;
info.MaxIndex = ~0u;
info.NumDraws = numDraws;
return info;
}
} // namespace
// Red once by: dropping `m_lastDraw.Info = info;` from OnDrawVbo - InstanceCount reads 1 and
// IndexBias reads 0 below.
TEST(ServerLoopTest, AnInstancedBaseVertexDrawRecordReachesTheSinkWithItsFieldsIntact) {
ServerFixture fixture;
ASSERT_TRUE(fixture.Handshake());
ASSERT_TRUE(fixture.StartLoop());
MG_Pipe::MGPDrawInfo info = D1DrawInfo(/*indexSize=*/2, /*numDraws=*/1);
info.InstanceCount = 7; // DrawElementsInstancedBaseVertex(..., 7, 5): the Minecraft slot
info.IndexResource = D1Handle(31);
const MG_Pipe::MGPDrawRange range{/*Start=*/12, /*Count=*/36, /*IndexBias=*/5};
ASSERT_TRUE(fixture.EmitAndWaitWithTail(MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info), &range,
sizeof(range)));
const Server::ServerVerbSink& verbs = fixture.session->Applier().Verbs();
EXPECT_EQ(verbs.DrawRecords(), 1u) << "the record did not reach OnDrawVbo";
EXPECT_EQ(verbs.Draws(), 0u) << "no backend object lives here, so the draw must be DECLINED "
"after the witness, never applied";
const Server::ServerVerbSink::LastDrawRecord& seen = verbs.LastDraw();
EXPECT_EQ(seen.Info.IndexSize, 2u);
EXPECT_EQ(seen.Info.InstanceCount, 7u);
EXPECT_EQ(seen.Info.IndexResource.Slot, 31u);
EXPECT_EQ(seen.FirstRange.Start, 12u);
EXPECT_EQ(seen.FirstRange.Count, 36u);
EXPECT_EQ(seen.FirstRange.IndexBias, 5);
EXPECT_FALSE(seen.HadUserIndices);
EXPECT_FALSE(seen.HadIndirect);
EXPECT_FALSE(MG_Pipe::gPipeInputs.ServerStampedVerb());
fixture.Stop();
}
// Red once by: setting `m_lastDraw.UserIndexBytes = 0` in OnDrawVbo's span arm - the byte
// count below reads 0 while HadUserIndices stays true.
TEST(ServerLoopTest, AClientIndexArrayCrossesAsAStagedSpanAndTheSinkSeesItsByteCount) {
ServerFixture fixture;
ASSERT_TRUE(fixture.Handshake());
ASSERT_TRUE(fixture.StartLoop());
// d1's rule for a client index array: the CLIENT stages count * IndexSize bytes and the
// record names the run (Ptr = nullptr, SEG_STAGE); the sink resolves it for the call only.
const Uint32 indices[6] = {0, 1, 2, 2, 3, 0};
const MG_Pipe::MGPBlobRef staged = fixture.encoder.StageBytes(indices, sizeof(indices));
MG_Pipe::MGHostSpan span{};
span.Ptr = nullptr;
span.Seg = staged.Seg;
span.Offset = staged.Offset;
span.Size = staged.Size;
MG_Pipe::MGPDrawInfo info = D1DrawInfo(/*indexSize=*/4, /*numDraws=*/1);
info.Flags = MG_Pipe::kDrawHasUserIndices;
const MG_Pipe::MGPDrawRange range{0, 6, 0};
const Codec::WireTail tails[2] = {{&range, sizeof(range)}, {&span, sizeof(span)}};
const Uint64 seq = fixture.encoder.EncodeRecord(MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info),
tails, 2);
ASSERT_NE(seq, Codec::kInvalidSeq);
fixture.encoder.Publish();
fixture.producer.PublishAndNotify(seq);
ASSERT_EQ(fixture.producer.WaitForApplied(seq, 5000), Transport::SessionWait::Reached);
const Server::ServerVerbSink::LastDrawRecord& seen = fixture.session->Applier().Verbs().LastDraw();
EXPECT_TRUE(seen.HadUserIndices) << "the span did not reach OnDrawVbo";
EXPECT_EQ(seen.UserIndexBytes, sizeof(indices));
EXPECT_EQ(seen.Info.Flags & MG_Pipe::kDrawHasUserIndices, MG_Pipe::kDrawHasUserIndices);
EXPECT_EQ(seen.FirstRange.Count, 6u);
EXPECT_EQ(fixture.session->Applier().Verbs().Draws(), 0u);
fixture.Stop();
}
// Red once by: dropping `m_lastDraw.Indirect = *indirect;` - DrawCount reads 0 below.
TEST(ServerLoopTest, AnIndirectDrawRecordReachesTheSinkWithItsBlockIntact) {
ServerFixture fixture;
ASSERT_TRUE(fixture.Handshake());
ASSERT_TRUE(fixture.StartLoop());
MG_Pipe::MGPDrawInfo info = D1DrawInfo(/*indexSize=*/0, /*numDraws=*/0);
info.Flags = MG_Pipe::kDrawIsIndirect;
MG_Pipe::MGPDrawIndirect block{};
block.Buffer = D1Handle(40);
block.ParameterBuffer = D1Handle(41); // the *IndirectCount shape
block.Offset = 32;
block.ParameterOffset = 8;
block.Stride = 16;
block.DrawCount = 3;
const Codec::WireTail tails[2] = {{nullptr, 0}, {&block, sizeof(block)}};
const Uint64 seq = fixture.encoder.EncodeRecord(MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info),
tails, 2);
ASSERT_NE(seq, Codec::kInvalidSeq);
fixture.encoder.Publish();
fixture.producer.PublishAndNotify(seq);
ASSERT_EQ(fixture.producer.WaitForApplied(seq, 5000), Transport::SessionWait::Reached);
const Server::ServerVerbSink::LastDrawRecord& seen = fixture.session->Applier().Verbs().LastDraw();
EXPECT_TRUE(seen.HadIndirect) << "the indirect block did not reach OnDrawVbo";
EXPECT_FALSE(seen.HadUserIndices);
EXPECT_EQ(seen.Indirect.Buffer.Slot, 40u);
EXPECT_EQ(seen.Indirect.ParameterBuffer.Slot, 41u);
EXPECT_EQ(seen.Indirect.Offset, 32u);
EXPECT_EQ(seen.Indirect.ParameterOffset, 8u);
EXPECT_EQ(seen.Indirect.Stride, 16u);
EXPECT_EQ(seen.Indirect.DrawCount, 3u);
EXPECT_EQ(seen.Info.NumDraws, 0u);
fixture.Stop();
}
int main(int argc, char** argv) {
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
// write, and caches the FILE*. The name carries this process's pid, because