[Perf] (Magma): wire draws memoise each unit's sampler and skip rebinding an unchanged pipeline, vertex or index binding (rd12 monolith 57.9 -> 60.3 fps).

This commit is contained in:
2026-10-07 10:50:38 -04:00
parent b556ec3134
commit 4ef9705ee0
6 changed files with 113 additions and 8 deletions
@@ -312,10 +312,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_E_ONCE("Magma wire: sampler CSO {%u, %u} named by texture unit %u is missing or no longer "
"live; sampling with default parameters", samplerHandle.Slot, samplerHandle.Gen, unit);
}
out.sampler = m_samplerManager->GetOrCreateSamplerFromParameters(stale ? kDefaultSamplerParams : sampler->Params,
static_cast<TextureInternalFormat>(record.Desc.InternalFormat),
domain == SamplerNumericDomain::SignedInteger || domain == SamplerNumericDomain::UnsignedInteger,
levels);
const Bool forceNearest =
domain == SamplerNumericDomain::SignedInteger || domain == SamplerNumericDomain::UnsignedInteger;
static_assert(kWireSamplerMemoUnits == MG_Pipe::kMGPipeMaxTextureUnits,
"the wire sampler memo has one entry per texture unit");
auto& memo = m_wireSamplerMemo[static_cast<SizeT>(unit)];
const Bool memoHit = !stale && memo.valid && memo.slot == samplerHandle.Slot &&
memo.gen == samplerHandle.Gen && memo.serial == sampler->Serial &&
memo.contextSerial == state.ContextSerial &&
memo.format == record.Desc.InternalFormat && memo.viewLevelCount == levels &&
memo.forceNearest == forceNearest &&
memo.frameBoundary == m_samplerManager->FrameBoundaryCount() &&
memo.destroyEpoch == m_samplerManager->DestroyEpoch();
if (memoHit) {
out.sampler = memo.sampler;
} else {
out.sampler = m_samplerManager->GetOrCreateSamplerFromParameters(
stale ? kDefaultSamplerParams : sampler->Params,
static_cast<TextureInternalFormat>(record.Desc.InternalFormat), forceNearest, levels);
memo.valid = !stale && out.sampler != VK_NULL_HANDLE;
if (memo.valid) {
memo.slot = samplerHandle.Slot;
memo.gen = samplerHandle.Gen;
memo.serial = sampler->Serial;
memo.contextSerial = state.ContextSerial;
memo.format = record.Desc.InternalFormat;
memo.viewLevelCount = levels;
memo.forceNearest = forceNearest;
memo.frameBoundary = m_samplerManager->FrameBoundaryCount();
memo.destroyEpoch = m_samplerManager->DestroyEpoch();
memo.sampler = out.sampler;
}
}
if (!out.sampler) return false;
}
return true;
@@ -500,5 +500,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
// WireImageWriteEpoch() when ResolveWireImageDescriptor last made image writes visible.
mutable Uint64 m_wireImageBarrierEpoch = 0;
// P14: the wire arm's sampler memo, one entry per texture unit. The sampler a unit resolves
// to is a function of the sampler CSO it names (immutable for a {slot, gen, Serial} within
// one applier - ContextSerial tells appliers apart), the texture's format, the numeric
// domain and the view's level count. A hit skips the parameter hash and the cache map; it
// is taken only while the sampler manager has destroyed nothing (DestroyEpoch) and only
// within the frame boundary of the last real lookup, so every used entry is still stamped
// once per frame and the idle sweep can never retire a sampler this memo hands out.
struct WireSamplerMemo {
Bool valid = false;
Uint32 slot = 0, gen = 0;
Uint64 serial = 0, contextSerial = 0;
Uint32 format = 0, viewLevelCount = 0;
Bool forceNearest = false;
Uint64 frameBoundary = 0, destroyEpoch = 0;
VkSampler sampler = VK_NULL_HANDLE;
};
static constexpr SizeT kWireSamplerMemoUnits = 192; // MG_Pipe::kMGPipeMaxTextureUnits
mutable Array<WireSamplerMemo, kWireSamplerMemoUnits> m_wireSamplerMemo{};
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -274,6 +274,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkSamplerManager::Shutdown() {
++m_destroyEpoch;
for (auto& [_, sampler] : m_samplers) {
if (m_device != VK_NULL_HANDLE && sampler.handle != VK_NULL_HANDLE) {
vkDestroySampler(m_device, sampler.handle, nullptr);
@@ -309,6 +310,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
vkDestroySampler(m_device, entry.handle, nullptr);
}
++m_destroyEpoch;
if (entry.usesCustomBorderColor && m_customBorderColorSamplerCount > 0) {
--m_customBorderColorSamplerCount;
}
@@ -56,6 +56,12 @@ public:
// fence wait. Self-gated: one counter bump and compare except on sweep
// boundaries.
void OnFrameBoundary();
// For a caller that memoises a returned VkSampler (UniformManager's wire sampler memo): the
// frame-boundary count a GetOrCreate call stamps, and a counter that moves whenever any cached
// sampler is destroyed. A memo may reuse a handle only while the destroy epoch is unchanged,
// and must still call GetOrCreate once per frame boundary so the entry stays stamped as used.
Uint64 FrameBoundaryCount() const { return m_frameBoundaryCounter; }
Uint64 DestroyEpoch() const { return m_destroyEpoch; }
// What GL_TEXTURE_BORDER_COLOR resolves to for one (sampler, texture) pair. `color` is always a
// legal VkBorderColor; when `isCustom` it is one of the *_CUSTOM_EXT values and `customValue`
@@ -117,6 +123,8 @@ private:
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameBoundaryCounter = 0;
// Moves on every vkDestroySampler of a cached entry (sweep and Shutdown).
Uint64 m_destroyEpoch = 0;
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -289,6 +289,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDeviceSize indexOffset = 0;
VkIndexType indexType = VK_INDEX_TYPE_MAX_ENUM;
static constexpr Uint32 kMaxShadowedVertexBindings = 8;
// The wire buffer manager's store-destroy epoch the vertex/index halves were stamped at:
// a wire store can be destroyed mid-recording and its VkBuffer handle value re-minted, so
// an equal handle is only the same buffer while this epoch has not moved.
Uint64 bindStoreEpoch = 0;
Bool vertexBindValid = false;
Uint32 vertexBindingCount = 0;
VkBuffer vertexBuffers[kMaxShadowedVertexBindings] = {};
@@ -645,8 +645,41 @@
MGL_WIRE_DECLINE_AT(PipelineUnavailable, "no graphics pipeline could be created for this draw");
return false;
}
for (Uint32 i = 0; i < vertexSlices.size(); ++i)
vkCmdBindVertexBuffers(frame.commandBuffer, i, 1, &vertexSlices[i].buffer, &vertexSlices[i].offset);
// One call for the whole run of bindings 0..n-1 rather than one per binding, and none when
// the command buffer already holds exactly these (DynamicStateShadow: reset at every
// recording begin and by every auxiliary pipeline; the current-attribute binds below only
// ever touch bindings past n).
auto& bindShadow = *g_dynamicStateShadow;
const Uint64 storeEpoch = m_bufferManager.GetWireStoreDestroyEpoch();
if (bindShadow.bindStoreEpoch != storeEpoch) {
bindShadow.vertexBindValid = false;
bindShadow.indexBindValid = false;
bindShadow.bindStoreEpoch = storeEpoch;
}
if (!vertexSlices.empty()) {
VkBuffer buffers[kMaxVertexAttribs];
VkDeviceSize offsets[kMaxVertexAttribs];
const Uint32 count = static_cast<Uint32>(std::min<SizeT>(vertexSlices.size(), kMaxVertexAttribs));
for (Uint32 i = 0; i < count; ++i) {
buffers[i] = vertexSlices[i].buffer;
offsets[i] = vertexSlices[i].offset;
}
constexpr Uint32 kShadowed = DynamicStateShadow::kMaxShadowedVertexBindings;
Bool same = bindShadow.vertexBindValid && bindShadow.vertexBindingCount == count && count <= kShadowed;
for (Uint32 i = 0; same && i < count; ++i)
same = bindShadow.vertexBuffers[i] == buffers[i] && bindShadow.vertexOffsets[i] == offsets[i];
if (!same) {
vkCmdBindVertexBuffers(frame.commandBuffer, 0, count, buffers, offsets);
bindShadow.vertexBindValid = count <= kShadowed;
bindShadow.vertexBindingCount = count;
for (Uint32 i = 0; i < count && i < kShadowed; ++i) {
bindShadow.vertexBuffers[i] = buffers[i];
bindShadow.vertexOffsets[i] = offsets[i];
}
}
for (Uint32 i = count; i < vertexSlices.size(); ++i)
vkCmdBindVertexBuffers(frame.commandBuffer, i, 1, &vertexSlices[i].buffer, &vertexSlices[i].offset);
}
Uint32 binding = static_cast<Uint32>(vertexSlices.size());
for (Uint32 location = 0; location < kMaxVertexAttribs; ++location) {
if (!(programObj.activeVertexInputLocationMask & (1u << location)) ||
@@ -667,7 +700,15 @@
}
vkCmdBindVertexBuffers(frame.commandBuffer, binding++, 1, &slice.buffer, &slice.offset);
}
if (indexSlice.IsValid()) vkCmdBindIndexBuffer(frame.commandBuffer, indexSlice.buffer, indexSlice.offset, indexType);
if (indexSlice.IsValid() &&
!(bindShadow.indexBindValid && bindShadow.indexBuffer == indexSlice.buffer &&
bindShadow.indexOffset == indexSlice.offset && bindShadow.indexType == indexType)) {
vkCmdBindIndexBuffer(frame.commandBuffer, indexSlice.buffer, indexSlice.offset, indexType);
bindShadow.indexBindValid = true;
bindShadow.indexBuffer = indexSlice.buffer;
bindShadow.indexOffset = indexSlice.offset;
bindShadow.indexType = indexType;
}
// Still open from the previous draw (nothing closed it, same command buffer): continue.
const auto* active = VkRenderPassManager::GetActiveRenderPass();
const Bool open = continuing && active != nullptr && active->commandBuffer == frame.commandBuffer &&
@@ -677,7 +718,11 @@
MGL_WIRE_DECLINE_AT(BeginRenderPass, "the render pass for this draw could not be begun");
return false;
}
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
if (!bindShadow.graphicsPipelineValid || bindShadow.graphicsPipeline != pipeline) {
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
bindShadow.graphicsPipelineValid = true;
bindShadow.graphicsPipeline = pipeline;
}
ApplyDynamicDrawStateTail(frame, pass.extent, fbo->IsDefault,
ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin));
// Storage images this draw writes are what the next sampled-image barrier must cover.