diff --git a/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp b/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp index 583479c0..f4396998 100644 --- a/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp +++ b/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp @@ -16,6 +16,11 @@ #include #include +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#include +#endif + namespace MobileGL::MG_Pipe { const char* MGPipeVerbName(MGPipeVerb verb) { const auto index = static_cast(verb); @@ -42,6 +47,132 @@ namespace MobileGL::MG_Pipe { return std::nullopt; } +#if MOBILEGL_BUILD_DISAGGREGATED + // ================================================================================ + // P5: the server's verb stamp, the residual-pull counter, and the four-way read verdict + // ================================================================================ + namespace { + Uint64 g_residualPulls = 0; + + // The strict arm of R-7.3. Same first line as the ordinary poison Fatal, so every + // existing filter on Fatal{UnmigratedPipeInput still matches, plus the class and the + // phase that retires it - a strict abort that did not say which phase owes the answer + // would leave the reader exactly where the gate found them. + [[noreturn]] void StrictBarrierPullFatal(MGPipeInputField field, MGPipeVerb verb) { + const SizeT index = static_cast(field); + MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, \"%s@%s\"} [BARRIER-PULLED, " + "MOBILEGL_IPC_STRICT_ERRORS=1, retires in %s]", + kMGPipeInputFieldNames[index], MGPipeVerbName(verb), kMGPipeFieldRetiringPhase[index]); + std::abort(); + } + + // One place decides what a BARRIER-PULLED read does, so the field accessors and the + // seven sticky forwards cannot drift apart on it. + void CountBarrierPull(MGPipeInputField field, MGPipeVerb verb) { + ++g_residualPulls; + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ResidualPulls, 1); + } + if (MG_Config::Ipc.StrictErrors) StrictBarrierPullFatal(field, verb); + } + + // The verb's OWN may-read table (FillPoints.def, kMGPipeClassFieldMask). The stamp + // respects it for the same reason the client's residual fill does: a field outside the + // verb's class is one the fill never copied, so answering it out of gPipeInputs would + // hand the server the PREVIOUS verb's value - the exact staleness the generation poison + // exists to catch, re-introduced by the very mechanism meant to instrument it. + Bool FieldIsInVerbClass(MGPipeInputField field, MGPipeVerb verb) { + const SizeT verbIndex = static_cast(verb); + if (verbIndex >= kMGPipeVerbCount) return false; + const MGPipeVerbClass verbClass = kMGPipeVerbClass[verbIndex]; + return MGPipeFieldMaskHas(kMGPipeClassFieldMask[static_cast(verbClass)], field); + } + } // namespace + + // The third door into the storage (see PipeInputs.h). It exists because neither of the + // other two can be the one that stamps: MGPipeApplyAccess deliberately does not, and + // MGPipeFillAccess lives in MG_Impl, the role a server does not have. + struct MGPipeStampAccess { + static MGPipeFilledState& Filled(PipeInputs& inputs) { return inputs.m_filled; } + static void SetVerb(PipeInputs& inputs, MGPipeVerb verb) { inputs.m_currentVerb = verb; } + static void SetServerStamped(PipeInputs& inputs, Bool stamped) { + inputs.m_serverStampedVerb = stamped; + } + }; + + void MGPipeServerStampVerbBoundary(MGPipeVerb verb) { + PipeInputs& inputs = gPipeInputs; + MGPipeFilledState& filled = MGPipeStampAccess::Filled(inputs); + MGPipeStampAccess::SetVerb(inputs, verb); + // Starts at 1 for MGPipeValidateForVerb's reason: FilledGen == 0 is "never filled" on + // BOTH branches of MGPipeInputFieldIsFresh, so the zeroing below is a real withdrawal + // rather than a stamp that happens to be old. + ++filled.CurrentVerbSerial; + for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) { + const auto field = static_cast(i); + const MGPipeFieldOwnership ownership = kMGPipeFieldOwnership[i]; + // STAMPED: in this verb's class AND answerable out of the records the applier has + // already applied. WITHDRAWN (0): everything else - which is BARRIER-PULLED, FATAL, + // and anything the verb's own may-read table says this verb does not read. + // + // The withdrawal is the load-bearing half of the rule: the client's residual fill + // stamped all 63 fields at its own verb boundary, so without it every field would + // read fresh on the server, `rsp` would be identically 0 and the exit gate would be + // decoration. It also cancels the sticky exemption for free - generated/ + // PipeFilled.inc tests "never filled" BEFORE it tests sticky, so 0 wins over + // kMGPipeInputFieldSticky without a line of the generated file changing. + const Bool answerable = (ownership == MGPipeFieldOwnership::kRecordSupplied || + ownership == MGPipeFieldOwnership::kApplierDerived) && + FieldIsInVerbClass(field, verb); + filled.FilledGen[i] = answerable ? filled.CurrentVerbSerial : 0; + } + MGPipeStampAccess::SetServerStamped(inputs, true); + } + + void MGPipeServerClearVerbBoundary() { MGPipeStampAccess::SetServerStamped(gPipeInputs, false); } + + Uint64 MGPipeResidualPullCount() { return g_residualPulls; } + void MGPipeResetResidualPullCountForTesting() { g_residualPulls = 0; } + + void MGPipeInputUnfreshRead(MGPipeInputField field, MGPipeVerb verb, Bool serverStamped) { + // OUTSIDE A SERVER-STAMPED VERB THIS IS THE MONOLITH ANSWER, UNCHANGED. A split BUILD + // running MOBILEGL_TRANSPORT=monolith - every unit and integration-gpu lane of + // build-split - has a client that stamped all 63 fields, so a stale read there is the + // same defect it is in a verify build. Softening it on the build rather than on the + // stamp would take 1842 unit cases' ability to go red away with it. + // + // AND A READ OUTSIDE THE VERB'S OWN CLASS IS STILL FATAL even for a BARRIER-PULLED + // field: the value it would be answered with was never copied for this verb, so + // counting it would trade a loud staleness for a quiet one. + if (!serverStamped || + kMGPipeFieldOwnership[static_cast(field)] != MGPipeFieldOwnership::kBarrierPulled || + !FieldIsInVerbClass(field, verb)) { + MGPipeInputPoisonFatalForVerb(field, verb); + } + CountBarrierPull(field, verb); + } + + void MGPipeInputArgumentRead(MGPipeInputField field, Uint32 arg0, MGPipeVerb verb, Bool serverStamped) { + if (!serverStamped) return; + const MGPipeFieldOwnership narrowed = MGPipeFieldOwnershipOf(field, arg0); + if (narrowed == MGPipeFieldOwnershipOf(field)) return; // the argument narrows nothing + if (narrowed == MGPipeFieldOwnership::kFatal) { + // The field's own stamp says fresh - the applier really did write the half that has + // a carrier - so only the argument can say that THIS read is unserved. + MGPipeInputPoisonFatalForVerb(field, verb); + } + if (narrowed == MGPipeFieldOwnership::kBarrierPulled) CountBarrierPull(field, verb); + } + + void MGPipeStickyForwardPull(MGPipeInputField field) { + // The seven carry no MGP_INPUT_CHECK at all (the declared exception argued at + // PipeInputs.h's F-class block), so freshness can never reach them and neither can the + // stamp's withdrawal. This is the only thing that puts them in `rsp`. + if (!gPipeInputs.ServerStampedVerb()) return; + CountBarrierPull(field, gPipeInputs.CurrentVerb()); + } +#endif // MOBILEGL_BUILD_DISAGGREGATED + #if MOBILEGL_PIPE_VERIFY namespace { using CurrentVertexAttributeValue = PipeInputs::CurrentVertexAttributeValue; diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 38317380..88be4a6b 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -1551,36 +1551,59 @@ namespace MobileGL::MG_Pipe { Bool PipeInputs::IsLive() const { return LiveContext() != nullptr; } // ---- the seven F-class forwarders ---- + // + // P5 (R-7.3, CONTRACT-P5.md table 2's "the seven sticky forwards"): each of them now opens + // with MGP_STICKY_FORWARD_PULL. They are THE SEVEN THAT HAND THE SERVER A RAW FRONTEND + // OBJECT OR WRITE INTO THE FRONTEND, and they are also the only fields the poison cannot + // see - they carry no MGP_INPUT_CHECK at all, by the declared exception argued at + // PipeInputs.h's F-class block, so freshness never reaches them and the exit gate was + // structurally blind on exactly the seven most dangerous rows. The hook is a no-op outside + // a server-stamped verb, so InvalidateCompileEnv keeps being reachable from backend + // initialisation - the case the exemption was written for - and every monolith lane, split + // build included, behaves as it does today. +#if MOBILEGL_BUILD_DISAGGREGATED +#define MGP_STICKY_FORWARD_PULL(Field) MGPipeStickyForwardPull(MGPipeInputField::Field) +#else +#define MGP_STICKY_FORWARD_PULL(Field) ((void)0) +#endif + SizeT PipeInputs::GetBufferBindingPointCount(BufferTarget target) const { + MGP_STICKY_FORWARD_PULL(GetBufferBindingPointCount); const auto* ctx = LiveContext(); return ctx != nullptr ? ctx->GetBufferBindingPointCount(target) : 0; } const SharedPtr& PipeInputs::GetProgramObject(Uint index) { + MGP_STICKY_FORWARD_PULL(GetProgramObject); auto* ctx = LiveContext(); return ctx != nullptr ? ctx->GetProgramObject(index) : NullShared(); } const SharedPtr& PipeInputs::GetTextureObject(Uint index) { + MGP_STICKY_FORWARD_PULL(GetTextureObject); auto* ctx = LiveContext(); return ctx != nullptr ? ctx->GetTextureObject(index) : NullShared(); } Bool PipeInputs::HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const { + MGP_STICKY_FORWARD_PULL(HasOpenTransformFeedbackSpan); const auto* ctx = LiveContext(); return ctx != nullptr && ctx->HasOpenTransformFeedbackSpan(lifetimeId); } void PipeInputs::InvalidateCompileEnv() { + MGP_STICKY_FORWARD_PULL(InvalidateCompileEnv); if (auto* ctx = LiveContext()) ctx->InvalidateCompileEnv(); } Bool PipeInputs::ValidateProgramName(Uint index) const { + MGP_STICKY_FORWARD_PULL(ValidateProgramName); const auto* ctx = LiveContext(); return ctx != nullptr && ctx->ValidateProgramName(index); } void PipeInputs::RecordError(ErrorCode code, UniquePtr info) { + MGP_STICKY_FORWARD_PULL(RecordError); auto* ctx = LiveContext(); if (ctx == nullptr) { MGLOG_E_ONCE("PipeInputs::RecordError: no live context, dropping error %d", static_cast(code)); @@ -1588,6 +1611,7 @@ namespace MobileGL::MG_Pipe { } ctx->RecordError(code, Move(info)); } +#undef MGP_STICKY_FORWARD_PULL // P3a D-H2.1. The draw's RAW vertex-fetch base instance, set immediately before the fill // at the three *BaseInstance draw entry points. It replaces the ambient process global @@ -1610,6 +1634,9 @@ namespace MobileGL::MG_Pipe { // Same bump the next fill would make, without a verb to fill from: no field is // stamped, so every stamp this verb made falls behind the serial. ++MGPipeFillAccess::Filled(inputs).CurrentVerbSerial; +#endif +#if MOBILEGL_BUILD_DISAGGREGATED + MGPipeServerClearVerbBoundary(); #endif MGPipeFillAccess::SetVerb(inputs, MGPipeVerb::kVerbCount); // The pending base instance belongs to the verb that was about to run, so leaving @@ -2382,6 +2409,14 @@ namespace MobileGL::MG_Pipe { // it on both branches, so a read before this first bump is // Fatal{UnmigratedPipeInput, "@"} rather than default storage. ++filled.CurrentVerbSerial; +#endif +#if MOBILEGL_BUILD_DISAGGREGATED + // The client is filling, so whatever the server stamped at its last verb boundary is + // withdrawn: the stamps below are the CLIENT's again and a stale read is a defect, not + // a residual pull. Disarming here rather than at the end of the applier's work is what + // makes the arming flag say "the current stamps are the server's" no matter which of + // the two roles ran last. + MGPipeServerClearVerbBoundary(); #endif MGPipeFillAccess::SetVerb(inputs, verb); auto* ctx = LiveContext(); diff --git a/MobileGL/MG_Util/Metrics/PipeStats.cpp b/MobileGL/MG_Util/Metrics/PipeStats.cpp index 92847bfd..bfa292cd 100644 --- a/MobileGL/MG_Util/Metrics/PipeStats.cpp +++ b/MobileGL/MG_Util/Metrics/PipeStats.cpp @@ -180,6 +180,7 @@ namespace MobileGL::MG_Util::PipeStats { "render-state-cso-mints", "render-state-cso-binds", "map-persistent-roundtrips", "framebuffer-emissions", "sampler-view-emissions", "sampler-state-emissions", "shader-image-emissions", "client-tex-upload-emissions", "tex-remint-pulls", + "residual-pulls", #endif }; const char* const kGateNames[kGateCount] = { @@ -457,6 +458,13 @@ namespace MobileGL::MG_Util::PipeStats { // Espryt had already allocated and then had to re-mint image-bindable, replaying its // levels from the client's shadow, because ImageBindableHint reached it too late. line += " trp=" + std::to_string(calls[static_cast(CallClass::TextureRemintPulls)]); + // rsp is P5's residual-pull count: reads, on the server side, of a PipeInputs field no + // pushed record supplies, answered out of the client's residual fill under the verb + // barrier. It is the SIZE OF THE P6/P7/P8 DEBT and it is published on this line rather + // than only at teardown because the number an operator needs is per frame: a debt that + // tracks the draw count is a pull inside a loop, and one that tracks the frame count is + // a pull per verb. Zero in every monolith lane by construction. + line += " rsp=" + std::to_string(calls[static_cast(CallClass::ResidualPulls)]); #endif line += "] gates["; for (Uint32 i = 0; i < kGateCount; ++i) { diff --git a/MobileGL/MG_Util/Metrics/PipeStats.h b/MobileGL/MG_Util/Metrics/PipeStats.h index fae4c201..79932baa 100644 --- a/MobileGL/MG_Util/Metrics/PipeStats.h +++ b/MobileGL/MG_Util/Metrics/PipeStats.h @@ -164,6 +164,21 @@ namespace MobileGL::MG_Util::PipeStats { // sync is allocated image-bindable up front and never counts. `trp=` on the summary // line; the number that decides MOBILEGL_PIPE_TEXEL_RETAIN_MB's default. TextureRemintPulls, + // P5's, and push-only for the same reason as the nine above. + // + // `rsp` - THE SIZE OF THE P6/P7/P8 DEBT. One per read, on the server side, of a + // BARRIER-PULLED PipeInputs field (CONTRACT-P5.md table 2): a field no pushed record + // supplies, which the server answers by reading the value the CLIENT's residual fill + // left in the single shared gPipeInputs while the verb barrier holds both threads + // apart. That is correct only because of the barrier, which is what makes the barrier + // load-bearing rather than cautious - so the count is the debt, and a phase that + // retires a family of fields is expected to move it down. + // + // Counted at the ONE place that decides what a stale read means + // (MG_Backend/MGPipe/PipeInputs.cpp), so the 56 checked accessors and the seven sticky + // forwards cannot drift apart on it. It is zero in every monolith lane by + // construction: nothing arms it but a server verb-boundary stamp. + ResidualPulls, #endif Count };