ADD: Texture cache system improvement

This commit is contained in:
2025-12-25 19:43:47 +09:00
parent 4b3338f7b5
commit fae718e9b3
4 changed files with 424 additions and 10 deletions

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View File

@@ -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
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------