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