mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[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:
@@ -533,6 +533,13 @@ if (MOBILEGL_PIPE_PUSH)
|
||||
# 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.
|
||||
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()
|
||||
|
||||
@@ -574,6 +581,12 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# 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.
|
||||
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
|
||||
# MarkGpuWritten producers are on the server's side of the line, and the
|
||||
# block-granularity persistent-map push that tier T2 makes mandatory.
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
|
||||
#include <cstring>
|
||||
@@ -154,7 +155,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// minted from a neighbour rather than from nothing.
|
||||
desc.ChunkMask = kAllPipelineChunks;
|
||||
desc.Blob.Size = kMGPipePipelineChunkBytes;
|
||||
MGPipeApplyCreateRenderState(desc, bytes.data());
|
||||
MGPipeRouteCreateRenderState(desc, bytes.data());
|
||||
payloadBytes += sizeof(MGPRenderStateDesc) + kMGPipePipelineChunkBytes;
|
||||
|
||||
Entry entry;
|
||||
@@ -175,7 +176,7 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPHandleOnly handle{};
|
||||
handle.Handle = m_entries[index].Cso;
|
||||
handle.Kind = static_cast<Uint32>(MGPipeKind::RenderStateCso);
|
||||
MGPipeApplyDeleteRenderState(handle);
|
||||
MGPipeRouteDeleteRenderState(handle);
|
||||
MGPipeSlots().Free(MGPipeKind::RenderStateCso, m_entries[index].Cso);
|
||||
m_entries[index] = m_entries.back();
|
||||
m_entries.pop_back();
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include <MG_Impl/Pipe/Tracker.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
|
||||
@@ -456,7 +457,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastDraw = state;
|
||||
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Both)) m_lastRead = state;
|
||||
}
|
||||
MGPipeApplySetFramebufferState(state);
|
||||
MGPipeRouteSetFramebufferState(state);
|
||||
++m_emissions;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::FramebufferEmissions, 1);
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/TextureState/TextureState.h>
|
||||
@@ -126,7 +127,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastImages.Start = 0;
|
||||
m_lastImages.Count = count;
|
||||
m_lastImages.ContentHash = hash;
|
||||
MGPipeApplySetShaderImages(m_lastImages, m_entries.data());
|
||||
MGPipeRouteSetShaderImages(m_lastImages, m_entries.data());
|
||||
++m_imageSets;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ShaderImageEmissions, 1);
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
#include <MG_Impl/Pipe/VertexInputEmit.h>
|
||||
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <Config.h>
|
||||
|
||||
@@ -46,6 +47,7 @@
|
||||
// build-split runs MOBILEGL_TRANSPORT=monolith in every unit and integration-gpu lane and those
|
||||
// lanes must keep answering exactly what they answered before.
|
||||
#include <MG_Remote/Client/CapsMirror.h>
|
||||
#include <MG_Remote/Client/WireTables.h>
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
@@ -677,7 +679,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// 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).
|
||||
tracker.NotePublished(handle);
|
||||
MGPipeApplyResourceCreate(desc);
|
||||
MGPipeRouteResourceCreate(desc);
|
||||
}
|
||||
|
||||
void MGPipeEmitResourceRespecify(BufferObject& buffer) {
|
||||
@@ -710,7 +712,7 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeBuildResourceDesc(buffer, handle, bindMask, /*storageDefined=*/false);
|
||||
tracker.NoteDesc(createDesc, true);
|
||||
tracker.NotePublished(handle);
|
||||
MGPipeApplyResourceCreate(createDesc);
|
||||
MGPipeRouteResourceCreate(createDesc);
|
||||
}
|
||||
const MGPResourceDesc desc = MGPipeBuildResourceDesc(buffer, handle, bindMask, true);
|
||||
tracker.NoteDesc(desc, false);
|
||||
@@ -723,7 +725,47 @@ namespace MobileGL::MG_Pipe {
|
||||
// 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
|
||||
// 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) {
|
||||
@@ -746,7 +788,11 @@ namespace MobileGL::MG_Pipe {
|
||||
// take the object, which is why it is set here and not in the builder.
|
||||
record.HasLiveHostWrites = buffer.HasLiveHostWritesForWire() ? 1 : 0;
|
||||
#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) {
|
||||
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;
|
||||
#endif
|
||||
// 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) {
|
||||
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
|
||||
// between a map+memcpy+unmap and an upload, so merging them here would change which.
|
||||
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) {
|
||||
@@ -811,7 +878,7 @@ namespace MobileGL::MG_Pipe {
|
||||
record.Size = buffer.GetSize();
|
||||
// The answer travels back through MGPipeClientOnBufferWriteback, and the server's
|
||||
// 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
|
||||
@@ -834,7 +901,7 @@ namespace MobileGL::MG_Pipe {
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
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) {
|
||||
@@ -849,7 +916,7 @@ namespace MobileGL::MG_Pipe {
|
||||
const Bool published = tracker.WasPublished(handle);
|
||||
if (published) {
|
||||
tracker.NoteDestroy();
|
||||
MGPipeApplyResourceDestroy(BufferHandleOnly(handle));
|
||||
MGPipeRouteResourceDestroy(BufferHandleOnly(handle));
|
||||
}
|
||||
// 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
|
||||
@@ -889,7 +956,7 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPHandleOnly only{};
|
||||
only.Handle = handle;
|
||||
only.Kind = static_cast<Uint32>(MGPipeKind::VertexElementsCso);
|
||||
MGPipeApplyDeleteVertexElements(only);
|
||||
MGPipeRouteDeleteVertexElements(only);
|
||||
emitter.NoteRecordDestroyed(handle);
|
||||
}
|
||||
|
||||
@@ -2103,7 +2170,7 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 EmitPixelPackState(GLContext& ctx) {
|
||||
MGPPixelPackState pack{};
|
||||
pack.Pack = ctx.GetPixelStoreParameters(false);
|
||||
MGPipeApplySetPixelPackState(pack);
|
||||
MGPipeRouteSetPixelPackState(pack);
|
||||
return sizeof(MGPPixelPackState);
|
||||
}
|
||||
|
||||
@@ -2116,7 +2183,7 @@ namespace MobileGL::MG_Pipe {
|
||||
patch.Vertices = live.PatchVertices;
|
||||
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];
|
||||
MGPipeApplySetPatchState(patch);
|
||||
MGPipeRouteSetPatchState(patch);
|
||||
return sizeof(MGPPatchState);
|
||||
}
|
||||
|
||||
@@ -2205,7 +2272,7 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
if (header.Count == 0) return 0;
|
||||
g_attribDefaultLastHeader = header;
|
||||
MGPipeApplySetVertexAttribDefaults(header, tail.data());
|
||||
MGPipeRouteSetVertexAttribDefaults(header, tail.data());
|
||||
|
||||
// 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
|
||||
@@ -2274,7 +2341,7 @@ namespace MobileGL::MG_Pipe {
|
||||
block.CapabilityBits |= Uint64{1} << i;
|
||||
}
|
||||
}
|
||||
MGPipeApplySetResidualValueState(block);
|
||||
MGPipeRouteSetResidualValueState(block);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// ByteClass::ResidualValueBlock has been a placeholder that "stays at 0
|
||||
// until P2" since P0. This is what makes it non-zero.
|
||||
@@ -2328,7 +2395,7 @@ namespace MobileGL::MG_Pipe {
|
||||
bind.Cso = cso;
|
||||
bind.Version = version;
|
||||
bind.PipelineVersion = pipelineVersion;
|
||||
MGPipeApplyBindRenderState(bind);
|
||||
MGPipeRouteBindRenderState(bind);
|
||||
payloadBytes += sizeof(MGPBindRenderState);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::RenderStateCsoBinds, 1);
|
||||
@@ -2350,7 +2417,7 @@ namespace MobileGL::MG_Pipe {
|
||||
dyn.ChunkMask = chunkMask;
|
||||
dyn.Version = version;
|
||||
dyn.Blob.Size = blobBytes;
|
||||
MGPipeApplySetDynamicState(dyn, blob.data());
|
||||
MGPipeRouteSetDynamicState(dyn, blob.data());
|
||||
payloadBytes += sizeof(MGPDynamicState) + blobBytes;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/MGPipeHostSpan.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||
@@ -137,19 +138,19 @@ namespace MobileGL::MG_Pipe {
|
||||
// exactly "nothing bound".
|
||||
const MGPipeHandle boundCso = !MGPipeHandleIsNull(drawCso) ? drawCso : dispatchCso;
|
||||
if (boundCso != m_boundCso) {
|
||||
MGPipeApplyBindShaderState(HandleOnly(boundCso));
|
||||
MGPipeRouteBindShaderState(HandleOnly(boundCso));
|
||||
m_boundCso = boundCso;
|
||||
++m_binds;
|
||||
bytes += sizeof(MGPHandleOnly);
|
||||
}
|
||||
if (drawCso != m_drawCso) {
|
||||
MGPipeApplySetDrawProgram(HandleOnly(drawCso));
|
||||
MGPipeRouteSetDrawProgram(HandleOnly(drawCso));
|
||||
m_drawCso = drawCso;
|
||||
++m_drawSets;
|
||||
bytes += sizeof(MGPHandleOnly);
|
||||
}
|
||||
if (dispatchCso != m_dispatchCso) {
|
||||
MGPipeApplySetDispatchProgram(HandleOnly(dispatchCso));
|
||||
MGPipeRouteSetDispatchProgram(HandleOnly(dispatchCso));
|
||||
m_dispatchCso = dispatchCso;
|
||||
++m_dispatchSets;
|
||||
bytes += sizeof(MGPHandleOnly);
|
||||
@@ -192,7 +193,12 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastConstants.Blob.Seg = kMGHostSpanSegNone;
|
||||
m_lastConstants.Blob.Offset = reinterpret_cast<Uint64>(program->GetUBOData());
|
||||
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_constantsVersion = version;
|
||||
++m_constantSets;
|
||||
@@ -254,7 +260,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastDesc.Reflection.Offset = reinterpret_cast<Uint64>(&link);
|
||||
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
|
||||
// (contract-v2 §3.1). MGPipeEmitShaderCsoDestroyAndFree reads it, and without it
|
||||
// delete_shader_state can never go out - for an ordinary program or for a
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/MGPipeHostSpan.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_State/GLState/Core.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
|
||||
// call and the bytes it stores are provably the bytes the memcmp will confirm
|
||||
// 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
|
||||
// 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.
|
||||
@@ -490,7 +491,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
@@ -783,7 +784,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastViews.Start = 0;
|
||||
m_lastViews.Count = count;
|
||||
m_lastViews.ContentHash = hash;
|
||||
MGPipeApplySetSamplerViews(m_lastViews, m_views.data());
|
||||
MGPipeRouteSetSamplerViews(m_lastViews, m_views.data());
|
||||
++m_viewSets;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::SamplerViewEmissions, 1);
|
||||
@@ -857,7 +858,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastStates.Start = 0;
|
||||
m_lastStates.Count = count;
|
||||
m_lastStates.ContentHash = hash;
|
||||
MGPipeApplyBindSamplerStates(m_lastStates, m_states.data());
|
||||
MGPipeRouteBindSamplerStates(m_lastStates, m_states.data());
|
||||
++m_stateSets;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
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.Samples = static_cast<Uint16>(texture.GetSamples() < 0 ? 0 : texture.GetSamples());
|
||||
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
|
||||
// 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
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/RenderbufferState/RenderbufferObject.h>
|
||||
@@ -802,7 +803,7 @@ namespace MobileGL::MG_Pipe {
|
||||
++m_paramSets;
|
||||
// Not behind MGPipeTextureRecordsReachTheApplier() (see its comment): the call is
|
||||
// dispatched whenever this emitter runs, so the answer is always a real one.
|
||||
Bool accepted = MGPipeApplySetTextureParams(params);
|
||||
Bool accepted = MGPipeRouteSetTextureParams(params);
|
||||
if (!accepted) {
|
||||
// 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
|
||||
@@ -828,7 +829,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// across it - `entry` is re-fetched below.
|
||||
EmitResourceRespecify(texture, MGPipeTextureRespecifyScope::WholeResource, 0, 0);
|
||||
}
|
||||
accepted = MGPipeApplySetTextureParams(params);
|
||||
accepted = MGPipeRouteSetTextureParams(params);
|
||||
}
|
||||
Entry& latched = EntryFor(m_textures, handle);
|
||||
if (!accepted) {
|
||||
@@ -1121,7 +1122,7 @@ namespace MobileGL::MG_Pipe {
|
||||
Bool dispatched = false;
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
|
||||
dispatched = true;
|
||||
accepted = MGPipeApplyResourceCreate(desc);
|
||||
accepted = MGPipeRouteResourceCreate(desc);
|
||||
}
|
||||
if (dispatched && !accepted) return;
|
||||
MGPipeNoteHandlePublished(kind, handle);
|
||||
@@ -1134,7 +1135,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// replaces no storage at all.
|
||||
static Bool ApplyRespecify(const MGPResourceDesc& desc, const MGPRespecifiedLevel* level) {
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
|
||||
return MGPipeApplyResourceRespecify(desc, nullptr, level);
|
||||
return MGPipeRouteResourceRespecify(desc, nullptr, level);
|
||||
}
|
||||
(void)level;
|
||||
return false;
|
||||
@@ -1275,7 +1276,14 @@ namespace MobileGL::MG_Pipe {
|
||||
Bool dispatched = false;
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
|
||||
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_subDatas;
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#include <MG_Impl/Pipe/Tracker.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
|
||||
@@ -158,7 +159,7 @@ namespace MobileGL::MG_Pipe {
|
||||
const auto& vao = ctx.GetBoundVertexArray();
|
||||
if (!vao) {
|
||||
if (!MGPipeHandleIsNull(m_boundHandle)) {
|
||||
MGPipeApplyBindVertexElements(HandleOnly(kMGPipeNullHandle));
|
||||
MGPipeRouteBindVertexElements(HandleOnly(kMGPipeNullHandle));
|
||||
++m_binds;
|
||||
m_boundHandle = kMGPipeNullHandle;
|
||||
m_boundLifetimeId = 0;
|
||||
@@ -178,7 +179,7 @@ namespace MobileGL::MG_Pipe {
|
||||
latch.Gen != handle.Gen;
|
||||
if (configMoved) bytes += EmitCreate(*vao, handle, latch, configVersion);
|
||||
if (lifetimeId != m_boundLifetimeId || m_boundHandle != handle) {
|
||||
MGPipeApplyBindVertexElements(HandleOnly(handle));
|
||||
MGPipeRouteBindVertexElements(HandleOnly(handle));
|
||||
++m_binds;
|
||||
bytes += sizeof(MGPHandleOnly);
|
||||
m_boundHandle = handle;
|
||||
@@ -242,7 +243,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// emulation is server-owned.
|
||||
m_lastBuffers.BaseInstance = baseInstance;
|
||||
m_lastBuffers.ContentHash = hash;
|
||||
MGPipeApplySetVertexBuffers(m_lastBuffers, m_entries.data());
|
||||
MGPipeRouteSetVertexBuffers(m_lastBuffers, m_entries.data());
|
||||
++m_bufferSets;
|
||||
return sizeof(MGPVertexBuffers) + static_cast<Uint64>(count) * sizeof(MGPVertexBuffer);
|
||||
}
|
||||
@@ -267,7 +268,7 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeResourceTrackerInstance().NoteBoundAs(m_lastIndex.Res, BufferTarget::Index);
|
||||
}
|
||||
}
|
||||
MGPipeApplySetIndexBuffer(m_lastIndex);
|
||||
MGPipeRouteSetIndexBuffer(m_lastIndex);
|
||||
++m_indexSets;
|
||||
return sizeof(MGPIndexBuffer);
|
||||
}
|
||||
@@ -396,7 +397,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastElements.Blob.Seg = kMGHostSpanSegNone;
|
||||
m_lastElements.Blob.Offset = 0;
|
||||
m_lastElements.Blob.Size = kAttribBytes + kBindingBytes;
|
||||
MGPipeApplyCreateVertexElements(m_lastElements, m_blob.data());
|
||||
MGPipeRouteCreateVertexElements(m_lastElements, m_blob.data());
|
||||
++m_creates;
|
||||
latch.Published = true;
|
||||
latch.Gen = handle.Gen;
|
||||
|
||||
@@ -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
|
||||
@@ -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(¶ms, &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
|
||||
@@ -15,7 +15,7 @@
|
||||
// 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).
|
||||
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 (*ResourceRespecify)(const MGPResourceDesc* payload, MGPReplySlot* reply);
|
||||
void (*ResourceDestroy)(const MGPHandleOnly* payload);
|
||||
@@ -37,20 +37,20 @@ struct MGPipeContext {
|
||||
void (*QueryAvailable)(const MGPHandleOnly* payload, MGPReplySlot* reply);
|
||||
void (*QueryResult)(const MGPQueryResultRequest* payload, MGPReplySlot* reply);
|
||||
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 (*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 (*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 (*CreateSamplerView)(const MGPSamplerView* 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 (*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 (*SetVertexBuffers)(const MGPVertexBuffers* payload, const void* varTail, Uint32 varTailCount);
|
||||
void (*SetIndexBuffer)(const MGPIndexBuffer* payload);
|
||||
@@ -60,16 +60,16 @@ struct MGPipeContext {
|
||||
void (*SetShaderImages)(const MGPShaderImages* 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 (*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 (*SetPixelPackState)(const MGPPixelPackState* payload);
|
||||
void (*SetPatchState)(const MGPPatchState* payload);
|
||||
void (*SetDrawProgram)(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 (*ResourceSubData)(const MGPSubData* payload, const void* varTail, Uint32 varTailCount, MGPReplySlot* reply);
|
||||
void (*BufferSubDataResident)(const MGPSubData* payload);
|
||||
void (*ResourceSubData)(const MGPSubData* payload, const void* blobBytes, Uint64 blobByteCount, const void* varTail, Uint32 varTailCount, MGPReplySlot* reply);
|
||||
void (*BufferSubDataResident)(const MGPSubData* payload, const void* blobBytes, Uint64 blobByteCount);
|
||||
void (*ResourceSubDataComplete)(const MGPSubDataComplete* payload);
|
||||
void (*ResourceFlushRange)(const MGPFlushRange* payload);
|
||||
void (*ResourceReadback)(const MGPReadback* payload, MGPReplySlot* reply);
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
// unimplemented (null) entry is the caller's business to check, exactly as it is
|
||||
// with the table this replaces.
|
||||
|
||||
inline void MGP_GetCaps(const MGPCaps* payload, MGPReplySlot* reply) {
|
||||
gMGPipeScreen.GetCaps(payload, reply);
|
||||
inline void MGP_GetCaps(const MGPCaps* payload, const void* blobBytes, Uint64 blobByteCount, MGPReplySlot* reply) {
|
||||
gMGPipeScreen.GetCaps(payload, blobBytes, blobByteCount, reply);
|
||||
}
|
||||
|
||||
inline void MGP_ResourceCreate(const MGPResourceDesc* payload, MGPReplySlot* reply) {
|
||||
@@ -81,8 +81,8 @@ inline void MGP_QueryDestroy(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.QueryDestroy(payload);
|
||||
}
|
||||
|
||||
inline void MGP_CreateRenderState(const MGPRenderStateDesc* payload) {
|
||||
gMGPipeContext.CreateRenderState(payload);
|
||||
inline void MGP_CreateRenderState(const MGPRenderStateDesc* payload, const void* blobBytes, Uint64 blobByteCount) {
|
||||
gMGPipeContext.CreateRenderState(payload, blobBytes, blobByteCount);
|
||||
}
|
||||
|
||||
inline void MGP_BindRenderState(const MGPBindRenderState* payload) {
|
||||
@@ -93,8 +93,8 @@ inline void MGP_DeleteRenderState(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.DeleteRenderState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_CreateVertexElements(const MGPVertexElements* payload) {
|
||||
gMGPipeContext.CreateVertexElements(payload);
|
||||
inline void MGP_CreateVertexElements(const MGPVertexElements* payload, const void* blobBytes, Uint64 blobByteCount) {
|
||||
gMGPipeContext.CreateVertexElements(payload, blobBytes, blobByteCount);
|
||||
}
|
||||
|
||||
inline void MGP_BindVertexElements(const MGPHandleOnly* payload) {
|
||||
@@ -105,8 +105,8 @@ inline void MGP_DeleteVertexElements(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.DeleteVertexElements(payload);
|
||||
}
|
||||
|
||||
inline void MGP_CreateSamplerState(const MGPSamplerDesc* payload) {
|
||||
gMGPipeContext.CreateSamplerState(payload);
|
||||
inline void MGP_CreateSamplerState(const MGPSamplerDesc* payload, const void* blobBytes, Uint64 blobByteCount) {
|
||||
gMGPipeContext.CreateSamplerState(payload, blobBytes, blobByteCount);
|
||||
}
|
||||
|
||||
inline void MGP_DeleteSamplerState(const MGPHandleOnly* payload) {
|
||||
@@ -121,8 +121,8 @@ inline void MGP_DeleteSamplerView(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.DeleteSamplerView(payload);
|
||||
}
|
||||
|
||||
inline void MGP_CreateShaderState(const MGPProgramDesc* payload) {
|
||||
gMGPipeContext.CreateShaderState(payload);
|
||||
inline void MGP_CreateShaderState(const MGPProgramDesc* payload, const void* blobBytes, Uint64 blobByteCount) {
|
||||
gMGPipeContext.CreateShaderState(payload, blobBytes, blobByteCount);
|
||||
}
|
||||
|
||||
inline void MGP_BindShaderState(const MGPHandleOnly* payload) {
|
||||
@@ -133,8 +133,8 @@ inline void MGP_DeleteShaderState(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.DeleteShaderState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_SetDynamicState(const MGPDynamicState* payload) {
|
||||
gMGPipeContext.SetDynamicState(payload);
|
||||
inline void MGP_SetDynamicState(const MGPDynamicState* payload, const void* blobBytes, Uint64 blobByteCount) {
|
||||
gMGPipeContext.SetDynamicState(payload, blobBytes, blobByteCount);
|
||||
}
|
||||
|
||||
inline void MGP_SetFramebufferState(const MGPFramebufferState* payload) {
|
||||
@@ -173,8 +173,8 @@ inline void MGP_SetStreamOutputTargets(const MGPStreamOutputTargets* payload, co
|
||||
gMGPipeContext.SetStreamOutputTargets(payload, varTail, varTailCount);
|
||||
}
|
||||
|
||||
inline void MGP_SetGlobalConstants(const MGPGlobalConstants* payload) {
|
||||
gMGPipeContext.SetGlobalConstants(payload);
|
||||
inline void MGP_SetGlobalConstants(const MGPGlobalConstants* payload, const void* blobBytes, Uint64 blobByteCount) {
|
||||
gMGPipeContext.SetGlobalConstants(payload, blobBytes, blobByteCount);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
inline void MGP_SetResidualValueState(const MGPResidualValueState* payload) {
|
||||
gMGPipeContext.SetResidualValueState(payload);
|
||||
inline void MGP_SetResidualValueState(const MGPResidualValueState* payload, const void* blobBytes, Uint64 blobByteCount) {
|
||||
gMGPipeContext.SetResidualValueState(payload, blobBytes, blobByteCount);
|
||||
}
|
||||
|
||||
inline void MGP_SetTextureParams(const MGPTextureParams* payload, MGPReplySlot* reply) {
|
||||
gMGPipeContext.SetTextureParams(payload, reply);
|
||||
}
|
||||
|
||||
inline void MGP_ResourceSubData(const MGPSubData* payload, const void* varTail, Uint32 varTailCount, MGPReplySlot* reply) {
|
||||
gMGPipeContext.ResourceSubData(payload, varTail, varTailCount, reply);
|
||||
inline void MGP_ResourceSubData(const MGPSubData* payload, const void* blobBytes, Uint64 blobByteCount, const void* varTail, Uint32 varTailCount, MGPReplySlot* reply) {
|
||||
gMGPipeContext.ResourceSubData(payload, blobBytes, blobByteCount, varTail, varTailCount, reply);
|
||||
}
|
||||
|
||||
inline void MGP_BufferSubDataResident(const MGPSubData* payload) {
|
||||
gMGPipeContext.BufferSubDataResident(payload);
|
||||
inline void MGP_BufferSubDataResident(const MGPSubData* payload, const void* blobBytes, Uint64 blobByteCount) {
|
||||
gMGPipeContext.BufferSubDataResident(payload, blobBytes, blobByteCount);
|
||||
}
|
||||
|
||||
inline void MGP_ResourceSubDataComplete(const MGPSubDataComplete* payload) {
|
||||
|
||||
@@ -29,113 +29,30 @@ namespace MobileGL::MG_Remote::Client {
|
||||
|
||||
namespace {
|
||||
|
||||
// ---- the EGL bridge ---------------------------------------------------------------
|
||||
// ---- the EGL seam ------------------------------------------------------------------
|
||||
//
|
||||
// Every one of the nine crosses as a BLOCKING control request on the apply thread,
|
||||
// because every one of them has a return value the caller acts on immediately. v1's
|
||||
// ServerLoop::RunOnApplyThread takes a raw function pointer plus a user pointer rather
|
||||
// than a std::function, deliberately: this path runs at teardown too, and the teardown
|
||||
// path may not allocate (ID-8).
|
||||
// THE NINE EGL VIRTUALS CALL v1's TWELVE FORWARDERS AND NOTHING ELSE. c1 round 1 built
|
||||
// its own trampolines over ServerLoop::RunOnApplyThread and ServerLoop::Backend(),
|
||||
// which ran the right driver call on the right thread and was still wrong, because
|
||||
// three of the twelve do MORE than forward:
|
||||
//
|
||||
// A NULL SERVER BACKEND IS "false", NOT A CRASH AND NOT A LOCAL SUCCESS. Under inproc
|
||||
// the apply thread creates it during ClientSession::Start, so a null one here means the
|
||||
// bring-up did not complete - and answering `true` would let the frontend believe it
|
||||
// has a context.
|
||||
MG_Backend::BackendObject* ServerBackend() { return Server::ServerLoopInstance().Backend(); }
|
||||
|
||||
struct DisplayArgs {
|
||||
EGLDisplay Dpy;
|
||||
EGLint* Major;
|
||||
EGLint* Minor;
|
||||
Bool Ok;
|
||||
};
|
||||
MobileGLResult RunInitDisplay(void* user) {
|
||||
auto* args = static_cast<DisplayArgs*>(user);
|
||||
args->Ok = ServerBackend()->InitializeEGLDisplay(args->Dpy, args->Major, args->Minor);
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// ServerMakeEGLCurrent re-publishes the caps snapshot after the server's own
|
||||
// InitCapabilities has run (R-12 arm (a))
|
||||
// ServerInitCapabilities the same, on the explicit path
|
||||
// ServerSetWindowHandle hands the surface to the server's backend
|
||||
//
|
||||
// Calling `Backend()->MakeEGLCurrent(...)` skips the republish, so the client's mirror
|
||||
// keeps the snapshot Accept() sent BEFORE any context existed - every limit, every
|
||||
// advertised extension and the compile-env fingerprint read off an empty backend, with
|
||||
// InitCapabilities below happily reporting success because a snapshot did arrive once.
|
||||
// That is the failure this seam exists to make impossible: there is no second route to
|
||||
// the server's EGL, so there is no route that can skip what the forwarder does.
|
||||
//
|
||||
// The forwarders block on mgl-srv-apply themselves and run INLINE when the caller is
|
||||
// already on that thread, so this file no longer needs RunOnApplyThread, a per-call
|
||||
// args struct, or a null-backend check of its own - ServerBackendOrNull() inside each
|
||||
// forwarder is the one place that answers "the bring-up did not complete", and it
|
||||
// answers `false` rather than crashing or succeeding locally.
|
||||
|
||||
// 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
|
||||
@@ -198,6 +115,18 @@ namespace MobileGL::MG_Remote::Client {
|
||||
MGLOG_E("MG_Remote client: InitCapabilities with no session");
|
||||
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();
|
||||
RefreshFormatCapabilities();
|
||||
// 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.
|
||||
|
||||
Bool BackendObject_Remote::InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) {
|
||||
DisplayArgs args{dpy, major, minor, false};
|
||||
if (!ForwardToApplyThread("InitializeEGLDisplay", &RunInitDisplay, &args) || !args.Ok) {
|
||||
return false;
|
||||
}
|
||||
if (!Server::ServerInitializeEGLDisplay(dpy, major, minor)) return false;
|
||||
return MG_Backend::BackendObject::InitializeEGLDisplay(dpy, major, minor);
|
||||
}
|
||||
|
||||
Bool BackendObject_Remote::CreateEGLWindowSurface(EGLSurface surface,
|
||||
const MG_Backend::WindowHandle& handle) {
|
||||
WindowSurfaceArgs args{surface, &handle, false};
|
||||
if (!ForwardToApplyThread("CreateEGLWindowSurface", &RunCreateWindowSurface, &args) ||
|
||||
!args.Ok) {
|
||||
return false;
|
||||
}
|
||||
// The handle first: the server's backend has to know which window it is about to make
|
||||
// a surface for, and ServerSetWindowHandle is the only way to tell it.
|
||||
Server::ServerSetWindowHandle(handle);
|
||||
if (!Server::ServerCreateEGLWindowSurface(surface, handle)) return false;
|
||||
return MG_Backend::BackendObject::CreateEGLWindowSurface(surface, handle);
|
||||
}
|
||||
|
||||
Bool BackendObject_Remote::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||
ResizeArgs args{surface, width, height, false};
|
||||
if (!ForwardToApplyThread("ResizeEGLWindowSurface", &RunResize, &args) || !args.Ok) {
|
||||
return false;
|
||||
}
|
||||
if (!Server::ServerResizeEGLWindowSurface(surface, width, height)) return false;
|
||||
return MG_Backend::BackendObject::ResizeEGLWindowSurface(surface, width, height);
|
||||
}
|
||||
|
||||
Bool BackendObject_Remote::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
|
||||
PbufferArgs args{surface, width, height, false};
|
||||
if (!ForwardToApplyThread("CreateEGLPbufferSurface", &RunCreatePbuffer, &args) || !args.Ok) {
|
||||
return false;
|
||||
}
|
||||
if (!Server::ServerCreateEGLPbufferSurface(surface, width, height)) return false;
|
||||
return MG_Backend::BackendObject::CreateEGLPbufferSurface(surface, width, height);
|
||||
}
|
||||
|
||||
Bool BackendObject_Remote::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read,
|
||||
EGLContext ctx) {
|
||||
MakeCurrentArgs args{dpy, draw, read, ctx, false};
|
||||
if (!ForwardToApplyThread("MakeEGLCurrent", &RunMakeCurrent, &args) || !args.Ok) {
|
||||
return false;
|
||||
}
|
||||
// AND ONLY NOW the client's own bookkeeping, which is what calls InitCapabilities.
|
||||
// FORWARD FIRST, THEN RUN THE BASE. The server has to own the context before the base
|
||||
// class latches "the surface is initialised" and calls InitCapabilities, because
|
||||
// InitCapabilities' answer comes from a snapshot the server can only publish once its
|
||||
// own InitCapabilities has run - and ServerMakeEGLCurrent is what publishes it.
|
||||
if (!Server::ServerMakeEGLCurrent(dpy, draw, read, ctx)) return false;
|
||||
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
|
||||
// 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
|
||||
// (it has zero MG_Impl call sites). Forwarding would present on the server directly and
|
||||
// put no record on the wire, which is the shape every gate in this phase exists to
|
||||
// catch.
|
||||
// (it has zero MG_Impl call sites). Calling Server::ServerSwapEGLBuffers would present
|
||||
// on the server directly and put no record on the wire, which is the shape every gate
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -311,13 +231,11 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// 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
|
||||
// an interval" belongs to the server, so it is asked of the server.
|
||||
SwapIntervalArgs args{interval};
|
||||
ForwardToApplyThread("SetEGLSwapInterval", &RunSwapInterval, &args);
|
||||
Server::ServerSetEGLSwapInterval(interval);
|
||||
}
|
||||
|
||||
void BackendObject_Remote::ReleaseEGLSurface(EGLSurface surface) {
|
||||
SurfaceArgs args{surface};
|
||||
ForwardToApplyThread("ReleaseEGLSurface", &RunReleaseSurface, &args);
|
||||
Server::ServerReleaseEGLSurface(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()
|
||||
// (MobileGL/Init.cpp:68) walks on the moment this returns, and the server still holds
|
||||
// the context until the apply thread has run it.
|
||||
ForwardToApplyThread("ReleaseEGLResources", &RunReleaseResources, nullptr);
|
||||
Server::ServerReleaseEGLResources();
|
||||
MG_Backend::BackendObject::ReleaseEGLResources();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "../Server/ServerLoop.h"
|
||||
#include "../Server/ServerSession.h"
|
||||
#include "../Transport/InProcessTransport.h"
|
||||
#include "WireTables.h"
|
||||
|
||||
#include <MGGitHash.h>
|
||||
#include <MG_Pipe/MGPipeCallbacks.h>
|
||||
@@ -509,6 +510,16 @@ namespace MobileGL::MG_Remote::Client {
|
||||
"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;
|
||||
LogMemory("handshake");
|
||||
|
||||
@@ -521,10 +532,38 @@ namespace MobileGL::MG_Remote::Client {
|
||||
Stop();
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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
|
||||
|
||||
@@ -86,6 +86,23 @@ namespace MobileGL::MG_Remote::Client {
|
||||
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 --------------------------------
|
||||
//
|
||||
// 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
|
||||
// pixel-store state. The client has to declare it - MGPReadbackInfo::DstSize is what
|
||||
// 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).
|
||||
// pixel-store state.
|
||||
//
|
||||
// GL 4.6 8.4.4's arithmetic, and nothing cleverer: a row is rounded up to Alignment,
|
||||
// the LAST row is not padded, and SkipRows/SkipPixels/SkipImages shift the destination
|
||||
// rather than growing it (the application owns those bytes and we never write them).
|
||||
Uint64 PackedReadbackBytes(GLsizei width, GLsizei height, GLenum format, GLenum type) {
|
||||
if (width <= 0 || height <= 0) return 0;
|
||||
// ID-49: THE PACK STATE NEVER CROSSES FOR A READ, AND DstSize IS THE TIGHT EXTENT.
|
||||
// The first version of this computed GL 4.6 8.4.4's PACKED size - row length, skips,
|
||||
// alignment - and handed it over as DstSize. v1's server allocates exactly DstSize and
|
||||
// the real backend honours the live pack state, so a 4x3 RGBA8 read with
|
||||
// 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 =
|
||||
MG_Util::ConvertGLEnumToTextureInputFormat(format);
|
||||
const TexturePixelDataType dataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
|
||||
@@ -226,18 +256,27 @@ namespace MobileGL::MG_Remote::Client {
|
||||
static_cast<unsigned>(format), static_cast<unsigned>(type));
|
||||
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);
|
||||
const Uint64 alignment = pack.Alignment > 0 ? static_cast<Uint64>(pack.Alignment) : 1ull;
|
||||
const Uint64 rowBytes = rowPixels * static_cast<Uint64>(bytesPerPixel);
|
||||
const Uint64 paddedRow = ((rowBytes + alignment - 1) / alignment) * alignment;
|
||||
const Uint64 lastRow = static_cast<Uint64>(width) * static_cast<Uint64>(bytesPerPixel);
|
||||
return paddedRow * (static_cast<Uint64>(height) - 1) + lastRow;
|
||||
Uint64 TightReadbackBytes(GLsizei width, GLsizei height, GLenum format, GLenum type) {
|
||||
if (width <= 0 || height <= 0) return 0;
|
||||
return static_cast<Uint64>(width) * static_cast<Uint64>(height) *
|
||||
ReadbackBytesPerPixel(format, type);
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -251,7 +290,9 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// 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
|
||||
// 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{};
|
||||
info.Res = MG_Pipe::kMGPipeNullHandle; // "the bound read surface answers"
|
||||
@@ -262,27 +303,41 @@ namespace MobileGL::MG_Remote::Client {
|
||||
info.Target = 0;
|
||||
info.Level = 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
|
||||
// Fatal on the server, and a Fatal there is a dead apply thread with a client
|
||||
// parked in the barrier for ever; here it is one line naming the number.
|
||||
const Uint64 capacity = session.MaxReplyBytes();
|
||||
if (bytes > capacity) {
|
||||
MGLOG_F("MGPipe: Fatal{ReplyTooLarge, \"read_pixels\"} %llu bytes into a %llu-byte "
|
||||
"reply slot - P5 does not chunk a readback (R-10); raise the reply pool's "
|
||||
"slot size or shrink the read",
|
||||
static_cast<unsigned long long>(bytes),
|
||||
static_cast<unsigned long long>(capacity));
|
||||
std::abort();
|
||||
// CHECKED BEFORE THE EMISSION, not after the answer (ID-47). A reply bigger than a
|
||||
// slot is Fatal on the SERVER, and a Fatal there is a dead apply thread with a
|
||||
// client parked in the barrier for ever, naming a byte count and not a read; here
|
||||
// it is one line naming the read. The capacity is read LIVE from the pool rather
|
||||
// than compared against a constant, so s1's growth of SEG_REPLY to 16 MiB / eight
|
||||
// 2 MiB slots needs no edit in this file.
|
||||
RefuseOversizeReadback(width, height, format, tight, session.MaxReplyBytes());
|
||||
|
||||
PixelStoreParameters pack{};
|
||||
if (MG_State::pGLContext != nullptr) {
|
||||
pack = MG_State::pGLContext->GetPixelStoreParameters(/*isUnpack=*/false);
|
||||
}
|
||||
|
||||
Int32 status = 0;
|
||||
// The pixels land straight in the application's buffer: the barrier's wait IS the
|
||||
// reply's wait (R-3), so this costs no round trip beyond the one the barrier was
|
||||
// already paying.
|
||||
if (ReadbackPackStateIsTight(width, bytesPerPixel, pack)) {
|
||||
// THE COMMON CASE, AND IT KEEPS THE ZERO-COPY. A neutral pack state means the
|
||||
// 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,
|
||||
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,
|
||||
pixels, bytes, &status);
|
||||
bounce.data(), tight, &status);
|
||||
ScatterTightReadbackIntoPackState(bounce.data(), pixels, width, height, bytesPerPixel,
|
||||
pack);
|
||||
}
|
||||
|
||||
void EmitPresent() {
|
||||
@@ -533,4 +588,50 @@ namespace MobileGL::MG_Remote::Client {
|
||||
void SetDropClearEmissionForNegativeControl(Bool drop) { g_dropClearEmission = drop; }
|
||||
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
|
||||
|
||||
@@ -47,16 +47,18 @@
|
||||
// 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'".
|
||||
//
|
||||
// NOTE the asymmetry this table does not resolve: the resource, CSO, framebuffer, texture,
|
||||
// sampler and program families do NOT come through here. They are emitted from
|
||||
// MG_Impl/Pipe/* by direct MGPipeApply* calls (37 entry points, 41 call sites), and under
|
||||
// split each of those becomes an encode. This table covers only the verbs - the draws,
|
||||
// clears, blits, readbacks, queries, fences and present.
|
||||
// NOTE the asymmetry this table does not resolve, AND WHERE IT IS RESOLVED (R-17): the
|
||||
// resource, CSO, framebuffer, texture, sampler and program families do NOT come through here.
|
||||
// They were emitted from MG_Impl/Pipe/* by 40 direct calls to the 37 MGPipeApply* entry
|
||||
// points; those call sites now go through the two generated tables
|
||||
// (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
|
||||
#include <Includes.h>
|
||||
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include <MG_Pipe/MGPipeValueTypes.h>
|
||||
|
||||
namespace MobileGL::MG_Remote::Client {
|
||||
|
||||
@@ -114,4 +116,40 @@ namespace MobileGL::MG_Remote::Client {
|
||||
void SetDropClearEmissionForNegativeControl(Bool drop);
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
// 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
|
||||
// ReplyPool from that table - see PostReply. The answer is recorded here and read
|
||||
// through LastAcceptance() / Accepted+DeclinedRecords() until the integrator rules on
|
||||
// IT NOW RIDES THE REPLY SLOT, AND THAT IS THE RULING THIS COMMENT ASKED FOR. The text
|
||||
// that stood here said the answer could not ride a slot "until the integrator rules on
|
||||
// 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) {
|
||||
m_lastAcceptanceKnown = true;
|
||||
m_lastAcceptance = accepted;
|
||||
@@ -1244,6 +1260,10 @@ namespace MobileGL::MG_Remote::Wire {
|
||||
} else {
|
||||
++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) {
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SlotAllocator.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
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -101,17 +105,111 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
|
||||
EXPECT_EQ(ClassCount<kCtxVerb>(), 13u);
|
||||
}
|
||||
|
||||
// An uninstalled pipe is every entry null - which is exactly what "this subsystem has not
|
||||
// been migrated, keep pulling" means (plan B section 4.1).
|
||||
// A row nobody has migrated is null - which is exactly what "this subsystem has not been
|
||||
// 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) {
|
||||
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);
|
||||
for (SizeT i = 0; i < kMGPipeContextCallCount; ++i) {
|
||||
EXPECT_EQ(context[i], nullptr) << "context entry " << i;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
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
|
||||
|
||||
@@ -4802,6 +4802,93 @@ TEST(DirectGLESBufferDrawProbe, UnderSplitTheRecordAloneAnswersTheLiveHostMapQue
|
||||
#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
|
||||
// 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 -
|
||||
@@ -4936,4 +5023,8 @@ TEST(DirectGLESBufferDrawProbe, ALiveHostMapKeepsTheHandleArmProbeDirtyBetweenTw
|
||||
TEST(DirectGLESBufferDrawProbe, UnderSplitTheRecordAloneAnswersTheLiveHostMapQuestion) {
|
||||
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
|
||||
|
||||
@@ -231,6 +231,30 @@ namespace {
|
||||
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 handle{};
|
||||
handle.Slot = slot;
|
||||
@@ -543,12 +567,22 @@ TEST_F(PipeWireCodecTest, KNeedsAckRespecifyCarriesItsRedefinitionScope) {
|
||||
kInvalidSeq);
|
||||
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||
EXPECT_TRUE(applied);
|
||||
// Two records, two ACCEPTANCE answers - and NOT in a reply slot. Neither ResourceCreate
|
||||
// nor ResourceRespecify carries kReplySlot, and s1 sizes ReplyPool from that table, so a
|
||||
// reply written here would overwrite some waiter's slot.
|
||||
EXPECT_TRUE(wire.Answers().All.empty());
|
||||
// Two records, two ACCEPTANCE answers, AND THEY NOW RIDE THE REPLY SLOT (ID-31 + R-17).
|
||||
// Both rows carry kReplySlot in kMGPipeCallFlags, ReplyPool is sized from that same table,
|
||||
// and CONTRACT-P5 table 0 always said DECLINED "is how the four Bool acceptance entry
|
||||
// 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_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) {
|
||||
@@ -741,7 +775,12 @@ TEST_F(PipeWireCodecTest, ResourceSubDataCarriesABlobAndARegionTailTogether) {
|
||||
kInvalidSeq);
|
||||
ASSERT_TRUE(wire.PumpOne(&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
|
||||
// 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
|
||||
@@ -1198,11 +1237,24 @@ TEST_F(PipeWireCodecTest, NoReplyIsWrittenForARowTheCatalogueGivesNoReplySlot) {
|
||||
// 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
|
||||
// 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;
|
||||
ASSERT_EQ(MGPipeCallFlagsFor(MGPWireOp::ResourceCreate) & static_cast<Uint32>(kReplySlot), 0u);
|
||||
ASSERT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetTextureParams) & static_cast<Uint32>(kReplySlot), 0u);
|
||||
ASSERT_EQ(MGPipeCallFlagsFor(MGPWireOp::ResourceDestroy) & 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{};
|
||||
create.Resource = MakeHandle(131);
|
||||
create.Target = static_cast<Uint8>(MGPipeResourceTarget::Buffer);
|
||||
@@ -1216,23 +1268,63 @@ TEST_F(PipeWireCodecTest, NoReplyIsWrittenForARowTheCatalogueGivesNoReplySlot) {
|
||||
kInvalidSeq);
|
||||
bool applied = false;
|
||||
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(wire.Answers().All.empty()) << "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_TRUE(wire.Decoder().LastAcceptanceKnown());
|
||||
EXPECT_EQ(wire.Decoder().AcceptedRecords() + wire.Decoder().DeclinedRecords(), 1u);
|
||||
EXPECT_EQ(wire.Answers().All.size(), 1u) << "a reply slot was written for a kNone row";
|
||||
EXPECT_EQ(wire.Answers().All[0].Seq, 1u) << "the answer that exists is the create's";
|
||||
}
|
||||
|
||||
TEST_F(PipeWireCodecTest, EveryRowThatDoesWriteAReplyCarriesKReplySlot) {
|
||||
// The other half: the three rows in P5 that answer into a slot all carry the flag, so
|
||||
// PostReply's gate cannot be firing on any of them.
|
||||
for (const MGPWireOp op :
|
||||
{MGPWireOp::MapPersistent, MGPWireOp::ResourceReadback, MGPWireOp::ReadPixels}) {
|
||||
// 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. The four Bool acceptance rows joined
|
||||
// this list at ID-31 and R-17 - and they are listed BY NAME rather than derived from the
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
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, ¶ms, 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) {
|
||||
// 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,
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include "Includes.h"
|
||||
@@ -33,6 +34,7 @@
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Remote/CapsCodec.h>
|
||||
#include <MG_Remote/Client/CapsMirror.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_Remote/Client/EmitTables.h>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
@@ -400,6 +402,235 @@ TEST(RemoteEmitTable, TheE2DropSwitchStartsDisarmed) {
|
||||
<< "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) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
|
||||
@@ -152,6 +152,19 @@ class Call(object):
|
||||
"""(parameter declaration list, argument list) for this call."""
|
||||
params = ["const %s* payload" % self.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:
|
||||
params.append("const void* varTail")
|
||||
params.append("Uint32 varTailCount")
|
||||
|
||||
Reference in New Issue
Block a user