[Perf] (Magma): wire draws stop rehashing program SPIR-V and stop allocating per draw (rd12 monolith 45.7 -> 56.7 fps).

This commit is contained in:
2026-10-07 10:07:42 -04:00
parent 51c1411696
commit 267211e8b4
6 changed files with 130 additions and 22 deletions
@@ -47,15 +47,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto& stages = m_record->Archive->LinkedStages;
return std::find(stages.begin(), stages.end(), static_cast<Uint32>(stage)) != stages.end();
}
// The view has no lifetime of its own, so the memo lives in the server record it views
// (MGPipeShaderCsoRecord::BackendHashMemo), never in the client's program object: an entry
// answers only while the record's Serial and BindingsSerial are the ones it was taken at.
Bool GetBackendHashMemo(Uint flags, Uint64& hash) const {
// A view has no lifetime in which a memo can safely persist. The server
// factory's content cache still owns compiled programs; hash from current
// archive + binding tails rather than borrowing the client's mutable memo.
(void)flags;
(void)hash;
for (const auto& entry : m_record->BackendHashMemo) {
if (entry.Valid && entry.Key == flags && entry.Serial == m_record->Serial &&
entry.BindingsSerial == m_record->BindingsSerial) {
hash = entry.Hash;
return true;
}
}
return false;
}
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
auto& memo = m_record->BackendHashMemo;
auto& entry = memo[m_record->BackendHashMemoNext++ % memo.size()];
entry.Serial = m_record->Serial;
entry.BindingsSerial = m_record->BindingsSerial;
entry.Hash = hash;
entry.Key = flags;
entry.Valid = true;
}
Uint64 GetBackendStateVersion() const { return m_record->Serial; }
Uint64 GetBlockBindingVersion() const { return m_record->BindingsSerial; }
@@ -20,7 +20,8 @@
namespace MobileGL::MG_Backend::DirectVulkan {
Bool VertexInputStateFactory::BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask, BackendVertexInputState& out) const {
out = BackendVertexInputState{};
// The caller's state is a per-draw scratch (WireDraw.inc): reset it without freeing.
out.ResetKeepingCapacity();
for (Uint32 location = 0; location < elements.AttributeCount; ++location) {
const auto& attr = elements.Attributes[location];
if (!attr.Enabled || !(activeMask & (1u << location))) continue;
@@ -74,6 +74,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineVertexInputStateCreateInfo state{
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
};
// Back to a default-constructed state while keeping the vectors' storage, so a
// per-draw scratch rebuilds without touching the heap. Every member is reset by the
// default assignment; only the vectors' buffers are carried across it.
void ResetKeepingCapacity() {
const auto keep = [](auto& vector) {
auto storage = Move(vector);
storage.clear();
return storage;
};
auto keptBindings = keep(bindings);
auto keptAttributes = keep(attributes);
auto keptBufferKeys = keep(bindingBufferKeys);
auto keptBaseOffsets = keep(bindingBaseOffsets);
auto keptLocations = keep(bindingAttributeLocations);
auto keptClientMemory = keep(bindingUsesClientMemory);
auto keptConversions = keep(bindingConversions);
auto keptDivisors = keep(bindingDivisors);
*this = BackendVertexInputState{};
bindings = Move(keptBindings);
attributes = Move(keptAttributes);
bindingBufferKeys = Move(keptBufferKeys);
bindingBaseOffsets = Move(keptBaseOffsets);
bindingAttributeLocations = Move(keptLocations);
bindingUsesClientMemory = Move(keptClientMemory);
bindingConversions = Move(keptConversions);
bindingDivisors = Move(keptDivisors);
}
};
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
@@ -785,6 +785,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 swapchainImageIndex = 0;
Bool isDefault = false, framebufferSrgb = false;
Bool operator==(const WireDrawPassKey&) const = default;
// Default values, vectors emptied but their storage kept (the per-draw scratch).
void ResetKeepingCapacity() {
auto keptAttachments = Move(attachments);
auto keptColorReferences = Move(colorReferences);
*this = WireDrawPassKey{};
keptAttachments.clear();
keptColorReferences.clear();
attachments = Move(keptAttachments);
colorReferences = Move(keptColorReferences);
}
};
struct WireDrawPassCacheEntry {
WireDrawPassKey key;
@@ -799,6 +809,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Vector<WireDrawPassCacheEntry>> m_wireDrawPassCaches;
WireDrawPassKey m_wireDrawPassKey;
WireRenderPassCompatibilityTable m_wireRenderPassCompatibility;
// The interned compatibility id of m_wireDrawPass's attachments, or 0 when not known yet.
// A draw that continues the pass (its WireDrawPassKey compares equal, which covers every
// input of the compatibility key) reuses it instead of building and hashing the key again.
Uint64 m_wireDrawPassCompatibilityId = 0;
// P14: SetupWireDraw's per-draw containers, cleared and refilled every draw instead of
// allocated and freed (the heap was ~10% of a Minecraft frame on the wire arm). A nested
// SetupWireDraw - nothing does that today - would use a local set (WireDraw.inc).
struct WireDrawScratch {
VertexInputStateFactory::BackendVertexInputState input;
Vector<BufferSlice> vertexSlices;
Vector<VkAttachmentDescription> attachments;
Vector<VkImageViewCreateInfo> viewInfos;
Vector<VkAttachmentReference> colors;
Vector<WireImage> sameLayoutAttachments;
WireDrawPassKey passKey;
WireRenderPassCompatibilityKey compatibilityKey;
};
WireDrawScratch m_wireDrawScratch;
struct WireRetiredObjects {
Uint64 submitIndex = 0;
UniquePtr<RenderPassEntry> drawPass;
@@ -104,6 +104,7 @@
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
}
m_wireDrawPass.reset();
m_wireDrawPassCompatibilityId = 0;
for (const auto view : m_wireDrawViews) vkDestroyImageView(m_device, view, nullptr);
m_wireDrawViews.clear();
m_wireDrawPassKey = {};
@@ -117,6 +118,7 @@
}
void VulkanRenderer::RetireWireDrawPass() {
m_wireDrawPassCompatibilityId = 0;
if (!m_wireDrawPass) return;
auto& frame = m_frameContext.GetCurrent();
if (frame.isCommandRecording && VkRenderPassManager::GetActiveRenderPass())
@@ -201,6 +203,11 @@
Bool VulkanRenderer::SetupWireDraw(FrameContext::FrameData& frame, GLenum mode,
Flags<DrawSetupAspect> aspects, const DrawCmdParam& drawParams, const IndexBufferView* indices) {
auto& state = MG_Pipe::MGPipeApplier();
// The per-draw containers (VulkanRenderer.h's WireDrawScratch). Nothing re-enters this
// function from inside it today; if something ever does, it gets its own set rather
// than clearing the outer draw's.
WireDrawScratch nestedScratch;
WireDrawScratch& scratch = m_wirePreparationDepth == 0 ? m_wireDrawScratch : nestedScratch;
const auto program = WireProgramSource(state.BoundShaderCso);
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) {
MGL_WIRE_DECLINE_AT(ProgramLinkOrSpirv, "program is not linked or carries no SPIR-V");
@@ -265,7 +272,7 @@
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");
VertexInputStateFactory::BackendVertexInputState input{};
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
@@ -301,7 +308,8 @@
WireDeclineTally::Count(WireDeclineSite::VertexFormatUnsupported);
return false;
}
Vector<BufferSlice> vertexSlices(input.bindings.size());
auto& vertexSlices = scratch.vertexSlices;
vertexSlices.assign(input.bindings.size(), BufferSlice{});
for (SizeT binding = 0; binding < input.bindings.size(); ++binding) {
const Uint32 location = input.bindingAttributeLocations[binding];
const auto& attr = vao.Attributes[location];
@@ -474,15 +482,19 @@
return false;
}
Vector<VkAttachmentDescription> attachments;
Vector<VkImageViewCreateInfo> viewInfos;
Vector<VkAttachmentReference> colors;
auto& attachments = scratch.attachments;
auto& viewInfos = scratch.viewInfos;
auto& colors = scratch.colors;
attachments.clear();
viewInfos.clear();
colors.clear();
VkAttachmentReference depth{VK_ATTACHMENT_UNUSED, VK_IMAGE_LAYOUT_GENERAL};
VkExtent2D extent{~0u, ~0u};
Uint32 layers = ~0u;
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
Uint64 passHash = 0;
WireDrawPassKey passKey{};
auto& passKey = scratch.passKey;
passKey.ResetKeepingCapacity();
passKey.isDefault = fbo->IsDefault;
passKey.framebufferSrgb = MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);
if (fbo->IsDefault) {
@@ -492,7 +504,8 @@
// attachments below resolve the same index, so this key has to be taken after it.
passKey.swapchainImageIndex = DefaultFramebufferWriteIndex(true);
}
Vector<WireImage> sameLayoutAttachments;
auto& sameLayoutAttachments = scratch.sameLayoutAttachments;
sameLayoutAttachments.clear();
const auto add = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Uint32 {
WireImage image = ResolveWireImage(*fbo, surface, aspect, /*isWriteTarget=*/true);
if (!image.image) return VK_ATTACHMENT_UNUSED;
@@ -580,9 +593,13 @@
if (!stillOpen)
for (auto& image : sameLayoutAttachments) TransitionWireImage(image, VK_IMAGE_LAYOUT_GENERAL);
}
if (!continuing) RetireWireDrawPass();
if (!continuing) {
RetireWireDrawPass();
m_wireDrawPassCompatibilityId = 0;
}
if (!continuing && !AcquireCachedWireDrawPass(passKey)) {
m_wireDrawPassKey = Move(passKey);
// A copy, not a move: passKey is the scratch's, and a new pass is the rare case.
m_wireDrawPassKey = passKey;
Vector<VkImageView> views;
views.reserve(viewInfos.size());
for (const auto& info : viewInfos) {
@@ -609,13 +626,19 @@
created.hash = created.compatibilityHash = XXH64(colors.data(), colors.size() * sizeof(colors[0]), passHash);
}
auto& pass = *m_wireDrawPass;
WireRenderPassCompatibilityKey pipelinePassKey{};
for (const auto& attachment : attachments)
pipelinePassKey.attachmentFormatsAndSamples.push_back(
(static_cast<Uint64>(attachment.format) << 32) | static_cast<Uint32>(attachment.samples));
for (const auto& color : colors) pipelinePassKey.colorReferences.push_back(color.attachment);
pipelinePassKey.depthReference = depth.attachment;
const Uint64 compatibilityId = m_wireRenderPassCompatibility.Intern(pipelinePassKey);
// Interned once per pass: a continued pass has the same key, so the same compatibility.
if (m_wireDrawPassCompatibilityId == 0) {
auto& pipelinePassKey = scratch.compatibilityKey;
pipelinePassKey.attachmentFormatsAndSamples.clear();
pipelinePassKey.colorReferences.clear();
for (const auto& attachment : attachments)
pipelinePassKey.attachmentFormatsAndSamples.push_back(
(static_cast<Uint64>(attachment.format) << 32) | static_cast<Uint32>(attachment.samples));
for (const auto& color : colors) pipelinePassKey.colorReferences.push_back(color.attachment);
pipelinePassKey.depthReference = depth.attachment;
m_wireDrawPassCompatibilityId = m_wireRenderPassCompatibility.Intern(pipelinePassKey);
}
const Uint64 compatibilityId = m_wireDrawPassCompatibilityId;
const auto pipeline = GetOrCreatePipelineWithInput(mode, program, programObj, flags, input, pass,
ResolvePrimitiveRestartEnable(aspects, indices), compatibilityId);
if (pipeline == VK_NULL_HANDLE) {
+16
View File
@@ -475,6 +475,22 @@ namespace MobileGL::MG_Pipe {
// so it obeys VertexBuffersSerial's rule: ADVANCED, never returned to a value it has
// already handed out.
Uint64 BindingsSerial = 0;
// P14: the backend program factory's content hash of THIS record's program, one entry per
// compile variant (the factory's own memo key). The hash reads the archive's SPIR-V, the
// block-binding tail and the variant, so an entry is valid exactly while Serial (a relink)
// and BindingsSerial (set_program_bindings) still hold the values it was taken at. It
// lives in the record so it dies with it: a recycled slot starts from a fresh record and
// no other record can ever answer from it. Nothing in MG_Pipe reads it.
struct BackendHashMemoEntry {
Uint64 Serial = 0;
Uint64 BindingsSerial = 0;
Uint64 Hash = 0;
Uint32 Key = 0;
Bool Valid = false;
};
mutable Array<BackendHashMemoEntry, 4> BackendHashMemo{};
mutable Uint32 BackendHashMemoNext = 0;
};
// The vertex-elements CSO as the applier holds it: the unpacked blob, both views, plus