diff --git a/CMakeLists.txt b/CMakeLists.txt index 260b76b3..b029d34e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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. diff --git a/MobileGL/MG_Impl/Pipe/CsoCache.h b/MobileGL/MG_Impl/Pipe/CsoCache.h index 43a0886c..fb159353 100644 --- a/MobileGL/MG_Impl/Pipe/CsoCache.h +++ b/MobileGL/MG_Impl/Pipe/CsoCache.h @@ -44,6 +44,7 @@ #include #include #include +#include #include #include @@ -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(MGPipeKind::RenderStateCso); - MGPipeApplyDeleteRenderState(handle); + MGPipeRouteDeleteRenderState(handle); MGPipeSlots().Free(MGPipeKind::RenderStateCso, m_entries[index].Cso); m_entries[index] = m_entries.back(); m_entries.pop_back(); diff --git a/MobileGL/MG_Impl/Pipe/FramebufferEmit.h b/MobileGL/MG_Impl/Pipe/FramebufferEmit.h index 90bc294d..887ff886 100644 --- a/MobileGL/MG_Impl/Pipe/FramebufferEmit.h +++ b/MobileGL/MG_Impl/Pipe/FramebufferEmit.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -456,7 +457,7 @@ namespace MobileGL::MG_Pipe { m_lastDraw = state; if (state.Target == static_cast(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); diff --git a/MobileGL/MG_Impl/Pipe/ImageEmit.h b/MobileGL/MG_Impl/Pipe/ImageEmit.h index 3b1b2d7b..811143e5 100644 --- a/MobileGL/MG_Impl/Pipe/ImageEmit.h +++ b/MobileGL/MG_Impl/Pipe/ImageEmit.h @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -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); diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 83f43a17..d4fe5ec4 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include @@ -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 +#include #endif #include @@ -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(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(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(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; } diff --git a/MobileGL/MG_Impl/Pipe/ProgramEmit.h b/MobileGL/MG_Impl/Pipe/ProgramEmit.h index 2d0d29a3..ce3d51e7 100644 --- a/MobileGL/MG_Impl/Pipe/ProgramEmit.h +++ b/MobileGL/MG_Impl/Pipe/ProgramEmit.h @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -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(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(size)); m_constantsCso = cso; m_constantsVersion = version; ++m_constantSets; @@ -254,7 +260,7 @@ namespace MobileGL::MG_Pipe { m_lastDesc.Reflection.Offset = reinterpret_cast(&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 diff --git a/MobileGL/MG_Impl/Pipe/SamplerEmit.h b/MobileGL/MG_Impl/Pipe/SamplerEmit.h index 3870ebc0..21318fb7 100644 --- a/MobileGL/MG_Impl/Pipe/SamplerEmit.h +++ b/MobileGL/MG_Impl/Pipe/SamplerEmit.h @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -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(texture.GetViewNumLayers()); m_lastView.Samples = static_cast(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 diff --git a/MobileGL/MG_Impl/Pipe/TextureEmit.h b/MobileGL/MG_Impl/Pipe/TextureEmit.h index b973c3bd..46f9c9fb 100644 --- a/MobileGL/MG_Impl/Pipe/TextureEmit.h +++ b/MobileGL/MG_Impl/Pipe/TextureEmit.h @@ -57,6 +57,7 @@ #include #include #include +#include #include #include #include @@ -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(levelBytes), m_regions.empty() ? nullptr : m_regions.data()); } ++m_subDatas; diff --git a/MobileGL/MG_Impl/Pipe/VertexInputEmit.h b/MobileGL/MG_Impl/Pipe/VertexInputEmit.h index 1b40e47f..716fb81e 100644 --- a/MobileGL/MG_Impl/Pipe/VertexInputEmit.h +++ b/MobileGL/MG_Impl/Pipe/VertexInputEmit.h @@ -43,6 +43,7 @@ #include #include #include +#include #include #include @@ -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(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; diff --git a/MobileGL/MG_Pipe/PipeRoute.cpp b/MobileGL/MG_Pipe/PipeRoute.cpp new file mode 100644 index 00000000..ff67bbb0 --- /dev/null +++ b/MobileGL/MG_Pipe/PipeRoute.cpp @@ -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 + +#include +#include +#include + +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(slot.Id), + static_cast(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(slot.Id), + static_cast(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(static_cast(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(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(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(blobByteCount), + static_cast(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(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 diff --git a/MobileGL/MG_Pipe/PipeRoute.h b/MobileGL/MG_Pipe/PipeRoute.h new file mode 100644 index 00000000..5dee9a84 --- /dev/null +++ b/MobileGL/MG_Pipe/PipeRoute.h @@ -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`. There was no +// encode twin. Every `MG_Impl/Pipe` emitter called `MGPipeApply` 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, ""}`, 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 + +#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 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 diff --git a/MobileGL/MG_Pipe/generated/PipeTables.inc b/MobileGL/MG_Pipe/generated/PipeTables.inc index a5d4f966..f62d915a 100644 --- a/MobileGL/MG_Pipe/generated/PipeTables.inc +++ b/MobileGL/MG_Pipe/generated/PipeTables.inc @@ -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); diff --git a/MobileGL/MG_Pipe/generated/PipeThunks.inc b/MobileGL/MG_Pipe/generated/PipeThunks.inc index 09638e04..585e2469 100644 --- a/MobileGL/MG_Pipe/generated/PipeThunks.inc +++ b/MobileGL/MG_Pipe/generated/PipeThunks.inc @@ -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) { diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp index a3a2dfa1..f8b616c6 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp @@ -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(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(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(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(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(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(user)->Interval); - return MOBILEGL_OK; - } - - struct SurfaceArgs { - EGLSurface Surface; - }; - MobileGLResult RunReleaseSurface(void* user) { - ServerBackend()->ReleaseEGLSurface(static_cast(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(); } diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index d02072d6..42dad770 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -16,6 +16,7 @@ #include "../Server/ServerLoop.h" #include "../Server/ServerSession.h" #include "../Transport/InProcessTransport.h" +#include "WireTables.h" #include #include @@ -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 diff --git a/MobileGL/MG_Remote/Client/EmitTables.cpp b/MobileGL/MG_Remote/Client/EmitTables.cpp index af0c6e94..7f97dd73 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.cpp +++ b/MobileGL/MG_Remote/Client/EmitTables.cpp @@ -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(pack.Alignment) : 1ull; + const Uint64 rowBytes = static_cast(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(format), static_cast(type)); std::abort(); } + return static_cast(bytesPerPixel); + } - PixelStoreParameters pack{}; - if (MG_State::pGLContext != nullptr) { - pack = MG_State::pGLContext->GetPixelStoreParameters(/*isUnpack=*/false); - } - const Uint64 rowPixels = - pack.RowLength > 0 ? static_cast(pack.RowLength) : static_cast(width); - const Uint64 alignment = pack.Alignment > 0 ? static_cast(pack.Alignment) : 1ull; - const Uint64 rowBytes = rowPixels * static_cast(bytesPerPixel); - const Uint64 paddedRow = ((rowBytes + alignment - 1) / alignment) * alignment; - const Uint64 lastRow = static_cast(width) * static_cast(bytesPerPixel); - return paddedRow * (static_cast(height) - 1) + lastRow; + Uint64 TightReadbackBytes(GLsizei width, GLsizei height, GLenum format, GLenum type) { + if (width <= 0 || height <= 0) return 0; + return static_cast(width) * static_cast(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(width), static_cast(height), + static_cast(format), static_cast(bytes), + static_cast(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(width) * static_cast(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(bytes), - static_cast(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 bounce(static_cast(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(pack.RowLength) : static_cast(width); + const Uint64 alignment = pack.Alignment > 0 ? static_cast(pack.Alignment) : 1ull; + const Uint64 strideBytes = + ((rowPixels * bytesPerPixel + alignment - 1) / alignment) * alignment; + const Uint64 writtenPerRow = static_cast(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(pack.ImageHeight) : static_cast(height); + auto* out = static_cast(destination) + + static_cast(pack.SkipImages) * imageRows * strideBytes + + static_cast(pack.SkipRows) * strideBytes + + static_cast(pack.SkipPixels) * bytesPerPixel; + const auto* in = static_cast(tight); + for (Uint64 row = 0; row < static_cast(height); ++row) { + std::memcpy(out + row * strideBytes, in + row * writtenPerRow, + static_cast(writtenPerRow)); + } + } + } // namespace MobileGL::MG_Remote::Client diff --git a/MobileGL/MG_Remote/Client/EmitTables.h b/MobileGL/MG_Remote/Client/EmitTables.h index 0c8c51b1..ace528fe 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.h +++ b/MobileGL/MG_Remote/Client/EmitTables.h @@ -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 #include +#include 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 diff --git a/MobileGL/MG_Remote/Client/WireTables.cpp b/MobileGL/MG_Remote/Client/WireTables.cpp new file mode 100644 index 00000000..820959a5 --- /dev/null +++ b/MobileGL/MG_Remote/Client/WireTables.cpp @@ -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 +#include +#include +#include + +#include +#include + +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(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(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(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 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 diff --git a/MobileGL/MG_Remote/Client/WireTables.h b/MobileGL/MG_Remote/Client/WireTables.h new file mode 100644 index 00000000..d87c3eca --- /dev/null +++ b/MobileGL/MG_Remote/Client/WireTables.h @@ -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 + +#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 diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp index 0b13148c..5ba4709f 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp @@ -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) { diff --git a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp index a3ea0bb0..b3e6c0c7 100644 --- a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp @@ -25,6 +25,10 @@ #if MOBILEGL_PIPE_PUSH #include #include +// 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 #endif using namespace MobileGL; @@ -101,17 +105,111 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) { EXPECT_EQ(ClassCount(), 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(&gMGPipeScreen); - for (SizeT i = 0; i < kMGPipeScreenCallCount; ++i) { - EXPECT_EQ(screen[i], nullptr) << "screen entry " << i; - } const void* const* context = reinterpret_cast(&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(&gMGPipeScreen); + const void* const* context = reinterpret_cast(&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(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(kMGPipeCallCount) - 33u); + const void* const* escapes = reinterpret_cast(&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(kMGPipeCallCount)); +#endif } // The retirement ratchet of the migration carrier (section 6.3): the constant and the diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index 89dc505f..98da37b5 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -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(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(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 diff --git a/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp b/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp index e29613c1..f8eb11cc 100644 --- a/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp +++ b/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp @@ -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(kReplySlot), 0u); - ASSERT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetTextureParams) & static_cast(kReplySlot), 0u); + ASSERT_EQ(MGPipeCallFlagsFor(MGPWireOp::ResourceDestroy) & static_cast(kReplySlot), 0u) + << "ResourceDestroy gained a reply slot; this case needs a kNone row to mean anything"; + ASSERT_EQ(MGPipeCallFlagsFor(MGPWireOp::BindRenderState) & static_cast(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(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(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, diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index 1cf103fa..76f33738 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include "Includes.h" @@ -33,6 +34,7 @@ #include #include #include +#include #include #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 tight(static_cast(kW) * kH * kBpp); + for (size_t i = 0; i < tight.size(); ++i) tight[i] = static_cast(i); + + std::vector 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(kH); ++row) { + for (size_t byte = 0; byte < static_cast(kW) * kBpp; ++byte) { + EXPECT_EQ(destination[first + row * stride + byte], + tight[row * static_cast(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(kW) * kBpp && + ((i - first) / stride) < static_cast(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 tight(static_cast(kW) * kH * kBpp); + for (size_t i = 0; i < tight.size(); ++i) tight[i] = static_cast(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 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 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(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(MGPipeInstalledArm()), static_cast(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 = diff --git a/scripts/gen_pipe.py b/scripts/gen_pipe.py index 6d08114e..785850c5 100644 --- a/scripts/gen_pipe.py +++ b/scripts/gen_pipe.py @@ -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")