From 3160c4b85bcfacd6123d54e5caf2185e8d7037c0 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 6 Sep 2026 08:12:45 -0400 Subject: [PATCH] [Test] (Espryt): pin the twin table identity contract - a reclaimed slot is a new handle and the stale one resolves to nothing - Five cases against BackendSlotTable directly, through a stand-in state object that carries only GetLifetimeId(), so none of them needs a GLContext, a driver or ES entry points. - The load-bearing one is the ABA: an object dies, the sweep returns its slot, the next object takes the same slot with a moved generation, and the predecessor handle answers null instead of the successor twin. That is the property the address key could only paper over with a weak_ptr. - Gen moves on reuse and only on reuse; Find never mutates the table (which is what lets SyncTextureObjectToBackend drop its second Find); a whole table saves, resets with = {} and restores, which is the shape ScopedDirectGLESTextureBindings needs; and two tables of one kind agree on a single object handle, because the identity comes from the client allocator rather than from either table. --- MobileGL/MG_Test/SanityTest.cpp | 166 ++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index 7fd2a8a2..7eafcff2 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -3132,3 +3132,169 @@ TEST(GetterSanity, CombinedUniformComponentsSaturateInsteadOfOverflowing) { MG_State::pGLContext.reset(); } + + +#if MOBILEGL_PIPE_PUSH +namespace { + // A stand-in frontend object for the twin table. It carries the one thing the table asks of a + // state object - GetLifetimeId() - so these cases can pin the identity contract without a + // GLContext, a driver or a backend twin that would want ES entry points. + struct FakeStateObject { + explicit FakeStateObject(MobileGL::Uint64 lifetimeId): m_lifetimeId(lifetimeId) {} + MobileGL::Uint64 GetLifetimeId() const { return m_lifetimeId; } + + private: + MobileGL::Uint64 m_lifetimeId; + }; + + struct FakeBackendObject { + int marker = 0; + }; + + // Kind Query is unused by every shipping path, so these cases cannot disturb the slot space + // any real twin table allocates out of. + using FakeSlotTable = MobileGL::MG_Backend::DirectGLES:: + BackendSlotTable; +} // namespace + +// The property the whole slice exists for. The pre-P2 registry keyed twins on the frontend heap +// ADDRESS and defended the recycle with a weak_ptr; here the key is {slot, gen}, so a successor +// object landing on a slot its predecessor owned is a DIFFERENT handle, and the predecessor's +// handle resolves to nothing rather than to the successor's twin. +TEST(DirectGLESSlotTable, ARecycledSlotIsANewHandleAndTheStaleOneResolvesToNothing) { + using namespace MobileGL; + + FakeSlotTable table; + auto first = MakeShared(0xA1u); + table.GetOrCreate(first) = MakeShared(); + (*table.Find(first.get()))->marker = 1; + + const MG_Pipe::MGPipeHandle firstHandle = table.HandleOf(first.get()); + ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(firstHandle)); + EXPECT_EQ(table.LiveCount(), 1u); + + // The frontend object dies and the slot is reclaimed - which is the only moment Gen moves. + first.reset(); + table.CollectGarbageNow(); + EXPECT_EQ(table.LiveCount(), 0u); + EXPECT_EQ(table.FindByHandle(firstHandle), nullptr) + << "a handle whose object is gone still resolved to a twin"; + + auto second = MakeShared(0xA2u); + table.GetOrCreate(second) = MakeShared(); + (*table.Find(second.get()))->marker = 2; + + const MG_Pipe::MGPipeHandle secondHandle = table.HandleOf(second.get()); + EXPECT_EQ(secondHandle.Slot, firstHandle.Slot) << "the free list did not hand the slot back"; + EXPECT_NE(secondHandle.Gen, firstHandle.Gen) << "the generation did not move on slot reuse"; + EXPECT_FALSE(firstHandle == secondHandle); + + // The stale handle must not resolve to its successor's twin. This is the ABA the address key + // could only paper over. + EXPECT_EQ(table.FindByHandle(firstHandle), nullptr); + ASSERT_NE(table.FindByHandle(secondHandle), nullptr); + EXPECT_EQ((*table.FindByHandle(secondHandle))->marker, 2); +} + +// Gen moves on reuse and ONLY on reuse: a live object that is looked up again, or respecified, +// keeps the handle it was minted with (MGPipeHandles.h). +TEST(DirectGLESSlotTable, RepeatedLookupsOfALiveObjectKeepOneHandle) { + using namespace MobileGL; + + FakeSlotTable table; + auto object = MakeShared(0xB1u); + table.GetOrCreate(object) = MakeShared(); + const MG_Pipe::MGPipeHandle handle = table.HandleOf(object.get()); + + for (int i = 0; i < 8; ++i) { + auto* slot = table.GetOrCreate(object) ? table.Find(object.get()) : nullptr; + ASSERT_NE(slot, nullptr); + EXPECT_TRUE(table.HandleOf(object.get()) == handle) << "handle moved on lookup " << i; + } + EXPECT_EQ(table.LiveCount(), 1u); +} + +// The lookup does not mutate the table, which is what lets SyncTextureObjectToBackend stop paying +// a by-value copy plus a second Find to survive the registry's erase-inside-Find. A dead entry +// stays put until the sweep, and a live entry's pointer is unaffected by looking up anything else. +TEST(DirectGLESSlotTable, FindNeverMutatesTheTable) { + using namespace MobileGL; + + FakeSlotTable table; + auto kept = MakeShared(0xC1u); + auto doomed = MakeShared(0xC2u); + table.GetOrCreate(kept) = MakeShared(); + table.GetOrCreate(doomed) = MakeShared(); + + auto* keptSlot = table.Find(kept.get()); + ASSERT_NE(keptSlot, nullptr); + const FakeBackendObject* keptTwin = keptSlot->get(); + + doomed.reset(); + // The registry's Find would have erased the expired entry here and relocated the rest of the + // probe cluster, invalidating keptSlot. This one answers null and touches nothing. + EXPECT_EQ(table.Find(kept.get()), keptSlot); + EXPECT_EQ(table.LiveCount(), 2u) << "Find reclaimed a slot; only the sweep may do that"; + EXPECT_EQ(keptSlot->get(), keptTwin); + + table.CollectGarbageNow(); + EXPECT_EQ(table.LiveCount(), 1u); + EXPECT_EQ(table.Find(kept.get())->get(), keptTwin); +} + +// ScopedDirectGLESTextureBindings saves a whole twin table by value, resets it with `= {}` and +// restores it. The slot table has to keep that shape or the fixture stops isolating anything. +TEST(DirectGLESSlotTable, AWholeTableSavesResetsAndRestores) { + using namespace MobileGL; + + FakeSlotTable table; + auto object = MakeShared(0xD1u); + table.GetOrCreate(object) = MakeShared(); + (*table.Find(object.get()))->marker = 7; + + const FakeSlotTable saved = table; + table = {}; + EXPECT_EQ(table.Find(object.get()), nullptr) << "the reset left the twin reachable"; + + table = saved; + ASSERT_NE(table.Find(object.get()), nullptr); + EXPECT_EQ((*table.Find(object.get()))->marker, 7); +} + +// The whole point of routing every twin through the client allocator: a table that keeps its own +// dense array still shares ONE identity per frontend object with every other holder of it. +TEST(DirectGLESSlotTable, TwoTablesOfTheSameKindAgreeOnOneObjectsHandle) { + using namespace MobileGL; + + FakeSlotTable a; + FakeSlotTable b; + auto object = MakeShared(0xE1u); + a.GetOrCreate(object) = MakeShared(); + b.GetOrCreate(object) = MakeShared(); + + EXPECT_TRUE(a.HandleOf(object.get()) == b.HandleOf(object.get())); +} +#else +// G2 wants the pull and the push build to list the SAME ctest entries. The twin table only +// exists under MOBILEGL_PIPE_PUSH, so in the pull build each case above keeps its name and +// skips visibly - a vanishing test is exactly what that gate is there to stop. +TEST(DirectGLESSlotTable, ARecycledSlotIsANewHandleAndTheStaleOneResolvesToNothing) { + GTEST_SKIP() << "the {slot, gen} twin table is compiled only under MOBILEGL_PIPE_PUSH"; +} + +TEST(DirectGLESSlotTable, RepeatedLookupsOfALiveObjectKeepOneHandle) { + GTEST_SKIP() << "the {slot, gen} twin table is compiled only under MOBILEGL_PIPE_PUSH"; +} + +TEST(DirectGLESSlotTable, FindNeverMutatesTheTable) { + GTEST_SKIP() << "the {slot, gen} twin table is compiled only under MOBILEGL_PIPE_PUSH"; +} + +TEST(DirectGLESSlotTable, AWholeTableSavesResetsAndRestores) { + GTEST_SKIP() << "the {slot, gen} twin table is compiled only under MOBILEGL_PIPE_PUSH"; +} + +TEST(DirectGLESSlotTable, TwoTablesOfTheSameKindAgreeOnOneObjectsHandle) { + GTEST_SKIP() << "the {slot, gen} twin table is compiled only under MOBILEGL_PIPE_PUSH"; +} +#endif // MOBILEGL_PIPE_PUSH