[Fix] (MG_Pipe, MG_Impl): gate the four P4a families on a backend having registered MGPipeResourceOps - Magma emitted, the applier accepted, and the acceptance-cleared dirty flags left its legacy path nothing to upload

This commit is contained in:
rereview
2026-09-08 17:37:37 -04:00
parent 17396216e0
commit 29d51ab98b
7 changed files with 570 additions and 11 deletions
+89 -11
View File
@@ -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);
+19
View File
@@ -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.
//
+93
View File
@@ -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<Uint32>(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<Uint32>(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<Uint32>(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<Uint32>(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;
+29
View File
@@ -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).
@@ -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();
+213
View File
@@ -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<Uint32>(MGPipeResourceTarget::Tex2D), 0u);
MGPFramebufferState fboState{};
fboState.Fbo = fbo;
fboState.Target = static_cast<Uint8>(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<Uint32>(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<SizeT>(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<SizeT>(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.
+113
View File
@@ -57,6 +57,7 @@
#include "Includes.h"
#include <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/PipeFill.h>
#include <MG_Impl/Pipe/SamplerEmit.h>
#include <MG_Impl/Pipe/TextureEmit.h>
#include <MG_Pipe/PipeApply.h>
@@ -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();