[Perf] (Magma): the wire vertex-input layout is kept on its vertex-elements record and buffer handles resolve through a slot-indexed front.

This commit is contained in:
2026-10-07 14:58:22 -04:00
parent c1497bb83e
commit 032dcd240e
5 changed files with 101 additions and 21 deletions
@@ -18,6 +18,64 @@
#include <utility>
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
struct WireVertexInputMemo {
struct Key {
Uint64 Offset;
Uint32 Stride;
Uint32 Divisor;
};
Bool Valid = false;
Uint64 ContentSerial = 0;
Uint32 ActiveMask = 0;
Uint32 Start = 0;
Uint32 Count = 0;
Uint32 KeyCount = 0;
Array<Key, MG_Pipe::kMGPipeMaxVertexAttribs> Keys{};
VertexInputStateFactory::BackendVertexInputState State;
};
} // namespace
const VertexInputStateFactory::BackendVertexInputState* VertexInputStateFactory::ResolveWireVertexInput(
const MG_Pipe::MGPipeVertexElementsRecord& elements, const MG_Pipe::MGPipeApplierState& state,
Uint32 activeMask) const {
auto* memo = static_cast<WireVertexInputMemo*>(elements.BackendMemo.get());
if (memo == nullptr) {
auto created = std::make_shared<WireVertexInputMemo>();
memo = created.get();
elements.BackendMemo = Move(created);
}
// The window first: a location outside it is BuildWireVertexInput's refusal, and the
// keys below index the window.
Bool hit = memo->Valid && memo->ContentSerial == elements.ContentSerial && memo->ActiveMask == activeMask &&
memo->Start == state.VertexBufferStart && memo->Count == state.VertexBufferCount;
if (hit) {
Uint32 n = 0;
for (Uint32 location = 0; location < elements.AttributeCount && hit; ++location) {
if (!elements.Attributes[location].Enabled || !(activeMask & (1u << location))) continue;
const auto& buffer = state.VertexBuffers[location];
const auto& key = memo->Keys[n++];
hit = key.Offset == buffer.Offset && key.Stride == buffer.Stride && key.Divisor == buffer.Divisor;
}
if (hit) return &memo->State;
}
memo->Valid = false;
if (!BuildWireVertexInput(elements, state, activeMask, memo->State)) return nullptr;
Uint32 n = 0;
for (Uint32 location = 0; location < elements.AttributeCount; ++location) {
if (!elements.Attributes[location].Enabled || !(activeMask & (1u << location))) continue;
const auto& buffer = state.VertexBuffers[location];
memo->Keys[n++] = {buffer.Offset, buffer.Stride, buffer.Divisor};
}
memo->KeyCount = n;
memo->ContentSerial = elements.ContentSerial;
memo->ActiveMask = activeMask;
memo->Start = state.VertexBufferStart;
memo->Count = state.VertexBufferCount;
memo->Valid = true;
return &memo->State;
}
Bool VertexInputStateFactory::BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask, BackendVertexInputState& out) const {
// The caller's state is a per-draw scratch (WireDraw.inc): reset it without freeing.
@@ -119,6 +119,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask,
BackendVertexInputState& out) const;
// The same layout, kept on the record (MGPipeVertexElementsRecord::BackendMemo) and rebuilt
// only when the record's configuration, the program's active mask, the buffer window or an
// active attribute's buffer offset/stride/divisor moved - everything the build reads that
// can change. Null where BuildWireVertexInput would return false.
const BackendVertexInputState* ResolveWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
const MG_Pipe::MGPipeApplierState& state,
Uint32 activeMask) const;
private:
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false,
@@ -184,13 +184,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} // namespace
VkBufferManager::WireBufferResource* VkBufferManager::FindWireBuffer(MG_Pipe::MGPipeHandle res) {
const Uint64 key = WireBufferKey(res);
auto& lookup = m_wireBufferLookup[key % kWireBufferLookupEntries];
if (lookup.resource != nullptr && lookup.key == key && lookup.eraseCount == m_wireBufferEraseCount)
return lookup.resource;
const auto found = m_wireBuffers.find(key);
const SizeT slot = res.Slot;
if (slot < m_wireBufferLookup.size()) {
const auto& lookup = m_wireBufferLookup[slot];
if (lookup.resource != nullptr && lookup.gen == res.Gen) return lookup.resource;
}
const auto found = m_wireBuffers.find(WireBufferKey(res));
if (found == m_wireBuffers.end()) return nullptr;
lookup = {key, m_wireBufferEraseCount, &found->second};
if (slot < kWireBufferLookupMaxSlots) {
if (slot >= m_wireBufferLookup.size()) m_wireBufferLookup.resize(slot + 1);
m_wireBufferLookup[slot] = {res.Gen, &found->second};
}
return &found->second;
}
@@ -998,7 +1002,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DeferWireRelease(std::move(found->second.buffer),
found->second.imported ? T0ReleaseSerial(found->second.lastUseSerial)
: found->second.lastUseSerial); // P11 B2, as Respecify
++m_wireBufferEraseCount;
ForgetWireBufferLookup(found->first);
m_wireBuffers.erase(found);
PublishWireReclaimGauges();
}
@@ -1170,7 +1174,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// DestroyAllDeferredReleases above emptied the parked list; this destroys the stores the
// records still hold, so nothing this arm minted outlives the count.
if (!m_wireBuffers.empty()) ++m_wireStoreDestroyEpoch;
++m_wireBufferEraseCount;
m_wireBufferLookup.clear();
m_wireBuffers.clear();
m_wireStoreCount = 0;
m_wireStoreCountPeak = 0;
@@ -292,18 +292,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
IBufferCopyCommandProvider* m_copyProvider = nullptr;
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
std::unordered_map<Uint64, WireBufferResource> m_wireBuffers;
// P14: a direct-mapped front for m_wireBuffers (every draw resolves each vertex, index and
// uniform buffer). Keyed on WireBufferKey ({slot, gen}, never reused) and believed only at
// the erase count it was taken at: a node never moves until erased, and every erase moves
// the count.
// P14: a slot-indexed front for m_wireBuffers (every draw resolves each vertex, index and
// uniform buffer). An entry is believed only for the generation it was taken at ({slot,
// gen} is never reused), and the erase of that node clears it: a node never moves until
// erased. Slot-indexed rather than direct-mapped because a Minecraft frame draws from a
// few thousand live buffers, which a small hashed table mostly missed.
struct WireBufferLookup {
Uint64 key = 0;
Uint64 eraseCount = 0;
Uint32 gen = 0;
WireBufferResource* resource = nullptr;
};
static constexpr SizeT kWireBufferLookupEntries = 256;
Array<WireBufferLookup, kWireBufferLookupEntries> m_wireBufferLookup{};
Uint64 m_wireBufferEraseCount = 0;
static constexpr SizeT kWireBufferLookupMaxSlots = SizeT{1} << 20;
Vector<WireBufferLookup> m_wireBufferLookup;
void ForgetWireBufferLookup(Uint64 key) {
const SizeT slot = static_cast<SizeT>(key & 0xffffffffu);
if (slot < m_wireBufferLookup.size()) m_wireBufferLookup[slot] = {};
}
// See DeferredWireRelease. ONE FLAT LIST rather than the per-frame-slot buckets above:
// the whole point is that these entries do not wait for a frame slot to come round.
Vector<DeferredWireRelease> m_deferredWireReleases;
@@ -311,7 +311,6 @@
if (vaoHandle.Slot >= state.VertexElementsCsos.size()) MagmaWireFatal("vertex-elements-record");
const auto& vao = state.VertexElementsCsos[vaoHandle.Slot];
if (!vao.Live || vao.Gen != vaoHandle.Gen) MagmaWireFatal("vertex-elements-generation");
auto& input = scratch.input;
// P7 wave 2 package C, CONTRACT-P7 §3.2: THE `vertex-layout` ROW IS RETIRED AND SPLIT.
//
// A false is now ONE reason and only one - `buffer-window`, an enabled attribute the
@@ -324,9 +323,18 @@
// protocol error, not a shape the split path has yet to migrate, so it must not keep
// counting against the census of unmigrated refusals (scripts/ci/fatal_census.py
// counts the marker literally, which is also why no comment in this package spells it).
if (!m_vertexInputStateFactory->BuildWireVertexInput(vao, state,
programObj.activeVertexInputLocationMask, input))
MagmaWireFatal("vertex-layout-buffer-window");
// P14: resolved through the record's own memo (ResolveWireVertexInput), except in a nested
// preparation, which builds into its own scratch rather than under the outer draw's feet.
const VertexInputStateFactory::BackendVertexInputState* resolvedInput = nullptr;
if (&scratch == &m_wireDrawScratch) {
resolvedInput = m_vertexInputStateFactory->ResolveWireVertexInput(vao, state,
programObj.activeVertexInputLocationMask);
} else if (m_vertexInputStateFactory->BuildWireVertexInput(vao, state,
programObj.activeVertexInputLocationMask, scratch.input)) {
resolvedInput = &scratch.input;
}
if (resolvedInput == nullptr) MagmaWireFatal("vertex-layout-buffer-window");
const auto& input = *resolvedInput;
// The seven device/format reasons are now MASKED OUT of the vertex input state, and
// this is the monolith arm's pre-flight for them, statement for statement
// (VulkanRenderer.cpp's SetupDraw: "Fail loudly rather than render wrong pixels"). An