diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f381869..e42d57e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -32,6 +32,8 @@ add_executable (vulkan_engine core/vk_pipeline_manager.cpp core/frame_resources.h core/frame_resources.cpp + core/texture_cache.h + core/texture_cache.cpp core/config.h core/vk_engine.h core/vk_engine.cpp diff --git a/src/core/engine_context.h b/src/core/engine_context.h index e50014f..4dad206 100644 --- a/src/core/engine_context.h +++ b/src/core/engine_context.h @@ -30,11 +30,12 @@ struct SDL_Window; class AssetManager; class RenderGraph; class RayTracingManager; +class TextureCache; struct ShadowSettings { // 0 = Clipmap only, 1 = Clipmap + RT assist, 2 = RT only - uint32_t mode = 0; + uint32_t mode = 2; bool hybridRayQueryEnabled = false; // derived convenience: (mode != 0) uint32_t hybridRayCascadesMask = 0b1110; // bit i => cascade i uses ray query assist (default: 1..3) float hybridRayNoLThreshold = 0.25f; // trigger when N·L below this (mode==1) @@ -56,6 +57,7 @@ public: // Per-frame and subsystem pointers for modules to use without VulkanEngine FrameResources* currentFrame = nullptr; // set by engine each frame + uint32_t frameIndex = 0; // incremented by engine each frame EngineStats* stats = nullptr; // points to engine stats ComputeManager* compute = nullptr; // compute subsystem PipelineManager* pipelines = nullptr; // graphics pipeline manager @@ -90,4 +92,7 @@ public: // Convenience alias (singular) requested AssetManager* getAsset() const { return assets; } RenderGraph* getRenderGraph() const { return renderGraph; } + + // Streaming subsystems (engine-owned) + TextureCache* textures = nullptr; // texture streaming + cache }; diff --git a/src/core/vk_engine.cpp b/src/core/vk_engine.cpp index 2db1489..ec8b11e 100644 --- a/src/core/vk_engine.cpp +++ b/src/core/vk_engine.cpp @@ -51,6 +51,44 @@ #include "core/vk_pipeline_manager.h" #include "core/config.h" +// Query a conservative streaming texture budget based on VMA-reported +// device-local heap budgets. Uses ~35% of total device-local budget. +static size_t query_texture_budget_bytes(DeviceManager* dev) +{ + if (!dev) return 512ull * 1024ull * 1024ull; // fallback + VmaAllocator alloc = dev->allocator(); + if (!alloc) return 512ull * 1024ull * 1024ull; + + const VkPhysicalDeviceMemoryProperties* memProps = nullptr; + vmaGetMemoryProperties(alloc, &memProps); + if (!memProps) return 512ull * 1024ull * 1024ull; + + VmaBudget budgets[VK_MAX_MEMORY_HEAPS] = {}; + vmaGetHeapBudgets(alloc, budgets); + + unsigned long long totalBudget = 0; + unsigned long long totalUsage = 0; + for (uint32_t i = 0; i < memProps->memoryHeapCount; ++i) + { + if (memProps->memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) + { + totalBudget += budgets[i].budget; + totalUsage += budgets[i].usage; + } + } + if (totalBudget == 0) return 512ull * 1024ull * 1024ull; + + // Reserve ~65% of VRAM for attachments, swapchain, meshes, AS, etc. + unsigned long long cap = static_cast(double(totalBudget) * 0.35); + + // If usage is already near the cap, still allow current textures to live; eviction will trim. + // Clamp to at least 128 MB, at most totalBudget. + unsigned long long minCap = 128ull * 1024ull * 1024ull; + if (cap < minCap) cap = minCap; + if (cap > totalBudget) cap = totalBudget; + return static_cast(cap); +} + // // ImGui helpers: keep UI code tidy and grouped in small functions. // These render inside a single consolidated Debug window using tab items. @@ -416,6 +454,11 @@ void VulkanEngine::init() _assetManager->init(this); _context->assets = _assetManager.get(); + // Create texture cache (engine-owned, accessible via EngineContext) + _textureCache = std::make_unique(); + _textureCache->init(_context.get()); + _context->textures = _textureCache.get(); + // Optional ray tracing manager if supported and extensions enabled if (_deviceManager->supportsRayQuery() && _deviceManager->supportsAccelerationStructure()) { @@ -568,7 +611,9 @@ void VulkanEngine::cleanup() print_vma_stats(_deviceManager.get(), "after MainDQ flush"); dump_vma_json(_deviceManager.get(), "after_MainDQ"); - _renderPassManager->cleanup(); + if (_textureCache) { _textureCache->cleanup(); } + + _renderPassManager->cleanup(); print_vma_stats(_deviceManager.get(), "after RenderPassManager"); dump_vma_json(_deviceManager.get(), "after_RenderPassManager"); @@ -673,8 +718,12 @@ void VulkanEngine::draw() // publish per-frame pointers and draw extent to context for passes _context->currentFrame = &get_current_frame(); + _context->frameIndex = static_cast(_frameNumber); _context->drawExtent = _drawExtent; + // Inform VMA of current frame for improved internal stats/aging (optional). + vmaSetCurrentFrameIndex(_deviceManager->allocator(), _context->frameIndex); + // Optional: check for shader changes and hot-reload pipelines if (_pipelineManager) { @@ -702,6 +751,14 @@ void VulkanEngine::draw() hShadowCascades[i] = _renderGraph->create_depth_image(name.c_str(), shadowExtent, VK_FORMAT_D32_SFLOAT); } + // Prior to building passes, pump texture loads for this frame. + if (_textureCache) + { + size_t budget = query_texture_budget_bytes(_deviceManager.get()); + _textureCache->evictToBudget(budget); + _textureCache->pumpLoads(*_resourceManager, get_current_frame()); + } + _resourceManager->register_upload_pass(*_renderGraph, get_current_frame()); ImGuiPass *imguiPass = nullptr; diff --git a/src/core/vk_engine.h b/src/core/vk_engine.h index a514df9..f9c850e 100644 --- a/src/core/vk_engine.h +++ b/src/core/vk_engine.h @@ -31,6 +31,7 @@ #include "core/asset_manager.h" #include "render/rg_graph.h" #include "core/vk_raytracing.h" +#include "core/texture_cache.h" // Number of frames-in-flight. Affects per-frame command buffers, fences, // semaphores, and transient descriptor pools in FrameResources. @@ -67,6 +68,7 @@ public: std::unique_ptr _assetManager; std::unique_ptr _renderGraph; std::unique_ptr _rayManager; + std::unique_ptr _textureCache; struct SDL_Window *_window{nullptr}; diff --git a/src/render/vk_renderpass_geometry.cpp b/src/render/vk_renderpass_geometry.cpp index 10b2247..fdbcb47 100644 --- a/src/render/vk_renderpass_geometry.cpp +++ b/src/render/vk_renderpass_geometry.cpp @@ -4,6 +4,7 @@ #include #include "frame_resources.h" +#include "texture_cache.h" #include "vk_descriptor_manager.h" #include "vk_device.h" #include "core/engine_context.h" @@ -240,6 +241,10 @@ void GeometryPass::draw_geometry(VkCommandBuffer cmd, vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->layout, 1, 1, &r.material->materialSet, 0, nullptr); + if (ctxLocal->textures) + { + ctxLocal->textures->markSetUsed(r.material->materialSet, ctxLocal->frameIndex); + } } if (r.indexBuffer != lastIndexBuffer) { diff --git a/src/render/vk_renderpass_transparent.cpp b/src/render/vk_renderpass_transparent.cpp index 94c586a..87705d2 100644 --- a/src/render/vk_renderpass_transparent.cpp +++ b/src/render/vk_renderpass_transparent.cpp @@ -2,6 +2,8 @@ #include #include + +#include "texture_cache.h" #include "vk_scene.h" #include "vk_swapchain.h" #include "core/engine_context.h" @@ -133,6 +135,10 @@ void TransparentPass::draw_transparent(VkCommandBuffer cmd, } vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->layout, 1, 1, &r.material->materialSet, 0, nullptr); + if (ctxLocal->textures) + { + ctxLocal->textures->markSetUsed(r.material->materialSet, ctxLocal->frameIndex); + } } if (r.indexBuffer != lastIndexBuffer) { diff --git a/src/scene/vk_loader.cpp b/src/scene/vk_loader.cpp index b67b6b3..2576a74 100644 --- a/src/scene/vk_loader.cpp +++ b/src/scene/vk_loader.cpp @@ -1,6 +1,7 @@ #include "stb_image.h" #include #include "vk_loader.h" +#include "core/texture_cache.h" #include "core/vk_engine.h" #include "render/vk_materials.h" @@ -260,40 +261,58 @@ std::optional > loadGltf(VulkanEngine *engine, std:: // temporal arrays for all the objects to use while creating the GLTF data std::vector > meshes; std::vector > nodes; - std::vector images; std::vector > materials; //< load_arrays - // load all textures - for (size_t i = 0; i < gltf.images.size(); ++i) + // Note: glTF images are now loaded on-demand via TextureCache. + auto buildTextureKey = [&](size_t imgIndex, bool srgb) -> TextureCache::TextureKey { - fastgltf::Image &image = gltf.images[i]; - // Default-load GLTF images as linear; baseColor is reloaded as sRGB when bound - std::optional img = load_image(engine, gltf, image, false); - - if (img.has_value()) + TextureCache::TextureKey key{}; + key.srgb = srgb; + key.mipmapped = true; + if (imgIndex >= gltf.images.size()) { - images.push_back(*img); - // Use a unique, stable key so every allocation is tracked and later freed. - std::string key = image.name.empty() ? (std::string("gltf.image.") + std::to_string(i)) - : std::string(image.name.c_str()); - // Avoid accidental collisions from duplicate names - int suffix = 1; - while (file.images.find(key) != file.images.end()) + key.hash = 0; // invalid + return key; + } + fastgltf::Image &image = gltf.images[imgIndex]; + std::visit(fastgltf::visitor{ + [&](fastgltf::sources::URI &filePath) { - key = (image.name.empty() ? std::string("gltf.image.") + std::to_string(i) - : std::string(image.name.c_str())) + std::string("#") + std::to_string(suffix++); - } - file.images[key] = *img; - } - else - { - // we failed to load, so lets give the slot a default white texture to not - // completely break loading - images.push_back(engine->_errorCheckerboardImage); - std::cout << "gltf failed to load texture index " << i << " (name='" << image.name << "')" << std::endl; - } - } + const std::string path(filePath.uri.path().begin(), filePath.uri.path().end()); + key.kind = TextureCache::TextureKey::SourceKind::FilePath; + key.path = path; + std::string id = std::string("GLTF:") + path + (srgb ? "#sRGB" : "#UNORM"); + key.hash = texcache::fnv1a64(id); + }, + [&](fastgltf::sources::Vector &vector) + { + key.kind = TextureCache::TextureKey::SourceKind::Bytes; + key.bytes.assign(vector.bytes.begin(), vector.bytes.end()); + uint64_t h = texcache::fnv1a64(key.bytes.data(), key.bytes.size()); + key.hash = h ^ (srgb ? 0x9E3779B97F4A7C15ull : 0ull); + }, + [&](fastgltf::sources::BufferView &view) + { + auto &bufferView = gltf.bufferViews[view.bufferViewIndex]; + auto &buffer = gltf.buffers[bufferView.bufferIndex]; + std::visit(fastgltf::visitor{ + [](auto &arg) {}, + [&](fastgltf::sources::Vector &vec) + { + size_t off = bufferView.byteOffset; + size_t len = bufferView.byteLength; + key.kind = TextureCache::TextureKey::SourceKind::Bytes; + key.bytes.assign(vec.bytes.begin() + off, vec.bytes.begin() + off + len); + uint64_t h = texcache::fnv1a64(key.bytes.data(), key.bytes.size()); + key.hash = h ^ (srgb ? 0x9E3779B97F4A7C15ull : 0ull); + } + }, buffer.data); + }, + [](auto &other) {} + }, image.data); + return key; + }; //> load_buffer // create buffer to hold the material data @@ -343,90 +362,79 @@ std::optional > loadGltf(VulkanEngine *engine, std:: // set the uniform buffer for the material data materialResources.dataBuffer = file.materialDataBuffer.buffer; materialResources.dataBufferOffset = data_index * sizeof(GLTFMetallic_Roughness::MaterialConstants); - // grab textures from gltf file - if (mat.pbrData.baseColorTexture.has_value()) + // Dynamic texture bindings via TextureCache (fallbacks are already set) + TextureCache *cache = engine->_context->textures; + TextureCache::TextureHandle hColor = TextureCache::InvalidHandle; + TextureCache::TextureHandle hMRO = TextureCache::InvalidHandle; + TextureCache::TextureHandle hNorm = TextureCache::InvalidHandle; + + if (cache && mat.pbrData.baseColorTexture.has_value()) { const auto &tex = gltf.textures[mat.pbrData.baseColorTexture.value().textureIndex]; - size_t imgIndex = tex.imageIndex.value(); - // Sampler is optional in glTF; fall back to default if missing - bool hasSampler = tex.samplerIndex.has_value(); - size_t sampler = hasSampler ? tex.samplerIndex.value() : SIZE_MAX; - - // Reload albedo as sRGB, independent of the global image cache - if (imgIndex < gltf.images.size()) + const size_t imgIndex = tex.imageIndex.value(); + const bool hasSampler = tex.samplerIndex.has_value(); + const VkSampler sampler = hasSampler ? file.samplers[tex.samplerIndex.value()] : engine->_samplerManager->defaultLinear(); + auto key = buildTextureKey(imgIndex, true); + if (key.hash != 0) { - auto albedoImg = load_image(engine, gltf, gltf.images[imgIndex], true); - if (albedoImg.has_value()) - { - materialResources.colorImage = *albedoImg; - // Track for cleanup using a unique key - std::string key = std::string("albedo_") + mat.name.c_str() + "_" + std::to_string(imgIndex); - file.images[key] = *albedoImg; - } - else - { - materialResources.colorImage = images[imgIndex]; - } + hColor = cache->request(key, sampler); + materialResources.colorSampler = sampler; } - else - { - materialResources.colorImage = engine->_errorCheckerboardImage; - } - materialResources.colorSampler = hasSampler ? file.samplers[sampler] - : engine->_samplerManager->defaultLinear(); } - // Metallic-Roughness texture - if (mat.pbrData.metallicRoughnessTexture.has_value()) + if (cache && mat.pbrData.metallicRoughnessTexture.has_value()) { const auto &tex = gltf.textures[mat.pbrData.metallicRoughnessTexture.value().textureIndex]; - size_t imgIndex = tex.imageIndex.value(); - bool hasSampler = tex.samplerIndex.has_value(); - size_t sampler = hasSampler ? tex.samplerIndex.value() : SIZE_MAX; - if (imgIndex < images.size()) + const size_t imgIndex = tex.imageIndex.value(); + const bool hasSampler = tex.samplerIndex.has_value(); + const VkSampler sampler = hasSampler ? file.samplers[tex.samplerIndex.value()] : engine->_samplerManager->defaultLinear(); + auto key = buildTextureKey(imgIndex, false); + if (key.hash != 0) { - materialResources.metalRoughImage = images[imgIndex]; - materialResources.metalRoughSampler = hasSampler ? file.samplers[sampler] - : engine->_samplerManager->defaultLinear(); + hMRO = cache->request(key, sampler); + materialResources.metalRoughSampler = sampler; } } - // Normal map (tangent-space) - if (mat.normalTexture.has_value()) + if (cache && mat.normalTexture.has_value()) { const auto &tex = gltf.textures[mat.normalTexture.value().textureIndex]; - size_t imgIndex = tex.imageIndex.value(); - bool hasSampler = tex.samplerIndex.has_value(); - size_t sampler = hasSampler ? tex.samplerIndex.value() : SIZE_MAX; - - if (imgIndex < gltf.images.size()) + const size_t imgIndex = tex.imageIndex.value(); + const bool hasSampler = tex.samplerIndex.has_value(); + const VkSampler sampler = hasSampler ? file.samplers[tex.samplerIndex.value()] : engine->_samplerManager->defaultLinear(); + auto key = buildTextureKey(imgIndex, false); + if (key.hash != 0) { - auto normalImg = load_image(engine, gltf, gltf.images[imgIndex], false); - if (normalImg.has_value()) - { - materialResources.normalImage = *normalImg; - std::string key = std::string("normal_") + mat.name.c_str() + "_" + std::to_string(imgIndex); - file.images[key] = *normalImg; - } - else - { - materialResources.normalImage = images[imgIndex]; - } + hNorm = cache->request(key, sampler); + materialResources.normalSampler = sampler; } - else - { - materialResources.normalImage = engine->_flatNormalImage; - } - materialResources.normalSampler = hasSampler ? file.samplers[sampler] - : engine->_samplerManager->defaultLinear(); - - // Store normal scale into material constants extra[0].x if available + // Store normal scale if provided sceneMaterialConstants[data_index].extra[0].x = mat.normalTexture->scale; } // build material newMat->data = engine->metalRoughMaterial.write_material(engine->_deviceManager->device(), passType, materialResources, file.descriptorPool); + // Register descriptor patches for dynamic textures + if (cache) + { + if (hColor != TextureCache::InvalidHandle) + { + cache->watchBinding(hColor, newMat->data.materialSet, 1u, materialResources.colorSampler, + engine->_whiteImage.imageView); + } + if (hMRO != TextureCache::InvalidHandle) + { + cache->watchBinding(hMRO, newMat->data.materialSet, 2u, materialResources.metalRoughSampler, + engine->_whiteImage.imageView); + } + if (hNorm != TextureCache::InvalidHandle) + { + cache->watchBinding(hNorm, newMat->data.materialSet, 3u, materialResources.normalSampler, + engine->_flatNormalImage.imageView); + } + } + data_index++; } //< load_material