ADD: Texture Cache

This commit is contained in:
2025-11-03 15:59:25 +09:00
parent cb495e3f4b
commit c64db86f82
2 changed files with 357 additions and 0 deletions

243
src/core/texture_cache.cpp Normal file
View File

@@ -0,0 +1,243 @@
#include "texture_cache.h"
#include <core/engine_context.h>
#include <core/vk_resource.h>
#include <core/vk_descriptors.h>
#include <core/config.h>
#include <algorithm>
#include "stb_image.h"
#include <algorithm>
#include "vk_device.h"
void TextureCache::init(EngineContext *ctx)
{
_context = ctx;
}
void TextureCache::cleanup()
{
if (!_context || !_context->getResources()) return;
auto *rm = _context->getResources();
for (auto &e : _entries)
{
if (e.state == EntryState::Resident && e.image.image)
{
rm->destroy_image(e.image);
e.image = {};
}
e.state = EntryState::Evicted;
}
_residentBytes = 0;
_lookup.clear();
_setToHandles.clear();
}
TextureCache::TextureHandle TextureCache::request(const TextureKey &key, VkSampler sampler)
{
auto it = _lookup.find(key.hash);
if (it != _lookup.end())
{
TextureHandle h = it->second;
// Keep most recent sampler for future patches if provided
if (h < _entries.size() && sampler != VK_NULL_HANDLE)
{
_entries[h].sampler = sampler;
}
return h;
}
TextureHandle h = static_cast<TextureHandle>(_entries.size());
_lookup.emplace(key.hash, h);
Entry e{};
e.key = key;
e.sampler = sampler;
e.state = EntryState::Unloaded;
if (key.kind == TextureKey::SourceKind::FilePath)
{
e.path = key.path;
}
else
{
e.bytes = key.bytes;
}
_entries.push_back(std::move(e));
return h;
}
void TextureCache::watchBinding(TextureHandle handle, VkDescriptorSet set, uint32_t binding,
VkSampler sampler, VkImageView fallbackView)
{
if (handle == InvalidHandle) return;
if (handle >= _entries.size()) return;
Entry &e = _entries[handle];
// Track patch
Patch p{};
p.set = set;
p.binding = binding;
p.sampler = sampler ? sampler : e.sampler;
p.fallbackView = fallbackView;
e.patches.push_back(p);
// Back-reference for fast per-set markUsed
_setToHandles[set].push_back(handle);
}
void TextureCache::markUsed(TextureHandle handle, uint32_t frameIndex)
{
if (handle == InvalidHandle) return;
if (handle >= _entries.size()) return;
_entries[handle].lastUsedFrame = frameIndex;
}
void TextureCache::markSetUsed(VkDescriptorSet set, uint32_t frameIndex)
{
auto it = _setToHandles.find(set);
if (it == _setToHandles.end()) return;
for (TextureHandle h : it->second)
{
if (h < _entries.size())
{
_entries[h].lastUsedFrame = frameIndex;
}
}
}
static inline size_t estimate_rgba8_bytes(uint32_t w, uint32_t h)
{
return static_cast<size_t>(w) * static_cast<size_t>(h) * 4u;
}
void TextureCache::start_load(Entry &e, ResourceManager &rm)
{
if (e.state == EntryState::Resident || e.state == EntryState::Loading) return;
int width = 0, height = 0, comp = 0;
unsigned char *data = nullptr;
if (e.key.kind == TextureKey::SourceKind::FilePath)
{
data = stbi_load(e.path.c_str(), &width, &height, &comp, 4);
}
else
{
if (!e.bytes.empty())
{
data = stbi_load_from_memory(e.bytes.data(), static_cast<int>(e.bytes.size()), &width, &height, &comp, 4);
}
}
if (!data || width <= 0 || height <= 0)
{
// Failed decode; keep fallbacks bound. Mark as evicted/unloaded.
if (data) stbi_image_free(data);
e.state = EntryState::Evicted;
return;
}
VkExtent3D extent{static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1u};
VkFormat fmt = e.key.srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM;
// Queue upload via ResourceManager (deferred pass if enabled)
e.image = rm.create_image(static_cast<void *>(data), extent, fmt, VK_IMAGE_USAGE_SAMPLED_BIT, e.key.mipmapped);
// Name VMA allocation for diagnostics
if (vmaDebugEnabled())
{
std::string name = e.key.kind == TextureKey::SourceKind::FilePath ? e.path : std::string("tex.bytes");
vmaSetAllocationName(_context->getDevice()->allocator(), e.image.allocation, name.c_str());
}
const float mipFactor = e.key.mipmapped ? 1.3333333f : 1.0f; // approx sum of 1/4^i
e.sizeBytes = static_cast<size_t>(estimate_rgba8_bytes(extent.width, extent.height) * mipFactor);
_residentBytes += e.sizeBytes;
e.state = EntryState::Resident;
stbi_image_free(data);
// Patch all watched descriptors to the new image
patch_ready_entry(e);
}
void TextureCache::patch_ready_entry(const Entry &e)
{
if (!_context || !_context->getDevice()) return;
if (e.state != EntryState::Resident) return;
DescriptorWriter writer;
for (const Patch &p : e.patches)
{
if (p.set == VK_NULL_HANDLE) continue;
writer.clear();
writer.write_image(static_cast<int>(p.binding), e.image.imageView,
p.sampler ? p.sampler : e.sampler,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
writer.update_set(_context->getDevice()->device(), p.set);
}
}
void TextureCache::patch_to_fallback(const Entry &e)
{
if (!_context || !_context->getDevice()) return;
DescriptorWriter writer;
for (const Patch &p : e.patches)
{
if (p.set == VK_NULL_HANDLE || p.fallbackView == VK_NULL_HANDLE) continue;
writer.clear();
writer.write_image(static_cast<int>(p.binding), p.fallbackView,
p.sampler ? p.sampler : e.sampler,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
writer.update_set(_context->getDevice()->device(), p.set);
}
}
void TextureCache::pumpLoads(ResourceManager &rm, FrameResources &)
{
// Simple throttle to avoid massive spikes.
const int kMaxLoadsPerPump = 4;
int started = 0;
for (auto &e : _entries)
{
if (e.state == EntryState::Unloaded)
{
start_load(e, rm);
if (++started >= kMaxLoadsPerPump) break;
}
}
}
void TextureCache::evictToBudget(size_t budgetBytes)
{
if (_residentBytes <= budgetBytes) return;
// Gather candidates
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)
{
order.emplace_back(h, e.lastUsedFrame);
}
}
std::sort(order.begin(), order.end(), [](auto &a, auto &b) { return a.second < b.second; });
for (auto &pair : order)
{
if (_residentBytes <= budgetBytes) break;
TextureHandle h = pair.first;
Entry &e = _entries[h];
if (e.state != EntryState::Resident) continue;
// Rewrite watchers back to fallback before destroying
patch_to_fallback(e);
_context->getResources()->destroy_image(e.image);
e.image = {};
e.state = EntryState::Evicted;
if (_residentBytes >= e.sizeBytes) _residentBytes -= e.sizeBytes; else _residentBytes = 0;
}
}

