mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 12:48:32 +09:00
[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:
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user