[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> #include <utility>
namespace MobileGL::MG_Backend::DirectVulkan { 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, Bool VertexInputStateFactory::BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask, BackendVertexInputState& out) const { 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. // 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, Bool BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask, const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask,
BackendVertexInputState& out) const; 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: private:
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false, static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false,
@@ -184,13 +184,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} // namespace } // namespace
VkBufferManager::WireBufferResource* VkBufferManager::FindWireBuffer(MG_Pipe::MGPipeHandle res) { VkBufferManager::WireBufferResource* VkBufferManager::FindWireBuffer(MG_Pipe::MGPipeHandle res) {
const Uint64 key = WireBufferKey(res); const SizeT slot = res.Slot;
auto& lookup = m_wireBufferLookup[key % kWireBufferLookupEntries]; if (slot < m_wireBufferLookup.size()) {
if (lookup.resource != nullptr && lookup.key == key && lookup.eraseCount == m_wireBufferEraseCount) const auto& lookup = m_wireBufferLookup[slot];
return lookup.resource; if (lookup.resource != nullptr && lookup.gen == res.Gen) return lookup.resource;
const auto found = m_wireBuffers.find(key); }
const auto found = m_wireBuffers.find(WireBufferKey(res));
if (found == m_wireBuffers.end()) return nullptr; 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; return &found->second;
} }
@@ -998,7 +1002,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DeferWireRelease(std::move(found->second.buffer), DeferWireRelease(std::move(found->second.buffer),
found->second.imported ? T0ReleaseSerial(found->second.lastUseSerial) found->second.imported ? T0ReleaseSerial(found->second.lastUseSerial)
: found->second.lastUseSerial); // P11 B2, as Respecify : found->second.lastUseSerial); // P11 B2, as Respecify
++m_wireBufferEraseCount; ForgetWireBufferLookup(found->first);
m_wireBuffers.erase(found); m_wireBuffers.erase(found);
PublishWireReclaimGauges(); PublishWireReclaimGauges();
} }
@@ -1170,7 +1174,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// DestroyAllDeferredReleases above emptied the parked list; this destroys the stores the // DestroyAllDeferredReleases above emptied the parked list; this destroys the stores the
// records still hold, so nothing this arm minted outlives the count. // records still hold, so nothing this arm minted outlives the count.
if (!m_wireBuffers.empty()) ++m_wireStoreDestroyEpoch; if (!m_wireBuffers.empty()) ++m_wireStoreDestroyEpoch;
++m_wireBufferEraseCount; m_wireBufferLookup.clear();
m_wireBuffers.clear(); m_wireBuffers.clear();
m_wireStoreCount = 0; m_wireStoreCount = 0;
m_wireStoreCountPeak = 0; m_wireStoreCountPeak = 0;
@@ -292,18 +292,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
IBufferCopyCommandProvider* m_copyProvider = nullptr; IBufferCopyCommandProvider* m_copyProvider = nullptr;
Vector<Vector<VkBufferObject>> m_deferredBufferReleases; Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
std::unordered_map<Uint64, WireBufferResource> m_wireBuffers; std::unordered_map<Uint64, WireBufferResource> m_wireBuffers;
// P14: a direct-mapped front for m_wireBuffers (every draw resolves each vertex, index and // P14: a slot-indexed front for m_wireBuffers (every draw resolves each vertex, index and
// uniform buffer). Keyed on WireBufferKey ({slot, gen}, never reused) and believed only at // uniform buffer). An entry is believed only for the generation it was taken at ({slot,
// the erase count it was taken at: a node never moves until erased, and every erase moves // gen} is never reused), and the erase of that node clears it: a node never moves until
// the count. // 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 { struct WireBufferLookup {
Uint64 key = 0; Uint32 gen = 0;
Uint64 eraseCount = 0;
WireBufferResource* resource = nullptr; WireBufferResource* resource = nullptr;
}; };
static constexpr SizeT kWireBufferLookupEntries = 256; static constexpr SizeT kWireBufferLookupMaxSlots = SizeT{1} << 20;
Array<WireBufferLookup, kWireBufferLookupEntries> m_wireBufferLookup{}; Vector<WireBufferLookup> m_wireBufferLookup;
Uint64 m_wireBufferEraseCount = 0; 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: // 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. // the whole point is that these entries do not wait for a frame slot to come round.
Vector<DeferredWireRelease> m_deferredWireReleases; Vector<DeferredWireRelease> m_deferredWireReleases;
@@ -311,7 +311,6 @@
if (vaoHandle.Slot >= state.VertexElementsCsos.size()) MagmaWireFatal("vertex-elements-record"); if (vaoHandle.Slot >= state.VertexElementsCsos.size()) MagmaWireFatal("vertex-elements-record");
const auto& vao = state.VertexElementsCsos[vaoHandle.Slot]; const auto& vao = state.VertexElementsCsos[vaoHandle.Slot];
if (!vao.Live || vao.Gen != vaoHandle.Gen) MagmaWireFatal("vertex-elements-generation"); 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. // 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 // 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 // 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 // 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). // counts the marker literally, which is also why no comment in this package spells it).
if (!m_vertexInputStateFactory->BuildWireVertexInput(vao, state, // P14: resolved through the record's own memo (ResolveWireVertexInput), except in a nested
programObj.activeVertexInputLocationMask, input)) // preparation, which builds into its own scratch rather than under the outer draw's feet.
MagmaWireFatal("vertex-layout-buffer-window"); 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 // 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 // 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 // (VulkanRenderer.cpp's SetupDraw: "Fail loudly rather than render wrong pixels"). An