diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index 245c2f5b..df43ed72 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -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(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(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(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; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index bf4cf270..bae2e5bc 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -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 m_wireSamplerMemo{}; }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp index b87a6198..7123442a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp @@ -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; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h index da5a4f97..2e90d9d3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h @@ -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 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 diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index e23c09e5..e9c51bf3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -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] = {}; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index da7741de..8a4c63aa 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -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(std::min(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(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.