ADD: Texture cache system improvement
This commit is contained in:
@@ -40,6 +40,23 @@ void TextureCache::cleanup()
|
|||||||
for (auto &t : _decodeThreads) if (t.joinable()) t.join();
|
for (auto &t : _decodeThreads) if (t.joinable()) t.join();
|
||||||
_decodeThreads.clear();
|
_decodeThreads.clear();
|
||||||
}
|
}
|
||||||
|
// Clear any pending decode/upload work (freeing decode heap pointers)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(_qMutex);
|
||||||
|
_queue.clear();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(_readyMutex);
|
||||||
|
for (auto &r : _ready)
|
||||||
|
{
|
||||||
|
if (r.heap)
|
||||||
|
{
|
||||||
|
stbi_image_free(r.heap);
|
||||||
|
r.heap = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ready.clear();
|
||||||
|
}
|
||||||
if (!_context || !_context->getResources()) return;
|
if (!_context || !_context->getResources()) return;
|
||||||
auto *rm = _context->getResources();
|
auto *rm = _context->getResources();
|
||||||
for (TextureHandle h = 0; h < _entries.size(); ++h)
|
for (TextureHandle h = 0; h < _entries.size(); ++h)
|
||||||
@@ -83,10 +100,21 @@ TextureCache::TextureHandle TextureCache::request(const TextureKey &key, VkSampl
|
|||||||
if (it != _lookup.end())
|
if (it != _lookup.end())
|
||||||
{
|
{
|
||||||
TextureHandle h = it->second;
|
TextureHandle h = it->second;
|
||||||
// Keep most recent sampler for future patches if provided
|
if (h < _entries.size())
|
||||||
if (h < _entries.size() && sampler != VK_NULL_HANDLE)
|
|
||||||
{
|
{
|
||||||
_entries[h].sampler = sampler;
|
Entry &e = _entries[h];
|
||||||
|
// Keep most recent sampler for future patches if provided
|
||||||
|
if (sampler != VK_NULL_HANDLE)
|
||||||
|
{
|
||||||
|
e.sampler = sampler;
|
||||||
|
}
|
||||||
|
// Allow re-supplying CPU source bytes for Bytes-backed textures after an unload.
|
||||||
|
if (normKey.kind == TextureKey::SourceKind::Bytes && !normKey.bytes.empty() &&
|
||||||
|
e.key.kind == TextureKey::SourceKind::Bytes && e.bytes.empty() && e.state != EntryState::Resident)
|
||||||
|
{
|
||||||
|
e.bytes = normKey.bytes;
|
||||||
|
_cpuSourceBytes += e.bytes.size();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
@@ -96,6 +124,10 @@ TextureCache::TextureHandle TextureCache::request(const TextureKey &key, VkSampl
|
|||||||
|
|
||||||
Entry e{};
|
Entry e{};
|
||||||
e.key = normKey;
|
e.key = normKey;
|
||||||
|
// Keep only metadata in the key to avoid duplicating potentially large payloads.
|
||||||
|
e.key.path.clear();
|
||||||
|
e.key.bytes.clear();
|
||||||
|
e.key.bytes.shrink_to_fit();
|
||||||
e.sampler = sampler;
|
e.sampler = sampler;
|
||||||
e.state = EntryState::Unloaded;
|
e.state = EntryState::Unloaded;
|
||||||
if (normKey.kind == TextureKey::SourceKind::FilePath)
|
if (normKey.kind == TextureKey::SourceKind::FilePath)
|
||||||
@@ -187,6 +219,27 @@ void TextureCache::markSetUsed(VkDescriptorSet set, uint32_t frameIndex)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TextureCache::pin(TextureHandle handle)
|
||||||
|
{
|
||||||
|
if (handle == InvalidHandle) return;
|
||||||
|
if (handle >= _entries.size()) return;
|
||||||
|
_entries[handle].pinned = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextureCache::unpin(TextureHandle handle)
|
||||||
|
{
|
||||||
|
if (handle == InvalidHandle) return;
|
||||||
|
if (handle >= _entries.size()) return;
|
||||||
|
_entries[handle].pinned = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextureCache::is_pinned(TextureHandle handle) const
|
||||||
|
{
|
||||||
|
if (handle == InvalidHandle) return false;
|
||||||
|
if (handle >= _entries.size()) return false;
|
||||||
|
return _entries[handle].pinned;
|
||||||
|
}
|
||||||
|
|
||||||
static inline size_t bytes_per_texel(VkFormat fmt)
|
static inline size_t bytes_per_texel(VkFormat fmt)
|
||||||
{
|
{
|
||||||
switch (fmt)
|
switch (fmt)
|
||||||
@@ -378,7 +431,7 @@ void TextureCache::evictToBudget(size_t budgetBytes)
|
|||||||
for (TextureHandle h = 0; h < _entries.size(); ++h)
|
for (TextureHandle h = 0; h < _entries.size(); ++h)
|
||||||
{
|
{
|
||||||
const auto &e = _entries[h];
|
const auto &e = _entries[h];
|
||||||
if (e.state == EntryState::Resident)
|
if (e.state == EntryState::Resident && !e.pinned)
|
||||||
{
|
{
|
||||||
order.emplace_back(h, e.lastUsedFrame);
|
order.emplace_back(h, e.lastUsedFrame);
|
||||||
}
|
}
|
||||||
@@ -392,6 +445,8 @@ void TextureCache::evictToBudget(size_t budgetBytes)
|
|||||||
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;
|
||||||
|
// Never evict pinned textures
|
||||||
|
if (e.pinned) continue;
|
||||||
// Prefer not to evict textures used this frame unless strictly necessary.
|
// Prefer not to evict textures used this frame unless strictly necessary.
|
||||||
if (e.lastUsedFrame == now) continue;
|
if (e.lastUsedFrame == now) continue;
|
||||||
|
|
||||||
@@ -412,12 +467,84 @@ void TextureCache::evictToBudget(size_t budgetBytes)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TextureCache::unload(TextureHandle handle, bool drop_source_bytes)
|
||||||
|
{
|
||||||
|
if (handle == InvalidHandle) return false;
|
||||||
|
if (handle >= _entries.size()) return false;
|
||||||
|
|
||||||
|
Entry &e = _entries[handle];
|
||||||
|
const uint32_t now = _context ? _context->frameIndex : 0u;
|
||||||
|
|
||||||
|
// Invalidate any in-flight decode results for this entry.
|
||||||
|
e.generation++;
|
||||||
|
|
||||||
|
// Drop queued decode requests for this handle.
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(_qMutex);
|
||||||
|
_queue.erase(std::remove_if(_queue.begin(), _queue.end(),
|
||||||
|
[&](const DecodeRequest &rq) { return rq.handle == handle; }),
|
||||||
|
_queue.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop already-decoded results waiting for upload for this handle.
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(_readyMutex);
|
||||||
|
for (auto it = _ready.begin(); it != _ready.end();)
|
||||||
|
{
|
||||||
|
if (it->handle == handle)
|
||||||
|
{
|
||||||
|
if (it->heap)
|
||||||
|
{
|
||||||
|
stbi_image_free(it->heap);
|
||||||
|
it->heap = nullptr;
|
||||||
|
}
|
||||||
|
it = _ready.erase(it);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If resident, patch watchers back to fallback and destroy the image.
|
||||||
|
if (e.state == EntryState::Resident && e.image.image != VK_NULL_HANDLE)
|
||||||
|
{
|
||||||
|
patch_to_fallback(e);
|
||||||
|
|
||||||
|
if (_context && _context->getResources())
|
||||||
|
{
|
||||||
|
fmt::println("[TextureCache] unload destroy handle={} path='{}' bytes={} residentBytesBefore={}",
|
||||||
|
handle,
|
||||||
|
e.path.empty() ? "<bytes>" : e.path,
|
||||||
|
e.sizeBytes,
|
||||||
|
_residentBytes);
|
||||||
|
_context->getResources()->destroy_image(e.image);
|
||||||
|
}
|
||||||
|
|
||||||
|
e.image = {};
|
||||||
|
if (_residentBytes >= e.sizeBytes) _residentBytes -= e.sizeBytes; else _residentBytes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.state = EntryState::Evicted;
|
||||||
|
e.lastEvictedFrame = now;
|
||||||
|
e.nextAttemptFrame = std::max(e.nextAttemptFrame, now + _reloadCooldownFrames);
|
||||||
|
|
||||||
|
if (drop_source_bytes)
|
||||||
|
{
|
||||||
|
this->drop_source_bytes(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void TextureCache::enqueue_decode(Entry &e)
|
void TextureCache::enqueue_decode(Entry &e)
|
||||||
{
|
{
|
||||||
if (e.state != EntryState::Unloaded && e.state != EntryState::Evicted) 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());
|
||||||
|
rq.generation = e.generation;
|
||||||
rq.key = e.key;
|
rq.key = e.key;
|
||||||
if (e.key.kind == TextureKey::SourceKind::FilePath) rq.path = e.path; else rq.bytes = e.bytes;
|
if (e.key.kind == TextureKey::SourceKind::FilePath) rq.path = e.path; else rq.bytes = e.bytes;
|
||||||
{
|
{
|
||||||
@@ -442,6 +569,7 @@ void TextureCache::worker_loop()
|
|||||||
|
|
||||||
DecodedResult out{};
|
DecodedResult out{};
|
||||||
out.handle = rq.handle;
|
out.handle = rq.handle;
|
||||||
|
out.generation = rq.generation;
|
||||||
out.mipmapped = rq.key.mipmapped;
|
out.mipmapped = rq.key.mipmapped;
|
||||||
out.srgb = rq.key.srgb;
|
out.srgb = rq.key.srgb;
|
||||||
out.channels = rq.key.channels;
|
out.channels = rq.key.channels;
|
||||||
@@ -618,15 +746,31 @@ size_t TextureCache::drain_ready_uploads(ResourceManager &rm, size_t budgetBytes
|
|||||||
size_t admitted = 0;
|
size_t admitted = 0;
|
||||||
for (auto &res : local)
|
for (auto &res : local)
|
||||||
{
|
{
|
||||||
if (res.handle == InvalidHandle || res.handle >= _entries.size()) continue;
|
const uint32_t now = _context ? _context->frameIndex : 0u;
|
||||||
Entry &e = _entries[res.handle];
|
if (res.handle == InvalidHandle || res.handle >= _entries.size())
|
||||||
if (!res.isKTX2 && ((res.heap == nullptr && res.rgba.empty()) || res.width <= 0 || res.height <= 0))
|
|
||||||
{
|
{
|
||||||
e.state = EntryState::Evicted; // failed decode; keep fallback
|
if (res.heap) { stbi_image_free(res.heap); res.heap = nullptr; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entry &e = _entries[res.handle];
|
||||||
|
|
||||||
|
// Drop stale results from cancelled/unloaded requests.
|
||||||
|
if (res.generation != e.generation || e.state != EntryState::Loading)
|
||||||
|
{
|
||||||
|
if (res.heap) { stbi_image_free(res.heap); res.heap = nullptr; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.isKTX2 && ((res.heap == nullptr && res.rgba.empty()) || res.width <= 0 || res.height <= 0))
|
||||||
|
{
|
||||||
|
if (res.heap) { stbi_image_free(res.heap); res.heap = nullptr; }
|
||||||
|
e.state = EntryState::Evicted; // failed decode; keep fallback
|
||||||
|
e.lastEvictedFrame = now;
|
||||||
|
e.nextAttemptFrame = std::max(e.nextAttemptFrame, now + _reloadCooldownFrames);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint32_t now = _context ? _context->frameIndex : 0u;
|
|
||||||
VkExtent3D extent{static_cast<uint32_t>(std::max(0, res.width)), static_cast<uint32_t>(std::max(0, res.height)), 1u};
|
VkExtent3D extent{static_cast<uint32_t>(std::max(0, res.width)), static_cast<uint32_t>(std::max(0, res.height)), 1u};
|
||||||
TextureKey::ChannelsHint hint = (e.key.channels == TextureKey::ChannelsHint::Auto)
|
TextureKey::ChannelsHint hint = (e.key.channels == TextureKey::ChannelsHint::Auto)
|
||||||
? TextureKey::ChannelsHint::Auto
|
? TextureKey::ChannelsHint::Auto
|
||||||
@@ -869,7 +1013,7 @@ bool TextureCache::try_make_space(size_t bytesNeeded, uint32_t now)
|
|||||||
for (TextureHandle h = 0; h < _entries.size(); ++h)
|
for (TextureHandle h = 0; h < _entries.size(); ++h)
|
||||||
{
|
{
|
||||||
const auto &e = _entries[h];
|
const auto &e = _entries[h];
|
||||||
if (e.state == EntryState::Resident && e.lastUsedFrame != now)
|
if (e.state == EntryState::Resident && e.lastUsedFrame != now && !e.pinned)
|
||||||
{
|
{
|
||||||
order.emplace_back(h, e.lastUsedFrame);
|
order.emplace_back(h, e.lastUsedFrame);
|
||||||
}
|
}
|
||||||
@@ -883,6 +1027,8 @@ bool TextureCache::try_make_space(size_t bytesNeeded, uint32_t now)
|
|||||||
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;
|
||||||
|
// Never evict pinned textures
|
||||||
|
if (e.pinned) continue;
|
||||||
|
|
||||||
patch_to_fallback(e);
|
patch_to_fallback(e);
|
||||||
fmt::println("[TextureCache] try_make_space destroy handle={} path='{}' bytes={} residentBytesBefore={}",
|
fmt::println("[TextureCache] try_make_space destroy handle={} path='{}' bytes={} residentBytesBefore={}",
|
||||||
@@ -948,3 +1094,13 @@ TextureCache::EntryState TextureCache::state(TextureHandle handle) const
|
|||||||
if (handle >= _entries.size()) return EntryState::Unloaded;
|
if (handle >= _entries.size()) return EntryState::Unloaded;
|
||||||
return _entries[handle].state;
|
return _entries[handle].state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VkImageView TextureCache::image_view(TextureHandle handle) const
|
||||||
|
{
|
||||||
|
if (handle == InvalidHandle) return VK_NULL_HANDLE;
|
||||||
|
if (handle >= _entries.size()) return VK_NULL_HANDLE;
|
||||||
|
|
||||||
|
const Entry &e = _entries[handle];
|
||||||
|
if (e.state != EntryState::Resident) return VK_NULL_HANDLE;
|
||||||
|
return e.image.imageView;
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,12 +63,26 @@ public:
|
|||||||
// Convenience: mark all handles watched by a descriptor set.
|
// Convenience: mark all handles watched by a descriptor set.
|
||||||
void markSetUsed(VkDescriptorSet set, uint32_t frameIndex);
|
void markSetUsed(VkDescriptorSet set, uint32_t frameIndex);
|
||||||
|
|
||||||
|
// Pin a texture to prevent eviction (useful for UI elements, critical assets).
|
||||||
|
// Pinned textures are never evicted by LRU or budget constraints.
|
||||||
|
void pin(TextureHandle handle);
|
||||||
|
// Unpin a texture, allowing it to be evicted normally.
|
||||||
|
void unpin(TextureHandle handle);
|
||||||
|
// Check if a texture is currently pinned.
|
||||||
|
bool is_pinned(TextureHandle handle) const;
|
||||||
|
|
||||||
// Schedule pending loads and patch descriptors for newly created images.
|
// Schedule pending loads and patch descriptors for newly created images.
|
||||||
void pumpLoads(ResourceManager &rm, FrameResources &frame);
|
void pumpLoads(ResourceManager &rm, FrameResources &frame);
|
||||||
|
|
||||||
// Evict least-recently-used entries to fit within a budget in bytes.
|
// Evict least-recently-used entries to fit within a budget in bytes.
|
||||||
void evictToBudget(size_t budgetBytes);
|
void evictToBudget(size_t budgetBytes);
|
||||||
|
|
||||||
|
// Manually unload a texture. This immediately frees GPU memory (if resident),
|
||||||
|
// patches watched descriptor bindings back to their fallbacks, and cancels any
|
||||||
|
// in-flight decode/upload work for the handle. The handle remains valid and
|
||||||
|
// can be re-requested/reloaded later.
|
||||||
|
bool unload(TextureHandle handle, bool drop_source_bytes = true);
|
||||||
|
|
||||||
// Debug snapshot for UI
|
// Debug snapshot for UI
|
||||||
struct DebugRow
|
struct DebugRow
|
||||||
{
|
{
|
||||||
@@ -87,6 +101,8 @@ public:
|
|||||||
void debug_snapshot(std::vector<DebugRow>& outRows, DebugStats& outStats) const;
|
void debug_snapshot(std::vector<DebugRow>& outRows, DebugStats& outStats) const;
|
||||||
// Read-only per-handle state query (main-thread only).
|
// Read-only per-handle state query (main-thread only).
|
||||||
EntryState state(TextureHandle handle) const;
|
EntryState state(TextureHandle handle) const;
|
||||||
|
// Returns the default image view for a Resident texture, otherwise VK_NULL_HANDLE.
|
||||||
|
VkImageView image_view(TextureHandle handle) const;
|
||||||
size_t resident_bytes() const { return _residentBytes; }
|
size_t resident_bytes() const { return _residentBytes; }
|
||||||
// CPU-side source bytes currently retained (compressed image payloads kept
|
// CPU-side source bytes currently retained (compressed image payloads kept
|
||||||
// for potential re-decode). Only applies to entries created with Bytes keys.
|
// for potential re-decode). Only applies to entries created with Bytes keys.
|
||||||
@@ -135,6 +151,8 @@ private:
|
|||||||
TextureKey key{};
|
TextureKey key{};
|
||||||
VkSampler sampler{VK_NULL_HANDLE};
|
VkSampler sampler{VK_NULL_HANDLE};
|
||||||
EntryState state{EntryState::Unloaded};
|
EntryState state{EntryState::Unloaded};
|
||||||
|
uint32_t generation{1}; // bumps to invalidate in-flight decode results
|
||||||
|
bool pinned{false}; // if true, never evict (for UI, critical assets)
|
||||||
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};
|
||||||
@@ -171,6 +189,7 @@ private:
|
|||||||
struct DecodeRequest
|
struct DecodeRequest
|
||||||
{
|
{
|
||||||
TextureHandle handle{InvalidHandle};
|
TextureHandle handle{InvalidHandle};
|
||||||
|
uint32_t generation{0};
|
||||||
TextureKey key{};
|
TextureKey key{};
|
||||||
std::string path;
|
std::string path;
|
||||||
std::vector<uint8_t> bytes;
|
std::vector<uint8_t> bytes;
|
||||||
@@ -178,6 +197,7 @@ private:
|
|||||||
struct DecodedResult
|
struct DecodedResult
|
||||||
{
|
{
|
||||||
TextureHandle handle{InvalidHandle};
|
TextureHandle handle{InvalidHandle};
|
||||||
|
uint32_t generation{0};
|
||||||
int width{0};
|
int width{0};
|
||||||
int height{0};
|
int height{0};
|
||||||
// Prefer heap pointer from stb to avoid an extra memcpy into a vector.
|
// Prefer heap pointer from stb to avoid an extra memcpy into a vector.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "context.h"
|
#include "context.h"
|
||||||
#include "core/assets/texture_cache.h"
|
#include "core/assets/texture_cache.h"
|
||||||
#include "core/assets/ibl_manager.h"
|
#include "core/assets/ibl_manager.h"
|
||||||
|
#include "core/assets/manager.h"
|
||||||
#include "core/pipeline/manager.h"
|
#include "core/pipeline/manager.h"
|
||||||
#include "core/debug_draw/debug_draw.h"
|
#include "core/debug_draw/debug_draw.h"
|
||||||
#include "render/passes/tonemap.h"
|
#include "render/passes/tonemap.h"
|
||||||
@@ -15,6 +16,8 @@
|
|||||||
|
|
||||||
#include <glm/gtx/matrix_decompose.hpp>
|
#include <glm/gtx/matrix_decompose.hpp>
|
||||||
#include <glm/gtx/quaternion.hpp>
|
#include <glm/gtx/quaternion.hpp>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
namespace GameAPI
|
namespace GameAPI
|
||||||
{
|
{
|
||||||
@@ -151,6 +154,184 @@ void Engine::evict_textures_to_budget()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Texture Loading
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TextureHandle Engine::load_texture(const std::string& path, const TextureLoadParams& params)
|
||||||
|
{
|
||||||
|
if (!_engine || !_engine->_textureCache || path.empty())
|
||||||
|
{
|
||||||
|
return InvalidTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve path relative to assets/textures/ if not absolute
|
||||||
|
std::string resolvedPath = path;
|
||||||
|
std::filesystem::path p(path);
|
||||||
|
if (p.is_relative())
|
||||||
|
{
|
||||||
|
resolvedPath = _engine->_assetManager->assetPath(std::string("textures/") + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build TextureKey
|
||||||
|
TextureCache::TextureKey key{};
|
||||||
|
key.kind = TextureCache::TextureKey::SourceKind::FilePath;
|
||||||
|
key.path = resolvedPath;
|
||||||
|
key.srgb = params.srgb;
|
||||||
|
key.mipmapped = params.mipmapped;
|
||||||
|
key.mipClampLevels = params.mipLevels;
|
||||||
|
|
||||||
|
// Map channel hint
|
||||||
|
switch (params.channels)
|
||||||
|
{
|
||||||
|
case TextureChannels::R:
|
||||||
|
key.channels = TextureCache::TextureKey::ChannelsHint::R;
|
||||||
|
break;
|
||||||
|
case TextureChannels::RG:
|
||||||
|
key.channels = TextureCache::TextureKey::ChannelsHint::RG;
|
||||||
|
break;
|
||||||
|
case TextureChannels::RGBA:
|
||||||
|
key.channels = TextureCache::TextureKey::ChannelsHint::RGBA;
|
||||||
|
break;
|
||||||
|
case TextureChannels::Auto:
|
||||||
|
default:
|
||||||
|
key.channels = TextureCache::TextureKey::ChannelsHint::Auto;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate hash for deduplication
|
||||||
|
std::string id = std::string("PATH:") + key.path + (key.srgb ? "#sRGB" : "#UNORM");
|
||||||
|
key.hash = texcache::fnv1a64(id);
|
||||||
|
|
||||||
|
// Use default linear sampler
|
||||||
|
VkSampler sampler = VK_NULL_HANDLE;
|
||||||
|
if (_engine->_context && _engine->_context->samplers)
|
||||||
|
{
|
||||||
|
sampler = _engine->_context->samplers->defaultLinear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request texture from cache
|
||||||
|
TextureCache::TextureHandle handle = _engine->_textureCache->request(key, sampler);
|
||||||
|
return static_cast<TextureHandle>(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextureHandle Engine::load_texture_from_memory(const std::vector<uint8_t>& data,
|
||||||
|
const TextureLoadParams& params)
|
||||||
|
{
|
||||||
|
if (!_engine || !_engine->_textureCache || data.empty())
|
||||||
|
{
|
||||||
|
return InvalidTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build TextureKey from bytes
|
||||||
|
TextureCache::TextureKey key{};
|
||||||
|
key.kind = TextureCache::TextureKey::SourceKind::Bytes;
|
||||||
|
key.bytes = data;
|
||||||
|
key.srgb = params.srgb;
|
||||||
|
key.mipmapped = params.mipmapped;
|
||||||
|
key.mipClampLevels = params.mipLevels;
|
||||||
|
|
||||||
|
// Map channel hint
|
||||||
|
switch (params.channels)
|
||||||
|
{
|
||||||
|
case TextureChannels::R:
|
||||||
|
key.channels = TextureCache::TextureKey::ChannelsHint::R;
|
||||||
|
break;
|
||||||
|
case TextureChannels::RG:
|
||||||
|
key.channels = TextureCache::TextureKey::ChannelsHint::RG;
|
||||||
|
break;
|
||||||
|
case TextureChannels::RGBA:
|
||||||
|
key.channels = TextureCache::TextureKey::ChannelsHint::RGBA;
|
||||||
|
break;
|
||||||
|
case TextureChannels::Auto:
|
||||||
|
default:
|
||||||
|
key.channels = TextureCache::TextureKey::ChannelsHint::Auto;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate hash for deduplication
|
||||||
|
uint64_t h = texcache::fnv1a64(key.bytes.data(), key.bytes.size());
|
||||||
|
key.hash = h ^ (key.srgb ? 0x9E3779B97F4A7C15ull : 0ull);
|
||||||
|
|
||||||
|
// Use default linear sampler
|
||||||
|
VkSampler sampler = VK_NULL_HANDLE;
|
||||||
|
if (_engine->_context && _engine->_context->samplers)
|
||||||
|
{
|
||||||
|
sampler = _engine->_context->samplers->defaultLinear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request texture from cache
|
||||||
|
TextureCache::TextureHandle handle = _engine->_textureCache->request(key, sampler);
|
||||||
|
return static_cast<TextureHandle>(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Engine::is_texture_loaded(TextureHandle handle) const
|
||||||
|
{
|
||||||
|
if (!_engine || !_engine->_textureCache)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto cacheHandle = static_cast<TextureCache::TextureHandle>(handle);
|
||||||
|
return _engine->_textureCache->state(cacheHandle) == TextureCache::EntryState::Resident;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* Engine::get_texture_image_view(TextureHandle handle) const
|
||||||
|
{
|
||||||
|
if (!_engine || !_engine->_textureCache)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto cacheHandle = static_cast<TextureCache::TextureHandle>(handle);
|
||||||
|
VkImageView view = _engine->_textureCache->image_view(cacheHandle);
|
||||||
|
return reinterpret_cast<void*>(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::pin_texture(TextureHandle handle)
|
||||||
|
{
|
||||||
|
if (!_engine || !_engine->_textureCache)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto cacheHandle = static_cast<TextureCache::TextureHandle>(handle);
|
||||||
|
_engine->_textureCache->pin(cacheHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::unpin_texture(TextureHandle handle)
|
||||||
|
{
|
||||||
|
if (!_engine || !_engine->_textureCache)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto cacheHandle = static_cast<TextureCache::TextureHandle>(handle);
|
||||||
|
_engine->_textureCache->unpin(cacheHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Engine::is_texture_pinned(TextureHandle handle) const
|
||||||
|
{
|
||||||
|
if (!_engine || !_engine->_textureCache)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto cacheHandle = static_cast<TextureCache::TextureHandle>(handle);
|
||||||
|
return _engine->_textureCache->is_pinned(cacheHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::unload_texture(TextureHandle handle)
|
||||||
|
{
|
||||||
|
if (!_engine || !_engine->_textureCache)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto cacheHandle = static_cast<TextureCache::TextureHandle>(handle);
|
||||||
|
_engine->_textureCache->unload(cacheHandle);
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Shadows
|
// Shadows
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -19,6 +19,28 @@ namespace GameAPI
|
|||||||
// Forward declarations and simple POD types
|
// Forward declarations and simple POD types
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
// Texture handle (opaque reference to a cached texture)
|
||||||
|
using TextureHandle = uint32_t;
|
||||||
|
static constexpr TextureHandle InvalidTexture = 0xFFFFFFFFu;
|
||||||
|
|
||||||
|
// Texture channel hint for memory optimization
|
||||||
|
enum class TextureChannels : uint8_t
|
||||||
|
{
|
||||||
|
Auto = 0, // Detect from source (default)
|
||||||
|
R = 1, // Single channel (e.g., occlusion, metallic)
|
||||||
|
RG = 2, // Two channels (e.g., normal map XY)
|
||||||
|
RGBA = 3 // Full color
|
||||||
|
};
|
||||||
|
|
||||||
|
// Texture loading parameters
|
||||||
|
struct TextureLoadParams
|
||||||
|
{
|
||||||
|
bool srgb{false}; // Use sRGB color space (true for albedo/emissive)
|
||||||
|
bool mipmapped{true}; // Generate mipmap chain
|
||||||
|
TextureChannels channels{TextureChannels::Auto}; // Channel hint
|
||||||
|
uint32_t mipLevels{0}; // 0 = full chain, otherwise limit to N levels
|
||||||
|
};
|
||||||
|
|
||||||
// Shadow rendering mode
|
// Shadow rendering mode
|
||||||
enum class ShadowMode : uint32_t
|
enum class ShadowMode : uint32_t
|
||||||
{
|
{
|
||||||
@@ -326,6 +348,41 @@ public:
|
|||||||
// Force eviction to budget (call after loading large assets)
|
// Force eviction to budget (call after loading large assets)
|
||||||
void evict_textures_to_budget();
|
void evict_textures_to_budget();
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// Texture Loading
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Load a texture from file path (relative to assets/textures/ or absolute)
|
||||||
|
// Returns a handle that can be used to query state or bind to materials
|
||||||
|
TextureHandle load_texture(const std::string& path,
|
||||||
|
const TextureLoadParams& params = {});
|
||||||
|
|
||||||
|
// Load a texture from memory (compressed image data: PNG, JPG, KTX2, etc.)
|
||||||
|
// Useful for runtime-generated or downloaded textures
|
||||||
|
TextureHandle load_texture_from_memory(const std::vector<uint8_t>& data,
|
||||||
|
const TextureLoadParams& params = {});
|
||||||
|
|
||||||
|
// Check if a texture is loaded and resident in VRAM
|
||||||
|
bool is_texture_loaded(TextureHandle handle) const;
|
||||||
|
|
||||||
|
// Get the internal Vulkan image view for advanced use cases
|
||||||
|
// Returns VK_NULL_HANDLE if texture is not yet loaded
|
||||||
|
void* get_texture_image_view(TextureHandle handle) const; // Returns VkImageView
|
||||||
|
|
||||||
|
// Pin a texture to prevent automatic eviction (useful for UI elements, critical assets)
|
||||||
|
// Pinned textures are never removed from VRAM by LRU or budget constraints
|
||||||
|
void pin_texture(TextureHandle handle);
|
||||||
|
|
||||||
|
// Unpin a texture, allowing it to be evicted normally
|
||||||
|
void unpin_texture(TextureHandle handle);
|
||||||
|
|
||||||
|
// Check if a texture is currently pinned
|
||||||
|
bool is_texture_pinned(TextureHandle handle) const;
|
||||||
|
|
||||||
|
// Unload a texture and free VRAM (textures are ref-counted and auto-evicted by LRU)
|
||||||
|
// This is optional - the cache manages memory automatically
|
||||||
|
void unload_texture(TextureHandle handle);
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
// Shadows
|
// Shadows
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user