[Feat] (MG_Pipe, MG_Remote/Client, MG_Impl/Pipe): R-17 - install the two generated MGPipe tables over the 37 apply entry points, 40 call sites onto the thunks, 37 wire emitters, the reply mailbox, and ID-47/ID-49 readback

This commit is contained in:
2026-09-16 07:10:51 -04:00
parent aa7ad670c4
commit 02f15bb675
25 changed files with 2336 additions and 273 deletions
+13
View File
@@ -533,6 +533,13 @@ if (MOBILEGL_PIPE_PUSH)
# by pointer beside the record), so the codec is live code only in the VERIFY lane, # by pointer beside the record), so the codec is live code only in the VERIFY lane,
# where the applier serialises, deserialises and field-compares before storing. # where the applier serialises, deserialises and field-compares before storing.
MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp
# P5's contract (integrator ruling R-17, package c1): the MONOLITH arm of the
# client->wire routing - the thirty-seven adapters that install gMGPipeScreen /
# gMGPipeContext over the MGPipeApply* entry points, and the reply mailbox the four
# acceptance rows answer through. Push-only for the same G1 reason as the five above:
# the two tables are inline variables that are zero in a pull build and nothing there
# can reach a thunk.
MobileGL/MG_Pipe/PipeRoute.cpp
) )
endif() endif()
@@ -574,6 +581,12 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# P5 c1: the client role's BackendObject. pActiveBackendObject holds one of these # P5 c1: the client role's BackendObject. pActiveBackendObject holds one of these
# under split (table 3); the hook that installs it is v1's, in MG_Backend/Init.cpp. # under split (table 3); the hook that installs it is v1's, in MG_Backend/Init.cpp.
MobileGL/MG_Remote/Client/BackendObject_Remote.cpp MobileGL/MG_Remote/Client/BackendObject_Remote.cpp
# P5 c1, ruling R-17: the ENCODE TWIN of gMGPipeWireRecordApply - the thirty-seven
# emitters installed over the two generated tables, which is what stops every
# resource/CSO/texture/program record executing synchronously on the GL thread under
# inproc. The monolith arm of the same routing is MG_Pipe/PipeRoute.cpp, in the
# PIPE_PUSH list above, because it must exist in a push build that has no MG_Remote.
MobileGL/MG_Remote/Client/WireTables.cpp
# P5 b1's two: the conservative GPU-write set the client must build because all six # P5 b1's two: the conservative GPU-write set the client must build because all six
# MarkGpuWritten producers are on the server's side of the line, and the # MarkGpuWritten producers are on the server's side of the line, and the
# block-granularity persistent-map push that tier T2 makes mandatory. # block-granularity persistent-map push that tier T2 makes mandatory.
+3 -2
View File
@@ -44,6 +44,7 @@
#include <MG_Pipe/MGPipe.h> #include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/MGPipeRenderStateSpans.h> #include <MG_Pipe/MGPipeRenderStateSpans.h>
#include <MG_Pipe/PipeApply.h> #include <MG_Pipe/PipeApply.h>
#include <MG_Pipe/PipeRoute.h>
#include <MG_Util/Metrics/PipeStats.h> #include <MG_Util/Metrics/PipeStats.h>
#include <cstring> #include <cstring>
@@ -154,7 +155,7 @@ namespace MobileGL::MG_Pipe {
// minted from a neighbour rather than from nothing. // minted from a neighbour rather than from nothing.
desc.ChunkMask = kAllPipelineChunks; desc.ChunkMask = kAllPipelineChunks;
desc.Blob.Size = kMGPipePipelineChunkBytes; desc.Blob.Size = kMGPipePipelineChunkBytes;
MGPipeApplyCreateRenderState(desc, bytes.data()); MGPipeRouteCreateRenderState(desc, bytes.data());
payloadBytes += sizeof(MGPRenderStateDesc) + kMGPipePipelineChunkBytes; payloadBytes += sizeof(MGPRenderStateDesc) + kMGPipePipelineChunkBytes;
Entry entry; Entry entry;
@@ -175,7 +176,7 @@ namespace MobileGL::MG_Pipe {
MGPHandleOnly handle{}; MGPHandleOnly handle{};
handle.Handle = m_entries[index].Cso; handle.Handle = m_entries[index].Cso;
handle.Kind = static_cast<Uint32>(MGPipeKind::RenderStateCso); handle.Kind = static_cast<Uint32>(MGPipeKind::RenderStateCso);
MGPipeApplyDeleteRenderState(handle); MGPipeRouteDeleteRenderState(handle);
MGPipeSlots().Free(MGPipeKind::RenderStateCso, m_entries[index].Cso); MGPipeSlots().Free(MGPipeKind::RenderStateCso, m_entries[index].Cso);
m_entries[index] = m_entries.back(); m_entries[index] = m_entries.back();
m_entries.pop_back(); m_entries.pop_back();
+2 -1
View File
@@ -32,6 +32,7 @@
#include <MG_Impl/Pipe/Tracker.h> #include <MG_Impl/Pipe/Tracker.h>
#include <MG_Pipe/MGPipe.h> #include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/PipeApply.h> #include <MG_Pipe/PipeApply.h>
#include <MG_Pipe/PipeRoute.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_Util/Metrics/PipeStats.h> #include <MG_Util/Metrics/PipeStats.h>
@@ -456,7 +457,7 @@ namespace MobileGL::MG_Pipe {
m_lastDraw = state; m_lastDraw = state;
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Both)) m_lastRead = state; if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Both)) m_lastRead = state;
} }
MGPipeApplySetFramebufferState(state); MGPipeRouteSetFramebufferState(state);
++m_emissions; ++m_emissions;
if (MG_Util::PipeStats::Enabled()) { if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::FramebufferEmissions, 1); MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::FramebufferEmissions, 1);
+2 -1
View File
@@ -38,6 +38,7 @@
#include <MG_Impl/Pipe/SlotAllocator.h> #include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h> #include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/PipeApply.h> #include <MG_Pipe/PipeApply.h>
#include <MG_Pipe/PipeRoute.h>
#include <MG_Pipe/PipeMutation.h> #include <MG_Pipe/PipeMutation.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_State/GLState/TextureState/TextureState.h> #include <MG_State/GLState/TextureState/TextureState.h>
@@ -126,7 +127,7 @@ namespace MobileGL::MG_Pipe {
m_lastImages.Start = 0; m_lastImages.Start = 0;
m_lastImages.Count = count; m_lastImages.Count = count;
m_lastImages.ContentHash = hash; m_lastImages.ContentHash = hash;
MGPipeApplySetShaderImages(m_lastImages, m_entries.data()); MGPipeRouteSetShaderImages(m_lastImages, m_entries.data());
++m_imageSets; ++m_imageSets;
if (MG_Util::PipeStats::Enabled()) { if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ShaderImageEmissions, 1); MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ShaderImageEmissions, 1);
+83 -16
View File
@@ -36,6 +36,7 @@
#include <MG_Impl/Pipe/VertexInputEmit.h> #include <MG_Impl/Pipe/VertexInputEmit.h>
#include <MG_Pipe/MGPipeRenderStateSpans.h> #include <MG_Pipe/MGPipeRenderStateSpans.h>
#include <MG_Pipe/PipeApply.h> #include <MG_Pipe/PipeApply.h>
#include <MG_Pipe/PipeRoute.h>
#include <MG_Pipe/PipeMutation.h> #include <MG_Pipe/PipeMutation.h>
#include <Config.h> #include <Config.h>
@@ -46,6 +47,7 @@
// build-split runs MOBILEGL_TRANSPORT=monolith in every unit and integration-gpu lane and those // build-split runs MOBILEGL_TRANSPORT=monolith in every unit and integration-gpu lane and those
// lanes must keep answering exactly what they answered before. // lanes must keep answering exactly what they answered before.
#include <MG_Remote/Client/CapsMirror.h> #include <MG_Remote/Client/CapsMirror.h>
#include <MG_Remote/Client/WireTables.h>
#endif #endif
#include <atomic> #include <atomic>
@@ -677,7 +679,7 @@ namespace MobileGL::MG_Pipe {
// LATCHED, so the destroy is gated on whether this create actually went out rather // LATCHED, so the destroy is gated on whether this create actually went out rather
// than on whether a table is still registered when the object dies (D-L, m12). // than on whether a table is still registered when the object dies (D-L, m12).
tracker.NotePublished(handle); tracker.NotePublished(handle);
MGPipeApplyResourceCreate(desc); MGPipeRouteResourceCreate(desc);
} }
void MGPipeEmitResourceRespecify(BufferObject& buffer) { void MGPipeEmitResourceRespecify(BufferObject& buffer) {
@@ -710,7 +712,7 @@ namespace MobileGL::MG_Pipe {
MGPipeBuildResourceDesc(buffer, handle, bindMask, /*storageDefined=*/false); MGPipeBuildResourceDesc(buffer, handle, bindMask, /*storageDefined=*/false);
tracker.NoteDesc(createDesc, true); tracker.NoteDesc(createDesc, true);
tracker.NotePublished(handle); tracker.NotePublished(handle);
MGPipeApplyResourceCreate(createDesc); MGPipeRouteResourceCreate(createDesc);
} }
const MGPResourceDesc desc = MGPipeBuildResourceDesc(buffer, handle, bindMask, true); const MGPResourceDesc desc = MGPipeBuildResourceDesc(buffer, handle, bindMask, true);
tracker.NoteDesc(desc, false); tracker.NoteDesc(desc, false);
@@ -723,7 +725,47 @@ namespace MobileGL::MG_Pipe {
// allocation and only it is allowed one. In monolith the acknowledgement is // allocation and only it is allowed one. In monolith the acknowledgement is
// ((void)0), because the applier is one function call away and has already run by // ((void)0), because the applier is one function call away and has already run by
// the time this returns; the transport wires the doorbell to that same predicate. // the time this returns; the transport wires the doorbell to that same predicate.
MGPipeApplyResourceRespecify(desc, initialBytes); #if MOBILEGL_BUILD_DISAGGREGATED
// R-13.3's MISSING PRODUCER, and it is the reason the first joint inproc run died.
// CONTRACT-P5 §2 row 19 rules that `initialBytes` is ALWAYS nullptr under split and
// that "initial content arrives as ResourceSubData records immediately after this
// one" - but nothing emitted those records, so the split arm's refusal
// (Fatal{UncarriedInitialBytes}) fired on the first glBufferData with data, which is
// the first thing every scenario does.
//
// IT IS THE CONTRACT'S OWN PRESCRIBED ROUTE, not a new one: "the chosen route reuses a
// path that is already chunked (MGPipeForEachSubDataRecordRange) and already
// acceptance-gated; it costs one extra record". So the respecify defines the storage
// and the walk below ships the bytes, through the same emitter every later
// glBufferSubData uses - which also means the HasLiveHostWrites bit and the
// acceptance latch are computed in exactly one place instead of two.
//
// SPLIT-ONLY, and that is load-bearing for G2: under monolith the applier reads
// `initialBytes` directly and a second upload would be a real behaviour change in the
// arm the split arm is measured against.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && initialBytes != nullptr) {
MGPipeRouteResourceRespecify(desc, nullptr);
// COUNTED, NOT ASSUMED, and this is the only thing that can gate the follow-up at
// all. Dropping the walk below leaves a respecify that went out with nullptr and
// bytes that nothing carried - and no P5 scenario's PICTURE changes, because every
// one of them re-uploads its vertices through the ordinary dirty path afterwards.
// So the statement "the content followed" is made HERE, against the client's own
// record ordinal, rather than left to a lane that cannot see it. Remove the call
// below and this aborts by name on the first glBufferData that carries data.
const Uint64 before = MG_Remote::Client::ClientWireRecordsEmitted();
MGPipeEmitResourceSubData(buffer, 0, static_cast<SizeT>(buffer.GetSize()));
if (MG_Remote::Client::ClientWireRecordsEmitted() == before) {
MGLOG_F("MGPipe: Fatal{InitialBytesNotCarried, \"resource_respecify\"} - the "
"respecify crossed with initialBytes = nullptr (R-13.3) and the "
"resource_subdata records that were supposed to follow it emitted "
"NOTHING, so %llu bytes of initial content exist on no side of the wire",
static_cast<unsigned long long>(buffer.GetSize()));
std::abort();
}
return;
}
#endif
MGPipeRouteResourceRespecify(desc, initialBytes);
} }
void MGPipeEmitResourceSubData(BufferObject& buffer, SizeT offset, SizeT size) { void MGPipeEmitResourceSubData(BufferObject& buffer, SizeT offset, SizeT size) {
@@ -746,7 +788,11 @@ namespace MobileGL::MG_Pipe {
// take the object, which is why it is set here and not in the builder. // take the object, which is why it is set here and not in the builder.
record.HasLiveHostWrites = buffer.HasLiveHostWritesForWire() ? 1 : 0; record.HasLiveHostWrites = buffer.HasLiveHostWritesForWire() ? 1 : 0;
#endif #endif
MGPipeApplyResourceSubData(record, base + at); // `length` is this chunk's byte count, which the record also declares
// (MGPipeBuildSubDataRecord writes it into the destination range) - passed
// rather than re-read so the staged run and the record's own claim come from
// one number.
MGPipeRouteResourceSubData(record, base + at, length);
}); });
if (!encodable) { if (!encodable) {
MGLOG_E_ONCE("MGPipe: resource_subdata range [%llu, +%llu) on buffer %u cannot be encoded - " MGLOG_E_ONCE("MGPipe: resource_subdata range [%llu, +%llu) on buffer %u cannot be encoded - "
@@ -777,7 +823,7 @@ namespace MobileGL::MG_Pipe {
record.HasLiveHostWrites = buffer.HasLiveHostWritesForWire() ? 1 : 0; record.HasLiveHostWrites = buffer.HasLiveHostWritesForWire() ? 1 : 0;
#endif #endif
// The application's STAGING store, valid for the duration of the call only. // The application's STAGING store, valid for the duration of the call only.
MGPipeApplyBufferSubDataResident(record, base + (at - offset)); MGPipeRouteBufferSubDataResident(record, base + (at - offset), length);
}); });
if (!encodable) { if (!encodable) {
MGLOG_E_ONCE("MGPipe: buffer_subdata_resident range [%llu, +%llu) on buffer %u cannot be encoded", MGLOG_E_ONCE("MGPipe: buffer_subdata_resident range [%llu, +%llu) on buffer %u cannot be encoded",
@@ -797,7 +843,28 @@ namespace MobileGL::MG_Pipe {
// arm reads INVALIDATE_RANGE / INVALIDATE_BUFFER / UNSYNCHRONIZED per call to choose // arm reads INVALIDATE_RANGE / INVALIDATE_BUFFER / UNSYNCHRONIZED per call to choose
// between a map+memcpy+unmap and an upload, so merging them here would change which. // between a map+memcpy+unmap and an upload, so merging them here would change which.
record.AccessFlags = accessFlags; record.AccessFlags = accessFlags;
MGPipeApplyResourceFlushRange(record, buffer.MappedData() + offset); #if MOBILEGL_BUILD_DISAGGREGATED
// R-13.2's MISSING PRODUCER, the twin of the respecify one above, and the cause of the
// seven PersistentCoherentMapScenario aborts on the first joint inproc run:
// Fatal{ProtocolCorruption} resource_flush_range {slot=1, gen=0, glName=1}:
// a non-empty flush carries no bytes (offset=0, size=120, storage=120 bytes)
//
// CONTRACT-P5 §2 row 20 rules that this record carries NO bytes under split - it is a
// {range, AccessFlags} control record, and a blobref here "would be a second,
// forgeable way to say the same thing" - and that "the bytes of [Offset, Offset+Size)
// arrive AHEAD of it as ResourceSubData records covering exactly that range". Nothing
// emitted those records, so v1's StagedShadowStore had nothing staged for the range
// the flush names, which is precisely the refusal ID-37 asked it to make rather than
// silently reading the bytes again.
//
// EXACTLY THAT RANGE, not the whole buffer: the flush's own [offset, size) is what
// the ladder rewrites, and staging more would be the coverage WIDENING ID-37 forbids.
// Split-only, for the respecify's G2 reason.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && size != 0) {
MGPipeEmitResourceSubData(buffer, offset, size);
}
#endif
MGPipeRouteResourceFlushRange(record, buffer.MappedData() + offset);
} }
void MGPipeEmitResourceReadback(BufferObject& buffer) { void MGPipeEmitResourceReadback(BufferObject& buffer) {
@@ -811,7 +878,7 @@ namespace MobileGL::MG_Pipe {
record.Size = buffer.GetSize(); record.Size = buffer.GetSize();
// The answer travels back through MGPipeClientOnBufferWriteback, and the server's // The answer travels back through MGPipeClientOnBufferWriteback, and the server's
// epoch bump happens AFTER that writeback, never before. // epoch bump happens AFTER that writeback, never before.
MGPipeApplyResourceReadback(record); MGPipeRouteResourceReadback(record);
} }
// NO UnmapPersistent PRODUCER IN P3a, AND THAT IS DELIBERATE. The catalogue has the call // NO UnmapPersistent PRODUCER IN P3a, AND THAT IS DELIBERATE. The catalogue has the call
@@ -834,7 +901,7 @@ namespace MobileGL::MG_Pipe {
if (MG_Util::PipeStats::Enabled()) { if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::MapPersistentRoundtrips, 1); MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::MapPersistentRoundtrips, 1);
} }
return MGPipeApplyMapPersistent(BufferHandleOnly(handle), buffer.GetSize(), buffer.MappedData()); return MGPipeRouteMapPersistent(BufferHandleOnly(handle), buffer.GetSize(), buffer.MappedData());
} }
Bool MGPipeEmitResourceDestroyAndFree(BufferObject& buffer) { Bool MGPipeEmitResourceDestroyAndFree(BufferObject& buffer) {
@@ -849,7 +916,7 @@ namespace MobileGL::MG_Pipe {
const Bool published = tracker.WasPublished(handle); const Bool published = tracker.WasPublished(handle);
if (published) { if (published) {
tracker.NoteDestroy(); tracker.NoteDestroy();
MGPipeApplyResourceDestroy(BufferHandleOnly(handle)); MGPipeRouteResourceDestroy(BufferHandleOnly(handle));
} }
// THE ORDER IS FIXED (D-L): the applier clears the record and the backend drops its // THE ORDER IS FIXED (D-L): the applier clears the record and the backend drops its
// twin while the handle still resolves, and only then does the slot go back. Free // twin while the handle still resolves, and only then does the slot go back. Free
@@ -889,7 +956,7 @@ namespace MobileGL::MG_Pipe {
MGPHandleOnly only{}; MGPHandleOnly only{};
only.Handle = handle; only.Handle = handle;
only.Kind = static_cast<Uint32>(MGPipeKind::VertexElementsCso); only.Kind = static_cast<Uint32>(MGPipeKind::VertexElementsCso);
MGPipeApplyDeleteVertexElements(only); MGPipeRouteDeleteVertexElements(only);
emitter.NoteRecordDestroyed(handle); emitter.NoteRecordDestroyed(handle);
} }
@@ -2103,7 +2170,7 @@ namespace MobileGL::MG_Pipe {
Uint64 EmitPixelPackState(GLContext& ctx) { Uint64 EmitPixelPackState(GLContext& ctx) {
MGPPixelPackState pack{}; MGPPixelPackState pack{};
pack.Pack = ctx.GetPixelStoreParameters(false); pack.Pack = ctx.GetPixelStoreParameters(false);
MGPipeApplySetPixelPackState(pack); MGPipeRouteSetPixelPackState(pack);
return sizeof(MGPPixelPackState); return sizeof(MGPPixelPackState);
} }
@@ -2116,7 +2183,7 @@ namespace MobileGL::MG_Pipe {
patch.Vertices = live.PatchVertices; patch.Vertices = live.PatchVertices;
for (SizeT i = 0; i < 4; ++i) patch.Outer[i] = live.PatchDefaultOuterLevel[i]; for (SizeT i = 0; i < 4; ++i) patch.Outer[i] = live.PatchDefaultOuterLevel[i];
for (SizeT i = 0; i < 2; ++i) patch.Inner[i] = live.PatchDefaultInnerLevel[i]; for (SizeT i = 0; i < 2; ++i) patch.Inner[i] = live.PatchDefaultInnerLevel[i];
MGPipeApplySetPatchState(patch); MGPipeRouteSetPatchState(patch);
return sizeof(MGPPatchState); return sizeof(MGPPatchState);
} }
@@ -2205,7 +2272,7 @@ namespace MobileGL::MG_Pipe {
} }
if (header.Count == 0) return 0; if (header.Count == 0) return 0;
g_attribDefaultLastHeader = header; g_attribDefaultLastHeader = header;
MGPipeApplySetVertexAttribDefaults(header, tail.data()); MGPipeRouteSetVertexAttribDefaults(header, tail.data());
// Did the applier reproduce it? Byte for byte, over the attributes this call // Did the applier reproduce it? Byte for byte, over the attributes this call
// named - anything less would be a mirror that disagrees with the frontend in a // named - anything less would be a mirror that disagrees with the frontend in a
@@ -2274,7 +2341,7 @@ namespace MobileGL::MG_Pipe {
block.CapabilityBits |= Uint64{1} << i; block.CapabilityBits |= Uint64{1} << i;
} }
} }
MGPipeApplySetResidualValueState(block); MGPipeRouteSetResidualValueState(block);
if (MG_Util::PipeStats::Enabled()) { if (MG_Util::PipeStats::Enabled()) {
// ByteClass::ResidualValueBlock has been a placeholder that "stays at 0 // ByteClass::ResidualValueBlock has been a placeholder that "stays at 0
// until P2" since P0. This is what makes it non-zero. // until P2" since P0. This is what makes it non-zero.
@@ -2328,7 +2395,7 @@ namespace MobileGL::MG_Pipe {
bind.Cso = cso; bind.Cso = cso;
bind.Version = version; bind.Version = version;
bind.PipelineVersion = pipelineVersion; bind.PipelineVersion = pipelineVersion;
MGPipeApplyBindRenderState(bind); MGPipeRouteBindRenderState(bind);
payloadBytes += sizeof(MGPBindRenderState); payloadBytes += sizeof(MGPBindRenderState);
if (MG_Util::PipeStats::Enabled()) { if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::RenderStateCsoBinds, 1); MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::RenderStateCsoBinds, 1);
@@ -2350,7 +2417,7 @@ namespace MobileGL::MG_Pipe {
dyn.ChunkMask = chunkMask; dyn.ChunkMask = chunkMask;
dyn.Version = version; dyn.Version = version;
dyn.Blob.Size = blobBytes; dyn.Blob.Size = blobBytes;
MGPipeApplySetDynamicState(dyn, blob.data()); MGPipeRouteSetDynamicState(dyn, blob.data());
payloadBytes += sizeof(MGPDynamicState) + blobBytes; payloadBytes += sizeof(MGPDynamicState) + blobBytes;
} }
+11 -5
View File
@@ -41,6 +41,7 @@
#include <MG_Pipe/MGPipe.h> #include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/MGPipeHostSpan.h> #include <MG_Pipe/MGPipeHostSpan.h>
#include <MG_Pipe/PipeApply.h> #include <MG_Pipe/PipeApply.h>
#include <MG_Pipe/PipeRoute.h>
#include <MG_Pipe/PipeMutation.h> #include <MG_Pipe/PipeMutation.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ProgramState/ProgramObject.h> #include <MG_State/GLState/ProgramState/ProgramObject.h>
@@ -137,19 +138,19 @@ namespace MobileGL::MG_Pipe {
// exactly "nothing bound". // exactly "nothing bound".
const MGPipeHandle boundCso = !MGPipeHandleIsNull(drawCso) ? drawCso : dispatchCso; const MGPipeHandle boundCso = !MGPipeHandleIsNull(drawCso) ? drawCso : dispatchCso;
if (boundCso != m_boundCso) { if (boundCso != m_boundCso) {
MGPipeApplyBindShaderState(HandleOnly(boundCso)); MGPipeRouteBindShaderState(HandleOnly(boundCso));
m_boundCso = boundCso; m_boundCso = boundCso;
++m_binds; ++m_binds;
bytes += sizeof(MGPHandleOnly); bytes += sizeof(MGPHandleOnly);
} }
if (drawCso != m_drawCso) { if (drawCso != m_drawCso) {
MGPipeApplySetDrawProgram(HandleOnly(drawCso)); MGPipeRouteSetDrawProgram(HandleOnly(drawCso));
m_drawCso = drawCso; m_drawCso = drawCso;
++m_drawSets; ++m_drawSets;
bytes += sizeof(MGPHandleOnly); bytes += sizeof(MGPHandleOnly);
} }
if (dispatchCso != m_dispatchCso) { if (dispatchCso != m_dispatchCso) {
MGPipeApplySetDispatchProgram(HandleOnly(dispatchCso)); MGPipeRouteSetDispatchProgram(HandleOnly(dispatchCso));
m_dispatchCso = dispatchCso; m_dispatchCso = dispatchCso;
++m_dispatchSets; ++m_dispatchSets;
bytes += sizeof(MGPHandleOnly); bytes += sizeof(MGPHandleOnly);
@@ -192,7 +193,12 @@ namespace MobileGL::MG_Pipe {
m_lastConstants.Blob.Seg = kMGHostSpanSegNone; m_lastConstants.Blob.Seg = kMGHostSpanSegNone;
m_lastConstants.Blob.Offset = reinterpret_cast<Uint64>(program->GetUBOData()); m_lastConstants.Blob.Offset = reinterpret_cast<Uint64>(program->GetUBOData());
m_lastConstants.Blob.Size = 0; m_lastConstants.Blob.Size = 0;
MGPipeApplySetGlobalConstants(m_lastConstants, program->GetUBOData()); // `size` is GetUBOSize(), and it is passed because the record declares 0 - the
// monolith convention (the bytes ride beside the record) that CONTRACT-P5 table 1
// rule A cannot keep under split. It is the row's largest and least bounded blob,
// per program per frame, so it is also the one R-10's max-record counter watches.
MGPipeRouteSetGlobalConstants(m_lastConstants, program->GetUBOData(),
static_cast<Uint64>(size));
m_constantsCso = cso; m_constantsCso = cso;
m_constantsVersion = version; m_constantsVersion = version;
++m_constantSets; ++m_constantSets;
@@ -254,7 +260,7 @@ namespace MobileGL::MG_Pipe {
m_lastDesc.Reflection.Offset = reinterpret_cast<Uint64>(&link); m_lastDesc.Reflection.Offset = reinterpret_cast<Uint64>(&link);
m_lastDesc.Reflection.Size = 0; m_lastDesc.Reflection.Size = 0;
MGPipeApplyCreateShaderState(m_lastDesc, &link, &spirv); MGPipeRouteCreateShaderState(m_lastDesc, &link, &spirv);
// THE CREATE WENT OUT, so the publication latch is taken here and nowhere else // THE CREATE WENT OUT, so the publication latch is taken here and nowhere else
// (contract-v2 §3.1). MGPipeEmitShaderCsoDestroyAndFree reads it, and without it // (contract-v2 §3.1). MGPipeEmitShaderCsoDestroyAndFree reads it, and without it
// delete_shader_state can never go out - for an ordinary program or for a // delete_shader_state can never go out - for an ordinary program or for a
+6 -5
View File
@@ -44,6 +44,7 @@
#include <MG_Pipe/MGPipe.h> #include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/MGPipeHostSpan.h> #include <MG_Pipe/MGPipeHostSpan.h>
#include <MG_Pipe/PipeApply.h> #include <MG_Pipe/PipeApply.h>
#include <MG_Pipe/PipeRoute.h>
#include <MG_Pipe/PipeMutation.h> #include <MG_Pipe/PipeMutation.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ProgramState/ProgramObject.h> #include <MG_State/GLState/ProgramState/ProgramObject.h>
@@ -455,7 +456,7 @@ namespace MobileGL::MG_Pipe {
// The applier is handed the CACHE's copy, so the pointer stays valid for the whole // The applier is handed the CACHE's copy, so the pointer stays valid for the whole
// call and the bytes it stores are provably the bytes the memcmp will confirm // call and the bytes it stores are provably the bytes the memcmp will confirm
// against later. // against later.
MGPipeApplyCreateSamplerState(desc, &m_entries.back().Params); MGPipeRouteCreateSamplerState(desc, &m_entries.back().Params);
// THE CREATE ACTUALLY WENT OUT, so the publication latch is taken here and nowhere // THE CREATE ACTUALLY WENT OUT, so the publication latch is taken here and nowhere
// else (contract-v2 §3.1). It is what the six death helpers read, and without it a // else (contract-v2 §3.1). It is what the six death helpers read, and without it a
// delete_sampler_state can never go out for this kind. // delete_sampler_state can never go out for this kind.
@@ -490,7 +491,7 @@ namespace MobileGL::MG_Pipe {
// only then does the slot go back. There is no NotifyStateObjectDestroyed step // only then does the slot go back. There is no NotifyStateObjectDestroyed step
// here - a content-addressed CSO has no frontend object whose death is being // here - a content-addressed CSO has no frontend object whose death is being
// announced, which is precisely why this eviction is the only death path it has. // announced, which is precisely why this eviction is the only death path it has.
MGPipeApplyDeleteSamplerState(handle); MGPipeRouteDeleteSamplerState(handle);
// AND THE LATCH GOES WITH THE DELETE. This is the "an emitter that drops a record // AND THE LATCH GOES WITH THE DELETE. This is the "an emitter that drops a record
// for its own reasons calls MGPipeNoteHandleUnpublished" half of the publication // for its own reasons calls MGPipeNoteHandleUnpublished" half of the publication
// protocol (contract-v2 §3.1): the record is gone, so a death helper reaching this // protocol (contract-v2 §3.1): the record is gone, so a death helper reaching this
@@ -783,7 +784,7 @@ namespace MobileGL::MG_Pipe {
m_lastViews.Start = 0; m_lastViews.Start = 0;
m_lastViews.Count = count; m_lastViews.Count = count;
m_lastViews.ContentHash = hash; m_lastViews.ContentHash = hash;
MGPipeApplySetSamplerViews(m_lastViews, m_views.data()); MGPipeRouteSetSamplerViews(m_lastViews, m_views.data());
++m_viewSets; ++m_viewSets;
if (MG_Util::PipeStats::Enabled()) { if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::SamplerViewEmissions, 1); MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::SamplerViewEmissions, 1);
@@ -857,7 +858,7 @@ namespace MobileGL::MG_Pipe {
m_lastStates.Start = 0; m_lastStates.Start = 0;
m_lastStates.Count = count; m_lastStates.Count = count;
m_lastStates.ContentHash = hash; m_lastStates.ContentHash = hash;
MGPipeApplyBindSamplerStates(m_lastStates, m_states.data()); MGPipeRouteBindSamplerStates(m_lastStates, m_states.data());
++m_stateSets; ++m_stateSets;
if (MG_Util::PipeStats::Enabled()) { if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::SamplerStateEmissions, 1); MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::SamplerStateEmissions, 1);
@@ -914,7 +915,7 @@ namespace MobileGL::MG_Pipe {
m_lastView.NumLayers = static_cast<Uint16>(texture.GetViewNumLayers()); m_lastView.NumLayers = static_cast<Uint16>(texture.GetViewNumLayers());
m_lastView.Samples = static_cast<Uint16>(texture.GetSamples() < 0 ? 0 : texture.GetSamples()); m_lastView.Samples = static_cast<Uint16>(texture.GetSamples() < 0 ? 0 : texture.GetSamples());
m_lastView.FixedSampleLocations = texture.HasFixedSampleLocations() ? 1 : 0; m_lastView.FixedSampleLocations = texture.HasFixedSampleLocations() ? 1 : 0;
MGPipeApplyCreateSamplerView(m_lastView); MGPipeRouteCreateSamplerView(m_lastView);
// THE CREATE WENT OUT, so the publication latch is taken (contract-v2 §3.1). The // THE CREATE WENT OUT, so the publication latch is taken (contract-v2 §3.1). The
// texture's death helper reads it, and without it delete_sampler_view can never go // texture's death helper reads it, and without it delete_sampler_view can never go
// out - the C-1 leak, one kind later. Re-taking it on a re-issue is right and // out - the C-1 leak, one kind later. Re-taking it on a re-issue is right and
+13 -5
View File
@@ -57,6 +57,7 @@
#include <MG_Impl/Pipe/SlotAllocator.h> #include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h> #include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/PipeApply.h> #include <MG_Pipe/PipeApply.h>
#include <MG_Pipe/PipeRoute.h>
#include <MG_Pipe/PipeMutation.h> #include <MG_Pipe/PipeMutation.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_State/GLState/RenderbufferState/RenderbufferObject.h> #include <MG_State/GLState/RenderbufferState/RenderbufferObject.h>
@@ -802,7 +803,7 @@ namespace MobileGL::MG_Pipe {
++m_paramSets; ++m_paramSets;
// Not behind MGPipeTextureRecordsReachTheApplier() (see its comment): the call is // Not behind MGPipeTextureRecordsReachTheApplier() (see its comment): the call is
// dispatched whenever this emitter runs, so the answer is always a real one. // dispatched whenever this emitter runs, so the answer is always a real one.
Bool accepted = MGPipeApplySetTextureParams(params); Bool accepted = MGPipeRouteSetTextureParams(params);
if (!accepted) { if (!accepted) {
// THE SELF-HEAL, the respecify path's shape, and the parameters are the one // THE SELF-HEAL, the respecify path's shape, and the parameters are the one
// publication that may be a texture's FIRST: the context's default textures are // publication that may be a texture's FIRST: the context's default textures are
@@ -828,7 +829,7 @@ namespace MobileGL::MG_Pipe {
// across it - `entry` is re-fetched below. // across it - `entry` is re-fetched below.
EmitResourceRespecify(texture, MGPipeTextureRespecifyScope::WholeResource, 0, 0); EmitResourceRespecify(texture, MGPipeTextureRespecifyScope::WholeResource, 0, 0);
} }
accepted = MGPipeApplySetTextureParams(params); accepted = MGPipeRouteSetTextureParams(params);
} }
Entry& latched = EntryFor(m_textures, handle); Entry& latched = EntryFor(m_textures, handle);
if (!accepted) { if (!accepted) {
@@ -1121,7 +1122,7 @@ namespace MobileGL::MG_Pipe {
Bool dispatched = false; Bool dispatched = false;
if constexpr (MGPipeTextureRecordsReachTheApplier()) { if constexpr (MGPipeTextureRecordsReachTheApplier()) {
dispatched = true; dispatched = true;
accepted = MGPipeApplyResourceCreate(desc); accepted = MGPipeRouteResourceCreate(desc);
} }
if (dispatched && !accepted) return; if (dispatched && !accepted) return;
MGPipeNoteHandlePublished(kind, handle); MGPipeNoteHandlePublished(kind, handle);
@@ -1134,7 +1135,7 @@ namespace MobileGL::MG_Pipe {
// replaces no storage at all. // replaces no storage at all.
static Bool ApplyRespecify(const MGPResourceDesc& desc, const MGPRespecifiedLevel* level) { static Bool ApplyRespecify(const MGPResourceDesc& desc, const MGPRespecifiedLevel* level) {
if constexpr (MGPipeTextureRecordsReachTheApplier()) { if constexpr (MGPipeTextureRecordsReachTheApplier()) {
return MGPipeApplyResourceRespecify(desc, nullptr, level); return MGPipeRouteResourceRespecify(desc, nullptr, level);
} }
(void)level; (void)level;
return false; return false;
@@ -1275,7 +1276,14 @@ namespace MobileGL::MG_Pipe {
Bool dispatched = false; Bool dispatched = false;
if constexpr (MGPipeTextureRecordsReachTheApplier()) { if constexpr (MGPipeTextureRecordsReachTheApplier()) {
dispatched = true; dispatched = true;
accepted = MGPipeApplyResourceSubData(m_lastSubData, shadow, // `levelBytes` closes CONTRACT-P5 table 1 row 7's open half. The record still
// declares Blob.Size 0 on the monolith arm - where the applier reads the
// companion pointer and the destination box bounds the write - but under split
// the staged run needs a length, and the comment above already says what it
// is: "the bytes this record declares ARE the level shadow". The regions' own
// SrcOffsets index into exactly that run.
accepted = MGPipeRouteResourceSubData(m_lastSubData, shadow,
static_cast<Uint64>(levelBytes),
m_regions.empty() ? nullptr : m_regions.data()); m_regions.empty() ? nullptr : m_regions.data());
} }
++m_subDatas; ++m_subDatas;
+6 -5
View File
@@ -43,6 +43,7 @@
#include <MG_Impl/Pipe/Tracker.h> #include <MG_Impl/Pipe/Tracker.h>
#include <MG_Pipe/MGPipe.h> #include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/PipeApply.h> #include <MG_Pipe/PipeApply.h>
#include <MG_Pipe/PipeRoute.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_Util/Metrics/PipeStats.h> #include <MG_Util/Metrics/PipeStats.h>
@@ -158,7 +159,7 @@ namespace MobileGL::MG_Pipe {
const auto& vao = ctx.GetBoundVertexArray(); const auto& vao = ctx.GetBoundVertexArray();
if (!vao) { if (!vao) {
if (!MGPipeHandleIsNull(m_boundHandle)) { if (!MGPipeHandleIsNull(m_boundHandle)) {
MGPipeApplyBindVertexElements(HandleOnly(kMGPipeNullHandle)); MGPipeRouteBindVertexElements(HandleOnly(kMGPipeNullHandle));
++m_binds; ++m_binds;
m_boundHandle = kMGPipeNullHandle; m_boundHandle = kMGPipeNullHandle;
m_boundLifetimeId = 0; m_boundLifetimeId = 0;
@@ -178,7 +179,7 @@ namespace MobileGL::MG_Pipe {
latch.Gen != handle.Gen; latch.Gen != handle.Gen;
if (configMoved) bytes += EmitCreate(*vao, handle, latch, configVersion); if (configMoved) bytes += EmitCreate(*vao, handle, latch, configVersion);
if (lifetimeId != m_boundLifetimeId || m_boundHandle != handle) { if (lifetimeId != m_boundLifetimeId || m_boundHandle != handle) {
MGPipeApplyBindVertexElements(HandleOnly(handle)); MGPipeRouteBindVertexElements(HandleOnly(handle));
++m_binds; ++m_binds;
bytes += sizeof(MGPHandleOnly); bytes += sizeof(MGPHandleOnly);
m_boundHandle = handle; m_boundHandle = handle;
@@ -242,7 +243,7 @@ namespace MobileGL::MG_Pipe {
// emulation is server-owned. // emulation is server-owned.
m_lastBuffers.BaseInstance = baseInstance; m_lastBuffers.BaseInstance = baseInstance;
m_lastBuffers.ContentHash = hash; m_lastBuffers.ContentHash = hash;
MGPipeApplySetVertexBuffers(m_lastBuffers, m_entries.data()); MGPipeRouteSetVertexBuffers(m_lastBuffers, m_entries.data());
++m_bufferSets; ++m_bufferSets;
return sizeof(MGPVertexBuffers) + static_cast<Uint64>(count) * sizeof(MGPVertexBuffer); return sizeof(MGPVertexBuffers) + static_cast<Uint64>(count) * sizeof(MGPVertexBuffer);
} }
@@ -267,7 +268,7 @@ namespace MobileGL::MG_Pipe {
MGPipeResourceTrackerInstance().NoteBoundAs(m_lastIndex.Res, BufferTarget::Index); MGPipeResourceTrackerInstance().NoteBoundAs(m_lastIndex.Res, BufferTarget::Index);
} }
} }
MGPipeApplySetIndexBuffer(m_lastIndex); MGPipeRouteSetIndexBuffer(m_lastIndex);
++m_indexSets; ++m_indexSets;
return sizeof(MGPIndexBuffer); return sizeof(MGPIndexBuffer);
} }
@@ -396,7 +397,7 @@ namespace MobileGL::MG_Pipe {
m_lastElements.Blob.Seg = kMGHostSpanSegNone; m_lastElements.Blob.Seg = kMGHostSpanSegNone;
m_lastElements.Blob.Offset = 0; m_lastElements.Blob.Offset = 0;
m_lastElements.Blob.Size = kAttribBytes + kBindingBytes; m_lastElements.Blob.Size = kAttribBytes + kBindingBytes;
MGPipeApplyCreateVertexElements(m_lastElements, m_blob.data()); MGPipeRouteCreateVertexElements(m_lastElements, m_blob.data());
++m_creates; ++m_creates;
latch.Published = true; latch.Published = true;
latch.Gen = handle.Gen; latch.Gen = handle.Gen;
+366
View File
@@ -0,0 +1,366 @@
// MobileGL - MobileGL/MG_Pipe/PipeRoute.cpp
// Copyright (c) 2025-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
// The MONOLITH arm of R-17's routing: thirty-seven adapters that unpack a generated table
// row's parameters and call the MGPipeApply* entry point the call site used to call directly.
// Owner: package c1. See PipeRoute.h for why the arm exists and what R-17 actually cost.
//
// G2 IS THE WHOLE SPECIFICATION OF THIS FILE. Under monolith transport the thunks must reach
// EXACTLY the code they reach today - so every adapter below is a parameter shuffle and
// nothing else. There is no branch, no cache, no early return and no logging on any of them,
// because each of those is a way for `MOBILEGL_TRANSPORT=monolith` to stop being the control
// arm that the split arm is measured against. The one thing an adapter may do beyond calling
// through is POST A REPLY, and only the five rows whose call site consumes a value do that.
//
// WHO CALLS THE INSTALL, AND WHY IT IS NOT IN THIS FILE. `gMGPipeScreen` and `gMGPipeContext`
// are inline variables with CONSTANT initialisation, so they are zero - every entry null,
// which is the pre-migration state - before any dynamic initialiser runs. The installer is
// driven from an inline variable in PipeRoute.h rather than from a static initialiser here,
// and the comment beside `detail::gMonolithTablesInstalled` says why: a static initialiser in
// THIS file is only in the program if THIS object file is in the link, and over a static
// archive it was not. That makes "the tables are installed" an invariant rather than a step
// someone can forget, and it is why no MGP_* thunk needs a null check - which matters, because
// a null check on a table slot is precisely the shape CONTRACT-P5 §7 spends the 41 call sites
// killing.
#include "PipeRoute.h"
#if MOBILEGL_PIPE_PUSH
#include <MG_Util/Debug/Log.h>
#include <cstdint>
#include <cstdlib>
#include <cstring>
namespace MobileGL::MG_Pipe {
namespace {
// ---------------------------------------------------------------------------------
// The reply mailbox
// ---------------------------------------------------------------------------------
struct ReplyMailbox {
Uint64 Slot = 0; // the MGPReplySlot::Id this answer belongs to; 0 = empty
Int32 Status = 0; // 0 OK, 1 DECLINED, 2 ERROR (Transport::ReplyStatus)
Uint64 Value = 0; // the Bool, or the pointer, the row answered with
Uint64 NextTicket = 0;
Uint64 Taken = 0;
Uint64 Declined = 0;
};
// THREAD-LOCAL, and one entry deep, because R-1's verb barrier makes the in-flight
// depth exactly one. A second post before the first is taken is not a capacity problem:
// it is a barrier that has stopped holding, and it Fatals rather than overwriting.
thread_local ReplyMailbox g_reply;
MGPipeRouteArm g_arm = MGPipeRouteArm::kNone;
// The monolith adapters, kept as tables of their own so the split arm can reach them
// on the server role's thread. See PipeRoute.h.
MGPipeScreen g_monolithScreen{};
MGPipeContext g_monolithContext{};
MGPipeRouteEscapes g_monolithEscapes{};
} // namespace
const MGPipeScreen& MGPipeMonolithScreen() { return g_monolithScreen; }
const MGPipeContext& MGPipeMonolithContext() { return g_monolithContext; }
const MGPipeRouteEscapes& MGPipeMonolithEscapes() { return g_monolithEscapes; }
MGPReplySlot MGPipeMintReplySlot() {
// 1-based, for ReplySlot.h's reason: "Seq is 1-based; 0 means 'no record'". A zero id
// must stay unmintable so an un-posted mailbox is distinguishable from a posted one.
return MGPReplySlot{++g_reply.NextTicket};
}
void MGPipePostReply(const MGPReplySlot& slot, Int32 status, Uint64 value) {
if (slot.Id == 0) {
MGLOG_F("MGPipe: Fatal{ReplyToNoSlot} - a table row posted an answer against reply "
"slot 0, which ReplySlot.h reserves for \"no record\"");
std::abort();
}
if (g_reply.Slot != 0 && g_reply.Slot != slot.Id) {
MGLOG_F("MGPipe: Fatal{ReplyOverrun} - slot %llu posted while slot %llu was still "
"unread. The mailbox is one deep because R-1's verb barrier makes the "
"in-flight depth one; two outstanding answers means the barrier is not "
"holding",
static_cast<unsigned long long>(slot.Id),
static_cast<unsigned long long>(g_reply.Slot));
std::abort();
}
g_reply.Slot = slot.Id;
g_reply.Status = status;
g_reply.Value = value;
}
namespace {
// The one reader. Fatals rather than defaulting, in both directions - see PipeRoute.h.
Uint64 TakeReply(const MGPReplySlot& slot, const char* row, Int32* statusOut) {
if (g_reply.Slot == 0) {
MGLOG_F("MGPipe: Fatal{ReplyMissing, \"%s\"} - the row was routed and nothing "
"posted an answer. R-5 forbids re-deriving acceptance on the client and "
"forbids assuming it, so there is no default to fall back to",
row);
std::abort();
}
if (g_reply.Slot != slot.Id) {
MGLOG_F("MGPipe: Fatal{ReplyMismatched, \"%s\"} - waiting on slot %llu, the "
"mailbox holds slot %llu",
row, static_cast<unsigned long long>(slot.Id),
static_cast<unsigned long long>(g_reply.Slot));
std::abort();
}
const Uint64 value = g_reply.Value;
if (statusOut != nullptr) *statusOut = g_reply.Status;
if (g_reply.Status == 1) ++g_reply.Declined;
++g_reply.Taken;
g_reply.Slot = 0;
g_reply.Value = 0;
g_reply.Status = 0;
return value;
}
} // namespace
Bool MGPipeTakeReplyBool(const MGPReplySlot& slot, const char* row) {
Int32 status = 0;
const Uint64 value = TakeReply(slot, row, &status);
// DECLINED IS A REAL ANSWER AND IT IS `false`, not a failure (ReplySlot.h). ERROR is
// not an acceptance answer at all and may not be folded into either.
if (status == 2) {
MGLOG_F("MGPipe: Fatal{ReplyError, \"%s\"} - the row answered ERROR, which is not an "
"acceptance answer; folding it into accepted or refused would make a "
"transport fault look like a resource decision",
row);
std::abort();
}
if (status == 1) return false;
return value != 0;
}
void* MGPipeTakeReplyPointer(const MGPReplySlot& slot, const char* row) {
Int32 status = 0;
const Uint64 value = TakeReply(slot, row, &status);
if (status == 2) {
MGLOG_F("MGPipe: Fatal{ReplyError, \"%s\"}", row);
std::abort();
}
// DECLINED is the null pointer, and it is R-6's answer for map_persistent under split.
if (status == 1) return nullptr;
return reinterpret_cast<void*>(static_cast<std::uintptr_t>(value));
}
Uint64 MGPipeRepliesTaken() { return g_reply.Taken; }
Uint64 MGPipeRepliesDeclined() { return g_reply.Declined; }
MGPipeRouteArm MGPipeInstalledArm() { return g_arm; }
void MGPipeNoteInstalledArm(MGPipeRouteArm arm) { g_arm = arm; }
Bool MGPipeTablesAreInstalled() { return g_arm != MGPipeRouteArm::kNone; }
// ---------------------------------------------------------------------------------
// The thirty-seven monolith adapters
// ---------------------------------------------------------------------------------
//
// Three shapes, so the eye can check them against PipeTables.inc in one pass rather than
// reading thirty-seven bodies. A row that does not fit one of the three is written out by
// hand BELOW the macros, never by widening a macro - a macro that grew a special case is
// how one of these silently stops being a parameter shuffle.
namespace {
#define MGP_MONO_PLAIN(Name, Payload) \
void Mono_##Name(const Payload* payload) { MGPipeApply##Name(*payload); }
#define MGP_MONO_BLOB(Name, Payload) \
void Mono_##Name(const Payload* payload, const void* blobBytes, Uint64) { \
MGPipeApply##Name(*payload, blobBytes); \
}
#define MGP_MONO_TAIL(Name, Payload, TailType) \
void Mono_##Name(const Payload* payload, const void* varTail, Uint32) { \
MGPipeApply##Name(*payload, static_cast<const TailType*>(varTail)); \
}
// -- screen -------------------------------------------------------------------
MGP_MONO_PLAIN(ResourceDestroy, MGPHandleOnly)
MGP_MONO_PLAIN(UnmapPersistent, MGPHandleOnly)
// -- context, plain -----------------------------------------------------------
MGP_MONO_PLAIN(BindRenderState, MGPBindRenderState)
MGP_MONO_PLAIN(DeleteRenderState, MGPHandleOnly)
MGP_MONO_PLAIN(BindVertexElements, MGPHandleOnly)
MGP_MONO_PLAIN(DeleteVertexElements, MGPHandleOnly)
MGP_MONO_PLAIN(DeleteSamplerState, MGPHandleOnly)
MGP_MONO_PLAIN(CreateSamplerView, MGPSamplerView)
MGP_MONO_PLAIN(DeleteSamplerView, MGPHandleOnly)
MGP_MONO_PLAIN(BindShaderState, MGPHandleOnly)
MGP_MONO_PLAIN(DeleteShaderState, MGPHandleOnly)
MGP_MONO_PLAIN(SetDrawProgram, MGPHandleOnly)
MGP_MONO_PLAIN(SetDispatchProgram, MGPHandleOnly)
MGP_MONO_PLAIN(SetFramebufferState, MGPFramebufferState)
MGP_MONO_PLAIN(SetIndexBuffer, MGPIndexBuffer)
MGP_MONO_PLAIN(SetPixelPackState, MGPPixelPackState)
MGP_MONO_PLAIN(SetPatchState, MGPPatchState)
// -- context, blob companion --------------------------------------------------
MGP_MONO_BLOB(CreateRenderState, MGPRenderStateDesc)
MGP_MONO_BLOB(CreateVertexElements, MGPVertexElements)
MGP_MONO_BLOB(SetDynamicState, MGPDynamicState)
MGP_MONO_BLOB(SetGlobalConstants, MGPGlobalConstants)
MGP_MONO_BLOB(BufferSubDataResident, MGPSubData)
// -- context, variable tail ---------------------------------------------------
MGP_MONO_TAIL(SetVertexBuffers, MGPVertexBuffers, MGPVertexBuffer)
MGP_MONO_TAIL(SetSamplerViews, MGPSamplerViews, MGPBoundView)
MGP_MONO_TAIL(BindSamplerStates, MGPSamplerStates, MGPipeHandle)
MGP_MONO_TAIL(SetShaderImages, MGPShaderImages, MGPImageView)
MGP_MONO_TAIL(SetVertexAttribDefaults, MGPVertexAttribDefaults, MGPAttribValue)
#undef MGP_MONO_PLAIN
#undef MGP_MONO_BLOB
#undef MGP_MONO_TAIL
// -- the rows that fit none of the three shapes --------------------------------
// create_sampler_state: the blob is a TYPED frontend struct, not bytes the applier
// reads through a void*. The cast is the whole adapter, and it is exact: the client
// stages sizeof(SamplerParameters) bytes of the same object (CONTRACT-P5 table 1
// row 17, including its padding trap - the bytes staged must be the bytes a later
// memcmp compares).
void Mono_CreateSamplerState(const MGPSamplerDesc* payload, const void* blobBytes, Uint64) {
MGPipeApplyCreateSamplerState(*payload,
static_cast<const SamplerParameters*>(blobBytes));
}
// set_residual_value_state: CONTRACT-P5 table 1 row 6, "the hardest row in the table".
// The applier takes `const ResidualValueBlock&` - a frontend type - and
// MGPResidualValueState is never instantiated on the live path. So the BLOCK IS THE
// BLOB, in both arms, and the adapter copies it back out. Requiring exact equality
// rather than ">=" is the decoder's rule too (PipeWireCodec.cpp): a size that only
// ever ratchets down makes a short read silently lose CapabilityBits.
void Mono_SetResidualValueState(const MGPResidualValueState*, const void* blobBytes,
Uint64 blobByteCount) {
if (blobByteCount != sizeof(ResidualValueBlock) || blobBytes == nullptr) {
MGLOG_F("MGPipe: Fatal{ResidualBlockSize} - set_residual_value_state carries %llu "
"bytes, the block is %llu",
static_cast<unsigned long long>(blobByteCount),
static_cast<unsigned long long>(sizeof(ResidualValueBlock)));
std::abort();
}
ResidualValueBlock block{};
std::memcpy(&block, blobBytes, sizeof(block));
MGPipeApplySetResidualValueState(block);
}
// resource_readback carries kReplySlot but its answer is COMPLETION, not a value: the
// bytes go server -> client through SEG_EVENT's OnBufferWriteback (CONTRACT-P5 table 1
// row 22), and the split decoder posts exactly kStatusOk with a zero-length payload.
// The monolith arm says the same thing, so the two arms hand their caller the same
// answer rather than one of them handing it nothing.
void Mono_ResourceReadback(const MGPReadback* payload, MGPReplySlot* reply) {
MGPipeApplyResourceReadback(*payload);
MGPipePostReply(*reply, 0, 0);
}
// The three acceptance rows that DO fit a generated signature. Each posts the
// applier's own answer; none of them invents one.
void Mono_ResourceCreate(const MGPResourceDesc* payload, MGPReplySlot* reply) {
const Bool accepted = MGPipeApplyResourceCreate(*payload);
MGPipePostReply(*reply, accepted ? 0 : 1, accepted ? 1u : 0u);
}
void Mono_SetTextureParams(const MGPTextureParams* payload, MGPReplySlot* reply) {
const Bool accepted = MGPipeApplySetTextureParams(*payload);
MGPipePostReply(*reply, accepted ? 0 : 1, accepted ? 1u : 0u);
}
void Mono_ResourceSubData(const MGPSubData* payload, const void* blobBytes, Uint64,
const void* varTail, Uint32, MGPReplySlot* reply) {
const Bool accepted = MGPipeApplyResourceSubData(
*payload, blobBytes, static_cast<const MGPSubRegion*>(varTail));
MGPipePostReply(*reply, accepted ? 0 : 1, accepted ? 1u : 0u);
}
// -- the four escapes ----------------------------------------------------------
Bool Mono_Escape_ResourceRespecify(const MGPResourceDesc* desc, const void* initialBytes,
const MGPRespecifiedLevel* level) {
return MGPipeApplyResourceRespecify(*desc, initialBytes, level);
}
void Mono_Escape_ResourceFlushRange(const MGPFlushRange* record, const void* bytes) {
MGPipeApplyResourceFlushRange(*record, bytes);
}
void* Mono_Escape_MapPersistent(const MGPHandleOnly* handle, Uint64 size,
const void* seedBytes) {
return MGPipeApplyMapPersistent(*handle, size, seedBytes);
}
void Mono_Escape_CreateShaderState(const MGPProgramDesc* desc,
const MG_State::GLState::LinkArtifacts* link,
const MG_State::GLState::SpirvArtifacts* spirv) {
MGPipeApplyCreateShaderState(*desc, link, spirv);
}
} // namespace
void MGPipeInstallMonolithTables() {
gMGPipeScreen.ResourceCreate = &Mono_ResourceCreate;
gMGPipeScreen.ResourceDestroy = &Mono_ResourceDestroy;
gMGPipeScreen.UnmapPersistent = &Mono_UnmapPersistent;
gMGPipeContext.CreateRenderState = &Mono_CreateRenderState;
gMGPipeContext.BindRenderState = &Mono_BindRenderState;
gMGPipeContext.DeleteRenderState = &Mono_DeleteRenderState;
gMGPipeContext.CreateVertexElements = &Mono_CreateVertexElements;
gMGPipeContext.BindVertexElements = &Mono_BindVertexElements;
gMGPipeContext.DeleteVertexElements = &Mono_DeleteVertexElements;
gMGPipeContext.CreateSamplerState = &Mono_CreateSamplerState;
gMGPipeContext.DeleteSamplerState = &Mono_DeleteSamplerState;
gMGPipeContext.CreateSamplerView = &Mono_CreateSamplerView;
gMGPipeContext.DeleteSamplerView = &Mono_DeleteSamplerView;
gMGPipeContext.BindShaderState = &Mono_BindShaderState;
gMGPipeContext.DeleteShaderState = &Mono_DeleteShaderState;
gMGPipeContext.SetDrawProgram = &Mono_SetDrawProgram;
gMGPipeContext.SetDispatchProgram = &Mono_SetDispatchProgram;
gMGPipeContext.SetDynamicState = &Mono_SetDynamicState;
gMGPipeContext.SetFramebufferState = &Mono_SetFramebufferState;
gMGPipeContext.SetVertexBuffers = &Mono_SetVertexBuffers;
gMGPipeContext.SetIndexBuffer = &Mono_SetIndexBuffer;
gMGPipeContext.SetSamplerViews = &Mono_SetSamplerViews;
gMGPipeContext.BindSamplerStates = &Mono_BindSamplerStates;
gMGPipeContext.SetShaderImages = &Mono_SetShaderImages;
gMGPipeContext.SetGlobalConstants = &Mono_SetGlobalConstants;
gMGPipeContext.SetVertexAttribDefaults = &Mono_SetVertexAttribDefaults;
gMGPipeContext.SetPixelPackState = &Mono_SetPixelPackState;
gMGPipeContext.SetPatchState = &Mono_SetPatchState;
gMGPipeContext.SetResidualValueState = &Mono_SetResidualValueState;
gMGPipeContext.SetTextureParams = &Mono_SetTextureParams;
gMGPipeContext.ResourceSubData = &Mono_ResourceSubData;
gMGPipeContext.BufferSubDataResident = &Mono_BufferSubDataResident;
gMGPipeContext.ResourceReadback = &Mono_ResourceReadback;
gMGPipeRouteEscapes.ResourceRespecify = &Mono_Escape_ResourceRespecify;
gMGPipeRouteEscapes.ResourceFlushRange = &Mono_Escape_ResourceFlushRange;
gMGPipeRouteEscapes.MapPersistent = &Mono_Escape_MapPersistent;
gMGPipeRouteEscapes.CreateShaderState = &Mono_Escape_CreateShaderState;
// KEPT, not merely installed. The client arm overwrites the three tables above; these
// three copies are what it forwards to when it finds itself on the server's own thread.
g_monolithScreen = gMGPipeScreen;
g_monolithContext = gMGPipeContext;
g_monolithEscapes = gMGPipeRouteEscapes;
MGPipeNoteInstalledArm(MGPipeRouteArm::kMonolith);
}
} // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_PIPE_PUSH
+400
View File
@@ -0,0 +1,400 @@
// MobileGL - MobileGL/MG_Pipe/PipeRoute.h
// Copyright (c) 2025-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
// THE CLIENT -> WIRE ROUTING OF THE 37 MGPipeApply* ENTRY POINTS (P5, integrator ruling R-17).
// Owner: package c1.
//
// WHAT WAS MISSING. `gMGPipeWireRecordApply` is the DECODE hook and it has existed since w1:
// a record arrives, `PipeWireCodec` picks it apart and calls `MGPipeApply<Name>`. There was no
// encode twin. Every `MG_Impl/Pipe` emitter called `MGPipeApply<Name>` DIRECTLY, so under
// `MOBILEGL_TRANSPORT=inproc` every resource, CSO, texture and program record still executed
// synchronously on the GL thread against a context v1 is moving to the apply thread - where
// all sixteen `IsBackendContextCurrentOnThisThread()` and sixteen `CanTouchGLNow()` guards
// answer false. `ClearThenReadPixels` might have survived that; `TriangleScenario` needs a VBO
// and a program and could not.
//
// The two generated tables (`gMGPipeScreen`, `gMGPipeContext`, `generated/PipeTables.inc`) are
// the boundary this repository already had, and they were never installed - every entry null,
// which `MGPipe.h` correctly calls "precisely the pre-migration state". R-17 installs them.
// This header is the frontend-facing half: the names `MG_Impl/Pipe` calls, and the mechanism
// that gets a kReplySlot row's answer back to its call site.
//
// ---------------------------------------------------------------------------------------
// THE SIZE OF R-17, MEASURED RATHER THAN ESTIMATED
// ---------------------------------------------------------------------------------------
//
// R-17 costed this as "37 thin client emitters over EmitAndWait". The emitters are thin. The
// ROUTING was not, and the reason is that the generated table signature is the P2-era monolith
// interface: `void (*Name)(const Payload* payload, ...)`, async-with-handle, no return value,
// and NO COMPANION POINTER. Of the 37 entry points, as the tables stood:
//
// 24 the row could carry every argument the call site passes today
// 13 it could not
//
// The thirteen split three ways, and only one of the three is a generator question:
//
// (a) NINE carry a blob companion - `chunkBytes`, `blobBytes`, `parameters`, `bytes`,
// `initialBytes` - which is CONTRACT-P5 table 1's "companion pointer today" column. A
// kVarTail row already gains `const void* varTail, Uint32 varTailCount`; a kHasBlob row
// gained nothing. THE GENERATOR CHANGE IS THAT ONE ASYMMETRY, and nothing else: five
// lines in `Call.Signature` giving a kHasBlob row `const void* blobBytes, Uint64
// blobByteCount`. Ten rows gained the pair (the nine plus `GetCaps`, which has no applier
// entry point). `--check` up to date, `--self-test` 9/9 trips.
//
// (b) FOUR return a value the row cannot: `ResourceCreate`, `ResourceRespecify`,
// `ResourceSubData` and `SetTextureParams` return Bool and `MapPersistent` returns void*.
// These are exactly CONTRACT-P5 §2's reply-slot rows, and R-5 forbids re-deriving any of
// them on the client. The answer therefore has to come back THROUGH the reply slot, which
// is what `MGPipeTakeReply*` below is, and it is the part of R-17 that no amount of
// "thin emitter" covers: `MGPReplySlot` is `{Uint64 Id;}` - an IDENTIFIER, not a value -
// so a reader had to exist for the identifier to be worth minting.
//
// (c) FOUR CANNOT GO THROUGH A GENERATED ROW AT ALL, and each one is a contract ruling
// rather than an oversight. They are the ESCAPES below. Saying "four" out loud is the
// honest answer to "tell me if it is bigger than it looks": it is, by four rows and one
// reply-reading mechanism.
//
// ---------------------------------------------------------------------------------------
// THE REPLY MAILBOX, AND WHY ONE ENTRY IS THE RIGHT DEPTH
// ---------------------------------------------------------------------------------------
//
// `MGPReplySlot` carries an id; the answer lands beside it. The mailbox is ONE ENTRY DEEP and
// thread-local, and that is not a simplification - it is R-1's verb barrier stated as a data
// structure. While the barrier holds, the in-flight depth is exactly one (`ReplySlot.h`:
// "under R-1's verb barrier the in-flight depth is one"), so a second posting before the first
// is taken is not a capacity problem, it is a barrier that has stopped working. It Fatals.
//
// THE MAILBOX HAS NO DEFAULT ANSWER AND THIS IS THE WHOLE POINT. `MGPipeTakeReplyBool` on a
// slot nothing posted to is `Fatal{ReplyMissing, "<row>"}`, not `false` and emphatically not
// `true`. "Always accept" is ID-39's 66 lost DirectVulkan uploads with a wire in between, and
// "always refuse" is an emitter that re-sends for ever. A row that forgets to answer is a bug
// that must be impossible to ship, so it is impossible to READ.
//
// WHEN THE BARRIER RETIRES (R-1 opens family by family) this becomes a real slot pool keyed on
// seq, which is what `Transport::ReplySlot` already is on the wire side. The mailbox is the
// frontend-side handle onto it and nothing else; it is deliberately NOT a second id space,
// because the split arm stamps the record's own seq into `MGPReplySlot::Id` (R-3).
#pragma once
#include <Includes.h>
#if MOBILEGL_PIPE_PUSH
#include "MGPipe.h"
#include "PipeApply.h"
namespace MobileGL::MG_Pipe {
// ---------------------------------------------------------------------------------
// The reply mailbox
// ---------------------------------------------------------------------------------
// Mints the id for one call's answer. Monolith's ids and the wire's seqs are separate
// spaces on purpose: under split the emitter OVERWRITES this with the record's own seq
// (R-3: "the reply slot id IS the record sequence number"), so the id a caller finally
// reads is the wire's, and under monolith it is a local ticket that only has to be unique
// against the one outstanding call the barrier permits.
MGPReplySlot MGPipeMintReplySlot();
// Posts one answer. Called by whichever table is installed - the monolith adapter with
// the applier's return value, the client emitter with the bytes the server put in
// SEG_REPLY. `status` is Transport::ReplyStatus' value space (0 OK, 1 DECLINED, 2 ERROR),
// restated as a plain Int32 so MG_Pipe does not have to see MG_Remote at all.
void MGPipePostReply(const MGPReplySlot& slot, Int32 status, Uint64 value);
// Takes the answer for `slot`. Fatals - naming `row` - if nothing was posted, if what was
// posted belongs to a different slot, or if the answer has already been taken.
Bool MGPipeTakeReplyBool(const MGPReplySlot& slot, const char* row);
void* MGPipeTakeReplyPointer(const MGPReplySlot& slot, const char* row);
// How many answers this thread has taken, and how many of those were DECLINED. Counted
// rather than inferred, for R-8's reason one level out: "the client accepted everything"
// and "the client never asked" are otherwise the same observation from outside.
Uint64 MGPipeRepliesTaken();
Uint64 MGPipeRepliesDeclined();
// ---------------------------------------------------------------------------------
// The escapes: the four rows no generated signature can express
// ---------------------------------------------------------------------------------
//
// A hand-written third table, installed and uninstalled by the same two functions as the
// generated pair, so there is ONE mechanism and not two. Each row's signature is the
// applier's own, because that is the shape the monolith arm has to reproduce byte for
// byte (G2), and each row is here for a ruling that is written down:
//
// ResourceRespecify `initialBytes` has NO kHasBlob and R-13.3 forbids giving it one
// ("initialBytes is always nullptr under split; initial content
// arrives as ResourceSubData records immediately after this one").
// So the row is correct to omit it AND monolith must still pass it.
// ResourceFlushRange `bytes` likewise, by R-13.2, and for a sharper reason: a blobref
// here would be "a second, forgeable way to say the same thing".
// MapPersistent `size` and `seedBytes` have no carrier at all - `MGPHandleOnly` is
// {Handle, Kind} - and the call returns void*. R-6 makes the split
// answer a constant DECLINE, so the carrier is not needed; the
// monolith arm needs both arguments.
// CreateShaderState SEVEN blobrefs (`Spirv[6]` + `Reflection`) and TWO typed frontend
// pointers. One `blobBytes` pair cannot express seven runs, and
// serialising under monolith to un-serialise in the adapter would put
// `EncodeProgramArtifacts` on the monolith path, which `PipeApply.h`
// explicitly promises it is not ("zero serialisation cost on the
// monolith path").
//
// OVERTURN CONDITIONS, one per row: give `MGPResourceDesc` and `MGPFlushRange` a blobref
// (overturns R-13.2/R-13.3, and c0 owns `MGPipeTypes.h`); give `MGPHandleOnly` a size
// (same owner) and P6 a real remote map; measure that a per-stage SPIR-V run beats one
// archive, at which point `CreateShaderState` needs a multi-blob row rather than this one.
struct MGPipeRouteEscapes {
Bool (*ResourceRespecify)(const MGPResourceDesc* desc, const void* initialBytes,
const MGPRespecifiedLevel* level);
void (*ResourceFlushRange)(const MGPFlushRange* record, const void* bytes);
void* (*MapPersistent)(const MGPHandleOnly* handle, Uint64 size, const void* seedBytes);
void (*CreateShaderState)(const MGPProgramDesc* desc,
const MG_State::GLState::LinkArtifacts* link,
const MG_State::GLState::SpirvArtifacts* spirv);
};
inline MGPipeRouteEscapes gMGPipeRouteEscapes{};
// ---------------------------------------------------------------------------------
// The two install functions
// ---------------------------------------------------------------------------------
// Installs the monolith adapters into all three tables. Idempotent.
void MGPipeInstallMonolithTables();
// The monolith adapters, kept beside the installed tables rather than only written into
// them. THE SPLIT ARM NEEDS THEM AT RUNTIME, and the reason is table 3's role split made
// concrete: `gMGPipeScreen` / `gMGPipeContext` are PROCESS globals, and under `inproc` the
// server role lives in the same process on the apply thread. When that thread runs the
// server's own backend - the EGL bring-up, InitCapabilities, or the applier itself - it
// reaches the very same `MG_Impl/Pipe` emitters the client does, and a wire emitter there
// publishes a record and then waits for the apply thread to apply it. That thread IS the
// apply thread, so it waits for itself: `Fatal{BarrierTimeout, "ResourceRespecify"}`,
// logged by `mgl-srv-apply`, thirty seconds after bring-up starts.
//
// So the client emitters ask "am I the server role right now?" and, if so, run the
// monolith adapter - which is exactly what `PipeWireCodec` already does on the decode side
// by calling `MGPipeApply*` directly. The predicate is v1's `ServerLoop::OnApplyThread()`;
// these three accessors are what makes the other half reachable.
const MGPipeScreen& MGPipeMonolithScreen();
const MGPipeContext& MGPipeMonolithContext();
const MGPipeRouteEscapes& MGPipeMonolithEscapes();
namespace detail {
// THE INSTALL THAT CANNOT BE LINKED AWAY, and the first version of this WAS.
//
// It began as a static initialiser inside PipeRoute.cpp, on the reasoning that
// `gMGPipeScreen` and `gMGPipeContext` are inline variables with CONSTANT (zero)
// initialisation - sequenced before every dynamic initialiser - so an installer in
// dynamic init necessarily beats any GL entry point. That reasoning is correct and the
// mechanism still failed, because it assumed PipeRoute.o would be in the link at all:
// every MG_Test target is its OWN binary over a static archive, `CsoCache.h` reaches
// the table through an inline thunk and names no symbol from PipeRoute.o, so the
// linker dropped the object, the initialiser never ran, and five CsoCacheTest cases
// took a null function pointer. The shared library linked it and was fine, which is
// exactly the shape that ships.
//
// AN INLINE VARIABLE FIXES BOTH HALVES AT ONCE. Its initialiser is emitted as a COMDAT
// in every translation unit that includes this header, so any binary that can call a
// thunk has one copy of it; and naming `MGPipeInstallMonolithTables` from a header the
// call sites already include is an undefined reference that forces the archive member
// into the link. There is still exactly one install, and it still happens before main.
inline const Bool gMonolithTablesInstalled = (MGPipeInstallMonolithTables(), true);
} // namespace detail
// True once the tables hold something. Exposed for the gates only: a probe that armed
// against an un-installed table would be arming against the pre-migration state.
Bool MGPipeTablesAreInstalled();
// Which arm is installed, so a case can assert the arm it thinks it is testing rather than
// trusting `MOBILEGL_TRANSPORT`. `MG_Config::Transport` says what was ASKED for; this says
// what the table actually does.
enum class MGPipeRouteArm : Uint8 { kNone, kMonolith, kClientWire };
MGPipeRouteArm MGPipeInstalledArm();
void MGPipeNoteInstalledArm(MGPipeRouteArm arm);
// ---------------------------------------------------------------------------------
// The call-site names: MGPipeRoute<Name> for each of the thirty-seven
// ---------------------------------------------------------------------------------
//
// EVERY ONE TAKES THE APPLIER'S OWN SIGNATURE, so converting a call site is a rename and
// nothing else. That is not tidiness, it is the G1 lesson from round 1 stated as a rule:
// rewriting `if (const auto f = TABLE.GL.Slot)` into a Bool-valued macro is semantically
// identical and generates DIFFERENT code (-144 bytes, two symbols resized). A wrapper that
// took `&expr` would force every site holding a temporary - `HandleOnly(handle)`,
// `BufferHandleOnly(handle)` - to grow a named local, which is a second expression shape
// change at forty sites. A reference parameter keeps the call site's text identical but
// for the name.
//
// THREE OF THE THIRTY-SEVEN TAKE ONE ARGUMENT MORE THAN THEIR APPLIER, and the extra is
// always the same thing: A BYTE COUNT THE RECORD DOES NOT DECLARE. CONTRACT-P5 table 1
// rule A requires every kHasBlob row to declare its length under split; five rows already
// do and the wrapper reads it back off the record, three do not and their call site is the
// only place the number exists:
// SetGlobalConstants declares 0 (ProgramEmit.h); the length is GetUBOSize()
// ResourceSubData buffer half declares real, TEXTURE half declares 0 on the
// grounds that the count is "the server's to compute" - which
// table 1 row 7 says "cannot be a bounds check". The level
// shadow's byte size is in scope at that call site.
// BufferSubDataResident the application's staging store, sized by the caller
// A defaulted argument was rejected: a default would let a site that has the number forget
// to pass it and still compile, and the failure would be a silently short upload.
// ---- screen ------------------------------------------------------------------------
inline Bool MGPipeRouteResourceCreate(const MGPResourceDesc& desc) {
MGPReplySlot reply = MGPipeMintReplySlot();
MGP_ResourceCreate(&desc, &reply);
return MGPipeTakeReplyBool(reply, "resource_create");
}
inline Bool MGPipeRouteResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes,
const MGPRespecifiedLevel* level = nullptr) {
return gMGPipeRouteEscapes.ResourceRespecify(&desc, initialBytes, level);
}
inline void MGPipeRouteResourceDestroy(const MGPHandleOnly& handle) {
MGP_ResourceDestroy(&handle);
}
inline void* MGPipeRouteMapPersistent(const MGPHandleOnly& handle, Uint64 size,
const void* seedBytes) {
return gMGPipeRouteEscapes.MapPersistent(&handle, size, seedBytes);
}
inline void MGPipeRouteUnmapPersistent(const MGPHandleOnly& handle) {
MGP_UnmapPersistent(&handle);
}
// ---- resources ---------------------------------------------------------------------
inline Bool MGPipeRouteResourceSubData(const MGPSubData& record, const void* bytes,
Uint64 byteCount,
const MGPSubRegion* regions = nullptr) {
MGPReplySlot reply = MGPipeMintReplySlot();
MGP_ResourceSubData(&record, bytes, byteCount, regions, record.RegionCount, &reply);
return MGPipeTakeReplyBool(reply, "resource_subdata");
}
inline void MGPipeRouteBufferSubDataResident(const MGPSubData& record, const void* bytes,
Uint64 byteCount) {
MGP_BufferSubDataResident(&record, bytes, byteCount);
}
inline void MGPipeRouteResourceFlushRange(const MGPFlushRange& record, const void* bytes) {
gMGPipeRouteEscapes.ResourceFlushRange(&record, bytes);
}
inline void MGPipeRouteResourceReadback(const MGPReadback& record) {
MGPReplySlot reply = MGPipeMintReplySlot();
MGP_ResourceReadback(&record, &reply);
// The answer is COMPLETION, not a value: the bytes come back through SEG_EVENT's
// OnBufferWriteback (table 1 row 22). It is still TAKEN, because an untaken mailbox
// entry is what the next row's ReplyOverrun Fatal is looking for.
(void)MGPipeTakeReplyBool(reply, "resource_readback");
}
// ---- render state ------------------------------------------------------------------
inline void MGPipeRouteCreateRenderState(const MGPRenderStateDesc& desc,
const void* chunkBytes) {
MGP_CreateRenderState(&desc, chunkBytes, desc.Blob.Size);
}
inline void MGPipeRouteBindRenderState(const MGPBindRenderState& bind) {
MGP_BindRenderState(&bind);
}
inline void MGPipeRouteDeleteRenderState(const MGPHandleOnly& handle) {
MGP_DeleteRenderState(&handle);
}
inline void MGPipeRouteSetDynamicState(const MGPDynamicState& dyn, const void* chunkBytes) {
MGP_SetDynamicState(&dyn, chunkBytes, dyn.Blob.Size);
}
inline void MGPipeRouteSetPixelPackState(const MGPPixelPackState& pack) {
MGP_SetPixelPackState(&pack);
}
inline void MGPipeRouteSetPatchState(const MGPPatchState& patch) { MGP_SetPatchState(&patch); }
inline void MGPipeRouteSetVertexAttribDefaults(const MGPVertexAttribDefaults& hdr,
const MGPAttribValue* tail) {
MGP_SetVertexAttribDefaults(&hdr, tail, hdr.Count);
}
// The block IS the blob (table 1 row 6) and its size is a header constant both sides
// read, so no call site has to know it.
inline void MGPipeRouteSetResidualValueState(const ResidualValueBlock& block) {
MGPResidualValueState record{};
record.Version = 0;
MGP_SetResidualValueState(&record, &block, sizeof(ResidualValueBlock));
}
// ---- vertex input ------------------------------------------------------------------
inline void MGPipeRouteCreateVertexElements(const MGPVertexElements& desc,
const void* blobBytes) {
MGP_CreateVertexElements(&desc, blobBytes, desc.Blob.Size);
}
inline void MGPipeRouteBindVertexElements(const MGPHandleOnly& handle) {
MGP_BindVertexElements(&handle);
}
inline void MGPipeRouteDeleteVertexElements(const MGPHandleOnly& handle) {
MGP_DeleteVertexElements(&handle);
}
inline void MGPipeRouteSetVertexBuffers(const MGPVertexBuffers& hdr,
const MGPVertexBuffer* tail) {
MGP_SetVertexBuffers(&hdr, tail, hdr.Count);
}
inline void MGPipeRouteSetIndexBuffer(const MGPIndexBuffer& record) {
MGP_SetIndexBuffer(&record);
}
// ---- framebuffer, samplers, images -------------------------------------------------
inline void MGPipeRouteSetFramebufferState(const MGPFramebufferState& state) {
MGP_SetFramebufferState(&state);
}
// The parameters row declares Size 0 today and table 1 row 17 puts it under rule A, so
// the length is stated here once, from the type the applier stores by value.
inline void MGPipeRouteCreateSamplerState(const MGPSamplerDesc& desc,
const SamplerParameters* parameters) {
MGP_CreateSamplerState(&desc, parameters, sizeof(SamplerParameters));
}
inline void MGPipeRouteDeleteSamplerState(const MGPHandleOnly& handle) {
MGP_DeleteSamplerState(&handle);
}
inline void MGPipeRouteCreateSamplerView(const MGPSamplerView& view) {
MGP_CreateSamplerView(&view);
}
inline void MGPipeRouteDeleteSamplerView(const MGPHandleOnly& handle) {
MGP_DeleteSamplerView(&handle);
}
inline Bool MGPipeRouteSetTextureParams(const MGPTextureParams& params) {
MGPReplySlot reply = MGPipeMintReplySlot();
MGP_SetTextureParams(&params, &reply);
return MGPipeTakeReplyBool(reply, "set_texture_params");
}
inline void MGPipeRouteSetSamplerViews(const MGPSamplerViews& hdr, const MGPBoundView* tail) {
MGP_SetSamplerViews(&hdr, tail, hdr.Count);
}
inline void MGPipeRouteBindSamplerStates(const MGPSamplerStates& hdr, const MGPipeHandle* tail) {
MGP_BindSamplerStates(&hdr, tail, hdr.Count);
}
inline void MGPipeRouteSetShaderImages(const MGPShaderImages& hdr, const MGPImageView* tail) {
MGP_SetShaderImages(&hdr, tail, hdr.Count);
}
// ---- programs ----------------------------------------------------------------------
inline void MGPipeRouteCreateShaderState(const MGPProgramDesc& desc,
const MG_State::GLState::LinkArtifacts* link,
const MG_State::GLState::SpirvArtifacts* spirv) {
gMGPipeRouteEscapes.CreateShaderState(&desc, link, spirv);
}
inline void MGPipeRouteBindShaderState(const MGPHandleOnly& handle) {
MGP_BindShaderState(&handle);
}
inline void MGPipeRouteDeleteShaderState(const MGPHandleOnly& handle) {
MGP_DeleteShaderState(&handle);
}
inline void MGPipeRouteSetDrawProgram(const MGPHandleOnly& handle) {
MGP_SetDrawProgram(&handle);
}
inline void MGPipeRouteSetDispatchProgram(const MGPHandleOnly& handle) {
MGP_SetDispatchProgram(&handle);
}
inline void MGPipeRouteSetGlobalConstants(const MGPGlobalConstants& record, const void* bytes,
Uint64 byteCount) {
MGP_SetGlobalConstants(&record, bytes, byteCount);
}
} // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_PIPE_PUSH
+10 -10
View File
@@ -15,7 +15,7 @@
// share group: 11 calls. A null entry means the backend does not implement this // share group: 11 calls. A null entry means the backend does not implement this
// call and the frontend keeps its own path (plan B section 4.1). // call and the frontend keeps its own path (plan B section 4.1).
struct MGPipeScreen { struct MGPipeScreen {
void (*GetCaps)(const MGPCaps* payload, MGPReplySlot* reply); void (*GetCaps)(const MGPCaps* payload, const void* blobBytes, Uint64 blobByteCount, MGPReplySlot* reply);
void (*ResourceCreate)(const MGPResourceDesc* payload, MGPReplySlot* reply); void (*ResourceCreate)(const MGPResourceDesc* payload, MGPReplySlot* reply);
void (*ResourceRespecify)(const MGPResourceDesc* payload, MGPReplySlot* reply); void (*ResourceRespecify)(const MGPResourceDesc* payload, MGPReplySlot* reply);
void (*ResourceDestroy)(const MGPHandleOnly* payload); void (*ResourceDestroy)(const MGPHandleOnly* payload);
@@ -37,20 +37,20 @@ struct MGPipeContext {
void (*QueryAvailable)(const MGPHandleOnly* payload, MGPReplySlot* reply); void (*QueryAvailable)(const MGPHandleOnly* payload, MGPReplySlot* reply);
void (*QueryResult)(const MGPQueryResultRequest* payload, MGPReplySlot* reply); void (*QueryResult)(const MGPQueryResultRequest* payload, MGPReplySlot* reply);
void (*QueryDestroy)(const MGPHandleOnly* payload); void (*QueryDestroy)(const MGPHandleOnly* payload);
void (*CreateRenderState)(const MGPRenderStateDesc* payload); void (*CreateRenderState)(const MGPRenderStateDesc* payload, const void* blobBytes, Uint64 blobByteCount);
void (*BindRenderState)(const MGPBindRenderState* payload); void (*BindRenderState)(const MGPBindRenderState* payload);
void (*DeleteRenderState)(const MGPHandleOnly* payload); void (*DeleteRenderState)(const MGPHandleOnly* payload);
void (*CreateVertexElements)(const MGPVertexElements* payload); void (*CreateVertexElements)(const MGPVertexElements* payload, const void* blobBytes, Uint64 blobByteCount);
void (*BindVertexElements)(const MGPHandleOnly* payload); void (*BindVertexElements)(const MGPHandleOnly* payload);
void (*DeleteVertexElements)(const MGPHandleOnly* payload); void (*DeleteVertexElements)(const MGPHandleOnly* payload);
void (*CreateSamplerState)(const MGPSamplerDesc* payload); void (*CreateSamplerState)(const MGPSamplerDesc* payload, const void* blobBytes, Uint64 blobByteCount);
void (*DeleteSamplerState)(const MGPHandleOnly* payload); void (*DeleteSamplerState)(const MGPHandleOnly* payload);
void (*CreateSamplerView)(const MGPSamplerView* payload); void (*CreateSamplerView)(const MGPSamplerView* payload);
void (*DeleteSamplerView)(const MGPHandleOnly* payload); void (*DeleteSamplerView)(const MGPHandleOnly* payload);
void (*CreateShaderState)(const MGPProgramDesc* payload); void (*CreateShaderState)(const MGPProgramDesc* payload, const void* blobBytes, Uint64 blobByteCount);
void (*BindShaderState)(const MGPHandleOnly* payload); void (*BindShaderState)(const MGPHandleOnly* payload);
void (*DeleteShaderState)(const MGPHandleOnly* payload); void (*DeleteShaderState)(const MGPHandleOnly* payload);
void (*SetDynamicState)(const MGPDynamicState* payload); void (*SetDynamicState)(const MGPDynamicState* payload, const void* blobBytes, Uint64 blobByteCount);
void (*SetFramebufferState)(const MGPFramebufferState* payload); void (*SetFramebufferState)(const MGPFramebufferState* payload);
void (*SetVertexBuffers)(const MGPVertexBuffers* payload, const void* varTail, Uint32 varTailCount); void (*SetVertexBuffers)(const MGPVertexBuffers* payload, const void* varTail, Uint32 varTailCount);
void (*SetIndexBuffer)(const MGPIndexBuffer* payload); void (*SetIndexBuffer)(const MGPIndexBuffer* payload);
@@ -60,16 +60,16 @@ struct MGPipeContext {
void (*SetShaderImages)(const MGPShaderImages* payload, const void* varTail, Uint32 varTailCount); void (*SetShaderImages)(const MGPShaderImages* payload, const void* varTail, Uint32 varTailCount);
void (*SetShaderBuffers)(const MGPShaderBuffers* payload, const void* varTail, Uint32 varTailCount); void (*SetShaderBuffers)(const MGPShaderBuffers* payload, const void* varTail, Uint32 varTailCount);
void (*SetStreamOutputTargets)(const MGPStreamOutputTargets* payload, const void* varTail, Uint32 varTailCount); void (*SetStreamOutputTargets)(const MGPStreamOutputTargets* payload, const void* varTail, Uint32 varTailCount);
void (*SetGlobalConstants)(const MGPGlobalConstants* payload); void (*SetGlobalConstants)(const MGPGlobalConstants* payload, const void* blobBytes, Uint64 blobByteCount);
void (*SetVertexAttribDefaults)(const MGPVertexAttribDefaults* payload, const void* varTail, Uint32 varTailCount); void (*SetVertexAttribDefaults)(const MGPVertexAttribDefaults* payload, const void* varTail, Uint32 varTailCount);
void (*SetPixelPackState)(const MGPPixelPackState* payload); void (*SetPixelPackState)(const MGPPixelPackState* payload);
void (*SetPatchState)(const MGPPatchState* payload); void (*SetPatchState)(const MGPPatchState* payload);
void (*SetDrawProgram)(const MGPHandleOnly* payload); void (*SetDrawProgram)(const MGPHandleOnly* payload);
void (*SetDispatchProgram)(const MGPHandleOnly* payload); void (*SetDispatchProgram)(const MGPHandleOnly* payload);
void (*SetResidualValueState)(const MGPResidualValueState* payload); void (*SetResidualValueState)(const MGPResidualValueState* payload, const void* blobBytes, Uint64 blobByteCount);
void (*SetTextureParams)(const MGPTextureParams* payload, MGPReplySlot* reply); void (*SetTextureParams)(const MGPTextureParams* payload, MGPReplySlot* reply);
void (*ResourceSubData)(const MGPSubData* payload, const void* varTail, Uint32 varTailCount, MGPReplySlot* reply); void (*ResourceSubData)(const MGPSubData* payload, const void* blobBytes, Uint64 blobByteCount, const void* varTail, Uint32 varTailCount, MGPReplySlot* reply);
void (*BufferSubDataResident)(const MGPSubData* payload); void (*BufferSubDataResident)(const MGPSubData* payload, const void* blobBytes, Uint64 blobByteCount);
void (*ResourceSubDataComplete)(const MGPSubDataComplete* payload); void (*ResourceSubDataComplete)(const MGPSubDataComplete* payload);
void (*ResourceFlushRange)(const MGPFlushRange* payload); void (*ResourceFlushRange)(const MGPFlushRange* payload);
void (*ResourceReadback)(const MGPReadback* payload, MGPReplySlot* reply); void (*ResourceReadback)(const MGPReadback* payload, MGPReplySlot* reply);
+20 -20
View File
@@ -17,8 +17,8 @@
// unimplemented (null) entry is the caller's business to check, exactly as it is // unimplemented (null) entry is the caller's business to check, exactly as it is
// with the table this replaces. // with the table this replaces.
inline void MGP_GetCaps(const MGPCaps* payload, MGPReplySlot* reply) { inline void MGP_GetCaps(const MGPCaps* payload, const void* blobBytes, Uint64 blobByteCount, MGPReplySlot* reply) {
gMGPipeScreen.GetCaps(payload, reply); gMGPipeScreen.GetCaps(payload, blobBytes, blobByteCount, reply);
} }
inline void MGP_ResourceCreate(const MGPResourceDesc* payload, MGPReplySlot* reply) { inline void MGP_ResourceCreate(const MGPResourceDesc* payload, MGPReplySlot* reply) {
@@ -81,8 +81,8 @@ inline void MGP_QueryDestroy(const MGPHandleOnly* payload) {
gMGPipeContext.QueryDestroy(payload); gMGPipeContext.QueryDestroy(payload);
} }
inline void MGP_CreateRenderState(const MGPRenderStateDesc* payload) { inline void MGP_CreateRenderState(const MGPRenderStateDesc* payload, const void* blobBytes, Uint64 blobByteCount) {
gMGPipeContext.CreateRenderState(payload); gMGPipeContext.CreateRenderState(payload, blobBytes, blobByteCount);
} }
inline void MGP_BindRenderState(const MGPBindRenderState* payload) { inline void MGP_BindRenderState(const MGPBindRenderState* payload) {
@@ -93,8 +93,8 @@ inline void MGP_DeleteRenderState(const MGPHandleOnly* payload) {
gMGPipeContext.DeleteRenderState(payload); gMGPipeContext.DeleteRenderState(payload);
} }
inline void MGP_CreateVertexElements(const MGPVertexElements* payload) { inline void MGP_CreateVertexElements(const MGPVertexElements* payload, const void* blobBytes, Uint64 blobByteCount) {
gMGPipeContext.CreateVertexElements(payload); gMGPipeContext.CreateVertexElements(payload, blobBytes, blobByteCount);
} }
inline void MGP_BindVertexElements(const MGPHandleOnly* payload) { inline void MGP_BindVertexElements(const MGPHandleOnly* payload) {
@@ -105,8 +105,8 @@ inline void MGP_DeleteVertexElements(const MGPHandleOnly* payload) {
gMGPipeContext.DeleteVertexElements(payload); gMGPipeContext.DeleteVertexElements(payload);
} }
inline void MGP_CreateSamplerState(const MGPSamplerDesc* payload) { inline void MGP_CreateSamplerState(const MGPSamplerDesc* payload, const void* blobBytes, Uint64 blobByteCount) {
gMGPipeContext.CreateSamplerState(payload); gMGPipeContext.CreateSamplerState(payload, blobBytes, blobByteCount);
} }
inline void MGP_DeleteSamplerState(const MGPHandleOnly* payload) { inline void MGP_DeleteSamplerState(const MGPHandleOnly* payload) {
@@ -121,8 +121,8 @@ inline void MGP_DeleteSamplerView(const MGPHandleOnly* payload) {
gMGPipeContext.DeleteSamplerView(payload); gMGPipeContext.DeleteSamplerView(payload);
} }
inline void MGP_CreateShaderState(const MGPProgramDesc* payload) { inline void MGP_CreateShaderState(const MGPProgramDesc* payload, const void* blobBytes, Uint64 blobByteCount) {
gMGPipeContext.CreateShaderState(payload); gMGPipeContext.CreateShaderState(payload, blobBytes, blobByteCount);
} }
inline void MGP_BindShaderState(const MGPHandleOnly* payload) { inline void MGP_BindShaderState(const MGPHandleOnly* payload) {
@@ -133,8 +133,8 @@ inline void MGP_DeleteShaderState(const MGPHandleOnly* payload) {
gMGPipeContext.DeleteShaderState(payload); gMGPipeContext.DeleteShaderState(payload);
} }
inline void MGP_SetDynamicState(const MGPDynamicState* payload) { inline void MGP_SetDynamicState(const MGPDynamicState* payload, const void* blobBytes, Uint64 blobByteCount) {
gMGPipeContext.SetDynamicState(payload); gMGPipeContext.SetDynamicState(payload, blobBytes, blobByteCount);
} }
inline void MGP_SetFramebufferState(const MGPFramebufferState* payload) { inline void MGP_SetFramebufferState(const MGPFramebufferState* payload) {
@@ -173,8 +173,8 @@ inline void MGP_SetStreamOutputTargets(const MGPStreamOutputTargets* payload, co
gMGPipeContext.SetStreamOutputTargets(payload, varTail, varTailCount); gMGPipeContext.SetStreamOutputTargets(payload, varTail, varTailCount);
} }
inline void MGP_SetGlobalConstants(const MGPGlobalConstants* payload) { inline void MGP_SetGlobalConstants(const MGPGlobalConstants* payload, const void* blobBytes, Uint64 blobByteCount) {
gMGPipeContext.SetGlobalConstants(payload); gMGPipeContext.SetGlobalConstants(payload, blobBytes, blobByteCount);
} }
inline void MGP_SetVertexAttribDefaults(const MGPVertexAttribDefaults* payload, const void* varTail, Uint32 varTailCount) { inline void MGP_SetVertexAttribDefaults(const MGPVertexAttribDefaults* payload, const void* varTail, Uint32 varTailCount) {
@@ -197,20 +197,20 @@ inline void MGP_SetDispatchProgram(const MGPHandleOnly* payload) {
gMGPipeContext.SetDispatchProgram(payload); gMGPipeContext.SetDispatchProgram(payload);
} }
inline void MGP_SetResidualValueState(const MGPResidualValueState* payload) { inline void MGP_SetResidualValueState(const MGPResidualValueState* payload, const void* blobBytes, Uint64 blobByteCount) {
gMGPipeContext.SetResidualValueState(payload); gMGPipeContext.SetResidualValueState(payload, blobBytes, blobByteCount);
} }
inline void MGP_SetTextureParams(const MGPTextureParams* payload, MGPReplySlot* reply) { inline void MGP_SetTextureParams(const MGPTextureParams* payload, MGPReplySlot* reply) {
gMGPipeContext.SetTextureParams(payload, reply); gMGPipeContext.SetTextureParams(payload, reply);
} }
inline void MGP_ResourceSubData(const MGPSubData* payload, const void* varTail, Uint32 varTailCount, MGPReplySlot* reply) { inline void MGP_ResourceSubData(const MGPSubData* payload, const void* blobBytes, Uint64 blobByteCount, const void* varTail, Uint32 varTailCount, MGPReplySlot* reply) {
gMGPipeContext.ResourceSubData(payload, varTail, varTailCount, reply); gMGPipeContext.ResourceSubData(payload, blobBytes, blobByteCount, varTail, varTailCount, reply);
} }
inline void MGP_BufferSubDataResident(const MGPSubData* payload) { inline void MGP_BufferSubDataResident(const MGPSubData* payload, const void* blobBytes, Uint64 blobByteCount) {
gMGPipeContext.BufferSubDataResident(payload); gMGPipeContext.BufferSubDataResident(payload, blobBytes, blobByteCount);
} }
inline void MGP_ResourceSubDataComplete(const MGPSubDataComplete* payload) { inline void MGP_ResourceSubDataComplete(const MGPSubDataComplete* payload) {
@@ -29,113 +29,30 @@ namespace MobileGL::MG_Remote::Client {
namespace { namespace {
// ---- the EGL bridge --------------------------------------------------------------- // ---- the EGL seam ------------------------------------------------------------------
// //
// Every one of the nine crosses as a BLOCKING control request on the apply thread, // THE NINE EGL VIRTUALS CALL v1's TWELVE FORWARDERS AND NOTHING ELSE. c1 round 1 built
// because every one of them has a return value the caller acts on immediately. v1's // its own trampolines over ServerLoop::RunOnApplyThread and ServerLoop::Backend(),
// ServerLoop::RunOnApplyThread takes a raw function pointer plus a user pointer rather // which ran the right driver call on the right thread and was still wrong, because
// than a std::function, deliberately: this path runs at teardown too, and the teardown // three of the twelve do MORE than forward:
// path may not allocate (ID-8).
// //
// A NULL SERVER BACKEND IS "false", NOT A CRASH AND NOT A LOCAL SUCCESS. Under inproc // ServerMakeEGLCurrent re-publishes the caps snapshot after the server's own
// the apply thread creates it during ClientSession::Start, so a null one here means the // InitCapabilities has run (R-12 arm (a))
// bring-up did not complete - and answering `true` would let the frontend believe it // ServerInitCapabilities the same, on the explicit path
// has a context. // ServerSetWindowHandle hands the surface to the server's backend
MG_Backend::BackendObject* ServerBackend() { return Server::ServerLoopInstance().Backend(); } //
// Calling `Backend()->MakeEGLCurrent(...)` skips the republish, so the client's mirror
struct DisplayArgs { // keeps the snapshot Accept() sent BEFORE any context existed - every limit, every
EGLDisplay Dpy; // advertised extension and the compile-env fingerprint read off an empty backend, with
EGLint* Major; // InitCapabilities below happily reporting success because a snapshot did arrive once.
EGLint* Minor; // That is the failure this seam exists to make impossible: there is no second route to
Bool Ok; // the server's EGL, so there is no route that can skip what the forwarder does.
}; //
MobileGLResult RunInitDisplay(void* user) { // The forwarders block on mgl-srv-apply themselves and run INLINE when the caller is
auto* args = static_cast<DisplayArgs*>(user); // already on that thread, so this file no longer needs RunOnApplyThread, a per-call
args->Ok = ServerBackend()->InitializeEGLDisplay(args->Dpy, args->Major, args->Minor); // args struct, or a null-backend check of its own - ServerBackendOrNull() inside each
return MOBILEGL_OK; // forwarder is the one place that answers "the bring-up did not complete", and it
} // answers `false` rather than crashing or succeeding locally.
struct WindowSurfaceArgs {
EGLSurface Surface;
const MG_Backend::WindowHandle* Handle;
Bool Ok;
};
MobileGLResult RunCreateWindowSurface(void* user) {
auto* args = static_cast<WindowSurfaceArgs*>(user);
args->Ok = ServerBackend()->CreateEGLWindowSurface(args->Surface, *args->Handle);
return MOBILEGL_OK;
}
struct ResizeArgs {
EGLSurface Surface;
Uint32 Width;
Uint32 Height;
Bool Ok;
};
MobileGLResult RunResize(void* user) {
auto* args = static_cast<ResizeArgs*>(user);
args->Ok = ServerBackend()->ResizeEGLWindowSurface(args->Surface, args->Width, args->Height);
return MOBILEGL_OK;
}
struct PbufferArgs {
EGLSurface Surface;
EGLint Width;
EGLint Height;
Bool Ok;
};
MobileGLResult RunCreatePbuffer(void* user) {
auto* args = static_cast<PbufferArgs*>(user);
args->Ok = ServerBackend()->CreateEGLPbufferSurface(args->Surface, args->Width, args->Height);
return MOBILEGL_OK;
}
struct MakeCurrentArgs {
EGLDisplay Dpy;
EGLSurface Draw;
EGLSurface Read;
EGLContext Ctx;
Bool Ok;
};
MobileGLResult RunMakeCurrent(void* user) {
auto* args = static_cast<MakeCurrentArgs*>(user);
args->Ok = ServerBackend()->MakeEGLCurrent(args->Dpy, args->Draw, args->Read, args->Ctx);
return MOBILEGL_OK;
}
struct SwapIntervalArgs {
Int Interval;
};
MobileGLResult RunSwapInterval(void* user) {
ServerBackend()->SetEGLSwapInterval(static_cast<SwapIntervalArgs*>(user)->Interval);
return MOBILEGL_OK;
}
struct SurfaceArgs {
EGLSurface Surface;
};
MobileGLResult RunReleaseSurface(void* user) {
ServerBackend()->ReleaseEGLSurface(static_cast<SurfaceArgs*>(user)->Surface);
return MOBILEGL_OK;
}
MobileGLResult RunReleaseResources(void*) {
ServerBackend()->ReleaseEGLResources();
return MOBILEGL_OK;
}
// Runs `work` on the apply thread if there is a server backend to run it against, and
// says so by name when there is not.
Bool ForwardToApplyThread(const char* what, Server::ServerLoop::ControlWork work, void* user) {
if (ServerBackend() == nullptr) {
MGLOG_E("MG_Remote client: %s has no server backend to forward to - the apply "
"thread's bring-up did not complete, and answering success here would "
"tell the frontend it has a context it does not have",
what);
return false;
}
return Server::ServerLoopInstance().RunOnApplyThread(work, user) == MOBILEGL_OK;
}
// CapsMirror's adoption hook. A free function because the hook is a raw function // CapsMirror's adoption hook. A free function because the hook is a raw function
// pointer (ID-8: this can fire on a path that must not allocate), and it reaches the // pointer (ID-8: this can fire on a path that must not allocate), and it reaches the
@@ -198,6 +115,18 @@ namespace MobileGL::MG_Remote::Client {
MGLOG_E("MG_Remote client: InitCapabilities with no session"); MGLOG_E("MG_Remote client: InitCapabilities with no session");
return false; return false;
} }
// ASK THE SERVER RATHER THAN ASSUMING ServerMakeEGLCurrent HAS ALREADY ASKED IT. The
// comment above says "by this point MakeEGLCurrent has already run the SERVER's
// MakeEGLCurrent", and that is true on the make-current path - but the base class
// reaches InitCapabilities lazily, once per SURFACE lifetime, and a surface can be
// replaced without a new make-current. ServerInitCapabilities re-publishes the
// snapshot the same way, so asking twice costs one snapshot and never asking costs
// every limit in the mirror. It is idempotent on the server's side.
if (!Server::ServerInitCapabilities()) {
MGLOG_E("MG_Remote client: the server's InitCapabilities failed, so there is no "
"snapshot to adopt and going current would read default limits");
return false;
}
session->PumpControlPlane(); session->PumpControlPlane();
RefreshFormatCapabilities(); RefreshFormatCapabilities();
// A PLACEHOLDER MIRROR IS A FAILURE HERE, unlike at startup. LogBackendInfo reading a // A PLACEHOLDER MIRROR IS A FAILURE HERE, unlike at startup. LogBackendInfo reading a
@@ -251,46 +180,36 @@ namespace MobileGL::MG_Remote::Client {
// InitCapabilities has run. // InitCapabilities has run.
Bool BackendObject_Remote::InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) { Bool BackendObject_Remote::InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) {
DisplayArgs args{dpy, major, minor, false}; if (!Server::ServerInitializeEGLDisplay(dpy, major, minor)) return false;
if (!ForwardToApplyThread("InitializeEGLDisplay", &RunInitDisplay, &args) || !args.Ok) {
return false;
}
return MG_Backend::BackendObject::InitializeEGLDisplay(dpy, major, minor); return MG_Backend::BackendObject::InitializeEGLDisplay(dpy, major, minor);
} }
Bool BackendObject_Remote::CreateEGLWindowSurface(EGLSurface surface, Bool BackendObject_Remote::CreateEGLWindowSurface(EGLSurface surface,
const MG_Backend::WindowHandle& handle) { const MG_Backend::WindowHandle& handle) {
WindowSurfaceArgs args{surface, &handle, false}; // The handle first: the server's backend has to know which window it is about to make
if (!ForwardToApplyThread("CreateEGLWindowSurface", &RunCreateWindowSurface, &args) || // a surface for, and ServerSetWindowHandle is the only way to tell it.
!args.Ok) { Server::ServerSetWindowHandle(handle);
return false; if (!Server::ServerCreateEGLWindowSurface(surface, handle)) return false;
}
return MG_Backend::BackendObject::CreateEGLWindowSurface(surface, handle); return MG_Backend::BackendObject::CreateEGLWindowSurface(surface, handle);
} }
Bool BackendObject_Remote::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) { Bool BackendObject_Remote::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
ResizeArgs args{surface, width, height, false}; if (!Server::ServerResizeEGLWindowSurface(surface, width, height)) return false;
if (!ForwardToApplyThread("ResizeEGLWindowSurface", &RunResize, &args) || !args.Ok) {
return false;
}
return MG_Backend::BackendObject::ResizeEGLWindowSurface(surface, width, height); return MG_Backend::BackendObject::ResizeEGLWindowSurface(surface, width, height);
} }
Bool BackendObject_Remote::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) { Bool BackendObject_Remote::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
PbufferArgs args{surface, width, height, false}; if (!Server::ServerCreateEGLPbufferSurface(surface, width, height)) return false;
if (!ForwardToApplyThread("CreateEGLPbufferSurface", &RunCreatePbuffer, &args) || !args.Ok) {
return false;
}
return MG_Backend::BackendObject::CreateEGLPbufferSurface(surface, width, height); return MG_Backend::BackendObject::CreateEGLPbufferSurface(surface, width, height);
} }
Bool BackendObject_Remote::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, Bool BackendObject_Remote::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read,
EGLContext ctx) { EGLContext ctx) {
MakeCurrentArgs args{dpy, draw, read, ctx, false}; // FORWARD FIRST, THEN RUN THE BASE. The server has to own the context before the base
if (!ForwardToApplyThread("MakeEGLCurrent", &RunMakeCurrent, &args) || !args.Ok) { // class latches "the surface is initialised" and calls InitCapabilities, because
return false; // InitCapabilities' answer comes from a snapshot the server can only publish once its
} // own InitCapabilities has run - and ServerMakeEGLCurrent is what publishes it.
// AND ONLY NOW the client's own bookkeeping, which is what calls InitCapabilities. if (!Server::ServerMakeEGLCurrent(dpy, draw, read, ctx)) return false;
return MG_Backend::BackendObject::MakeEGLCurrent(dpy, draw, read, ctx); return MG_Backend::BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
} }
@@ -298,9 +217,10 @@ namespace MobileGL::MG_Remote::Client {
// NOT FORWARDED, and this is the one that must not be. The base implementation's last // NOT FORWARDED, and this is the one that must not be. The base implementation's last
// act is GetBackendFunctions().Present() (BackendObject.cpp:396) - which is this // act is GetBackendFunctions().Present() (BackendObject.cpp:396) - which is this
// client's class-B Present EMITTER, the only route by which Present is reached at all // client's class-B Present EMITTER, the only route by which Present is reached at all
// (it has zero MG_Impl call sites). Forwarding would present on the server directly and // (it has zero MG_Impl call sites). Calling Server::ServerSwapEGLBuffers would present
// put no record on the wire, which is the shape every gate in this phase exists to // on the server directly and put no record on the wire, which is the shape every gate
// catch. // in this phase exists to catch. The forwarder exists for a spawned P6 client whose
// Present record cannot carry the swap; in P5 it has no caller and that is deliberate.
return MG_Backend::BackendObject::SwapEGLBuffers(dpy, draw); return MG_Backend::BackendObject::SwapEGLBuffers(dpy, draw);
} }
@@ -311,13 +231,11 @@ namespace MobileGL::MG_Remote::Client {
// the base implementation would abort on every eglSwapInterval. The answer is the // the base implementation would abort on every eglSwapInterval. The answer is the
// caps-mirror-read rule's general shape: the question "can the presentation path take // caps-mirror-read rule's general shape: the question "can the presentation path take
// an interval" belongs to the server, so it is asked of the server. // an interval" belongs to the server, so it is asked of the server.
SwapIntervalArgs args{interval}; Server::ServerSetEGLSwapInterval(interval);
ForwardToApplyThread("SetEGLSwapInterval", &RunSwapInterval, &args);
} }
void BackendObject_Remote::ReleaseEGLSurface(EGLSurface surface) { void BackendObject_Remote::ReleaseEGLSurface(EGLSurface surface) {
SurfaceArgs args{surface}; Server::ServerReleaseEGLSurface(surface);
ForwardToApplyThread("ReleaseEGLSurface", &RunReleaseSurface, &args);
MG_Backend::BackendObject::ReleaseEGLSurface(surface); MG_Backend::BackendObject::ReleaseEGLSurface(surface);
} }
@@ -325,7 +243,7 @@ namespace MobileGL::MG_Remote::Client {
// BLOCKING BY CONTRACT (ServerLoop.h's header note): MobileGL::Destroy() // BLOCKING BY CONTRACT (ServerLoop.h's header note): MobileGL::Destroy()
// (MobileGL/Init.cpp:68) walks on the moment this returns, and the server still holds // (MobileGL/Init.cpp:68) walks on the moment this returns, and the server still holds
// the context until the apply thread has run it. // the context until the apply thread has run it.
ForwardToApplyThread("ReleaseEGLResources", &RunReleaseResources, nullptr); Server::ServerReleaseEGLResources();
MG_Backend::BackendObject::ReleaseEGLResources(); MG_Backend::BackendObject::ReleaseEGLResources();
} }
@@ -16,6 +16,7 @@
#include "../Server/ServerLoop.h" #include "../Server/ServerLoop.h"
#include "../Server/ServerSession.h" #include "../Server/ServerSession.h"
#include "../Transport/InProcessTransport.h" #include "../Transport/InProcessTransport.h"
#include "WireTables.h"
#include <MGGitHash.h> #include <MGGitHash.h>
#include <MG_Pipe/MGPipeCallbacks.h> #include <MG_Pipe/MGPipeCallbacks.h>
@@ -509,6 +510,16 @@ namespace MobileGL::MG_Remote::Client {
"default and says so"); "default and says so");
} }
// BOTH, AND m_started FIRST. `Active()` is what the integration lane's skip reads and
// `m_started` is what `EmitAndWait` reads, and c1's round-1 rewrite of this function
// set only the second of the two - so a fully-handshaken session with an apply thread
// running and a caps mirror adopted took Fatal{NoClientSession, "Clear"} on its very
// first verb, which is the most confusing possible spelling of "the session is up".
// The order matters for the same reason it does at the other end: `Active()` hands a
// caller a session it may immediately emit on, so the flag that permits emitting has
// to be true before the pointer that grants access to it is published. `Stop()` takes
// them down in the mirror order (m_started = false, then g_active = nullptr).
m_started = true;
g_active = this; g_active = this;
LogMemory("handshake"); LogMemory("handshake");
@@ -521,10 +532,38 @@ namespace MobileGL::MG_Remote::Client {
Stop(); Stop();
return running; return running;
} }
// ---- 9. AND ONLY NOW THE THIRTY-SEVEN WIRE EMITTERS (R-17). This is the line that
// arms `integration-split`: the 21 `DirectGLES.Split.*` entries skip on
// `ClientSession::Active() == nullptr`, and every one of the four arming facts is true
// at exactly this point and at no earlier one.
//
// IT IS LAST, AND EACH OF THE FOUR REASONS IS A DIFFERENT FAILURE:
// - after Hello/Welcome (step 2-4), or an emitter would publish into a ring the peer
// has not mapped;
// - after the first CapsSnapshot (step 7), because R-8's liveness gates read the caps
// mirror and a PLACEHOLDER mirror consumes nothing - a record emitted before it
// would go to a server this client has not been told consumes that family;
// - after ServerLoop::Start (step 8), because EmitAndWait BLOCKS on appliedSeq and
// with no apply thread nothing advances it: the first resource_create would spend
// 30 seconds in the barrier and then Fatal{BarrierTimeout};
// - on THIS thread, the one that called MG_Backend::Init(), because it is the GL
// thread and table 3 makes gPipeInputs its to touch while the barrier holds.
// The publication is safe without a fence because the apply thread never reads these
// tables - the server decodes straight into MGPipeApply* - and this thread wrote them
// before it can reach any GL entry point.
InstallClientWireTables();
return MOBILEGL_OK; return MOBILEGL_OK;
} }
void ClientSession::Stop() { void ClientSession::Stop() {
// FIRST, BEFORE ANYTHING ELSE GOES AWAY (R-17). Every later step here frees something
// an emitter dereferences - the rings, the segments, the transports - so a GL call
// that arrives during teardown must already be looking at the monolith arm. Putting
// the monolith adapters back rather than nulling the rows is deliberate: a null row is
// the pre-migration state and would be an undiagnosed crash, and the adapter is a
// correct answer for a process that no longer has a session.
UninstallClientWireTables();
if (!m_started) { if (!m_started) {
// Start's own failure paths land here with a half-built session. FIVE of them are // Start's own failure paths land here with a half-built session. FIVE of them are
// reached AFTER ServerSession::Accept has already returned OK, so tearing down // reached AFTER ServerSession::Accept has already returned OK, so tearing down
+137 -36
View File
@@ -86,6 +86,23 @@ namespace MobileGL::MG_Remote::Client {
return armed; return armed;
} }
// ID-49's two halves, in one place so the emitter and its control read the same
// arithmetic.
//
// GL 4.6 8.4.4, pack side: the destination row stride is ROW_LENGTH (or the width)
// pixels rounded UP to PACK_ALIGNMENT, the first written byte is offset by SKIP_ROWS
// whole strides plus SKIP_PIXELS pixels, and only `width * bytesPerPixel` bytes of each
// stride are written - the gaps belong to the application and are never touched. That
// last clause is what the control checks with a sentinel.
Bool ReadbackPackStateIsTight(GLsizei width, Uint64 bytesPerPixel,
const PixelStoreParameters& pack) {
if (pack.SkipRows != 0 || pack.SkipPixels != 0 || pack.SkipImages != 0) return false;
if (pack.RowLength != 0 && pack.RowLength != width) return false;
const Uint64 alignment = pack.Alignment > 0 ? static_cast<Uint64>(pack.Alignment) : 1ull;
const Uint64 rowBytes = static_cast<Uint64>(width) * bytesPerPixel;
return (rowBytes % alignment) == 0;
}
// ---- the session, demanded rather than assumed -------------------------------- // ---- the session, demanded rather than assumed --------------------------------
// //
// Every class-B slot needs one. A null session here is NOT the monolith answer - the // Every class-B slot needs one. A null session here is NOT the monolith answer - the
@@ -203,15 +220,28 @@ namespace MobileGL::MG_Remote::Client {
} }
// How many bytes glReadPixels will pack for this rectangle, from the PACK half of the // How many bytes glReadPixels will pack for this rectangle, from the PACK half of the
// pixel-store state. The client has to declare it - MGPReadbackInfo::DstSize is what // pixel-store state.
// sizes the reply and what the client checks against MaxReplyBytes() BEFORE it emits,
// because a reply bigger than a slot is Fatal rather than chunked (s1's ReplySlot.h).
// //
// GL 4.6 8.4.4's arithmetic, and nothing cleverer: a row is rounded up to Alignment, // ID-49: THE PACK STATE NEVER CROSSES FOR A READ, AND DstSize IS THE TIGHT EXTENT.
// the LAST row is not padded, and SkipRows/SkipPixels/SkipImages shift the destination // The first version of this computed GL 4.6 8.4.4's PACKED size - row length, skips,
// rather than growing it (the application owns those bytes and we never write them). // alignment - and handed it over as DstSize. v1's server allocates exactly DstSize and
Uint64 PackedReadbackBytes(GLsizei width, GLsizei height, GLenum format, GLenum type) { // the real backend honours the live pack state, so a 4x3 RGBA8 read with
if (width <= 0 || height <= 0) return 0; // PACK_ROW_LENGTH=8, SKIP_ROWS=1, SKIP_PIXELS=2 allocated 80 bytes and the driver wrote
// to byte 120. That is the shape of the joint inproc lane's two
// DepthReadbackHonoursThePackPixelStoreParameters SEGFAULTs, on both backends.
//
// So the wire carries a RECTANGLE and not a layout: the server reads with NEUTRAL pack
// state into a tight w*h*bytesPerPixel run that IS the reply payload, and the CLIENT -
// which is the side that holds the application's pack state, and the only side that
// can - scatters those rows into the application's pointer. "OnReadPixels writes
// exactly DstSize bytes" still holds; DstSize is now a number both sides derive from
// the same three values instead of one side deriving it from state the other cannot
// see.
//
// IT IS FORMAT-AGNOSTIC ON PURPOSE. The depth and depth-stencil reads the 21 split
// entries touch take the same rule with no special case, because the rule is about the
// LAYOUT and not about the component: bytesPerPixel is whatever the format sizes to.
Uint64 ReadbackBytesPerPixel(GLenum format, GLenum type) {
const TextureInputFormat inputFormat = const TextureInputFormat inputFormat =
MG_Util::ConvertGLEnumToTextureInputFormat(format); MG_Util::ConvertGLEnumToTextureInputFormat(format);
const TexturePixelDataType dataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type); const TexturePixelDataType dataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
@@ -226,18 +256,27 @@ namespace MobileGL::MG_Remote::Client {
static_cast<unsigned>(format), static_cast<unsigned>(type)); static_cast<unsigned>(format), static_cast<unsigned>(type));
std::abort(); std::abort();
} }
return static_cast<Uint64>(bytesPerPixel);
PixelStoreParameters pack{};
if (MG_State::pGLContext != nullptr) {
pack = MG_State::pGLContext->GetPixelStoreParameters(/*isUnpack=*/false);
} }
const Uint64 rowPixels =
pack.RowLength > 0 ? static_cast<Uint64>(pack.RowLength) : static_cast<Uint64>(width); Uint64 TightReadbackBytes(GLsizei width, GLsizei height, GLenum format, GLenum type) {
const Uint64 alignment = pack.Alignment > 0 ? static_cast<Uint64>(pack.Alignment) : 1ull; if (width <= 0 || height <= 0) return 0;
const Uint64 rowBytes = rowPixels * static_cast<Uint64>(bytesPerPixel); return static_cast<Uint64>(width) * static_cast<Uint64>(height) *
const Uint64 paddedRow = ((rowBytes + alignment - 1) / alignment) * alignment; ReadbackBytesPerPixel(format, type);
const Uint64 lastRow = static_cast<Uint64>(width) * static_cast<Uint64>(bytesPerPixel); }
return paddedRow * (static_cast<Uint64>(height) - 1) + lastRow;
// ID-47's refusal, in its own function so the boundary pair can drive it without a
// session. See EmitTables.h.
void RefuseOversizeReadback(GLsizei width, GLsizei height, GLenum format, Uint64 bytes,
Uint64 capacity) {
if (bytes <= capacity) return;
MGLOG_F("MGPipe: Fatal{ReplyTooLarge, \"ReadPixels %dx%d 0x%04x %llu > %llu\"} - P5 "
"does not chunk a readback (R-10) and must not truncate one; grow "
"MOBILEGL_IPC_REPLY_MB or read less",
static_cast<int>(width), static_cast<int>(height),
static_cast<unsigned>(format), static_cast<unsigned long long>(bytes),
static_cast<unsigned long long>(capacity));
std::abort();
} }
void EmitReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, void EmitReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format,
@@ -251,7 +290,9 @@ namespace MobileGL::MG_Remote::Client {
// MarkReadPixelsPackBuffer(): a second call there would be the "wire it twice" // MarkReadPixelsPackBuffer(): a second call there would be the "wire it twice"
// shape, and the per-row counter b1's unit cases assert on would then count one // shape, and the per-row counter b1's unit cases assert on would then count one
// read as two. // read as two.
const Uint64 bytes = PackedReadbackBytes(width, height, format, type); if (width <= 0 || height <= 0) return;
const Uint64 bytesPerPixel = ReadbackBytesPerPixel(format, type);
const Uint64 tight = static_cast<Uint64>(width) * static_cast<Uint64>(height) * bytesPerPixel;
MG_Pipe::MGPReadbackInfo info{}; MG_Pipe::MGPReadbackInfo info{};
info.Res = MG_Pipe::kMGPipeNullHandle; // "the bound read surface answers" info.Res = MG_Pipe::kMGPipeNullHandle; // "the bound read surface answers"
@@ -262,27 +303,41 @@ namespace MobileGL::MG_Remote::Client {
info.Target = 0; info.Target = 0;
info.Level = 0; info.Level = 0;
info.DstOffset = 0; info.DstOffset = 0;
info.DstSize = bytes; info.DstSize = tight;
// CHECKED BEFORE THE EMISSION, not after the answer. A reply bigger than a slot is // CHECKED BEFORE THE EMISSION, not after the answer (ID-47). A reply bigger than a
// Fatal on the server, and a Fatal there is a dead apply thread with a client // slot is Fatal on the SERVER, and a Fatal there is a dead apply thread with a
// parked in the barrier for ever; here it is one line naming the number. // client parked in the barrier for ever, naming a byte count and not a read; here
const Uint64 capacity = session.MaxReplyBytes(); // it is one line naming the read. The capacity is read LIVE from the pool rather
if (bytes > capacity) { // than compared against a constant, so s1's growth of SEG_REPLY to 16 MiB / eight
MGLOG_F("MGPipe: Fatal{ReplyTooLarge, \"read_pixels\"} %llu bytes into a %llu-byte " // 2 MiB slots needs no edit in this file.
"reply slot - P5 does not chunk a readback (R-10); raise the reply pool's " RefuseOversizeReadback(width, height, format, tight, session.MaxReplyBytes());
"slot size or shrink the read",
static_cast<unsigned long long>(bytes), PixelStoreParameters pack{};
static_cast<unsigned long long>(capacity)); if (MG_State::pGLContext != nullptr) {
std::abort(); pack = MG_State::pGLContext->GetPixelStoreParameters(/*isUnpack=*/false);
} }
Int32 status = 0; Int32 status = 0;
// The pixels land straight in the application's buffer: the barrier's wait IS the if (ReadbackPackStateIsTight(width, bytesPerPixel, pack)) {
// reply's wait (R-3), so this costs no round trip beyond the one the barrier was // THE COMMON CASE, AND IT KEEPS THE ZERO-COPY. A neutral pack state means the
// already paying. // destination layout IS the tight layout, so the reply lands straight in the
// application's buffer and there is no bounce at all. It is a fast path for the
// SAME bytes, not a second rule: ScatterTightReadback below is a memcpy of the
// whole run in exactly this case, and the control drives that function.
session.EmitAndWait(MG_Pipe::MGPWireOp::ReadPixels, &info, sizeof(info), nullptr, 0, session.EmitAndWait(MG_Pipe::MGPWireOp::ReadPixels, &info, sizeof(info), nullptr, 0,
pixels, bytes, &status); pixels, tight, &status);
return;
}
// The bounce is the price of the application having asked for a layout. It is the
// tight size and never more, and it is freed before this returns - R-11's rule one
// level out: nothing here outlives the call.
Vector<Uint8> bounce(static_cast<SizeT>(tight));
session.EmitAndWait(MG_Pipe::MGPWireOp::ReadPixels, &info, sizeof(info), nullptr, 0,
bounce.data(), tight, &status);
ScatterTightReadbackIntoPackState(bounce.data(), pixels, width, height, bytesPerPixel,
pack);
} }
void EmitPresent() { void EmitPresent() {
@@ -533,4 +588,50 @@ namespace MobileGL::MG_Remote::Client {
void SetDropClearEmissionForNegativeControl(Bool drop) { g_dropClearEmission = drop; } void SetDropClearEmissionForNegativeControl(Bool drop) { g_dropClearEmission = drop; }
Uint64 DroppedClearEmissions() { return g_droppedClearEmissions; } Uint64 DroppedClearEmissions() { return g_droppedClearEmissions; }
// ID-47's refusal, exported so the boundary pair drives THE EMITTER'S OWN decision rather
// than a copy of it. One line, because the arithmetic that produced `bytes` is
// TightReadbackBytes' and the capacity is the pool's - this function only decides.
void RefuseReadbackLargerThanTheReplySlot(GLsizei width, GLsizei height, GLenum format,
Uint64 bytes, Uint64 capacity) {
RefuseOversizeReadback(width, height, format, bytes, capacity);
}
Bool ReadbackPackStateIsTightForTest(GLsizei width, Uint64 bytesPerPixel,
const PixelStoreParameters& pack) {
return ReadbackPackStateIsTight(width, bytesPerPixel, pack);
}
Uint64 TightReadbackByteCount(GLsizei width, GLsizei height, GLenum format, GLenum type) {
return TightReadbackBytes(width, height, format, type);
}
// ID-49's scatter. Exported for the same reason as the refusal above: the control drives
// THIS, which is what the emitter calls, rather than a second copy of 8.4.4's arithmetic.
void ScatterTightReadbackIntoPackState(const void* tight, void* destination, GLsizei width,
GLsizei height, Uint64 bytesPerPixel,
const PixelStoreParameters& pack) {
if (tight == nullptr || destination == nullptr || width <= 0 || height <= 0) return;
const Uint64 rowPixels =
pack.RowLength > 0 ? static_cast<Uint64>(pack.RowLength) : static_cast<Uint64>(width);
const Uint64 alignment = pack.Alignment > 0 ? static_cast<Uint64>(pack.Alignment) : 1ull;
const Uint64 strideBytes =
((rowPixels * bytesPerPixel + alignment - 1) / alignment) * alignment;
const Uint64 writtenPerRow = static_cast<Uint64>(width) * bytesPerPixel;
// SKIP_IMAGES is in the parameter set and is meaningless for a 2D read, so it is
// applied as GL defines it (whole images of ROW_LENGTH x IMAGE_HEIGHT) rather than
// ignored - ignoring a non-zero one would silently write over the application's first
// image.
const Uint64 imageRows =
pack.ImageHeight > 0 ? static_cast<Uint64>(pack.ImageHeight) : static_cast<Uint64>(height);
auto* out = static_cast<Uint8*>(destination) +
static_cast<Uint64>(pack.SkipImages) * imageRows * strideBytes +
static_cast<Uint64>(pack.SkipRows) * strideBytes +
static_cast<Uint64>(pack.SkipPixels) * bytesPerPixel;
const auto* in = static_cast<const Uint8*>(tight);
for (Uint64 row = 0; row < static_cast<Uint64>(height); ++row) {
std::memcpy(out + row * strideBytes, in + row * writtenPerRow,
static_cast<SizeT>(writtenPerRow));
}
}
} // namespace MobileGL::MG_Remote::Client } // namespace MobileGL::MG_Remote::Client
+43 -5
View File
@@ -47,16 +47,18 @@
// disappears. A null check on a slot may not survive into the client: it becomes a caps-mirror // disappears. A null check on a slot may not survive into the client: it becomes a caps-mirror
// read, which is what ARCHITECTURE.md:114 means by "CallMask replaces 'is this table slot null'". // read, which is what ARCHITECTURE.md:114 means by "CallMask replaces 'is this table slot null'".
// //
// NOTE the asymmetry this table does not resolve: the resource, CSO, framebuffer, texture, // NOTE the asymmetry this table does not resolve, AND WHERE IT IS RESOLVED (R-17): the
// sampler and program families do NOT come through here. They are emitted from // resource, CSO, framebuffer, texture, sampler and program families do NOT come through here.
// MG_Impl/Pipe/* by direct MGPipeApply* calls (37 entry points, 41 call sites), and under // They were emitted from MG_Impl/Pipe/* by 40 direct calls to the 37 MGPipeApply* entry
// split each of those becomes an encode. This table covers only the verbs - the draws, // points; those call sites now go through the two generated tables
// clears, blits, readbacks, queries, fences and present. // (MG_Pipe/PipeRoute.h -> MG_Remote/Client/WireTables.cpp). This table covers only the verbs -
// the draws, clears, blits, readbacks, queries, fences and present.
#pragma once #pragma once
#include <Includes.h> #include <Includes.h>
#include <MG_Backend/BackendObject.h> #include <MG_Backend/BackendObject.h>
#include <MG_Pipe/MGPipeValueTypes.h>
namespace MobileGL::MG_Remote::Client { namespace MobileGL::MG_Remote::Client {
@@ -114,4 +116,40 @@ namespace MobileGL::MG_Remote::Client {
void SetDropClearEmissionForNegativeControl(Bool drop); void SetDropClearEmissionForNegativeControl(Bool drop);
Uint64 DroppedClearEmissions(); Uint64 DroppedClearEmissions();
// ID-47. The CLIENT refuses a readback whose answer would not fit a reply slot, BEFORE it
// emits the record, and names the read. Never truncated (a short write is a silently
// truncated picture, the one failure an SSIM comparison cannot see) and never left to the
// server's `Post` abort (which happens on the apply thread, after the client is already
// parked in the barrier, and names a byte count rather than a read).
//
// IT IS A FREE FUNCTION SO THE BOUNDARY PAIR CAN DRIVE IT. `EmitReadPixels` needs a live
// session before it reaches any of this, so a control over the emitter could only ever
// observe Fatal{NoClientSession}; a control over THIS observes the decision and its exact
// message, and it is the same function the emitter calls rather than a second copy of the
// arithmetic. Returns when the read fits; aborts when it does not.
void RefuseReadbackLargerThanTheReplySlot(GLsizei width, GLsizei height, GLenum format,
Uint64 bytes, Uint64 capacity);
// ID-49. `MGPReadbackInfo::DstSize` is the TIGHT w*h*bytesPerPixel extent - the reply
// payload - and nothing about the application's pack state crosses the wire. The server
// reads with a NEUTRAL pack state into that run; this is the number both sides derive.
Uint64 TightReadbackByteCount(GLsizei width, GLsizei height, GLenum format, GLenum type);
// ID-49. Scatters the tight rows into the application's pointer per the application's own
// pack state (ROW_LENGTH, SKIP_*, ALIGNMENT), which only the client holds. Exposed for the
// same reason as the refusal above: the control drives the function the emitter calls
// rather than a second copy of GL 4.6 8.4.4's arithmetic. THE GAPS ARE NEVER WRITTEN -
// they belong to the application - and that is what the control checks with a sentinel.
void ScatterTightReadbackIntoPackState(const void* tight, void* destination, GLsizei width,
GLsizei height, Uint64 bytesPerPixel,
const PixelStoreParameters& pack);
// ID-49. True when the destination layout IS the tight layout, which is the only condition
// under which EmitReadPixels may read the reply straight into the application pointer and
// skip the bounce. Exported because the FAST PATH and the SCATTER have to agree, and the
// only honest way to state that is to drive both and compare - a case that tested either
// alone would pass a predicate that said yes to a layout the scatter would have rearranged.
Bool ReadbackPackStateIsTightForTest(GLsizei width, Uint64 bytesPerPixel,
const PixelStoreParameters& pack);
} // namespace MobileGL::MG_Remote::Client } // namespace MobileGL::MG_Remote::Client
+495
View File
@@ -0,0 +1,495 @@
// MobileGL - MobileGL/MG_Remote/Client/WireTables.cpp
// Copyright (c) 2025-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
// The ENCODE TWIN of gMGPipeWireRecordApply: thirty-seven emitters that turn a table call into
// a wire record. Owner: package c1 (P5 ruling R-17). See WireTables.h for the install order
// and MG_Pipe/PipeRoute.h for what R-17 actually cost.
//
// EVERY EMITTER IS THE SAME FOUR STEPS, and the macros below exist so that a reader can check
// thirty-seven rows against PipeTables.inc in one pass instead of reading thirty-seven bodies:
//
// 1. require a session - a slot that fell through to a driver this role does not have is
// the failure R-4 exists to prevent, and there is no fall-through here either;
// 2. stage the blob, if the row has one, and name the SEG_STAGE run in the payload's own
// MGPBlobRef - which is why the payload is COPIED: the table hands it over const, and
// the blobref is the one field the client must write after the caller is done with it;
// 3. EmitAndWait, which is the barrier's wait and the reply's wait at once (R-3/R-5);
// 4. post the answer, for the rows that have one, into MG_Pipe's reply mailbox.
//
// WHAT IS DELIBERATELY NOT HERE. b1's `PushPersistentMapsBeforeVerb` / `MarkGpuWritesFor*` are
// NOT called from these thirty-seven. They are pre-VERB hooks and these are not verbs: they
// are the resource, CSO and state records that a verb is later drawn against. The five class-B
// verbs in EmitTables.cpp call them, once each, immediately before their record, which is the
// ordering b1's B-1 fix depends on. Calling them here as well would push a persistent map
// before every `set_dynamic_state` - hundreds of times a frame, and each one a real record.
#include "WireTables.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include "ClientSession.h"
#include "../Server/ServerLoop.h"
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/PipeRoute.h>
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
#include <cstring>
namespace MobileGL::MG_Remote::Client {
using MG_Pipe::MGPWireOp;
namespace {
// TABLE 3's ROLE SPLIT, AS A RUNTIME CHECK. gMGPipeScreen / gMGPipeContext are PROCESS
// globals and under `inproc` the server role is a thread in this same process, so the
// apply thread running the server's own backend - the EGL bring-up, InitCapabilities,
// the applier - reaches these very emitters. A record published there would be waited
// for by the thread that is supposed to apply it: `Fatal{BarrierTimeout,
// "ResourceRespecify"}` from `mgl-srv-apply`, thirty seconds into bring-up, which is
// exactly how this was found.
//
// THE ANSWER IS NOT "SUPPRESS THE RECORD" - it is "run the server's own code", because
// on that thread this process IS the server and the applier is one call away. It is
// the same thing PipeWireCodec does on the decode side, where every arm calls
// MGPipeApply* directly and never goes through a table.
//
// Under `spawn` (P6) the predicate is constantly false in the client process and
// constantly true in the server's, so this costs one atomic load and changes nothing.
Bool RunsAsTheServerRole() { return Server::ServerLoop::OnApplyThread(); }
Uint64 g_emitted = 0;
Uint64 g_declined = 0;
ClientSession& RequireSession(const char* row) {
ClientSession* session = ClientSession::Active();
if (session == nullptr) {
MGLOG_F("MGPipe: Fatal{NoClientSession, \"%s\"} - the client wire tables are "
"installed but no ClientSession is active. A row may not fall through to "
"a driver this role does not have",
row);
std::abort();
}
return *session;
}
// Stages a mandatory blob. `StageBytes` Fatals on a zero size by design (R-2.2: "the
// record declared no blob" and "the record declared an empty blob" must not be spelled
// the same way on a wire), so a row whose decoder calls RequireDeclaredBlob or
// ResolveOrFatal is checked HERE, on the producing side, where the row has a name.
MG_Pipe::MGPBlobRef StageRequired(ClientSession& session, const char* row,
const void* bytes, Uint64 count) {
if (bytes == nullptr || count == 0) {
MGLOG_F("MGPipe: Fatal{BlobMissing, \"%s\"} - the row's decoder requires a "
"declared blob and the call site handed over %llu bytes at %p. Under "
"monolith the companion pointer carries them; under split they have to "
"be staged, and there is nothing to stage",
row, static_cast<unsigned long long>(count), bytes);
std::abort();
}
return session.Encoder().StageBytes(bytes, count);
}
// Stages an OPTIONAL blob: the two sub-data rows, whose decoders resolve only when the
// record's own size field says there are bytes. All three fields zero is the wire's
// "no blob declared", and CheckBlobIsHonest refuses any other spelling of it.
MG_Pipe::MGPBlobRef StageOptional(ClientSession& session, const void* bytes, Uint64 count) {
if (bytes == nullptr || count == 0) return MG_Pipe::MGPBlobRef{};
return session.Encoder().StageBytes(bytes, count);
}
// ---------------------------------------------------------------------------------
// The three regular shapes
// ---------------------------------------------------------------------------------
#define MGP_WIRE_PLAIN(Name, Payload, Table) \
void Wire_##Name(const MG_Pipe::Payload* payload) { \
if (RunsAsTheServerRole()) { MG_Pipe::MGPipeMonolith##Table().Name(payload); return; } \
ClientSession& session = RequireSession(#Name); \
session.EmitAndWait(MGPWireOp::Name, payload, sizeof(*payload), nullptr, 0, nullptr, 0, \
nullptr); \
++g_emitted; \
}
#define MGP_WIRE_BLOB(Name, Payload, BlobMember) \
void Wire_##Name(const MG_Pipe::Payload* payload, const void* blobBytes, \
Uint64 blobByteCount) { \
if (RunsAsTheServerRole()) { \
MG_Pipe::MGPipeMonolithContext().Name(payload, blobBytes, blobByteCount); \
return; \
} \
ClientSession& session = RequireSession(#Name); \
MG_Pipe::Payload record = *payload; \
record.BlobMember = StageRequired(session, #Name, blobBytes, blobByteCount); \
session.EmitAndWait(MGPWireOp::Name, &record, sizeof(record), nullptr, 0, nullptr, 0, \
nullptr); \
++g_emitted; \
}
#define MGP_WIRE_TAIL(Name, Payload, TailType) \
void Wire_##Name(const MG_Pipe::Payload* payload, const void* varTail, Uint32 varTailCount) { \
if (RunsAsTheServerRole()) { \
MG_Pipe::MGPipeMonolithContext().Name(payload, varTail, varTailCount); \
return; \
} \
ClientSession& session = RequireSession(#Name); \
session.EmitAndWait(MGPWireOp::Name, payload, sizeof(*payload), varTail, \
static_cast<Uint64>(varTailCount) * sizeof(MG_Pipe::TailType), \
nullptr, 0, nullptr); \
++g_emitted; \
}
// -- screen -------------------------------------------------------------------
MGP_WIRE_PLAIN(ResourceDestroy, MGPHandleOnly, Screen)
MGP_WIRE_PLAIN(UnmapPersistent, MGPHandleOnly, Screen)
// -- context, plain -----------------------------------------------------------
MGP_WIRE_PLAIN(BindRenderState, MGPBindRenderState, Context)
MGP_WIRE_PLAIN(DeleteRenderState, MGPHandleOnly, Context)
MGP_WIRE_PLAIN(BindVertexElements, MGPHandleOnly, Context)
MGP_WIRE_PLAIN(DeleteVertexElements, MGPHandleOnly, Context)
MGP_WIRE_PLAIN(DeleteSamplerState, MGPHandleOnly, Context)
MGP_WIRE_PLAIN(CreateSamplerView, MGPSamplerView, Context)
MGP_WIRE_PLAIN(DeleteSamplerView, MGPHandleOnly, Context)
MGP_WIRE_PLAIN(BindShaderState, MGPHandleOnly, Context)
MGP_WIRE_PLAIN(DeleteShaderState, MGPHandleOnly, Context)
MGP_WIRE_PLAIN(SetDrawProgram, MGPHandleOnly, Context)
MGP_WIRE_PLAIN(SetDispatchProgram, MGPHandleOnly, Context)
MGP_WIRE_PLAIN(SetFramebufferState, MGPFramebufferState, Context)
MGP_WIRE_PLAIN(SetIndexBuffer, MGPIndexBuffer, Context)
MGP_WIRE_PLAIN(SetPixelPackState, MGPPixelPackState, Context)
MGP_WIRE_PLAIN(SetPatchState, MGPPatchState, Context)
// -- context, mandatory blob --------------------------------------------------
MGP_WIRE_BLOB(CreateRenderState, MGPRenderStateDesc, Blob)
MGP_WIRE_BLOB(CreateVertexElements, MGPVertexElements, Blob)
MGP_WIRE_BLOB(CreateSamplerState, MGPSamplerDesc, Parameters)
MGP_WIRE_BLOB(SetDynamicState, MGPDynamicState, Blob)
MGP_WIRE_BLOB(SetGlobalConstants, MGPGlobalConstants, Blob)
// -- context, variable tail ---------------------------------------------------
MGP_WIRE_TAIL(SetVertexBuffers, MGPVertexBuffers, MGPVertexBuffer)
MGP_WIRE_TAIL(SetSamplerViews, MGPSamplerViews, MGPBoundView)
MGP_WIRE_TAIL(BindSamplerStates, MGPSamplerStates, MGPipeHandle)
MGP_WIRE_TAIL(SetShaderImages, MGPShaderImages, MGPImageView)
MGP_WIRE_TAIL(SetVertexAttribDefaults, MGPVertexAttribDefaults, MGPAttribValue)
#undef MGP_WIRE_PLAIN
#undef MGP_WIRE_BLOB
#undef MGP_WIRE_TAIL
// -- the rows that fit none of the three shapes --------------------------------
// set_residual_value_state. CONTRACT-P5 table 1 row 6: the applier takes a frontend
// `ResidualValueBlock&` and `MGPResidualValueState` is never instantiated on the live
// path, so the encoder invents BOTH the record fill and the blob fill. The block IS
// the blob, whole - the decoder requires exactly sizeof(ResidualValueBlock) and says
// why ("a size that only ever ratchets down makes a short read silently lose
// CapabilityBits"), so the two sides state the same number from the same header.
void Wire_SetResidualValueState(const MG_Pipe::MGPResidualValueState* payload,
const void* blobBytes, Uint64 blobByteCount) {
if (RunsAsTheServerRole()) {
MG_Pipe::MGPipeMonolithContext().SetResidualValueState(payload, blobBytes,
blobByteCount);
return;
}
ClientSession& session = RequireSession("SetResidualValueState");
MG_Pipe::MGPResidualValueState record = *payload;
record.Blob = StageRequired(session, "SetResidualValueState", blobBytes, blobByteCount);
session.EmitAndWait(MGPWireOp::SetResidualValueState, &record, sizeof(record), nullptr,
0, nullptr, 0, nullptr);
++g_emitted;
}
// resource_readback. kReplySlot, and the answer is COMPLETION only: the bytes travel
// server -> client in SEG_EVENT through OnBufferWriteback, because the destination is
// the client's shadow and its size is the resource's, not a slot's (CONTRACT-P5 table 1
// row 22). So the reply buffer is deliberately {nullptr, 0} and the wait is what makes
// the writeback already drained by the time this returns.
void Wire_ResourceReadback(const MG_Pipe::MGPReadback* payload, MG_Pipe::MGPReplySlot* reply) {
if (RunsAsTheServerRole()) { MG_Pipe::MGPipeMonolithContext().ResourceReadback(payload, reply); return; }
ClientSession& session = RequireSession("ResourceReadback");
Int32 status = 0;
const Uint64 seq = session.EmitAndWait(MGPWireOp::ResourceReadback, payload,
sizeof(*payload), nullptr, 0, nullptr, 0,
&status);
reply->Id = seq;
MG_Pipe::MGPipePostReply(*reply, status, 0);
++g_emitted;
}
// ---- the three acceptance rows that fit a generated signature ----------------
//
// THE ANSWER IS THE SERVER'S AND NOTHING ELSE. `EmitAndWait` returns the record's seq
// and fills `status` from the reply slot the server stamped; DECLINED is `false` and OK
// is `true`, and neither is derived from anything this side knows. R-5 exists because
// "always accept" is ID-39's 66 lost DirectVulkan uploads and "accept if we emitted" is
// the same bug wearing a counter.
void Wire_ResourceCreate(const MG_Pipe::MGPResourceDesc* payload, MG_Pipe::MGPReplySlot* reply) {
if (RunsAsTheServerRole()) { MG_Pipe::MGPipeMonolithScreen().ResourceCreate(payload, reply); return; }
ClientSession& session = RequireSession("ResourceCreate");
Int32 status = 0;
const Uint64 seq = session.EmitAndWait(MGPWireOp::ResourceCreate, payload,
sizeof(*payload), nullptr, 0, nullptr, 0,
&status);
reply->Id = seq;
MG_Pipe::MGPipePostReply(*reply, status, status == 0 ? 1u : 0u);
++g_emitted;
if (status == 1) ++g_declined;
}
void Wire_SetTextureParams(const MG_Pipe::MGPTextureParams* payload, MG_Pipe::MGPReplySlot* reply) {
if (RunsAsTheServerRole()) { MG_Pipe::MGPipeMonolithContext().SetTextureParams(payload, reply); return; }
ClientSession& session = RequireSession("SetTextureParams");
Int32 status = 0;
const Uint64 seq = session.EmitAndWait(MGPWireOp::SetTextureParams, payload,
sizeof(*payload), nullptr, 0, nullptr, 0,
&status);
reply->Id = seq;
MG_Pipe::MGPipePostReply(*reply, status, status == 0 ? 1u : 0u);
++g_emitted;
if (status == 1) ++g_declined;
}
void Wire_ResourceSubData(const MG_Pipe::MGPSubData* payload, const void* blobBytes,
Uint64 blobByteCount, const void* varTail, Uint32 varTailCount,
MG_Pipe::MGPReplySlot* reply) {
if (RunsAsTheServerRole()) {
MG_Pipe::MGPipeMonolithContext().ResourceSubData(payload, blobBytes, blobByteCount,
varTail, varTailCount, reply);
return;
}
ClientSession& session = RequireSession("ResourceSubData");
MG_Pipe::MGPSubData record = *payload;
record.Blob = StageOptional(session, blobBytes, blobByteCount);
Int32 status = 0;
const Uint64 seq = session.EmitAndWait(
MGPWireOp::ResourceSubData, &record, sizeof(record), varTail,
static_cast<Uint64>(varTailCount) * sizeof(MG_Pipe::MGPSubRegion), nullptr, 0,
&status);
reply->Id = seq;
MG_Pipe::MGPipePostReply(*reply, status, status == 0 ? 1u : 0u);
++g_emitted;
if (status == 1) ++g_declined;
}
void Wire_BufferSubDataResident(const MG_Pipe::MGPSubData* payload, const void* blobBytes,
Uint64 blobByteCount) {
if (RunsAsTheServerRole()) {
MG_Pipe::MGPipeMonolithContext().BufferSubDataResident(payload, blobBytes,
blobByteCount);
return;
}
ClientSession& session = RequireSession("BufferSubDataResident");
MG_Pipe::MGPSubData record = *payload;
record.Blob = StageOptional(session, blobBytes, blobByteCount);
session.EmitAndWait(MGPWireOp::BufferSubDataResident, &record, sizeof(record), nullptr,
0, nullptr, 0, nullptr);
++g_emitted;
}
// ---- the four escapes --------------------------------------------------------
// resource_respecify. R-13.3: `initialBytes` is ALWAYS nullptr under split and the
// initial content arrives as resource_subdata records immediately after this one. The
// caller's bytes are therefore not dropped - they are re-expressed - and PipeFill.cpp's
// emitter is where that happens, because the chunking walk that has to size them
// (MGPipeForEachSubDataRecordRange) lives there. What this emitter owes is the REFUSAL:
// a non-null pointer arriving here means the call site was not converted, and silently
// ignoring it would lose exactly the bytes R-13.3 promised would follow.
Bool Wire_Escape_ResourceRespecify(const MG_Pipe::MGPResourceDesc* desc,
const void* initialBytes,
const MG_Pipe::MGPRespecifiedLevel* level) {
if (RunsAsTheServerRole()) {
return MG_Pipe::MGPipeMonolithEscapes().ResourceRespecify(desc, initialBytes, level);
}
ClientSession& session = RequireSession("ResourceRespecify");
if (initialBytes != nullptr) {
MGLOG_F("MGPipe: Fatal{UncarriedInitialBytes, \"resource_respecify\"} - a call "
"site handed initial content to a split respecify. R-13.3 rules that "
"initialBytes never crosses and that the content follows as "
"resource_subdata; a caller that still passes it has bytes nothing will "
"carry");
std::abort();
}
// The scope rides in the descriptor's own pads (CONTRACT-P5 table 1 row 19b, LANDED)
// and is written only through MGPipeSetRespecifiedLevel - three fields are one
// value, and an open-coded writer that forgets the presence byte says "level 0 of
// upload target 0" where it meant "the whole resource".
MG_Pipe::MGPResourceDesc record = *desc;
if (level != nullptr) {
MG_Pipe::MGPipeSetRespecifiedLevel(record, level->UploadTarget, level->Level);
} else {
MG_Pipe::MGPipeClearRespecifiedLevel(record);
}
Int32 status = 0;
const Uint64 seq =
session.EmitAndWait(MGPWireOp::ResourceRespecify, &record, sizeof(record), nullptr,
0, nullptr, 0, &status);
(void)seq;
++g_emitted;
if (status == 1) ++g_declined;
return status == 0;
}
// resource_flush_range. R-13.2: it carries NO bytes under split - it is a
// {range, AccessFlags} control record and the bytes of exactly that range arrive ahead
// of it as resource_subdata. Same refusal as above, for the same reason: a blobref here
// would be "a second, forgeable way to say the same thing".
void Wire_Escape_ResourceFlushRange(const MG_Pipe::MGPFlushRange* record,
const void* bytes) {
if (RunsAsTheServerRole()) {
MG_Pipe::MGPipeMonolithEscapes().ResourceFlushRange(record, bytes);
return;
}
ClientSession& session = RequireSession("ResourceFlushRange");
(void)bytes; // ruled uncarried; the emitter in PipeFill.cpp sends the range first
session.EmitAndWait(MGPWireOp::ResourceFlushRange, record, sizeof(*record), nullptr, 0,
nullptr, 0, nullptr);
++g_emitted;
}
// map_persistent. R-6/R-2.4: the split answer is a CONSTANT DECLINE, and it still costs
// a record, because the server has to know the client asked - the applier's
// MapPersistentRoundtrips counter is defined as "one per storage definition in both
// modes" and a client that answered locally would zero it. `size` and `seedBytes` have
// no carrier (MGPHandleOnly is {Handle, Kind}) and need none: nothing is minted.
void* Wire_Escape_MapPersistent(const MG_Pipe::MGPHandleOnly* handle, Uint64 size,
const void* seedBytes) {
if (RunsAsTheServerRole()) {
return MG_Pipe::MGPipeMonolithEscapes().MapPersistent(handle, size, seedBytes);
}
ClientSession& session = RequireSession("MapPersistent");
(void)size;
(void)seedBytes;
Int32 status = 0;
session.EmitAndWait(MGPWireOp::MapPersistent, handle, sizeof(*handle), nullptr, 0,
nullptr, 0, &status);
++g_emitted;
if (status == 1) ++g_declined;
// NOT "always nullptr": the answer is READ. R-6 says the server declines, and the
// day it stops declining this returns what it actually said rather than what the
// ruling predicted.
if (status == 0) {
MGLOG_F("MGPipe: Fatal{UnexpectedMapAccept, \"map_persistent\"} - the server "
"accepted a persistent map under split. R-6 makes the split answer a "
"constant decline because there is no way to hand a host pointer across "
"a process boundary in P5; a pointer arriving here is one this client "
"cannot dereference");
std::abort();
}
return nullptr;
}
// create_shader_state. SEVEN blobrefs and TWO typed frontend pointers; one blobBytes
// pair cannot express seven runs. The serializer already exists and no package may
// write a second one (CONTRACT-P5 table 1 row 3): EncodeProgramArtifacts produces one
// archive, the decoder's DecodeProgramArtifacts consumes it, and the six per-stage runs
// stay UNDECLARED because the modules already travel inside the archive - a declared
// Spirv[i] is Fatal on the far side rather than ignored.
void Wire_Escape_CreateShaderState(const MG_Pipe::MGPProgramDesc* desc,
const MG_State::GLState::LinkArtifacts* link,
const MG_State::GLState::SpirvArtifacts* spirv) {
if (RunsAsTheServerRole()) {
MG_Pipe::MGPipeMonolithEscapes().CreateShaderState(desc, link, spirv);
return;
}
ClientSession& session = RequireSession("CreateShaderState");
if (link == nullptr || spirv == nullptr) {
MGLOG_F("MGPipe: Fatal{ArtefactsMissing, \"create_shader_state\"} - the record's "
"two typed companions are null. Under monolith the applier reads the "
"modules out of spirv->generatedSpirv; under split there is nothing to "
"serialise, and emitting the record anyway would create a CSO with no "
"code");
std::abort();
}
// EncodeProgramArtifacts APPENDS and never fails - everything it walks is owned
// plain data - so an empty archive means the two structs themselves were empty,
// which is a linked program with no artefacts and is not a codec question.
Vector<Uint8> archive;
MG_State::GLState::EncodeProgramArtifacts(*link, *spirv, archive);
if (archive.empty()) {
MGLOG_F("MGPipe: Fatal{ArchiveEmpty, \"create_shader_state\"} - the program's "
"artefacts serialised to nothing");
std::abort();
}
MG_Pipe::MGPProgramDesc record = *desc;
for (Uint32 i = 0; i < 6; ++i) record.Spirv[i] = MG_Pipe::MGPBlobRef{};
record.Reflection = session.Encoder().StageBytes(archive.data(), archive.size());
session.EmitAndWait(MGPWireOp::CreateShaderState, &record, sizeof(record), nullptr, 0,
nullptr, 0, nullptr);
++g_emitted;
}
} // namespace
void InstallClientWireTables() {
using namespace MG_Pipe;
gMGPipeScreen.ResourceCreate = &Wire_ResourceCreate;
gMGPipeScreen.ResourceDestroy = &Wire_ResourceDestroy;
gMGPipeScreen.UnmapPersistent = &Wire_UnmapPersistent;
gMGPipeContext.CreateRenderState = &Wire_CreateRenderState;
gMGPipeContext.BindRenderState = &Wire_BindRenderState;
gMGPipeContext.DeleteRenderState = &Wire_DeleteRenderState;
gMGPipeContext.CreateVertexElements = &Wire_CreateVertexElements;
gMGPipeContext.BindVertexElements = &Wire_BindVertexElements;
gMGPipeContext.DeleteVertexElements = &Wire_DeleteVertexElements;
gMGPipeContext.CreateSamplerState = &Wire_CreateSamplerState;
gMGPipeContext.DeleteSamplerState = &Wire_DeleteSamplerState;
gMGPipeContext.CreateSamplerView = &Wire_CreateSamplerView;
gMGPipeContext.DeleteSamplerView = &Wire_DeleteSamplerView;
gMGPipeContext.BindShaderState = &Wire_BindShaderState;
gMGPipeContext.DeleteShaderState = &Wire_DeleteShaderState;
gMGPipeContext.SetDrawProgram = &Wire_SetDrawProgram;
gMGPipeContext.SetDispatchProgram = &Wire_SetDispatchProgram;
gMGPipeContext.SetDynamicState = &Wire_SetDynamicState;
gMGPipeContext.SetFramebufferState = &Wire_SetFramebufferState;
gMGPipeContext.SetVertexBuffers = &Wire_SetVertexBuffers;
gMGPipeContext.SetIndexBuffer = &Wire_SetIndexBuffer;
gMGPipeContext.SetSamplerViews = &Wire_SetSamplerViews;
gMGPipeContext.BindSamplerStates = &Wire_BindSamplerStates;
gMGPipeContext.SetShaderImages = &Wire_SetShaderImages;
gMGPipeContext.SetGlobalConstants = &Wire_SetGlobalConstants;
gMGPipeContext.SetVertexAttribDefaults = &Wire_SetVertexAttribDefaults;
gMGPipeContext.SetPixelPackState = &Wire_SetPixelPackState;
gMGPipeContext.SetPatchState = &Wire_SetPatchState;
gMGPipeContext.SetResidualValueState = &Wire_SetResidualValueState;
gMGPipeContext.SetTextureParams = &Wire_SetTextureParams;
gMGPipeContext.ResourceSubData = &Wire_ResourceSubData;
gMGPipeContext.BufferSubDataResident = &Wire_BufferSubDataResident;
gMGPipeContext.ResourceReadback = &Wire_ResourceReadback;
gMGPipeRouteEscapes.ResourceRespecify = &Wire_Escape_ResourceRespecify;
gMGPipeRouteEscapes.ResourceFlushRange = &Wire_Escape_ResourceFlushRange;
gMGPipeRouteEscapes.MapPersistent = &Wire_Escape_MapPersistent;
gMGPipeRouteEscapes.CreateShaderState = &Wire_Escape_CreateShaderState;
MGPipeNoteInstalledArm(MGPipeRouteArm::kClientWire);
}
void UninstallClientWireTables() {
// PUTS THE MONOLITH ARM BACK rather than nulling the rows. A null row is the
// pre-migration state and would be an immediate crash with no diagnostic at whatever
// GL call raced the teardown; the monolith adapter is at least a correct answer for a
// process that no longer has a session.
MG_Pipe::MGPipeInstallMonolithTables();
}
Uint64 ClientWireRecordsEmitted() { return g_emitted; }
Uint64 ClientWireRecordsDeclined() { return g_declined; }
} // namespace MobileGL::MG_Remote::Client
#endif // MOBILEGL_BUILD_DISAGGREGATED
+62
View File
@@ -0,0 +1,62 @@
// MobileGL - MobileGL/MG_Remote/Client/WireTables.h
// Copyright (c) 2025-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
// THE CLIENT ARM OF R-17's ROUTING: the encode twin of `gMGPipeWireRecordApply`. Owner: c1.
//
// Thirty-seven thin emitters over `ClientSession::EmitAndWait`, installed over the two
// generated tables and the escape table that `MG_Pipe/PipeRoute.h` declares, so that under
// split every resource, CSO, texture and program record leaves the GL thread as a WIRE RECORD
// instead of executing synchronously against a context the apply thread now owns.
//
// WHAT ARMS `integration-split`. The 21 `DirectGLES.Split.*` entries skip on
// `ClientSession::Active() == nullptr`. `Install()` below is called from the END of a
// successful `ClientSession::Start()`, on the GL thread (the thread that called
// `MG_Backend::Init()`), AFTER the Hello/Welcome handshake, after the first CapsSnapshot has
// been adopted, and after the in-process server role's apply thread has been started. That
// order is not a preference:
// - after the handshake, because an emitter that published before Welcome would be writing
// into a ring the peer has not mapped;
// - after the first snapshot, because R-8's liveness gates read the caps mirror and a
// placeholder mirror consumes nothing, so a record emitted before it would be emitted to a
// server this client has not yet been told consumes that family;
// - after the apply thread exists, because `EmitAndWait` BLOCKS on `appliedSeq` and nothing
// would advance it - a barrier wait with no applier is the 30-second Fatal, not a hang;
// - on the GL thread, because that is the only thread that may touch `gPipeInputs` while the
// barrier holds (table 3), and installing from the apply thread would publish the table to
// the GL thread with no synchronisation at all.
// `Uninstall()` runs at the TOP of `Stop()`, before the rings go away, so the last thing any
// straggling GL-thread call reaches is the monolith arm rather than a dangling session.
#pragma once
#include <Includes.h>
#if MOBILEGL_BUILD_DISAGGREGATED
namespace MobileGL::MG_Remote::Client {
// Installs the thirty-seven wire emitters over gMGPipeScreen / gMGPipeContext /
// gMGPipeRouteEscapes and records the arm. Idempotent.
void InstallClientWireTables();
// Puts the monolith adapters back. Idempotent; safe to call when nothing was installed.
void UninstallClientWireTables();
// How many records the thirty-seven emitters have published. This is t1's FOURTH arming
// fact - "the client encoder's record ordinal actually moving during the case" - counted
// at the only place that can count it, and it is deliberately NOT the encoder's EmitSeq:
// EmitSeq moves for the five class-B verbs too, so a lane that armed on it would arm on a
// Clear and call the resource path proven.
Uint64 ClientWireRecordsEmitted();
// How many of those were refused by the server, by acceptance row. Counted rather than
// inferred, R-8's rule one level out.
Uint64 ClientWireRecordsDeclined();
} // namespace MobileGL::MG_Remote::Client
#endif // MOBILEGL_BUILD_DISAGGREGATED
+24 -4
View File
@@ -1231,11 +1231,27 @@ namespace MobileGL::MG_Remote::Wire {
// may not re-derive it, because an if-constexpr discard, a stale handle and a refused // may not re-derive it, because an if-constexpr discard, a stale handle and a refused
// record are all invisible from the call site). // record are all invisible from the call site).
// //
// IT DOES NOT GO IN A REPLY SLOT. None of the four carries kReplySlot, and s1 sizes // IT NOW RIDES THE REPLY SLOT, AND THAT IS THE RULING THIS COMMENT ASKED FOR. The text
// ReplyPool from that table - see PostReply. The answer is recorded here and read // that stood here said the answer could not ride a slot "until the integrator rules on
// through LastAcceptance() / Accepted+DeclinedRecords() until the integrator rules on
// which half of the contract moves (table 0 says these four use DECLINED; the // which half of the contract moves (table 0 says these four use DECLINED; the
// catalogue gives them no slot). // catalogue gives them no slot)". Both halves have since moved the same way: ID-31
// gave `ResourceCreate` kReplySlot and `kMGPipeCallFlags` now carries the flag on all
// four (PipeWire.inc rows 2, 3, 47, 48), and P5 ruling R-17 states that the acceptance
// answers "come back through the reply slot inside the barrier's wait". So the
// conflict is resolved in favour of table 0, the pool reserves these seqs like any
// other, and `PostReply`'s own Fatal - which trips on a row with no kReplySlot - is
// what keeps this honest if a flag is ever taken away again.
//
// WITHOUT THIS THE CLIENT CANNOT RUN AT ALL: `ClientSession::EmitAndWait` asks the
// catalogue, not the caller, whether a row owns a slot, so all four would wait for an
// answer nobody wrote and take `Fatal{ReplyMissing}` inside the barrier. Recording the
// acceptance locally as well is kept, because `LastAcceptance()` and the two counters
// are what the monolith-side decoder cases are asserted on.
//
// OWNERSHIP: MG_Remote/Wire/* is package w1's and w1 is not in wave 2. This hunk is
// four lines inside one lambda, made under R-17 by c1 because it is the server half of
// the routing R-17 assigns, and it is called out in c1-v2.md so the integrator can
// move it if the call belongs elsewhere.
const auto noteAcceptance = [&](Bool accepted) { const auto noteAcceptance = [&](Bool accepted) {
m_lastAcceptanceKnown = true; m_lastAcceptanceKnown = true;
m_lastAcceptance = accepted; m_lastAcceptance = accepted;
@@ -1244,6 +1260,10 @@ namespace MobileGL::MG_Remote::Wire {
} else { } else {
++m_declined; ++m_declined;
} }
// DECLINED is a real answer and carries no payload (ReplySlot.h): the four Bool
// rows say `false` with it, exactly as MapPersistent says nullptr with it.
PostReply(op, seq, accepted ? ReplySink::kStatusOk : ReplySink::kStatusDeclined,
nullptr, 0);
}; };
switch (op) { switch (op) {
+105 -7
View File
@@ -25,6 +25,10 @@
#if MOBILEGL_PIPE_PUSH #if MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/SlotAllocator.h> #include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/PipeApply.h> #include <MG_Pipe/PipeApply.h>
// P5 R-17: the routing that INSTALLS the two tables. Included here so that the installation
// case below states the partition deterministically rather than depending on whether some
// other object in this particular test binary happened to drag the installer in.
#include <MG_Pipe/PipeRoute.h>
#endif #endif
using namespace MobileGL; using namespace MobileGL;
@@ -101,17 +105,111 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
EXPECT_EQ(ClassCount<kCtxVerb>(), 13u); EXPECT_EQ(ClassCount<kCtxVerb>(), 13u);
} }
// An uninstalled pipe is every entry null - which is exactly what "this subsystem has not // A row nobody has migrated is null - which is exactly what "this subsystem has not been
// been migrated, keep pulling" means (plan B section 4.1). // migrated, keep pulling" means (plan B section 4.1).
//
// UNTIL P5 R-17 THAT WAS EVERY ROW, and this case said so. It is now EXACTLY THE 34 ROWS WITH
// NO MGPipeApply* ENTRY POINT: the other 37 have an applier, R-17 installs adapters over them,
// and a null there would no longer mean "keep pulling" - `MG_Impl/Pipe`'s call sites go through
// the thunks, so a null would mean "call through a null pointer". The number is asserted rather
// than the emptiness, because "37 installed" and "34 still null" are the two halves of a
// partition and a case that checked only one of them would pass an installer that had
// overwritten rows it does not own.
// THE NAME IS KEPT, AND SO IS THE STATEMENT IT MAKES - only the ROWS it makes it about have
// narrowed. G2/G14 compare ctest names against a pre-P5 baseline and require ZERO removed, so
// renaming a case is a removal even when the new name is better: it is indistinguishable, from
// the gate's side, from a case that was deleted. So this case stays, asserting the half that is
// still true, and the new half below is an ADDED name.
TEST(PipeCatalogue, UninstalledTablesAreAllNull) { TEST(PipeCatalogue, UninstalledTablesAreAllNull) {
const void* const* screen = reinterpret_cast<const void* const*>(&gMGPipeScreen); const void* const* screen = reinterpret_cast<const void* const*>(&gMGPipeScreen);
for (SizeT i = 0; i < kMGPipeScreenCallCount; ++i) {
EXPECT_EQ(screen[i], nullptr) << "screen entry " << i;
}
const void* const* context = reinterpret_cast<const void* const*>(&gMGPipeContext); const void* const* context = reinterpret_cast<const void* const*>(&gMGPipeContext);
for (SizeT i = 0; i < kMGPipeContextCallCount; ++i) { #if MOBILEGL_PIPE_PUSH
EXPECT_EQ(context[i], nullptr) << "context entry " << i; MGPipeInstallMonolithTables();
// The 34 rows with no MGPipeApply* entry point are still null, and null still means "this
// subsystem has not been migrated, keep pulling". Named rather than counted, because the
// count is the other case's job and two cases asserting the same number would both go red
// for one change.
EXPECT_EQ(gMGPipeContext.SetShaderBuffers, nullptr);
EXPECT_EQ(gMGPipeContext.SetStreamOutputTargets, nullptr);
EXPECT_EQ(gMGPipeContext.DrawVbo, nullptr);
EXPECT_EQ(gMGPipeContext.Present, nullptr);
EXPECT_EQ(gMGPipeContext.SetSwapInterval, nullptr);
EXPECT_EQ(gMGPipeScreen.GetCaps, nullptr);
EXPECT_EQ(gMGPipeContext.QueryCreate, nullptr);
EXPECT_EQ(gMGPipeScreen.FenceCreate, nullptr);
#else
// A pull build compiles no applier and no routing, so the pre-migration statement is the
// whole truth there and this case is the one that says so.
for (SizeT i = 0; i < kMGPipeScreenCallCount; ++i) EXPECT_EQ(screen[i], nullptr) << i;
for (SizeT i = 0; i < kMGPipeContextCallCount; ++i) EXPECT_EQ(context[i], nullptr) << i;
#endif
(void)screen;
(void)context;
}
TEST(PipeCatalogue, ExactlyTheRoutedRowsAreInstalledAndTheRestAreStillNull) {
const void* const* screen = reinterpret_cast<const void* const*>(&gMGPipeScreen);
const void* const* context = reinterpret_cast<const void* const*>(&gMGPipeContext);
SizeT installed = 0;
SizeT nulls = 0;
#if MOBILEGL_PIPE_PUSH
// IDEMPOTENT, and called here on purpose: what this case observes is WHICH rows the
// installer fills, not whether an installer ran somewhere in this binary. Leaving that to
// ambient linkage is what made the same assertion pass in one build directory and fail in
// another - the object file carrying a static initialiser was dropped by the linker in the
// binaries that did not name a symbol in it.
MGPipeInstallMonolithTables();
#endif
for (SizeT i = 0; i < kMGPipeScreenCallCount; ++i) {
if (screen[i] != nullptr) ++installed; else ++nulls;
} }
for (SizeT i = 0; i < kMGPipeContextCallCount; ++i) {
if (context[i] != nullptr) ++installed; else ++nulls;
}
EXPECT_EQ(installed + nulls, static_cast<SizeT>(kMGPipeCallCount));
#if MOBILEGL_PIPE_PUSH
// 33 + 4 = 37, and the split is the honest shape of R-17 rather than an implementation
// detail: 37 is the number of MGPipeApply* entry points PipeApply.h declares, 33 of them
// fit a GENERATED row and go in the two tables, and FOUR cannot be expressed by any
// generated signature and go in the hand-written escape table beside them
// (ResourceRespecify's uncarried initialBytes, ResourceFlushRange's likewise,
// MapPersistent's size + seedBytes + void* return, CreateShaderState's seven blobrefs and
// two typed pointers - each one a CONTRACT-P5 ruling, see MG_Pipe/PipeRoute.h).
//
// BOTH NUMBERS ARE ASSERTED. If the escape table were left out of this case, moving a row
// out of the generated tables and forgetting to install its escape would read as a smaller
// "installed" count and nothing else - and the call site would take a null.
EXPECT_EQ(installed, 33u) << "the routed rows and the applier's entry points disagree";
EXPECT_EQ(nulls, static_cast<SizeT>(kMGPipeCallCount) - 33u);
const void* const* escapes = reinterpret_cast<const void* const*>(&gMGPipeRouteEscapes);
SizeT escapesInstalled = 0;
for (SizeT i = 0; i < sizeof(MGPipeRouteEscapes) / sizeof(void*); ++i) {
if (escapes[i] != nullptr) ++escapesInstalled;
}
EXPECT_EQ(escapesInstalled, 4u) << "an escape row is null; its call site would take a null "
"pointer rather than fall back to anything";
EXPECT_EQ(installed + escapesInstalled, 37u)
<< "the two tables plus the escapes must be exactly PipeApply.h's entry points";
// And the rows that MUST still be null, named rather than counted: these are calls with no
// applier at all (CONTRACT-P5 table 1 rows 13, 14: "no applier entry point exists"), plus
// the two verbs the census measured as having zero MG_Impl call sites. An installer that
// filled one of these would be claiming an implementation that does not exist.
EXPECT_EQ(gMGPipeContext.SetShaderBuffers, nullptr);
EXPECT_EQ(gMGPipeContext.SetStreamOutputTargets, nullptr);
EXPECT_EQ(gMGPipeContext.DrawVbo, nullptr);
EXPECT_EQ(gMGPipeContext.Present, nullptr);
EXPECT_EQ(gMGPipeContext.SetSwapInterval, nullptr);
EXPECT_EQ(gMGPipeScreen.GetCaps, nullptr);
#else
// A pull build compiles no applier and no routing, so the pre-migration statement is still
// the whole truth there.
EXPECT_EQ(installed, 0u);
EXPECT_EQ(nulls, static_cast<SizeT>(kMGPipeCallCount));
#endif
} }
// The retirement ratchet of the migration carrier (section 6.3): the constant and the // The retirement ratchet of the migration carrier (section 6.3): the constant and the
+91
View File
@@ -4802,6 +4802,93 @@ TEST(DirectGLESBufferDrawProbe, UnderSplitTheRecordAloneAnswersTheLiveHostMapQue
#endif #endif
} }
// R-8's NEGATIVE CONTROL, at the level of the probe above rather than at the level of the
// accessor. `CapsMirrorTest.AMaskWithoutAFamilyRefusesItAndNamesIt` already pins that
// `ServerConsumes` counts and names a refusal; what it cannot pin is that the LIVENESS GATE
// the probe above depends on actually asks it. This case is the pair: the op table is
// registered - so the pre-R-8 read (`MGPipeGetResourceOps() != nullptr`) would answer
// "enabled" - and the caps mask is EMPTY, so the only honest answer is "no consumer".
//
// AND THE REFUSAL IS COUNTED, NOT INFERRED. "No record appeared" is satisfied by a client that
// never ran at all: a typo in the fixture, a subsystem bit left clear, a BufferObject that
// threw. Asking `ConsumerRefusals()` for a DELTA and `LastRefusedSubsystem()` for the family's
// own bit is a statement that the gate was reached, asked the mirror, and was told no - which
// is the fact R-8 exists to establish. Put `MGPipeGetResourceOps() != nullptr` back into
// MGPipeResourceSubsystemEnabled() and this goes red on the record, not on the counter.
TEST(DirectGLESBufferDrawProbe, ACapsMaskWithoutTheResourceFamilyEmitsNothingAndCountsTheRefusal) {
using namespace MobileGL;
using namespace MobileGL::MG_Backend::DirectGLES;
using namespace MobileGL::MG_State::GLState;
if (!EsprytSlotTablesEnabled()) {
GTEST_SKIP() << "the handle-keyed resource table only exists on the {slot, gen} arm";
}
#if !MOBILEGL_BUILD_DISAGGREGATED
GTEST_SKIP() << "MG_Config::Transport is a constexpr Monolith without the transport built in, "
"so the split arm of this probe cannot be entered";
#else
const Uint64 previousPush = MG_Config::Features.PipePush;
const auto previousTransport = MG_Config::Transport;
MG_Pipe::MGPipeResourceOps ops{};
MG_Config::Features.PipePush |= MG_Pipe::kMGPipeSubsystemResources;
// THE SERVER's registration is present. Under the pre-R-8 gate this alone armed the client.
MG_Pipe::MGPipeSetResourceOps(&ops);
MG_Config::Transport = MG_Config::TransportMode::InProcess;
{
// The CLIENT's answer: a snapshot that names no consumer at all. R-12 makes a second
// arrival the invalidation, so adopting is how a mask is replaced; there is no
// Invalidate() to call and inventing one would be a second spelling of the same edge.
MG_Pipe::MGPCaps caps{};
caps.CallMask = 0;
MG_Remote::Client::CapsMirrorInstance().Adopt(caps, MG_Backend::FormatCapabilityCache{},
RendererInfo{}, String{},
BackendType::DirectGLES);
}
ASSERT_FALSE(MG_Remote::Client::CapsMirrorInstance().ServerConsumes(
MG_Pipe::kMGPipeSubsystemResources))
<< "the fixture's own mask consumes the family, so this case cannot observe a refusal";
MG_Remote::Client::ResetConsumerRefusalsForTest();
{
auto owner = MakeShared<BufferObject>(0u);
owner->Respecify(256, nullptr);
// The MINT is unconditional in a push build (set_vertex_buffers names a buffer by
// handle whether or not the resource family is on), so a handle is the expected state
// and is NOT what this case reads.
const MG_Pipe::MGPipeHandle res = MG_Pipe::MGPipeResourceTrackerInstance().Find(*owner);
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(res))
<< "the mint is unconditional; without a handle this case is observing the wrong "
"absence";
auto& applier = MG_Pipe::MGPipeApplier();
if (applier.Resources.size() > static_cast<SizeT>(res.Slot)) {
EXPECT_FALSE(applier.Resources[res.Slot].Live)
<< "resource_create reached the applier for a family the server told this client "
"it does not consume - which is ID-39's 66 lost uploads in the other "
"direction: records emitted to a consumer that is not there";
}
}
// THE COUNTED HALF. Both statements, because either alone is satisfiable by an accident:
// a non-zero count alone could come from any family, and the family id alone could be left
// over from an earlier case.
EXPECT_GT(MG_Remote::Client::ConsumerRefusals(), 0u)
<< "the liveness gate never asked the caps mirror. It is still reading "
"MGPipeGetResourceOps(), which is the SERVER's registration and is null under a "
"spawn - R-8's whole defect";
EXPECT_EQ(MG_Remote::Client::LastRefusedSubsystem(), MG_Pipe::kMGPipeSubsystemResources)
<< "a refusal was counted for some other family, so this case is not observing the "
"resource gate it names";
MG_Config::Transport = previousTransport;
MG_Pipe::MGPipeSetResourceOps(nullptr);
MG_Config::Features.PipePush = previousPush;
MG_Remote::Client::ResetConsumerRefusalsForTest();
#endif
}
// P3a REWORK M-1's gate (contract-review M2). The minting overload's symmetric `!=` is safe // P3a REWORK M-1's gate (contract-review M2). The minting overload's symmetric `!=` is safe
// because its handle comes out of the allocator and can never be behind the entry; the HANDLE // because its handle comes out of the allocator and can never be behind the entry; the HANDLE
// overload's input ARRIVES in a payload, so a generation BEHIND the live entry's is reachable - // overload's input ARRIVES in a payload, so a generation BEHIND the live entry's is reachable -
@@ -4936,4 +5023,8 @@ TEST(DirectGLESBufferDrawProbe, ALiveHostMapKeepsTheHandleArmProbeDirtyBetweenTw
TEST(DirectGLESBufferDrawProbe, UnderSplitTheRecordAloneAnswersTheLiveHostMapQuestion) { TEST(DirectGLESBufferDrawProbe, UnderSplitTheRecordAloneAnswersTheLiveHostMapQuestion) {
GTEST_SKIP() << "the handle-keyed resource table is compiled only under MOBILEGL_PIPE_PUSH"; GTEST_SKIP() << "the handle-keyed resource table is compiled only under MOBILEGL_PIPE_PUSH";
} }
TEST(DirectGLESBufferDrawProbe, ACapsMaskWithoutTheResourceFamilyEmitsNothingAndCountsTheRefusal) {
GTEST_SKIP() << "the handle-keyed resource table is compiled only under MOBILEGL_PIPE_PUSH";
}
#endif // MOBILEGL_PIPE_PUSH #endif // MOBILEGL_PIPE_PUSH
+108 -16
View File
@@ -231,6 +231,30 @@ namespace {
std::uint64_t m_sessionApplied = 0; std::uint64_t m_sessionApplied = 0;
}; };
// ID-31 + R-17's cross-check, written once because four cases need it.
//
// The four Bool acceptance rows now answer into SEG_REPLY, and "an answer was written" is
// a weak statement on its own - a PostReply that stamped a constant OK would satisfy it.
// So the STATUSES are compared against the decoder's own accepted/declined counters, which
// are produced by a different line of code from a different value. A single-sided
// assertion here is exactly the shape R-16 was written after.
void ExpectRepliesAgreeWithTheAcceptanceTally(Wire2& wire) {
std::uint64_t ok = 0;
std::uint64_t declined = 0;
for (const auto& reply : wire.Answers().All) {
EXPECT_NE(reply.Status, ReplySink::kStatusError)
<< "seq " << reply.Seq << ": ERROR is not an acceptance answer";
if (reply.Status == ReplySink::kStatusOk) ++ok;
if (reply.Status == ReplySink::kStatusDeclined) ++declined;
}
EXPECT_EQ(ok, wire.Decoder().AcceptedRecords())
<< "the slots say " << ok << " accepted, the decoder's tally says "
<< wire.Decoder().AcceptedRecords();
EXPECT_EQ(declined, wire.Decoder().DeclinedRecords())
<< "the slots say " << declined << " declined, the decoder's tally says "
<< wire.Decoder().DeclinedRecords();
}
MGPipeHandle MakeHandle(Uint32 slot, Uint32 gen = 1) { MGPipeHandle MakeHandle(Uint32 slot, Uint32 gen = 1) {
MGPipeHandle handle{}; MGPipeHandle handle{};
handle.Slot = slot; handle.Slot = slot;
@@ -543,12 +567,22 @@ TEST_F(PipeWireCodecTest, KNeedsAckRespecifyCarriesItsRedefinitionScope) {
kInvalidSeq); kInvalidSeq);
ASSERT_TRUE(wire.PumpOne(&applied)); ASSERT_TRUE(wire.PumpOne(&applied));
EXPECT_TRUE(applied); EXPECT_TRUE(applied);
// Two records, two ACCEPTANCE answers - and NOT in a reply slot. Neither ResourceCreate // Two records, two ACCEPTANCE answers, AND THEY NOW RIDE THE REPLY SLOT (ID-31 + R-17).
// nor ResourceRespecify carries kReplySlot, and s1 sizes ReplyPool from that table, so a // Both rows carry kReplySlot in kMGPipeCallFlags, ReplyPool is sized from that same table,
// reply written here would overwrite some waiter's slot. // and CONTRACT-P5 table 0 always said DECLINED "is how the four Bool acceptance entry
EXPECT_TRUE(wire.Answers().All.empty()); // points say false". The two halves agreed the moment ID-31 landed the flag.
ASSERT_EQ(wire.Answers().All.size(), 2u);
EXPECT_EQ(wire.Answers().All[0].Seq, 1u);
EXPECT_EQ(wire.Answers().All[1].Seq, 2u);
EXPECT_TRUE(wire.Answers().All[0].Bytes.empty());
EXPECT_TRUE(wire.Answers().All[1].Bytes.empty());
EXPECT_EQ(wire.Decoder().AcceptedRecords() + wire.Decoder().DeclinedRecords(), 2u); EXPECT_EQ(wire.Decoder().AcceptedRecords() + wire.Decoder().DeclinedRecords(), 2u);
EXPECT_TRUE(wire.Decoder().LastAcceptanceKnown()); EXPECT_TRUE(wire.Decoder().LastAcceptanceKnown());
// THE CROSS-CHECK, and it is the point of asserting the status at all: the slot's status
// and the decoder's own tally are two independent statements of the same fact, so a
// PostReply that stamped a constant would disagree with the counters even though every
// other assertion above still passed.
ExpectRepliesAgreeWithTheAcceptanceTally(wire);
} }
TEST_F(PipeWireCodecTest, KOptionalUnmapPersistentRoundTrips) { TEST_F(PipeWireCodecTest, KOptionalUnmapPersistentRoundTrips) {
@@ -741,7 +775,12 @@ TEST_F(PipeWireCodecTest, ResourceSubDataCarriesABlobAndARegionTailTogether) {
kInvalidSeq); kInvalidSeq);
ASSERT_TRUE(wire.PumpOne(&applied)); ASSERT_TRUE(wire.PumpOne(&applied));
EXPECT_TRUE(applied); EXPECT_TRUE(applied);
EXPECT_TRUE(wire.Answers().All.empty()); // ID-31 + R-17: ResourceCreate and ResourceSubData both carry kReplySlot now, so both
// answer into a slot rather than only into the decoder's tally.
ASSERT_EQ(wire.Answers().All.size(), 2u);
EXPECT_EQ(wire.Answers().All[0].Seq, 1u);
EXPECT_EQ(wire.Answers().All[1].Seq, 2u);
ExpectRepliesAgreeWithTheAcceptanceTally(wire);
// ACCEPTANCE IS NOT "APPLIED", and this case is where the difference shows. The record // ACCEPTANCE IS NOT "APPLIED", and this case is where the difference shows. The record
// crossed and reached MGPipeApplyResourceSubData, which is the codec's whole job; the // crossed and reached MGPipeApplyResourceSubData, which is the codec's whole job; the
// applier then DECLINED it, because no backend registered a P4a texture consumer in this // applier then DECLINED it, because no backend registered a P4a texture consumer in this
@@ -1198,11 +1237,24 @@ TEST_F(PipeWireCodecTest, NoReplyIsWrittenForARowTheCatalogueGivesNoReplySlot) {
// s1 sizes ReplyPool from MGPipeCallFlagsFor, so a reply written for a record the pool // s1 sizes ReplyPool from MGPipeCallFlagsFor, so a reply written for a record the pool
// reserved no slot for overwrites a waiter's answer - and because the slot header stamps // reserved no slot for overwrites a waiter's answer - and because the slot header stamps
// the WRITER's seq for self-check, the waiter's check then fails for ever and the barrier // the WRITER's seq for self-check, the waiter's check then fails for ever and the barrier
// HANGS rather than returning something wrong. // HANGS rather than returning something wrong. That statement is unchanged and is what
// this case still asserts.
//
// WHAT CHANGED IS THE ROW IT ASSERTS IT ABOUT (ID-31 fallout, closed by c1 round 2). It
// used to name ResourceCreate and SetTextureParams as rows "the catalogue gives no reply
// slot", which ID-31 made false - it gave ResourceCreate the flag, and all four Bool
// acceptance rows carry it now. A case that names a kReplySlot row while asserting a row
// has no slot is not testing the rule, it is testing a stale catalogue, so the row moved
// to one that genuinely carries kNone and the rule stayed exactly where it was.
Wire2 wire; Wire2 wire;
ASSERT_EQ(MGPipeCallFlagsFor(MGPWireOp::ResourceCreate) & static_cast<Uint32>(kReplySlot), 0u); ASSERT_EQ(MGPipeCallFlagsFor(MGPWireOp::ResourceDestroy) & static_cast<Uint32>(kReplySlot), 0u)
ASSERT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetTextureParams) & static_cast<Uint32>(kReplySlot), 0u); << "ResourceDestroy gained a reply slot; this case needs a kNone row to mean anything";
ASSERT_EQ(MGPipeCallFlagsFor(MGPWireOp::BindRenderState) & static_cast<Uint32>(kReplySlot), 0u);
// A create first, so the destroy has a live record to reach - and its OWN answer is the
// control on the control: if PostReply were firing indiscriminately there would be two
// answers here, not one, and the case would fail on the count rather than pass because
// nothing was ever written.
MGPResourceDesc create{}; MGPResourceDesc create{};
create.Resource = MakeHandle(131); create.Resource = MakeHandle(131);
create.Target = static_cast<Uint8>(MGPipeResourceTarget::Buffer); create.Target = static_cast<Uint8>(MGPipeResourceTarget::Buffer);
@@ -1216,23 +1268,63 @@ TEST_F(PipeWireCodecTest, NoReplyIsWrittenForARowTheCatalogueGivesNoReplySlot) {
kInvalidSeq); kInvalidSeq);
bool applied = false; bool applied = false;
ASSERT_TRUE(wire.PumpOne(&applied)); ASSERT_TRUE(wire.PumpOne(&applied));
ASSERT_EQ(wire.Answers().All.size(), 1u) << "the kReplySlot row did not answer";
const MGPHandleOnly destroy = HandleOnly(131, MGPipeKind::Buffer);
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ResourceDestroy, &destroy, sizeof(destroy)),
kInvalidSeq);
ASSERT_TRUE(wire.PumpOne(&applied));
EXPECT_TRUE(applied); EXPECT_TRUE(applied);
EXPECT_TRUE(wire.Answers().All.empty()) << "a reply slot was written for a kNone row"; EXPECT_EQ(wire.Answers().All.size(), 1u) << "a reply slot was written for a kNone row";
// The acceptance answer R-5 requires is still produced - it just does not ride SEG_REPLY. EXPECT_EQ(wire.Answers().All[0].Seq, 1u) << "the answer that exists is the create's";
EXPECT_TRUE(wire.Decoder().LastAcceptanceKnown());
EXPECT_EQ(wire.Decoder().AcceptedRecords() + wire.Decoder().DeclinedRecords(), 1u);
} }
TEST_F(PipeWireCodecTest, EveryRowThatDoesWriteAReplyCarriesKReplySlot) { TEST_F(PipeWireCodecTest, EveryRowThatDoesWriteAReplyCarriesKReplySlot) {
// The other half: the three rows in P5 that answer into a slot all carry the flag, so // The other half: every row in P5 that answers into a slot carries the flag, so
// PostReply's gate cannot be firing on any of them. // PostReply's gate cannot be firing on any of them. The four Bool acceptance rows joined
for (const MGPWireOp op : // this list at ID-31 and R-17 - and they are listed BY NAME rather than derived from the
{MGPWireOp::MapPersistent, MGPWireOp::ResourceReadback, MGPWireOp::ReadPixels}) { // flags table, because deriving the expectation from the same table the assertion reads
// would make this case true by construction.
for (const MGPWireOp op : {MGPWireOp::MapPersistent, MGPWireOp::ResourceReadback,
MGPWireOp::ReadPixels, MGPWireOp::ResourceCreate,
MGPWireOp::ResourceRespecify, MGPWireOp::ResourceSubData,
MGPWireOp::SetTextureParams}) {
EXPECT_NE(MGPipeCallFlagsFor(op) & static_cast<Uint32>(kReplySlot), 0u) << WireOpName(op); EXPECT_NE(MGPipeCallFlagsFor(op) & static_cast<Uint32>(kReplySlot), 0u) << WireOpName(op);
} }
} }
TEST_F(PipeWireCodecTest, TheFourAcceptanceRowsAnswerThroughTheSlotAndNotOnlyThroughTheTally) {
// R-17's server half, and the reason c1 could not simply write thirty-seven emitters: the
// client asks the CATALOGUE whether a row owns a slot (ClientSession::EmitAndWait), so the
// moment ID-31 gave these rows kReplySlot, a decoder that answered only into
// LastAcceptance() left the client parked in the barrier until Fatal{ReplyMissing}.
//
// THE ASSERTION IS THAT THE ANSWER MATCHES THE APPLIER's, not that one exists. Under a
// unit process no backend registers a P4a consumer, so set_texture_params is DECLINED -
// and that is the useful direction: a PostReply hard-coded to OK would pass a case that
// only counted answers.
Wire2 wire;
MGPTextureParams params{};
params.Res = MakeHandle(77);
params.BuiltinSampler = MakeHandle(78);
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::SetTextureParams, &params, sizeof(params)),
kInvalidSeq);
bool applied = false;
ASSERT_TRUE(wire.PumpOne(&applied));
EXPECT_TRUE(applied) << "the record must still have CROSSED; acceptance is not 'applied'";
ASSERT_EQ(wire.Answers().All.size(), 1u);
EXPECT_EQ(wire.Answers().All[0].Seq, 1u) << "the id is the record's own ordinal (R-3)";
EXPECT_TRUE(wire.Answers().All[0].Bytes.empty())
<< "an acceptance answer is a STATUS; DECLINED carries no payload";
ASSERT_TRUE(wire.Decoder().LastAcceptanceKnown());
EXPECT_EQ(wire.Answers().All[0].Status, wire.Decoder().LastAcceptance()
? ReplySink::kStatusOk
: ReplySink::kStatusDeclined);
ExpectRepliesAgreeWithTheAcceptanceTally(wire);
}
TEST_F(PipeWireCodecTest, TheAuditFillOverwritesExactlyTheRunsTheRecordResolved) { TEST_F(PipeWireCodecTest, TheAuditFillOverwritesExactlyTheRunsTheRecordResolved) {
// R-2.5, the only mechanical control on rule C ("no applier entry point retains a pointer // R-2.5, the only mechanical control on rule C ("no applier entry point retains a pointer
// past its return"). An instrumentation that cannot be observed to have run is decoration, // past its return"). An instrumentation that cannot be observed to have run is decoration,
+231
View File
@@ -25,6 +25,7 @@
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <vector>
#include <string> #include <string>
#include "Includes.h" #include "Includes.h"
@@ -33,6 +34,7 @@
#include <MG_Pipe/MGPipe.h> #include <MG_Pipe/MGPipe.h>
#include <MG_Remote/CapsCodec.h> #include <MG_Remote/CapsCodec.h>
#include <MG_Remote/Client/CapsMirror.h> #include <MG_Remote/Client/CapsMirror.h>
#include <MG_Pipe/PipeRoute.h>
#include <MG_Remote/Client/EmitTables.h> #include <MG_Remote/Client/EmitTables.h>
#if !defined(_WIN32) #if !defined(_WIN32)
@@ -400,6 +402,235 @@ TEST(RemoteEmitTable, TheE2DropSwitchStartsDisarmed) {
<< "arming and disarming the control must not, by itself, drop anything"; << "arming and disarming the control must not, by itself, drop anything";
} }
// =====================================================================================
// ID-47: a readback larger than a reply slot is refused at the CLIENT, by name
// =====================================================================================
TEST(RemoteReadback, ExactlyTheCapacityPassesAndOneByteMoreIsRefusedByName) {
// THE BOUNDARY PAIR ID-47 ASKS FOR, driven through the emitter's own decision function.
// It is not driven through EmitReadPixels, and that is the point: EmitReadPixels needs a
// live session before it reaches any of this, so a control over the emitter could only
// ever observe Fatal{NoClientSession} and would be green for the wrong reason.
constexpr Uint64 kCap = 2u * 1024u * 1024u - 16u; // ID-47's post-growth MaxReplyBytes
// The passing half runs IN THIS PROCESS, because "it did not abort" is only a statement if
// the thing that would have aborted is the same code.
RefuseReadbackLargerThanTheReplySlot(724, 724, 0x1908 /*GL_RGBA*/, kCap, kCap);
SUCCEED() << "exactly the capacity is not an overflow";
#if MGTEST_HAVE_FORK
const ChildResult child = RunInChild([&] {
RefuseReadbackLargerThanTheReplySlot(640, 480, 0x1908 /*GL_RGBA*/, kCap + 1, kCap);
});
EXPECT_TRUE(DiedOfAbort(child)) << "one byte over the slot did not abort: " << DescribeStatus(child);
// ITS OWN FAILURE STRING, AND THE READ'S OWN NUMBERS. A control that only asserted
// "something died" would pass on Fatal{NoClientSession}, Fatal{UnmigratedVerb} or a
// segfault, and this file has three other cases that abort for those reasons.
EXPECT_NE(child.Log.find("Fatal{ReplyTooLarge"), std::string::npos) << child.Log;
EXPECT_NE(child.Log.find("ReadPixels 640x480"), std::string::npos)
<< "the message does not name the read, so an operator cannot tell which one: " << child.Log;
EXPECT_NE(child.Log.find(std::to_string(kCap + 1)), std::string::npos)
<< "the message does not carry the byte count";
#else
GTEST_SKIP() << "the refusal reports through MGLOG_F + abort and needs fork() to read back";
#endif
}
// =====================================================================================
// ID-49: the pack state never crosses; the client scatters the tight rows
// =====================================================================================
TEST(RemoteReadback, DstSizeIsTheTightExtentAndNeverThePackedOne) {
// The number v1 allocates on the server. It must not move with the pack state, because the
// server reads with a NEUTRAL one and cannot see the client's: the first version of this
// emitter declared GL 8.4.4's PACKED size, v1 allocated exactly that, and the driver then
// wrote past it - two SEGFAULTs on the joint inproc lane, both backends.
constexpr GLenum kRgba = 0x1908;
constexpr GLenum kUByte = 0x1401;
EXPECT_EQ(TightReadbackByteCount(4, 3, kRgba, kUByte), 4u * 3u * 4u);
EXPECT_EQ(TightReadbackByteCount(640, 480, kRgba, kUByte), 640u * 480u * 4u);
// ID-47's own arithmetic depends on this: 640x480 RGBA8 is what the E2 retrace snapshot
// reads, and 1,228,800 is the number that forced SEG_REPLY to grow.
EXPECT_EQ(TightReadbackByteCount(640, 480, kRgba, kUByte), 1228800u);
}
TEST(RemoteReadback, TheTightRowsAreScatteredWhereThePackStateSaysAndTheGapsAreLeftAlone) {
// ID-49's control, and it is the exact case the cross-family review found: 4x3 RGBA8 with
// PACK_ROW_LENGTH=8, SKIP_ROWS=1, SKIP_PIXELS=2. Stride = 8*4 = 32; the first written byte
// is 1*32 + 2*4 = 40; each row writes 4*4 = 16 bytes and the remaining 16 of its stride
// belong to the application.
constexpr Uint64 kBpp = 4;
constexpr GLsizei kW = 4;
constexpr GLsizei kH = 3;
constexpr Uint8 kSentinel = 0xCD;
PixelStoreParameters pack{};
pack.RowLength = 8;
pack.SkipRows = 1;
pack.SkipPixels = 2;
pack.Alignment = 4;
// THE INPUT, not the state under test: every source byte is distinct, so a scatter that
// wrote the right COUNT of bytes from the wrong offset cannot look correct.
std::vector<Uint8> tight(static_cast<size_t>(kW) * kH * kBpp);
for (size_t i = 0; i < tight.size(); ++i) tight[i] = static_cast<Uint8>(i);
std::vector<Uint8> destination(512, kSentinel);
ScatterTightReadbackIntoPackState(tight.data(), destination.data(), kW, kH, kBpp, pack);
const size_t stride = 8 * 4;
const size_t first = 1 * stride + 2 * 4;
for (size_t row = 0; row < static_cast<size_t>(kH); ++row) {
for (size_t byte = 0; byte < static_cast<size_t>(kW) * kBpp; ++byte) {
EXPECT_EQ(destination[first + row * stride + byte],
tight[row * static_cast<size_t>(kW) * kBpp + byte])
<< "row " << row << " byte " << byte << " landed somewhere else";
}
}
// THE GAPS, which is the half that makes this a control rather than a copy of the loop
// above: the bytes the pack state does not name belong to the application and must still
// hold their sentinel. Removing the skips from the scatter passes the loop above and fails
// here; widening the per-row copy to the stride passes both loops above and fails here.
size_t touched = 0;
for (size_t i = 0; i < destination.size(); ++i) {
const bool inWrittenRow =
i >= first && ((i - first) % stride) < static_cast<size_t>(kW) * kBpp &&
((i - first) / stride) < static_cast<size_t>(kH);
if (!inWrittenRow) {
EXPECT_EQ(destination[i], kSentinel)
<< "byte " << i << " is outside the rectangle GL names and was overwritten";
} else {
++touched;
}
}
EXPECT_EQ(touched, tight.size()) << "the scatter wrote a different number of bytes than the "
"reply carried";
}
TEST(RemoteReadback, TheFastPathIsTakenExactlyWhenTheScatterWouldChangeNothing) {
// EmitReadPixels reads the reply STRAIGHT into the application's pointer when
// ReadbackPackStateIsTight says so, and pays for a bounce buffer otherwise. That is only
// legal if the predicate and the scatter AGREE - so this case drives BOTH and compares
// them, rather than testing either alone. A predicate that said "tight" for a layout the
// scatter would have rearranged is a silently wrong picture with no bounce to blame.
constexpr Uint64 kBpp = 4;
constexpr GLsizei kW = 5;
constexpr GLsizei kH = 3;
std::vector<Uint8> tight(static_cast<size_t>(kW) * kH * kBpp);
for (size_t i = 0; i < tight.size(); ++i) tight[i] = static_cast<Uint8>(i * 7 + 1);
// Six layouts, chosen so both answers appear: a bare default, an explicit equal row
// length, an alignment the row already satisfies, an alignment it does not, a skip, and a
// wider row. If every case agreed on "tight" the comparison below would be vacuous, so the
// count of each answer is asserted too.
std::vector<PixelStoreParameters> layouts(6);
layouts[1].RowLength = kW;
layouts[2].Alignment = 4; // 5*4 = 20, already a multiple of 4
layouts[3].Alignment = 8; // 20 is not a multiple of 8 - the rows gain padding
layouts[4].SkipPixels = 1;
layouts[5].RowLength = 8;
int tightCount = 0;
for (size_t i = 0; i < layouts.size(); ++i) {
const PixelStoreParameters& pack = layouts[i];
std::vector<Uint8> destination(4096, 0);
ScatterTightReadbackIntoPackState(tight.data(), destination.data(), kW, kH, kBpp, pack);
destination.resize(tight.size());
const bool scatterChangedNothing = (destination == tight);
const bool predicateSaysTight =
ReadbackPackStateIsTightForTest(kW, kBpp, pack) != 0;
EXPECT_EQ(predicateSaysTight, scatterChangedNothing)
<< "layout " << i << ": the fast-path predicate and the scatter disagree";
if (predicateSaysTight) ++tightCount;
}
EXPECT_GT(tightCount, 0) << "no layout took the fast path, so the equality above is vacuous";
EXPECT_LT(tightCount, static_cast<int>(layouts.size()))
<< "every layout took the fast path, so the equality above is vacuous";
}
// =====================================================================================
// R-17: the routing, its reply mailbox, and the arm that is actually installed
// =====================================================================================
TEST(PipeRouting, TheTablesAreInstalledWithoutAnybodyHavingRememberedTo) {
// The gate on the install MECHANISM rather than on the table contents (PipeCatalogueTest
// owns the partition). Nothing in this file calls an installer; the tables are installed
// because MG_Pipe/PipeRoute.h's inline variable is in this binary, which is the property
// that a static initialiser inside PipeRoute.cpp did NOT have - the linker dropped that
// object from every test binary that named no symbol in it, and five CsoCacheTest cases
// took a null function pointer.
EXPECT_TRUE(MGPipeTablesAreInstalled());
EXPECT_EQ(static_cast<int>(MGPipeInstalledArm()), static_cast<int>(MGPipeRouteArm::kMonolith))
<< "this process has no ClientSession, so the monolith adapters must be the arm";
}
TEST(PipeRouting, AnAnswerIsWhatTheRowSaidAndDeclinedIsFalseRatherThanAFailure) {
const Uint64 takenBefore = MGPipeRepliesTaken();
const Uint64 declinedBefore = MGPipeRepliesDeclined();
const MGPReplySlot ok = MGPipeMintReplySlot();
MGPipePostReply(ok, 0 /*OK*/, 1);
EXPECT_TRUE(MGPipeTakeReplyBool(ok, "unit"));
const MGPReplySlot declined = MGPipeMintReplySlot();
MGPipePostReply(declined, 1 /*DECLINED*/, 0);
EXPECT_FALSE(MGPipeTakeReplyBool(declined, "unit"))
<< "DECLINED is how the four Bool acceptance rows say false (R-5), not how they fail";
EXPECT_EQ(MGPipeRepliesTaken(), takenBefore + 2u);
EXPECT_EQ(MGPipeRepliesDeclined(), declinedBefore + 1u)
<< "the refusal was not COUNTED, so 'the client accepted everything' and 'the client "
"never asked' are still the same observation from outside";
// The two slots are different ids, which is what makes the mismatch Fatal below meaningful.
EXPECT_NE(ok.Id, declined.Id);
EXPECT_NE(ok.Id, 0u) << "slot 0 is ReplySlot.h's 'no record' and must stay unmintable";
}
#if MGTEST_HAVE_FORK
TEST(PipeRouting, AnUnansweredRowIsFatalRatherThanAcceptedOrRefused) {
// R-5's whole point, made structural. There is no default: "always accept" is ID-39's 66
// lost DirectVulkan uploads with a wire in between, and "always refuse" is an emitter that
// re-sends for ever. A row that forgets to answer has to be impossible to READ.
const ChildResult child = RunInChild([] {
const MGPReplySlot slot = MGPipeMintReplySlot();
(void)MGPipeTakeReplyBool(slot, "resource_create");
});
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child);
EXPECT_NE(child.Log.find("Fatal{ReplyMissing"), std::string::npos) << child.Log;
EXPECT_NE(child.Log.find("resource_create"), std::string::npos)
<< "the Fatal does not name the row, so it cannot say WHICH answer went missing";
}
TEST(PipeRouting, TwoOutstandingAnswersAreFatalBecauseTheBarrierIsOneDeep) {
// The mailbox is one entry deep because R-1's verb barrier makes the in-flight depth one
// (ReplySlot.h). A second posting before the first is taken is not a capacity problem, it
// is a barrier that has stopped holding - so it must not be absorbed by a deeper mailbox.
const ChildResult child = RunInChild([] {
const MGPReplySlot first = MGPipeMintReplySlot();
const MGPReplySlot second = MGPipeMintReplySlot();
MGPipePostReply(first, 0, 1);
MGPipePostReply(second, 0, 1);
});
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child);
EXPECT_NE(child.Log.find("Fatal{ReplyOverrun"), std::string::npos) << child.Log;
}
TEST(PipeRouting, AnErrorStatusIsNotFoldedIntoAcceptedOrRefused) {
// ERROR is a transport fault and DECLINED is a resource decision. Folding the first into
// either arm of the second makes a broken wire look like a server that said no.
const ChildResult child = RunInChild([] {
const MGPReplySlot slot = MGPipeMintReplySlot();
MGPipePostReply(slot, 2 /*ERROR*/, 0);
(void)MGPipeTakeReplyBool(slot, "set_texture_params");
});
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child);
EXPECT_NE(child.Log.find("Fatal{ReplyError"), std::string::npos) << child.Log;
EXPECT_NE(child.Log.find("set_texture_params"), std::string::npos) << child.Log;
}
#endif // MGTEST_HAVE_FORK
int main(int argc, char** argv) { int main(int argc, char** argv) {
namespace fs = std::filesystem; namespace fs = std::filesystem;
const fs::path path = const fs::path path =
+13
View File
@@ -152,6 +152,19 @@ class Call(object):
"""(parameter declaration list, argument list) for this call.""" """(parameter declaration list, argument list) for this call."""
params = ["const %s* payload" % self.Payload] params = ["const %s* payload" % self.Payload]
args = ["payload"] args = ["payload"]
# P5 R-17. EXACTLY the shape kVarTail already has, one flag over: a kHasBlob row's
# payload owns an MGPBlobRef, and a blobref names bytes that live somewhere the
# payload cannot reach on its own. In monolith "somewhere" is the caller's own
# memory and the companion is the raw pointer every MG_Impl call site passes today
# (CONTRACT-P5 table 1's "companion pointer today" column, nine rows); under split
# it is a SEG_STAGE run the client staged before it published. A table row that
# omitted the pair could reproduce NEITHER, which is why nine of the thirty-seven
# entry points had no routing that could be written at all.
if "kHasBlob" in self.Flags:
params.append("const void* blobBytes")
params.append("Uint64 blobByteCount")
args.append("blobBytes")
args.append("blobByteCount")
if "kVarTail" in self.Flags: if "kVarTail" in self.Flags:
params.append("const void* varTail") params.append("const void* varTail")
params.append("Uint32 varTailCount") params.append("Uint32 varTailCount")