mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user