diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index 2961477e..2afe9416 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -6,31 +6,26 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -// P5 c0 stubs for package v1 (with p1 for the stamp rule). +// P5 package v1: the applier bridge, and the consumer for contract 7's five class-B verbs. #include "PipeApplier.h" #include "../Transport/ReplySlot.h" +#include +#include #include #include +#include namespace MobileGL::MG_Remote::Server { -#define MGP5_C0_STUB(what) \ - do { \ - MGLOG_F("MGPipe: Fatal{UnimplementedPipeApplier, \"%s\"} - P5 package v1 has not landed " \ - "this yet; c0 shipped the signature only", \ - what); \ - std::abort(); \ - } while (0) - ReplyPool::ReplyPool(void* base, Uint64 sizeBytes, Uint32 slotCount, Uint32 slotBytes) : m_base(static_cast(base)), m_size(sizeBytes), m_slots(slotCount), m_slotBytes(slotBytes) {} // PACKAGE s1's, not v1's, even though the class is declared in v1's header: the SEG_REPLY - // slot pool is s1's deliverable (BRIEF §5) and its addressing lives in one place, + // slot pool is s1's deliverable (BRIEF 5) and its addressing lives in one place, // Transport/ReplySlot.h, which the CLIENT reads the same slots back through. Duplicating // `seq % slots` on this side is how the two halves come to disagree about which slot an // answer is in - and because seq IS the reply-slot id (R-3), a disagreement reads another @@ -47,21 +42,324 @@ namespace MobileGL::MG_Remote::Server { Uint32 ReplyPool::SlotBytes() const { return m_slotBytes; } + // ----------------------------------------------------------------------------------- + // ServerVerbSink - the five class-B verbs + // ----------------------------------------------------------------------------------- + + void ServerVerbSink::SetBackend(MG_Backend::BackendObject* backend) { m_backend = backend; } + + const MG_Backend::GlobalBackendFunctionsTable* ServerVerbSink::Table(const char* verb) const { + if (m_backend == nullptr) { + // DECLINE BY NAME, DO NOT DEREFERENCE. A verb that arrives before + // ServerLoop::CreateBackend has run means the hook order changed under us, and the + // honest answer is "this build did not apply it" - which DecodeAndApply reports as + // false and the lane sees as a record that did not render, rather than as a crash + // with no line saying which verb was first. + MGLOG_E_ONCE("MG_Remote server: %s arrived with no backend object; the verb is " + "DECLINED. ServerLoop::CreateBackend runs from MG_Backend::Init()'s " + "hook, before ClientSession::Start", + verb); + return nullptr; + } + return &m_backend->GetBackendFunctions(); + } + + Bool ServerVerbSink::OnClear(const MG_Pipe::MGPClear& clear) { + const MG_Backend::GlobalBackendFunctionsTable* table = Table("clear"); + if (table == nullptr) return false; + const MG_Backend::GLFunctionsTable& gl = table->GL; + + // THE FBO HANDLE IS NOT RESOLVED HERE, AND THAT IS THE RULING RATHER THAN AN OMISSION. + // MGPClear::Fbo names the framebuffer the clear belongs to, but the BINDING is already + // server state: set_framebuffer_state (op 33) arrives ahead of the clear and the + // applier has bound it. Re-resolving the handle to a frontend FramebufferObject here + // would need the SharedPtr the four ClearNamedFramebuffer* entries take - a frontend + // heap reference that table 2 lists as one of the six fields with no wire carrier. So + // P5 clears THE BOUND FRAMEBUFFER, which for the reduced path (default FBO) is exactly + // right, and the named form is P7's along with the handle it needs. + switch (clear.Kind) { + case kMGPClearKindWhole: + if (gl.Clear == nullptr) return false; + gl.Clear(static_cast(clear.BufferMask)); + break; + case kMGPClearKindColor: + switch (clear.ValueClass) { + case kMGPClearValueClassFloat: + if (gl.ClearBufferfv == nullptr) return false; + gl.ClearBufferfv(GL_COLOR, clear.DrawBufferIndex, + reinterpret_cast(clear.ColorValue)); + break; + case kMGPClearValueClassInt: + if (gl.ClearBufferiv == nullptr) return false; + gl.ClearBufferiv(GL_COLOR, clear.DrawBufferIndex, + reinterpret_cast(clear.ColorValue)); + break; + case kMGPClearValueClassUint: + if (gl.ClearBufferuiv == nullptr) return false; + gl.ClearBufferuiv(GL_COLOR, clear.DrawBufferIndex, + reinterpret_cast(clear.ColorValue)); + break; + default: + // A value class outside the three is a wire fault, not a fallback: all three + // representations of a clear colour are numerically populated by the frontend + // and only this field says which one the backend must use, so guessing renders + // a plausible wrong colour. + Wire::WireProtocolFatalAt("MGPClear::ValueClass", clear.ValueClass, 3); + } + break; + case kMGPClearKindDepth: + if (gl.ClearBufferfv == nullptr) return false; + gl.ClearBufferfv(GL_DEPTH, 0, &clear.DepthValue); + break; + case kMGPClearKindStencil: + if (gl.ClearBufferiv == nullptr) return false; + gl.ClearBufferiv(GL_STENCIL, 0, &clear.StencilValue); + break; + case kMGPClearKindDepthStencil: + if (gl.ClearBufferfi == nullptr) return false; + gl.ClearBufferfi(GL_DEPTH_STENCIL, 0, clear.DepthValue, clear.StencilValue); + break; + default: + Wire::WireProtocolFatalAt("MGPClear::Kind", clear.Kind, kMGPClearKindDepthStencil + 1); + } + ++m_clears; + return true; + } + + Bool ServerVerbSink::OnBlit(const MG_Pipe::MGPBlit& blit) { + const MG_Backend::GlobalBackendFunctionsTable* table = Table("blit"); + if (table == nullptr) return false; + if (table->GL.BlitFramebuffer == nullptr) return false; + // Same ruling as OnClear's: the read and draw framebuffers are already bound by the + // set_framebuffer_state records that preceded this one, so the unnamed entry point is + // the one that matches what the server's state actually is. BlitNamedFramebuffer needs + // two frontend SharedPtrs, which table 2 lists as uncarried. + table->GL.BlitFramebuffer(blit.SrcX0, blit.SrcY0, blit.SrcX1, blit.SrcY1, blit.DstX0, + blit.DstY0, blit.DstX1, blit.DstY1, + static_cast(blit.Mask), + static_cast(blit.Filter)); + ++m_blits; + return true; + } + + Bool ServerVerbSink::OnPresent(const MG_Pipe::MGPPresent& present) { + const MG_Backend::GlobalBackendFunctionsTable* table = Table("present"); + if (table == nullptr) return false; + if (table->Present == nullptr) return false; + // Present is the ONLY frame-boundary drain the backend has (DirectGLES.cpp:12424-12470: + // the fence poll, the four ring OnPresent hooks, TrimBufferPool, PipeStats::OnPresent), + // which is why ARCHITECTURE.md:531 insists present <-> eglSwapBuffers stays strictly + // 1:1. It is NOT eglSwapBuffers itself: the swap is the client's EGL call and crosses + // as the SwapEGLBuffers control request, which runs Present on this thread through this + // same table. Both paths therefore end here and the 1:1 is structural. + table->Present(); + ++m_presents; + // FrameSerial 0 means "the server stamps its own" (c1-v1 8.3): P5 has no client-side + // present credit, so the client sends 0 and the frame count on this side IS the serial. + m_lastPresentSerial = present.FrameSerial != 0 ? present.FrameSerial : m_presents; + return true; + } + + Bool ServerVerbSink::OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq, + Wire::ReplySink* replies) { + if (replies == nullptr) { + // The decoder always passes its ReplySink; a null one means the applier was built + // without a reply pool, and answering nothing would leave the client's barrier + // waiting for a slot that never gets stamped - a hang, not a wrong picture. + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"read_pixels without a reply sink\"} - " + "the pixels' only destination in P5 is SEG_REPLY (contract table 1 row 23) " + "and a client blocked on seq %llu would never be answered", + static_cast(seq)); + std::abort(); + } + const MG_Backend::GlobalBackendFunctionsTable* table = Table("read_pixels"); + if (table == nullptr || table->GL.ReadPixels == nullptr) { + // DECLINED IS A REAL ANSWER (table 0's slot-header row) and it is the RIGHT one + // here: the client is parked on this seq inside the verb barrier, so returning + // false without posting would convert "not implemented" into "never returns". + replies->PostReply(seq, Wire::ReplySink::kStatusDeclined, nullptr, 0); + return false; + } + if (info.DstSize == 0) { + replies->PostReply(seq, Wire::ReplySink::kStatusError, nullptr, 0); + return false; + } + // THE CLIENT DECLARES THE BYTE COUNT AND THE SERVER DOES NOT RECOMPUTE IT. DstSize is + // sized on the client from the same GL_PACK_* state the frontend owns, and it is what + // the client will read back out of the slot; a server that recomputed from Box x + // Format x Type would be a SECOND opinion about a pack alignment the client half owns, + // and the two disagreeing is a short read with plausible pixels in it. + if (info.DstSize > m_readbackScratch.size()) { + m_readbackScratch.resize(static_cast(info.DstSize)); + } + table->GL.ReadPixels(info.Box.X, info.Box.Y, static_cast(info.Box.W), + static_cast(info.Box.H), static_cast(info.Format), + static_cast(info.Type), m_readbackScratch.data()); + replies->PostReply(seq, Wire::ReplySink::kStatusOk, m_readbackScratch.data(), info.DstSize); + m_readbackBytes += info.DstSize; + ++m_readbacks; + return true; + } + + Bool ServerVerbSink::OnDrawVbo(const MG_Pipe::MGPDrawInfo& info, + const MG_Pipe::MGPDrawRange* ranges, + const MG_Pipe::MGHostSpan* userIndices) { + const MG_Backend::GlobalBackendFunctionsTable* table = Table("draw_vbo"); + if (table == nullptr) return false; + if (userIndices != nullptr) { + // kCapNeedsHostIndexBytes is 0 for the whole of P5 by ruling (table 0's cap-bit + // row) precisely so this tail never appears; a span that arrived anyway means the + // client's cap gate did not hold, and filling one is P8's. + MGLOG_E_ONCE("MG_Remote server: draw_vbo carries an MGHostSpan of user indices. P5 " + "rules kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes to 0 so that " + "no host span reaches the first IPC frame (contract table 0); filling " + "one under split is P8's. The draw is DECLINED rather than drawn from " + "a pointer that does not belong to this process"); + return false; + } + if (ranges == nullptr || info.NumDraws == 0) return false; + + // P5 IMPLEMENTS THE TWO SHAPES ITS REDUCED PATH USES AND DECLINES THE REST BY NAME. + // draw_vbo collapses all twenty draw entry points, and picking the right one needs the + // instancing / base-vertex / base-instance / multi-draw cross product. TriangleScenario + // is a single non-instanced array draw and OpenRA's are single indexed draws from a + // bound element buffer; the rest are P8's, together with the MGPDrawIndirect record + // that has no producer yet. + const MG_Backend::GLFunctionsTable& gl = table->GL; + const Bool instanced = info.InstanceCount > 1 || info.StartInstance != 0; + if (info.NumDraws != 1 || instanced) { + MGLOG_E_ONCE("MG_Remote server: draw_vbo with NumDraws=%u InstanceCount=%u " + "StartInstance=%u is DECLINED - P5's reduced path is the single " + "non-instanced draw (BRIEF 4); the multi-draw and instanced arms are " + "P8's", + info.NumDraws, info.InstanceCount, info.StartInstance); + return false; + } + const MG_Pipe::MGPDrawRange& range = ranges[0]; + if (info.IndexSize == 0) { + if (gl.DrawArrays == nullptr) return false; + gl.DrawArrays(static_cast(info.Mode), static_cast(range.Start), + static_cast(range.Count)); + } else { + if (gl.DrawElementsBaseVertex == nullptr) return false; + GLenum indexType = GL_UNSIGNED_INT; + switch (info.IndexSize) { + case 1: indexType = GL_UNSIGNED_BYTE; break; + case 2: indexType = GL_UNSIGNED_SHORT; break; + case 4: indexType = GL_UNSIGNED_INT; break; + default: + // IndexSize is "0 = arrays, else 1 / 2 / 4" (MGPipeTypes.h:1323) and nothing + // else is a legal width; defaulting to 4 would read past the element buffer. + Wire::WireProtocolFatalAt("MGPDrawInfo::IndexSize", info.IndexSize, 4); + } + // Start is the FIRST INDEX, so the byte offset into the bound element buffer is + // Start * IndexSize - the same arithmetic PipeFill's emitter inverted. + const auto offset = static_cast(range.Start) * info.IndexSize; + gl.DrawElementsBaseVertex(static_cast(info.Mode), + static_cast(range.Count), indexType, + reinterpret_cast(offset), range.IndexBias); + } + ++m_draws; + return true; + } + + // ----------------------------------------------------------------------------------- + // PipeApplier + // ----------------------------------------------------------------------------------- + PipeApplier::PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies) : m_segments(segments), m_replies(replies) {} - Bool PipeApplier::ApplyOne(const Transport::RingRecordView&) { MGP5_C0_STUB("PipeApplier::ApplyOne"); } - - void PipeApplier::StampVerbBoundary(MG_Pipe::MGPWireOp) { - MGP5_C0_STUB("PipeApplier::StampVerbBoundary"); + void PipeApplier::Attach(Transport::RingControl* control, MG_Backend::BackendObject* backend) { + if (control == nullptr || m_segments == nullptr) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"PipeApplier::Attach\"} - no control " + "page or no segment table; ServerSession::Accept builds both before the " + "apply thread starts"); + std::abort(); + } + m_verbs.SetBackend(backend); + m_decoder = Wire::PipeWireDecoder(control, m_segments, m_replies); + m_decoder.SetVerbSink(&m_verbs); + m_attached = true; } - Uint64 PipeApplier::ResidualPullCount() const { return m_residualPulls; } - - void PipeApplier::PoisonRetiredStageBytes(Uint64, Uint64) { - MGP5_C0_STUB("PipeApplier::PoisonRetiredStageBytes"); + void PipeApplier::Detach() { + m_decoder = Wire::PipeWireDecoder(); + m_verbs.SetBackend(nullptr); + m_attached = false; } -#undef MGP5_C0_STUB + Bool PipeApplier::Attached() const { return m_attached; } + + Bool PipeApplier::ApplyOne(const Transport::RingRecordView& record) { + if (!m_attached) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"PipeApplier::ApplyOne before Attach\"} " + "- a record reached the applier with no decoder; the apply thread calls " + "Attach once before its first pop"); + std::abort(); + } + // ORDER IS THE CONTRACT'S: stamp, then apply. The stamp is what makes any server-side + // read of gPipeInputs legal at all (PipeApplier.h's block 1), so a record applied + // before it aborts on the FIRST field inside SyncRenderState. + StampVerbBoundary(static_cast(record.kind)); + const Bool applied = m_decoder.DecodeAndApply(record); + // AND THE CLEAR IS INSIDE ApplyOne, NOT AFTER THE DRAIN BATCH. That is not tidiness, + // it is the barrier invariant. s1's SessionConsumer::ApplyOne publishes appliedSeq the + // instant this returns, and publishing appliedSeq is what makes the CLIENT runnable + // again (R-1: the barrier waits on exactly that watermark). A clear that ran after the + // batch would therefore be a second writer of gPipeInputs while the client is already + // touching it - the one thing table 3 says may not be introduced before the barrier + // retires - and the first version of this file had it there. It was caught by + // AClearRecordCrossesAndIsStampedAsAVerbBoundary failing INTERMITTENTLY, which is what + // a race looks like from the outside. + // + // THE COST, STATED: a record that is NOT a verb boundary now applies with the flag + // disarmed, so a sticky forward pulled from inside such a record's applier is not + // counted in `rsp`. Closing that needs an "enter the applier" entry point beside + // MGPipeServerStampVerbBoundary that arms the flag WITHOUT re-stamping - re-stamping on + // a non-verb op is what p1 forbids outright - and PipeInputs.cpp is p1's file. Left for + // the integrator to sequence; it makes `rsp` larger, never smaller, so the number this + // phase reports is a floor. + LeaveApplier(); + return applied; + } + + // p1's rule verbatim (p1-v1 2). MGPipeVerbForWireOp is generated from MGP_VERB_OP_LIST in + // FieldOwnership.def and answers kVerbCount for every op that is NOT a verb boundary, so + // calling it unconditionally on every record is both correct and cheap. Four ops stamp: + // Clear -> Clear, DrawVbo -> DrawArrays, ReadPixels -> ReadPixels, Blit -> BlitFramebuffer. + // + // PRESENT IS DELIBERATELY NOT ONE, although contract 7 puts it in class B: FillPoints.def:21 + // says Present and SetSwapInterval "go through BackendObject virtuals and read no frontend + // state, so they are not verbs here". There is no MGPipeVerb::Present, and stamping there + // would retire the previous verb's answers with nothing to put in their place. + void PipeApplier::StampVerbBoundary(MG_Pipe::MGPWireOp op) { + const MG_Pipe::MGPipeVerb verb = MG_Pipe::MGPipeVerbForWireOp(op); + if (verb == MG_Pipe::MGPipeVerb::kVerbCount) return; // not a verb boundary: stamp nothing + MG_Pipe::MGPipeServerStampVerbBoundary(verb); + } + + void PipeApplier::LeaveApplier() { MG_Pipe::MGPipeServerClearVerbBoundary(); } + + Uint64 PipeApplier::ResidualPullCount() const { return MG_Pipe::MGPipeResidualPullCount(); } + + // The decoder poisons EXACTLY the runs it resolved, from inside DecodeAndApply, once the + // applier has returned - so this entry point is the manual one, for a caller that knows a + // range is dead and is not the decoder. It is kept because c0's signature block declares + // it and because the R-11 copy in Managers.cpp is verified by poisoning a range by hand in + // a unit case; nothing on the live path calls it. + void PipeApplier::PoisonRetiredStageBytes(Uint64 offset, Uint64 size) { + if (size == 0 || m_segments == nullptr) return; +#if MOBILEGL_BUILD_DISAGGREGATED + if (!MG_Config::Ipc.Audit) return; +#endif + const void* run = m_segments->Resolve(Wire::kSegStage, offset, size); + if (run == nullptr) return; + std::memset(const_cast(run), 0xDD, static_cast(size)); + } + + Uint64 PipeApplier::PoisonedStageBytes() const { return m_decoder.PoisonedStageBytes(); } + + Uint64 PipeApplier::DecoderAppliedSeq() const { return m_decoder.AppliedSeq(); } } // namespace MobileGL::MG_Remote::Server diff --git a/MobileGL/MG_Remote/Server/PipeApplier.h b/MobileGL/MG_Remote/Server/PipeApplier.h index 48401b3c..c11ca7e5 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.h +++ b/MobileGL/MG_Remote/Server/PipeApplier.h @@ -40,6 +40,7 @@ #pragma once #include +#include #include #include "../Transport/Ring.h" @@ -69,34 +70,147 @@ namespace MobileGL::MG_Remote::Server { Uint32 m_slotBytes = 0; }; + // ---- MGPClear's two discriminants --------------------------------------------------- + // + // MGPClear (MGPipeTypes.h:1271) names `Kind` "Whole | Color | Depth | Stencil | + // DepthStencil" and `ValueClass` "Float | Int | Uint" IN A COMMENT AND NOWHERE ELSE: the + // catalogue ships no enum for either, and the record has no producer or consumer in the + // tree, so P5 writes both halves and the two halves have to agree on a number. Declaring + // them here rather than open-coding 0..4 on each side is table 0's own rule for exactly + // this shape ("a decoder that open-codes it is the class-1 defect"), applied to a field + // table 0 did not reach. + // + // THE ORDER IS THE COMMENT'S, LEFT TO RIGHT, and ValueClass reuses the numbering + // MG_State/GLState/Core.h:39-41 already gives the identical three-way split on + // MGPAttribValue::ValueClass. c1 encodes against these constants; a disagreement is a + // clear of the wrong attachment with the wrong value type, which renders plausibly. + // FLAGGED FOR THE INTEGRATOR: this belongs in MGPipeTypes.h, which is c0's file. + inline constexpr Uint32 kMGPClearKindWhole = 0; // glClear(mask) + inline constexpr Uint32 kMGPClearKindColor = 1; // glClearBuffer{f,i,ui}v(GL_COLOR, i, v) + inline constexpr Uint32 kMGPClearKindDepth = 2; // glClearBufferfv(GL_DEPTH, 0, &d) + inline constexpr Uint32 kMGPClearKindStencil = 3; // glClearBufferiv(GL_STENCIL, 0, &s) + inline constexpr Uint32 kMGPClearKindDepthStencil = 4; // glClearBufferfi(GL_DEPTH_STENCIL,...) + inline constexpr Uint32 kMGPClearValueClassFloat = 0; + inline constexpr Uint32 kMGPClearValueClassInt = 1; + inline constexpr Uint32 kMGPClearValueClassUint = 2; + + // ---- the five class-B verbs' consumer ------------------------------------------------ + // + // Contract §7 class B is Clear (57), Blit (56), ReadPixels (58), DrawVbo (59) and Present + // (67), and NONE of them has an MGPipeApply* entry point - the 37 that exist are the object + // and state families. So w1's decoder validates and hands over a checked argument list and + // stops, and this is the other half: the SERVER'S OWN BACKEND CALL, through the private + // GlobalBackendFunctionsTable ServerLoop holds. It is not gBackendFunctionsTable, which in + // a split process is the client's emit table (table 3) - calling THAT here would re-emit + // the record the server is in the middle of applying, which is an infinite loop that + // renders nothing and looks like a hang. + class ServerVerbSink final : public Wire::WireVerbSink { + public: + // The server's private backend. Null until ServerLoop::CreateBackend has run, and a + // verb that arrives before then declines by name rather than dereferencing. + void SetBackend(MG_Backend::BackendObject* backend); + + Bool OnClear(const MG_Pipe::MGPClear& clear) override; + Bool OnBlit(const MG_Pipe::MGPBlit& blit) override; + Bool OnPresent(const MG_Pipe::MGPPresent& present) override; + Bool OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq, + Wire::ReplySink* replies) override; + Bool OnDrawVbo(const MG_Pipe::MGPDrawInfo& info, const MG_Pipe::MGPDrawRange* ranges, + const MG_Pipe::MGHostSpan* userIndices) override; + + // Per-verb tallies. The lane asserts these moved, because "the scenario passed" on a + // split build is also what a scenario that ran entirely on the monolith path looks + // like (R-16: a probe may not arm against a stub). + Uint64 Clears() const { return m_clears; } + Uint64 Draws() const { return m_draws; } + Uint64 Readbacks() const { return m_readbacks; } + Uint64 Blits() const { return m_blits; } + Uint64 Presents() const { return m_presents; } + Uint64 LastPresentSerial() const { return m_lastPresentSerial; } + Uint64 ReadbackBytes() const { return m_readbackBytes; } + + private: + const MG_Backend::GlobalBackendFunctionsTable* Table(const char* verb) const; + + MG_Backend::BackendObject* m_backend = nullptr; + Uint64 m_clears = 0; + Uint64 m_draws = 0; + Uint64 m_readbacks = 0; + Uint64 m_blits = 0; + Uint64 m_presents = 0; + Uint64 m_lastPresentSerial = 0; + Uint64 m_readbackBytes = 0; + // ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable:: + // ReadPixels writes into a caller buffer, so one staging vector per session sits + // between them. Grown, never shrunk, and never handed out past the call. + Vector m_readbackScratch; + }; + class PipeApplier { public: PipeApplier() = default; PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies); - // Decode one record, stamp the verb, apply, post the reply if the call has one, then - // advance appliedSeq by exactly one. P5 FORBIDS BATCHING appliedSeq (R-9): the barrier's - // waiter reads it, and a batched watermark promises work that has not run. + // Builds the decoder over the session's control page and points it at this applier's + // verb sink. Separate from the constructor because ServerSession::Accept constructs the + // applier before it has decided anything about the apply thread, and the decoder needs + // the RingControl the constructor was never given. + // + // CALLED ON THE APPLY THREAD, ONCE, BEFORE THE FIRST RECORD. PipeWireDecoder is "not + // thread safe: one decoder on the apply thread, by construction", and its constructor + // installs the process-wide apply hook. + void Attach(Transport::RingControl* control, MG_Backend::BackendObject* backend); + void Detach(); + Bool Attached() const; + + // Decode one record, stamp the verb, apply, post the reply if the call has one. THE + // CALLER advances appliedSeq by exactly one, through s1's SessionConsumer::ApplyOne, + // which is that watermark's single writer; P5 FORBIDS BATCHING it (R-9), because the + // barrier's waiter reads it and a batched watermark promises work that has not run. Bool ApplyOne(const Transport::RingRecordView& record); // p1's rule, v1's call site. Called at the verb boundary, before the record's applier // runs, with the verb the record belongs to. void StampVerbBoundary(MG_Pipe::MGPWireOp op); + // MANDATORY on leaving the applier (p1's M-5, PipeInputs.h's ServerStampedVerb block). + // The client's MGPipeValidateForVerb / MGPipeLeaveVerb also clear it, which is enough + // for inproc and NOT enough for a spawned server, where MG_Impl is not in the process: + // there the flag would latch TRUE for the server's life, every later read anywhere + // would be judged against the last verb's mask, and the sticky forwards would start + // aborting under strict on exactly the case their exemption exists for. + void LeaveApplier(); + // R-7.2's counter, read by the gate. A BARRIER-PULLED field read on the server side // increments PipeStats::CallClass::ResidualPulls (short name `rsp`); its value at the // end of P5 IS the size of the P6/P7/P8 debt and goes into MEASUREMENTS. + // + // IT FORWARDS TO MGPipeResidualPullCount() AND KEEPS NO MEMBER OF ITS OWN. The member + // c0's signature block declared is deleted rather than wired: the counter is + // process-wide in PipeInputs.cpp because the reads that increment it happen inside the + // BACKEND, arbitrarily deep under an MGPipeApply* call, with no PipeApplier in scope. + // A second copy here could only ever be a number that disagreed with the one the exit + // gate reads (p1-v1 5). Uint64 ResidualPullCount() const; // R-11's audit: after a record retires, fill the SEG_STAGE bytes it referenced with // 0xDD. Only under MOBILEGL_IPC_AUDIT=1, because it costs a write of every staged byte. void PoisonRetiredStageBytes(Uint64 offset, Uint64 size); + // How many staged bytes the decoder has poisoned, and how many records it has applied - + // the two numbers the audit lane asserts are non-zero, since an instrumentation that + // cannot be observed to have run is decoration. + Uint64 PoisonedStageBytes() const; + Uint64 DecoderAppliedSeq() const; + ServerVerbSink& Verbs() { return m_verbs; } + const ServerVerbSink& Verbs() const { return m_verbs; } + private: Wire::SegmentTable* m_segments = nullptr; ReplyPool* m_replies = nullptr; Wire::PipeWireDecoder m_decoder; - Uint64 m_residualPulls = 0; + ServerVerbSink m_verbs; + Bool m_attached = false; }; } // namespace MobileGL::MG_Remote::Server