mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[Fix] (Magma): size the {slot, gen} mint by the live working set instead of by a capacity, and give it to the renderer that uses it
- MagmaPipeIdentityTable was a FIXED 2048/8192-entry, 2-way set-associative LRU. Above capacity it evicted LIVE objects, and every memo keyed on the handle died with them: a verbatim transcription of the previous Acquire lost 54% of uses' handles 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 - the Minecraft chunk shape this exists for). - Two of the three memos it fed had NO capacity before this package: the content-hash memo and the resolved-state memo were unbounded mutable fields on VertexArrayObject. Eviction there turns one ComputeHash per VAO reconfiguration into one per DRAW; once the buffer table thrashes too, the vertex-input content hash becomes a per-draw value that inserts a fresh heap-allocated BackendVertexInputState into an unbounded map on every draw, swept only every 256 frame boundaries. That is a worse leak than the one the fixed table was introduced to avoid. - So the mint grows on demand and reclaims by AGE: a lifetime-id map with a one-entry front memo, a free list, and an OnFrameBoundary sweep on the same cadence and retirement age as the cache entries those slots key. Footprint tracks the live DRAWN working set instead of objects ever created, which is the property MG_Impl/Pipe/SlotAllocator cannot have here (nothing in P2 can call its Free). Re-run of the same workloads: handle churn is 0.0% at 512, 1024, 2048, 2500, 3000, 4000, 8192, 10000 and 16000 live objects, consecutive and sparse ids alike, at 1 and 5 acquisitions per use. - VertexInputStateFactory::m_vaoMemos follows the mint with no capacity of its own, through a chunked table whose entry addresses never move - which is what the fixed table's only real guarantee was, and D12.4's grow-on-demand ask without a relocating Vector. - VulkanRenderer::m_vaoDrawMemoTable deliberately keeps the base ref's 2048 entries and the base ref's older-frameSerial victim rule, and changes only its KEY. It is the one memo of the three that had a capacity before P2, a VaoDrawMemo is ~450 B, and losing one costs one vertex-binding re-resolve. Measured steady-state miss rate against the base ref's address-hashed table: 0.0% vs 6.4% at 512 live VAOs, 0.0% vs 24.0% at 1024, 0.0% vs 60.0% at 2048, 36.2% vs 69.5% at 2500, 63.5% vs 79.1% at 3000; both are ~100% at 4096 (2x capacity), where an LRU on a cyclic pattern cannot win. - The two tables are now a MagmaPipeIdentityTables member of VulkanRenderer, handed to its VertexInputStateFactory, instead of two function-local statics that outlived every context and shared one reclamation clock across two. - A Gen that reaches 2^32-1 retires its slot for good rather than wrapping. MOBILEGL_ASSERT is compiled out of every build P2 runs, and a DEBUG-level build of this tree does not compile at all (MG_Util/Types.h uses MOBILEGL_ASSERT before MGLOG_F is declared - untouched since the base ref, and not this package's file), so the defence has to be on the release path to exist. - The no-CSO pipeline-memo fallback stops using MGLOG_W_ONCE. MOBILEGL_LOG_ONCE_INTERNAL is an unconditional std::atomic_flag::test_and_set - a locked xchg per evaluation, not "one static bool test" - and this site is on the per-draw path in exactly the configuration that reaches it. A plain per-renderer bool replaces it, and the comment now says what the warning's absence does and does not prove (nothing at all while bit 0 is clear). - MOBILEGL_PIPE_LEGACY_MEMOS=0 with kMGPipeSubsystemRenderState clear still runs the pre-handle state hash - there is a correct answer there and bit 0 is not Track H, so it is not fatal - but it is no longer silent: the startup gate names the combination. - The D12.3 static_assert block now names D19's DynamicChunksCoverMagmasDynamicTailKey, whose ctest entry lives in package A's file, so the integrator can see which half is missing.
This commit is contained in:
@@ -21,7 +21,7 @@
|
|||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
|
||||||
// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14), and the
|
// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14), and the
|
||||||
// bounded {slot, gen} mint the re-keyed sites are written against.
|
// {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:
|
// Two switches decide which arm a re-keyed site runs, and they are NOT the same switch:
|
||||||
//
|
//
|
||||||
@@ -64,7 +64,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// DirectVulkan run does not execute one line of DirectGLES' re-key), so
|
// 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
|
// MOBILEGL_PIPE_PUSH=0x20 must not kill a Magma run, and MOBILEGL_PIPE_PUSH=0x40 must
|
||||||
// not kill an Espryt one.
|
// not kill an Espryt one.
|
||||||
// * bit 0 (kMGPipeSubsystemRenderState) is NOT Track H and is NOT checked. It is not a
|
// * 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
|
// 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
|
// 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
|
// run, which GetOrCreatePipeline answers with its own state hash. D14 labels bits 5
|
||||||
@@ -72,7 +72,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// * it is Fatal at STARTUP, once, not on a draw. A per-draw abort inside
|
// * 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
|
// GetOrCreatePipeline turns a configuration mistake into a mid-frame crash and puts a
|
||||||
// branch nobody needs on the hottest path in the backend.
|
// 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() {
|
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
|
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||||
// The pre-handle arm is compiled AND the operator has not forbidden entering it, so a
|
// 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.
|
// clear bit is an ordinary, valid A/B: the site takes the legacy arm.
|
||||||
@@ -114,102 +134,221 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// The {slot, gen} mint
|
// The {slot, gen} mint
|
||||||
// ---------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------
|
||||||
//
|
//
|
||||||
// A FIXED-CAPACITY, SELF-RECYCLING identity table: 2-way set-associative, indexed by the
|
// Maps a frontend object's never-reused lifetime id to a dense {slot, gen}. Three
|
||||||
// frontend object's lifetime id, LRU victim within the set, and Gen incremented whenever
|
// properties, and the third is the one review v2 got wrong:
|
||||||
// a slot changes owner. It hands out slots in [kMGPipeFirstAllocatableSlot, Count], so a
|
|
||||||
// consumer's per-slot table is a BIJECTION with this one - one entry per slot, no
|
|
||||||
// masking, no collision, no probe.
|
|
||||||
//
|
//
|
||||||
// Why not MG_Impl/Pipe/SlotAllocator (the client's allocator, which is what mints handles
|
// 1. exact identity - Gen moves whenever a slot changes owner, so a stale handle can
|
||||||
// in the finished design)? Because in P2 nothing on this side ever frees one. The tracker
|
// never match a live object even if the allocator hands back the same heap address
|
||||||
// does not emit object-class state yet (P2 emits for dirty bits 0-4), so no create_*/
|
// (the ABA HandleRecycleScenario reproduces);
|
||||||
// delete_* pair travels for a VAO or a buffer, and the frontend has no death notification
|
// 2. dense slots - the slot IS an index, so a consumer's per-slot table needs no hash,
|
||||||
// Magma could hook: BufferBackendOps::OnDestroy is handed a BackendBufferResource, not the
|
// no probe and no mix;
|
||||||
// BufferObject, and fires only for a buffer that ever had one, while VertexArrayObject has
|
// 3. NO CAPACITY CLIFF. A live object's handle never changes while the object is being
|
||||||
// no hook at all (adding one is D13's explicit-destroy work, and it covers Espryt's six
|
// drawn, whatever the working set size.
|
||||||
// kinds, not VertexElementsCso). An allocator with a live Allocate and a dead Free grows
|
|
||||||
// by one SlotState plus one hash-map node per object EVER created, for the life of the
|
|
||||||
// process, on a platform with an LMK - and its slot numbers then grow monotonically with
|
|
||||||
// objects ever created, which is exactly what would make a slot-indexed table collide.
|
|
||||||
//
|
//
|
||||||
// So Magma mints its own, bounded, and says so. This is a P2 STAND-IN either way (the
|
// Property 3 is why this is not the fixed 2-way set-associative LRU the previous round
|
||||||
// client is what mints handles once object-class state travels); what it must not be is a
|
// shipped. That structure evicted a LIVE object once the working set passed its capacity,
|
||||||
// leak. Recycling costs the same thing the address-hashed table it replaces cost: a
|
// and every consumer memo keyed on the handle died with it: measured on a verbatim
|
||||||
// colliding pair of live objects evicts each other and re-derives. It is strictly better
|
// transcription, 54% of uses lost their handle at 2500 live VAOs against 2048 entries, and
|
||||||
// than that table, because the {slot, gen} compare is an exact identity, so an eviction
|
// 20% at 1024 live VAOs once the lifetime ids are sparse (an app that creates and destroys
|
||||||
// can only ever cost a recompute - never the ABA the lifetime-id compare was added for.
|
// 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.
|
||||||
//
|
//
|
||||||
// Single-threaded, like MGPipeSlots() and like the rest of the renderer.
|
// 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 {
|
class MagmaPipeIdentityTable {
|
||||||
public:
|
public:
|
||||||
explicit MagmaPipeIdentityTable(Uint32 entryCount) : m_entryCount(entryCount) {}
|
explicit MagmaPipeIdentityTable(const char* kindName) : m_kindName(kindName) {}
|
||||||
|
|
||||||
// One entry per slot, so a consumer table sized Count() and indexed by
|
// Slots ever minted. A consumer table indexed by MagmaPipeSlotIndex() needs this many
|
||||||
// MagmaPipeSlotIndex() has exactly one entry per handle this table can hand out.
|
// entries; MagmaPipeSlotTable below grows itself, so nobody has to ask.
|
||||||
Uint32 Count() const { return m_entryCount; }
|
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) {
|
MG_Pipe::MGPipeHandle Acquire(Uint64 lifetimeId) {
|
||||||
// Unreachable: MG_State hands out lifetime ids from 1 precisely so that a
|
// 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
|
// 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.
|
// that a zero can never be minted into a slot and then indexed with.
|
||||||
if (lifetimeId == 0) return MG_Pipe::kMGPipeNullHandle;
|
if (lifetimeId == 0) return MG_Pipe::kMGPipeNullHandle;
|
||||||
if (m_entries.empty()) m_entries.resize(m_entryCount);
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
// Lifetime ids are monotonic from 1, so the low bits ARE the dense index: object
|
// Ages the table and returns idle slots to the free list. Same shape and the same
|
||||||
// n and object n+1 land in adjacent sets. No mix, because there is no entropy to
|
// self-gating as VertexInputStateFactory::OnFrameBoundary, which is what the reclaimed
|
||||||
// spread - a multiply here would only scatter a sequence that is already perfect.
|
// slots' consumers use.
|
||||||
const Uint32 set = static_cast<Uint32>(lifetimeId) & (SetCount() - 1u);
|
void OnFrameBoundary() {
|
||||||
const Uint32 way0 = set * 2u;
|
++m_boundary;
|
||||||
const Uint32 way1 = way0 + 1u;
|
if ((m_boundary % kSweepInterval) != 0) return;
|
||||||
|
SizeT retired = 0;
|
||||||
if (m_entries[way0].LifetimeId == lifetimeId) return Touch(way0);
|
for (auto it = m_index.begin(); it != m_index.end();) {
|
||||||
if (m_entries[way1].LifetimeId == lifetimeId) return Touch(way1);
|
Entry& entry = m_entries[it->second];
|
||||||
|
if ((m_boundary - entry.LastUse) > kRetireAgeBoundaries) {
|
||||||
// Miss. Evict the set's least recently used way - the same victim rule the
|
entry.LifetimeId = 0;
|
||||||
// address-hashed VaoDrawMemo table used, kept here so that it lives in ONE place
|
m_freeSlots.push_back(it->second);
|
||||||
// instead of once per consumer table.
|
it = m_index.erase(it);
|
||||||
const Uint32 victim = (m_entries[way0].LastUse <= m_entries[way1].LastUse) ? way0 : way1;
|
++retired;
|
||||||
Entry& entry = m_entries[victim];
|
} else {
|
||||||
// The one place Gen may move, and it moves on REUSE: a respecify of the same
|
++it;
|
||||||
// object keeps its {slot, gen} because its lifetime id still matches above.
|
}
|
||||||
MOBILEGL_ASSERT(entry.Gen != ~Uint32{0},
|
}
|
||||||
"Magma handle generation wrapped on slot %u; {slot, gen} is no longer "
|
if (retired != 0) {
|
||||||
"unique",
|
// A retired slot's Gen has not moved yet - it moves when the slot is reused -
|
||||||
victim + MG_Pipe::kMGPipeFirstAllocatableSlot);
|
// so a front memo pointing at one would still hand out a handle the consumer
|
||||||
++entry.Gen;
|
// tables would accept. Drop it.
|
||||||
entry.LifetimeId = lifetimeId;
|
m_lastLifetimeId = 0;
|
||||||
return Touch(victim);
|
m_lastHandle = MG_Pipe::kMGPipeNullHandle;
|
||||||
|
MGLOG_D("MagmaPipeIdentityTable(%s): retired %zu idle slots, %u live of %u minted",
|
||||||
|
m_kindName, retired, LiveCount(), Count());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
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 {
|
struct Entry {
|
||||||
Uint64 LifetimeId = 0;
|
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 Gen = 0;
|
||||||
Uint32 LastUse = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Uint32 SetCount() const { return m_entryCount / 2u; }
|
Uint32 ClaimSlot() {
|
||||||
|
while (!m_freeSlots.empty()) {
|
||||||
MG_Pipe::MGPipeHandle Touch(Uint32 index) {
|
const Uint32 index = m_freeSlots.back();
|
||||||
m_entries[index].LastUse = ++m_clock;
|
m_freeSlots.pop_back();
|
||||||
return MG_Pipe::MGPipeHandle{index + MG_Pipe::kMGPipeFirstAllocatableSlot,
|
// MGPipeHandles.h:52-58 defends the Gen wrap only in a debug allocator, and
|
||||||
m_entries[index].Gen};
|
// 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. Once per NEW slot, which is
|
||||||
|
// once per object this backend has ever seen - never on a draw. This is the number
|
||||||
|
// D.4.2 should read out of a device log to size anything that ever does need a
|
||||||
|
// capacity (ROADMAP.md:7: no instrumentation on the hot path).
|
||||||
|
const SizeT minted = m_entries.size();
|
||||||
|
if (minted >= 1024 && (minted & (minted - 1)) == 0) {
|
||||||
|
MGLOG_D("MagmaPipeIdentityTable(%s): high-water %zu slots minted, %u live",
|
||||||
|
m_kindName, minted, LiveCount());
|
||||||
|
}
|
||||||
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint32 m_entryCount = 0;
|
const char* m_kindName = "";
|
||||||
// Wraps every 2^32 acquisitions. A wrapped clock can only ever pick the wrong victim
|
Uint64 m_boundary = 0;
|
||||||
// inside one set - a cache decision, never a correctness one.
|
|
||||||
Uint32 m_clock = 0;
|
|
||||||
Vector<Entry> m_entries;
|
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 table entry a handle names. Every per-slot table Magma keeps is sized Count() and
|
// The two mints one renderer owns. Per renderer, NOT process-global: two live contexts (or
|
||||||
// indexed by this, so the index is exact and in range by construction.
|
// 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
|
// 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.
|
// 1 (VertexArrayObject.cpp, BufferObject.cpp), so Acquire's zero guard never fires. The
|
||||||
// Asserted rather than assumed, because being wrong about it would be an out-of-range
|
// ternary, not the assertion, is what has effect in a shipped build (Defines.h compiles
|
||||||
// index rather than a wrong answer.
|
// 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) {
|
inline Uint32 MagmaPipeSlotIndex(const MG_Pipe::MGPipeHandle& handle) {
|
||||||
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
||||||
"a null MGPipeHandle has no slot to index a per-slot table with");
|
"a null MGPipeHandle has no slot to index a per-slot table with");
|
||||||
@@ -218,35 +357,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
: handle.Slot - MG_Pipe::kMGPipeFirstAllocatableSlot;
|
: handle.Slot - MG_Pipe::kMGPipeFirstAllocatableSlot;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A VAO is kind VertexElementsCso: that is the gallium-shaped CSO a vertex array resolves
|
// A grow-on-demand per-slot table whose ENTRY ADDRESSES NEVER MOVE.
|
||||||
// to, and it is the only kind in MGPipeKind that names vertex-input state. 2048 entries
|
//
|
||||||
// is what the address-hashed VaoDrawMemo table it replaces held, so the working set this
|
// D12.4 asks for a grow-on-demand Vector, and with an unbounded mint that is what a
|
||||||
// covers without eviction is unchanged; at 16 B/entry the table itself is 32 KB.
|
// consumer needs - but a Vector that grows relocates its elements, and the draw path holds
|
||||||
inline constexpr Uint32 kMagmaVaoIdentityEntries = 2048;
|
// references into these entries across nested calls. Chunks of kChunkEntries are appended
|
||||||
// Buffers are far more numerous than VAOs (Minecraft cycles chunk vertex/index buffers),
|
// instead: the Vector of owning pointers reallocates, the chunks never do, so an entry
|
||||||
// and unlike the VAO table this one feeds a CONTENT hash: an eviction changes the key a
|
// reference is valid for the life of the table. That is the same guarantee the fixed table
|
||||||
// vertex-input cache entry was built under, so it costs a rebuild rather than a lookup.
|
// it replaces gave, without the fixed capacity.
|
||||||
// It is only ever consulted when a VAO's configuration version moved (ComputeHash is
|
template <typename T, Uint32 kChunkEntries = 256>
|
||||||
// memoised per VAO), so the price is paid per reconfiguration, not per draw - but the
|
class MagmaPipeSlotTable {
|
||||||
// table is sized four times the VAO one anyway, 128 KB, to keep it rare.
|
public:
|
||||||
inline constexpr Uint32 kMagmaBufferIdentityEntries = 8192;
|
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; }
|
||||||
|
|
||||||
inline MagmaPipeIdentityTable& MagmaPipeVaoIdentity() {
|
private:
|
||||||
static MagmaPipeIdentityTable table(kMagmaVaoIdentityEntries);
|
struct Chunk {
|
||||||
return table;
|
T Entries[kChunkEntries] = {};
|
||||||
}
|
};
|
||||||
inline MagmaPipeIdentityTable& MagmaPipeBufferIdentity() {
|
Vector<UniquePtr<Chunk>> m_chunks;
|
||||||
static MagmaPipeIdentityTable table(kMagmaBufferIdentityEntries);
|
};
|
||||||
return table;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The {slot, gen} of a frontend object. `lifetimeId` is the client's own identity for the
|
|
||||||
// object - never a GL name, never a heap address - so a deleted-and-recreated object at
|
|
||||||
// the same address cannot reproduce a handle, which is precisely the ABA
|
|
||||||
// HandleRecycleScenario reproduces.
|
|
||||||
inline MG_Pipe::MGPipeHandle MagmaPipeHandleOf(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
|
|
||||||
return kind == MG_Pipe::MGPipeKind::Buffer ? MagmaPipeBufferIdentity().Acquire(lifetimeId)
|
|
||||||
: MagmaPipeVaoIdentity().Acquire(lifetimeId);
|
|
||||||
}
|
|
||||||
#endif // MOBILEGL_PIPE_PUSH
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// identity while its neighbours keyed on the handle.
|
// identity while its neighbours keyed on the handle.
|
||||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||||
const MG_Pipe::MGPipeHandle handle =
|
const MG_Pipe::MGPipeHandle handle =
|
||||||
MagmaPipeHandleOf(MG_Pipe::MGPipeKind::Buffer, attr.Buffer->GetLifetimeId());
|
m_identity->HandleOf(MG_Pipe::MGPipeKind::Buffer, attr.Buffer->GetLifetimeId());
|
||||||
bufferKey = static_cast<Uint64>(handle.Slot) | (static_cast<Uint64>(handle.Gen) << 32);
|
bufferKey = static_cast<Uint64>(handle.Slot) | (static_cast<Uint64>(handle.Gen) << 32);
|
||||||
} else if (MG_Config::Features.PipeHandleAbaControl) {
|
} else if (MG_Config::Features.PipeHandleAbaControl) {
|
||||||
// Negative control C (P2 brief D18), and it applies to the PRE-HANDLE arm
|
// Negative control C (P2 brief D18), and it applies to the PRE-HANDLE arm
|
||||||
@@ -83,18 +83,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
#if MOBILEGL_PIPE_PUSH
|
#if MOBILEGL_PIPE_PUSH
|
||||||
VertexInputStateFactory::VaoBackendMemos& VertexInputStateFactory::MemosFor(
|
VertexInputStateFactory::VaoBackendMemos& VertexInputStateFactory::MemosFor(
|
||||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
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 MG_Pipe::MGPipeHandle handle =
|
const MG_Pipe::MGPipeHandle handle =
|
||||||
MagmaPipeHandleOf(MG_Pipe::MGPipeKind::VertexElementsCso, vao.GetLifetimeId());
|
m_identity->HandleOf(MG_Pipe::MGPipeKind::VertexElementsCso, vao.GetLifetimeId());
|
||||||
// One entry per mintable slot - see the static_assert on kVaoMemoSlotCount - so this
|
// One entry per mintable slot, grown on demand: the mint has no capacity, so neither
|
||||||
// index is exact and two live VAOs cannot share an entry. There is no probe in front
|
// does this, and no two live VAOs can share an entry however large the working set is.
|
||||||
// of it because the mint itself is one: an array index and at most two Uint64
|
// There is no probe in front of it because the mint itself is one - a one-entry memo
|
||||||
// compares, which is less than the address hash the pre-handle arm ran.
|
// hit for every acquisition after this draw's first, and a hash probe otherwise.
|
||||||
VaoBackendMemos& memos = m_vaoMemos[MagmaPipeSlotIndex(handle)];
|
VaoBackendMemos& memos = m_vaoMemos[MagmaPipeSlotIndex(handle)];
|
||||||
if (!(memos.Owner == handle)) {
|
if (!(memos.Owner == handle)) {
|
||||||
// A slot whose Gen moved because the identity table recycled it for a different
|
// A slot whose Gen moved because the identity table recycled it for a different
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <MG_Pipe/MGPipeHandles.h>
|
#include <MG_Pipe/MGPipeHandles.h>
|
||||||
|
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
|
#include "MagmaPipeArms.h"
|
||||||
#include "VertexInputStateBuilder.h"
|
#include "VertexInputStateBuilder.h"
|
||||||
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
|
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
@@ -73,8 +74,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// The mint is the RENDERER's (MagmaPipeIdentityTables), not a process-global and not
|
||||||
|
// this factory's: VulkanRenderer::LookupVaoDrawMemo has to derive the same {slot, gen}
|
||||||
|
// for the same VAO, and a table that outlived the context it was minted for would share
|
||||||
|
// one reclamation clock across two live contexts (review v2 minor 4).
|
||||||
|
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice,
|
||||||
|
MagmaPipeIdentityTables& identity):
|
||||||
|
m_config(config), m_physicalDevice(physicalDevice), m_identity(&identity) {}
|
||||||
|
#else
|
||||||
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
|
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
|
||||||
m_config(config), m_physicalDevice(physicalDevice) {}
|
m_config(config), m_physicalDevice(physicalDevice) {}
|
||||||
|
#endif
|
||||||
~VertexInputStateFactory() = default;
|
~VertexInputStateFactory() = default;
|
||||||
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
||||||
|
|
||||||
@@ -146,14 +157,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 StateEpoch = 0;
|
Uint64 StateEpoch = 0;
|
||||||
Uint32 StateConfigVersion = ~0u;
|
Uint32 StateConfigVersion = ~0u;
|
||||||
};
|
};
|
||||||
// Fixed, and a BIJECTION with the identity table that mints the slots
|
// Grow-on-demand (D12.4), one entry per slot the renderer's mint has ever handed
|
||||||
// (MagmaPipeVaoIdentity): entry i is slot i + kMGPipeFirstAllocatableSlot, so the
|
// out, and NO CAPACITY: these two memos had none before this package either - they
|
||||||
// index is exact, no two live VAOs can share an entry, and the eviction decision lives
|
// were unbounded mutable fields on the VertexArrayObject itself - and re-introducing
|
||||||
// once - in the identity table's 2-way LRU - instead of once per consumer table.
|
// eviction here is what review v2 rejected. MagmaPipeSlotTable grows in chunks so an
|
||||||
// Pinned against the mint by a static_assert in VertexInputStateFactory.cpp.
|
// entry reference stays valid across the nested GetOrCreateVertexInputState call.
|
||||||
// 2048 x 48 B is 96 KB.
|
// 48 B per live VAO, reclaimed with the slot when the object goes idle.
|
||||||
static constexpr Uint32 kVaoMemoSlotCount = 2048; // power of two
|
mutable MagmaPipeSlotTable<VaoBackendMemos> m_vaoMemos;
|
||||||
mutable Vector<VaoBackendMemos> m_vaoMemos;
|
// The renderer's {slot, gen} mint (see the constructor). Never null under push.
|
||||||
|
MagmaPipeIdentityTables* m_identity = nullptr;
|
||||||
// The entry belonging to `vao`, claimed (and cleared) if the slot currently holds
|
// The entry belonging to `vao`, claimed (and cleared) if the slot currently holds
|
||||||
// someone else's.
|
// someone else's.
|
||||||
VaoBackendMemos& MemosFor(const MG_State::GLState::VertexArrayObject& vao) const;
|
VaoBackendMemos& MemosFor(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||||
|
|||||||
@@ -410,6 +410,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
//
|
//
|
||||||
// The brief (P2 D12.3) expects every input to be dynamic; the tree says otherwise for
|
// The brief (P2 D12.3) expects every input to be dynamic; the tree says otherwise for
|
||||||
// exactly one, and the tree is right - see the ScissorTestEnabledMask note below.
|
// exactly one, and the tree is right - see the ScissorTestEnabledMask note below.
|
||||||
|
//
|
||||||
|
// These assertions ARE D19's DynamicChunksCoverMagmasDynamicTailKey, in the only file this
|
||||||
|
// package owns. D19 names it as a case in MG_Test/Pipe/RenderStateSpansTest.cpp, which
|
||||||
|
// belongs to package A (C.5). INTEGRATOR: make sure the outcome is not "neither" - if
|
||||||
|
// package A did not land that case, this static_assert block is the whole gate, and if it
|
||||||
|
// did, the two are redundant on purpose and both should stay.
|
||||||
namespace {
|
namespace {
|
||||||
// Is [begin, begin + size) covered entirely by DYNAMIC chunks?
|
// Is [begin, begin + size) covered entirely by DYNAMIC chunks?
|
||||||
constexpr Bool MagmaRenderStateRangeIsDynamic(SizeT begin, SizeT size) {
|
constexpr Bool MagmaRenderStateRangeIsDynamic(SizeT begin, SizeT size) {
|
||||||
@@ -3290,7 +3296,12 @@ void main() {
|
|||||||
m_physicalDevice.properties.limits.minUniformBufferOffsetAlignment, m_config.MaxFramesInFlight,
|
m_physicalDevice.properties.limits.minUniformBufferOffsetAlignment, m_config.MaxFramesInFlight,
|
||||||
maxProgramBindings, kDescriptorSetsPerFrame, m_textureManager.get(), m_samplerManager.get());
|
maxProgramBindings, kDescriptorSetsPerFrame, m_textureManager.get(), m_samplerManager.get());
|
||||||
MOBILEGL_ASSERT(succeeded, "UniformDescriptorBinder initialization failed.");
|
MOBILEGL_ASSERT(succeeded, "UniformDescriptorBinder initialization failed.");
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
m_vertexInputStateFactory =
|
||||||
|
MakeUnique<VertexInputStateFactory>(m_config, m_physicalDevice.handle, m_pipeIdentity);
|
||||||
|
#else
|
||||||
m_vertexInputStateFactory = MakeUnique<VertexInputStateFactory>(m_config, m_physicalDevice.handle);
|
m_vertexInputStateFactory = MakeUnique<VertexInputStateFactory>(m_config, m_physicalDevice.handle);
|
||||||
|
#endif
|
||||||
MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed.");
|
MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed.");
|
||||||
|
|
||||||
// Prime the first frame so Render() always targets an acquired swapchain image.
|
// Prime the first frame so Render() always targets an acquired swapchain image.
|
||||||
@@ -3641,36 +3652,54 @@ void main() {
|
|||||||
#if MOBILEGL_PIPE_PUSH
|
#if MOBILEGL_PIPE_PUSH
|
||||||
// ---- P2 D12.4, the handle arm ----
|
// ---- P2 D12.4, the handle arm ----
|
||||||
//
|
//
|
||||||
// The slot IS the index, exactly: this table and MagmaPipeVaoIdentity() hold the same
|
// The slot PICKS the entry, and the handle DECIDES whether the entry is this VAO's -
|
||||||
// number of entries and the mint hands out slot i + kMGPipeFirstAllocatableSlot for
|
// the same division of labour the legacy arm below gives the address and the lifetime
|
||||||
// entry i, so the map from live handle to entry is a BIJECTION. No Fibonacci mix of an
|
// id, with two differences that are both improvements:
|
||||||
// address, no two-way probe here, no frame-serial recycling choice here - not because
|
|
||||||
// eviction stopped being necessary, but because it happens ONE level down, in the
|
|
||||||
// identity table's 2-way LRU, where a single decision serves this table and the
|
|
||||||
// factory's. Two live VAOs cannot land on one entry of this table at all.
|
|
||||||
//
|
//
|
||||||
// The whole validation is one handle compare, and a handle cannot alias: Gen moves
|
// * the slot is dense from 1, so below kVaoDrawMemoSlotCount live slots the map is a
|
||||||
// whenever a slot changes owner, so neither a deleted VAO's successor at the same heap
|
// bijection and the two-way probe never collides at all, where an address hash
|
||||||
// address nor a VAO whose slot was recycled under LRU pressure can match a predecessor's
|
// collides by the birthday rule from the first few dozen VAOs;
|
||||||
// entry, even with a byte-identical configuration.
|
// * the handle is an exact identity - Gen moves whenever a slot changes owner - so
|
||||||
|
// neither a deleted VAO's successor at the same heap address nor a VAO whose slot
|
||||||
|
// was recycled can match a predecessor's entry, even byte-identically configured.
|
||||||
|
// That is what makes the lifetime-id half of the legacy compare unnecessary here.
|
||||||
|
//
|
||||||
|
// The capacity and the victim rule are deliberately the base ref's, unchanged: this is
|
||||||
|
// the one memo of the three that HAD a capacity before P2, and an entry lost to a
|
||||||
|
// collision costs exactly what it cost then (one vertex-binding re-resolve). Above
|
||||||
|
// kVaoDrawMemoSlotCount live VAOs a set of two ways serves four slots, and degrades
|
||||||
|
// from there - never worse than the address-hashed table it replaces, which was already
|
||||||
|
// colliding.
|
||||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||||
const MG_Pipe::MGPipeHandle handle = ResolveVaoHandle(*vao);
|
const MG_Pipe::MGPipeHandle handle = ResolveVaoHandle(*vao);
|
||||||
const Uint32 index = MagmaPipeSlotIndex(handle);
|
const Uint32 index = MagmaPipeSlotIndex(handle) & (kVaoDrawMemoSlotCount - 1u);
|
||||||
VaoDrawMemo& entry = m_vaoDrawMemoTable[index];
|
VaoDrawMemo& first = m_vaoDrawMemoTable[index];
|
||||||
if (entry.vaoHandle == handle) {
|
if (first.vaoHandle == handle) {
|
||||||
return &entry;
|
return &first;
|
||||||
}
|
}
|
||||||
entry.vaoHandle = handle;
|
VaoDrawMemo& second = m_vaoDrawMemoTable[index ^ 1u];
|
||||||
entry.vaoKey = vao;
|
if (second.vaoHandle == handle) {
|
||||||
entry.vaoLifetimeId = vao->GetLifetimeId();
|
return &second;
|
||||||
entry.contentHash = 0;
|
}
|
||||||
entry.layoutFactsValid = false;
|
// Miss: recycle a slot. Prefer an unclaimed one; otherwise evict the entry whose
|
||||||
|
// bindings memo is older (its VAO is the one drawn less recently).
|
||||||
|
VaoDrawMemo* victim = &first;
|
||||||
|
if (!MG_Pipe::MGPipeHandleIsNull(first.vaoHandle) &&
|
||||||
|
(MG_Pipe::MGPipeHandleIsNull(second.vaoHandle) ||
|
||||||
|
second.bindings.frameSerial < first.bindings.frameSerial)) {
|
||||||
|
victim = &second;
|
||||||
|
}
|
||||||
|
victim->vaoHandle = handle;
|
||||||
|
victim->vaoKey = vao;
|
||||||
|
victim->vaoLifetimeId = vao->GetLifetimeId();
|
||||||
|
victim->contentHash = 0;
|
||||||
|
victim->layoutFactsValid = false;
|
||||||
// Unmatchable until a resolve completes (same rule as the legacy arm: a bailed-out
|
// Unmatchable until a resolve completes (same rule as the legacy arm: a bailed-out
|
||||||
// resolve must never leave stale contents matchable).
|
// resolve must never leave stale contents matchable).
|
||||||
entry.bindings.frameSerial = 0;
|
victim->bindings.frameSerial = 0;
|
||||||
entry.bindings.indexFrameSerial = 0;
|
victim->bindings.indexFrameSerial = 0;
|
||||||
entry.bindings.indexBuffer = nullptr;
|
victim->bindings.indexBuffer = nullptr;
|
||||||
return &entry;
|
return victim;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
// Multiplicative mix of the (16-byte-aligned) address; take high bits, they
|
// Multiplicative mix of the (16-byte-aligned) address; take high bits, they
|
||||||
@@ -12771,6 +12800,14 @@ void main() {
|
|||||||
if (m_vertexInputStateFactory) {
|
if (m_vertexInputStateFactory) {
|
||||||
m_vertexInputStateFactory->OnFrameBoundary();
|
m_vertexInputStateFactory->OnFrameBoundary();
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// Reclaim {slot, gen} for objects that have not been drawn for a long time, on the same
|
||||||
|
// cadence and the same retirement age as the entries those slots key. This is the
|
||||||
|
// stand-in for the frontend death notification P2 has no hook for, and it is what keeps
|
||||||
|
// the mint's footprint the LIVE working set rather than every object ever created
|
||||||
|
// (review v2 MAJOR 1 / MAJOR 3).
|
||||||
|
m_pipeIdentity.OnFrameBoundary();
|
||||||
|
#endif
|
||||||
if (m_samplerManager) {
|
if (m_samplerManager) {
|
||||||
m_samplerManager->OnFrameBoundary();
|
m_samplerManager->OnFrameBoundary();
|
||||||
}
|
}
|
||||||
@@ -13136,6 +13173,9 @@ void main() {
|
|||||||
InvalidateSetupDrawSnapshots();
|
InvalidateSetupDrawSnapshots();
|
||||||
}
|
}
|
||||||
m_vertexInputStateFactory->OnFrameBoundary();
|
m_vertexInputStateFactory->OnFrameBoundary();
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
m_pipeIdentity.OnFrameBoundary();
|
||||||
|
#endif
|
||||||
m_samplerManager->OnFrameBoundary();
|
m_samplerManager->OnFrameBoundary();
|
||||||
auto& frame = m_frameContext.GetCurrent();
|
auto& frame = m_frameContext.GetCurrent();
|
||||||
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
||||||
|
|||||||
@@ -893,24 +893,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
//
|
//
|
||||||
// The fallback is warned ONCE rather than logged at debug, and that is deliberate: a
|
// The fallback is warned ONCE rather than logged at debug, and that is deliberate: a
|
||||||
// silent fallback is what makes "the CSO arm never ran" easy to miss. W is compiled in
|
// silent fallback is what makes "the CSO arm never ran" easy to miss. W is compiled in
|
||||||
// at every shipped log level, _ONCE costs one static bool test, and its ABSENCE from a
|
// at every shipped log level.
|
||||||
// run's log is the positive evidence that every draw keyed on a handle.
|
//
|
||||||
|
// The latch is a plain member bool, NOT MGLOG_W_ONCE. MOBILEGL_LOG_ONCE_INTERNAL
|
||||||
|
// (MG_Util/Debug/Log.h) is an UNCONDITIONAL std::atomic_flag::test_and_set - a locked
|
||||||
|
// xchg, executed on every evaluation, not "one static bool test" as an earlier round of
|
||||||
|
// this comment claimed - and this site is on the per-draw pipeline path in the very
|
||||||
|
// configuration that reaches it (no tracker: every draw). ROADMAP.md:7 forbids leaving
|
||||||
|
// instrumentation on a hot path, so the once-ness is one non-atomic, always-predicted
|
||||||
|
// load of a member that is false exactly once. Single-threaded like the rest of the
|
||||||
|
// renderer, and per renderer rather than per process, which is also the right scope: a
|
||||||
|
// second context that never binds a CSO deserves to say so.
|
||||||
|
//
|
||||||
|
// What the absence of this warning from a run's log proves, EXACTLY: that no draw took
|
||||||
|
// the fallback WHILE bit 0 was set. With kMGPipeSubsystemRenderState clear the function
|
||||||
|
// returns before the latch, so absence proves nothing at all - and no draw is keyed on a
|
||||||
|
// handle either. Grep the mask out of the log beside it (review v2 minor 3).
|
||||||
//
|
//
|
||||||
// Push-only by construction: the pull build does not compile this function at all, so
|
// Push-only by construction: the pull build does not compile this function at all, so
|
||||||
// its two callers are statement-for-statement what they were (G1).
|
// its two callers are statement-for-statement what they were (G1).
|
||||||
|
//
|
||||||
|
// [routed to the integrator, review v2 minor 11] MG_Pipe::MGPipeApplier() is ONE
|
||||||
|
// process-global applier (MG_Pipe/PipeApply.cpp), not the per-context CSO store D2
|
||||||
|
// specifies. In a multi-context process this reads whatever CSO another context last
|
||||||
|
// bound. The defect is package A's and the fix belongs there; Magma is its only P2
|
||||||
|
// consumer, so it is named here rather than left for both reviews to assume the other
|
||||||
|
// caught it.
|
||||||
MG_Pipe::MGPipeHandle ResolveBoundRenderStateCso() const {
|
MG_Pipe::MGPipeHandle ResolveBoundRenderStateCso() const {
|
||||||
if (!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
|
if (!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
|
||||||
return MG_Pipe::kMGPipeNullHandle;
|
return MG_Pipe::kMGPipeNullHandle;
|
||||||
}
|
}
|
||||||
const MG_Pipe::MGPipeHandle boundCso = MG_Pipe::MGPipeApplier().BoundRenderStateCso;
|
const MG_Pipe::MGPipeHandle boundCso = MG_Pipe::MGPipeApplier().BoundRenderStateCso;
|
||||||
if (MG_Pipe::MGPipeHandleIsNull(boundCso)) {
|
if (MG_Pipe::MGPipeHandleIsNull(boundCso) && !m_pipelineCsoFallbackWarned) {
|
||||||
MGLOG_W_ONCE("MGPipe: kMGPipeSubsystemRenderState is on but no render-state CSO is "
|
m_pipelineCsoFallbackWarned = true;
|
||||||
"bound; the pipeline memo is running on a state hash, not on the CSO "
|
MGLOG_W("MGPipe: kMGPipeSubsystemRenderState is on but no render-state CSO is "
|
||||||
"handle (no tracker on this build, or a draw between "
|
"bound; the pipeline memo is running on a state hash, not on the CSO "
|
||||||
"delete_render_state and the next bind)");
|
"handle (no tracker on this build, or a draw between "
|
||||||
|
"delete_render_state and the next bind)");
|
||||||
}
|
}
|
||||||
return boundCso;
|
return boundCso;
|
||||||
}
|
}
|
||||||
|
// Latch for the warning above. Mutable because the resolve is const and the latch is
|
||||||
|
// not part of the renderer's observable state.
|
||||||
|
mutable Bool m_pipelineCsoFallbackWarned = false;
|
||||||
// The memo key's STATE-HASH half, for a draw that has no CSO handle to key on: the
|
// The memo key's STATE-HASH half, for a draw that has no CSO handle to key on: the
|
||||||
// pre-handle arm, and the fallback of D12.1's handle arm. Cached on the pipeline-state
|
// pre-handle arm, and the fallback of D12.1's handle arm. Cached on the pipeline-state
|
||||||
// version plus the two render-pass facts the hash's inputs depend on, so an unchanged
|
// version plus the two render-pass facts the hash's inputs depend on, so an unchanged
|
||||||
@@ -1407,25 +1432,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// fixed table also makes every VaoDrawMemo/ResolvedVertexBindings pointer
|
// fixed table also makes every VaoDrawMemo/ResolvedVertexBindings pointer
|
||||||
// stable for the duration of a draw, which the EBO memo handoff
|
// stable for the duration of a draw, which the EBO memo handoff
|
||||||
// (m_currentDrawResolvedEntry) relies on.
|
// (m_currentDrawResolvedEntry) relies on.
|
||||||
// [deviation from D12.4] The brief asks for a grow-on-demand Vector. The table is
|
//
|
||||||
// FIXED, and under the handle arm it is sized to - and is a BIJECTION with - the
|
// [deviation from D12.4, deliberate and narrow] The brief asks for a grow-on-demand
|
||||||
// identity table that mints the slots (MagmaPipeVaoIdentity), so entry i is slot
|
// Vector. This one stays FIXED at exactly the capacity and exactly the 2-way victim
|
||||||
// i + kMGPipeFirstAllocatableSlot and no two live VAOs can ever share it. Growing on
|
// rule it has on the base ref, and only its KEY changes (a {slot, gen} handle instead
|
||||||
// demand only makes sense against an allocator that frees, and nothing in P2 frees a
|
// of a hashed heap address plus a lifetime id). Two reasons, and the second is the
|
||||||
// VertexElementsCso slot; the eviction that has to happen somewhere happens once, in
|
// whole of review v2's MAJOR 1:
|
||||||
// the identity table's LRU, instead of twice in two tables that could disagree.
|
// * a VaoDrawMemo is ~450 B (ResolvedVertexBindings dominates), so growing this
|
||||||
|
// table with the live VAO set is megabytes on a platform with an LMK, where the
|
||||||
|
// other two memos are 48 B and can afford it;
|
||||||
|
// * this is the ONLY one of the three memos that had a capacity before this package.
|
||||||
|
// Losing an entry here costs a vertex-binding re-resolve, exactly what losing it
|
||||||
|
// cost on the base ref, so at any working-set size this table is no worse than what
|
||||||
|
// it replaces - and strictly better below capacity, where the handle is a bijection
|
||||||
|
// with the slot and the two-way probe never collides at all. The other two memos
|
||||||
|
// (VertexInputStateFactory::m_vaoMemos) had NO capacity, so they keep having none.
|
||||||
static constexpr Uint32 kVaoDrawMemoSlotCount = 2048; // power of two
|
static constexpr Uint32 kVaoDrawMemoSlotCount = 2048; // power of two
|
||||||
Vector<VaoDrawMemo> m_vaoDrawMemoTable;
|
Vector<VaoDrawMemo> m_vaoDrawMemoTable;
|
||||||
#if MOBILEGL_PIPE_PUSH
|
#if MOBILEGL_PIPE_PUSH
|
||||||
static_assert(kVaoDrawMemoSlotCount == kMagmaVaoIdentityEntries,
|
// The renderer's {slot, gen} mint, shared with its VertexInputStateFactory so both
|
||||||
"the VAO draw memo table is indexed directly by MagmaPipeSlotIndex, so it has "
|
// derive the same handle for the same VAO. Per renderer, never a process-global: a
|
||||||
"to hold exactly one entry per slot the VAO identity table can mint");
|
// global would share one table and one reclamation clock across two live contexts and
|
||||||
// The VAO's {slot, gen}. An array probe (one mask, at most two Uint64 compares), so
|
// outlive every one of them (review v2 minor 4).
|
||||||
// there is no memo in front of it: the address multiply plus two-way probe it replaces
|
MagmaPipeIdentityTables m_pipeIdentity;
|
||||||
// cost more than this does, and a cached handle could go stale behind the identity
|
// The VAO's {slot, gen}. A one-entry memo hit for every acquisition after a draw's
|
||||||
// table's own eviction, which is a class of bug worth not having.
|
// first, so there is no second memo in front of it here.
|
||||||
MG_Pipe::MGPipeHandle ResolveVaoHandle(const MG_State::GLState::VertexArrayObject& vao) {
|
MG_Pipe::MGPipeHandle ResolveVaoHandle(const MG_State::GLState::VertexArrayObject& vao) {
|
||||||
return MagmaPipeHandleOf(MG_Pipe::MGPipeKind::VertexElementsCso, vao.GetLifetimeId());
|
return m_pipeIdentity.HandleOf(MG_Pipe::MGPipeKind::VertexElementsCso,
|
||||||
|
vao.GetLifetimeId());
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
// "Is this VAO's content hash already memoized?", asked of whichever side owns the
|
// "Is this VAO's content hash already memoized?", asked of whichever side owns the
|
||||||
|
|||||||
Reference in New Issue
Block a user