[Fix] (MG_Impl, MG_Test): publish the create a respecify's handle never got - a buffer born while the resource op table was unregistered had every later respecify refused and drew through id 0 with no diagnostic

This commit is contained in:
2026-09-08 07:00:06 -04:00
parent 9ba5d7ba1e
commit 433f51a065
2 changed files with 84 additions and 0 deletions
+27
View File
@@ -634,6 +634,33 @@ namespace MobileGL::MG_Pipe {
const MGPipeHandle handle = tracker.Acquire(buffer);
Uint16 bindMask = tracker.BindMask(handle);
if (auto* ctx = LiveContext()) bindMask = tracker.RefreshBindMask(*ctx, buffer, handle);
// M-1: THE CREATE FIRST, IF THIS HANDLE NEVER PUBLISHED ONE - which makes the
// create/destroy latch self-healing in both directions instead of only one.
//
// The constructor's create is gated on MGPipeResourceSubsystemEnabled(), which is bit 7
// AND "a backend registered MGPipeResourceOps"; the CONSUMER's gate is bit 7 alone. The
// two disagree across a register/unregister boundary, and there is a real window:
// UnregisterBufferBackendOps nulls the table from OnBackendContextDestroyed
// (DestroyEGLContext) and the re-register happens at the next MakeCurrent, while D-A2
// deliberately keeps NotifySubData reachable off the render thread. A buffer born in
// that window latched Published = false, so the applier had no record for it and every
// later respecify was REFUSED - after which EnsureBufferResourceForHandle read
// ResourceRecordOf == nullptr, took size 0, returned a twin with no store and drew
// through id 0, with no diagnostic anywhere. The legacy arm recovers from the same
// window by twinning lazily off the frontend object and full-uploading from the shadow;
// this is the handle arm's equivalent, and it costs one bool compare per respecify.
//
// A create rather than a respecify because that is what the record's absence means: the
// applier starts the record over on a create (it does not edit one), so this cannot
// resurrect a field from a recycled slot, and the respecify below then defines the
// storage exactly as it would have.
if (!tracker.WasPublished(handle)) {
const MGPResourceDesc createDesc =
MGPipeBuildResourceDesc(buffer, handle, bindMask, /*storageDefined=*/false);
tracker.NoteDesc(createDesc, true);
tracker.NotePublished(handle);
MGPipeApplyResourceCreate(createDesc);
}
const MGPResourceDesc desc = MGPipeBuildResourceDesc(buffer, handle, bindMask, true);
tracker.NoteDesc(desc, false);
// initialBytes is the client's own shadow base - zero copy, and null is a real answer
@@ -1791,6 +1791,63 @@ namespace {
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::Buffer, handle));
EXPECT_EQ(MGPipeApplier().RefusedResourceCalls, 0u);
}
// M-1: ...AND THE LATCH HEALS IN THE OTHER DIRECTION TOO. The case above covers a buffer
// that was published and then lost its backend; this is the mirror - a buffer BORN while no
// resource op table was registered, which is a real window and not a theoretical one:
// UnregisterBufferBackendOps nulls the table from OnBackendContextDestroyed and the
// re-register happens at the next MakeCurrent, while D-A2 keeps the content path reachable
// off the render thread.
//
// Before the repair the buffer latched Published = false, so no applier record existed;
// every later respecify was REFUSED and the backend's ensure path then read a null record,
// took size 0 and drew through id 0, silently, for the object's whole life. The legacy arm
// recovers from the same window by twinning lazily and full-uploading from the shadow.
TEST(ResourceEmit, ARespecifyPublishesTheCreateAHandleNeverGot) {
PushArm arm;
MGPipeResourceTracker& tracker = MGPipeResourceTrackerInstance();
// The window: the table is gone, so the constructor mints the handle (unconditional)
// and emits nothing.
MGPipeSetResourceOps(nullptr);
ASSERT_FALSE(MGPipeResourceSubsystemEnabled());
const SharedPtr<BufferObject> buffer = MakeBuffer(1);
const MGPipeHandle handle = tracker.Find(*buffer);
ASSERT_FALSE(MGPipeHandleIsNull(handle)) << "the constructor did not mint a handle";
ASSERT_FALSE(tracker.WasPublished(handle));
// Not through RecordOf: the applier's vector may not even reach this slot yet, which is
// the whole point, and RecordOf would index past its end to find out.
ASSERT_TRUE(MGPipeApplier().Resources.size() <= static_cast<SizeT>(handle.Slot) ||
!MGPipeApplier().Resources[handle.Slot].Live)
<< "a create went out with no table registered";
// The window closes - MakeCurrent re-registers - and the application defines the store.
MGPipeSetResourceOps(&arm.m_ops);
ASSERT_TRUE(MGPipeResourceSubsystemEnabled());
const Uint64 createsBefore = tracker.CreateCount();
const Uint64 refusalsBefore = MGPipeApplier().RefusedResourceCalls;
buffer->Respecify(256, nullptr);
EXPECT_EQ(tracker.CreateCount(), createsBefore + 1)
<< "the respecify did not publish the create this handle never got, so the applier "
"still has no record to respecify into";
EXPECT_EQ(MGPipeApplier().RefusedResourceCalls, refusalsBefore)
<< "the respecify was refused: the record the create should have opened is missing";
EXPECT_TRUE(tracker.WasPublished(handle)) << "the repair did not latch";
ASSERT_TRUE(RecordOf(handle.Slot).Live);
EXPECT_EQ(RecordOf(handle.Slot).Gen, handle.Gen);
EXPECT_EQ(RecordOf(handle.Slot).Desc.Width, 256u)
<< "the storage the repair's create deliberately does not carry was not defined by "
"the respecify that follows it";
// ...and the repair is once, not per respecify.
const Uint64 createsAfterRepair = tracker.CreateCount();
buffer->Respecify(512, nullptr);
EXPECT_EQ(tracker.CreateCount(), createsAfterRepair)
<< "every respecify re-published a create; the latch is not being read";
EXPECT_EQ(RecordOf(handle.Slot).Desc.Width, 512u);
EXPECT_EQ(MGPipeApplier().RefusedResourceCalls, refusalsBefore);
}
#endif // MOBILEGL_PIPE_PUSH
} // namespace