114
src/core/texture_cache.h Normal file
View File

@@ -0,0 +1,114 @@
#pragma once
#include <core/vk_types.h>
#include <cstdint>
#include <string>
#include <vector>
#include <unordered_map>
class EngineContext;
class ResourceManager;
struct FrameResources;
// Lightweight texture streaming cache.
// - Requests are deduplicated by a hashable TextureKey.
// - Loads happen via ResourceManager (deferred uploads supported).
// - Descriptors registered via watchBinding() are patched in-place
// when the image becomes Resident, leveraging UPDATE_AFTER_BIND.
// - evictToBudget() rewrites watchers to provided fallbacks.
class TextureCache
{
public:
struct TextureKey
{
enum class SourceKind : uint8_t { FilePath, Bytes };
SourceKind kind{SourceKind::FilePath};
std::string path; // used when kind==FilePath
std::vector<uint8_t> bytes; // used when kind==Bytes
bool srgb{false}; // desired sampling format
bool mipmapped{true}; // generate full mip chain
uint64_t hash{0}; // stable dedup key
};
using TextureHandle = uint32_t;
static constexpr TextureHandle InvalidHandle = 0xFFFFFFFFu;
void init(EngineContext *ctx);
void cleanup();
// Deduplicated request; returns a stable handle.
TextureHandle request(const TextureKey &key, VkSampler sampler);
// Register a descriptor binding to patch when the texture is ready.
void watchBinding(TextureHandle handle, VkDescriptorSet set, uint32_t binding,
VkSampler sampler, VkImageView fallbackView);
// Mark a texture as used this frame (for LRU).
void markUsed(TextureHandle handle, uint32_t frameIndex);
// Convenience: mark all handles watched by a descriptor set.
void markSetUsed(VkDescriptorSet set, uint32_t frameIndex);
// Schedule pending loads and patch descriptors for newly created images.
void pumpLoads(ResourceManager &rm, FrameResources &frame);
// Evict least-recently-used entries to fit within a budget in bytes.
void evictToBudget(size_t budgetBytes);
private:
struct Patch
{
VkDescriptorSet set{VK_NULL_HANDLE};
uint32_t binding{0};
VkSampler sampler{VK_NULL_HANDLE};
VkImageView fallbackView{VK_NULL_HANDLE};
};
enum class EntryState : uint8_t { Unloaded, Loading, Resident, Evicted };
struct Entry
{
TextureKey key{};
VkSampler sampler{VK_NULL_HANDLE};
EntryState state{EntryState::Unloaded};
AllocatedImage image{}; // valid when Resident
size_t sizeBytes{0}; // approximate VRAM cost
uint32_t lastUsedFrame{0};
std::vector<Patch> patches; // descriptor patches to rewrite
// Source payload for deferred load
std::string path; // for FilePath
std::vector<uint8_t> bytes; // for Bytes
};
EngineContext *_context{nullptr};
std::vector<Entry> _entries;
std::unordered_map<uint64_t, TextureHandle> _lookup; // key.hash -> handle
std::unordered_map<VkDescriptorSet, std::vector<TextureHandle>> _setToHandles;
size_t _residentBytes{0};
void start_load(Entry &e, ResourceManager &rm);
void patch_ready_entry(const Entry &e);
void patch_to_fallback(const Entry &e);
};
// Helpers to build/digest keys
namespace texcache
{
// 64-bit FNV-1a
inline uint64_t fnv1a64(std::string_view s)
{
const uint64_t FNV_OFFSET = 1469598103934665603ull;
const uint64_t FNV_PRIME = 1099511628211ull;
uint64_t h = FNV_OFFSET;
for (unsigned char c : s) { h ^= c; h *= FNV_PRIME; }
return h;
}
inline uint64_t fnv1a64(const uint8_t *data, size_t n)
{
const uint64_t FNV_OFFSET = 1469598103934665603ull;
const uint64_t FNV_PRIME = 1099511628211ull;
uint64_t h = FNV_OFFSET;
for (size_t i = 0; i < n; ++i) { h ^= data[i]; h *= FNV_PRIME; }
return h;
}
}