[Fix] (Magma): make the legacy-memo lever a startup gate for Magma's own bit, bound the {slot, gen} mint, and keep the all-pull arm free of push-only cost

- MOBILEGL_PIPE_LEGACY_MEMOS=0 no longer aborts a draw. D14 spends that lever at
  STARTUP and only on a Track-H subsystem, so MagmaPipeValidateSubsystemConfiguration
  runs once from VulkanRenderer::Initialize and checks bit 6 alone: an Espryt-side
  bitmask cannot kill a Magma run, and bit 0 - which is not Track H and not a memo
  re-key - is out of the lever's scope entirely. MOBILEGL_PIPE_PUSH=0x60 with the
  lever off went from 9/9 aborted to 432/432.
- A pipeline memo with no render-state CSO bound falls back instead of aborting.
  delete_render_state clears the binding, so the null handle is reachable on any
  tree; the fallback is the pre-handle state hash where one is compiled, and the
  client's own MGPipeComputePipelineSubsetHash over the same 396 pipeline bytes
  where it is not - which is what makes -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF a runnable
  configuration (180/432 aborted before, 432/432 now) instead of a build that dies
  on its first draw. The fallback warns once, so a run that never keys on a CSO
  handle says so in its log instead of passing silently.
- The handles are minted by a fixed-capacity, self-recycling identity table in the
  backend, not by MG_Impl's client allocator. Nothing in P2 frees a VertexElementsCso
  or Buffer slot - the frontend has no death notification Magma can hook - so the
  allocator's live Allocate and dead Free grew one SlotState plus one hash-map node
  per object ever created, for the life of the process. The table is 2-way
  set-associative with an LRU victim and a Gen bump on reuse: bounded (32 KB for
  VAOs, 128 KB for buffers), exactly as ABA-proof, and it takes MG_Backend's only
  include of MG_Impl back out.
- Both per-slot memo tables are now a BIJECTION with that mint rather than a masked
  direct map, so two live VAOs cannot share an entry and the eviction decision lives
  once, in the identity table, instead of once per consumer table. The density claim
  the masked tables rested on was false while slots grew monotonically, and the
  masked form had also dropped the second candidate and the frame-serial victim
  choice the address-hashed table used to have.
- snap.vaoHandle is stamped only when bit 6 is on. It was guarded by the compile
  switch alone, so MOBILEGL_PIPE_PUSH=0 - the all-pull control D14 defines as
  reproducing P1 exactly, and the arm D.4.3's T2 is measured on - paid a mint per
  new VAO and a compare per draw for a field that arm never reads.
- Every re-keyed Track-H site now asks the same MagmaPipeTrackHArmIsHandles helper,
  including VertexInputStateFactory::ComputeHash, which decided for itself before
  and could key on the pre-handle identity while its neighbours keyed on the handle.
- The pull build's two pipeline-memo sites keep the base ref's text statement for
  statement: G1 is back to the contract's four resized symbols, 0 added/removed.
