From 7dec32a57423b6501dc56165fcc2df2550d086ca Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 6 Sep 2026 08:44:58 -0400 Subject: [PATCH] [Feat] (Pipe): carry what has no call of its own in the residual value block and abort when it disagrees with the assembled state - set_residual_value_state emits the 35 capability bits, read from the FRONTEND's own IsCapabilityEnabled rather than from the assembled mirror. That direction is the whole design: the applier then compares the carried answer against the assembled one, so the block is an independent oracle instead of a tautology - which is the failure the P1 entry compare had and P2 is paying to remove. - It goes out AFTER the residual fill, not with the other emissions: the mirror the trip wire compares against is written either by the applier's derivation or by that fill, so before it the block would be compared against the previous verb's answer. - It is HELD, not dropped, when the verb's class does not carry IsCapabilityEnabled. kQuery and kXfbSpan do not read it, so at those verbs the mirror is stale by construction; a capability that moved between two queries would silently disarm the wire if the emission were skipped instead of deferred. - ByteClass::ResidualValueBlock has been a placeholder that "stays at 0 until P2" since P0. This makes it non-zero, which is half of G10. - The wire is not theoretical: the first version of this commit fired it for real - Fatal{PipeResidualDiverged, "Dither"} carried=1 assembled=0, on every verb whose class does not read the capability mirror - and that is what the holding latch above is for. GL_DITHER defaults to enabled, so the very first mismatch the block could have found is the one it found. - integration-verify 818 green, integration-gpu 878 green under the default bitmask and again under MOBILEGL_PIPE_PUSH=0, unit 1499 green. --- MobileGL/MG_Impl/Pipe/PipeFill.cpp | 76 +++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 4cd43fca..efb1fb71 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -784,6 +784,48 @@ namespace MobileGL::MG_Pipe { return sizeof(MGPVertexAttribDefaults) + header.Count * sizeof(MGPAttribValue); } + + // set_residual_value_state (P2 brief D9, ARCHITECTURE.md 9.4). + // + // Since P2 the block is one Uint64 of capability bits, and every one of the 35 is + // ALSO answerable from the assembled working block now that the contract closed the + // FramebufferSrgb / DepthClamp / TextureCubeMapSeamless storage holes. That + // redundancy is the whole point: the bits are read HERE from the frontend, and the + // applier compares them against the assembled answer, so the day a later call takes + // a capability over and forgets to carry it the block says so on the next draw + // (Fatal{PipeResidualDiverged, ""}). + // + // Building the carried bits from the ASSEMBLED block instead would make the trip + // wire a tautology, which is exactly the failure P1's entry compare had and P2 is + // paying to remove. + // + // Emitted once per context and again whenever the capability set may have moved, + // which is whenever the pipeline version moved: every SET_CAPABILITY arm calls + // BumpVersions, so that shutter cannot miss one. + Uint64 EmitResidualValueState(GLContext& ctx) { + ResidualValueBlock block{}; + constexpr SizeT kCapabilityCount = static_cast(CapabilityInput::CapabilityInputCount); + static_assert(kCapabilityCount <= 64, "CapabilityBits is a Uint64"); + for (SizeT i = 0; i < kCapabilityCount; ++i) { + if (ctx.IsCapabilityEnabled(static_cast(i))) { + block.CapabilityBits |= Uint64{1} << i; + } + } + MGPipeApplySetResidualValueState(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. + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::ResidualValueBlock, + sizeof(ResidualValueBlock)); + } + return sizeof(MGPResidualValueState) + sizeof(ResidualValueBlock); + } + + // Set when the capability set may have moved, cleared when the block goes out. It is + // not part of the tracker because it is emission state, not a shutter: the shutter + // (the pipeline version) has already been consumed by the time this is read. + Bool g_residualDue = true; + constexpr Uint32 kAllDynamicChunks = static_cast((Uint64{1} << kMGPipeDynamicChunkCount) - 1); @@ -903,13 +945,6 @@ namespace MobileGL::MG_Pipe { (dirty & MGPipeDirtyBit(MGPipeDirty::NewVertexAttribDefaults)) != 0) { payloadBytes += EmitVertexAttribDefaults(*ctx); } - if (payloadBytes != 0 && MG_Util::PipeStats::Enabled()) { - // PipeStats::RecordDrawPayloadBytes has been implemented and unit-tested since - // P0 and called by nothing; this is its first emitter, and the 24-bucket - // histogram is what answers ROADMAP.md open question 4's chunk-granularity - // retune with data instead of a guess. - MG_Util::PipeStats::RecordDrawPayloadBytes(payloadBytes); - } // ---- step 4: the residual fill, for what an emitted call did NOT supply ---- const Bool applierDerives = ApplierDerivesRenderStateFields(); @@ -943,6 +978,33 @@ namespace MobileGL::MG_Pipe { if (!IsOmitted(verb, field)) filled.FilledGen[i] = filled.CurrentVerbSerial; #endif } + // ---- step 4b: the residual value block, and it goes out HERE ---- + // Its trip wire compares the carried bits against the ASSEMBLED capability mirror, + // and that mirror is written either by the applier's derivation or by the fill loop + // above - so the block is only meaningful once step 4 has run. Emitting it with the + // other calls would compare against the previous verb's answer. + if ((pushMask & kMGPipeSubsystemResidualValues) != 0) { + if (tracker.FreshlyPrimed() || (dirty & MGPipeDirtyBit(MGPipeDirty::NewPipelineState)) != 0) { + g_residualDue = true; + } + // The trip wire compares against the ASSEMBLED capability mirror, so it can only + // run at a verb whose class actually carries that mirror - IsCapabilityEnabled is + // in seven of the nine class masks and kQuery and kXfbSpan do not read it, so at + // those verbs the mirror is whatever the last verb that did read it left behind. + // The change is HELD rather than dropped: dropping it would silently disarm the + // wire for a capability that moved between two queries. + if (g_residualDue && MGPipeFieldMaskHas(mask, MGPipeInputField::IsCapabilityEnabled)) { + payloadBytes += EmitResidualValueState(*ctx); + g_residualDue = false; + } + } + if (payloadBytes != 0 && MG_Util::PipeStats::Enabled()) { + // PipeStats::RecordDrawPayloadBytes has been implemented and unit-tested since + // P0 and called by nothing; this is its first emitter, and the 24-bucket + // histogram is what answers ROADMAP.md open question 4's chunk-granularity + // retune with data instead of a guess. + MG_Util::PipeStats::RecordDrawPayloadBytes(payloadBytes); + } #if MOBILEGL_PIPE_VERIFY EntryCompare(inputs, mask); #endif