[Merge] (P5b): integrate t2 and reconcile all migrated emitter families

This commit is contained in:
2026-09-16 13:27:15 -04:00
12 changed files with 566 additions and 47 deletions
+17 -1
View File
@@ -150,7 +150,23 @@ namespace MobileGL::MG_Backend {
// kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes must be 0 for the whole of P5 // kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes must be 0 for the whole of P5
// by ruling - they are the only two things that ask for an MGHostSpan, and 0 is // by ruling - they are the only two things that ask for an MGHostSpan, and 0 is
// what keeps every one of them out of the first IPC frame (contract table 0). // what keeps every one of them out of the first IPC frame (contract table 0).
session.SetCapabilityBits(0); //
// P5b t2 (CONTRACT-P5B.md §6.5) PUBLISHES THE ONE BIT P5b ADDS, and this is the
// only place that can: the question kCapBackendOwnsXfbCapture answers is "does the
// SERVER's backend own the transform-feedback capture", and the server's table is
// visible here and nowhere on the client. It is read straight off the table
// ServerLoop::CreateBackend just built - Espryt registers XfbImpl::EndTransformFeedback
// (BackendObject_DirectGLES.cpp:1458) and Magma registers no XFB slot at all - so the
// bit is a statement about THIS backend rather than about a build option, which is
// what makes it survive a backend switch. The client reads it through
// MGL_BACKEND_SLOT_CAP at GL_Drawing.cpp's FixupGsStripCaptureOrder.
Uint64 capBits = 0;
if (const MG_Backend::BackendObject* serverBackend = loop.Backend();
serverBackend != nullptr &&
serverBackend->GetBackendFunctions().GL.EndTransformFeedback != nullptr) {
capBits |= MG_Pipe::kCapBackendOwnsXfbCapture;
}
session.SetCapabilityBits(capBits);
session.SetBackend(loop.Backend()); session.SetBackend(loop.Backend());
// 3. the handshake, the four segments, and - at its end - the apply thread. // 3. the handshake, the four segments, and - at its end - the apply thread.
+15 -1
View File
@@ -15,6 +15,12 @@
#if MOBILEGL_BUILD_DISAGGREGATED #if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Client/GpuWritePending.h> #include <MG_Remote/Client/GpuWritePending.h>
#endif #endif
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
// client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test
// that decides which of its two spellings a site takes; in a pull build both expand to exactly
// the check they replaced. P5b t2 converts ONE site in this file - the capture-ownership probe
// in FixupGsStripCaptureOrder - for the reason CONTRACT-P5B.md §6.5 gives.
#include <MG_Remote/Client/SlotCaps.h>
#include "../Getter/GL_Getter.h" #include "../Getter/GL_Getter.h"
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
@@ -1287,7 +1293,15 @@ namespace MobileGL::MG_Impl::GLImpl {
// Only Vulkan-order captures need this. A backend that runs the capture on its // Only Vulkan-order captures need this. A backend that runs the capture on its
// own GL/ES driver (it owns the span, hence the EndTransformFeedback entry) has // own GL/ES driver (it owns the span, hence the EndTransformFeedback entry) has
// already produced GL's vertex order, and reordering it again would corrupt it. // already produced GL's vertex order, and reordering it again would corrupt it.
if (MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback != nullptr) { //
// P5b t2 (CONTRACT-P5B.md §6.5): UNDER SPLIT THE TABLE THIS USED TO ASK IS THE CLIENT'S
// EMIT TABLE, whose EndTransformFeedback slot t2 just made non-null for every server -
// so the raw null check would answer "the backend owns the capture" even against Magma,
// which registers no XFB slot at all, and would skip a reorder Magma needs. The question
// is about the SERVER's table, so it is answered from the bit the server publishes.
// Under monolith (and in a pull build) this expands to the null check it replaced,
// character for character.
if (MGL_BACKEND_SLOT_CAP(EndTransformFeedback, MG_Pipe::kCapBackendOwnsXfbCapture)) {
return; return;
} }
if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) { if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) {
@@ -1984,6 +1984,53 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
) )
endforeach() endforeach()
# ---- P5b t2: the transform-feedback spans, the XFB object bind, the patch parameter -------
#
# MG_Remote/CONTRACT-P5B.md §2 t2. The three P5 lanes above are the REDUCED PATH's targets
# (a clear, a triangle, a persistent map); these two are the first lane entries whose GREEN
# DEPENDS ON A t2 RECORD HAVING CROSSED AND BEEN APPLIED BY THE BACKEND, which is the only
# statement a package flipping a verb can make that a unit case cannot:
#
# TessellationXfbCaptureScenario covers BOTH halves of t2 in one workload. Every case
# calls glPatchParameteri(GL_PATCH_VERTICES, n) and then draws GL_PATCHES into a
# transform-feedback capture, and asserts the CAPTURED BYTES. A patch_parameter (73)
# that did not reach the server tessellates at the previous patch size and the capture
# is the wrong length; a begin/end_stream_output (62/63) that did not reach it leaves
# the buffer holding the scenario's poison value, which is what those cases print.
# XfbCaptureBufferReuseScenario is the span family alone, across four buffer lifetimes
# (a buffer per span, one immutable-storage buffer, one respecified buffer, a
# respecification that changes the capture size). It is the case that would notice a
# span whose END crossed but whose BEGIN did not, because the second span's bytes
# would be the first span's.
#
# Both are DirectGLES only, deliberately, and the reason is measured rather than assumed:
# under Magma the capture is written into the server's resident slice and there is no route
# back, because MG_Backend/Init.cpp's ConsumedSubsystemsFor(DirectVulkan) withholds
# kMGPipeSubsystemResources, so BufferObject::SyncGpuWrites() emits no resource_readback.
# Every DirectVulkan XFB capture entry in the inproc census fails on exactly that (t2-v1.md
# §"the 51"), and it retires with P7 (Magma's resource family) / P9 (the readback carrier) -
# not here. A DirectVulkan arm of this lane would be red for a reason t2 cannot fix.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.Split."
TEST_LIST MGL_SPLIT_T2_TESS_TESTS
TEST_FILTER "TessellationXfbCaptureScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-split"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.Split."
TEST_LIST MGL_SPLIT_T2_XFB_TESTS
TEST_FILTER "XfbCaptureBufferReuseScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-split"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}"
)
# 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
@@ -1,7 +1,10 @@
# Included by CTest after all GoogleTest discovery files (ID-53). # Included by CTest after all GoogleTest discovery files (ID-53).
# CTest appends ENVIRONMENT here; keep all previously discovered lane settings. # CTest appends ENVIRONMENT here; keep all previously discovered lane settings.
file(MAKE_DIRECTORY "@CMAKE_CURRENT_BINARY_DIR@/split-logs") file(MAKE_DIRECTORY "@CMAKE_CURRENT_BINARY_DIR@/split-logs")
foreach(entry IN LISTS MGL_SPLIT_CLEAR_TESTS MGL_SPLIT_TRIANGLE_TESTS MGL_SPLIT_PMAP_TESTS) # P5b t2's two lanes ride the same rule: one private log path per entry, or
# SplitLogPaths.PrivateAndDistinct is red for them (ID-53).
foreach(entry IN LISTS MGL_SPLIT_CLEAR_TESTS MGL_SPLIT_TRIANGLE_TESTS MGL_SPLIT_PMAP_TESTS
MGL_SPLIT_T2_TESS_TESTS MGL_SPLIT_T2_XFB_TESTS)
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
"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()
+12
View File
@@ -121,6 +121,18 @@ namespace MobileGL::MG_Pipe {
// The server packs named uniform blocks into its own ring and therefore needs the // The server packs named uniform blocks into its own ring and therefore needs the
// host bytes of a set_shader_buffers(Uniform) range (D-B8). // host bytes of a set_shader_buffers(Uniform) range (D-B8).
kCapNeedsHostUboBytes = 1ull << 8, kCapNeedsHostUboBytes = 1ull << 8,
// P5b t2 (CONTRACT-P5B.md §6.5), the one cap bit P5b adds. The SERVER's backend owns
// the transform-feedback capture, i.e. its own table registers EndTransformFeedback.
// FixupGsStripCaptureOrder (GL_Drawing.cpp:1290) asks that question to decide whether
// the CLIENT must reorder the captured records into GL's vertex order, and it used to
// ask it of gBackendFunctionsTable.GL.EndTransformFeedback - which under split is the
// client's EMIT table, where the slot is non-null the moment t2 installs an emitter,
// for every server. A client talking to Espryt would then be right by accident and a
// client talking to Magma (which registers no XFB slot at all, so the sink DECLINES)
// would skip a reorder Magma needs and hand the application a silently corrupt capture
// buffer. So the answer is the SERVER's table, published as a bit and read through
// MGL_BACKEND_SLOT_CAP. The first of the "XFB span family" bits SlotCaps.h predicted.
kCapBackendOwnsXfbCapture = 1ull << 9,
}; };
struct MGPCaps { struct MGPCaps {
+5 -3
View File
@@ -44,9 +44,11 @@
namespace MobileGL::MG_Remote { namespace MobileGL::MG_Remote {
// The consumer mask may not collide with the MGPCapBits below it. kCapNeedsHostUboBytes // The consumer mask may not collide with the MGPCapBits below it. The HIGHEST allocated
// is 1<<8 today; this asserts the gap stays a gap rather than trusting the comment. // feature bit is kCapBackendOwnsXfbCapture, 1<<9 (P5b t2 raised it from
static_assert((static_cast<Uint64>(MG_Pipe::kCapNeedsHostUboBytes) & kMGCapsConsumerMask) == 0, // kCapNeedsHostUboBytes' 1<<8); this asserts the gap stays a gap rather than trusting the
// comment, so it has to name whichever bit is currently the top one.
static_assert((static_cast<Uint64>(MG_Pipe::kCapBackendOwnsXfbCapture) & kMGCapsConsumerMask) == 0,
"an MGPCapBit has grown into CallMask's consumer block (bits 32..47)"); "an MGPCapBit has grown into CallMask's consumer block (bits 32..47)");
static_assert(MGCapsServerConsumes(MGCapsConsumerBits(MG_Pipe::kMGPipeSubsystemResources), static_assert(MGCapsServerConsumes(MGCapsConsumerBits(MG_Pipe::kMGPipeSubsystemResources),
MG_Pipe::kMGPipeSubsystemResources), MG_Pipe::kMGPipeSubsystemResources),
+150 -12
View File
@@ -16,6 +16,12 @@
// 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.
// //
// P5b MOVES SLOTS FROM C TO B, ONE PACKAGE AT A TIME (MG_Remote/CONTRACT-P5B.md §7). The three
// numbers above are the partition AT THE P5b CONTRACT COMMIT and they are the ones the contract
// states; the arithmetic below is what the tree currently has, and the per-package ownership
// assertions say which package moved which slot. On this head t2 has landed: class B is 5 + 6
// and class C is 58.
//
// THE PRE-VERB HOOKS RUN BEFORE THE RECORD, NEVER AFTER (b1, ID-18). PushPersistentMapsBeforeVerb // THE PRE-VERB HOOKS RUN BEFORE THE RECORD, NEVER AFTER (b1, ID-18). PushPersistentMapsBeforeVerb
// publishes the bytes an application wrote through a coherent map with no API call at all, and // publishes the bytes an application wrote through a coherent map with no API call at all, and
// MarkGpuWritesForDraw builds the conservative GPU-write set the client now owns. Both describe // MarkGpuWritesForDraw builds the conservative GPU-write set the client now owns. Both describe
@@ -897,6 +903,131 @@ namespace MobileGL::MG_Remote::Client {
nullptr, 0, nullptr, 0, nullptr); nullptr, 0, nullptr, 0, nullptr);
} }
// CLASS B - P5b package t2: the transform-feedback spans, the XFB object bind and the
// tessellation patch parameter (MG_Remote/CONTRACT-P5B.md §2 t2).
// =============================================================================
//
// SIX SLOTS, SIX ROWS, AND NOT ONE OF THEM STARTS A SHADER. Every one of the six is a
// control call - it opens, closes, pauses, resumes or re-targets a capture span, or sets
// the patch size the next tessellation draw uses - so each takes BeforeReadOnlyVerb(),
// which is CONTRACT-P5B.md §4's rule for "a verb that reads buffers but starts no
// shader" naming the XFB and patch controls by hand. The GPU-WRITE MARK FOR THE CAPTURE
// TARGETS IS NOT TAKEN HERE and that is deliberate: it belongs at the END of the span,
// after the record and before GLContext::EndTransformFeedback clears the live bindings,
// which is exactly where b1 already put it (GL_Drawing.cpp's
// MarkEndTransformFeedbackCaptureTargets). Taking it here as well would mark the same
// buffers twice and taking it INSTEAD of there would mark nothing.
//
// RULE D (CONTRACT-P5B.md §0): each record carries the GL arguments the frontend handed
// the backend slot and nothing that is a READING of them. The capture program, the
// capture-buffer bindings, the patch state and the bound XFB object all stay
// BARRIER-PULLED - the server's backend reads the client's gPipeInputs fill of the
// moment, which MGP_FILL at each call site has just written and the verb barrier holds
// still (R-1). That is why these are six two-line emitters and not an XFB protocol.
//
// WHAT MUST HAVE CROSSED BEFORE begin_stream_output, since it is the ordering question
// this package was asked: the capture buffers' own resource records (emitted at their
// own call sites through the resource family, long before this point), the program
// (the CSO/program family, likewise), and the buffer BINDINGS - which do not cross as a
// record at all, because set_stream_output_targets (39) has no producer and no consumer
// and CONTRACT-P5B.md §2 rules it NOT required for t2: under the barrier the server's
// StartPendingTransformFeedback reads them through the kXfbSpan/kDraw pulls
// (GetTransformFeedbackProgram, GetBufferBindingPoint). Producing that row is P9's.
void EmitBeginTransformFeedback(GLenum primitiveMode) {
ClientSession& session = RequireSession("BeginTransformFeedback");
BeforeReadOnlyVerb();
MG_Pipe::MGPStreamOutputBegin record{};
// The GL token verbatim (contract table 0's "GL enums on the wire"): the sink hands
// it to the backend slot that takes it, and nothing between here and there reads it.
record.PrimitiveMode = static_cast<Uint32>(primitiveMode);
session.EmitAndWait(MG_Pipe::MGPWireOp::BeginStreamOutput, &record, sizeof(record),
nullptr, 0, nullptr, 0, nullptr);
}
void EmitEndTransformFeedback() {
ClientSession& session = RequireSession("EndTransformFeedback");
BeforeReadOnlyVerb();
MG_Pipe::MGPXfbAccounting record{};
// THE ACCOUNTING IS THE CLIENT'S OWN AND IT IS INFORMATIONAL ON THIS SIDE OF THE
// WIRE: end_stream_output's backend call takes no arguments, and the three numbers
// are what the frontend has counted over this span (CONTRACT-P5B.md §2 t2, the
// companions row). They travel because the row has carried them since P4a and
// because they are what a server-side scatter would need when P9 lands one; the
// sink today calls GL.EndTransformFeedback() and reads none of them. Read here,
// BEFORE GLContext::EndTransformFeedback resets the counters at the call site.
const auto& context = *MG_State::pGLContext;
record.CapturedVertices = context.GetTransformFeedbackCapturedVertices();
record.PrimitivesWritten = context.GetTransformFeedbackPrimitiveCounter();
record.PrimitiveMode = static_cast<Uint32>(context.GetTransformFeedbackPrimitiveMode());
session.EmitAndWait(MG_Pipe::MGPWireOp::EndStreamOutput, &record, sizeof(record),
nullptr, 0, nullptr, 0, nullptr);
}
void EmitPauseTransformFeedback() {
ClientSession& session = RequireSession("PauseTransformFeedback");
BeforeReadOnlyVerb();
// Reserved IS zero and the contract says so (MGPStreamOutputControl{Reserved = 0}).
// The row exists to BE the verb boundary - the stamp the server puts up before the
// sink runs - not to carry anything.
MG_Pipe::MGPStreamOutputControl record{};
record.Reserved = 0;
session.EmitAndWait(MG_Pipe::MGPWireOp::PauseStreamOutput, &record, sizeof(record),
nullptr, 0, nullptr, 0, nullptr);
}
void EmitResumeTransformFeedback() {
ClientSession& session = RequireSession("ResumeTransformFeedback");
BeforeReadOnlyVerb();
MG_Pipe::MGPStreamOutputControl record{};
record.Reserved = 0;
session.EmitAndWait(MG_Pipe::MGPWireOp::ResumeStreamOutput, &record, sizeof(record),
nullptr, 0, nullptr, 0, nullptr);
}
void EmitBindTransformFeedback(GLuint name) {
ClientSession& session = RequireSession("BindTransformFeedback");
BeforeReadOnlyVerb();
MG_Pipe::MGPStreamOutputBind record{};
// THE GL NAME IS NOT AN IDENTITY (ARCHITECTURE 4.2.1) and is carried anyway, because
// it is the key the backend has always used: Espryt indexes its driver objects by it
// (XfbImpl::g_xfbObjects[name], DirectGLES.cpp:1401) and generates the ES object on
// first bind. Name 0 is the default object, which is why the field is not a handle.
record.GlName = static_cast<Uint32>(name);
// Beside it, the identity that WILL dispatch: the frontend's per-object lifetime id,
// process-wide and never reused, which is what survives glGenTransformFeedbacks
// recycling a name. Read AFTER GLContext::BindTransformFeedbackObject at the call
// site, so it is the id of the object being bound and not of the previous one.
record.LifetimeId = MG_State::pGLContext->GetBoundTransformFeedbackLifetimeId();
session.EmitAndWait(MG_Pipe::MGPWireOp::BindStreamOutput, &record, sizeof(record),
nullptr, 0, nullptr, 0, nullptr);
}
void EmitPatchParameteri(GLenum pname, GLint value) {
ClientSession& session = RequireSession("PatchParameteri");
BeforeReadOnlyVerb();
MG_Pipe::MGPPatchParameter record{};
// GL_PATCH_VERTICES is the only pname that reaches a backend slot - the frontend
// answers GL_PATCH_DEFAULT_*_LEVEL itself and bakes those into the synthesized
// control stage - and the frontend has already rejected every other pname with
// INVALID_ENUM before this call (GL_Drawing.cpp's PatchParameteri). Carried verbatim
// so the sink reproduces the call rather than a reading of it.
record.Pname = static_cast<Uint32>(pname);
record.Value = static_cast<Int32>(value);
// set_patch_state (43) STILL TRAVELS, at the next validate, and that is not a
// duplicate: it is the applier's working-block copy, this is the driver push Espryt
// does AT THE CALL (DirectGLES.cpp:8760), and both pushes happen today on the
// monolith path too (CONTRACT-P5B.md §2 t2).
session.EmitAndWait(MG_Pipe::MGPWireOp::PatchParameter, &record, sizeof(record),
nullptr, 0, nullptr, 0, nullptr);
}
// ============================================================================= // =============================================================================
// CLASS A - answered locally from the caps mirror (R-15). NO RECORD, EVER. // CLASS A - answered locally from the caps mirror (R-15). NO RECORD, EVER.
// ============================================================================= // =============================================================================
@@ -995,13 +1126,14 @@ namespace MobileGL::MG_Remote::Client {
// (a P5b wave-3 image/compute slot) has the partition to put it in. // (a P5b wave-3 image/compute slot) has the partition to put it in.
#define MGR_UNMIGRATED_I1_SLOTS(X) #define MGR_UNMIGRATED_I1_SLOTS(X)
// t2 LANDED (CONTRACT-P5B.md §2 t2): the three measured slots - BeginTransformFeedback
// (95 lane entries), PatchParameteri (43), BindTransformFeedback (2) - and the three
// companions that share their rows are class B now and live in the block above.
// DeleteTransformFeedback is the one that stays: it has NO ROW in P5b, by ruling and
// not by omission (unmeasured; the driver object leaks on the server until P9's XFB
// namespace work, and a bind of name 0 is what the backend does on delete of the bound
// one, DirectGLES.cpp:1422). It therefore still aborts by its own name.
#define MGR_UNMIGRATED_T2_SLOTS(X) \ #define MGR_UNMIGRATED_T2_SLOTS(X) \
X(PatchParameteri, void, (GLenum, GLint)) \
X(BeginTransformFeedback, void, (GLenum)) \
X(EndTransformFeedback, void, ()) \
X(PauseTransformFeedback, void, ()) \
X(ResumeTransformFeedback, void, ()) \
X(BindTransformFeedback, void, (GLuint)) \
X(DeleteTransformFeedback, void, (GLuint)) X(DeleteTransformFeedback, void, (GLuint))
#define MGR_UNMIGRATED_F1_SLOTS(X) #define MGR_UNMIGRATED_F1_SLOTS(X)
@@ -1081,10 +1213,8 @@ 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 = 0;
// P5b i1: BindImageTexture, DispatchCompute, DispatchComputeIndirect, MemoryBarrier,
// MemoryBarrierByRegion, CopyImageSubData, ShaderStorageBlockBinding.
constexpr Uint32 kEmittedSlotsI1 = 7; constexpr Uint32 kEmittedSlotsI1 = 7;
constexpr Uint32 kEmittedSlotsT2 = 0; constexpr Uint32 kEmittedSlotsT2 = 6;
constexpr Uint32 kEmittedSlotsF1 = 11; constexpr Uint32 kEmittedSlotsF1 = 11;
constexpr Uint32 kEmittedSlots = constexpr Uint32 kEmittedSlots =
kEmittedSlotsP5 + kEmittedSlotsD1 + kEmittedSlotsI1 + kEmittedSlotsT2 + kEmittedSlotsF1; kEmittedSlotsP5 + kEmittedSlotsD1 + kEmittedSlotsI1 + kEmittedSlotsT2 + kEmittedSlotsF1;
@@ -1134,8 +1264,17 @@ namespace MobileGL::MG_Remote::Client {
table.GL.ReadPixels = &EmitReadPixels; table.GL.ReadPixels = &EmitReadPixels;
table.GL.BlitFramebuffer = &EmitBlitFramebuffer; table.GL.BlitFramebuffer = &EmitBlitFramebuffer;
table.Present = &EmitPresent; table.Present = &EmitPresent;
// ---- class B, P5b t2. Assigned AFTER the class-C block above, which is what makes
// the flip a single-line change per slot: the Fatal thunk is overwritten, and a slot
// whose row is removed from MGR_UNMIGRATED_T2_SLOTS but not assigned here would be
// NULL and caught by RemoteEmitTable.NoSlotIsNull rather than silently skipped.
table.GL.BeginTransformFeedback = &EmitBeginTransformFeedback;
table.GL.EndTransformFeedback = &EmitEndTransformFeedback;
table.GL.PauseTransformFeedback = &EmitPauseTransformFeedback;
table.GL.ResumeTransformFeedback = &EmitResumeTransformFeedback;
table.GL.BindTransformFeedback = &EmitBindTransformFeedback;
table.GL.PatchParameteri = &EmitPatchParameteri;
<<<<<<< HEAD
// ---- f1 ---- // ---- f1 ----
table.GL.ClearBufferfi = &EmitClearBufferfi; table.GL.ClearBufferfi = &EmitClearBufferfi;
table.GL.ClearBufferfv = &EmitClearBufferfv; table.GL.ClearBufferfv = &EmitClearBufferfv;
@@ -1148,7 +1287,7 @@ namespace MobileGL::MG_Remote::Client {
table.GL.CopyTexImage2D = &EmitCopyTexImage2D; table.GL.CopyTexImage2D = &EmitCopyTexImage2D;
table.GL.CopyTexSubImage2D = &EmitCopyTexSubImage2D; table.GL.CopyTexSubImage2D = &EmitCopyTexSubImage2D;
table.GL.GenerateMipmap = &EmitGenerateMipmap; table.GL.GenerateMipmap = &EmitGenerateMipmap;
=======
// ---- class B, P5b package i1 (kEmittedSlotsI1 = 7) // ---- class B, P5b package i1 (kEmittedSlotsI1 = 7)
table.GL.BindImageTexture = &EmitBindImageTexture; table.GL.BindImageTexture = &EmitBindImageTexture;
table.GL.DispatchCompute = &EmitDispatchCompute; table.GL.DispatchCompute = &EmitDispatchCompute;
@@ -1157,7 +1296,6 @@ namespace MobileGL::MG_Remote::Client {
table.GL.MemoryBarrierByRegion = &EmitMemoryBarrierByRegion; table.GL.MemoryBarrierByRegion = &EmitMemoryBarrierByRegion;
table.GL.CopyImageSubData = &EmitCopyImageSubData; table.GL.CopyImageSubData = &EmitCopyImageSubData;
table.GL.ShaderStorageBlockBinding = &EmitShaderStorageBlockBinding; table.GL.ShaderStorageBlockBinding = &EmitShaderStorageBlockBinding;
>>>>>>> p5b/i1
return table; return table;
} }
+20 -1
View File
@@ -75,11 +75,30 @@
// GL_Drawing.cpp:844 PatchParameteri. "Absent" means the patch size is never set and every // GL_Drawing.cpp:844 PatchParameteri. "Absent" means the patch size is never set and every
// tessellation draw silently uses the previous one. Class C; it aborts by name. // tessellation draw silently uses the previous one. Class C; it aborts by name.
// //
// P5b t2 CLOSED THE FIRST OF THOSE TWO, AND THE OTHER SIX SITES NEEDED NOTHING (CONTRACT-P5B.md
// §2 t2, §6.5). The six SPAN sites - :1274 Begin, :1371 End, :1420 Pause, :1435 Resume, :1641
// Delete, :1673 Bind - are guards over a slot that is now class B in the client's table, so they
// simply call the emitter; their `if (const auto f = ...)` shape is left exactly as it was,
// because under split the slot is non-null and under monolith nothing moved. THE PROBE AT :1290
// IS THE ONE THAT HAD TO CHANGE, and it is the reason this header said the XFB span family would
// be the first to need a bit: it is not a guard on a call, it is a QUESTION ABOUT THE BACKEND
// asked of a table that under split belongs to the client. It now reads
// MGL_BACKEND_SLOT_CAP(EndTransformFeedback, kCapBackendOwnsXfbCapture), the bit the server sets
// from ITS table in MG_Backend/Init.cpp's InitSplitRoles. Espryt registers the slot and answers
// yes; Magma registers no XFB slot, answers no, and the client keeps reordering for it exactly
// as it does under monolith. GL_Drawing.cpp:844's PatchParameteri stays a plain guard for the
// same reason as the six: the slot is class B now, so "absent" never arises.
//
// The one XFB slot still class C is DeleteTransformFeedback, which CONTRACT-P5B.md gives no row
// (unmeasured); :1641's guard therefore still reaches Fatal{UnmigratedVerb} by name, which is
// the outcome R-4 asks for.
//
// WHAT THIS HEADER DELIBERATELY DOES NOT DO. It does not touch the 28 unguarded slots: those // WHAT THIS HEADER DELIBERATELY DOES NOT DO. It does not touch the 28 unguarded slots: those
// have no probe to convert, and calling one reaches Fatal{UnmigratedVerb, "<slot>"} by name, // have no probe to convert, and calling one reaches Fatal{UnmigratedVerb, "<slot>"} by name,
// which is R-4's intent. And it does not invent a cap bit - a new MGPCapBit is an // which is R-4's intent. And it does not invent a cap bit - a new MGPCapBit is an
// MGPipeTypes.h edit and that file is c0's, so a family that needs one goes through the // MGPipeTypes.h edit and that file is c0's, so a family that needs one goes through the
// integrator (the XFB span family is the first that will). // integrator (the XFB span family is the first that will). It did: P5b's contract granted t2
// exactly that one bit, kCapBackendOwnsXfbCapture (CONTRACT-P5B.md §6.5, §8), and t2 added it.
// //
// G1: in a build without MOBILEGL_BUILD_DISAGGREGATED both macros expand to the null check the // G1: in a build without MOBILEGL_BUILD_DISAGGREGATED both macros expand to the null check the
// site already had, so the pull build's code generation is unchanged. // site already had, so the pull build's code generation is unchanged.
+80 -11
View File
@@ -497,35 +497,104 @@ namespace MobileGL::MG_Remote::Server {
return true; return true;
} }
// ---- t2 ---- // ---- t2 ---- (MG_Remote/CONTRACT-P5B.md §2 t2)
//
// SIX BODIES, SIX BACKEND CALLS, NO STATE OF THEIR OWN. Rule D: the record IS the call, and
// everything the backend reads around it - the capture program, the capture-buffer
// bindings, the bound XFB object, the patch state - it reads from gPipeInputs through its
// verb class's BARRIER-PULLED fields, which the client filled at the call site and the
// verb barrier holds still (R-1). That is why none of these touches m_backend beyond
// Table() and why not one of them caches anything across records.
//
// A NULL SLOT DECLINES, AND THE DECLINE IS THE MONOLITH'S ANSWER IN THE SAME WORDS. Magma
// (DirectVulkan) registers NO XFB slot and no PatchParameteri at all
// (BackendObject_DirectVulkan.cpp), and under monolith the frontend's own
// `if (const auto f = table.GL.X)` guard simply skips the call; `return false` here is that
// same skip, reported to DecodeAndApply as "this build did not apply it" rather than as a
// crash or as a silent success. Contract §2 t2 says so for PatchParameteri by name.
Bool ServerVerbSink::OnBeginStreamOutput(const MG_Pipe::MGPStreamOutputBegin& begin) { Bool ServerVerbSink::OnBeginStreamOutput(const MG_Pipe::MGPStreamOutputBegin& begin) {
(void)begin; const MG_Backend::GlobalBackendFunctionsTable* table = Table("begin_stream_output");
ServerUnmigratedVerbFatal("BeginTransformFeedback"); if (table == nullptr) return false;
if (table->GL.BeginTransformFeedback == nullptr) return false;
// Espryt's Begin only ARMS the span (DirectGLES.cpp:1212-1220: primitiveMode, pending,
// targets cleared); the driver glBeginTransformFeedback happens in the tail of the next
// PrepareForDraw (StartPendingTransformFeedback, :1224), where the capture program and
// the buffer bindings are read through the pulls. So this record's effect is not
// visible until a DRAW crosses - which is why an XFB scenario whose draw is still class
// C moves its first blocker to that draw rather than rendering.
table->GL.BeginTransformFeedback(static_cast<GLenum>(begin.PrimitiveMode));
++m_streamOutputSpans;
return true;
} }
Bool ServerVerbSink::OnEndStreamOutput(const MG_Pipe::MGPXfbAccounting& accounting) { Bool ServerVerbSink::OnEndStreamOutput(const MG_Pipe::MGPXfbAccounting& accounting) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("end_stream_output");
if (table == nullptr) return false;
if (table->GL.EndTransformFeedback == nullptr) return false;
// THE THREE ACCOUNTING FIELDS ARE NOT READ, AND THAT IS THE RULING RATHER THAN AN
// OMISSION. glEndTransformFeedback takes no arguments; the numbers are the CLIENT's own
// per-span accounting (contract §2 t2's companions row) and the client is where they are
// consumed - by the primitive queries and by the capture-capacity clamp. A server that
// second-guessed them from its own driver would be publishing a second answer to a
// question the frontend already answers, and the second answer is the one that goes
// stale. They cross because the row has carried them since P4a and because P9's
// server-side scatter is what will need them.
(void)accounting; (void)accounting;
ServerUnmigratedVerbFatal("EndTransformFeedback"); table->GL.EndTransformFeedback();
++m_streamOutputSpans;
return true;
} }
Bool ServerVerbSink::OnPauseStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) { Bool ServerVerbSink::OnPauseStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) {
(void)control; const MG_Backend::GlobalBackendFunctionsTable* table = Table("pause_stream_output");
ServerUnmigratedVerbFatal("PauseTransformFeedback"); if (table == nullptr) return false;
if (table->GL.PauseTransformFeedback == nullptr) return false;
(void)control; // Reserved, and the contract says it is 0.
table->GL.PauseTransformFeedback();
++m_streamOutputControls;
return true;
} }
Bool ServerVerbSink::OnResumeStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) { Bool ServerVerbSink::OnResumeStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("resume_stream_output");
if (table == nullptr) return false;
if (table->GL.ResumeTransformFeedback == nullptr) return false;
(void)control; (void)control;
ServerUnmigratedVerbFatal("ResumeTransformFeedback"); table->GL.ResumeTransformFeedback();
++m_streamOutputControls;
return true;
} }
Bool ServerVerbSink::OnBindStreamOutput(const MG_Pipe::MGPStreamOutputBind& bind) { Bool ServerVerbSink::OnBindStreamOutput(const MG_Pipe::MGPStreamOutputBind& bind) {
(void)bind; const MG_Backend::GlobalBackendFunctionsTable* table = Table("bind_stream_output");
ServerUnmigratedVerbFatal("BindTransformFeedback"); if (table == nullptr) return false;
if (table->GL.BindTransformFeedback == nullptr) return false;
// THE GL NAME IS THE ARGUMENT, NOT THE LifetimeId BESIDE IT. Espryt keys its driver
// objects by the GL name (XfbImpl::g_xfbObjects[name], DirectGLES.cpp:1401) and creates
// the ES object on first bind; passing the lifetime id would index a map that has never
// heard of it and silently create a second driver object per bind. The lifetime id
// travels as the identity P7/P9 will dispatch on once the XFB namespace has a wire
// lifetime of its own - it has no reader on this side today, and pretending otherwise
// by folding it into the key is exactly the "a GL name is never an identity" confusion
// the contract's GlName row is written against.
table->GL.BindTransformFeedback(static_cast<GLuint>(bind.GlName));
++m_streamOutputBinds;
return true;
} }
Bool ServerVerbSink::OnPatchParameter(const MG_Pipe::MGPPatchParameter& patch) { Bool ServerVerbSink::OnPatchParameter(const MG_Pipe::MGPPatchParameter& patch) {
(void)patch; const MG_Backend::GlobalBackendFunctionsTable* table = Table("patch_parameter");
ServerUnmigratedVerbFatal("PatchParameteri"); if (table == nullptr) return false;
// Magma registers no PatchParameteri: it compiles the patch size into its synthesized
// control stage from set_patch_state instead, so the DECLINE below is the whole of the
// right answer for that backend and not a gap (contract §2 t2).
if (table->GL.PatchParameteri == nullptr) return false;
// Pname is GL_PATCH_VERTICES and the frontend has already rejected every other spelling
// with INVALID_ENUM before the record was built, so this is a forward and not a switch.
table->GL.PatchParameteri(static_cast<GLenum>(patch.Pname), static_cast<GLint>(patch.Value));
++m_patchParameters;
return true;
} }
// ---- f1 ---- // ---- f1 ----
+18 -3
View File
@@ -121,9 +121,9 @@ namespace MobileGL::MG_Remote::Server {
// i1 OnLaunchGrid ("DispatchCompute"), OnMemoryBarrier, OnResourceCopyRegion // i1 OnLaunchGrid ("DispatchCompute"), OnMemoryBarrier, OnResourceCopyRegion
// ("CopyImageSubData"), OnBindShaderImage ("BindImageTexture"), // ("CopyImageSubData"), OnBindShaderImage ("BindImageTexture"),
// OnSetStorageBlockBinding ("ShaderStorageBlockBinding") // OnSetStorageBlockBinding ("ShaderStorageBlockBinding")
// t2 OnBeginStreamOutput / OnEndStreamOutput / OnPauseStreamOutput / // t2 LANDED. OnBeginStreamOutput / OnEndStreamOutput / OnPauseStreamOutput /
// OnResumeStreamOutput ("*TransformFeedback"), OnBindStreamOutput // OnResumeStreamOutput / OnBindStreamOutput / OnPatchParameter are real bodies
// ("BindTransformFeedback"), OnPatchParameter ("PatchParameteri") // now: the backend call the contract names, the null-slot DECLINE, a tally.
// 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
@@ -168,6 +168,17 @@ 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 t2's tallies, for the same reason the five above exist (R-16): under split "the
// scenario passed" is also what a scenario that ran entirely on the monolith path looks
// like, so a lane that wants to say the XFB spans CROSSED has to read a counter the
// server moved. Spans counts Begin and End together - they are one span and a lane that
// saw only one of them has a bug the two-counter version would have hidden behind a
// sum; controls counts Pause and Resume; binds and patch parameters count their own.
Uint64 StreamOutputSpans() const { return m_streamOutputSpans; }
Uint64 StreamOutputControls() const { return m_streamOutputControls; }
Uint64 StreamOutputBinds() const { return m_streamOutputBinds; }
Uint64 PatchParameters() const { return m_patchParameters; }
private: private:
const MG_Backend::GlobalBackendFunctionsTable* Table(const char* verb) const; const MG_Backend::GlobalBackendFunctionsTable* Table(const char* verb) const;
@@ -184,6 +195,10 @@ namespace MobileGL::MG_Remote::Server {
Uint64 m_memoryBarriers = 0; Uint64 m_memoryBarriers = 0;
Uint64 m_imageCopies = 0; Uint64 m_imageCopies = 0;
Uint64 m_storageBlockBindings = 0; Uint64 m_storageBlockBindings = 0;
Uint64 m_streamOutputSpans = 0;
Uint64 m_streamOutputControls = 0;
Uint64 m_streamOutputBinds = 0;
Uint64 m_patchParameters = 0;
// ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable:: // ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable::
// 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.
+107
View File
@@ -1204,6 +1204,113 @@ TEST_F(PipeWireCodecTest, BindStreamOutputReachesTheSink) {
EXPECT_EQ(wire.Sink().StreamOutputBinds[0].LifetimeId, 0x1234567890ull); EXPECT_EQ(wire.Sink().StreamOutputBinds[0].LifetimeId, 0x1234567890ull);
} }
// =====================================================================================
// P5b t2 (MG_Remote/CONTRACT-P5B.md §2 t2). c0b's cases above round-trip each row once; these
// pin the fields t2's EMITTERS actually fill and the one ordering property the span family has.
// =====================================================================================
TEST_F(PipeWireCodecTest, EndStreamOutputCarriesAllThreeAccountingFieldsAndNotJustTheVertices) {
// t2's emitter fills all three from the frontend's own per-span accounting
// (GetTransformFeedbackCapturedVertices / GetTransformFeedbackPrimitiveCounter /
// GetTransformFeedbackPrimitiveMode) and the row above asserts only CapturedVertices, so a
// codec that dropped either of the other two - or an emitter that left them zero - reads
// green there. THE THREE ARE DELIBERATELY DIFFERENT NUMBERS: with 300/300 a swap of the two
// 64-bit fields is invisible.
//
// Red once by making the recorder above push a default-constructed MGPXfbAccounting{}
// instead of the one the decoder handed it - the shape of a seam that loses the payload:
// "end_stream_output lost the primitives-written half of its accounting".
Wire2 wire;
MGPXfbAccounting end{};
end.CapturedVertices = 21;
end.PrimitivesWritten = 7;
end.PrimitiveMode = 0x0000; // GL_POINTS - and 0 is a legal primitive mode, not "unset"
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::EndStreamOutput, &end, sizeof(end)), kInvalidSeq);
bool applied = false;
ASSERT_TRUE(wire.PumpOne(&applied));
EXPECT_TRUE(applied);
ASSERT_EQ(wire.Sink().Ends.size(), 1u);
EXPECT_EQ(wire.Sink().Ends[0].CapturedVertices, 21u);
EXPECT_EQ(wire.Sink().Ends[0].PrimitivesWritten, 7u)
<< "end_stream_output lost the primitives-written half of its accounting";
EXPECT_EQ(wire.Sink().Ends[0].PrimitiveMode, 0u)
<< "GL_POINTS is 0 and a row that treats 0 as 'no mode' has made a legal capture "
"indistinguishable from an unfilled record";
}
TEST_F(PipeWireCodecTest, BindStreamOutputOfNameZeroIsTheDefaultObjectAndNotAnAbsentOne) {
// THE ONE NAME t2 CANNOT TREAT AS "NOTHING". CONTRACT-P5B.md gives DeleteTransformFeedback
// no row, and the backend's answer to deleting the bound object is a bind of NAME 0
// (DirectGLES.cpp:1422) - so a row that folded 0 into kMGPipeNullHandle, or a sink that read
// 0 as "no object", would silently stop rebinding the default object and leave the driver
// bound to a deleted one. The lifetime id beside it is 0 too here, which is the frontend's
// seed value for the default object, so this case also pins that a wholly-zero record is
// legal and applies rather than being refused as unfilled.
//
// Red once by making the sink's OnBindStreamOutput refuse GlName == 0 (return false): the
// EXPECT_TRUE(applied) below failed, which is the shape the wrong reading would take.
Wire2 wire;
MGPStreamOutputBind bind{};
bind.GlName = 0;
bind.LifetimeId = 0;
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::BindStreamOutput, &bind, sizeof(bind)), kInvalidSeq);
bool applied = false;
ASSERT_TRUE(wire.PumpOne(&applied));
EXPECT_TRUE(applied) << "a bind of the default transform-feedback object did not apply";
ASSERT_EQ(wire.Sink().StreamOutputBinds.size(), 1u);
EXPECT_EQ(wire.Sink().StreamOutputBinds[0].GlName, 0u);
}
TEST_F(PipeWireCodecTest, AWholeCaptureSpanReachesTheSinkInTheOrderTheClientEmittedIt) {
// THE PROPERTY A PER-ROW ROUND TRIP CANNOT STATE. A capture span is five calls whose MEANING
// is their order - bind the object, open the span, pause it, resume it, close it - and every
// one of them rides its own row with no sequence field of its own to check. The decoder's
// ordering is the ring's, so this is a pin on the seam rather than a new guarantee: if a
// later change ever batches or reorders records per row, a capture reordered into
// bind/begin/end/pause/resume applies five records, sets `applied` five times, and leaves
// every per-row case above green while the capture is destroyed.
//
// Red once by encoding the Pause AFTER the End: the interleaved-order EXPECT below failed on
// Pauses being 0 at the point the End was seen.
Wire2 wire;
MGPStreamOutputBind bind{};
bind.GlName = 3;
bind.LifetimeId = 0x5150ull;
MGPStreamOutputBegin begin{};
begin.PrimitiveMode = 0x0004; // GL_TRIANGLES
MGPStreamOutputControl control{};
MGPXfbAccounting end{};
end.CapturedVertices = 9;
end.PrimitivesWritten = 3;
end.PrimitiveMode = 0x0004;
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::BindStreamOutput, &bind, sizeof(bind)), kInvalidSeq);
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::BeginStreamOutput, &begin, sizeof(begin)), kInvalidSeq);
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::PauseStreamOutput, &control, sizeof(control)), kInvalidSeq);
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ResumeStreamOutput, &control, sizeof(control)), kInvalidSeq);
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::EndStreamOutput, &end, sizeof(end)), kInvalidSeq);
// Pumped ONE AT A TIME with the sink read between pumps, which is what makes this a
// statement about order rather than about totals: after the third record the pause must
// have happened and the end must NOT have.
bool applied = false;
ASSERT_TRUE(wire.PumpOne(&applied));
EXPECT_EQ(wire.Sink().StreamOutputBinds.size(), 1u) << "the object bind did not come first";
EXPECT_EQ(wire.Sink().Begins.size(), 0u);
ASSERT_TRUE(wire.PumpOne(&applied));
EXPECT_EQ(wire.Sink().Begins.size(), 1u) << "the span did not open second";
ASSERT_TRUE(wire.PumpOne(&applied));
EXPECT_EQ(wire.Sink().Pauses, 1u) << "the pause did not arrive third";
EXPECT_EQ(wire.Sink().Ends.size(), 0u)
<< "the span closed before it was paused - the capture's order did not survive the wire";
ASSERT_TRUE(wire.PumpOne(&applied));
EXPECT_EQ(wire.Sink().Resumes, 1u);
ASSERT_TRUE(wire.PumpOne(&applied));
ASSERT_EQ(wire.Sink().Ends.size(), 1u);
EXPECT_EQ(wire.Sink().Ends[0].PrimitivesWritten, 3u);
EXPECT_EQ(wire.Decoder().AppliedSeq(), 5u);
}
TEST_F(PipeWireCodecTest, SetStorageBlockBindingCarriesItsNameAsAStagedBlob) { TEST_F(PipeWireCodecTest, SetStorageBlockBindingCarriesItsNameAsAStagedBlob) {
// i1: the ONE string on the wire. Size is strlen + 1 - the NUL travels - and the decoder // i1: the ONE string on the wire. Size is strlen + 1 - the NUL travels - and the decoder
// hands the sink a pointer that dies with the call. // hands the sink a pointer that dies with the call.
+91 -14
View File
@@ -171,25 +171,102 @@ namespace {
// ===================================================================================== // =====================================================================================
TEST(RemoteEmitTable, TheThreeClassesPartitionAllSeventyOneSlots) { TEST(RemoteEmitTable, TheThreeClassesPartitionAllSeventyOneSlots) {
// CONTRACT-P5.md §7: 2 answered locally + 5 emitted + 64 Fatal at the P5b contract commit. // P5 baseline five + f1 eleven + i1 seven + t2 six emitted slots.
// Read from the functions the 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.
//
// P5b package i1 (CONTRACT-P5B.md §2 i1) flipped SEVEN slots C -> B: BindImageTexture,
// DispatchCompute, DispatchComputeIndirect, MemoryBarrier, MemoryBarrierByRegion,
// CopyImageSubData, ShaderStorageBlockBinding. So the two numbers that move are 5 -> 12 and
// 64 -> 57, and the SUM below is the invariant that does not move whichever package lands
// next. RED ONCE BY DOING X: comment out `table.GL.MemoryBarrier = &EmitMemoryBarrier;` in
// BuildRemoteEmitTable and this case stays green while NoSlotIsNull goes red - which is why
// the per-package count is asserted here and the null walk is a separate case.
EXPECT_EQ(LocallyAnsweredSlotCount(), 2u); EXPECT_EQ(LocallyAnsweredSlotCount(), 2u);
EXPECT_EQ(ImplementedVerbCount(), 23u); EXPECT_EQ(ImplementedVerbCount(), 29u);
EXPECT_EQ(UnmigratedSlotCount(), 46u); EXPECT_EQ(UnmigratedSlotCount(), 40u);
EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(), EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(),
kRemoteEmitSlotCount); kRemoteEmitSlotCount);
} }
TEST(RemoteEmitTable, TheSixXfbAndPatchSlotsAreNonNullAndDistinct) {
// P5b t2 (CONTRACT-P5B.md §2 t2), the half that needs no fork: the six slots exist and are
// six DIFFERENT functions. Six identical pointers would be one emitter assigned six times,
// which is how a copy-paste flip loses five records and still passes every count.
//
// Red once by assigning `table.GL.PauseTransformFeedback = &EmitResumeTransformFeedback;`
// in BuildRemoteEmitTable - the exact copy-paste this guards: "t2 slots 2 and 3 are one
// function".
const MG_Backend::GlobalBackendFunctionsTable& table = RemoteEmitTable();
const void* const six[] = {
reinterpret_cast<const void*>(table.GL.BeginTransformFeedback),
reinterpret_cast<const void*>(table.GL.EndTransformFeedback),
reinterpret_cast<const void*>(table.GL.PauseTransformFeedback),
reinterpret_cast<const void*>(table.GL.ResumeTransformFeedback),
reinterpret_cast<const void*>(table.GL.BindTransformFeedback),
reinterpret_cast<const void*>(table.GL.PatchParameteri),
};
for (SizeT i = 0; i < 6; ++i) {
EXPECT_NE(six[i], nullptr) << "t2 slot " << i << " is null";
for (SizeT j = i + 1; j < 6; ++j) {
EXPECT_NE(six[i], six[j]) << "t2 slots " << i << " and " << j << " are one function";
}
}
// And the one XFB slot t2 does NOT flip is still there to be Fatal - CONTRACT-P5B.md gives
// DeleteTransformFeedback no row (unmeasured), so it must not have been swept up.
EXPECT_NE(table.GL.DeleteTransformFeedback, nullptr);
}
#if MGTEST_HAVE_FORK
TEST(RemoteEmitTable, EachXfbAndPatchSlotIsClassBAndDemandsASessionByItsOwnName) {
// P5b t2, THE HALF THAT DECIDES THE CLASS. A pointer comparison cannot tell a class-B
// emitter from a class-C thunk - each unmigrated slot gets its own generated function, so
// every slot in the table is already a distinct non-null address. What distinguishes them is
// WHAT THEY SAY when called with no ClientSession: an emitter reaches RequireSession and
// dies Fatal{NoClientSession, "<slot>"}; a thunk dies Fatal{UnmigratedVerb, "<slot>"}. Both
// strings are asserted, because a case that only looked for the first would be satisfied by
// a build where every one of these had been flipped by accident.
//
// Red once by SWAPPING the Pause and Resume assignments in BuildRemoteEmitTable - the two
// emitters with the same signature, so the swap compiles and neither is orphaned (the first
// attempt redirected one slot at another's emitter and the build failed on the signature
// and on -Wunused-function, which is a control that did not run). The child called through
// PauseTransformFeedback died Fatal{NoClientSession, "ResumeTransformFeedback"} and this
// case failed with "PauseTransformFeedback did not reach the class-B emitter's session
// demand", so the string really is the slot's own name and not a shared constant.
struct Slot {
const char* Name;
void (*Call)();
};
static const Slot kSlots[] = {
{"BeginTransformFeedback", [] { RemoteEmitTable().GL.BeginTransformFeedback(0x0004); }},
{"EndTransformFeedback", [] { RemoteEmitTable().GL.EndTransformFeedback(); }},
{"PauseTransformFeedback", [] { RemoteEmitTable().GL.PauseTransformFeedback(); }},
{"ResumeTransformFeedback", [] { RemoteEmitTable().GL.ResumeTransformFeedback(); }},
{"BindTransformFeedback", [] { RemoteEmitTable().GL.BindTransformFeedback(0); }},
{"PatchParameteri", [] { RemoteEmitTable().GL.PatchParameteri(0x8E72, 3); }},
};
for (const Slot& slot : kSlots) {
const ChildResult r = RunInChild([&slot] { slot.Call(); });
ASSERT_TRUE(DiedOfAbort(r)) << slot.Name << ": " << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find(std::string("Fatal{NoClientSession, \"") + slot.Name + "\"}"),
std::string::npos)
<< slot.Name << " did not reach the class-B emitter's session demand:\n"
<< r.Log;
EXPECT_EQ(r.Log.find("Fatal{UnmigratedVerb"), std::string::npos)
<< slot.Name << " is still class C:\n"
<< r.Log;
}
}
TEST(RemoteEmitTable, DeleteTransformFeedbackHasNoRowAndStillAbortsByItsOwnName) {
// CONTRACT-P5B.md §2 t2 and c0b-v1.md §6: the seventh slot in t2's ownership block gets NO
// row in P5b - it is unmeasured, the driver object leaks on the server until P9's XFB
// namespace work, and a bind of name 0 is what the backend does on delete of the bound one.
// That is a RULING, so it is pinned rather than left to be re-derived from a count: a later
// round that gives it a row has to delete this case and say why.
//
// Red once by assigning `table.GL.DeleteTransformFeedback = &EmitBindTransformFeedback;` in
// BuildRemoteEmitTable - a package sweeping the whole XFB family into class B: the child
// died Fatal{NoClientSession, "BindTransformFeedback"} and the UnmigratedVerb expectation
// failed.
const ChildResult r = RunInChild([] { RemoteEmitTable().GL.DeleteTransformFeedback(7); });
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"DeleteTransformFeedback\"}"), std::string::npos)
<< r.Log;
}
#endif // MGTEST_HAVE_FORK
TEST(RemoteEmitTable, NoSlotIsNull) { TEST(RemoteEmitTable, NoSlotIsNull) {
// R-4's whole rule, asserted over the STRUCT rather than over the list that built it. 91 // R-4's whole rule, asserted over the STRUCT rather than over the list that built it. 91
// MG_Impl sites call through this table directly; a null slot is 91 potential null calls, // MG_Impl sites call through this table directly; a null slot is 91 potential null calls,