graphics: cut native-RHI churn stalls (batched view retirement, bounded drains, buffer bind classes, rebind skip)

This commit is contained in:
mchughalex 2026-07-21 05:24:35 +01:00
parent 7466d3c75e
commit aa0f046ecf
3 changed files with 197 additions and 61 deletions

View file

@ -235,9 +235,20 @@ class Pipeline {
// Descriptors. // Descriptors.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Binding classes a buffer will be used with. kFull is the historical
// default (constant buffer / buffer SRV / vertex / index / copy source).
// Narrower classes let backends drop the descriptor-window padding that only
// dynamic-offset constant/SRV binds need, and request narrower usage.
enum class BufferBindClass : uint32_t {
kFull,
kVertexIndex, // vertex/index binds and copy source only
kCopySrc, // copy source only (texture upload staging)
};
struct BufferDesc { struct BufferDesc {
uint64_t size = 0; uint64_t size = 0;
HeapKind heap = HeapKind::kDefault; HeapKind heap = HeapKind::kDefault;
BufferBindClass bind_class = BufferBindClass::kFull;
}; };
struct TextureDesc { struct TextureDesc {

View file

@ -15,6 +15,7 @@
#include <map> #include <map>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <unordered_set>
#include <utility> #include <utility>
#include <vector> #include <vector>
@ -395,7 +396,26 @@ class NrCmdD3D12 : public nrhi::Cmd {
ResourceState after) override; ResourceState after) override;
void FlushBarriers() override; void FlushBarriers() override;
// Called at frame begin and on root-signature changes: the fresh deferred
// command list / new root signature carries no bindings, so the latched
// tuples must not suppress the first re-bind.
void ResetFrameState() {
std::memset(last_table_views_, 0, sizeof(last_table_views_));
std::memset(last_table_counts_, 0, sizeof(last_table_counts_));
}
NrDeviceD3D12* device = nullptr; NrDeviceD3D12* device = nullptr;
private:
void BindTextureTable(uint32_t param, NrTextureViewD3D12* const* views, uint32_t count);
// Last tuple bound per root param: consecutive draws mostly rebind the
// same views (shadow atlas, cube map, white fallback), skipping both the
// binding-cache lookup and the root-table re-record. A zero count is the
// empty/invalid state; real bindings always have count >= 1.
NrTextureViewD3D12* last_table_views_[nrhi::kMaxBindingParams]
[nrhi::kMaxTextureTableSize] = {};
uint32_t last_table_counts_[nrhi::kMaxBindingParams] = {};
}; };
class NrDeviceD3D12 : public nrhi::Device { class NrDeviceD3D12 : public nrhi::Device {
@ -432,6 +452,7 @@ class NrDeviceD3D12 : public nrhi::Device {
~NrDeviceD3D12() override { ~NrDeviceD3D12() override {
// Callers guarantee GPU idle; release everything immediately. // Callers guarantee GPU idle; release everything immediately.
FlushDissolvedViews();
DrainRetired(~0ull); DrainRetired(~0ull);
for (auto& entry : guest_outputs_) { for (auto& entry : guest_outputs_) {
entry.second->resource->Release(); entry.second->resource->Release();
@ -585,22 +606,37 @@ class NrDeviceD3D12 : public nrhi::Device {
void DestroyDeferred(nrhi::TextureView* view) override { void DestroyDeferred(nrhi::TextureView* view) override {
if (view == nullptr) return; if (view == nullptr) return;
auto* v = static_cast<NrTextureViewD3D12*>(view); // Destruction is batched: the view object stays allocated (so its
// address cannot be reused while stale binding-cache keys still hold it)
// and FlushDissolvedViews sweeps the binding cache ONCE per frame for
// the whole batch instead of once per destroyed view.
dissolved_views_.push_back(static_cast<NrTextureViewD3D12*>(view));
}
// Render thread, once per frame (and at device destruction): retire every
// shader-visible binding referencing a view destroyed since the last
// flush, then retire the views themselves.
void FlushDissolvedViews() {
if (dissolved_views_.empty()) return;
std::unordered_set<const NrTextureViewD3D12*> dissolved(dissolved_views_.begin(),
dissolved_views_.end());
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
const uint64_t submission = cp_->GetCurrentSubmission(); const uint64_t submission = cp_->GetCurrentSubmission();
staging_slots_.Retire(v->staging_slot, 1, submission);
// Retire every shader-visible binding this view participates in.
std::erase_if(bindings_, [&](auto& entry) { std::erase_if(bindings_, [&](auto& entry) {
for (uint32_t i = 0; i < entry.first.count; ++i) { for (uint32_t i = 0; i < entry.first.count; ++i) {
if (entry.first.views[i] == v) { if (dissolved.count(entry.first.views[i]) != 0) {
srv_slots_.Retire(entry.second.first_slot, entry.second.count, submission); srv_slots_.Retire(entry.second.first_slot, entry.second.count, submission);
return true; return true;
} }
} }
return false; return false;
}); });
for (NrTextureViewD3D12* v : dissolved_views_) {
staging_slots_.Retire(v->staging_slot, 1, submission);
delete v; delete v;
} }
dissolved_views_.clear();
}
void DestroyDeferred(nrhi::Pipeline* pipeline) override { void DestroyDeferred(nrhi::Pipeline* pipeline) override {
if (pipeline == nullptr) return; if (pipeline == nullptr) return;
@ -867,9 +903,14 @@ class NrDeviceD3D12 : public nrhi::Device {
D3D12_RESOURCE_STATES guest_output_internal_state, uint32_t width, D3D12_RESOURCE_STATES guest_output_internal_state, uint32_t width,
uint32_t height, nrhi::Texture** guest_output_out) { uint32_t height, nrhi::Texture** guest_output_out) {
guest_output_state_ = guest_output_internal_state; guest_output_state_ = guest_output_internal_state;
cmd_.ResetFrameState();
FlushDissolvedViews();
{ {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
DrainRetired(cp_->GetCompletedSubmission()); // Bounded per frame: the eviction sweeps retire thousands of objects
// at once, and releasing them all in one frame is its own hitch (the
// remainder drains over the following frames).
DrainRetired(cp_->GetCompletedSubmission(), 512);
} }
NrTextureD3D12*& wrapper = guest_outputs_[guest_output_resource]; NrTextureD3D12*& wrapper = guest_outputs_[guest_output_resource];
if (wrapper != nullptr && if (wrapper != nullptr &&
@ -1006,9 +1047,11 @@ class NrDeviceD3D12 : public nrhi::Device {
uint32_t count; uint32_t count;
}; };
void DrainRetired(uint64_t completed) { void DrainRetired(uint64_t completed, size_t max_objects = SIZE_MAX) {
size_t released = 0;
std::erase_if(retired_, [&](const RetiredObject& r) { std::erase_if(retired_, [&](const RetiredObject& r) {
if (r.submission < completed) { if (r.submission < completed && released < max_objects) {
++released;
if (r.resource) r.resource->Release(); if (r.resource) r.resource->Release();
if (r.pso) r.pso->Release(); if (r.pso) r.pso->Release();
return true; return true;
@ -1053,6 +1096,10 @@ class NrDeviceD3D12 : public nrhi::Device {
SlotAllocator dsv_slots_; SlotAllocator dsv_slots_;
std::vector<RetiredObject> retired_; std::vector<RetiredObject> retired_;
std::map<BindingKey, Binding> bindings_; std::map<BindingKey, Binding> bindings_;
// Views destroyed since the last FlushDissolvedViews (render thread only).
// The objects stay allocated until the flush so stale binding-cache keys
// can never collide with a newly created view at the same address.
std::vector<NrTextureViewD3D12*> dissolved_views_;
std::map<ID3D12Resource*, NrTextureD3D12*> guest_outputs_; std::map<ID3D12Resource*, NrTextureD3D12*> guest_outputs_;
D3D12_RESOURCE_STATES guest_output_state_ = D3D12_RESOURCE_STATE_COMMON; D3D12_RESOURCE_STATES guest_output_state_ = D3D12_RESOURCE_STATE_COMMON;
}; };
@ -1062,6 +1109,8 @@ void NrCmdD3D12::SetBindingLayout(nrhi::BindingLayout* layout) {
DeferredCommandList& list = device->cp()->GetDeferredCommandList(); DeferredCommandList& list = device->cp()->GetDeferredCommandList();
list.SetDescriptorHeaps(device->srv_heap(), nullptr); list.SetDescriptorHeaps(device->srv_heap(), nullptr);
list.D3DSetGraphicsRootSignature(l->root_signature); list.D3DSetGraphicsRootSignature(l->root_signature);
// Root-signature semantics: all bindings reset.
ResetFrameState();
} }
void NrCmdD3D12::SetPipeline(nrhi::Pipeline* pipeline) { void NrCmdD3D12::SetPipeline(nrhi::Pipeline* pipeline) {
@ -1085,22 +1134,31 @@ void NrCmdD3D12::SetBufferSrv(uint32_t param, nrhi::Buffer* buffer, uint64_t off
param, static_cast<NrBufferD3D12*>(buffer)->gpu_va + offset); param, static_cast<NrBufferD3D12*>(buffer)->gpu_va + offset);
} }
void NrCmdD3D12::BindTextureTable(uint32_t param, NrTextureViewD3D12* const* views,
uint32_t count) {
if (param >= nrhi::kMaxBindingParams) return;
if (count == last_table_counts_[param] &&
std::memcmp(last_table_views_[param], views, count * sizeof(views[0])) == 0) {
return; // identical tuple already bound on this root param
}
D3D12_GPU_DESCRIPTOR_HANDLE handle;
if (device->GetBinding(views, count, &handle)) {
device->cp()->GetDeferredCommandList().D3DSetGraphicsRootDescriptorTable(param, handle);
std::memcpy(last_table_views_[param], views, count * sizeof(views[0]));
last_table_counts_[param] = count;
}
}
void NrCmdD3D12::SetTexture(uint32_t param, nrhi::TextureView* view) { void NrCmdD3D12::SetTexture(uint32_t param, nrhi::TextureView* view) {
NrTextureViewD3D12* views[1] = {static_cast<NrTextureViewD3D12*>(view)}; NrTextureViewD3D12* views[1] = {static_cast<NrTextureViewD3D12*>(view)};
D3D12_GPU_DESCRIPTOR_HANDLE handle; BindTextureTable(param, views, 1);
if (device->GetBinding(views, 1, &handle)) {
device->cp()->GetDeferredCommandList().D3DSetGraphicsRootDescriptorTable(param, handle);
}
} }
void NrCmdD3D12::SetTexturePair(uint32_t param, nrhi::TextureView* first, void NrCmdD3D12::SetTexturePair(uint32_t param, nrhi::TextureView* first,
nrhi::TextureView* second) { nrhi::TextureView* second) {
NrTextureViewD3D12* views[2] = {static_cast<NrTextureViewD3D12*>(first), NrTextureViewD3D12* views[2] = {static_cast<NrTextureViewD3D12*>(first),
static_cast<NrTextureViewD3D12*>(second)}; static_cast<NrTextureViewD3D12*>(second)};
D3D12_GPU_DESCRIPTOR_HANDLE handle; BindTextureTable(param, views, 2);
if (device->GetBinding(views, 2, &handle)) {
device->cp()->GetDeferredCommandList().D3DSetGraphicsRootDescriptorTable(param, handle);
}
} }
void NrCmdD3D12::SetTextures(uint32_t param, nrhi::TextureView* const* views, uint32_t count) { void NrCmdD3D12::SetTextures(uint32_t param, nrhi::TextureView* const* views, uint32_t count) {
@ -1109,10 +1167,7 @@ void NrCmdD3D12::SetTextures(uint32_t param, nrhi::TextureView* const* views, ui
for (uint32_t i = 0; i < count; ++i) { for (uint32_t i = 0; i < count; ++i) {
typed[i] = static_cast<NrTextureViewD3D12*>(views[i]); typed[i] = static_cast<NrTextureViewD3D12*>(views[i]);
} }
D3D12_GPU_DESCRIPTOR_HANDLE handle; BindTextureTable(param, typed, count);
if (device->GetBinding(typed, count, &handle)) {
device->cp()->GetDeferredCommandList().D3DSetGraphicsRootDescriptorTable(param, handle);
}
} }
void NrCmdD3D12::SetRenderTargets(nrhi::Texture* color, nrhi::Texture* depth) { void NrCmdD3D12::SetRenderTargets(nrhi::Texture* color, nrhi::Texture* depth) {

View file

@ -248,8 +248,9 @@ class NrBufferVulkan : public nrhi::Buffer {
VkBuffer buffer = VK_NULL_HANDLE; VkBuffer buffer = VK_NULL_HANDLE;
VmaAllocation allocation = VK_NULL_HANDLE; VmaAllocation allocation = VK_NULL_HANDLE;
void* mapping = nullptr; void* mapping = nullptr;
uint64_t size_ = 0; // the app-requested size (allocation is padded) uint64_t size_ = 0; // the app-requested size (kFull allocations are padded)
HeapKind heap = HeapKind::kDefault; HeapKind heap = HeapKind::kDefault;
nrhi::BufferBindClass bind_class = nrhi::BufferBindClass::kFull;
bool device_local = false; // memory-type DEVICE_LOCAL (diagnostics) bool device_local = false; // memory-type DEVICE_LOCAL (diagnostics)
}; };
@ -384,10 +385,11 @@ struct NrFrameProf {
Bucket view_create; Bucket view_create;
Bucket const_slice; Bucket const_slice;
Bucket pipeline_build; Bucket pipeline_build;
Bucket drain;
void Reset() { *this = NrFrameProf{}; } void Reset() { *this = NrFrameProf{}; }
uint64_t Total() const { uint64_t Total() const {
return pass_open.us + flush_barriers.us + copies.us + table_miss.us + set0_miss.us + return pass_open.us + flush_barriers.us + copies.us + table_miss.us + set0_miss.us +
view_destroy.us + view_create.us + const_slice.us + pipeline_build.us; view_destroy.us + view_create.us + const_slice.us + pipeline_build.us + drain.us;
} }
}; };
@ -535,6 +537,7 @@ class NrDeviceVulkan : public nrhi::Device {
// Callers guarantee GPU idle; release everything immediately. // Callers guarantee GPU idle; release everything immediately.
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device_->functions(); const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device_->functions();
const VkDevice device = vulkan_device_->device(); const VkDevice device = vulkan_device_->device();
FlushDissolvedViews();
DrainRetired(~0ull); DrainRetired(~0ull);
for (auto& entry : set0_sets_) { for (auto& entry : set0_sets_) {
dfn.vkFreeDescriptorSets(device, entry.second.pool, 1, &entry.second.set); dfn.vkFreeDescriptorSets(device, entry.second.pool, 1, &entry.second.set);
@ -649,12 +652,31 @@ class NrDeviceVulkan : public nrhi::Device {
info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VmaAllocationCreateInfo alloc_info = {}; VmaAllocationCreateInfo alloc_info = {};
switch (desc.heap) { // The descriptor-window padding only matters for buffers that can be
case HeapKind::kUpload: // bound through dynamic-offset UBO/SSBO descriptors; narrower bind
info.size = desc.size + kBufferPadding; // classes skip it (and the descriptor usage bits).
info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | const bool descriptor_bindable = desc.bind_class == nrhi::BufferBindClass::kFull;
const uint64_t padding = descriptor_bindable ? kBufferPadding : 0;
VkBufferUsageFlags app_usage;
switch (desc.bind_class) {
case nrhi::BufferBindClass::kCopySrc:
app_usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
break;
case nrhi::BufferBindClass::kVertexIndex:
app_usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT |
VK_BUFFER_USAGE_INDEX_BUFFER_BIT;
break;
case nrhi::BufferBindClass::kFull:
default:
app_usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT |
VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT |
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
break;
}
switch (desc.heap) {
case HeapKind::kUpload:
info.size = desc.size + padding;
info.usage = app_usage;
alloc_info.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT | alloc_info.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT |
VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
alloc_info.usage = VMA_MEMORY_USAGE_AUTO; alloc_info.usage = VMA_MEMORY_USAGE_AUTO;
@ -676,13 +698,14 @@ class NrDeviceVulkan : public nrhi::Device {
break; break;
case HeapKind::kDefault: case HeapKind::kDefault:
default: default:
info.size = desc.size + kBufferPadding; info.size = desc.size + padding;
info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | app_usage;
VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT |
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
alloc_info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; alloc_info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
break; break;
} }
// Unpadded classes could otherwise reach vkCreateBuffer with size 0
// (invalid); the padded classes never could.
info.size = std::max<VkDeviceSize>(info.size, 4);
VkBuffer buffer = VK_NULL_HANDLE; VkBuffer buffer = VK_NULL_HANDLE;
VmaAllocation allocation = VK_NULL_HANDLE; VmaAllocation allocation = VK_NULL_HANDLE;
VmaAllocationInfo result_info = {}; VmaAllocationInfo result_info = {};
@ -712,6 +735,7 @@ class NrDeviceVulkan : public nrhi::Device {
b->mapping = result_info.pMappedData; // persistent map for host heaps b->mapping = result_info.pMappedData; // persistent map for host heaps
b->size_ = desc.size; b->size_ = desc.size;
b->heap = desc.heap; b->heap = desc.heap;
b->bind_class = desc.bind_class;
b->device_local = mem_dl; b->device_local = mem_dl;
{ {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
@ -806,8 +830,10 @@ class NrDeviceVulkan : public nrhi::Device {
// Retire cached set-0 descriptor sets whose buffer tuple references this // Retire cached set-0 descriptor sets whose buffer tuple references this
// buffer. set0_sets_ is render-thread-only, like the D3D12 backend's // buffer. set0_sets_ is render-thread-only, like the D3D12 backend's
// bindings_: buffer destruction of bound buffers happens on the render // bindings_: buffer destruction of bound buffers happens on the render
// thread. // thread. Non-kFull buffers can never appear in a descriptor tuple, so
// their destruction (the mesh/staging churn path) skips the scan.
const uint64_t submission = cp_->GetCurrentSubmission(); const uint64_t submission = cp_->GetCurrentSubmission();
if (b->bind_class == nrhi::BufferBindClass::kFull) {
for (auto it = set0_sets_.begin(); it != set0_sets_.end();) { for (auto it = set0_sets_.begin(); it != set0_sets_.end();) {
bool references = false; bool references = false;
for (uint32_t i = 0; i < it->first.count; ++i) { for (uint32_t i = 0; i < it->first.count; ++i) {
@ -824,6 +850,7 @@ class NrDeviceVulkan : public nrhi::Device {
++it; ++it;
} }
} }
}
if (b->heap == HeapKind::kUpload) { if (b->heap == HeapKind::kUpload) {
(b->device_local ? g_buf_bytes_upload_dl : g_buf_bytes_upload_host) (b->device_local ? g_buf_bytes_upload_dl : g_buf_bytes_upload_host)
.fetch_sub(b->size_, std::memory_order_relaxed); .fetch_sub(b->size_, std::memory_order_relaxed);
@ -864,15 +891,33 @@ class NrDeviceVulkan : public nrhi::Device {
void DestroyDeferred(nrhi::TextureView* view) override { void DestroyDeferred(nrhi::TextureView* view) override {
if (view == nullptr) return; if (view == nullptr) return;
NrProfScope prof_scope(prof_.view_destroy); // Destruction is batched: the view object stays allocated (so its
// address cannot be reused by a new view while stale cache keys still
// hold it) and FlushDissolvedViews sweeps the descriptor-set cache ONCE
// per frame for the whole batch. The eviction sweeps retire ~1500 views
// in a single frame; a per-view cache scan was a measured 10-15 ms
// recurring hitch.
auto* v = static_cast<NrTextureViewVulkan*>(view); auto* v = static_cast<NrTextureViewVulkan*>(view);
{
std::lock_guard<std::mutex> lock(mutex_);
live_views_.erase(v);
}
dissolved_views_.push_back(v);
}
// Render thread, once per frame (and at device destruction): retire every
// cached texture descriptor set referencing a view destroyed since the
// last flush, then retire the views themselves.
void FlushDissolvedViews() {
if (dissolved_views_.empty()) return;
NrProfScope prof_scope(prof_.view_destroy);
const uint64_t submission = cp_->GetCurrentSubmission(); const uint64_t submission = cp_->GetCurrentSubmission();
// Retire every cached texture descriptor set this view participates in std::unordered_set<const NrTextureViewVulkan*> dissolved(dissolved_views_.begin(),
// (render-thread-only cache, mirroring the D3D12 backend's bindings_). dissolved_views_.end());
for (auto it = table_sets_.begin(); it != table_sets_.end();) { for (auto it = table_sets_.begin(); it != table_sets_.end();) {
bool references = false; bool references = false;
for (uint32_t i = 0; i < it->first.count; ++i) { for (uint32_t i = 0; i < it->first.count; ++i) {
if (it->first.views[i] == v) { if (dissolved.count(it->first.views[i]) != 0) {
references = true; references = true;
break; break;
} }
@ -886,13 +931,15 @@ class NrDeviceVulkan : public nrhi::Device {
} }
} }
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
live_views_.erase(v); for (NrTextureViewVulkan* v : dissolved_views_) {
RetiredObject r; RetiredObject r;
r.submission = submission; r.submission = submission;
r.view = v->view; r.view = v->view;
retired_.push_back(r); retired_.push_back(r);
delete v; delete v;
} }
dissolved_views_.clear();
}
void DestroyDeferred(nrhi::Pipeline* pipeline) override { void DestroyDeferred(nrhi::Pipeline* pipeline) override {
if (pipeline == nullptr) return; if (pipeline == nullptr) return;
@ -1210,9 +1257,13 @@ class NrDeviceVulkan : public nrhi::Device {
bool guest_output_ever_written, uint32_t width, uint32_t height, bool guest_output_ever_written, uint32_t width, uint32_t height,
nrhi::Texture** guest_output_out) { nrhi::Texture** guest_output_out) {
prof_.Reset(); prof_.Reset();
FlushDissolvedViews();
{ {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
DrainRetired(cp_->GetCompletedSubmission()); // Bounded per frame: the eviction sweeps retire thousands of objects
// at once, and destroying them all in one frame is its own hitch (the
// remainder drains over the following frames).
DrainRetired(cp_->GetCompletedSubmission(), 512);
} }
++frame_index_; ++frame_index_;
if ((frame_index_ % 600) == 0) { if ((frame_index_ % 600) == 0) {
@ -1307,13 +1358,14 @@ class NrDeviceVulkan : public nrhi::Device {
REXLOG_INFO( REXLOG_INFO(
"nrhi-vulkan: SLOW frame {}us: pass_open={}us/{} flush={}us/{} copies={}us/{} " "nrhi-vulkan: SLOW frame {}us: pass_open={}us/{} flush={}us/{} copies={}us/{} "
"table_miss={}us/{} set0_miss={}us/{} view_destroy={}us/{} view_create={}us/{} " "table_miss={}us/{} set0_miss={}us/{} view_destroy={}us/{} view_create={}us/{} "
"const={}us/{} pso={}us/{}", "const={}us/{} pso={}us/{} drain={}us/{}",
total_us, prof_.pass_open.us, prof_.pass_open.count, prof_.flush_barriers.us, total_us, prof_.pass_open.us, prof_.pass_open.count, prof_.flush_barriers.us,
prof_.flush_barriers.count, prof_.copies.us, prof_.copies.count, prof_.table_miss.us, prof_.flush_barriers.count, prof_.copies.us, prof_.copies.count, prof_.table_miss.us,
prof_.table_miss.count, prof_.set0_miss.us, prof_.set0_miss.count, prof_.table_miss.count, prof_.set0_miss.us, prof_.set0_miss.count,
prof_.view_destroy.us, prof_.view_destroy.count, prof_.view_create.us, prof_.view_destroy.us, prof_.view_destroy.count, prof_.view_create.us,
prof_.view_create.count, prof_.const_slice.us, prof_.const_slice.count, prof_.view_create.count, prof_.const_slice.us, prof_.const_slice.count,
prof_.pipeline_build.us, prof_.pipeline_build.count); prof_.pipeline_build.us, prof_.pipeline_build.count, prof_.drain.us,
prof_.drain.count);
} }
} }
} }
@ -1742,11 +1794,14 @@ class NrDeviceVulkan : public nrhi::Device {
retired_.push_back(r); retired_.push_back(r);
} }
void DrainRetired(uint64_t completed) { void DrainRetired(uint64_t completed, size_t max_objects = SIZE_MAX) {
NrProfScope prof_scope(prof_.drain);
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device_->functions(); const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device_->functions();
const VkDevice device = vulkan_device_->device(); const VkDevice device = vulkan_device_->device();
size_t destroyed = 0;
std::erase_if(retired_, [&](const RetiredObject& r) { std::erase_if(retired_, [&](const RetiredObject& r) {
if (r.submission >= completed) return false; if (r.submission >= completed || destroyed >= max_objects) return false;
++destroyed;
if (r.framebuffer != VK_NULL_HANDLE) dfn.vkDestroyFramebuffer(device, r.framebuffer, nullptr); if (r.framebuffer != VK_NULL_HANDLE) dfn.vkDestroyFramebuffer(device, r.framebuffer, nullptr);
if (r.view != VK_NULL_HANDLE) dfn.vkDestroyImageView(device, r.view, nullptr); if (r.view != VK_NULL_HANDLE) dfn.vkDestroyImageView(device, r.view, nullptr);
for (VkPipeline pipeline : r.pipelines) { for (VkPipeline pipeline : r.pipelines) {
@ -2027,6 +2082,11 @@ class NrDeviceVulkan : public nrhi::Device {
std::unordered_set<NrPipelineVulkan*> live_pipelines_; std::unordered_set<NrPipelineVulkan*> live_pipelines_;
std::unordered_set<NrShaderVulkan*> live_shaders_; std::unordered_set<NrShaderVulkan*> live_shaders_;
// Views destroyed since the last FlushDissolvedViews (render thread only).
// The objects stay allocated until the flush so stale descriptor-set cache
// keys can never collide with a newly created view at the same address.
std::vector<NrTextureViewVulkan*> dissolved_views_;
std::map<RenderPassKey, VkRenderPass> render_passes_; std::map<RenderPassKey, VkRenderPass> render_passes_;
std::map<FramebufferKey, VkFramebuffer> framebuffers_; std::map<FramebufferKey, VkFramebuffer> framebuffers_;
std::vector<VkDescriptorPool> descriptor_pools_; std::vector<VkDescriptorPool> descriptor_pools_;
@ -2312,14 +2372,24 @@ void NrCmdVulkan::SetTextures(uint32_t param, nrhi::TextureView* const* views, u
if (layout_ == nullptr || param >= layout_->param_count) return; if (layout_ == nullptr || param >= layout_->param_count) return;
const NrBindingLayoutVulkan::ParamInfo& p = layout_->params[param]; const NrBindingLayoutVulkan::ParamInfo& p = layout_->params[param];
if (p.kind != nrhi::BindingParamKind::kTextureTable) return; if (p.kind != nrhi::BindingParamKind::kTextureTable) return;
// Consecutive draws mostly rebind the same tuple (shadow atlas, cube map,
// white fallback); leaving the table clean skips the per-draw descriptor
// set lookup and rebind entirely.
bool changed = false;
for (uint32_t i = 0; i < p.table_size; ++i) { for (uint32_t i = 0; i < p.table_size; ++i) {
// Missing tail entries (table declares N, bound M<N) and null entries // Missing tail entries (table declares N, bound M<N) and null entries
// fall back to the backend's 1x1 white. // fall back to the backend's 1x1 white.
table_views_[p.table_index][i] = NrTextureViewVulkan* view =
i < count ? static_cast<NrTextureViewVulkan*>(views[i]) : nullptr; i < count ? static_cast<NrTextureViewVulkan*>(views[i]) : nullptr;
if (table_views_[p.table_index][i] != view) {
table_views_[p.table_index][i] = view;
changed = true;
} }
}
if (changed) {
table_dirty_[p.table_index] = true; table_dirty_[p.table_index] = true;
} }
}
void NrCmdVulkan::SetRenderTargets(nrhi::Texture* color, nrhi::Texture* depth) { void NrCmdVulkan::SetRenderTargets(nrhi::Texture* color, nrhi::Texture* depth) {
EndRenderPassIfOpen(); EndRenderPassIfOpen();