[Test] (Espryt): drive the sampler-view table through the handle arm and prove every P4a death notice is idempotent

This commit is contained in:
2026-09-08 16:57:36 -04:00
committed by rereview
parent a3bd7f0d7d
commit e021b4f62b
+196
View File
@@ -3954,6 +3954,198 @@ TEST(DirectGLESSlotTable, EverySwitchedOverKindResolvesItsTwinThroughTheHandleAr
table.ReleaseByHandle(second);
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Buffer, second);
}
// P4a: the EIGHTH table and the SIXTH kind - the sampler view, which is the only kind in
// the phase with no frontend object at all. MobileGL has no sampler-view class: GL binds a
// texture to a unit and the sampler uniform's type plus the two completeness predicates
// decide what the shader sees, gallium's one-view-per-slot IS that resolved form, and the
// resolution moves to the CLIENT (D-F3). So this table is handle-keyed only, like the
// buffer resource table above, and its twin owns no driver id whatsoever - what it holds is
// the server's memo of one resolved view plus the raw-depth-fetch substitution decision,
// which is one of the two backend post-processings ARCHITECTURE.md:206 keeps on the server.
//
// The handle is minted off the TEXTURE's lifetime id (D-F2: one view per ITextureObject),
// which is what makes HandleOf resolve at all - and it is legal precisely because the two
// kinds have separate slot spaces, so the same lifetime id names a Texture slot and a
// SamplerViewCso slot without either shadowing the other.
{
using MobileGL::MG_Backend::DirectGLES::SamplerViewImpl::g_backendSamplerViews;
using MobileGL::MG_Backend::DirectGLES::SamplerViewImpl::BackendSamplerViewObject;
auto& table = g_backendSamplerViews;
SharedPtr<ITextureObject> owner = MakeShared<TextureObject2D>(0u);
const MG_Pipe::MGPipeHandle first =
MG_Pipe::MGPipeSlots().Acquire(MG_Pipe::MGPipeKind::SamplerViewCso, owner->GetLifetimeId());
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(first))
<< "SamplerViewCso: the client allocator minted no handle for a live texture's view";
// The same lifetime id, two kinds, two independent slot spaces.
const MG_Pipe::MGPipeHandle textureHandle =
MG_Pipe::MGPipeSlots().FindByLifetimeId(MG_Pipe::MGPipeKind::Texture, owner->GetLifetimeId());
EXPECT_FALSE(first == textureHandle)
<< "SamplerViewCso: the view handle and the texture handle are the same {slot, gen}, so "
"one kind's slot space is aliasing the other's";
auto& firstTwin = table.GetOrCreate(first);
firstTwin = MakeShared<BackendSamplerViewObject>();
firstTwin->SyncedSerial = 0xABCDEFull;
const BackendSamplerViewObject* const firstRaw = firstTwin.get();
EXPECT_EQ(table.FindByHandle(first), &firstTwin)
<< "SamplerViewCso: the handle does not address the twin GetOrCreate handed back";
EXPECT_EQ(table.HandleOf(owner.get()), first)
<< "SamplerViewCso: HandleOf did not resolve the view minted off this texture's "
"lifetime id, so a backend path that still arrives holding the object cannot find "
"its view";
const SharedPtr<BackendSamplerViewObject> released = table.ReleaseByHandle(first);
EXPECT_NE(released, nullptr) << "SamplerViewCso: delete_sampler_view found no twin to retire";
EXPECT_EQ(table.FindByHandle(first), nullptr)
<< "SamplerViewCso: the twin outlived its delete_sampler_view";
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::SamplerViewCso, first);
auto successorOwner = MakeShared<TextureObject2D>(0u);
const MG_Pipe::MGPipeHandle second = MG_Pipe::MGPipeSlots().Acquire(
MG_Pipe::MGPipeKind::SamplerViewCso, successorOwner->GetLifetimeId());
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(second)) << "SamplerViewCso";
EXPECT_EQ(second.Slot, first.Slot)
<< "SamplerViewCso: the freed slot was not handed back, so this walk did not exercise "
"the recycle it exists to test";
EXPECT_NE(second.Gen, first.Gen)
<< "SamplerViewCso: Gen did not move on slot reuse - the predecessor's handle would "
"resolve to the successor's view, which is the ABA the {slot, gen} key exists to stop";
EXPECT_EQ(table.FindByHandle(first), nullptr) << "SamplerViewCso: the STALE handle resolved";
auto& secondTwin = table.GetOrCreate(second);
EXPECT_EQ(secondTwin, nullptr)
<< "SamplerViewCso: a view at a recycled slot inherited its predecessor's memo, so the "
"raw-depth-fetch decision of a dead texture would be replayed for a live one";
secondTwin = MakeShared<BackendSamplerViewObject>();
EXPECT_NE(table.FindByHandle(second)->get(), firstRaw) << "SamplerViewCso";
// BACKWARD generations are REFUSED rather than adopted, exactly as they are on every
// other handle-keyed table (SlotTables.h:301-321). Forward is a recycle; adopting a
// backward one would retire the incumbent LIVE twin and then stamp the slot back to the
// dead view's generation.
auto& stale = table.GetOrCreate(first);
EXPECT_EQ(stale, nullptr) << "SamplerViewCso: a generation BEHIND the live twin was adopted";
EXPECT_NE(table.FindByHandle(second), nullptr)
<< "SamplerViewCso: the live twin was destroyed by a handle from its slot's past";
table.ReleaseByHandle(second);
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::SamplerViewCso, second);
}
}
// P4a (ID-8): the backend death notice is the REDUNDANT SECOND PATH for every kind the client
// mints, and it must be IDEMPOTENT - the client's own helper emits the wire delete, raises this
// notice and frees the slot, in that order, so whichever of the two frees first wins and the
// other must find nothing and do nothing.
//
// Driven through the REAL consumer the backend installed rather than through a recording stub:
// what is under test is Espryt's OnFrontendStateObjectDestroyed arm, including the SamplerViewCso
// arm P4a adds, and a stub would prove only that the test can call itself.
TEST(DirectGLESSlotTable, ADeathNoticeForEveryP4aKindIsIdempotent) {
using namespace MobileGL;
using namespace MobileGL::MG_Backend::DirectGLES;
using namespace MobileGL::MG_State::GLState;
if (!EsprytSlotTablesEnabled()) {
GTEST_SKIP() << "the legacy arm keys twins on the frontend heap address and cannot answer "
"a death notice at all";
}
const MG_State::GLState::StateObjectDeathOps* ops = MG_State::GLState::GetStateObjectDeathOps();
ASSERT_NE(ops, nullptr) << "the backend installed no death-notice consumer";
ASSERT_NE(ops->OnDestroyed, nullptr);
// The sampler view first, because it is the arm P4a adds and the only kind whose notice
// names a lifetime id belonging to ANOTHER object (the texture the view was minted off).
{
using SamplerViewImpl::BackendSamplerViewObject;
using SamplerViewImpl::g_backendSamplerViews;
auto owner = MakeShared<TextureObject2D>(0u);
const Uint64 lifetimeId = owner->GetLifetimeId();
const MG_Pipe::MGPipeHandle view =
MG_Pipe::MGPipeSlots().Acquire(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId);
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(view));
g_backendSamplerViews.GetOrCreate(view) = MakeShared<BackendSamplerViewObject>();
ASSERT_NE(g_backendSamplerViews.FindByHandle(view), nullptr);
ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId);
EXPECT_EQ(g_backendSamplerViews.FindByHandle(view), nullptr)
<< "the sampler-view notice left the twin behind, so the slot's next owner would "
"inherit a dead texture's resolved view";
EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(
MG_Pipe::MGPipeSlots().FindByLifetimeId(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId)))
<< "the notice did not return the slot";
// The second path. The client's helper calls Free right after raising the notice, and a
// Free on a slot that is no longer live at that generation is a proven no-op
// (SlotAllocator.cpp:117-119) - so this is the shape that actually ships, twice over.
ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId);
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::SamplerViewCso, view);
ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId);
EXPECT_EQ(g_backendSamplerViews.FindByHandle(view), nullptr);
// And the slot really is back: a successor gets it with a moved generation, which is
// the property a double free would break by skipping one.
auto successor = MakeShared<TextureObject2D>(0u);
const MG_Pipe::MGPipeHandle reused = MG_Pipe::MGPipeSlots().Acquire(
MG_Pipe::MGPipeKind::SamplerViewCso, successor->GetLifetimeId());
EXPECT_EQ(reused.Slot, view.Slot);
EXPECT_NE(reused.Gen, view.Gen);
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::SamplerViewCso, reused);
}
// The five kinds that DO have a frontend object: their notice is raised by the object's own
// destructor today and by the client's helper after P4a's client packages land, so it is
// delivered twice for one death. A second delivery must be a no-op rather than a second
// free - which is what would drop a successor's twin under it.
struct KindCase {
const char* name;
MG_Pipe::MGPipeKind kind;
Uint64 lifetimeId;
};
Vector<KindCase> cases;
{
auto texture = MakeShared<TextureObject2D>(0u);
auto renderbuffer = MakeShared<RenderbufferObject>(0u);
auto framebuffer = MakeShared<FramebufferObject>(1u);
auto sampler = MakeShared<SamplerObject>(0u);
auto program = MakeShared<ProgramObject>(0u);
TextureImpl::g_backendTextureObjects.GetOrCreate(SharedPtr<ITextureObject>(texture));
RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreate(renderbuffer);
FramebufferImpl::g_backendFramebufferObjects.GetOrCreate(framebuffer);
SamplerImpl::g_backendSamplerObjects.GetOrCreate(sampler);
PrgramImpl::g_backendProgramObjects.GetOrCreate(program);
cases.push_back({"Texture", MG_Pipe::MGPipeKind::Texture, texture->GetLifetimeId()});
cases.push_back({"Renderbuffer", MG_Pipe::MGPipeKind::Renderbuffer, renderbuffer->GetLifetimeId()});
cases.push_back({"Framebuffer", MG_Pipe::MGPipeKind::Framebuffer, framebuffer->GetLifetimeId()});
cases.push_back({"SamplerCso", MG_Pipe::MGPipeKind::SamplerCso, sampler->GetLifetimeId()});
cases.push_back({"ShaderCso", MG_Pipe::MGPipeKind::ShaderCso, program->GetLifetimeId()});
for (const auto& one : cases) {
EXPECT_FALSE(MG_Pipe::MGPipeHandleIsNull(
MG_Pipe::MGPipeSlots().FindByLifetimeId(one.kind, one.lifetimeId)))
<< one.name << ": nothing was twinned, so this walk proves nothing";
}
// The real destructors run here and raise the first notice.
}
for (const auto& one : cases) {
EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(
MG_Pipe::MGPipeSlots().FindByLifetimeId(one.kind, one.lifetimeId)))
<< one.name << ": the object's own death did not return its slot";
// Second and third deliveries: nothing to resolve, nothing to free, no abort.
ops->OnDestroyed(one.kind, one.lifetimeId);
ops->OnDestroyed(one.kind, one.lifetimeId);
EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(
MG_Pipe::MGPipeSlots().FindByLifetimeId(one.kind, one.lifetimeId)))
<< one.name << ": a redundant notice resurrected a mapping";
}
}
// The two-holder fix, end to end through the REAL Texture registry and the REAL destructor:
@@ -4372,6 +4564,10 @@ TEST(DirectGLESSlotTable, AGenerationBehindTheLiveTwinIsRefusedRatherThanAdopted
GTEST_SKIP() << "the {slot, gen} twin table is compiled only under MOBILEGL_PIPE_PUSH";
}
TEST(DirectGLESSlotTable, ADeathNoticeForEveryP4aKindIsIdempotent) {
GTEST_SKIP() << "the {slot, gen} twin table is compiled only under MOBILEGL_PIPE_PUSH";
}
TEST(DirectGLESBufferDrawProbe, ALiveHostMapKeepsTheHandleArmProbeDirtyBetweenTwoDraws) {
GTEST_SKIP() << "the handle-keyed resource table is compiled only under MOBILEGL_PIPE_PUSH";
}