diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 5d6687ab..d464338b 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -922,17 +922,150 @@ namespace MobileGL::MG_Pipe { MGPipeGetResourceOps() != nullptr; } - // THE SAME TRIPLE `wants()` APPLIES TO EVERY EMISSION at the validate point, and it is + // ================================================================================ + // AND THE FOURTH HALF: D-K2's DEPENDENCY TABLE, ON THE CLIENT (S-3, ID-41) + // ================================================================================ + // + // THE DEFECT THIS CLOSES. Espryt's four `ResolveSubsystemArm()` functions + // (Managers.cpp ~:3595-3745) REFUSE a family whose D-K2 dependency bit is clear and run + // the legacy arm instead - the shape ResolveVertexInputSubsystemArm's bit-8-requires- + // bit-7 refusal set as the precedent. That refusal is a BACKSTOP and it cannot restore a + // correct picture on its own, because the client's emission was gated on the operator's + // mask ALONE: at 0x7ff (bit 10 set, bit 11 clear) the client emitted the whole texture + // family, the applier accepted it, the emitter cleared each level's per-level dirty flag + // on that acceptance (D-D5 as amended by ID-18 M3) - and then the server refused bit 10 + // and ran the legacy path, which found nothing left to upload. 438/491 on the DirectGLES + // integration lane, the same 47 texture-upload failures ID-39 saw on Magma for the + // consumer-less version of exactly this mistake. + // + // So the rule is the SAME "nothing at all, not less" rule as the consumer conjunct + // above: with a dependency unmet the client emits NOTHING for that family and the legacy + // pull path runs untouched, on both sides of the boundary. + // + // THE TABLE IS WRITTEN ONCE, HERE, and every one of its rows is the client mirror of the + // refusal Espryt already implements, bit for bit and non-transitively - the two must say + // the SAME thing, because a client that withheld more than the server refuses would + // leave the server's handle arm live with no records to read, and a client that withheld + // less is the defect above. + struct P4aFamilyDependencyRow { + Uint64 Family; // exactly one bit, and it is one of kMGPipeP4aFamilySubsystems + Uint64 Requires; // the bits MOBILEGL_PIPE_PUSH must ALSO carry for it to be live + }; + + inline constexpr P4aFamilyDependencyRow kMGPipeP4aFamilyDependencies[] = { + // BIT 9 REQUIRES BIT 10. Every MGPSurface::Res in a set_framebuffer_state record + // names a Texture or a Renderbuffer handle, and only bit 10 populates those two slot + // tables (Managers.cpp ResolveFramebufferSubsystemArm). + {kMGPipeSubsystemFramebuffer, kMGPipeSubsystemTextureResources}, + + // BIT 10 REQUIRES BIT 7 - a buffer texture's MGPResourceDesc::BufferForTexBuffer + // names a Buffer handle and only bit 7 puts twins in the resource slot table (D-D1, + // ResolveTextureResourceSubsystemArm's first row) - AND BIT 11, which is D-K2's + // FOURTH row (ID-14/ID-15): MGPTextureParams::BuiltinSampler is a SamplerCso HANDLE, + // only bit 11 mints sampler CSOs (c0b's four unconditional mints deliberately + // exclude it), and the applier's verdict for a null one is Fatal{ProtocolCorruption} + // rather than a decline. The brief's original "bit 10 without 11 is fine" is + // WITHDRAWN for P4a as built. + {kMGPipeSubsystemTextureResources, + kMGPipeSubsystemResources | kMGPipeSubsystemSamplers}, + + // BIT 11 REQUIRES BIT 10. Every MGPBoundView::Texture and every MGPImageView::Res + // names a Texture handle and only bit 10 populates that slot table; without it every + // per-unit lookup would miss and the walk would `continue` WITHOUT unbinding + // (ResolveSamplerSubsystemArm). With the row above this is SYMMETRIC: bits 10 and 11 + // are one arm with two switches, and the only two masks that reach either handle arm + // are "both set" and "neither set". + {kMGPipeSubsystemSamplers, kMGPipeSubsystemTextureResources}, + + // BIT 12 DEPENDS ON NOTHING, and that is a ROW rather than an absence so the table + // covers the four families exhaustively (the static_assert below): a ShaderCso handle + // names no texture and no buffer, the archive rides beside the record as a companion + // pointer, and the extra inputs the server specialises on are read from state the + // backend already holds (ResolveProgramSubsystemArm). + {kMGPipeSubsystemPrograms, 0}, + }; + + // THE MIRROR PAIRS THAT STAY FINE, said out loud rather than left as an absence, because + // an unreachable branch that says something different is how the reachable one drifts + // (Managers.cpp's own words at :2377-2381) - and because the table is only trustworthy if + // what it does NOT contain was decided rather than forgotten: + // - bit 10 set, bit 9 clear: FINE. The legacy FBO sync reaches the texture twin through + // SyncTextureObjectToBackend, which dispatches to the handle arm by itself. + // - bit 11 set, bit 9 clear: FINE, for the same reason - a sampler view names a texture, + // never a framebuffer. + // - bit 7 set, bit 10 clear: FINE, and it is P3a's shipped configuration. + // - bit 12 set with any or none of 9/10/11: FINE, per the last row. + // - bit 10 set, bit 11 clear (and its mirror) is NOT fine and is the row above; this is + // the one sentence in the brief that P4a as built withdrew. + constexpr Uint64 P4aFamilyDependencyBits(Uint64 subsystem) { + Uint64 required = 0; + for (const P4aFamilyDependencyRow& row : kMGPipeP4aFamilyDependencies) { + if ((subsystem & row.Family) != 0) required |= row.Requires; + } + return required; + } + + // The table covers the four families this phase migrates and nothing else, so a fifth + // family added to kMGPipeP4aFamilySubsystems without a row here does not silently inherit + // "depends on nothing". + constexpr Uint64 P4aFamilyDependencyTableCoverage() { + Uint64 covered = 0; + for (const P4aFamilyDependencyRow& row : kMGPipeP4aFamilyDependencies) covered |= row.Family; + return covered; + } + static_assert(P4aFamilyDependencyTableCoverage() == kMGPipeP4aFamilySubsystems, + "every P4a family needs a D-K2 dependency row, even an empty one"); + static_assert(P4aFamilyDependencyBits(kMGPipeSubsystemFramebuffer) == + kMGPipeSubsystemTextureResources, + "bit 9 requires bit 10"); + static_assert(P4aFamilyDependencyBits(kMGPipeSubsystemTextureResources) == + (kMGPipeSubsystemResources | kMGPipeSubsystemSamplers), + "bit 10 requires bit 7 and bit 11"); + static_assert(P4aFamilyDependencyBits(kMGPipeSubsystemSamplers) == + kMGPipeSubsystemTextureResources, + "bit 11 requires bit 10"); + static_assert(P4aFamilyDependencyBits(kMGPipeSubsystemPrograms) == 0, "bit 12 depends on nothing"); + // No family may depend on itself: a row that did would be unfalsifiable (its own bit is + // set by the time the conjunct is evaluated) and would read as a dependency nobody has. + static_assert((P4aFamilyDependencyBits(kMGPipeSubsystemFramebuffer) & + kMGPipeSubsystemFramebuffer) == 0 && + (P4aFamilyDependencyBits(kMGPipeSubsystemTextureResources) & + kMGPipeSubsystemTextureResources) == 0 && + (P4aFamilyDependencyBits(kMGPipeSubsystemSamplers) & + kMGPipeSubsystemSamplers) == 0, + "a D-K2 row must not name its own family"); + // The default mask carries every dependency, so the shipped arm is unchanged by all of + // this - the table only ever narrows a HAND-PICKED A/B mask. + static_assert((kMGPipeSubsystemsMigratedAtP4a & + P4aFamilyDependencyBits(kMGPipeP4aFamilySubsystems)) == + P4aFamilyDependencyBits(kMGPipeP4aFamilySubsystems), + "the P4a phase mask must satisfy every dependency it declares"); + + // IT IS THE RUNTIME BIT THAT IS TESTED, NOT THE OTHER FAMILY'S LIVENESS, and that is + // deliberate: Espryt's resolvers classify their arms from MOBILEGL_PIPE_PUSH alone, so + // testing anything else here would make the two sides disagree at some mask - which is + // the failure this whole commit is about, one level up. The mask is passed in rather than + // read, so the walk's single read of MG_Config::Features.PipePush stays the one read a + // whole validate point resolves against. + Bool P4aFamilyDependenciesAreSet(Uint64 subsystem, Uint64 pushMask) { + const Uint64 required = P4aFamilyDependencyBits(subsystem); + return (pushMask & required) == required; + } + + // THE SAME QUADRUPLE `wants()` APPLIES TO EVERY EMISSION at the validate point, and it is // deliberately the same predicate rather than a second copy of it: the operator's // per-subsystem A/B bit in MOBILEGL_PIPE_PUSH, this build having WIRED the family - // at all, AND - for a P4a family - a backend having registered the consumer. The second - // half is the family's own kMGPipeWired*Subsystem constant, which lives in the family's - // emit header and is 0 until the commit that gives the emitter its body - so a client - // path that lands before its emitter does is inert by construction rather than by - // everyone remembering to check; the third is P4aFamilyHasItsConsumer above. + // at all, AND - for a P4a family - a backend having registered the consumer and every + // D-K2 dependency bit of the family being set. The second half is the family's own + // kMGPipeWired*Subsystem constant, which lives in the family's emit header and is 0 until + // the commit that gives the emitter its body - so a client path that lands before its + // emitter does is inert by construction rather than by everyone remembering to check; the + // third is P4aFamilyHasItsConsumer above and the fourth is P4aFamilyDependenciesAreSet. Bool FamilyIsLive(Uint64 subsystem, Uint64 wired) { - return (MG_Config::Features.PipePush & subsystem) != 0 && (wired & subsystem) != 0 && - P4aFamilyHasItsConsumer(subsystem); + const Uint64 pushMask = MG_Config::Features.PipePush; + return (pushMask & subsystem) != 0 && (wired & subsystem) != 0 && + P4aFamilyHasItsConsumer(subsystem) && + P4aFamilyDependenciesAreSet(subsystem, pushMask); } // ---- THE FAMILY SEAM ---- @@ -1088,12 +1221,14 @@ namespace MobileGL::MG_Pipe { PublicationLatch().NoteUnpublished(kind, handle); } - // THE GATE ITSELF, AS AN OBSERVABLE (ID-39). Every P4a birth hook below and every `wants()` - // row in the walk resolve through FamilyIsLive / P4aFamilyHasItsConsumer, and neither is - // reachable from a test - so this is the one door a unit case has onto the answer, and it - // is the SAME expression rather than a second copy of it. A subsystem outside - // kMGPipeP4aFamilySubsystems answers the pair the P2/P3a families have always answered, - // which is what makes "nothing that emits today changes" checkable instead of asserted. + // THE GATE ITSELF, AS AN OBSERVABLE (ID-39, widened by S-3 / ID-41). Every P4a birth hook + // below and every `wants()` row in the walk resolve through FamilyIsLive / + // P4aFamilyHasItsConsumer / P4aFamilyDependenciesAreSet, and none of the three is reachable + // from a test - so this is the one door a unit case has onto the answer, and it is the SAME + // expression rather than a second copy of it. A subsystem outside kMGPipeP4aFamilySubsystems + // answers the pair the P2/P3a families have always answered (its consumer conjunct is + // vacuous and its dependency set is empty), which is what makes "nothing that emits today + // changes" checkable instead of asserted. Bool MGPipeP4aFamilyEmits(Uint64 subsystem, Uint64 wired) { return FamilyIsLive(subsystem, wired); } @@ -2249,12 +2384,19 @@ namespace MobileGL::MG_Pipe { // a property of the RUNNING BACKEND, and collapsing the two would make a bisect that // lands between them unreadable. The P2/P3a bits are outside kMGPipeP4aFamilySubsystems, // so the conjunct is true for every one of them and nothing that emits today changes. + // + // AND THE SIXTH IS P4aFamilyDependenciesAreSet (S-3 / ID-41), the client half of D-K2: + // a family one of whose dependency bits the operator left clear emits NOTHING here for + // the same reason - the server REFUSES that family and runs its legacy arm, and an + // emission the server refuses is an emission whose acceptance already cleared a frontend + // dirty flag the legacy arm still owed. Same table, same four families, one place. const Uint64 pushMask = MG_Config::Features.PipePush; const auto wants = [&](MGPipeDirty bit) { const Uint64 subsystem = MGPipeSubsystemForDirty(bit); return subsystem != 0 && (pushMask & subsystem) != 0 && (kMGPipeWiredSubsystems & subsystem) != 0 && P4aFamilyHasItsConsumer(subsystem) && + P4aFamilyDependenciesAreSet(subsystem, pushMask) && (dirty & MGPipeDirtyBit(bit)) != 0; }; Uint64 payloadBytes = 0; @@ -2319,14 +2461,17 @@ namespace MobileGL::MG_Pipe { payloadBytes += EmitShaderState(*ctx); } // The texture drain has no dirty bit over it (see its definition); it is gated on the - // subsystem bit, on this build having wired the family at all and on a backend having - // registered the consumer, which is the same triple `wants()` applies to every other - // emission. The third one is the whole of ID-39 on the path where it mattered most: - // the drain is what clears a level's dirty flags on acceptance, so a drain that ran - // against an applier no backend reads is exactly how Magma lost its texel uploads. + // subsystem bit, on this build having wired the family at all, on a backend having + // registered the consumer and on D-K2's dependency bits for the family being set, which + // is the same quadruple `wants()` applies to every other emission. The last two are the + // whole of ID-39 and of S-3 on the path where they mattered most: the drain is what + // clears a level's dirty flags on acceptance, so a drain that ran against an applier no + // backend reads is exactly how Magma lost its texel uploads, and a drain that ran at a + // mask whose bit 11 or bit 7 is clear is how Espryt lost them at 0x7ff and 0x5ff. if ((pushMask & kMGPipeSubsystemTextureResources) != 0 && (kMGPipeWiredSubsystems & kMGPipeSubsystemTextureResources) != 0 && - P4aFamilyHasItsConsumer(kMGPipeSubsystemTextureResources)) { + P4aFamilyHasItsConsumer(kMGPipeSubsystemTextureResources) && + P4aFamilyDependenciesAreSet(kMGPipeSubsystemTextureResources, pushMask)) { payloadBytes += DrainTextureSubData(*ctx); } if (wants(MGPipeDirty::NewSamplerViews)) { @@ -2399,13 +2544,15 @@ namespace MobileGL::MG_Pipe { // the very fields the migration just took over. const MGPipeFieldEmitter emitter = kMGPipeFieldEmittedBy[i]; const Uint64 subsystem = SubsystemForEmitter(emitter); - // P4aFamilyHasItsConsumer is in this conjunction for the reason it is in `wants()`: - // "supplied" means A CALL WENT OUT CARRYING THIS FIELD, and on a backend with no - // consumer no P4a call went out at all - so withholding the pull here would leave - // the field unfilled at the very verb that reads it. + // P4aFamilyHasItsConsumer and P4aFamilyDependenciesAreSet are in this conjunction for + // the reason they are in `wants()`: "supplied" means A CALL WENT OUT CARRYING THIS + // FIELD, and on a backend with no consumer - or at a mask that leaves one of the + // family's D-K2 dependency bits clear - no P4a call went out at all, so withholding + // the pull here would leave the field unfilled at the very verb that reads it. const Bool supplied = subsystem != 0 && (subsystem & kMGPipeWiredSubsystems) != 0 && (pushMask & subsystem) != 0 && P4aFamilyHasItsConsumer(subsystem) && + P4aFamilyDependenciesAreSet(subsystem, pushMask) && EmittedCallSuppliesTheWholeField(field) && (applierDerives || AppliedWithoutDerivation(field)); if (!supplied) MGPipeFillAccess::CopyField(inputs, *ctx, field); diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.h b/MobileGL/MG_Impl/Pipe/PipeFill.h index 4adc913f..6f17fb01 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.h +++ b/MobileGL/MG_Impl/Pipe/PipeFill.h @@ -43,19 +43,27 @@ namespace MobileGL::MG_Pipe { // stop where it says it stops (MG_Test/ScopedPipeVerb.h). void MGPipeLeaveVerb(); - // PipeFill.cpp. DOES THIS BUILD, ON THIS BACKEND, EMIT FOR THIS P4a FAMILY? (ID-39.) The - // three conjuncts are the operator's per-subsystem bit in MOBILEGL_PIPE_PUSH, the family's - // own kMGPipeWired*Subsystem constant (`wired`, which the caller passes because it lives in - // the family's emit header and this header may not include one), and - for the four - // families P4a migrates - a backend having registered MGPipeResourceOps, which is the same - // per-backend signal `MGPipeResourceSubsystemEnabled()` has applied to P3a's buffers since - // the phase began. + // PipeFill.cpp. DOES THIS BUILD, ON THIS BACKEND, AT THIS MASK, EMIT FOR THIS P4a FAMILY? + // (ID-39, widened by S-3 / ID-41.) The four conjuncts are the operator's per-subsystem bit + // in MOBILEGL_PIPE_PUSH, the family's own kMGPipeWired*Subsystem constant (`wired`, which + // the caller passes because it lives in the family's emit header and this header may not + // include one), and - for the four families P4a migrates - a backend having registered + // MGPipeResourceOps (the same per-backend signal `MGPipeResourceSubsystemEnabled()` has + // applied to P3a's buffers since the phase began) and every D-K2 dependency bit of the + // family being set in the same mask. // - // THE THIRD CONJUNCT IS THE ONE THIS DECLARATION EXISTS FOR. Magma (DirectVulkan) registers - // no table and has no P4a twins; before it, the client emitted, the applier accepted, the - // emitters cleared their per-level dirty flags on that acceptance, and Magma's legacy - // upload path found nothing to upload. With it the four families emit NOTHING there and the - // legacy pull path runs exactly as it does on a pull build. + // THE LAST TWO CONJUNCTS ARE THE ONES THIS DECLARATION EXISTS FOR, and they are the same + // defect twice. Magma (DirectVulkan) registers no table and has no P4a twins; at a mask like + // 0x7ff Espryt REFUSES the texture family server-side because D-K2's fourth row says bit 10 + // requires bit 11. In both cases the client emitted anyway, the applier accepted, the + // emitters cleared their per-level dirty flags on that acceptance, and the legacy upload + // path that still owed those texels found nothing to upload (66 DirectVulkan cases at ID-39, + // 47 DirectGLES cases at ID-41). With them the four families emit NOTHING in that state and + // the legacy pull path runs exactly as it does on a pull build. + // + // D-K2's TABLE IS IN PipeFill.cpp, ONCE: bit 9 requires bit 10, bit 10 requires bits 7 and + // 11, bit 11 requires bit 10, bit 12 depends on nothing - the client mirror, bit for bit, of + // the four `ResolveSubsystemArm()` refusals in DirectGLES/Managers.cpp. // // It is exported for the unit gate and for no other caller: the gate itself is // FamilyIsLive() inside PipeFill.cpp, every birth hook and every `wants()` row resolves diff --git a/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp b/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp index 147bb373..e2c8902d 100644 --- a/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp @@ -256,11 +256,17 @@ namespace { struct FramebufferScope { FramebufferScope() { m_previousPush = MG_Config::Features.PipePush; - // D-K2, both rows: bit 9 requires bit 10 (MGPSurface::Res names a texture or - // renderbuffer handle) and bit 10 requires bit 11 (MGPTextureParams::BuiltinSampler - // is a SamplerCso out of the sampler family's content-addressed cache). - MG_Config::Features.PipePush |= - kMGPipeSubsystemFramebuffer | kMGPipeSubsystemTextureResources | kMGPipeSubsystemSamplers; + // D-K2, ALL THREE ROWS THAT REACH THIS SUITE, because the client enforces them since + // S-3 / ID-41 and not only Espryt's Resolve*SubsystemArm: bit 9 requires bit 10 + // (MGPSurface::Res names a texture or renderbuffer handle), bit 10 requires bit 11 + // (MGPTextureParams::BuiltinSampler is a SamplerCso out of the sampler family's + // content-addressed cache) and bit 10 requires bit 7 (a buffer texture's + // BufferForTexBuffer names a Buffer handle, D-D1). Bit 7 changes nothing else here: + // this file constructs no BufferObject. + MG_Config::Features.PipePush |= kMGPipeSubsystemResources | + kMGPipeSubsystemFramebuffer | + kMGPipeSubsystemTextureResources | + kMGPipeSubsystemSamplers; m_previousContext = Move(MG_State::pGLContext); MG_State::pGLContext = MakeUnique(); MGPipeFramebufferEmitterInstance().ResetForTest(); diff --git a/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp b/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp index bf81c021..9f7ce015 100644 --- a/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp @@ -212,7 +212,11 @@ TEST(TextureEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) { X(TextureEmit, ATexturesBuiltinSamplerHoldsOneCacheReferenceAndSwapsItWithTheContent) \ X(TextureEmit, ARecycledTextureSlotDoesNotInheritItsPredecessorsBindMask) \ X(TextureEmit, ALevelMarkedCleanIsCollectedAtTheNextDrain) \ - X(TextureEmit, WithNoBackendConsumerTheFamilyGateIsFalseAndNothingReachesTheApplier) + X(TextureEmit, WithNoBackendConsumerTheFamilyGateIsFalseAndNothingReachesTheApplier) \ + X(TextureEmit, WithTheSamplerBitClearTheTextureFamilyGateIsFalseAndNothingReachesTheApplier) \ + X(TextureEmit, \ + WithTheBufferResourceBitClearTheTextureFamilyGateIsFalseAndNothingReachesTheApplier) \ + X(TextureEmit, EveryDKTwoDependencyRowGatesItsOwnFamilyAndTheMirrorPairsStayLive) #define MGL_DECLARE_PULL_SKIP(Suite, Name) \ TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; } @@ -240,10 +244,20 @@ namespace { struct TextureScope { TextureScope() { m_previousPush = MG_Config::Features.PipePush; - // D-K2's fourth row, in the fixture: BIT 10 REQUIRES BIT 11. - // MGPTextureParams::BuiltinSampler is a SamplerCso handle out of the sampler - // family's content-addressed cache, and a null there is Fatal{ProtocolCorruption}. - MG_Config::Features.PipePush |= kMGPipeSubsystemTextureResources | kMGPipeSubsystemSamplers; + // D-K2's WHOLE ROW FOR BIT 10, in the fixture, because the client now enforces it + // (S-3 / ID-41) and not only Espryt's ResolveTextureResourceSubsystemArm: + // - BIT 10 REQUIRES BIT 11 - MGPTextureParams::BuiltinSampler is a SamplerCso + // handle out of the sampler family's content-addressed cache and a null there is + // Fatal{ProtocolCorruption}; + // - BIT 10 REQUIRES BIT 7 - a buffer texture's BufferForTexBuffer names a Buffer + // handle and only bit 7 puts twins in the resource slot table (D-D1). Arming it + // changes nothing else here: this file constructs no BufferObject, so no P3a + // resource hook has anything to fire on. + // Without all three the family gate is FALSE and every case below would be green for + // the wrong reason - which is what the dependency cases at the end of this file pin. + MG_Config::Features.PipePush |= kMGPipeSubsystemResources | + kMGPipeSubsystemTextureResources | + kMGPipeSubsystemSamplers; m_previousContext = Move(MG_State::pGLContext); MG_State::pGLContext = MakeUnique(); MGPipeTextureEmitterInstance().ResetForTest(); @@ -910,6 +924,177 @@ TEST(TextureEmit, WithNoBackendConsumerTheFamilyGateIsFalseAndNothingReachesTheA EXPECT_FALSE(consumed->IsStorageDirty(TextureUploadTarget::Texture2D, 0)); } +// ==================== S-3 (ID-41): D-K2's DEPENDENCY TABLE, ON THE CLIENT ==================== +// +// THE SAME DEFECT AS THE CASE ABOVE, ONE SIGNAL OVER. Espryt's four +// `ResolveSubsystemArm()` functions REFUSE a family whose D-K2 dependency bit is clear +// and run the legacy arm instead - the backstop. But the client's emission used to be gated on +// MOBILEGL_PIPE_PUSH's own bit alone, so at 0x7ff (bit 10 set, bit 11 clear) it emitted the whole +// texture family anyway, the applier accepted it, the emitter cleared each level's dirty flag on +// that acceptance - and the server then refused bit 10 and ran a legacy path with nothing left to +// upload. 438/491 on the DirectGLES integration lane, the same 47 texture-upload failures ID-39 +// saw on Magma for the consumer-less version of the identical mistake. +// +// WHAT THESE THREE CASES PIN IS "NOTHING AT ALL", NOT "LESS", exactly as the consumer case above +// does: no create, no respecify, no params, no entry on the drain list - and the FRONTEND's own +// dirty flag still set, which is the state the legacy pull path reads and the one whose loss no +// pixel comparison on this side can see. +TEST(TextureEmit, WithTheSamplerBitClearTheTextureFamilyGateIsFalseAndNothingReachesTheApplier) { + TextureScope scope; + // THE 0x7ff SHAPE EXACTLY: bit 10 set, bit 11 clear. D-K2's FOURTH row (ID-14/ID-15) - + // MGPTextureParams::BuiltinSampler is a SamplerCso handle, only bit 11 mints sampler CSOs, + // and the applier's verdict for a null one is Fatal{ProtocolCorruption}. + MG_Config::Features.PipePush &= ~kMGPipeSubsystemSamplers; + + EXPECT_FALSE(MGPipeP4aFamilyEmits(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) + << "bit 10 is set and bit 11 is clear; Espryt refuses this family, so the client must not " + "emit into it"; + + const auto texture = MakeTexture2D(43, 32); + const MGPipeHandle handle = Textures().FindTexture(*texture); + // The mint is unconditional and stays that way: what a dependency withholds is the EMISSION, + // never the identity, exactly as for the consumer conjunct. + ASSERT_FALSE(MGPipeHandleIsNull(handle)); + EXPECT_FALSE(MGPipeHandleIsPublished(MGPipeKind::Texture, handle)) + << "a create was published for a family the server refuses at this mask"; + EXPECT_EQ(Textures().CreateCount(), 0u); + EXPECT_EQ(Textures().RespecifyCount(), 0u); + EXPECT_TRUE(MGPipeApplier().TextureResources.empty()); + + texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, + IntVec3{8, 8, 1}); + EXPECT_EQ(Textures().DrainListSize(), 0u); + Textures().DrainTextureSubData(Ctx()); + EXPECT_EQ(Textures().SubDataCount(), 0u); + // THE ONE THAT MATTERED, and it is the whole of S-3: the level's flag is what Espryt's LEGACY + // texture arm uploads from once it has refused bit 10. + EXPECT_TRUE(texture->IsStorageDirty(TextureUploadTarget::Texture2D, 0)) + << "the level's dirty flag was cleared at a mask whose server-side arm is the legacy one, " + "so those texels exist nowhere"; + EXPECT_EQ(MGPipeApplier().RefusedNoConsumer, 0u) + << "the belt is about the consumer; the DEPENDENCY is the gate's job and the gate is what " + "must have stopped this"; + + // ---- and with bit 11 back, the same sequence lands ---- + MG_Config::Features.PipePush |= kMGPipeSubsystemSamplers; + EXPECT_TRUE(MGPipeP4aFamilyEmits(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)); + const auto live = MakeTexture2D(44, 32); + const MGPipeHandle liveHandle = Textures().FindTexture(*live); + ASSERT_FALSE(MGPipeHandleIsNull(liveHandle)); + EXPECT_TRUE(MGPipeHandleIsPublished(MGPipeKind::Texture, liveHandle)); + live->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, + IntVec3{8, 8, 1}); + EXPECT_EQ(Textures().DrainListSize(), 1u); + Textures().DrainTextureSubData(Ctx()); + EXPECT_EQ(Textures().SubDataCount(), 1u); + EXPECT_FALSE(live->IsStorageDirty(TextureUploadTarget::Texture2D, 0)); +} + +// THE OTHER HALF OF THE SAME ROW: bit 10 requires bit 7 as well (D-D1). A buffer texture's +// MGPResourceDesc::BufferForTexBuffer names a Buffer handle and only bit 7 puts twins in the +// resource slot table, so ResolveTextureResourceSubsystemArm refuses bit 10 without it - and a +// refused family must not have had its flags cleared by an emission that already went out. This +// is the arm the 0x5ff-shaped masks reach from the other side. +TEST(TextureEmit, WithTheBufferResourceBitClearTheTextureFamilyGateIsFalseAndNothingReachesTheApplier) { + TextureScope scope; + MG_Config::Features.PipePush &= ~kMGPipeSubsystemResources; + + EXPECT_FALSE(MGPipeP4aFamilyEmits(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) + << "bit 10 is set and bit 7 is clear; Espryt refuses this family, so the client must not " + "emit into it"; + + const auto texture = MakeTexture2D(45, 32); + const MGPipeHandle handle = Textures().FindTexture(*texture); + ASSERT_FALSE(MGPipeHandleIsNull(handle)); + EXPECT_FALSE(MGPipeHandleIsPublished(MGPipeKind::Texture, handle)); + EXPECT_EQ(Textures().CreateCount(), 0u); + EXPECT_EQ(Textures().RespecifyCount(), 0u); + EXPECT_TRUE(MGPipeApplier().TextureResources.empty()); + + texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, + IntVec3{8, 8, 1}); + EXPECT_EQ(Textures().DrainListSize(), 0u); + Textures().DrainTextureSubData(Ctx()); + EXPECT_EQ(Textures().SubDataCount(), 0u); + EXPECT_TRUE(texture->IsStorageDirty(TextureUploadTarget::Texture2D, 0)); + EXPECT_EQ(MGPipeApplier().RefusedNoConsumer, 0u); +} + +// THE TABLE ITSELF, ONE ROW PER FAMILY, AND THE MIRROR PAIRS THAT STAY LIVE. The two cases above +// drive the texture family end to end because its emitter is the one this suite owns; the +// framebuffer, sampler and program families are asserted through the gate itself, which is the +// single predicate every one of their birth hooks and every `wants()` row in the walk resolves +// through (MGPipeP4aFamilyEmits returns FamilyIsLive, not a second copy of it). `wired` is passed +// as the family's own bit, which is what that constant is once the family has landed its emitter. +// +// THE ROWS ARE NON-TRANSITIVE ON PURPOSE. Espryt's resolvers classify their arms from +// MOBILEGL_PIPE_PUSH alone, so this table asks the same question they ask - "is the dependency +// BIT set" - and not "is the other family live". At a mask like 0x7ff that means the framebuffer +// family stays LIVE on both sides while the texture family is dead on both sides, which is the +// state the two must agree on; a client that withheld more than the server refuses would leave +// the server's handle arm live with no records to read. +TEST(TextureEmit, EveryDKTwoDependencyRowGatesItsOwnFamilyAndTheMirrorPairsStayLive) { + TextureScope scope; + const auto emits = [](Uint64 family) { return MGPipeP4aFamilyEmits(family, family); }; + const Uint64 kAll = kMGPipeSubsystemResources | kMGPipeSubsystemFramebuffer | + kMGPipeSubsystemTextureResources | kMGPipeSubsystemSamplers | + kMGPipeSubsystemPrograms; + + // ---- every dependency satisfied: all four families live ---- + MG_Config::Features.PipePush = kAll; + EXPECT_TRUE(emits(kMGPipeSubsystemFramebuffer)); + EXPECT_TRUE(emits(kMGPipeSubsystemTextureResources)); + EXPECT_TRUE(emits(kMGPipeSubsystemSamplers)); + EXPECT_TRUE(emits(kMGPipeSubsystemPrograms)); + + // ---- ROW 1: bit 9 requires bit 10 (MGPSurface::Res names a texture or renderbuffer) ---- + MG_Config::Features.PipePush = kAll & ~kMGPipeSubsystemTextureResources; + EXPECT_FALSE(emits(kMGPipeSubsystemFramebuffer)) << "bit 9 set, bit 10 clear"; + // ROW 3 falls out of the same mask: bit 11 requires bit 10. + EXPECT_FALSE(emits(kMGPipeSubsystemSamplers)) << "bit 11 set, bit 10 clear"; + EXPECT_TRUE(emits(kMGPipeSubsystemPrograms)) << "bit 12 depends on nothing"; + + // ---- ROW 2a: bit 10 requires bit 11 - the 0x7ff shape ---- + MG_Config::Features.PipePush = kAll & ~kMGPipeSubsystemSamplers; + EXPECT_FALSE(emits(kMGPipeSubsystemTextureResources)) << "bit 10 set, bit 11 clear"; + EXPECT_TRUE(emits(kMGPipeSubsystemFramebuffer)) + << "bit 9's row names bit 10 and bit 10 IS set at this mask - the rows are non-transitive " + "because Espryt's ResolveFramebufferSubsystemArm is"; + EXPECT_TRUE(emits(kMGPipeSubsystemPrograms)); + + // ---- ROW 2b: bit 10 requires bit 7 - the D-D1 half ---- + MG_Config::Features.PipePush = kAll & ~kMGPipeSubsystemResources; + EXPECT_FALSE(emits(kMGPipeSubsystemTextureResources)) << "bit 10 set, bit 7 clear"; + EXPECT_TRUE(emits(kMGPipeSubsystemSamplers)) << "bit 11's only row is bit 10, which is set"; + EXPECT_TRUE(emits(kMGPipeSubsystemPrograms)); + + // ---- ROW 4: bit 12 depends on NOTHING, so it is live entirely on its own ---- + MG_Config::Features.PipePush = kMGPipeSubsystemPrograms; + EXPECT_TRUE(emits(kMGPipeSubsystemPrograms)); + + // ---- THE MIRROR PAIRS, said out loud: an unreachable branch that says something different + // is how the reachable one drifts (Managers.cpp:2377-2381's own words) ---- + // bits 10 + 11 without bit 9: FINE, and it is the 0xdff arm. + MG_Config::Features.PipePush = kAll & ~kMGPipeSubsystemFramebuffer; + EXPECT_TRUE(emits(kMGPipeSubsystemTextureResources)); + EXPECT_TRUE(emits(kMGPipeSubsystemSamplers)); + EXPECT_TRUE(emits(kMGPipeSubsystemPrograms)); + // bit 7 without bit 10: FINE, and it is P3a's shipped configuration - no P4a family is live + // because none of their own bits is set, and that is the ONLY reason. + MG_Config::Features.PipePush = kMGPipeSubsystemResources; + EXPECT_FALSE(emits(kMGPipeSubsystemFramebuffer)); + EXPECT_FALSE(emits(kMGPipeSubsystemTextureResources)); + EXPECT_FALSE(emits(kMGPipeSubsystemSamplers)); + EXPECT_FALSE(emits(kMGPipeSubsystemPrograms)); + + // ---- AND THE P2/P3a FAMILIES ARE NOT NARROWED BY ANY OF IT: their bits are outside + // kMGPipeP4aFamilySubsystems, so they have no dependency row and no consumer conjunct. + // This is what makes "nothing that emits today changes" checkable rather than asserted. + MG_Config::Features.PipePush = kMGPipeSubsystemVertexInput; + EXPECT_TRUE(emits(kMGPipeSubsystemVertexInput)) + << "bit 8 alone, with bit 7 clear: P3a's own rule, which this table must not touch"; +} + // ============================ M4 ============================ // // THE CANONICAL ORDER FOR THE TEXTURES THE HINT WAS WRITTEN FOR: glTexStorage2D, then