mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 13:18:31 +09:00
- 55d2af9b claimed - in its message, in MagmaPipeArms.h, in VertexInputStateFactory.cpp and
in MG_IntegrationTest/CMakeLists.txt - that the AbaControlHandles lane defeats the
GENERATION in {slot, gen}. It does not, and no lane of that shape can. Magma's mint has no
death notification (nothing in MG_Backend/DirectVulkan consumes NotifyStateObjectDestroyed)
and returns a slot only through OnFrameBoundary's age sweep, kSweepInterval 256 /
kRetireAgeBoundaries 1024; HandleRecycleScenario issues five frame boundaries, so the
replacement VAO acquires against an empty free list and gets a BRAND-NEW slot at Gen 1
(measured: redVao slot=2 gen=1, greenVao slot=3 gen=1). The knob-off FRESH verdict there is
decided by the SLOT alone, and deleting ++m_entries[index].Gen leaves all 32 HandleRecycle
entries green - re-measured this round.
- What the lane does defeat is the object identity that SELECTS the slot, which IS the key the
handle arm ships, and that is what the three code sites now say. The two requirements are
mutually exclusive for the pixel-visible memo: a genuine slot reuse needs >= 1024 idle
boundaries after the dead object's last draw, which necessarily puts the two draws in
different frames, and ResolvedVertexBindings - the only memo carrying a GPU slice rather
than a layout - declines across frames by design.
- So the generation is covered where it IS expressible. MG_Test/Pipe/MagmaPipeIdentityTest.cpp
drives the mint's real retire -> reuse (1280 boundaries, with a keep-alive object holding the
first allocatable slot so the reuse is not the slot the control aliases onto) and asserts
four things: the retired slot comes back with Gen+1; with the knob OFF a memo stamped at
{slot, gen=N} is NOT served at {slot, gen=N+1}; with the knob ON it IS, out of one uncleared
and unclaimed entry; and a live object keeps its slot, its generation and its memo across two
sweeps, so the generation cannot be "fixed" by bumping it on every acquisition.
- The claim rule itself moves into MagmaPipeArms.h as MagmaPipeClaimSlotMemos so the suite
exercises production code rather than a copy of it. VertexInputStateFactory::MemosFor is now
one call to it and is otherwise unchanged, on both the knob-on and the knob-off path.
- Load-bearing, measured: with ++m_entries[index].Gen commented out, ctest -L unit in
build-push goes 1563/1566 - three of the four new cases red, one of them naming the inherited
0xDEAD payload out of the same slot - while ctest -R HandleRecycle stays 32/32. Restored, all
four pass in build-push and build-verify and skip visibly in the pull build, so the ctest name
sets stay identical (G2).
499 lines
29 KiB
C++
499 lines
29 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
|
|
#pragma once
|
|
#include <Includes.h>
|
|
|
|
#include <Config.h>
|
|
#if MOBILEGL_PIPE_PUSH
|
|
// kMGPipeSubsystem* - the runtime bitmask's named bits - and MGPipeHandle itself. Both are
|
|
// header-only constant/POD declarations, and both are push-only, so the pull build's include
|
|
// graph is unchanged (G1).
|
|
#include <MG_Pipe/MGPipe.h>
|
|
#include <MG_Pipe/MGPipeHandles.h>
|
|
#endif
|
|
|
|
#include <cstdlib>
|
|
|
|
// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14), and the
|
|
// {slot, gen} mint the re-keyed sites are written against.
|
|
//
|
|
// Two switches decide which arm a re-keyed site runs, and they are NOT the same switch:
|
|
//
|
|
// MOBILEGL_PIPE_PUSH (compile) - is the pushed state there to be keyed on at all
|
|
// Features.PipePush (runtime bitmask) - is THIS subsystem migrated in THIS run
|
|
// MOBILEGL_PIPE_LEGACY_MEMOS (compile) - is the pre-handle arm compiled beside it
|
|
// Features.PipeLegacyMemos (runtime) - may the pre-handle arm be ENTERED in this run
|
|
//
|
|
// ARCHITECTURE.md 9.6's point: once a handle wave lands, a clear MOBILEGL_PIPE_PUSH bit is
|
|
// only a valid A/B while the legacy arm is still compiled, because with the bit clear the
|
|
// backend would otherwise still run the re-keyed code. So a clear bit selects the legacy
|
|
// arm, and a run that has explicitly disabled the legacy arm may not fall into it.
|
|
//
|
|
// D14 spends that last sentence at STARTUP, not per draw: "a Track-H subsystem whose bit is
|
|
// clear is a startup Fatal{PipeLegacyMemosDisabled}". Nothing in the draw path aborts, and
|
|
// nothing outside Track H consults the legacy-memo lever at all - see
|
|
// MagmaPipeValidateSubsystemConfiguration below for both halves of that rule.
|
|
//
|
|
// The whole header is inert in a pull build: MOBILEGL_PIPE_PUSH is 0 there, every helper
|
|
// below is behind it, and the pull build's translation units are byte-identical (G1).
|
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
|
|
|
#if MOBILEGL_PIPE_PUSH
|
|
// Is `subsystemBit` (MG_Pipe/MGPipe.h's kMGPipeSubsystem*) migrated in this run?
|
|
inline Bool MagmaPipeSubsystemOn(Uint64 subsystemBit) {
|
|
return (MG_Config::Features.PipePush & subsystemBit) != 0;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// D14's startup gate
|
|
// ---------------------------------------------------------------------------------
|
|
//
|
|
// Called once from VulkanRenderer::Initialize(), i.e. only when Magma is the backend
|
|
// that is actually running. It answers exactly one question and it answers it before the
|
|
// first draw: is there an arm for Magma's Track-H subsystem in this configuration?
|
|
//
|
|
// Three deliberate boundaries, each of which the per-draw shape this replaces got wrong:
|
|
//
|
|
// * ONLY Magma's own Track-H bit is checked. Espryt's bit 5 is Espryt's business (a
|
|
// DirectVulkan run does not execute one line of DirectGLES' re-key), so
|
|
// MOBILEGL_PIPE_PUSH=0x20 must not kill a Magma run, and MOBILEGL_PIPE_PUSH=0x40 must
|
|
// not kill an Espryt one.
|
|
// * bit 0 (kMGPipeSubsystemRenderState) is NOT Track H and is NOT fatal. It is not a
|
|
// memo re-key at all: it decides where the pipeline memo's STATE KEY comes from, and
|
|
// a clear bit there simply means the client is not pushing render-state CSOs in this
|
|
// run, which GetOrCreatePipeline answers with its own state hash. D14 labels bits 5
|
|
// and 6 "Track H" and labels bit 0 nothing of the sort.
|
|
// * it is Fatal at STARTUP, once, not on a draw. A per-draw abort inside
|
|
// GetOrCreatePipeline turns a configuration mistake into a mid-frame crash and puts a
|
|
// branch nobody needs on the hottest path in the backend.
|
|
//
|
|
// [declared deviation from D14, review v2 minor 2] D14's runtime row reads "false: the
|
|
// legacy arm is never entered", and D14's compile-switch row names ComputePipelineStateHash
|
|
// as part of the pre-handle arm. Those two together would make MOBILEGL_PIPE_LEGACY_MEMOS=0
|
|
// with bit 0 CLEAR a contradiction: the pipeline memo has no CSO handle to key on, so it
|
|
// keys on a state hash, and in a build that compiles the pre-handle arm that hash IS
|
|
// ComputePipelineStateHash. Magma does not make that fatal - bit 0 is not Track H, and
|
|
// there is a correct answer (the state hash) where for bits 5/6 there is none - but it no
|
|
// longer does it SILENTLY: the combination is named once, at startup, right here.
|
|
inline void MagmaPipeValidateSubsystemConfiguration() {
|
|
if (!MG_Config::Features.PipeLegacyMemos &&
|
|
!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
|
|
MGLOG_W("MGPipe: MOBILEGL_PIPE_LEGACY_MEMOS=0 with kMGPipeSubsystemRenderState (bit 0 "
|
|
"of MOBILEGL_PIPE_PUSH) clear - Magma's pipeline memo has no CSO handle to key "
|
|
"on, so every draw whose pipeline-state version moved runs the pre-handle STATE "
|
|
"HASH instead. That is not a Track-H subsystem and not fatal, but it is not the "
|
|
"handle arm either: set bit 0 (MOBILEGL_PIPE_PUSH=0x%llx) if this run was meant "
|
|
"to measure it.",
|
|
static_cast<unsigned long long>(MG_Config::Features.PipePush |
|
|
MG_Pipe::kMGPipeSubsystemRenderState));
|
|
}
|
|
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
|
// The pre-handle arm is compiled AND the operator has not forbidden entering it, so a
|
|
// clear bit is an ordinary, valid A/B: the site takes the legacy arm.
|
|
if (MG_Config::Features.PipeLegacyMemos) return;
|
|
#endif
|
|
if (MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) return;
|
|
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
|
const char* const why = "this run has MOBILEGL_PIPE_LEGACY_MEMOS=0";
|
|
#else
|
|
const char* const why =
|
|
"this build has cmake -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF, which compiles no such arm";
|
|
#endif
|
|
MGLOG_F("MGPipe: Fatal{PipeLegacyMemosDisabled} Magma's Track-H subsystem "
|
|
"(kMGPipeSubsystemMagmaVertexInput, bit 6 of MOBILEGL_PIPE_PUSH) is clear, so the "
|
|
"vertex-input cache and the VAO draw memo want the pre-handle arm - but %s. Set "
|
|
"bit 6 (MOBILEGL_PIPE_PUSH=0x%llx, or the default 0x%llx), or allow the legacy arm.",
|
|
why,
|
|
static_cast<unsigned long long>(MG_Config::Features.PipePush |
|
|
MG_Pipe::kMGPipeSubsystemMagmaVertexInput),
|
|
static_cast<unsigned long long>(MG_Pipe::kMGPipeSubsystemsMigratedAtP2));
|
|
std::abort();
|
|
}
|
|
|
|
// "Does this Track-H site run the handle arm?" - the ONE question every re-keyed Track-H
|
|
// site asks, so that they cannot disagree with each other or with the startup gate.
|
|
inline Bool MagmaPipeTrackHArmIsHandles(Uint64 trackHBit) {
|
|
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
|
return MagmaPipeSubsystemOn(trackHBit);
|
|
#else
|
|
// No pre-handle arm exists in this build, and MagmaPipeValidateSubsystemConfiguration
|
|
// has already made a clear bit a startup Fatal, so the handle arm is the only arm a
|
|
// running process can be on.
|
|
(void)trackHBit;
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// Negative control C (P2 brief D18): MOBILEGL_PIPE_HANDLE_ABA_CONTROL
|
|
// ---------------------------------------------------------------------------------
|
|
//
|
|
// "Is the object-identity half of every vertex-input memo key deliberately defeated in
|
|
// this run?" - the ONE question the control's sites ask, for the same reason
|
|
// MagmaPipeTrackHArmIsHandles exists: three sites deciding separately could disagree,
|
|
// and a control that defeats two of three guards proves nothing.
|
|
//
|
|
// WHAT IT DEFEATS, AND WHY IT IS SPELLED AS "REPLACE THE IDENTITY WITH A CONSTANT"
|
|
// RATHER THAN "USE THE HEAP ADDRESS".
|
|
//
|
|
// D18 wrote the control as "hash attr.Buffer.get() instead of GetLifetimeId(), and skip
|
|
// the vaoLifetimeId compare", on the theory that a deleted object's replacement lands at
|
|
// the freed heap block and so reproduces the key. Measured, it does not: in
|
|
// HandleRecycleScenario the GL NAMES come back (glGen* hands the deleted name straight
|
|
// out) but the C++ heap blocks do not - a VertexArrayObject is 3920 bytes, too large for
|
|
// glibc's tcache, so its chunk goes to the unsorted bin and is split by the very next
|
|
// allocation the replacement path makes. Four create/delete cycles in one run produced
|
|
// four distinct addresses, ~1 MiB apart. With no address reuse there is nothing for
|
|
// "hash the address" to collide with: the replacement hashes differently, indexes a
|
|
// different memo slot, and inherits nothing - so the arm asserted stale pixels and saw
|
|
// fresh ones, which is a FAILING negative control that had stopped controlling anything.
|
|
//
|
|
// So the control no longer asks the allocator for the collision; it manufactures it. On
|
|
// both arms the object identity is replaced by a constant, which is the strongest form of
|
|
// "the allocator handed the block back" and is deterministic. That covers strictly more
|
|
// than D18's spelling, and in particular it reaches the arm P2 SHIPS: on the handle arm
|
|
// the constant defeats the OBJECT IDENTITY THAT SELECTS THE SLOT - the key the handle arm
|
|
// ships - so the replacement VAO is handed the dead one's memo entry and its content hash.
|
|
// Defeating only the retired lifetime-id/address guards would leave that key untested,
|
|
// which is exactly the vacuity this control exists to catch.
|
|
//
|
|
// WHAT IT DOES NOT COVER, AND WHY NO REPRODUCER OF THIS SHAPE CAN [fix-aba review v1,
|
|
// MAJOR 1]. It does NOT exercise the GENERATION half of {slot, gen}:
|
|
//
|
|
// * this mint has no death notification - nothing in MG_Backend/DirectVulkan consumes
|
|
// NotifyStateObjectDestroyed - so a slot returns to the free list only through
|
|
// OnFrameBoundary's age sweep (kSweepInterval 256, kRetireAgeBoundaries 1024, below);
|
|
// * HandleRecycleScenario issues five frame boundaries, so the free list is empty when
|
|
// the replacement VAO acquires and it gets a BRAND-NEW slot at Gen 1 (measured:
|
|
// redVao slot=2 gen=1, greenVao slot=3 gen=1). The knob-off FRESH verdict there is
|
|
// decided by the SLOT alone, and deleting the ++Gen below leaves all four arms green;
|
|
// * a genuine slot REUSE needs >= 1024 idle boundaries after the dead object's last
|
|
// draw, which necessarily puts the two draws in different frames - and the only memo
|
|
// that carries a GPU slice rather than a layout, ResolvedVertexBindings, declines
|
|
// across frames by design. The two requirements are mutually exclusive, so the
|
|
// generation is out of reach of any same-frame pixel reproducer for this memo.
|
|
//
|
|
// The generation is covered where it IS expressible, over this mint and the claim rule
|
|
// MagmaPipeClaimSlotMemos below: MG_Test/Pipe/MagmaPipeIdentityTest.cpp drives a real
|
|
// retire -> reuse and asserts that a memo stamped at {slot, gen=N} is not served at
|
|
// {slot, gen=N+1} with the knob off and IS served with it on. Deleting the ++Gen reds that
|
|
// suite; it is the only place in the tree where that deletion is caught.
|
|
//
|
|
// Everything the control does NOT defeat is as load-bearing as what it does. It never
|
|
// touches a guard that is not an IDENTITY guard: the resolved-bindings memo's frame
|
|
// serial, its slice-epoch compares and its host-map check all stay in force, so a green
|
|
// AbaControl arm still means "a replacement object was handed its dead predecessor's
|
|
// resolved vertex bindings because the identity halves of the keys were defeated", not
|
|
// "every safety net was switched off until something broke".
|
|
//
|
|
// Off by default (Config.h), set only by the HandleRecycle AbaControl ctest lanes, and
|
|
// #if MOBILEGL_PIPE_PUSH throughout, so no shipping pull build can even parse it.
|
|
inline Bool MagmaPipeAbaControlDefeatsIdentity() {
|
|
return MG_Config::Features.PipeHandleAbaControl;
|
|
}
|
|
|
|
// The single consumer-table entry every VAO collapses onto while the control is on. Slot
|
|
// 0 is a real, ordinary entry of both tables (MagmaPipeSlotIndex maps the first allocatable
|
|
// handle onto it), so nothing about the tables changes shape for the control's sake.
|
|
inline constexpr Uint32 kMagmaPipeAbaControlSlotIndex = 0;
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// The {slot, gen} mint
|
|
// ---------------------------------------------------------------------------------
|
|
//
|
|
// Maps a frontend object's never-reused lifetime id to a dense {slot, gen}. Three
|
|
// properties, and the third is the one review v2 got wrong:
|
|
//
|
|
// 1. exact identity - Gen moves whenever a slot changes owner, so a stale handle can
|
|
// never match a live object even if the allocator hands back the same heap address
|
|
// (the ABA HandleRecycleScenario reproduces);
|
|
// 2. dense slots - the slot IS an index, so a consumer's per-slot table needs no hash,
|
|
// no probe and no mix;
|
|
// 3. NO CAPACITY CLIFF. A live object's handle never changes while the object is being
|
|
// drawn, whatever the working set size.
|
|
//
|
|
// Property 3 is why this is not the fixed 2-way set-associative LRU the previous round
|
|
// shipped. That structure evicted a LIVE object once the working set passed its capacity,
|
|
// and every consumer memo keyed on the handle died with it: measured on a verbatim
|
|
// transcription, 54% of uses lost their handle at 2500 live VAOs against 2048 entries, and
|
|
// 20% at 1024 live VAOs once the lifetime ids are sparse (an app that creates and destroys
|
|
// VAOs, which is the Minecraft chunk shape this exists for). Two of the three memos it
|
|
// fed - the content-hash memo and the resolved-state memo - had NO capacity before this
|
|
// package: they were unbounded mutable fields on VertexArrayObject. Introducing eviction
|
|
// there turns one ComputeHash per VAO reconfiguration into one per DRAW, and, once the
|
|
// buffer table thrashes too, makes the vertex-input content hash a per-draw value that
|
|
// inserts a fresh heap-allocated BackendVertexInputState into an unbounded map on every
|
|
// draw. That is a worse leak than the one it was introduced to avoid.
|
|
//
|
|
// So: grow on demand, and reclaim by AGE instead of by capacity.
|
|
//
|
|
// * Acquire hits an UnorderedMap<lifetimeId, slotIndex>, in front of which sits a
|
|
// one-entry memo. Every re-keyed site in a draw asks about the SAME VAO, so the memo
|
|
// turns the five-or-six acquisitions a draw makes into one map probe plus five Uint64
|
|
// compares - less than the address multiply plus two-way probe the pre-handle arm ran.
|
|
// * OnFrameBoundary retires slots whose object has not been drawn for
|
|
// kRetireAgeBoundaries boundaries and returns them to a free list, so the table's
|
|
// footprint tracks the LIVE DRAWN working set, not objects ever created. That is the
|
|
// property MG_Impl/Pipe/SlotAllocator cannot have here: nothing in P2 can call its
|
|
// Free (the tracker emits no object-class state, BufferBackendOps::OnDestroy is handed
|
|
// a BackendBufferResource rather than the BufferObject, and VertexArrayObject has no
|
|
// death hook at all - adding one is D13's explicit-destroy work, which covers Espryt's
|
|
// six kinds, not VertexElementsCso), so an allocator here would grow by one SlotState
|
|
// plus one map node per object EVER created, for the life of the process, on a
|
|
// platform with an LMK. Age-based reclamation is the stand-in for the death
|
|
// notification, and it is exactly as ABA-proof, because reuse bumps Gen.
|
|
// * A retire costs at most one memo recompute if the object is drawn again - the same
|
|
// price a cache miss costs - and it is charged only to objects that went idle for
|
|
// ~1024 frames, never to a hot one.
|
|
//
|
|
// Memory: one map node plus one 24-byte Entry per live object, i.e. tens of bytes against
|
|
// the kilobyte a VertexArrayObject or a BufferObject already costs the frontend. There is
|
|
// no capacity to size off a device measurement because there is no capacity; what the
|
|
// device run in D.4.2 can still want is the number itself, so the high-water mark is
|
|
// logged at MGLOG_D on the allocate-a-new-slot branch (once per new object, never on a
|
|
// draw - ROADMAP.md:7).
|
|
//
|
|
// Single-threaded, like the rest of the renderer. Owned per VulkanRenderer (see
|
|
// MagmaPipeIdentityTables): a process-global would share one table, and one reclamation
|
|
// clock, across two live contexts.
|
|
class MagmaPipeIdentityTable {
|
|
public:
|
|
explicit MagmaPipeIdentityTable(const char* kindName) : m_kindName(kindName) {}
|
|
|
|
// Slots ever minted. A consumer table indexed by MagmaPipeSlotIndex() needs this many
|
|
// entries; MagmaPipeSlotTable below grows itself, so nobody has to ask.
|
|
Uint32 Count() const { return static_cast<Uint32>(m_entries.size()); }
|
|
// Objects currently holding a slot - the live working set this table tracks.
|
|
Uint32 LiveCount() const { return static_cast<Uint32>(m_index.size()); }
|
|
|
|
MG_Pipe::MGPipeHandle Acquire(Uint64 lifetimeId) {
|
|
// Unreachable: MG_State hands out lifetime ids from 1 precisely so that a
|
|
// zero-initialised memo slot cannot carry a live object's id. Guarded anyway so
|
|
// that a zero can never be minted into a slot and then indexed with.
|
|
if (lifetimeId == 0) return MG_Pipe::kMGPipeNullHandle;
|
|
// The one-entry front memo. Cleared by any retire, so it can never serve a slot
|
|
// that has been handed back to the free list.
|
|
if (lifetimeId == m_lastLifetimeId) {
|
|
m_entries[m_lastIndex].LastUse = m_boundary;
|
|
return m_lastHandle;
|
|
}
|
|
Uint32 index = 0;
|
|
const auto it = m_index.find(lifetimeId);
|
|
if (it != m_index.end()) {
|
|
index = it->second;
|
|
} else {
|
|
index = ClaimSlot();
|
|
m_entries[index].LifetimeId = lifetimeId;
|
|
m_index.emplace(lifetimeId, index);
|
|
}
|
|
Entry& entry = m_entries[index];
|
|
entry.LastUse = m_boundary;
|
|
m_lastLifetimeId = lifetimeId;
|
|
m_lastIndex = index;
|
|
m_lastHandle = MG_Pipe::MGPipeHandle{index + MG_Pipe::kMGPipeFirstAllocatableSlot,
|
|
entry.Gen};
|
|
return m_lastHandle;
|
|
}
|
|
|
|
// Ages the table and returns idle slots to the free list. Same shape and the same
|
|
// self-gating as VertexInputStateFactory::OnFrameBoundary, which is what the reclaimed
|
|
// slots' consumers use.
|
|
void OnFrameBoundary() {
|
|
++m_boundary;
|
|
if ((m_boundary % kSweepInterval) != 0) return;
|
|
SizeT retired = 0;
|
|
for (auto it = m_index.begin(); it != m_index.end();) {
|
|
Entry& entry = m_entries[it->second];
|
|
if ((m_boundary - entry.LastUse) > kRetireAgeBoundaries) {
|
|
entry.LifetimeId = 0;
|
|
m_freeSlots.push_back(it->second);
|
|
it = m_index.erase(it);
|
|
++retired;
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
if (retired != 0) {
|
|
// A retired slot's Gen has not moved yet - it moves when the slot is reused -
|
|
// so a front memo pointing at one would still hand out a handle the consumer
|
|
// tables would accept. Drop it.
|
|
m_lastLifetimeId = 0;
|
|
m_lastHandle = MG_Pipe::kMGPipeNullHandle;
|
|
MGLOG_D("MagmaPipeIdentityTable(%s): retired %zu idle slots, %u live of %u minted",
|
|
m_kindName, retired, LiveCount(), Count());
|
|
}
|
|
}
|
|
|
|
private:
|
|
// Sweep cadence and retirement age, deliberately the same numbers
|
|
// VertexInputStateFactory::OnFrameBoundary uses for the entries these slots key: a slot
|
|
// retired earlier than its cache entry would mint a new handle for an object whose
|
|
// entry is still live and still correct, which is a pure waste.
|
|
static constexpr Uint64 kSweepInterval = 256;
|
|
static constexpr Uint64 kRetireAgeBoundaries = 1024;
|
|
|
|
struct Entry {
|
|
Uint64 LifetimeId = 0;
|
|
Uint64 LastUse = 0;
|
|
// Moves ONLY on slot reuse, never on respecify: an object that keeps its slot keeps
|
|
// its generation, which is what makes a memo survive a reconfiguration.
|
|
Uint32 Gen = 0;
|
|
};
|
|
|
|
Uint32 ClaimSlot() {
|
|
while (!m_freeSlots.empty()) {
|
|
const Uint32 index = m_freeSlots.back();
|
|
m_freeSlots.pop_back();
|
|
// MGPipeHandles.h:52-58 defends the Gen wrap only in a debug allocator, and
|
|
// MOBILEGL_ASSERT is compiled out of every build P2 runs (Defines.h: asserts are
|
|
// live only at MOBILEGL_LOG_ACTIVE_LEVEL == DEBUG). So the wrap is handled on the
|
|
// RELEASE path instead of asserted: a slot that has been reused 2^32 times is
|
|
// permanently retired rather than wrapped, because a wrapped Gen would let a
|
|
// stale handle match a live object. It costs one slot.
|
|
if (m_entries[index].Gen == ~Uint32{0}) {
|
|
MGLOG_W("MagmaPipeIdentityTable(%s): slot %u reached generation 2^32-1 and is "
|
|
"retired for good; {slot, gen} stays unique",
|
|
m_kindName, index + MG_Pipe::kMGPipeFirstAllocatableSlot);
|
|
continue;
|
|
}
|
|
++m_entries[index].Gen;
|
|
return index;
|
|
}
|
|
const Uint32 index = static_cast<Uint32>(m_entries.size());
|
|
m_entries.push_back(Entry{});
|
|
m_entries[index].Gen = 1;
|
|
// The high-water mark, at powers of two from 1024 up: at most a handful of lines
|
|
// for a whole session, emitted from the allocate-a-NEW-slot branch, i.e. once per
|
|
// object this backend has ever seen and never on a draw (ROADMAP.md:7).
|
|
//
|
|
// [narrow, declared deviation from D20's "MGLOG_D for anything non-critical"] This
|
|
// one is I, not D, because D is compiled out of every build that ships and of every
|
|
// build P2 measures, and this line IS the measurement review v2's MAJOR 1 asks for:
|
|
// the live-object high-water mark of minecraft-1.21.4-in-world and
|
|
// ...-sodium-in-world, which nothing on desktop reaches and no gate here can see.
|
|
// The structure no longer has a capacity to size off it, so the number is evidence
|
|
// rather than a tuning input - but D.4.2 should still read it out of the device log,
|
|
// and it cannot read a line that was compiled away.
|
|
const SizeT minted = m_entries.size();
|
|
if (minted >= 1024 && (minted & (minted - 1)) == 0) {
|
|
MGLOG_I("MagmaPipeIdentityTable(%s): high-water %zu slots minted, %u live",
|
|
m_kindName, minted, LiveCount());
|
|
}
|
|
return index;
|
|
}
|
|
|
|
const char* m_kindName = "";
|
|
Uint64 m_boundary = 0;
|
|
Vector<Entry> m_entries;
|
|
Vector<Uint32> m_freeSlots;
|
|
UnorderedMap<Uint64, Uint32> m_index;
|
|
// One-entry front memo (see Acquire). m_lastLifetimeId == 0 means "empty": a live
|
|
// object's lifetime id is never 0.
|
|
Uint64 m_lastLifetimeId = 0;
|
|
Uint32 m_lastIndex = 0;
|
|
MG_Pipe::MGPipeHandle m_lastHandle = MG_Pipe::kMGPipeNullHandle;
|
|
};
|
|
|
|
// The two mints one renderer owns. Per renderer, NOT process-global: two live contexts (or
|
|
// a context recreation, which destroys and rebuilds the renderer) would otherwise share one
|
|
// table and one reclamation clock, and both consumer tables are per-instance already.
|
|
class MagmaPipeIdentityTables {
|
|
public:
|
|
// A VAO is kind VertexElementsCso: that is the gallium-shaped CSO a vertex array
|
|
// resolves to, and the only kind in MGPipeKind that names vertex-input state.
|
|
MG_Pipe::MGPipeHandle HandleOf(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
|
|
return kind == MG_Pipe::MGPipeKind::Buffer ? m_buffers.Acquire(lifetimeId)
|
|
: m_vaos.Acquire(lifetimeId);
|
|
}
|
|
void OnFrameBoundary() {
|
|
m_vaos.OnFrameBoundary();
|
|
m_buffers.OnFrameBoundary();
|
|
}
|
|
const MagmaPipeIdentityTable& Vaos() const { return m_vaos; }
|
|
const MagmaPipeIdentityTable& Buffers() const { return m_buffers; }
|
|
|
|
private:
|
|
MagmaPipeIdentityTable m_vaos{"VertexElementsCso"};
|
|
MagmaPipeIdentityTable m_buffers{"Buffer"};
|
|
};
|
|
|
|
// The table entry a handle names. Every per-slot table Magma keeps is indexed by this.
|
|
//
|
|
// A null handle has no slot, and it is unreachable here: both lifetime-id sources start at
|
|
// 1 (VertexArrayObject.cpp, BufferObject.cpp), so Acquire's zero guard never fires. The
|
|
// ternary, not the assertion, is what has effect in a shipped build (Defines.h compiles
|
|
// MOBILEGL_ASSERT out at INFO), and slot 0 of a consumer table is a real entry that a null
|
|
// handle can never match, because MGPipeHandleIsNull is also what the consumers compare.
|
|
inline Uint32 MagmaPipeSlotIndex(const MG_Pipe::MGPipeHandle& handle) {
|
|
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
|
"a null MGPipeHandle has no slot to index a per-slot table with");
|
|
return MG_Pipe::MGPipeHandleIsNull(handle)
|
|
? 0u
|
|
: handle.Slot - MG_Pipe::kMGPipeFirstAllocatableSlot;
|
|
}
|
|
|
|
// A grow-on-demand per-slot table whose ENTRY ADDRESSES NEVER MOVE.
|
|
//
|
|
// D12.4 asks for a grow-on-demand Vector, and with an unbounded mint that is what a
|
|
// consumer needs - but a Vector that grows relocates its elements, and the draw path holds
|
|
// references into these entries across nested calls. Chunks of kChunkEntries are appended
|
|
// instead: the Vector of owning pointers reallocates, the chunks never do, so an entry
|
|
// reference is valid for the life of the table. That is the same guarantee the fixed table
|
|
// it replaces gave, without the fixed capacity.
|
|
template <typename T, Uint32 kChunkEntries = 256>
|
|
class MagmaPipeSlotTable {
|
|
public:
|
|
T& operator[](Uint32 index) {
|
|
const Uint32 chunk = index / kChunkEntries;
|
|
while (m_chunks.size() <= chunk) {
|
|
m_chunks.push_back(MakeUnique<Chunk>());
|
|
}
|
|
return m_chunks[chunk]->Entries[index % kChunkEntries];
|
|
}
|
|
SizeT Capacity() const { return m_chunks.size() * kChunkEntries; }
|
|
|
|
private:
|
|
struct Chunk {
|
|
T Entries[kChunkEntries] = {};
|
|
};
|
|
Vector<UniquePtr<Chunk>> m_chunks;
|
|
};
|
|
|
|
// The claim rule every per-slot memo table uses, in one place so that the rule and the
|
|
// negative control that defeats it cannot drift apart between consumers - and so that the
|
|
// unit suite which drives a REAL slot reuse (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) tests
|
|
// this code rather than a copy of it.
|
|
//
|
|
// The SLOT picks the entry; the WHOLE handle - Gen included - decides whether the entry is
|
|
// this object's. A slot the mint recycled for a different object comes back with a moved
|
|
// Gen, so the compare fails and the entry is cleared rather than inherited. That is the
|
|
// half HandleRecycleScenario cannot reach (see MagmaPipeAbaControlDefeatsIdentity).
|
|
//
|
|
// With negative control C on, every object collapses onto one entry and the entry is handed
|
|
// back UNCLEARED and UNCLAIMED - at once "the replacement reproduced its predecessor's
|
|
// slot" and "the slot was reused and Gen did not move".
|
|
//
|
|
// `Memos` needs a MG_Pipe::MGPipeHandle member named Owner and a default constructor that
|
|
// means "empty"; VertexInputStateFactory::VaoBackendMemos is the one production instance.
|
|
template <typename Memos, Uint32 kChunkEntries>
|
|
inline Memos& MagmaPipeClaimSlotMemos(MagmaPipeSlotTable<Memos, kChunkEntries>& table,
|
|
const MG_Pipe::MGPipeHandle& handle) {
|
|
if (MagmaPipeAbaControlDefeatsIdentity()) {
|
|
return table[kMagmaPipeAbaControlSlotIndex];
|
|
}
|
|
Memos& memos = table[MagmaPipeSlotIndex(handle)];
|
|
if (!(memos.Owner == handle)) {
|
|
memos = Memos{};
|
|
memos.Owner = handle;
|
|
}
|
|
return memos;
|
|
}
|
|
#endif // MOBILEGL_PIPE_PUSH
|
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|