This commit is contained in:
2026-09-07 23:18:09 -04:00
parent 3594f03c4e
commit 01179c54d2
5 changed files with 386 additions and 161 deletions
@@ -56,7 +56,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;
#if MOBILEGL_PIPE_PUSH
if (attr.Buffer) {
if (MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
// The SAME arm question the other four re-keyed sites ask, through the same
// helper: a site that decided for itself could silently key on the pre-handle
// identity while its neighbours keyed on the handle.
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
const MG_Pipe::MGPipeHandle handle =
MagmaPipeHandleOf(MG_Pipe::MGPipeKind::Buffer, attr.Buffer->GetLifetimeId());
bufferKey = static_cast<Uint64>(handle.Slot) | (static_cast<Uint64>(handle.Gen) << 32);
@@ -80,21 +83,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
#if MOBILEGL_PIPE_PUSH
VertexInputStateFactory::VaoBackendMemos& VertexInputStateFactory::MemosFor(
const MG_State::GLState::VertexArrayObject& vao) const {
static_assert(kVaoMemoSlotCount == kMagmaVaoIdentityEntries,
"this table is indexed directly by MagmaPipeSlotIndex, so it has to hold "
"exactly one entry per slot the VAO identity table can mint");
if (m_vaoMemos.empty()) {
m_vaoMemos.resize(kVaoMemoSlotCount);
}
const Uint64 lifetimeId = vao.GetLifetimeId();
if (!m_lastVaoHandleValid || m_lastVaoLifetimeId != lifetimeId) {
m_lastVaoHandle = MagmaPipeHandleOf(MG_Pipe::MGPipeKind::VertexElementsCso, lifetimeId);
m_lastVaoLifetimeId = lifetimeId;
m_lastVaoHandleValid = true;
}
const MG_Pipe::MGPipeHandle handle = m_lastVaoHandle;
VaoBackendMemos& memos = m_vaoMemos[handle.Slot & (kVaoMemoSlotCount - 1)];
const MG_Pipe::MGPipeHandle handle =
MagmaPipeHandleOf(MG_Pipe::MGPipeKind::VertexElementsCso, vao.GetLifetimeId());
// One entry per mintable slot - see the static_assert on kVaoMemoSlotCount - so this
// index is exact and two live VAOs cannot share an entry. There is no probe in front
// of it because the mint itself is one: an array index and at most two Uint64
// compares, which is less than the address hash the pre-handle arm ran.
VaoBackendMemos& memos = m_vaoMemos[MagmaPipeSlotIndex(handle)];
if (!(memos.Owner == handle)) {
// Someone else's entry (a colliding slot, or a slot whose Gen moved because the
// slot was REUSED for a different object). Claim it, contents cleared - never
// inherited, which is the whole point of keying on the generation.
// A slot whose Gen moved because the identity table recycled it for a different
// object. Claim it, contents cleared - never inherited, which is the whole point
// of keying on the generation.
memos = VaoBackendMemos{};
memos.Owner = handle;
}
@@ -105,13 +110,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
#if MOBILEGL_PIPE_PUSH
Bool VertexInputStateFactory::TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao,
Uint64& outHash) const {
if (MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
const VaoBackendMemos& memos = MemosFor(vao);
if (memos.HashConfigVersion != vao.GetConfigVersion()) return false;
outHash = memos.Hash;
return true;
}
MagmaPipeRequireLegacyArm("VertexInputStateFactory::TryGetMemoizedHash");
#if MOBILEGL_PIPE_LEGACY_MEMOS
return vao.GetBackendHashMemo(outHash);
#else
@@ -125,7 +129,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
HashType hash = 0;
#if MOBILEGL_PIPE_PUSH
// P2 D12.5: the same memo, on the backend's side of the boundary.
if (MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
VaoBackendMemos& memos = MemosFor(vao);
if (memos.HashConfigVersion == vao.GetConfigVersion()) {
return memos.Hash;
@@ -135,7 +139,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
memos.HashConfigVersion = vao.GetConfigVersion();
return hash;
}
MagmaPipeRequireLegacyArm("VertexInputStateFactory::GetOrComputeHash");
#endif
#if MOBILEGL_PIPE_LEGACY_MEMOS
if (!vao.GetBackendHashMemo(hash)) {
@@ -154,7 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// survives the move and is still what stops a stale pointer being dereferenced: the
// POINTEE is a cache entry this factory can erase at a frame boundary, and moving the
// memo does not change that.
if (MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
VaoBackendMemos& memos = MemosFor(vao);
if (memos.StateConfigVersion == vao.GetConfigVersion() && memos.State != nullptr &&
memos.StateEpoch == m_evictionEpoch) {
@@ -164,10 +167,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const BackendVertexInputState& resolved =
GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
// MemosFor is re-taken: GetOrComputeHash above went through it, and a colliding
// VAO could have claimed the entry in between (it cannot here, since both calls
// name the same VAO, but the reference is not worth keeping live across a call
// that can resize the table).
// MemosFor is re-taken rather than kept live across GetOrCreateVertexInputState:
// the reference is not worth holding across a call that can resize the table.
VaoBackendMemos& stamp = MemosFor(vao);
stamp.State = &resolved;
stamp.StateEpoch = m_evictionEpoch;
@@ -177,12 +178,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// live reader anywhere, so the handle arm retires it rather than moving it.
return resolved;
}
MagmaPipeRequireLegacyArm("VertexInputStateFactory::GetOrCreateVertexInputState");
#endif
#if !MOBILEGL_PIPE_LEGACY_MEMOS
// Unreachable: with no legacy arm compiled, MagmaPipeRequireLegacyArm above aborts.
// Written out rather than left to fall off the end so the function still has a return
// on every path a compiler can see.
// Unreachable: with no legacy arm compiled MagmaPipeTrackHArmIsHandles is a compile-
// time true, so the handle arm above always returns. Written out rather than left to
// fall off the end so the function still has a return on every path a compiler sees.
return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
#else
// Per-draw fast path: the VAO carries a pointer to its resolved entry,