ADD: texture reload from disk enabled
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include "vk_device.h"
|
#include "vk_device.h"
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
void TextureCache::init(EngineContext *ctx)
|
void TextureCache::init(EngineContext *ctx)
|
||||||
{
|
{
|
||||||
@@ -118,6 +119,24 @@ void TextureCache::watchBinding(TextureHandle handle, VkDescriptorSet set, uint3
|
|||||||
_setToHandles[set].push_back(handle);
|
_setToHandles[set].push_back(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TextureCache::unwatchSet(VkDescriptorSet set)
|
||||||
|
{
|
||||||
|
if (set == VK_NULL_HANDLE) return;
|
||||||
|
auto it = _setToHandles.find(set);
|
||||||
|
if (it == _setToHandles.end()) return;
|
||||||
|
|
||||||
|
const auto &handles = it->second;
|
||||||
|
for (TextureHandle h : handles)
|
||||||
|
{
|
||||||
|
if (h >= _entries.size()) continue;
|
||||||
|
auto &patches = _entries[h].patches;
|
||||||
|
patches.erase(std::remove_if(patches.begin(), patches.end(),
|
||||||
|
[&](const Patch &p){ return p.set == set; }),
|
||||||
|
patches.end());
|
||||||
|
}
|
||||||
|
_setToHandles.erase(it);
|
||||||
|
}
|
||||||
|
|
||||||
void TextureCache::markUsed(TextureHandle handle, uint32_t frameIndex)
|
void TextureCache::markUsed(TextureHandle handle, uint32_t frameIndex)
|
||||||
{
|
{
|
||||||
if (handle == InvalidHandle) return;
|
if (handle == InvalidHandle) return;
|
||||||
@@ -190,7 +209,8 @@ void TextureCache::pumpLoads(ResourceManager &rm, FrameResources &)
|
|||||||
const uint32_t now = _context ? _context->frameIndex : 0u;
|
const uint32_t now = _context ? _context->frameIndex : 0u;
|
||||||
for (auto &e : _entries)
|
for (auto &e : _entries)
|
||||||
{
|
{
|
||||||
if (e.state == EntryState::Unloaded)
|
// Allow both Unloaded and Evicted entries to start work if seen again.
|
||||||
|
if (e.state == EntryState::Unloaded || e.state == EntryState::Evicted)
|
||||||
{
|
{
|
||||||
// Visibility-driven residency: only start uploads for textures
|
// Visibility-driven residency: only start uploads for textures
|
||||||
// that were marked used recently (current or previous frame).
|
// that were marked used recently (current or previous frame).
|
||||||
@@ -201,7 +221,9 @@ void TextureCache::pumpLoads(ResourceManager &rm, FrameResources &)
|
|||||||
// Schedule when first seen (previous frame) or if seen again.
|
// Schedule when first seen (previous frame) or if seen again.
|
||||||
recentlyUsed = (now == 0u) || (now - e.lastUsedFrame <= 1u);
|
recentlyUsed = (now == 0u) || (now - e.lastUsedFrame <= 1u);
|
||||||
}
|
}
|
||||||
if (recentlyUsed)
|
// Gate reload attempts to avoid rapid oscillation right after eviction.
|
||||||
|
bool cooldownPassed = (now >= e.nextAttemptFrame);
|
||||||
|
if (recentlyUsed && cooldownPassed)
|
||||||
{
|
{
|
||||||
enqueue_decode(e);
|
enqueue_decode(e);
|
||||||
if (++started >= _maxLoadsPerPump) break;
|
if (++started >= _maxLoadsPerPump) break;
|
||||||
@@ -233,12 +255,15 @@ void TextureCache::evictToBudget(size_t budgetBytes)
|
|||||||
}
|
}
|
||||||
std::sort(order.begin(), order.end(), [](auto &a, auto &b) { return a.second < b.second; });
|
std::sort(order.begin(), order.end(), [](auto &a, auto &b) { return a.second < b.second; });
|
||||||
|
|
||||||
|
const uint32_t now = _context ? _context->frameIndex : 0u;
|
||||||
for (auto &pair : order)
|
for (auto &pair : order)
|
||||||
{
|
{
|
||||||
if (_residentBytes <= budgetBytes) break;
|
if (_residentBytes <= budgetBytes) break;
|
||||||
TextureHandle h = pair.first;
|
TextureHandle h = pair.first;
|
||||||
Entry &e = _entries[h];
|
Entry &e = _entries[h];
|
||||||
if (e.state != EntryState::Resident) continue;
|
if (e.state != EntryState::Resident) continue;
|
||||||
|
// Prefer not to evict textures used this frame unless strictly necessary.
|
||||||
|
if (e.lastUsedFrame == now) continue;
|
||||||
|
|
||||||
// Rewrite watchers back to fallback before destroying
|
// Rewrite watchers back to fallback before destroying
|
||||||
patch_to_fallback(e);
|
patch_to_fallback(e);
|
||||||
@@ -246,13 +271,15 @@ void TextureCache::evictToBudget(size_t budgetBytes)
|
|||||||
_context->getResources()->destroy_image(e.image);
|
_context->getResources()->destroy_image(e.image);
|
||||||
e.image = {};
|
e.image = {};
|
||||||
e.state = EntryState::Evicted;
|
e.state = EntryState::Evicted;
|
||||||
|
e.lastEvictedFrame = now;
|
||||||
|
e.nextAttemptFrame = std::max(e.nextAttemptFrame, now + _reloadCooldownFrames);
|
||||||
if (_residentBytes >= e.sizeBytes) _residentBytes -= e.sizeBytes; else _residentBytes = 0;
|
if (_residentBytes >= e.sizeBytes) _residentBytes -= e.sizeBytes; else _residentBytes = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextureCache::enqueue_decode(Entry &e)
|
void TextureCache::enqueue_decode(Entry &e)
|
||||||
{
|
{
|
||||||
if (e.state != EntryState::Unloaded) return;
|
if (e.state != EntryState::Unloaded && e.state != EntryState::Evicted) return;
|
||||||
e.state = EntryState::Loading;
|
e.state = EntryState::Loading;
|
||||||
DecodeRequest rq{};
|
DecodeRequest rq{};
|
||||||
rq.handle = static_cast<TextureHandle>(&e - _entries.data());
|
rq.handle = static_cast<TextureHandle>(&e - _entries.data());
|
||||||
@@ -335,17 +362,34 @@ void TextureCache::drain_ready_uploads(ResourceManager &rm)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uint32_t now = _context ? _context->frameIndex : 0u;
|
||||||
VkExtent3D extent{static_cast<uint32_t>(res.width), static_cast<uint32_t>(res.height), 1u};
|
VkExtent3D extent{static_cast<uint32_t>(res.width), static_cast<uint32_t>(res.height), 1u};
|
||||||
VkFormat fmt = res.srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM;
|
VkFormat fmt = res.srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM;
|
||||||
const void *src = nullptr;
|
|
||||||
if (res.heap)
|
// Estimate resident size for admission control (match post-upload computation)
|
||||||
|
const float mipFactor = res.mipmapped ? 1.3333333f : 1.0f;
|
||||||
|
const size_t expectedBytes = static_cast<size_t>(estimate_rgba8_bytes(extent.width, extent.height) * mipFactor);
|
||||||
|
|
||||||
|
if (_gpuBudgetBytes != std::numeric_limits<size_t>::max())
|
||||||
{
|
{
|
||||||
src = static_cast<const void *>(res.heap);
|
if (_residentBytes + expectedBytes > _gpuBudgetBytes)
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
src = static_cast<const void *>(res.rgba.data());
|
size_t need = (_residentBytes + expectedBytes) - _gpuBudgetBytes;
|
||||||
|
(void)try_make_space(need, now);
|
||||||
}
|
}
|
||||||
|
if (_residentBytes + expectedBytes > _gpuBudgetBytes)
|
||||||
|
{
|
||||||
|
// Not enough space even after eviction → back off; free decode heap
|
||||||
|
if (res.heap) { stbi_image_free(res.heap); res.heap = nullptr; }
|
||||||
|
e.state = EntryState::Evicted;
|
||||||
|
e.lastEvictedFrame = now;
|
||||||
|
e.nextAttemptFrame = std::max(e.nextAttemptFrame, now + _reloadCooldownFrames);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const void *src = res.heap ? static_cast<const void *>(res.heap)
|
||||||
|
: static_cast<const void *>(res.rgba.data());
|
||||||
e.image = rm.create_image(src, extent, fmt, VK_IMAGE_USAGE_SAMPLED_BIT, res.mipmapped);
|
e.image = rm.create_image(src, extent, fmt, VK_IMAGE_USAGE_SAMPLED_BIT, res.mipmapped);
|
||||||
|
|
||||||
if (vmaDebugEnabled())
|
if (vmaDebugEnabled())
|
||||||
@@ -354,10 +398,10 @@ void TextureCache::drain_ready_uploads(ResourceManager &rm)
|
|||||||
vmaSetAllocationName(_context->getDevice()->allocator(), e.image.allocation, name.c_str());
|
vmaSetAllocationName(_context->getDevice()->allocator(), e.image.allocation, name.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
const float mipFactor = res.mipmapped ? 1.3333333f : 1.0f;
|
e.sizeBytes = expectedBytes;
|
||||||
e.sizeBytes = static_cast<size_t>(estimate_rgba8_bytes(extent.width, extent.height) * mipFactor);
|
|
||||||
_residentBytes += e.sizeBytes;
|
_residentBytes += e.sizeBytes;
|
||||||
e.state = EntryState::Resident;
|
e.state = EntryState::Resident;
|
||||||
|
e.nextAttemptFrame = 0; // clear backoff after success
|
||||||
|
|
||||||
// Drop source bytes if policy says so (only for Bytes-backed keys).
|
// Drop source bytes if policy says so (only for Bytes-backed keys).
|
||||||
if (!_keepSourceBytes && e.key.kind == TextureKey::SourceKind::Bytes)
|
if (!_keepSourceBytes && e.key.kind == TextureKey::SourceKind::Bytes)
|
||||||
@@ -411,6 +455,43 @@ void TextureCache::evictCpuToBudget()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TextureCache::try_make_space(size_t bytesNeeded, uint32_t now)
|
||||||
|
{
|
||||||
|
if (bytesNeeded == 0) return true;
|
||||||
|
if (_residentBytes == 0) return false;
|
||||||
|
|
||||||
|
// Collect candidates that were not used this frame, oldest first
|
||||||
|
std::vector<std::pair<TextureHandle, uint32_t>> order;
|
||||||
|
order.reserve(_entries.size());
|
||||||
|
for (TextureHandle h = 0; h < _entries.size(); ++h)
|
||||||
|
{
|
||||||
|
const auto &e = _entries[h];
|
||||||
|
if (e.state == EntryState::Resident && e.lastUsedFrame != now)
|
||||||
|
{
|
||||||
|
order.emplace_back(h, e.lastUsedFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::sort(order.begin(), order.end(), [](auto &a, auto &b) { return a.second < b.second; });
|
||||||
|
|
||||||
|
size_t freed = 0;
|
||||||
|
for (auto &pair : order)
|
||||||
|
{
|
||||||
|
if (freed >= bytesNeeded) break;
|
||||||
|
Entry &e = _entries[pair.first];
|
||||||
|
if (e.state != EntryState::Resident) continue;
|
||||||
|
|
||||||
|
patch_to_fallback(e);
|
||||||
|
_context->getResources()->destroy_image(e.image);
|
||||||
|
e.image = {};
|
||||||
|
e.state = EntryState::Evicted;
|
||||||
|
e.lastEvictedFrame = now;
|
||||||
|
e.nextAttemptFrame = std::max(e.nextAttemptFrame, now + _reloadCooldownFrames);
|
||||||
|
if (_residentBytes >= e.sizeBytes) _residentBytes -= e.sizeBytes; else _residentBytes = 0;
|
||||||
|
freed += e.sizeBytes;
|
||||||
|
}
|
||||||
|
return freed >= bytesNeeded;
|
||||||
|
}
|
||||||
|
|
||||||
void TextureCache::debug_snapshot(std::vector<DebugRow> &outRows, DebugStats &outStats) const
|
void TextureCache::debug_snapshot(std::vector<DebugRow> &outRows, DebugStats &outStats) const
|
||||||
{
|
{
|
||||||
outRows.clear();
|
outRows.clear();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
class EngineContext;
|
class EngineContext;
|
||||||
class ResourceManager;
|
class ResourceManager;
|
||||||
@@ -48,6 +49,10 @@ public:
|
|||||||
void watchBinding(TextureHandle handle, VkDescriptorSet set, uint32_t binding,
|
void watchBinding(TextureHandle handle, VkDescriptorSet set, uint32_t binding,
|
||||||
VkSampler sampler, VkImageView fallbackView);
|
VkSampler sampler, VkImageView fallbackView);
|
||||||
|
|
||||||
|
// Remove all watches for a descriptor set (call before destroying the
|
||||||
|
// pool that owns the set). Prevents attempts to patch dead sets.
|
||||||
|
void unwatchSet(VkDescriptorSet set);
|
||||||
|
|
||||||
// Mark a texture as used this frame (for LRU).
|
// Mark a texture as used this frame (for LRU).
|
||||||
void markUsed(TextureHandle handle, uint32_t frameIndex);
|
void markUsed(TextureHandle handle, uint32_t frameIndex);
|
||||||
// Convenience: mark all handles watched by a descriptor set.
|
// Convenience: mark all handles watched by a descriptor set.
|
||||||
@@ -96,6 +101,11 @@ public:
|
|||||||
void set_cpu_source_budget(size_t bytes) { _cpuSourceBudget = bytes; }
|
void set_cpu_source_budget(size_t bytes) { _cpuSourceBudget = bytes; }
|
||||||
size_t cpu_source_budget() const { return _cpuSourceBudget; }
|
size_t cpu_source_budget() const { return _cpuSourceBudget; }
|
||||||
|
|
||||||
|
// Optional GPU residency budget, used to avoid immediate thrashing when
|
||||||
|
// accepting new uploads. The engine should refresh this each frame.
|
||||||
|
void set_gpu_budget_bytes(size_t bytes) { _gpuBudgetBytes = bytes; }
|
||||||
|
size_t gpu_budget_bytes() const { return _gpuBudgetBytes; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Patch
|
struct Patch
|
||||||
{
|
{
|
||||||
@@ -115,6 +125,8 @@ private:
|
|||||||
AllocatedImage image{}; // valid when Resident
|
AllocatedImage image{}; // valid when Resident
|
||||||
size_t sizeBytes{0}; // approximate VRAM cost
|
size_t sizeBytes{0}; // approximate VRAM cost
|
||||||
uint32_t lastUsedFrame{0};
|
uint32_t lastUsedFrame{0};
|
||||||
|
uint32_t lastEvictedFrame{0};
|
||||||
|
uint32_t nextAttemptFrame{0}; // gate reload attempts to reduce churn
|
||||||
std::vector<Patch> patches; // descriptor patches to rewrite
|
std::vector<Patch> patches; // descriptor patches to rewrite
|
||||||
|
|
||||||
// Source payload for deferred load
|
// Source payload for deferred load
|
||||||
@@ -133,6 +145,8 @@ private:
|
|||||||
int _maxLoadsPerPump{4};
|
int _maxLoadsPerPump{4};
|
||||||
bool _keepSourceBytes{false};
|
bool _keepSourceBytes{false};
|
||||||
size_t _cpuSourceBudget{64ull * 1024ull * 1024ull}; // 64 MiB default
|
size_t _cpuSourceBudget{64ull * 1024ull * 1024ull}; // 64 MiB default
|
||||||
|
size_t _gpuBudgetBytes{std::numeric_limits<size_t>::max()}; // unlimited unless set
|
||||||
|
uint32_t _reloadCooldownFrames{2};
|
||||||
|
|
||||||
void start_load(Entry &e, ResourceManager &rm);
|
void start_load(Entry &e, ResourceManager &rm);
|
||||||
void patch_ready_entry(const Entry &e);
|
void patch_ready_entry(const Entry &e);
|
||||||
@@ -167,6 +181,11 @@ private:
|
|||||||
void drop_source_bytes(Entry &e);
|
void drop_source_bytes(Entry &e);
|
||||||
void evictCpuToBudget();
|
void evictCpuToBudget();
|
||||||
|
|
||||||
|
// Try to free at least 'bytesNeeded' by evicting least-recently-used
|
||||||
|
// Resident entries that were not used in the current frame. Returns true
|
||||||
|
// if enough space was reclaimed. Does not evict textures used this frame.
|
||||||
|
bool try_make_space(size_t bytesNeeded, uint32_t now);
|
||||||
|
|
||||||
std::vector<std::thread> _decodeThreads;
|
std::vector<std::thread> _decodeThreads;
|
||||||
std::mutex _qMutex;
|
std::mutex _qMutex;
|
||||||
std::condition_variable _qCV;
|
std::condition_variable _qCV;
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const size_t texBudget = query_texture_budget_bytes(dev);
|
const size_t texBudget = query_texture_budget_bytes(dev);
|
||||||
|
eng->_textureCache->set_gpu_budget_bytes(texBudget);
|
||||||
const size_t resBytes = eng->_textureCache->resident_bytes();
|
const size_t resBytes = eng->_textureCache->resident_bytes();
|
||||||
const size_t cpuSrcBytes = eng->_textureCache->cpu_source_bytes();
|
const size_t cpuSrcBytes = eng->_textureCache->cpu_source_bytes();
|
||||||
ImGui::Text("Device local: %.1f / %.1f MiB", (double)devLocalUsage/1048576.0, (double)devLocalBudget/1048576.0);
|
ImGui::Text("Device local: %.1f / %.1f MiB", (double)devLocalUsage/1048576.0, (double)devLocalBudget/1048576.0);
|
||||||
@@ -560,7 +561,7 @@ void VulkanEngine::init()
|
|||||||
// Conservative defaults to avoid CPU spikes during heavy glTF loads.
|
// Conservative defaults to avoid CPU spikes during heavy glTF loads.
|
||||||
_textureCache->set_max_loads_per_pump(3);
|
_textureCache->set_max_loads_per_pump(3);
|
||||||
_textureCache->set_keep_source_bytes(false);
|
_textureCache->set_keep_source_bytes(false);
|
||||||
_textureCache->set_cpu_source_budget(32ull * 1024ull * 1024ull); // 32 MiB
|
_textureCache->set_cpu_source_budget(64ull * 1024ull * 1024ull); // 32 MiB
|
||||||
|
|
||||||
// Optional ray tracing manager if supported and extensions enabled
|
// Optional ray tracing manager if supported and extensions enabled
|
||||||
if (_deviceManager->supportsRayQuery() && _deviceManager->supportsAccelerationStructure())
|
if (_deviceManager->supportsRayQuery() && _deviceManager->supportsAccelerationStructure())
|
||||||
@@ -598,7 +599,7 @@ void VulkanEngine::init()
|
|||||||
auto imguiPass = std::make_unique<ImGuiPass>();
|
auto imguiPass = std::make_unique<ImGuiPass>();
|
||||||
_renderPassManager->setImGuiPass(std::move(imguiPass));
|
_renderPassManager->setImGuiPass(std::move(imguiPass));
|
||||||
|
|
||||||
const std::string structurePath = _assetManager->modelPath("Untitled.glb");
|
const std::string structurePath = _assetManager->modelPath("seoul_high/scene.gltf");
|
||||||
const auto structureFile = _assetManager->loadGLTF(structurePath);
|
const auto structureFile = _assetManager->loadGLTF(structurePath);
|
||||||
|
|
||||||
assert(structureFile.has_value());
|
assert(structureFile.has_value());
|
||||||
@@ -864,6 +865,7 @@ void VulkanEngine::draw()
|
|||||||
if (_textureCache)
|
if (_textureCache)
|
||||||
{
|
{
|
||||||
size_t budget = query_texture_budget_bytes(_deviceManager.get());
|
size_t budget = query_texture_budget_bytes(_deviceManager.get());
|
||||||
|
_textureCache->set_gpu_budget_bytes(budget);
|
||||||
_textureCache->evictToBudget(budget);
|
_textureCache->evictToBudget(budget);
|
||||||
_textureCache->pumpLoads(*_resourceManager, get_current_frame());
|
_textureCache->pumpLoads(*_resourceManager, get_current_frame());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -704,6 +704,20 @@ void LoadedGLTF::clearAll()
|
|||||||
{
|
{
|
||||||
VkDevice dv = creator->_deviceManager->device();
|
VkDevice dv = creator->_deviceManager->device();
|
||||||
|
|
||||||
|
// Before destroying descriptor pools, unregister descriptor-set watches so
|
||||||
|
// the TextureCache will not attempt to patch dead sets.
|
||||||
|
if (creator && creator->_context && creator->_context->textures)
|
||||||
|
{
|
||||||
|
TextureCache *cache = creator->_context->textures;
|
||||||
|
for (auto &[k, mat] : materials)
|
||||||
|
{
|
||||||
|
if (mat && mat->data.materialSet != VK_NULL_HANDLE)
|
||||||
|
{
|
||||||
|
cache->unwatchSet(mat->data.materialSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (auto &[k, v]: meshes)
|
for (auto &[k, v]: meshes)
|
||||||
{
|
{
|
||||||
if (creator->_rayManager)
|
if (creator->_rayManager)
|
||||||
|
|||||||
Reference in New Issue
Block a user