diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index f1758521..5d6687ab 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -875,15 +875,64 @@ namespace MobileGL::MG_Pipe { using MG_State::GLState::RenderbufferObject; using MG_State::GLState::SamplerObject; - // THE SAME PAIR `wants()` APPLIES TO EVERY EMISSION at the validate point, and it is + // THE FOUR FAMILIES P4a MIGRATES, as one mask, so the consumer rule below is stated + // once instead of four times. It is deliberately NOT kMGPipeSubsystemsMigratedAtP4a + // (which is 0x1fff, every bit through P4a): the rule belongs to the families this + // phase adds and to no earlier one. + inline constexpr Uint64 kMGPipeP4aFamilySubsystems = + kMGPipeSubsystemFramebuffer | kMGPipeSubsystemTextureResources | + kMGPipeSubsystemSamplers | kMGPipeSubsystemPrograms; + + // AND THE THIRD HALF, WHICH IS P3a's SECOND ONE: HAS A BACKEND REGISTERED THE CONSUMER? + // + // `MGPipeResourceSubsystemEnabled()` (above, ~:612) is bit 7 AND + // `MGPipeGetResourceOps() != nullptr`, and the second conjunct is not decoration - it is + // what keeps P3a's buffers on the legacy pull path under a backend that registers no + // table. DirectVulkan (Magma) is exactly that backend: it registers no + // MGPipeResourceOps and has none of P4a's twins. Without this conjunct the four P4a + // families emitted there anyway, the applier ACCEPTED every record, the emitters cleared + // their per-level dirty flags on that acceptance (D-D5 as amended by ID-18 M3), and + // Magma's legacy upload path then found nothing left to upload: 66 texture-upload-shaped + // DirectVulkan integration-gpu cases red on the push build at the default mask, with the + // pull build 966/966 green (ID-39). + // + // ALL FOUR FAMILIES RIDE THE ONE SIGNAL, and the reason is D-D1: a texture and a + // renderbuffer are RESOURCE rows - they travel on P3a's own resource_create / + // resource_respecify / resource_subdata catalogue, whose consumer IS this table - so the + // texture family's gate is P3a's gate by construction. The other three name texture + // handles and cannot be live without it (MGPSurface::Res is a texture or renderbuffer + // handle, MGPBoundView::Texture and MGPImageView::Res are texture handles, and + // MGPTextureParams is addressed by one), so they follow. There is no fifth signal to + // invent and no per-family registration to add: a backend that consumes P4a records + // consumes resource rows first. + // + // A BACKEND THAT REGISTERS ONE IS UNAFFECTED. DirectGLES (Espryt) registers the table + // at RegisterBufferBackendOps, unconditionally and at bring-up, so every predicate + // below answers exactly what it answered before this commit. + // + // THE REGISTER/UNREGISTER WINDOW IS THE SAME ONE P3a LIVES WITH, and it is closed the + // same way: UnregisterBufferBackendOps nulls the table at context teardown and the + // re-register happens at the next MakeCurrent, so an object born in that window never + // publishes a create and latches Published = false - after which the family's own + // self-healing create on the next respecify (TextureEmit.h ~:576 / ~:709, the shape + // MGPipeEmitResourceRespecify above uses for buffers) publishes it. Nothing here needs + // to remember the window. + Bool P4aFamilyHasItsConsumer(Uint64 subsystem) { + return (subsystem & kMGPipeP4aFamilySubsystems) == 0 || + MGPipeGetResourceOps() != nullptr; + } + + // THE SAME TRIPLE `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, AND this build having WIRED the family - // at all. 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. + // 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. Bool FamilyIsLive(Uint64 subsystem, Uint64 wired) { - return (MG_Config::Features.PipePush & subsystem) != 0 && (wired & subsystem) != 0; + return (MG_Config::Features.PipePush & subsystem) != 0 && (wired & subsystem) != 0 && + P4aFamilyHasItsConsumer(subsystem); } // ---- THE FAMILY SEAM ---- @@ -1039,6 +1088,16 @@ 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. + Bool MGPipeP4aFamilyEmits(Uint64 subsystem, Uint64 wired) { + return FamilyIsLive(subsystem, wired); + } + void MGPipeMintTextureHandle(ITextureObject& texture) { MGPipeSlots().Acquire(MGPipeKind::Texture, texture.GetLifetimeId()); } @@ -2171,7 +2230,7 @@ namespace MobileGL::MG_Pipe { // would be a second copy of that map in the only path that runs, and mis-gating a bit // in it would pass every test the map has. // - // FOUR CONDITIONS, AND THE WIRED MASK IS ONE OF THEM. `kMGPipeWiredSubsystems` is the + // FIVE CONDITIONS, AND THE WIRED MASK IS ONE OF THEM. `kMGPipeWiredSubsystems` is the // OR of the per-family constants each emit header defines, and the whole ownership // design rests on it MEANING what the headers, this file and the result files all say // it means: an emitter runs only once the commit that gave it a body set its family's @@ -2181,11 +2240,21 @@ namespace MobileGL::MG_Pipe { // measure an arm nobody thinks is on - and the mirror error is worse: a family that // lands its body and forgets the constant would emit nothing and look broken. The // P2/P3a bits are all in the mask, so nothing that emits today changes. + // + // AND THE FIFTH IS P4aFamilyHasItsConsumer (ID-39), the same conjunct FamilyIsLive + // applies to every birth hook: a P4a family whose records nothing on this backend + // consumes emits NOTHING, so the legacy pull path runs exactly as it does on the pull + // build. It is written here rather than folded into kMGPipeWiredSubsystems because the + // wired mask is a property of the BUILD - a constexpr an emit header sets - and this is + // 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. 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) && (dirty & MGPipeDirtyBit(bit)) != 0; }; Uint64 payloadBytes = 0; @@ -2250,10 +2319,14 @@ 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 and on this build having wired the family at all, which is the same - // pair `wants()` applies to every other emission. + // 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. if ((pushMask & kMGPipeSubsystemTextureResources) != 0 && - (kMGPipeWiredSubsystems & kMGPipeSubsystemTextureResources) != 0) { + (kMGPipeWiredSubsystems & kMGPipeSubsystemTextureResources) != 0 && + P4aFamilyHasItsConsumer(kMGPipeSubsystemTextureResources)) { payloadBytes += DrainTextureSubData(*ctx); } if (wants(MGPipeDirty::NewSamplerViews)) { @@ -2326,8 +2399,13 @@ 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. const Bool supplied = subsystem != 0 && (subsystem & kMGPipeWiredSubsystems) != 0 && (pushMask & subsystem) != 0 && + P4aFamilyHasItsConsumer(subsystem) && 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 ae013cb7..4adc913f 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.h +++ b/MobileGL/MG_Impl/Pipe/PipeFill.h @@ -43,6 +43,25 @@ 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. + // + // 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. + // + // 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 + // through it, and this returns that same expression rather than a second copy of it. + Bool MGPipeP4aFamilyEmits(Uint64 subsystem, Uint64 wired); + // PipeFill.cpp. P3a D-H2.1: the DRAW's raw vertex-fetch base instance, which // set_vertex_buffers now carries as an explicit field. // diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index 4d4993b0..625c4367 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -1094,6 +1094,32 @@ namespace MobileGL::MG_Pipe { void MGPipeSetResourceOps(const MGPipeResourceOps* ops) { g_resourceOps = ops; } const MGPipeResourceOps* MGPipeGetResourceOps() { return g_resourceOps; } + namespace { + // P4a's BELT (ID-39). See MGPipeApplierState::RefusedNoConsumer for the whole argument; + // in one line: acceptance is a contract with the client, and accepting a record on a + // backend that consumes none of them makes the emitter clear a dirty flag the legacy + // pull path still owed. + // + // IT IS THE SAME SIGNAL THE CLIENT'S GATE READS, deliberately - g_resourceOps is the + // table a backend installs at its own bring-up and uninstalls at teardown, and + // MGPipeApplierReset does NOT touch it (see there), so the gate cannot flap between a + // make-current and the emissions that follow it. The two therefore agree on every path + // except the one this exists for: an emitter called directly, without passing PipeFill. + // + // EVERY CALLER PLACES IT AFTER THE RECORD'S OWN SHAPE CHECKS AND BEFORE ANYTHING MOVES. + // A malformed record is Fatal{ProtocolCorruption} whether or not anything would have + // read it - a trip wire that fires only on some backends is a trip wire nobody can + // trust - so the refusal is the LAST thing asked and the first thing that stops the + // write. The three unit-set entry points are the exception and say so at their call + // site: ApplyUnitWindow validates and writes in one step, so the question has to be + // asked in front of it. + Bool NoP4aConsumer() { + if (g_resourceOps != nullptr) return false; + ++g_applier.RefusedNoConsumer; + return true; + } + } // namespace + void MGPipeApplierReset() { g_applier.RenderStateCsos.clear(); g_applier.BoundRenderStateCso = kMGPipeNullHandle; @@ -1123,6 +1149,7 @@ namespace MobileGL::MG_Pipe { g_applier.RefusedResourceCalls = 0; g_applier.RefusedVertexInputCalls = 0; g_applier.RefusedObjectCalls = 0; + g_applier.RefusedNoConsumer = 0; g_applier.BoundVertexElements = kMGPipeNullHandle; g_applier.VertexBuffers = {}; g_applier.VertexBufferStart = 0; @@ -1553,6 +1580,15 @@ namespace MobileGL::MG_Pipe { kMGPipeMaxResourceSlots); return false; } + // P4a's belt, and only over the P4a half of this call: the BUFFER row is P3a's and its + // consumer question is the frontend's (MGPipeResourceSubsystemEnabled), which has + // already answered it before the emission - a buffer record can only arrive here on a + // backend that registered the table, so asking again would be a second copy of that + // rule in the one path that runs. A texture or renderbuffer create declined here leaves + // the client's publication latch at false, which is what makes the next respecify's + // self-healing create do the right thing if a table appears later. + if (desc.Target != kMGPipeResourceTargetBuffer && NoP4aConsumer()) return false; + *record = MGPipeResourceRecord{}; record->Gen = desc.Resource.Gen; record->Live = true; @@ -1583,6 +1619,12 @@ namespace MobileGL::MG_Pipe { desc.Resource.Slot, desc.Resource.Gen, desc.GlNameForDiag, desc.Target); return false; } + // P4a's belt, for MGPipeApplyResourceCreate's reason and in front of the resolution + // rather than after it: with no consumer no create was ever accepted, so resolving + // first would report the absence as RefusedResourceCalls - the counter that means "a + // seam defect" - for a state that is by design. + if (desc.Target != kMGPipeResourceTargetBuffer && NoP4aConsumer()) return false; + MGPipeResourceRecord* record = ResolveResourceIn(*table, "resource_respecify", desc.Resource); if (record == nullptr) return false; PinNoLiveHostWrites(*record, desc.Resource, "resource_respecify"); @@ -1711,6 +1753,10 @@ namespace MobileGL::MG_Pipe { static_cast(MGPipeSubDataUploadTargetOf(record.Target))); return false; } + // P4a's belt, AFTER the target fault above and before the upload is accumulated: this + // is the call whose acceptance clears a level's dirty flags (D-D5 / ID-18 M3), so it is + // the one that turned "no consumer" into lost texels on Magma. + if (NoP4aConsumer()) return false; return ApplyTextureUpload(record, bytes, regions); } @@ -2200,6 +2246,14 @@ namespace MobileGL::MG_Pipe { // The surfaces' Res handles are not resolved either: an attachment PINS its texture, // and in monolith the frontend's own SharedPtr is that keep-alive, so a refusal here // would be enforcing a lifetime rule monolith cannot need and split has not defined. + // P4a's belt, after the record's own shape checks and before the table is touched. This + // entry point is the ONE the client can reach without passing PipeFill's gate - + // GL_Framebuffer.cpp's PipePublishFramebufferByName calls the emitter directly at the + // fifteen DSA sites - so on a backend with no consumer this is where those records + // stop. RefusedNoConsumer and not RefusedObjectCalls: the framebuffer family's counter + // contract (see the header) is that no framebuffer call ever moves that one. + if (NoP4aConsumer()) return; + MGPipeFramebufferRecord* record = RecordAt(g_applier.FramebufferRecords, state.Fbo.Slot, kMGPipeMaxFramebufferSlots); // Unreachable: the bound was checked above, before anything moved. The null check is @@ -2311,6 +2365,9 @@ namespace MobileGL::MG_Pipe { desc.Cso.Slot, desc.Cso.Gen, kMGPipeMaxSamplerCsoSlots); return; } + // P4a's belt, after the blob rule and the slot bound and before the record moves. + if (NoP4aConsumer()) return; + MGPipeSamplerCsoRecord& record = *recordAt; // A CREATE STARTS THE RECORD OVER AND LEAVES Serial AT 0; A RE-ISSUE ON A LIVE IDENTITY // COUNTS UP. The first half is what stops a recycled slot contributing one field of its @@ -2367,6 +2424,9 @@ namespace MobileGL::MG_Pipe { view.Cso.Slot, view.Cso.Gen, kMGPipeMaxSamplerViewSlots); return; } + // P4a's belt, after the slot bound and before the record moves. + if (NoP4aConsumer()) return; + MGPipeSamplerViewRecord& record = *recordAt; // RE-ISSUING ON THE SAME HANDLE IS HOW A RESTRICTION CHANGE TRAVELS - a view is // identity-addressed one per texture object, minted off that object's lifetime id, and @@ -2411,6 +2471,11 @@ namespace MobileGL::MG_Pipe { } void MGPipeApplySetTextureParams(const MGPTextureParams& params) { + // P4a's belt, and FIRST here because this call's first act is a resolution: with no + // consumer no texture create was accepted, so resolving would report the absence as + // RefusedObjectCalls - the counter that means a seam defect - for the designed state. + if (NoP4aConsumer()) return; + // ADDRESSED BY RESOURCE AND BY NOTHING ELSE, which is the whole point of the call: a // texture that is only an FBO attachment, only an image-unit binding or only a // glCopyImageSubData endpoint has no sampler view to hang its parameters on, and the @@ -2458,7 +2523,16 @@ namespace MobileGL::MG_Pipe { // unit with no texture carries a null resource. None of the three is resolved against a // record either - a set is WORKING STATE, the records it names are OBJECT state, and the // backend resolves the pair at its own sync point where both are current. + // + // P4a's belt sits IN FRONT of ApplyUnitWindow on all three, and this is the one place it + // is not last: ApplyUnitWindow validates the window and writes it in the same step, so + // there is no point between the two to stand at. The cost is that on a backend with no + // consumer a malformed window is declined rather than Fatal - which is the right trade the + // one way round it can be made, because that backend would never have been handed the + // window at all (the client's gate stops it) and the shipped configuration that DOES + // consume these still trips the wire. void MGPipeApplySetSamplerViews(const MGPSamplerViews& hdr, const MGPBoundView* tail) { + if (NoP4aConsumer()) return; if (!ApplyUnitWindow("set_sampler_views", hdr.Start, hdr.Count, hdr.ContentHash, tail, g_applier.BoundSamplerViews, g_applier.SamplerViewStart, g_applier.SamplerViewCount)) { @@ -2468,6 +2542,7 @@ namespace MobileGL::MG_Pipe { } void MGPipeApplyBindSamplerStates(const MGPSamplerStates& hdr, const MGPipeHandle* tail) { + if (NoP4aConsumer()) return; if (!ApplyUnitWindow("bind_sampler_states", hdr.Start, hdr.Count, hdr.ContentHash, tail, g_applier.BoundSamplerStates, g_applier.SamplerStateStart, g_applier.SamplerStateCount)) { @@ -2482,6 +2557,7 @@ namespace MobileGL::MG_Pipe { // sent and recast on the server - the record carries the application's format, and the // bind-format recast that turns a GL_RG32F bind into something 19 of 26 non-core // formats on Adreno will accept is the backend's, not this applier's. + if (NoP4aConsumer()) return; if (!ApplyUnitWindow("set_shader_images", hdr.Start, hdr.Count, hdr.ContentHash, tail, g_applier.BoundShaderImages, g_applier.ShaderImageStart, g_applier.ShaderImageCount)) { @@ -2552,6 +2628,10 @@ namespace MobileGL::MG_Pipe { PinProgramArchiveRoundTrip(desc, *link, *spirv); #endif + // P4a's belt, after the blob rule, the slot bound and the verify round trip - all three + // are checks on the RECORD and stay honest on every backend - and before it is stored. + if (NoP4aConsumer()) return; + MGPipeShaderCsoRecord& record = *recordAt; // A RE-ISSUE ON THE SAME HANDLE IS HOW A RELINK TRAVELS: the handle is minted per // frontend program and Gen moves only on slot reuse, so an existing record of the same @@ -2583,6 +2663,11 @@ namespace MobileGL::MG_Pipe { void MGPipeApplyBindShaderState(const MGPHandleOnly& handle) { MOBILEGL_ASSERT(handle.Kind == static_cast(MGPipeKind::ShaderCso), "bind_shader_state on kind %u", handle.Kind); + // P4a's belt, and first for set_texture_params' reason: with no consumer this applier + // holds no shader CSO record, so the resolution below would report the designed state + // as RefusedObjectCalls. The null-handle unbind is behind it too - a binding this + // applier never made is not one it may clear. + if (NoP4aConsumer()) return; // The null handle is legal and means "nothing bound", which is a GL state and not an // error; a DEAD handle leaves the previous binding untouched and is counted, which is // bind_render_state's precedent for the same question. @@ -2639,6 +2724,7 @@ namespace MobileGL::MG_Pipe { void MGPipeApplySetDrawProgram(const MGPHandleOnly& handle) { MOBILEGL_ASSERT(handle.Kind == static_cast(MGPipeKind::ShaderCso), "set_draw_program on kind %u", handle.Kind); + if (NoP4aConsumer()) return; // P4a's belt, for MGPipeApplyBindShaderState's reason. if (MGPipeHandleIsNull(handle.Handle)) { g_applier.DrawProgram = kMGPipeNullHandle; ++g_applier.ProgramBindingSerial; @@ -2653,6 +2739,7 @@ namespace MobileGL::MG_Pipe { void MGPipeApplySetDispatchProgram(const MGPHandleOnly& handle) { MOBILEGL_ASSERT(handle.Kind == static_cast(MGPipeKind::ShaderCso), "set_dispatch_program on kind %u", handle.Kind); + if (NoP4aConsumer()) return; // P4a's belt, for MGPipeApplyBindShaderState's reason. if (MGPipeHandleIsNull(handle.Handle)) { g_applier.DispatchProgram = kMGPipeNullHandle; ++g_applier.ProgramBindingSerial; @@ -2668,6 +2755,12 @@ namespace MobileGL::MG_Pipe { // ON THE PROGRAM'S RECORD, not in the working state, and that is what makes it survive a // make-current: the block is (ShaderCso, Version) keyed and belongs to the program, not // to the context that last uploaded it. + // + // P4a's belt goes in front of the resolution for set_texture_params' reason, and it is + // in front of the fault block too because THIS call's bounds come out of the resolved + // program's own GlobalUboSize - there is nothing to validate against until the record + // is in hand. + if (NoP4aConsumer()) return; MGPipeShaderCsoRecord* stored = ResolveShaderCso("set_global_constants", record.ShaderCso); if (stored == nullptr) return; diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index f183f3d4..133e26f7 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -523,6 +523,35 @@ namespace MobileGL::MG_Pipe { // that is Fatal{ProtocolCorruption}, not a dropped call. Uint64 RefusedObjectCalls = 0; + // P4a's BELT (ID-39): every call in one of the four families P4a migrates that this + // applier declined because NO BACKEND HAS REGISTERED MGPipeResourceOps - i.e. because + // nothing in this process consumes what the record publishes. + // + // WHY THE APPLIER ASKS A QUESTION ABOUT THE BACKEND AT ALL, when it is otherwise + // backend-neutral: acceptance is a CONTRACT WITH THE CLIENT since ID-18 M3. The + // emitters clear a texture level's dirty flags, advance their descriptor mirrors and + // latch their suppressors on the answer this applier returns, so an applier that + // accepts a record nothing will ever read makes the client forget work the legacy pull + // path still owed - which is exactly how 66 texture-upload-shaped DirectVulkan cases + // went red on the push build (ID-39). The client's own gate + // (MG_Impl/Pipe/PipeFill.cpp's FamilyIsLive) stops the emission upstream; this is the + // belt under it, so a record that reaches here by any other route - GL_Framebuffer.cpp's + // PipePublishFramebufferByName calls its emitter directly, without passing PipeFill - + // is declined rather than accepted. + // + // IT IS NOT A DEFECT COUNTER, WHICH IS WHY IT IS SILENT. RefusedResourceCalls, + // RefusedVertexInputCalls and RefusedObjectCalls each mean "a record named something + // this applier should have had"; a non-zero value there is a seam defect. A non-zero + // value HERE is the designed steady state of a backend with no P4a twins, so logging it + // would put an ERROR line in every ordinary Magma run. The number is the observable. + // + // THE DEATH PATHS ARE DELIBERATELY NOT ON THIS LIST. resource_destroy, + // delete_sampler_state, delete_sampler_view and delete_shader_state are idempotent + // cleanup that must keep working whatever the registration did, and with no consumer + // there is no record for them to find anyway (they count their own refusal). Per + // context and cleared by MGPipeApplierReset, like the three above it. + Uint64 RefusedNoConsumer = 0; + // ---- working state: what the next draw fetches with. All of it is per context and // all of it is cleared by MGPipeApplierReset, EXCEPT the two serials, which only ever // advance (see there). diff --git a/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp b/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp index 060f72cb..147bb373 100644 --- a/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp @@ -1141,6 +1141,20 @@ int main(int argc, char** argv) { _putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str()); #else setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1); +#endif +#if MOBILEGL_PIPE_PUSH + // ID-39: A BACKEND IS PRESENT, for the whole binary. Since ID-39 every P4a-family entry + // point in MG_Pipe/PipeApply.cpp declines a record - and the client's gate in + // MG_Impl/Pipe/PipeFill.cpp emits none at all - when no backend has registered + // MGPipeResourceOps, because acceptance is a contract with the emitter and an accepted + // record nothing reads makes the client clear work the legacy pull path still owed. Every + // case in this suite is about the arm where a backend DOES consume the records, which is + // the shipped DirectGLES configuration, so it installs the same signal that backend + // installs. The table is empty because none of its hooks is on a framebuffer path at all: + // set_framebuffer_state stores a record and dispatches nothing. The two arms of the rule + // itself are pinned in ResourceEmitTest and TextureEmitTest. + static const MGPipeResourceOps kConsumerPresent{}; + MGPipeSetResourceOps(&kConsumerPresent); #endif ::testing::InitGoogleTest(&argc, argv); const int rc = RUN_ALL_TESTS(); diff --git a/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp b/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp index 4d98e252..d1d9f696 100644 --- a/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp @@ -156,6 +156,27 @@ namespace { } }; + // "A BACKEND IS PRESENT", which since ID-39 is a thing the applier ASKS: every P4a-family + // entry point declines a record - and counts RefusedNoConsumer - when no backend has + // registered MGPipeResourceOps, because acceptance is a contract with the emitter and an + // accepted record nothing will read makes the client clear a dirty flag the legacy pull + // path still owed. A case that wants the P4a half of this applier to behave as it does + // under DirectGLES scopes this on; the case that wants the OTHER arm simply does not. + // + // THE TABLE IS EMPTY AND THAT IS DELIBERATE. Its hooks are the BUFFER family's, and every + // non-buffer resource row is stored and returned rather than dispatched (see + // MGPipeApplyResourceCreate) - so what registering it changes here is the consumer question + // and nothing else. It nests: the previous table is restored, not nulled. + struct ScopedResourceOps { + ScopedResourceOps() : m_saved(MGPipeGetResourceOps()) { + static const MGPipeResourceOps kEmpty{}; + MGPipeSetResourceOps(&kEmpty); + } + ~ScopedResourceOps() { MGPipeSetResourceOps(m_saved); } + + const MGPipeResourceOps* m_saved; + }; + MGPResourceDesc BufferDesc(MGPipeHandle res, Uint32 width, Uint32 glName) { MGPResourceDesc desc{}; desc.Resource = res; @@ -1355,6 +1376,9 @@ namespace { GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build"; #else ApplierGuard guard; + // The texture and renderbuffer rows below are P4a's, and P4a's belt declines those on a + // backend that consumes none of them (ID-39) - so this case says which arm it is about. + ScopedResourceOps consumer; const MGPipeHandle shared{7, 3}; MGPipeApplyResourceCreate(TargetedDesc(shared, MGPipeResourceTarget::Buffer, 0, 11)); @@ -1393,6 +1417,195 @@ namespace { #endif } + // ID-39: THE APPLIER'S HALF OF THE "NO CONSUMER" RULE, over every P4a-family entry point. + // + // WHY AN APPLIER ASKS A QUESTION ABOUT THE BACKEND AT ALL is written beside + // MGPipeApplierState::RefusedNoConsumer: acceptance became a CONTRACT WITH THE CLIENT at + // ID-18 M3 - the emitters clear a texture level's dirty flags, advance their descriptor + // mirrors and latch their suppressors on the answer these calls return - so an applier that + // accepts a record nothing in the process will ever read makes the client forget work the + // legacy pull path still owed. On DirectVulkan, which registers no MGPipeResourceOps and + // has none of P4a's twins, that put 66 texture-upload-shaped integration-gpu cases red on + // the push build while the pull build stayed 966/966 green. + // + // IT IS A BELT AND NOT THE GATE. The client's gate is FamilyIsLive in + // MG_Impl/Pipe/PipeFill.cpp (pinned by TextureEmit.WithNoBackendConsumerTheFamilyGateIsFalse + // AndNothingReachesTheApplier) and it stops the emission upstream. This is under it, and it + // is not redundant: GL_Framebuffer.cpp's PipePublishFramebufferByName reaches the + // framebuffer emitter DIRECTLY at the fifteen DSA sites, without passing through PipeFill, + // so set_framebuffer_state is a record that can arrive here on a backend with no consumer. + // + // THE DEATH PATHS ARE DELIBERATELY NOT IN THE LIST and the second half of the case says so: + // a destroy is idempotent cleanup that must keep working whatever the registration did. + TEST(ResourceEmit, EveryP4aFamilyEntryPointDeclinesWhenNoBackendRegisteredTheConsumer) { +#if !MOBILEGL_PIPE_PUSH + GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build"; +#else + ApplierGuard guard; // leaves the table UNREGISTERED, which is this half's whole point + ASSERT_EQ(MGPipeGetResourceOps(), nullptr); + + const MGPipeHandle texture{7, 3}; + const MGPipeHandle cso{9, 1}; + const MGPipeHandle fbo{4, 2}; + + MGPTextureParams params{}; + params.Res = texture; + params.BuiltinSampler = cso; + + MGPSubData upload{}; + upload.Res = texture; + upload.Target = MGPipePackSubDataTarget(static_cast(MGPipeResourceTarget::Tex2D), 0u); + + MGPFramebufferState fboState{}; + fboState.Fbo = fbo; + fboState.Target = static_cast(MGPipeFramebufferTarget::Draw); + + MGPSamplerDesc samplerDesc{}; + samplerDesc.Cso = cso; + const SamplerParameters samplerParams{}; + + MGPSamplerView view{}; + view.Cso = cso; + view.Texture = texture; + + const MGPSamplerViews viewSet{0, 1, 0}; + const MGPBoundView viewTail[1]{}; + const MGPSamplerStates stateSet{0, 1, 0}; + const MGPipeHandle stateTail[1]{kMGPipeNullHandle}; + const MGPShaderImages imageSet{0, 1, 0}; + const MGPImageView imageTail[1]{}; + + MGPProgramDesc program{}; + program.Cso = cso; + program.StageMask = 0x3u; + const MG_State::GLState::LinkArtifacts link; + const MG_State::GLState::SpirvArtifacts spirv; + + MGPGlobalConstants constants{}; + constants.ShaderCso = cso; + + const MGPHandleOnly csoHandle{cso, static_cast(MGPipeKind::ShaderCso), 0}; + + // ---- with no consumer: every one of them declines, and NOTHING is stored ---- + EXPECT_FALSE(MGPipeApplyResourceCreate(TargetedDesc(texture, MGPipeResourceTarget::Tex2D, 0, 22))); + EXPECT_FALSE( + MGPipeApplyResourceRespecify(TargetedDesc(texture, MGPipeResourceTarget::Tex2D, 64, 22), nullptr)); + EXPECT_FALSE(MGPipeApplyResourceSubData(upload, nullptr)); + MGPipeApplySetTextureParams(params); + MGPipeApplySetFramebufferState(fboState); + MGPipeApplyCreateSamplerState(samplerDesc, &samplerParams); + MGPipeApplyCreateSamplerView(view); + MGPipeApplySetSamplerViews(viewSet, viewTail); + MGPipeApplyBindSamplerStates(stateSet, stateTail); + MGPipeApplySetShaderImages(imageSet, imageTail); + MGPipeApplyCreateShaderState(program, &link, &spirv); + MGPipeApplyBindShaderState(csoHandle); + MGPipeApplySetDrawProgram(csoHandle); + MGPipeApplySetDispatchProgram(csoHandle); + MGPipeApplySetGlobalConstants(constants, nullptr); + + // FIFTEEN CALLS, FIFTEEN REFUSALS, AND THE NUMBER IS THE ASSERTION: an entry point that + // is added to a P4a family later and forgets the belt makes this line fail rather than + // silently accepting a record on a backend that reads none. + EXPECT_EQ(MGPipeApplier().RefusedNoConsumer, 15u); + // AND NOT ONE OF THE OTHER THREE MOVED. The refusal is a configuration fact, not a seam + // defect, so it must not read as one to an operator grepping the counters. + EXPECT_EQ(MGPipeApplier().RefusedResourceCalls, 0u); + EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u); + EXPECT_EQ(MGPipeApplier().StaleFramebufferRecordLookups, 0u); + + // NOTHING IS LIVE, which is the property, rather than "no table exists". The belt stands + // AFTER each entry point's own record-shape checks so that a malformed record is + // Fatal{ProtocolCorruption} on every backend and not only on the ones that consume - and + // the bound check for the four create-shaped calls IS RecordAt, which grows the table to + // the slot on its way to answering. So a refused create may leave a zeroed row behind + // and stores nothing in it. It costs nothing where it matters: on a backend with no + // consumer the client's gate emits none of these at all, and the one record that reaches + // this applier without passing that gate - set_framebuffer_state, published by name from + // GL_Framebuffer.cpp - is declined in front of its RecordAt. + const auto nothingLiveAt = [](const auto& table, SizeT slot) { + return table.size() <= slot || !table[slot].Live; + }; + EXPECT_TRUE(nothingLiveAt(MGPipeApplier().TextureResources, 7)); + EXPECT_TRUE(nothingLiveAt(MGPipeApplier().SamplerCsos, 9)); + EXPECT_TRUE(nothingLiveAt(MGPipeApplier().SamplerViewCsos, 9)); + EXPECT_TRUE(nothingLiveAt(MGPipeApplier().ShaderCsos, 9)); + // set_framebuffer_state's table is the one that must not even be grown: it is declined + // in front of its RecordAt, because it is the one call a backend with no consumer can + // actually receive. + EXPECT_TRUE(MGPipeApplier().FramebufferRecords.empty()); + EXPECT_EQ(MGPipeApplier().SamplerViewCount, 0u); + EXPECT_EQ(MGPipeApplier().SamplerStateCount, 0u); + EXPECT_EQ(MGPipeApplier().ShaderImageCount, 0u); + EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundShaderCso)); + EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().DrawProgram)); + EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().DispatchProgram)); + EXPECT_TRUE(MGPipeHandleIsNull( + MGPipeApplier().BoundFramebuffer[static_cast(MGPipeFramebufferTarget::Draw)])); + + // ---- and with one, every one of them lands. Same records, same order ---- + { + ScopedResourceOps consumer; + const Uint64 refusalsBefore = MGPipeApplier().RefusedNoConsumer; + + EXPECT_TRUE( + MGPipeApplyResourceCreate(TargetedDesc(texture, MGPipeResourceTarget::Tex2D, 0, 22))); + EXPECT_TRUE(MGPipeApplyResourceRespecify( + TargetedDesc(texture, MGPipeResourceTarget::Tex2D, 64, 22), nullptr)); + MGPipeApplySetTextureParams(params); + MGPipeApplySetFramebufferState(fboState); + MGPipeApplyCreateSamplerState(samplerDesc, &samplerParams); + MGPipeApplyCreateSamplerView(view); + MGPipeApplySetSamplerViews(viewSet, viewTail); + MGPipeApplyBindSamplerStates(stateSet, stateTail); + MGPipeApplySetShaderImages(imageSet, imageTail); + MGPipeApplyCreateShaderState(program, &link, &spirv); + MGPipeApplyBindShaderState(csoHandle); + MGPipeApplySetDrawProgram(csoHandle); + MGPipeApplySetDispatchProgram(csoHandle); + MGPipeApplySetGlobalConstants(constants, nullptr); + + EXPECT_EQ(MGPipeApplier().RefusedNoConsumer, refusalsBefore); + ASSERT_GT(MGPipeApplier().TextureResources.size(), 7u); + EXPECT_TRUE(MGPipeApplier().TextureResources[7].Live); + EXPECT_EQ(MGPipeApplier().TextureResources[7].Desc.Width, 64u); + EXPECT_EQ(MGPipeApplier().TextureResources[7].Params.BuiltinSampler, cso); + ASSERT_GT(MGPipeApplier().FramebufferRecords.size(), 4u); + EXPECT_TRUE(MGPipeApplier().FramebufferRecords[4].Live); + EXPECT_EQ(MGPipeApplier().BoundFramebuffer[static_cast(MGPipeFramebufferTarget::Draw)], + fbo); + ASSERT_GT(MGPipeApplier().SamplerCsos.size(), 9u); + EXPECT_TRUE(MGPipeApplier().SamplerCsos[9].Live); + ASSERT_GT(MGPipeApplier().SamplerViewCsos.size(), 9u); + EXPECT_TRUE(MGPipeApplier().SamplerViewCsos[9].Live); + ASSERT_GT(MGPipeApplier().ShaderCsos.size(), 9u); + EXPECT_TRUE(MGPipeApplier().ShaderCsos[9].Live); + EXPECT_EQ(MGPipeApplier().SamplerViewCount, 1u); + EXPECT_EQ(MGPipeApplier().SamplerStateCount, 1u); + EXPECT_EQ(MGPipeApplier().ShaderImageCount, 1u); + EXPECT_EQ(MGPipeApplier().BoundShaderCso, cso); + EXPECT_EQ(MGPipeApplier().DrawProgram, cso); + EXPECT_EQ(MGPipeApplier().DispatchProgram, cso); + // resource_subdata's ACCEPTED path wants a real destination box against real + // storage, which is a texture-emitter fixture and not this file's; it is proved end + // to end by TextureEmit.WithNoBackendConsumerTheFamilyGateIsFalseAndNothingReaches + // TheApplier's second half (SubDataCount 1, RefusedSubDataCount 0). Driving a + // half-built record through it here would trip the upload validator's own wire, + // which is a different rule and not this case's. + + // AND THE DEATH PATHS ARE NOT BELTED, which is the other half of the ruling: they + // are idempotent cleanup and they run on whatever the registration is. Driven with + // the table registered here and asserted UNCOUNTED, so that a later commit which + // adds them to the belt has to change this line. + MGPipeApplyDeleteShaderState(csoHandle); + MGPipeApplyResourceDestroy(KindHandle(texture, MGPipeKind::Texture)); + EXPECT_EQ(MGPipeApplier().RefusedNoConsumer, refusalsBefore); + EXPECT_FALSE(MGPipeApplier().ShaderCsos[9].Live); + EXPECT_FALSE(MGPipeApplier().TextureResources[7].Live); + } +#endif + } + // Neither branch may fall through to a table it was not named. A target or a kind outside // the catalogue would otherwise land in whichever table the code happened to reach first, // and destroy a live object of a kind the call was never about. diff --git a/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp b/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp index 9fae130f..c3fa7423 100644 --- a/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp @@ -57,6 +57,7 @@ #include "Includes.h" #include #if MOBILEGL_PIPE_PUSH +#include #include #include #include @@ -270,6 +271,20 @@ namespace { Uint64 m_previousPush = 0; }; + // ID-39's OTHER ARM, scoped. This binary registers an (empty) MGPipeResourceOps table in + // main() because the whole suite is about the emitter and the applier as they behave under + // a backend that CONSUMES what they publish - DirectGLES, which registers the table at its + // own bring-up. The one case that is about a backend with no consumer takes the table away + // for its own duration and puts it back. + struct ScopedNoResourceOps { + ScopedNoResourceOps() : m_saved(MGPipeGetResourceOps()) { MGPipeSetResourceOps(nullptr); } + ~ScopedNoResourceOps() { MGPipeSetResourceOps(m_saved); } + ScopedNoResourceOps(const ScopedNoResourceOps&) = delete; + ScopedNoResourceOps& operator=(const ScopedNoResourceOps&) = delete; + + const MGPipeResourceOps* m_saved; + }; + MGPipeTextureEmitter& Textures() { return MGPipeTextureEmitterInstance(); } GLContext& Ctx() { return *MG_State::pGLContext; } @@ -810,6 +825,90 @@ TEST(TextureEmit, ARefusedUploadLeavesTheLevelDirtyAndOnTheDrainList) { EXPECT_EQ(Textures().DrainListSize(), 0u); } +// ID-39: THE CLIENT'S HALF OF THE "NO CONSUMER" RULE, and the defect it closes. +// +// A P4a family's gate used to be two conjuncts - the operator's subsystem bit in +// MOBILEGL_PIPE_PUSH, and this build having WIRED the family. P3a's buffers have always had a +// THIRD (MGPipeResourceSubsystemEnabled() is bit 7 AND MGPipeGetResourceOps() != nullptr) and +// P4a's four families did not. Magma (DirectVulkan) registers no table and has none of P4a's +// twins, so with kMGPipeWiredTextureSubsystem = 1 the client emitted, the applier ACCEPTED, the +// emitter cleared the level's dirty flags on that acceptance (D-D5 as amended by ID-18 M3), and +// Magma's legacy upload path then found nothing to upload: 66 texture-upload-shaped +// DirectVulkan integration-gpu cases red on the push build while the pull build stayed green. +// +// WHAT THIS PINS IS "NOTHING AT ALL", NOT "LESS". 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. The applier's +// belt (ResourceEmit.EveryP4aFamilyEntryPointDeclinesWhenNoBackendRegisteredTheConsumer) is +// under this and is asserted here to have caught NOTHING: if it had, the gate would be the thing +// that failed. +TEST(TextureEmit, WithNoBackendConsumerTheFamilyGateIsFalseAndNothingReachesTheApplier) { + TextureScope scope; + { + ScopedNoResourceOps noConsumer; + + // All four families, because all four ride the one signal (D-D1: a texture and a + // renderbuffer ARE resource rows, and the other three name texture handles). + EXPECT_FALSE( + MGPipeP4aFamilyEmits(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)); + EXPECT_FALSE(MGPipeP4aFamilyEmits(kMGPipeSubsystemFramebuffer, kMGPipeSubsystemFramebuffer)); + EXPECT_FALSE(MGPipeP4aFamilyEmits(kMGPipeSubsystemSamplers, kMGPipeSubsystemSamplers)); + EXPECT_FALSE(MGPipeP4aFamilyEmits(kMGPipeSubsystemPrograms, kMGPipeSubsystemPrograms)); + // And the P2/P3a families are NOT narrowed by it - their bits are outside + // kMGPipeP4aFamilySubsystems, which is what makes "nothing that emits today changes" + // checkable rather than asserted. The bit is set here because TextureScope sets only + // the two this suite needs, and restored before anything else runs. + const Uint64 savedPush = MG_Config::Features.PipePush; + MG_Config::Features.PipePush |= kMGPipeSubsystemVertexInput; + EXPECT_TRUE(MGPipeP4aFamilyEmits(kMGPipeSubsystemVertexInput, kMGPipeSubsystemVertexInput)); + MG_Config::Features.PipePush = savedPush; + + const auto texture = MakeTexture2D(41, 32); + const MGPipeHandle handle = Textures().FindTexture(*texture); + // THE HANDLE IS STILL MINTED, deliberately: the mint is unconditional (PipeFill.cpp), + // costs one free-list pop and emits nothing, and other families name a texture by + // handle whether or not this family is switched on. What the gate withholds is the + // EMISSION, never the identity. + ASSERT_FALSE(MGPipeHandleIsNull(handle)); + + EXPECT_FALSE(MGPipeHandleIsPublished(MGPipeKind::Texture, handle)) + << "a create was published to an applier no backend reads"; + 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) << "a level was queued for a drain that has no consumer"; + EXPECT_EQ(Textures().SubDataCount(), 0u); + // THE ONE THAT MATTERED. The frontend's flag is what Magma's legacy path uploads from, + // and clearing it on an acceptance nobody would read is the whole of the defect. + EXPECT_TRUE(texture->IsStorageDirty(TextureUploadTarget::Texture2D, 0)) + << "the level's dirty flag was cleared on a backend whose legacy path still owes the " + "upload, so those texels exist nowhere"; + + EXPECT_EQ(MGPipeApplier().RefusedNoConsumer, 0u) + << "the client emitted anyway and the applier's belt caught it; the GATE is what must " + "have stopped it"; + } + + // ---- and with a consumer registered, the same sequence lands ---- + EXPECT_TRUE(MGPipeP4aFamilyEmits(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)); + const auto consumed = MakeTexture2D(42, 32); + const MGPipeHandle live = Textures().FindTexture(*consumed); + ASSERT_FALSE(MGPipeHandleIsNull(live)); + EXPECT_TRUE(MGPipeHandleIsPublished(MGPipeKind::Texture, live)); + EXPECT_GT(Textures().CreateCount(), 0u); + consumed->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_EQ(Textures().RefusedSubDataCount(), 0u); + EXPECT_EQ(MGPipeApplier().RefusedNoConsumer, 0u); + EXPECT_FALSE(consumed->IsStorageDirty(TextureUploadTarget::Texture2D, 0)); +} + // ============================ M4 ============================ // // THE CANONICAL ORDER FOR THE TEXTURES THE HINT WAS WRITTEN FOR: glTexStorage2D, then @@ -1623,6 +1722,20 @@ int main(int argc, char** argv) { _putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str()); #else setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1); +#endif +#if MOBILEGL_PIPE_PUSH + // ID-39: A BACKEND IS PRESENT, for the whole binary. Since ID-39 every P4a-family entry + // point in MG_Pipe/PipeApply.cpp declines a record - and the client's own gate in + // MG_Impl/Pipe/PipeFill.cpp emits none at all - when no backend has registered + // MGPipeResourceOps, because acceptance is a contract with the emitter and an accepted + // record nothing reads makes the client clear a dirty flag the legacy pull path still owed. + // Every case in this suite is about the arm where a backend DOES consume the records, which + // is the shipped DirectGLES configuration, so the suite installs the same signal that + // backend installs. The table is empty because none of its hooks is on a texture path: a + // non-buffer resource row is stored and returned, never dispatched. + // WithNoBackendConsumerTheFamilyGateIsFalseAndNothingReachesTheApplier takes it away again. + static const MGPipeResourceOps kConsumerPresent{}; + MGPipeSetResourceOps(&kConsumerPresent); #endif ::testing::InitGoogleTest(&argc, argv); const int rc = RUN_ALL_TESTS();