mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-18 00:58:30 +09:00
[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:
@@ -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
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
|
||||||
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) {
|
switch (info.IndexSize) {
|
||||||
|
case 0: break; // arrays
|
||||||
case 1: indexType = GL_UNSIGNED_BYTE; break;
|
case 1: indexType = GL_UNSIGNED_BYTE; break;
|
||||||
case 2: indexType = GL_UNSIGNED_SHORT; break;
|
case 2: indexType = GL_UNSIGNED_SHORT; break;
|
||||||
case 4: indexType = GL_UNSIGNED_INT; break;
|
case 4: indexType = GL_UNSIGNED_INT; break;
|
||||||
default:
|
default:
|
||||||
// IndexSize is "0 = arrays, else 1 / 2 / 4" (MGPipeTypes.h:1323) and nothing
|
// IndexSize is "0 = arrays, else 1 / 2 / 4" (MGPipeTypes.h) and nothing else is a
|
||||||
// else is a legal width; defaulting to 4 would read past the element buffer.
|
// legal width; defaulting to 4 would read past the element buffer.
|
||||||
Wire::WireProtocolFatalAt("MGPDrawInfo::IndexSize", info.IndexSize, 4);
|
Wire::WireProtocolFatalAt("MGPDrawInfo::IndexSize", info.IndexSize, 4);
|
||||||
}
|
}
|
||||||
// Start is the FIRST INDEX, so the byte offset into the bound element buffer is
|
|
||||||
// Start * IndexSize - the same arithmetic PipeFill's emitter inverted.
|
// ---- the indirect family: the block is the whole description --------------------
|
||||||
const auto offset = static_cast<std::uintptr_t>(range.Start) * info.IndexSize;
|
if (indirect != nullptr) {
|
||||||
gl.DrawElementsBaseVertex(static_cast<GLenum>(info.Mode),
|
// The layout already refused a record that sets both flags or declares ranges
|
||||||
static_cast<GLsizei>(range.Count), indexType,
|
// beside the block, so NumDraws is 0 and there is no span here.
|
||||||
reinterpret_cast<const void*>(offset), range.IndexBias);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++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;
|
++m_draws;
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user