diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 7d09c564..1494b8e8 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -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 diff --git a/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp b/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp index 8b00106d..4098c701 100644 --- a/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp @@ -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 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(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