From 0f41d718cf05e54555a67b6c5987af23cfd580a9 Mon Sep 17 00:00:00 2001 From: Jean Philippe Date: Fri, 4 Sep 2026 20:10:20 +0900 Subject: [PATCH] feat(rendering): redesign texture pipeline around one canonical handle RRM's own ImageHandle/m_image_slots texture system had zero real consumers and was disconnected from what actually renders (materials sample via a raw bindless index into Device->GlobalTextures). Delete it in favor of Rendering::Textures::TextureHandle everywhere, and fix the three structural gaps that came with it: - No working texture disposal: TextureHandleToDispose had a consumer but no producer. VulkanDevice::DestroyTexture is now the sole producer, timeline- gating both the VkImage free and the bindless slot reclaim in Present(). - No real hot-reload trigger: AssetManager::IngestTexture's dedup silently blocked re-ingest. A new TextureImporter routes texture files through ImportCoordinator like every other asset type, and a dedup hit now calls RenderResourceManager::ScheduleTextureReload instead of no-op'ing. - No reference safety: materials stored texture refs as a bare uint64_t index with no generation. AssetManager::ReleaseTexture/FlushTextureReleases patch every referencing material to the INVALID_MAP_HANDLE sentinel before the underlying bindless slot can ever be reused. Also: VulkanDevice::ReconstructTexture generalizes the in-place resize pattern (same handle, same slot) previously duplicated inline in RenderGraph::Resize; TextureHandleToDispose is now a lock-free SPSC queue since producer and consumer are both render-thread only; AssetRegistry:: InferTypeFromExtension recognizes all 8 raster extensions TextureImporter claims plus the pre-existing .exr gap; Image2DBuffer renamed to ImageBuffer (it holds 2D, cube, and array images, not just 2D); and the fully dead Texture2D.h/.cpp (a superseded Ref-based texture class, zero callers) is removed. Adversarially reviewed in 4 parallel passes; 2 real bugs found and fixed (an unlocked concurrent read, and a missing arena Clear() that would have grown unboundedly and crashed on exhaustion). 554/554 tests passing, 5 new (AssetRegistry extension coverage + OnRemoved callback firing, TextureImporter::CanImport coverage). Verified live in Obelisk under an aggressive resize stress test with no leaks or crashes. --- .../Applications/AppRenderPipeline.cpp | 1 + .../Core/VFS/Registry/AssetRegistry.cpp | 18 +- .../ZEngine/Core/VFS/Registry/AssetRegistry.h | 6 + ZEngine/ZEngine/Engine.cpp | 6 +- ZEngine/ZEngine/Hardwares/DeviceSwapchain.cpp | 24 +- ZEngine/ZEngine/Hardwares/VulkanDevice.cpp | 166 +++++---- ZEngine/ZEngine/Hardwares/VulkanDevice.h | 61 ++- ZEngine/ZEngine/Importers/TextureImporter.cpp | 50 +++ ZEngine/ZEngine/Importers/TextureImporter.h | 23 ++ ZEngine/ZEngine/Managers/AssetManager.cpp | 127 ++++++- ZEngine/ZEngine/Managers/AssetManager.h | 21 ++ .../ZEngine/Rendering/Buffers/FrameBuffer.cpp | 2 +- ZEngine/ZEngine/Rendering/RenderHandle.h | 4 - .../Rendering/RenderResourceManager.cpp | 350 +++++++----------- .../ZEngine/Rendering/RenderResourceManager.h | 286 +++++++------- .../Rendering/Renderers/RenderGraph.cpp | 52 +-- .../Renderers/RenderPasses/RenderPass.cpp | 4 +- .../Specifications/TextureSpecification.h | 2 +- ZEngine/ZEngine/Rendering/Textures/Texture.h | 25 +- .../ZEngine/Rendering/Textures/Texture2D.cpp | 263 ------------- .../ZEngine/Rendering/Textures/Texture2D.h | 46 --- .../Rendering/RenderResourceManagerTest.cpp | 15 +- .../tests/Rendering/TextureImporterTest.cpp | 31 ++ ZEngine/tests/VFS/AssetRegistryTest.cpp | 44 +++ 24 files changed, 758 insertions(+), 869 deletions(-) create mode 100644 ZEngine/ZEngine/Importers/TextureImporter.cpp create mode 100644 ZEngine/ZEngine/Importers/TextureImporter.h delete mode 100644 ZEngine/ZEngine/Rendering/Textures/Texture2D.cpp delete mode 100644 ZEngine/ZEngine/Rendering/Textures/Texture2D.h create mode 100644 ZEngine/tests/Rendering/TextureImporterTest.cpp diff --git a/ZEngine/ZEngine/Applications/AppRenderPipeline.cpp b/ZEngine/ZEngine/Applications/AppRenderPipeline.cpp index eba265ead..9eabbebf0 100644 --- a/ZEngine/ZEngine/Applications/AppRenderPipeline.cpp +++ b/ZEngine/ZEngine/Applications/AppRenderPipeline.cpp @@ -122,6 +122,7 @@ namespace ZEngine::Applications if (Device->RRM) static_cast(Device->RRM)->BeginFrame(swapchain->CurrentFrame->Index); + Managers::AssetManager::FlushTextureReleases(); for (uint8_t thread_idx = 0; thread_idx < Device->CommandBufferMgr->TotalThreadCount; ++thread_idx) { diff --git a/ZEngine/ZEngine/Core/VFS/Registry/AssetRegistry.cpp b/ZEngine/ZEngine/Core/VFS/Registry/AssetRegistry.cpp index 57137fa7c..469dfbb87 100644 --- a/ZEngine/ZEngine/Core/VFS/Registry/AssetRegistry.cpp +++ b/ZEngine/ZEngine/Core/VFS/Registry/AssetRegistry.cpp @@ -150,6 +150,12 @@ namespace ZEngine::Core::VFS m_stale_cb = cb; } + void AssetRegistry::SetOnRemovedCallback(void* ctx, void (*cb)(void*, const uuids::uuid&, Managers::AssetType)) + { + m_removed_cb_ctx = ctx; + m_removed_cb = cb; + } + void AssetRegistry::OnAssetModified(const Core::VFS::VFSPath& path) { Helpers::Handle handle = m_index.FindByPath(path); @@ -204,6 +210,16 @@ namespace ZEngine::Core::VFS if (m_reload_cb && !cascade.empty()) m_reload_cb(m_reload_cb_ctx, std::span(cascade.data(), cascade.size())); + if (m_removed_cb) + { + for (uint32_t i = 0; i < cascade.size(); ++i) + { + AssetRecord* cascade_rec = FindByUUID(cascade[i]); + if (cascade_rec) + m_removed_cb(m_removed_cb_ctx, cascade[i], cascade_rec->Type); + } + } + Remove(rec->UUID); } @@ -356,7 +372,7 @@ namespace ZEngine::Core::VFS if (ext.Empty() || !ext.Data) return Managers::AssetType::MESH; - if (ext.Equals(".png") || ext.Equals(".jpg") || ext.Equals(".jpeg") || ext.Equals(".hdr") || ext.Equals(".ktx") || ext.Equals(".ktx2")) + if (ext.Equals(".png") || ext.Equals(".jpg") || ext.Equals(".jpeg") || ext.Equals(".bmp") || ext.Equals(".tga") || ext.Equals(".gif") || ext.Equals(".psd") || ext.Equals(".pic") || ext.Equals(".hdr") || ext.Equals(".exr") || ext.Equals(".ktx") || ext.Equals(".ktx2")) return Managers::AssetType::TEXTURE; if (ext.Equals(".zematerial")) return Managers::AssetType::MATERIAL; diff --git a/ZEngine/ZEngine/Core/VFS/Registry/AssetRegistry.h b/ZEngine/ZEngine/Core/VFS/Registry/AssetRegistry.h index b733456c7..e774d0c16 100644 --- a/ZEngine/ZEngine/Core/VFS/Registry/AssetRegistry.h +++ b/ZEngine/ZEngine/Core/VFS/Registry/AssetRegistry.h @@ -72,6 +72,10 @@ namespace ZEngine::Core::VFS void SetOnReadyCallback(void* ctx, void (*cb)(void*, const uuids::uuid&, Managers::AssetHandle)); void SetOnStaleCallback(void* ctx, void (*cb)(void*, const uuids::uuid&)); + /// @brief Register a callback fired once per cascade UUID from OnAssetDeleted, + /// before each record is erased (so it can still read the record's Type). + void SetOnRemovedCallback(void* ctx, void (*cb)(void*, const uuids::uuid&, Managers::AssetType)); + void OnAssetModified(const Core::VFS::VFSPath& path); void OnAssetDeleted(const Core::VFS::VFSPath& path); void OnAssetRenamed(const Core::VFS::VFSPath& old_path, const Core::VFS::VFSPath& new_path); @@ -102,6 +106,8 @@ namespace ZEngine::Core::VFS void (*m_ready_cb)(void*, const uuids::uuid&, Managers::AssetHandle) = nullptr; void* m_stale_cb_ctx = nullptr; void (*m_stale_cb)(void*, const uuids::uuid&) = nullptr; + void* m_removed_cb_ctx = nullptr; + void (*m_removed_cb)(void*, const uuids::uuid&, Managers::AssetType) = nullptr; Core::Memory::ArenaAllocator m_scratch = {}; diff --git a/ZEngine/ZEngine/Engine.cpp b/ZEngine/ZEngine/Engine.cpp index 8314129ce..28a1e137e 100644 --- a/ZEngine/ZEngine/Engine.cpp +++ b/ZEngine/ZEngine/Engine.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -95,7 +96,7 @@ namespace ZEngine ECS::Components::RegisterBuiltInComponentReflection(); // ImportPipeline arena: each importer carves its own sub-arena directly from this - // parent (glTF 64 MB + Assimp 128 MB + envmap 32 MB + editor ~414 MB). + // parent (glTF 64 MB + Assimp 128 MB + envmap 32 MB + texture 512 KB + editor ~414 MB). // Each Import() call ends with Arena.Clear() so the sub-arena is reused, not consumed. memory->CreateBudgetedArena(memory->Budget.ImportPipeline, &g_engine_ctx->ImportPipelineArena); memory->CreateBudgetedArena(memory->Budget.UIContext, &g_engine_ctx->UIContextArena); @@ -106,14 +107,17 @@ namespace ZEngine static Importers::FbxImporter s_fbx_importer; static Importers::AssimpImporter s_assimp_importer; static Importers::EnvironmentMapImporter s_env_map_importer; + static Importers::TextureImporter s_texture_importer; s_gltf_importer.Initialize(&g_engine_ctx->ImportPipelineArena); s_fbx_importer.Initialize(&g_engine_ctx->ImportPipelineArena); s_assimp_importer.Initialize(&g_engine_ctx->ImportPipelineArena); s_env_map_importer.Initialize(&g_engine_ctx->ImportPipelineArena); + s_texture_importer.Initialize(&g_engine_ctx->ImportPipelineArena); g_engine_ctx->ImportCoordinator->RegisterImporter(&s_gltf_importer); g_engine_ctx->ImportCoordinator->RegisterImporter(&s_fbx_importer); g_engine_ctx->ImportCoordinator->RegisterImporter(&s_assimp_importer); g_engine_ctx->ImportCoordinator->RegisterImporter(&s_env_map_importer); + g_engine_ctx->ImportCoordinator->RegisterImporter(&s_texture_importer); // RenderResourceManager — GPU lifetime authority, bridges asset layer and VulkanDevice g_engine_ctx->RenderResourceManager = ZPushStructCtor(&g_engine_ctx->AssetArena, Rendering::RenderResourceManager); diff --git a/ZEngine/ZEngine/Hardwares/DeviceSwapchain.cpp b/ZEngine/ZEngine/Hardwares/DeviceSwapchain.cpp index e1dd49f04..94917a5d7 100644 --- a/ZEngine/ZEngine/Hardwares/DeviceSwapchain.cpp +++ b/ZEngine/ZEngine/Hardwares/DeviceSwapchain.cpp @@ -357,7 +357,7 @@ namespace ZEngine::Hardwares Device->TextureHandleToUpdates.Enqueue(tex_handle); break; } - auto img_buf = Device->Image2DBufferManager.Access(texture->BufferHandle); + auto img_buf = Device->ImageBufferManager.Access(texture->BufferHandle); const auto& image_info = img_buf->GetDescriptorImageInfo(); auto scratch = ZGetScratch(&Arena); @@ -388,19 +388,29 @@ namespace ZEngine::Hardwares } { - Textures::TextureHandle tex_to_dispose = {}; - while (Device->TextureHandleToDispose.Pop(tex_to_dispose)) + uint64_t completed = 0; + vkGetSemaphoreCounterValue(Device->LogicalDevice, RenderTimeline->GetHandle(), &completed); + + TextureDisposeEntry entry = {}; + while (Device->TextureHandleToDispose.pop(entry)) { - auto texture = Device->GlobalTextures.Access(tex_to_dispose); + if (entry.TimelineValue > completed) + { + // Not yet safe — push back and stop rather than skip past it (mirrors the + // TextureHandleToUpdates pattern above; render-thread-only, so no race). + Device->TextureHandleToDispose.push(entry); + break; + } + auto texture = Device->GlobalTextures.Access(entry.Handle); if (texture) { - auto buf = Device->Image2DBufferManager.Access(texture->BufferHandle); + auto buf = Device->ImageBufferManager.Access(texture->BufferHandle); if (buf) { buf->Dispose(); } - Device->Image2DBufferManager.Remove(texture->BufferHandle); - Device->GlobalTextures.Remove(tex_to_dispose); + Device->ImageBufferManager.Remove(texture->BufferHandle); + Device->GlobalTextures.Remove(entry.Handle); } } } diff --git a/ZEngine/ZEngine/Hardwares/VulkanDevice.cpp b/ZEngine/ZEngine/Hardwares/VulkanDevice.cpp index a08f5b0ee..0ca64543d 100644 --- a/ZEngine/ZEngine/Hardwares/VulkanDevice.cpp +++ b/ZEngine/ZEngine/Hardwares/VulkanDevice.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -528,7 +527,7 @@ namespace ZEngine::Hardwares MaxGlobalTexture = std::min(MaxGlobalTexture, PhysicalDeviceVulkan12Properties.maxPerStageDescriptorUpdateAfterBindSampledImages - 1); GlobalTextures.Initialize(Arena, MaxGlobalTexture); - Image2DBufferManager.Initialize(Arena, MaxGlobalTexture); + ImageBufferManager.Initialize(Arena, MaxGlobalTexture); { VkDescriptorSetLayoutCreateInfo empty_layout_info = {}; empty_layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; @@ -663,25 +662,27 @@ namespace ZEngine::Hardwares GpuMem.Ring.Drain(UINT64_MAX); { - Rendering::Textures::TextureHandle tex_to_dispose = {}; - while (TextureHandleToDispose.Pop(tex_to_dispose)) + // Full shutdown — drain unconditionally, no timeline gate needed (mirrors + // PendingFree.Drain(..., UINT64_MAX) above). + TextureDisposeEntry entry = {}; + while (TextureHandleToDispose.pop(entry)) { - auto texture = GlobalTextures.Access(tex_to_dispose); + auto texture = GlobalTextures.Access(entry.Handle); if (texture) { - auto buf = Image2DBufferManager.Access(texture->BufferHandle); + auto buf = ImageBufferManager.Access(texture->BufferHandle); if (buf) { buf->Dispose(); } - Image2DBufferManager.Remove(texture->BufferHandle); - GlobalTextures.Remove(tex_to_dispose); + ImageBufferManager.Remove(texture->BufferHandle); + GlobalTextures.Remove(entry.Handle); } } } GlobalTextures.Dispose(); - Image2DBufferManager.Dispose(); + ImageBufferManager.Dispose(); ShaderManager.Dispose(); SwapchainPtr->Dispose(); @@ -1794,7 +1795,7 @@ namespace ZEngine::Hardwares - void Image2DBuffer::Construct(Hardwares::VulkanDevice* device) + void ImageBuffer::Construct(Hardwares::VulkanDevice* device) { Device = device; Layout = Rendering::Specifications::ImageLayout::UNDEFINED; @@ -1813,32 +1814,32 @@ namespace ZEngine::Hardwares m_buffer_image = Device->CreateImage(Specification.Width, Specification.Height, VK_IMAGE_TYPE_2D, Specifications::ImageViewTypeMap[VALUE_FROM_SPEC_MAP(image_view_type)], Specification.ImageFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED, Specification.ImageUsage, VK_SHARING_MODE_EXCLUSIVE, VK_SAMPLE_COUNT_1_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, Specification.ImageAspectFlag, Specification.LayerCount, Specifications::ImageCreateFlagMap[VALUE_FROM_SPEC_MAP(image_create_flag)]); } - Image2DBuffer::~Image2DBuffer() + ImageBuffer::~ImageBuffer() { Dispose(); } - BufferImage& Image2DBuffer::GetBuffer() + BufferImage& ImageBuffer::GetBuffer() { return m_buffer_image; } - const BufferImage& Image2DBuffer::GetBuffer() const + const BufferImage& ImageBuffer::GetBuffer() const { return m_buffer_image; } - VkImage Image2DBuffer::GetHandle() const + VkImage ImageBuffer::GetHandle() const { return m_buffer_image.Handle; } - VkSampler Image2DBuffer::GetSampler() const + VkSampler ImageBuffer::GetSampler() const { return m_buffer_image.Sampler; } - void Image2DBuffer::Dispose() + void ImageBuffer::Dispose() { if (m_buffer_image) { @@ -1850,7 +1851,7 @@ namespace ZEngine::Hardwares } } - VkDescriptorImageInfo& Image2DBuffer::GetDescriptorImageInfo() + VkDescriptorImageInfo& ImageBuffer::GetDescriptorImageInfo() { m_image_info.sampler = m_buffer_image.Sampler; m_image_info.imageView = m_buffer_image.ViewHandle; @@ -1858,7 +1859,7 @@ namespace ZEngine::Hardwares return m_image_info; } - VkImageView Image2DBuffer::GetImageViewHandle() const + VkImageView ImageBuffer::GetImageViewHandle() const { return m_buffer_image.ViewHandle; } @@ -1873,55 +1874,27 @@ namespace ZEngine::Hardwares - Rendering::Textures::TextureHandle VulkanDevice::CreateTexture(uint32_t width, uint32_t height) + // Shared by CreateTexture and ReconstructTexture: derives Texture metadata and the + // ImageBuffer's Specification from a TextureSpecification. Does not call Construct(). + static void PopulateTextureResource(const Rendering::Specifications::TextureSpecification& spec, Rendering::Textures::Texture* resource, ImageBuffer* buffer_res, VulkanDevice* device) { - return CreateTexture(width, height, 255, 255, 255, 255); - } - - Rendering::Textures::TextureHandle VulkanDevice::CreateTexture(uint32_t width, uint32_t height, float r, float g, float b, float a) - { - uint32_t byte_per_pixel = Specifications::BytePerChannelMap[VALUE_FROM_SPEC_MAP(Specifications::ImageFormat::R8G8B8A8_SRGB)]; - - Specifications::TextureSpecification spec = { - // clang-format off - .Width = width, - .Height = height, - .BytePerPixel = byte_per_pixel, - .Format = Specifications::ImageFormat::R8G8B8A8_SRGB, - // clang-format on - }; - - auto tex_handle = CreateTexture(spec); - - if (!tex_handle) - { - return Rendering::Textures::TextureHandle{}; - } - - auto scratch = ZGetScratch(Arena); - - size_t data_size = width * height * byte_per_pixel; - Array image_data = {}; - image_data.init(scratch.Arena, data_size, data_size); + resource->Specification = spec; + resource->Width = spec.Width; + resource->Height = spec.Height; + resource->BytePerPixel = spec.BytePerPixel; + resource->BufferSize = spec.Width * spec.Height * spec.BytePerPixel * spec.LayerCount; + resource->IsDepthTexture = (spec.Format == Specifications::ImageFormat::DEPTH_STENCIL_FROM_DEVICE); - unsigned char r_byte = static_cast(std::clamp(r * 255.0f, 0.0f, 255.0f)); - unsigned char g_byte = static_cast(std::clamp(g * 255.0f, 0.0f, 255.0f)); - unsigned char b_byte = static_cast(std::clamp(b * 255.0f, 0.0f, 255.0f)); - unsigned char a_byte = static_cast(std::clamp(a * 255.0f, 0.0f, 255.0f)); + uint32_t storage_bit = spec.IsUsageStorage ? VK_IMAGE_USAGE_STORAGE_BIT : 0; + uint32_t transfert_bit = spec.IsUsageTransfert ? VK_IMAGE_USAGE_TRANSFER_DST_BIT : 0; + uint32_t sampled_bit = spec.IsUsageSampled ? VK_IMAGE_USAGE_SAMPLED_BIT : 0; + uint32_t image_aspect = (spec.Format == Specifications::ImageFormat::DEPTH_STENCIL_FROM_DEVICE) ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT; + uint32_t image_usage_attachment = (spec.Format == Specifications::ImageFormat::DEPTH_STENCIL_FROM_DEVICE) ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; - for (size_t i = 0; i < data_size; i += byte_per_pixel) - { - image_data[i] = r_byte; - image_data[i + 1] = g_byte; - image_data[i + 2] = b_byte; - image_data[i + 3] = a_byte; - } - - if (RRM) - static_cast(RRM)->UploadTextureBuffer(0, 0, tex_handle, image_data.data()); - ZReleaseScratch(scratch); + VkFormat image_format = (spec.Format == Specifications::ImageFormat::DEPTH_STENCIL_FROM_DEVICE) ? device->FindDepthFormat() : Specifications::ImageFormatMap[VALUE_FROM_SPEC_MAP(spec.Format)]; - return tex_handle; + buffer_res->Specification = {.Width = spec.Width, .Height = spec.Height, .BufferUsageType = spec.IsCubemap ? Specifications::ImageBufferUsageType::CUBEMAP : Specifications::ImageBufferUsageType::SINGLE_2D_IMAGE, .ImageFormat = image_format, .ImageAspectFlag = VkImageAspectFlagBits(image_aspect), .LayerCount = spec.LayerCount}; + buffer_res->Specification.ImageUsage = VkImageUsageFlagBits(image_usage_attachment | transfert_bit | sampled_bit | storage_bit); } Rendering::Textures::TextureHandle VulkanDevice::CreateTexture(const Rendering::Specifications::TextureSpecification& spec) @@ -1942,31 +1915,60 @@ namespace ZEngine::Hardwares return Rendering::Textures::TextureHandle{}; } - resource->Specification = spec; - resource->Width = spec.Width; - resource->Height = spec.Height; - resource->BytePerPixel = spec.BytePerPixel; - resource->BufferSize = spec.Width * spec.Height * spec.BytePerPixel * spec.LayerCount; - resource->IsDepthTexture = (spec.Format == Specifications::ImageFormat::DEPTH_STENCIL_FROM_DEVICE); + auto buff_handle = ImageBufferManager.Create(); + auto buffer_res = ImageBufferManager.Access(buff_handle); - uint32_t storage_bit = spec.IsUsageStorage ? VK_IMAGE_USAGE_STORAGE_BIT : 0; - uint32_t transfert_bit = spec.IsUsageTransfert ? VK_IMAGE_USAGE_TRANSFER_DST_BIT : 0; - uint32_t sampled_bit = spec.IsUsageSampled ? VK_IMAGE_USAGE_SAMPLED_BIT : 0; - uint32_t image_aspect = (spec.Format == Specifications::ImageFormat::DEPTH_STENCIL_FROM_DEVICE) ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT; - uint32_t image_usage_attachment = (spec.Format == Specifications::ImageFormat::DEPTH_STENCIL_FROM_DEVICE) ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + PopulateTextureResource(spec, resource, buffer_res, this); + buffer_res->Construct(this); - VkFormat image_format = (spec.Format == Specifications::ImageFormat::DEPTH_STENCIL_FROM_DEVICE) ? FindDepthFormat() : Specifications::ImageFormatMap[VALUE_FROM_SPEC_MAP(spec.Format)]; + resource->BufferHandle = buff_handle; - auto buff_handle = Image2DBufferManager.Create(); - auto buffer_res = Image2DBufferManager.Access(buff_handle); + return handle; + } - buffer_res->Specification = {.Width = spec.Width, .Height = spec.Height, .BufferUsageType = spec.IsCubemap ? Specifications::ImageBufferUsageType::CUBEMAP : Specifications::ImageBufferUsageType::SINGLE_2D_IMAGE, .ImageFormat = image_format, .ImageAspectFlag = VkImageAspectFlagBits(image_aspect), .LayerCount = spec.LayerCount}; - buffer_res->Specification.ImageUsage = VkImageUsageFlagBits(image_usage_attachment | transfert_bit | sampled_bit | storage_bit); + bool VulkanDevice::ReconstructTexture(const Rendering::Textures::TextureHandle& handle, const Rendering::Specifications::TextureSpecification& spec) + { + std::unique_lock l(Mutex); + + auto resource = GlobalTextures.Access(handle); + if (!resource) + { + return false; + } + + auto buffer_res = ImageBufferManager.Access(resource->BufferHandle); + if (!buffer_res) + { + return false; + } + + // Defer-free the old VkImage/VkImageView before overwriting it — Construct() doesn't + // dispose the previous image itself. + DeferredFreeEntry old_image_entry = {}; + old_image_entry.EntryKind = DeferredFreeEntry::Kind::Image; + old_image_entry.Data.Image = buffer_res->GetBuffer(); + DeferFree(old_image_entry); + + PopulateTextureResource(spec, resource, buffer_res, this); buffer_res->Construct(this); - resource->BufferHandle = buff_handle; + return true; + } - return handle; + void VulkanDevice::RequestDescriptorUpdate(const Rendering::Textures::TextureHandle& handle) + { + TextureHandleToUpdates.Enqueue(handle); + } + + void VulkanDevice::DestroyTexture(const Rendering::Textures::TextureHandle& handle) + { + TextureDisposeEntry entry = {}; + entry.Handle = handle; + entry.TimelineValue = SwapchainPtr->RenderTimelineNextValue; + if (!TextureHandleToDispose.push(entry)) + { + ZENGINE_CORE_ERROR("[!] TextureHandleToDispose overflow — texture handle (index {}) leaked", handle.Index) + } } BufferView VulkanDevice::WriteTextureData(CommandBufferPtr command_buf, const Rendering::Textures::TextureHandle& handle, const void* data) @@ -1977,7 +1979,7 @@ namespace ZEngine::Hardwares } auto resource = GlobalTextures.Access(handle); - auto image_buf = Image2DBufferManager.Access(resource->BufferHandle); + auto image_buf = ImageBufferManager.Access(resource->BufferHandle); uint32_t ring_offset = 0; void* ring_ptr = GpuMem.Ring.Allocate(static_cast(resource->BufferSize), 4, &ring_offset); diff --git a/ZEngine/ZEngine/Hardwares/VulkanDevice.h b/ZEngine/ZEngine/Hardwares/VulkanDevice.h index 5543fe83a..d9efb6a3e 100644 --- a/ZEngine/ZEngine/Hardwares/VulkanDevice.h +++ b/ZEngine/ZEngine/Hardwares/VulkanDevice.h @@ -1,6 +1,7 @@ #pragma once #include // clang-format off +#include #include #include #include @@ -64,24 +65,24 @@ namespace ZEngine::Hardwares */ struct VulkanDevice; - struct Image2DBuffer + struct ImageBuffer { - Image2DBuffer() = default; - ~Image2DBuffer(); + ImageBuffer() = default; + ~ImageBuffer(); - Rendering::Specifications::ImageLayout Layout = Rendering::Specifications::ImageLayout::UNDEFINED; - Rendering::Specifications::Image2DBufferSpecification Specification = {}; - VulkanDevice* Device = nullptr; + Rendering::Specifications::ImageLayout Layout = Rendering::Specifications::ImageLayout::UNDEFINED; + Rendering::Specifications::ImageBufferSpecification Specification = {}; + VulkanDevice* Device = nullptr; - void Construct(VulkanDevice* device); + void Construct(VulkanDevice* device); - BufferImage& GetBuffer(); - const BufferImage& GetBuffer() const; - VkImageView GetImageViewHandle() const; - VkImage GetHandle() const; - VkSampler GetSampler() const; - void Dispose(); - VkDescriptorImageInfo& GetDescriptorImageInfo(); + BufferImage& GetBuffer(); + const BufferImage& GetBuffer() const; + VkImageView GetImageViewHandle() const; + VkImage GetHandle() const; + VkSampler GetSampler() const; + void Dispose(); + VkDescriptorImageInfo& GetDescriptorImageInfo(); private: BufferImage m_buffer_image; @@ -94,6 +95,17 @@ namespace ZEngine::Hardwares VkQueue Handle{VK_NULL_HANDLE}; }; + /// @brief Element of TextureHandleToDispose; TimelineValue gates Present()'s drain. + struct TextureDisposeEntry + { + Rendering::Textures::TextureHandle Handle = {}; + uint64_t TimelineValue = 0; + }; + + /// @brief Lock-free SPSC ring for TextureHandleToDispose — producer and consumer are + /// both render-thread only, so no mutex is needed. + using TextureDisposeQueue = Core::Containers::SPSCQueue; + /* * Command Buffer definition */ @@ -257,7 +269,7 @@ namespace ZEngine::Hardwares uint32_t TransferFamilyIndex = std::numeric_limits::max(); uint32_t WriteDescriptorSetIndex = 0; - uint32_t MaxGlobalTexture = 1024; + uint32_t MaxGlobalTexture = 8192; VkInstance Instance = VK_NULL_HANDLE; VkSurfaceKHR Surface = VK_NULL_HANDLE; VkSurfaceFormatKHR SurfaceFormat = {}; @@ -290,9 +302,9 @@ namespace ZEngine::Hardwares std::set BindlessTextureSlotRequests = {}; std::unordered_set ShaderReservedBindingSets = {}; Rendering::Textures::TextureHandleManager GlobalTextures = {}; - Helpers::HandleManager Image2DBufferManager = {}; + Helpers::HandleManager ImageBufferManager = {}; Helpers::ThreadSafeQueue TextureHandleToUpdates = {}; - Helpers::ThreadSafeQueue TextureHandleToDispose = {}; + TextureDisposeQueue TextureHandleToDispose = {}; Helpers::ThreadSafeQueue AsyncGPUOperations = {}; Helpers::HandleManager ShaderManager = {}; std::mutex Mutex = {}; @@ -324,9 +336,18 @@ namespace ZEngine::Hardwares Helpers::Handle CompileShader(Rendering::Specifications::ShaderSpecification& spec); - Rendering::Textures::TextureHandle CreateTexture(uint32_t width, uint32_t height); - Rendering::Textures::TextureHandle CreateTexture(uint32_t width, uint32_t height, float r = 255, float g = 255, float b = 255, float a = 255); Rendering::Textures::TextureHandle CreateTexture(const Rendering::Specifications::TextureSpecification& spec); + + /// @brief In-place resize/format change: same handle, same slot, same bindless index. + /// @return false if handle is not live. + bool ReconstructTexture(const Rendering::Textures::TextureHandle& handle, const Rendering::Specifications::TextureSpecification& spec); + + /// @brief Dirty this handle's bindless descriptor for the next Present() to refresh. + void RequestDescriptorUpdate(const Rendering::Textures::TextureHandle& handle); + + /// @brief Timeline-gated disposal. Render-thread only. + void DestroyTexture(const Rendering::Textures::TextureHandle& handle); + BufferView WriteTextureData(CommandBufferPtr command_buf, const Rendering::Textures::TextureHandle& handle, const void* data); Rendering::Renderers::RenderPasses::RenderPass* CreateRenderPass(Rendering::Specifications::RenderPassSpecification spec); @@ -346,7 +367,7 @@ namespace ZEngine::Hardwares namespace ZEngine::Helpers { template <> - inline void HandleManager::Dispose() + inline void HandleManager::Dispose() { for (size_t i = 0; i < m_count; ++i) { diff --git a/ZEngine/ZEngine/Importers/TextureImporter.cpp b/ZEngine/ZEngine/Importers/TextureImporter.cpp new file mode 100644 index 000000000..ce35a18f3 --- /dev/null +++ b/ZEngine/ZEngine/Importers/TextureImporter.cpp @@ -0,0 +1,50 @@ +#include +#include +#include +#include + +// stb_image implementation is defined once in RenderResourceManager.cpp. +#include + +namespace ZEngine::Importers +{ + void TextureImporter::Initialize(Core::Memory::ArenaAllocator* arena) + { + arena->CreateSubArena(ZKilo(512), &Arena); + } + + bool TextureImporter::CanImport(const char* extension) const + { + if (!extension) + return false; + return Helpers::secure_strcmp(extension, "png") == 0 || Helpers::secure_strcmp(extension, "jpg") == 0 || Helpers::secure_strcmp(extension, "jpeg") == 0 || Helpers::secure_strcmp(extension, "bmp") == 0 || Helpers::secure_strcmp(extension, "tga") == 0 || Helpers::secure_strcmp(extension, "gif") == 0 || Helpers::secure_strcmp(extension, "psd") == 0 || Helpers::secure_strcmp(extension, "pic") == 0; + } + + Core::VFS::VFSResult TextureImporter::Import(Core::VFS::IVFSContext& ctx, const Core::VFS::VFSPath& path, const Core::VFS::MetaFileData& meta) + { + (void) ctx; + + // Resolve to native path — stbi_info works on the filesystem, not the VFS. + char native[MAX_FILE_PATH_COUNT] = {}; + path.ToNative(native, sizeof(native)); + + int w = 0, h = 0, ch = 0; + if (!stbi_info(native, &w, &h, &ch)) + { + ZENGINE_CORE_ERROR("TextureImporter: failed to probe '{}': {}", native, stbi_failure_reason()) + return Core::VFS::VFSResult::Fail(Core::VFS::VFSError::IOError); + } + + // AssetManager::IngestTexture resolves against CurrentWorkingSpacePath itself, so it + // takes the project-relative VFS path, not the native one probed above. + Core::Containers::String vfs_path = {}; + vfs_path.init(&Arena, path.CStr()); + Managers::AssetManager::IngestTexture(meta.AssetUUID, vfs_path); + + // IngestTexture copies the path into its own arena before returning, so vfs_path + // doesn't need to survive past this point — reclaim it so Arena is reused, not + // consumed, across repeated Import() calls (matches AssimpImporter/GltfImporter). + Arena.Clear(); + return Core::VFS::VFSResult::Ok(); + } +} // namespace ZEngine::Importers diff --git a/ZEngine/ZEngine/Importers/TextureImporter.h b/ZEngine/ZEngine/Importers/TextureImporter.h new file mode 100644 index 000000000..cb320001a --- /dev/null +++ b/ZEngine/ZEngine/Importers/TextureImporter.h @@ -0,0 +1,23 @@ +#pragma once +#include + +namespace ZEngine::Importers +{ + /// @brief Imports flat 2D raster textures (png/jpg/jpeg/bmp/tga/gif/psd/pic). + /// @details Does not claim hdr/exr (EnvironmentMapImporter's domain) or ktx/ktx2 + /// (not decodable by stb_image today). + class TextureImporter : public IAssetImporter + { + public: + TextureImporter() = default; + ~TextureImporter() = default; + + void Initialize(Core::Memory::ArenaAllocator* arena); + + // IAssetImporter + bool CanImport(const char* extension) const override; + Core::VFS::VFSResult Import(Core::VFS::IVFSContext& ctx, const Core::VFS::VFSPath& path, const Core::VFS::MetaFileData& meta) override; + + Core::Memory::ArenaAllocator Arena = {}; + }; +} // namespace ZEngine::Importers diff --git a/ZEngine/ZEngine/Managers/AssetManager.cpp b/ZEngine/ZEngine/Managers/AssetManager.cpp index 229534974..b75e65f72 100644 --- a/ZEngine/ZEngine/Managers/AssetManager.cpp +++ b/ZEngine/ZEngine/Managers/AssetManager.cpp @@ -198,7 +198,14 @@ namespace ZEngine::Managers // Use UUIDToTextureHandle (not IsRegistered): VFSScanner pre-registers texture // UUIDs without uploading them, so IsRegistered gives a false positive. if (auto* h = s_Instance->UUIDToTextureHandle.find(uuid)) + { + // Already known — this is a reimport signal (e.g. TextureImporter after a file + // edit). The handle value doesn't change: RRM's reload path reconstructs the + // existing GPU resource in place. Ask RRM to do that on the render thread. + if (s_Instance->Device && s_Instance->Device->RRM) + static_cast(s_Instance->Device->RRM)->ScheduleTextureReload(uuid); return *h; + } auto slot = static_cast(s_Instance->Textures.size()); auto& new_tex = s_Instance->Textures.push_use({}); @@ -263,24 +270,91 @@ namespace ZEngine::Managers gpu_mat.AmbientColor = mat.AmbientColor; gpu_mat.Factors = mat.Factors; - // Resolve handle for a texture slot: UUID lookup first, then fall back to uploading - // from the stored path — handles scene-reload and dragged-.zmesh cases where - // IngestTextures may not have run yet for this material's textures. - auto tex_handle = [&](const uuids::uuid& id, const Core::Containers::String& path) -> uint32_t { - if (id.is_nil()) - return INVALID_MAP_HANDLE; - auto* h = s_Instance->UUIDToTextureHandle.find(id); - if (h) - return h->Index; - if (!path.empty()) - return IngestTexture(id, path).Index; + // Resolve handle for each texture slot: UUID lookup first, then fall back to + // uploading from the stored path — handles scene-reload and dragged-.zmesh cases + // where IngestTextures may not have run yet for this material's textures. + gpu_mat.AlbedoMap = ResolveTextureMapIndex(mat.AlbedoTexUUID, mat.AlbedoTexPath); + gpu_mat.EmissiveMap = ResolveTextureMapIndex(mat.EmissiveTexUUID, mat.EmissiveTexPath); + gpu_mat.NormalMap = ResolveTextureMapIndex(mat.NormalTexUUID, mat.NormalTexPath); + gpu_mat.OpacityMap = ResolveTextureMapIndex(mat.OpacityTexUUID, mat.OpacityTexPath); + gpu_mat.SpecularMap = ResolveTextureMapIndex(mat.SpecularTexUUID, mat.SpecularTexPath); + } + + uint32_t AssetManager::ResolveTextureMapIndex(const uuids::uuid& id, const Core::Containers::String& path) + { + if (!s_Instance || id.is_nil()) return INVALID_MAP_HANDLE; - }; - gpu_mat.AlbedoMap = tex_handle(mat.AlbedoTexUUID, mat.AlbedoTexPath); - gpu_mat.EmissiveMap = tex_handle(mat.EmissiveTexUUID, mat.EmissiveTexPath); - gpu_mat.NormalMap = tex_handle(mat.NormalTexUUID, mat.NormalTexPath); - gpu_mat.OpacityMap = tex_handle(mat.OpacityTexUUID, mat.OpacityTexPath); - gpu_mat.SpecularMap = tex_handle(mat.SpecularTexUUID, mat.SpecularTexPath); + auto* h = s_Instance->UUIDToTextureHandle.find(id); + if (h) + return h->Index; + if (!path.empty()) + return IngestTexture(id, path).Index; + return INVALID_MAP_HANDLE; + } + + Rendering::Textures::TextureHandle AssetManager::FindTextureHandle(const uuids::uuid& uuid) + { + if (!s_Instance) + return {}; + std::lock_guard lock(s_Instance->IngestMutex); + auto* h = s_Instance->UUIDToTextureHandle.find(uuid); + return h ? *h : Rendering::Textures::TextureHandle{}; + } + + void AssetManager::ReleaseTexture(const uuids::uuid& uuid) + { + if (!s_Instance) + return; + std::lock_guard lock(s_Instance->PendingTextureReleaseMutex); + if (s_Instance->PendingTextureReleaseCount >= MAX_PENDING_TEXTURE_RELEASES) + { + ZENGINE_LOG_ASSET_WARN("[AssetManager] Pending texture release queue full — dropping release for {}", uuids::to_string(uuid)) + return; + } + s_Instance->PendingTextureReleases[s_Instance->PendingTextureReleaseCount++] = uuid; + } + + void AssetManager::FlushTextureReleases() + { + if (!s_Instance) + return; + + uuids::uuid local[MAX_PENDING_TEXTURE_RELEASES]; + uint32_t count = 0; + { + std::lock_guard lock(s_Instance->PendingTextureReleaseMutex); + count = s_Instance->PendingTextureReleaseCount; + Helpers::secure_memcpy(local, sizeof(local), s_Instance->PendingTextureReleases, count * sizeof(local[0])); + s_Instance->PendingTextureReleaseCount = 0; + } + if (count == 0) + return; + + // IngestMutex, not just PendingTextureReleaseMutex: Materials/GPUMeshMaterials are + // unsynchronized and also written by IngestMaterial/IngestTexture under this lock. + std::lock_guard lock(s_Instance->IngestMutex); + for (uint32_t r = 0; r < count; ++r) + { + s_Instance->UUIDToTextureHandle.remove(local[r]); + + // Sentinel set directly, not via ResolveTextureMapIndex — its path fallback + // would re-ingest (undo) the release for any material with a stored path. + for (uint32_t i = 0; i < s_Instance->Materials.size(); ++i) + { + auto& mat = s_Instance->Materials[i]; + auto& gpu_mat = s_Instance->GPUMeshMaterials[i]; + if (mat.AlbedoTexUUID == local[r]) + gpu_mat.AlbedoMap = INVALID_MAP_HANDLE; + if (mat.EmissiveTexUUID == local[r]) + gpu_mat.EmissiveMap = INVALID_MAP_HANDLE; + if (mat.NormalTexUUID == local[r]) + gpu_mat.NormalMap = INVALID_MAP_HANDLE; + if (mat.OpacityTexUUID == local[r]) + gpu_mat.OpacityMap = INVALID_MAP_HANDLE; + if (mat.SpecularTexUUID == local[r]) + gpu_mat.SpecularMap = INVALID_MAP_HANDLE; + } + } } Importers::AssetMesh* AssetManager::GetMeshAsset(const uuids::uuid& id) @@ -338,6 +412,25 @@ namespace ZEngine::Managers } } + // Textures — ingest each raster texture that has not been ingested yet. + { + auto result = s_Instance->Registry->Query({.Type = AssetType::TEXTURE}, scratch); + for (uint32_t i = 0; i < result.Handles.size(); ++i) + { + auto* rec = s_Instance->Registry->Access(result.Handles[i]); + if (!rec || rec->UUID.is_nil()) + continue; + if (s_Instance->UUIDToTextureHandle.find(rec->UUID) != nullptr) + continue; + + Core::Containers::String rel_path = {}; + rel_path.init(scratch, rec->Path.CStr()); + + ZENGINE_LOG_ASSET_INFO("Reloading texture from disk: {}", rec->Path.CStr()) + IngestTexture(rec->UUID, rel_path); + } + } + // Meshes — deserialize each .zemesh that has not been ingested yet. { auto result = s_Instance->Registry->Query({.Type = AssetType::MESH}, scratch); diff --git a/ZEngine/ZEngine/Managers/AssetManager.h b/ZEngine/ZEngine/Managers/AssetManager.h index 334b07f29..3a863d608 100644 --- a/ZEngine/ZEngine/Managers/AssetManager.h +++ b/ZEngine/ZEngine/Managers/AssetManager.h @@ -52,6 +52,13 @@ namespace ZEngine::Managers // Recursive so IngestMaterial can call IngestTexture while holding the lock. mutable std::recursive_mutex IngestMutex; + // Pending texture releases — written from any thread via ReleaseTexture, drained by + // FlushTextureReleases on the render thread. + static constexpr uint32_t MAX_PENDING_TEXTURE_RELEASES = 256; + uuids::uuid PendingTextureReleases[MAX_PENDING_TEXTURE_RELEASES] = {}; + uint32_t PendingTextureReleaseCount = 0; + std::mutex PendingTextureReleaseMutex; + Hardwares::VulkanDevice* Device = nullptr; ::ZEngine::Core::VFS::AssetRegistry* Registry = nullptr; @@ -81,6 +88,20 @@ namespace ZEngine::Managers static void IngestTextures(Core::Containers::Array&& textures); static void IngestMaterial(Importers::AssetMaterial&& material); + /// @brief Thread-safe lookup of a texture's current handle by UUID. + static Rendering::Textures::TextureHandle FindTextureHandle(const uuids::uuid& uuid); + + /// @brief Resolve a material's texture map field to a bindless index. + /// @details UUID lookup first, else ingest from path, else INVALID_MAP_HANDLE. + static uint32_t ResolveTextureMapIndex(const uuids::uuid& id, const Core::Containers::String& path); + + /// @brief Thread-safe enqueue: patches every referencing material to the sentinel + /// once FlushTextureReleases drains it. + static void ReleaseTexture(const uuids::uuid& uuid); + + /// @brief Render-thread drain of ReleaseTexture's queue. + static void FlushTextureReleases(); + static uuids::uuid GetOrCreateUUID(Core::VFS::IVFSContext& ctx, const Core::VFS::VFSPath& asset_path, const char* importer_name); // Reload all .zemesh and .zematerial assets already registered by the VFSScanner diff --git a/ZEngine/ZEngine/Rendering/Buffers/FrameBuffer.cpp b/ZEngine/ZEngine/Rendering/Buffers/FrameBuffer.cpp index efa0a00d0..983e5050f 100644 --- a/ZEngine/ZEngine/Rendering/Buffers/FrameBuffer.cpp +++ b/ZEngine/ZEngine/Rendering/Buffers/FrameBuffer.cpp @@ -52,7 +52,7 @@ namespace ZEngine::Rendering::Buffers auto handle = m_device->GlobalTextures.ToHandle(index); auto resource = m_device->GlobalTextures.Access(handle); - auto img_buf = m_device->Image2DBufferManager.Access(resource->BufferHandle); + auto img_buf = m_device->ImageBufferManager.Access(resource->BufferHandle); views[i] = img_buf->GetImageViewHandle(); } Handle = m_device->CreateFramebuffer(views, m_specification.Attachment->GetHandle(), m_specification.Width, m_specification.Height, m_specification.Layers); diff --git a/ZEngine/ZEngine/Rendering/RenderHandle.h b/ZEngine/ZEngine/Rendering/RenderHandle.h index af882989c..bbad17a00 100644 --- a/ZEngine/ZEngine/Rendering/RenderHandle.h +++ b/ZEngine/ZEngine/Rendering/RenderHandle.h @@ -25,9 +25,6 @@ namespace ZEngine::Rendering struct BufferTag { }; - struct ImageTag - { - }; struct SamplerTag { }; @@ -36,7 +33,6 @@ namespace ZEngine::Rendering }; using BufferHandle = RenderHandle; - using ImageHandle = RenderHandle; using SamplerHandle = RenderHandle; using PipelineHandle = RenderHandle; diff --git a/ZEngine/ZEngine/Rendering/RenderResourceManager.cpp b/ZEngine/ZEngine/Rendering/RenderResourceManager.cpp index e24a9ada8..7be7aa3d0 100644 --- a/ZEngine/ZEngine/Rendering/RenderResourceManager.cpp +++ b/ZEngine/ZEngine/Rendering/RenderResourceManager.cpp @@ -64,40 +64,20 @@ namespace ZEngine::Rendering auto* rrm = static_cast(ctx); const AssetRecord* rec = rrm->m_registry->FindByUUID(uuid); - if (!rec) - return; - - UploadKind kind; - if (rec->Type == AssetType::MESH) - kind = UploadKind::Mesh; - else if (rec->Type == AssetType::TEXTURE) - kind = UploadKind::Texture; - else - return; + if (!rec || rec->Type != AssetType::MESH) + return; // textures are ingested directly by AssetManager::IngestTexture, not via this path // Deduplicate: hold both locks together so two concurrent callbacks // for the same UUID can't both pass the check before either pushes. std::lock_guard map_lock(rrm->m_uuid_map_mutex); std::lock_guard pend_lock(rrm->m_pending_mutex); - if (kind == UploadKind::Mesh) - { - for (uint32_t i = 0; i < rrm->m_uuid_to_buffer_count; ++i) - if (rrm->m_uuid_to_buffer[i].UUID == uuid) - return; - for (uint32_t i = 0; i < rrm->m_pending_count; ++i) - if (rrm->m_pending[i].Kind == UploadKind::Mesh && rrm->m_pending[i].UUID == uuid) - return; - } - else - { - for (uint32_t i = 0; i < rrm->m_uuid_to_image_count; ++i) - if (rrm->m_uuid_to_image[i].UUID == uuid) - return; - for (uint32_t i = 0; i < rrm->m_pending_count; ++i) - if (rrm->m_pending[i].Kind == UploadKind::Texture && rrm->m_pending[i].UUID == uuid) - return; - } + for (uint32_t i = 0; i < rrm->m_uuid_to_buffer_count; ++i) + if (rrm->m_uuid_to_buffer[i].UUID == uuid) + return; + for (uint32_t i = 0; i < rrm->m_pending_count; ++i) + if (rrm->m_pending[i].UUID == uuid) + return; if (rrm->m_pending_count >= MAX_PENDING) { @@ -105,13 +85,14 @@ namespace ZEngine::Rendering return; } - rrm->m_pending[rrm->m_pending_count++] = {kind, handle, uuid}; + rrm->m_pending[rrm->m_pending_count++] = {handle, uuid}; }); registry->SetOnStaleCallback(this, [](void* ctx, const uuids::uuid& uuid) { auto* rrm = static_cast(ctx); - // Look up current GPU handle for this UUID and schedule a swap. + // Look up current GPU handle for this UUID and schedule a swap. Mesh/buffer only — + // texture hot-reload is triggered via TextureImporter/ImportCoordinator instead. std::lock_guard lock(rrm->m_uuid_map_mutex); for (uint32_t i = 0; i < rrm->m_uuid_to_buffer_count; ++i) { @@ -127,20 +108,12 @@ namespace ZEngine::Rendering return; } } - for (uint32_t i = 0; i < rrm->m_uuid_to_image_count; ++i) - { - if (rrm->m_uuid_to_image[i].UUID == uuid) - { - AssetHandle new_asset = 0; - { - const AssetRecord* rec = rrm->m_registry->FindByUUID(uuid); - if (rec) - new_asset = rec->SlotHandle; - } - rrm->ScheduleSwap(rrm->m_uuid_to_image[i].Handle, new_asset); - return; - } - } + }); + + registry->SetOnRemovedCallback(this, [](void* ctx, const uuids::uuid& uuid, AssetType type) { + if (type != AssetType::TEXTURE) + return; + static_cast(ctx)->ReleaseTexture(uuid); }); } @@ -216,12 +189,6 @@ namespace ZEngine::Rendering if (m_global_index_buf) m_device->GpuMem.FreeBuffer(m_global_index_buf); - for (uint32_t i = 0; i < m_image_slot_count; ++i) - { - if (m_image_slots[i].Generation != 0 && m_image_slots[i].Data) - m_device->GpuMem.FreeImage(m_image_slots[i].Data, m_device->LogicalDevice); - } - for (uint32_t i = 0; i < m_gbuf_slot_count; ++i) { if (m_gbuf_slots[i].Generation != 0 && m_gbuf_slots[i].Data) @@ -236,6 +203,8 @@ namespace ZEngine::Rendering { FlushPendingUploads(frame_index); FlushPendingSwaps(frame_index); + FlushPendingTextureReloads(); + FlushPendingTextureReleases(); } void RenderResourceManager::EndFrame(uint32_t frame_index) @@ -261,38 +230,7 @@ namespace ZEngine::Rendering for (uint32_t i = 0; i < count; ++i) { const PendingSwap& s = local[i]; - if (s.Kind == SwapKind::Image) - { - BufferImage* old_slot = GetImageMutable(s.OldImage); - if (!old_slot) - { - continue; // stale handle — asset was released before the swap could apply - } - ImageHandle new_handle = DoUploadTexture(s.NewAsset); - if (!new_handle.IsValid()) - { - ZENGINE_LOG_RENDER_ERR("[RRM] Hot-reload swap failed to re-upload texture — old image left in place") - continue; - } - BufferImage* new_slot = GetImageMutable(new_handle); - if (!new_slot) - { - continue; - } - - DeferredFreeEntry entry; - entry.EntryKind = DeferredFreeEntry::Kind::Image; - entry.TimelineValue = m_device->SwapchainPtr->RenderTimelineNextValue; - entry.Data.Image = *old_slot; - m_device->DeferFree(entry); - - *old_slot = *new_slot; - - // The scratch slot's data now lives in old_slot — release it. - *new_slot = {}; - m_image_slots[new_handle.Index].Generation = 0; - } - else if (s.OldBuffer.Generation & GBUF_GEN_TAG) + if (s.OldBuffer.Generation & GBUF_GEN_TAG) { // No AssetHandle-driven re-upload path exists for generic buffers today — // every live ScheduleSwap(BufferHandle,...) call carries a mesh handle. @@ -510,23 +448,6 @@ namespace ZEngine::Rendering return {slot, m_mesh_slots[slot].Generation}; } - ImageHandle RenderResourceManager::DoUploadTexture(AssetHandle asset) - { - AssetTexture* tex = AssetManager::GetAsset(asset); - if (!tex || tex->Path.empty()) - return {}; - - if (!tex->Handle.Valid()) - return {}; - - // Texture is already on GPU (via SubmitTextureFile/UploadTextureBuffer); register its - // slot. AllocImageSlot already assigned Generation — we don't own a BufferImage for - // this path yet, so the slot's Data stays a sentinel. - uint32_t slot_idx = AllocImageSlot(); - - return {slot_idx, m_image_slots[slot_idx].Generation}; - } - void RenderResourceManager::FlushPendingUploads(uint32_t frame_index) { // Compact geometry buffers if a scene reload was requested @@ -544,50 +465,23 @@ namespace ZEngine::Rendering if (count == 0) return; - // Partition into mesh and texture uploads - PendingUpload mesh_local[MAX_PENDING]; - PendingUpload tex_local[MAX_PENDING]; - uint32_t mesh_count = 0, tex_count = 0; - for (uint32_t i = 0; i < count; ++i) - { - if (local[i].Kind == UploadKind::Mesh) - mesh_local[mesh_count++] = local[i]; - else - tex_local[tex_count++] = local[i]; - } - // Batch all mesh uploads into one GPU command buffer submission - if (mesh_count > 0) - { - BeginBatchUpload(); - for (uint32_t i = 0; i < mesh_count; ++i) - { - BufferHandle h = DoUploadMesh(mesh_local[i].Asset, frame_index); - if (h.IsValid()) - { - std::lock_guard lock(m_uuid_map_mutex); - if (m_uuid_to_buffer_count < MAX_UUID_MAP) - m_uuid_to_buffer[m_uuid_to_buffer_count++] = {mesh_local[i].UUID, h}; - } - else - { - ZENGINE_CORE_ERROR("[RRM] Mesh upload failed for asset handle {}", mesh_local[i].Asset) - } - } - EndBatchUpload(); - } - - // Texture uploads: per-texture path (independent submission chain) - for (uint32_t i = 0; i < tex_count; ++i) + BeginBatchUpload(); + for (uint32_t i = 0; i < count; ++i) { - ImageHandle h = DoUploadTexture(tex_local[i].Asset); + BufferHandle h = DoUploadMesh(local[i].Asset, frame_index); if (h.IsValid()) { std::lock_guard lock(m_uuid_map_mutex); - if (m_uuid_to_image_count < MAX_UUID_MAP) - m_uuid_to_image[m_uuid_to_image_count++] = {tex_local[i].UUID, h}; + if (m_uuid_to_buffer_count < MAX_UUID_MAP) + m_uuid_to_buffer[m_uuid_to_buffer_count++] = {local[i].UUID, h}; + } + else + { + ZENGINE_CORE_ERROR("[RRM] Mesh upload failed for asset handle {}", local[i].Asset) } } + EndBatchUpload(); } void RenderResourceManager::UpdateBuffer(BufferView& dst, const void* data, size_t byte_size, uint32_t dst_offset) @@ -660,11 +554,6 @@ namespace ZEngine::Rendering return DoUploadMesh(asset, m_current_frame); } - ImageHandle RenderResourceManager::UploadTexture(AssetHandle asset) - { - return DoUploadTexture(asset); - } - void RenderResourceManager::ScheduleSwap(BufferHandle old_handle, AssetHandle new_asset) { if (!old_handle.IsValid()) @@ -678,29 +567,10 @@ namespace ZEngine::Rendering return; } PendingSwap& s = m_pending_swaps[m_pending_swap_count++]; - s.Kind = SwapKind::Buffer; s.OldBuffer = old_handle; s.NewAsset = new_asset; } - void RenderResourceManager::ScheduleSwap(ImageHandle old_handle, AssetHandle new_asset) - { - if (!old_handle.IsValid()) - { - return; - } - std::lock_guard lock(m_pending_swap_mutex); - if (m_pending_swap_count >= MAX_PENDING) - { - ZENGINE_LOG_RENDER_WARN("[RRM] Pending swap queue full — dropping hot-reload swap") - return; - } - PendingSwap& s = m_pending_swaps[m_pending_swap_count++]; - s.Kind = SwapKind::Image; - s.OldImage = old_handle; - s.NewAsset = new_asset; - } - bool RenderResourceManager::GetMeshOffsets(BufferHandle handle, uint32_t& vtx_offset, uint32_t& idx_offset) const { if (!handle.IsValid() || handle.Index >= m_mesh_slot_count) @@ -776,36 +646,9 @@ namespace ZEngine::Rendering } } - void RenderResourceManager::Release(ImageHandle handle) + const Rendering::Textures::Texture* RenderResourceManager::GetTexture(const Rendering::Textures::TextureHandle& handle) const { - BufferImage* slot = GetImageMutable(handle); - if (!slot) - return; - - DeferredFreeEntry e; - e.EntryKind = DeferredFreeEntry::Kind::Image; - e.TimelineValue = m_device->SwapchainPtr->RenderTimelineNextValue; - e.Data.Image = *slot; - m_device->DeferFree(e); - - *slot = {}; - m_image_slots[handle.Index].Generation = 0; - } - - const BufferImage* RenderResourceManager::GetImage(ImageHandle handle) const - { - if (!handle.IsValid() || handle.Index >= m_image_slot_count) - return nullptr; - const auto& slot = m_image_slots[handle.Index]; - return slot.Generation == handle.Generation ? &slot.Data : nullptr; - } - - BufferImage* RenderResourceManager::GetImageMutable(ImageHandle handle) - { - if (!handle.IsValid() || handle.Index >= m_image_slot_count) - return nullptr; - auto& slot = m_image_slots[handle.Index]; - return slot.Generation == handle.Generation ? &slot.Data : nullptr; + return m_device->GlobalTextures.Access(handle); } void RenderResourceManager::EnqueueDeletion(DeferredFreeEntry entry) @@ -880,22 +723,6 @@ namespace ZEngine::Rendering return idx; } - uint32_t RenderResourceManager::AllocImageSlot() - { - for (uint32_t i = 0; i < m_image_slot_count; ++i) - { - if (m_image_slots[i].Generation == 0) - { - m_image_slots[i].Generation = ++m_image_slot_gen_counter[i]; - return i; - } - } - ZENGINE_VALIDATE_ASSERT(m_image_slot_count < MAX_IMAGES, "RRM: MAX_IMAGES exceeded") - uint32_t idx = m_image_slot_count++; - m_image_slots[idx].Generation = ++m_image_slot_gen_counter[idx]; - return idx; - } - // Masks the monotonic counter to 31 bits before OR-ing in GBUF_GEN_TAG (bit 31) so the // counter can never collide with the tag, and skips 0 on the (practically unreachable) // wraparound since Generation == 0 is the universal free/invalid sentinel. @@ -1053,7 +880,7 @@ namespace ZEngine::Rendering uint32_t pool_index = (frame_index * m_device->CommandBufferMgr->TotalThreadCount) + thread_index; auto texture = m_device->GlobalTextures.Access(handle); - auto img_buf = m_device->Image2DBufferManager.Access(texture->BufferHandle); + auto img_buf = m_device->ImageBufferManager.Access(texture->BufferHandle); auto img_buf_aspect = (texture->Specification.Format == ImageFormat::DEPTH_STENCIL_FROM_DEVICE) ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT; auto buffer_handle = img_buf->GetHandle(); @@ -1206,7 +1033,7 @@ namespace ZEngine::Rendering auto handle = m_device->CreateTexture(spec); auto texture = m_device->GlobalTextures.Access(handle); - auto img_buf = m_device->Image2DBufferManager.Access(texture->BufferHandle); + auto img_buf = m_device->ImageBufferManager.Access(texture->BufferHandle); auto buffer_handle = img_buf->GetHandle(); ImageMemoryBarrierSpecification to_transfer = {}; @@ -1373,7 +1200,94 @@ namespace ZEngine::Rendering } } - Rendering::Textures::TextureHandle RenderResourceManager::SubmitTextureFile(uint8_t frame_index, uint8_t thread_index, const char* filename) + Rendering::Textures::TextureHandle RenderResourceManager::IngestTexture(const uuids::uuid& uuid, const char* absolute_path, Rendering::Textures::TextureHandle existing) + { + ZENGINE_LOG_RENDER_INFO("[RRM] {} texture {} from {}", existing.Valid() ? "Reloading" : "Ingesting", uuids::to_string(uuid), absolute_path) + return SubmitTextureFile(0, 0, absolute_path, existing); + } + + void RenderResourceManager::ScheduleTextureReload(const uuids::uuid& uuid) + { + std::lock_guard lock(m_pending_texture_reload_mutex); + for (uint32_t i = 0; i < m_pending_texture_reload_count; ++i) + if (m_pending_texture_reloads[i] == uuid) + return; // already pending — dedupe + if (m_pending_texture_reload_count >= MAX_PENDING) + { + ZENGINE_LOG_RENDER_WARN("[RRM] Pending texture reload queue full — dropping reload for {}", uuids::to_string(uuid)) + return; + } + m_pending_texture_reloads[m_pending_texture_reload_count++] = uuid; + } + + void RenderResourceManager::FlushPendingTextureReloads() + { + uint32_t count = 0; + uuids::uuid local[MAX_PENDING]; + { + std::lock_guard lock(m_pending_texture_reload_mutex); + count = m_pending_texture_reload_count; + secure_memcpy(local, sizeof(local), m_pending_texture_reloads, count * sizeof(local[0])); + m_pending_texture_reload_count = 0; + } + + for (uint32_t i = 0; i < count; ++i) + { + Rendering::Textures::TextureHandle existing = AssetManager::FindTextureHandle(local[i]); + if (!existing.Valid()) + continue; + + // IngestMutex guards the read: AssetManager::Textures (unlike Meshes/Materials) + // is arena-backed, so a concurrent IngestTexture on the import thread can + // reallocate its backing storage mid-read without this lock. + char full_path[MAX_FILE_PATH_COUNT] = {}; + { + std::lock_guard lock(AssetManager::Instance()->IngestMutex); + AssetTexture* tex = AssetManager::GetAsset(local[i]); + if (!tex || tex->Path.empty()) + continue; + snprintf(full_path, sizeof(full_path), "%s%c%s", AssetManager::Instance()->CurrentWorkingSpacePath, PLATFORM_OS_BACKSLASH, tex->Path.c_str()); + } + IngestTexture(local[i], full_path, existing); + } + } + + void RenderResourceManager::ReleaseTexture(const uuids::uuid& uuid) + { + // Captured before AssetManager::ReleaseTexture's deferred patch runs — that patch + // erases the UUID→handle map entry, so the handle must be read now or it's lost. + Rendering::Textures::TextureHandle handle = AssetManager::FindTextureHandle(uuid); + + AssetManager::ReleaseTexture(uuid); + + if (!handle.Valid()) + return; + + std::lock_guard lock(m_pending_texture_release_mutex); + if (m_pending_texture_release_count >= MAX_PENDING) + { + ZENGINE_LOG_RENDER_ERR("[RRM] Pending texture release queue full — texture handle (index {}) leaked", handle.Index) + return; + } + m_pending_texture_releases[m_pending_texture_release_count++] = handle; + } + + void RenderResourceManager::FlushPendingTextureReleases() + { + uint32_t count = 0; + Rendering::Textures::TextureHandle local[MAX_PENDING]; + { + std::lock_guard lock(m_pending_texture_release_mutex); + count = m_pending_texture_release_count; + secure_memcpy(local, sizeof(local), m_pending_texture_releases, count * sizeof(local[0])); + m_pending_texture_release_count = 0; + } + + for (uint32_t i = 0; i < count; ++i) + m_device->DestroyTexture(local[i]); + } + + Rendering::Textures::TextureHandle RenderResourceManager::SubmitTextureFile(uint8_t frame_index, uint8_t thread_index, const char* filename, Rendering::Textures::TextureHandle existing) { using namespace Rendering::Specifications; @@ -1420,8 +1334,22 @@ namespace ZEngine::Rendering } } - spec.BytePerPixel = Specifications::BytePerChannelMap[VALUE_FROM_SPEC_MAP(spec.Format)]; - auto tex_handle = m_device->CreateTexture(spec); + spec.BytePerPixel = Specifications::BytePerChannelMap[VALUE_FROM_SPEC_MAP(spec.Format)]; + + Rendering::Textures::TextureHandle tex_handle; + if (existing.Valid()) + { + // Reimport — reconstruct in place only if dimensions/format actually changed; + // same handle, same bindless index either way. + auto* texture = m_device->GlobalTextures.Access(existing); + if (texture && (texture->Width != spec.Width || texture->Height != spec.Height || texture->Specification.Format != spec.Format)) + m_device->ReconstructTexture(existing, spec); + tex_handle = existing; + } + else + { + tex_handle = m_device->CreateTexture(spec); + } // Capture everything by value for the thread pool lambda. std::string captured_filename = abs_filename; @@ -1536,7 +1464,7 @@ namespace ZEngine::Rendering deferral.ThreadIdx = ti; deferral.TexHandle = captured_handle; EnqueueTextureDeferral(std::move(deferral)); - m_device->TextureHandleToUpdates.Enqueue(captured_handle); + m_device->RequestDescriptorUpdate(captured_handle); }); return tex_handle; diff --git a/ZEngine/ZEngine/Rendering/RenderResourceManager.h b/ZEngine/ZEngine/Rendering/RenderResourceManager.h index 3ea5f0286..be7c58c6d 100644 --- a/ZEngine/ZEngine/Rendering/RenderResourceManager.h +++ b/ZEngine/ZEngine/Rendering/RenderResourceManager.h @@ -31,15 +31,19 @@ namespace ZEngine::Rendering // the other; all coupling flows through here. // // THREAD SAFETY: - // Initialize / Shutdown — main thread, once at startup/teardown - // BeginFrame / EndFrame — render thread only - // OnAssetReady callback — asset/import thread; protected by m_pending_mutex - // OnAssetStale callback — asset/import thread; ScheduleSwap protected by m_pending_swap_mutex - // GetBuffer / GetImage — render thread read; asset thread writes via pending queues + // Initialize / Shutdown — main thread, once at startup/teardown + // BeginFrame / EndFrame — render thread only + // OnAssetReady callback — asset/import thread; mesh only (protected by m_pending_mutex) + // OnAssetStale callback — asset/import thread; mesh only, ScheduleSwap protected + // by m_pending_swap_mutex. Textures are triggered via + // TextureImporter/ImportCoordinator, not this callback. + // OnAssetRemoved callback — asset/import thread; textures only, via ReleaseTexture + // ScheduleTextureReload — any thread; protected by m_pending_texture_reload_mutex + // GetBuffer / GetTexture — render thread read; asset thread writes via pending queues class RenderResourceManager { public: - static constexpr uint32_t FRAMES_IN_FLIGHT = 3; + static constexpr uint32_t FRAMES_IN_FLIGHT = 3; /// @brief Initialize the RRM and bind it to a VulkanDevice and AssetRegistry. /// @details Registers OnAssetReady and OnAssetStale callbacks on the registry, @@ -47,40 +51,54 @@ namespace ZEngine::Rendering /// the packed global vertex and index buffers. /// @param device The active Vulkan device; must outlive this RRM instance. /// @param registry The asset registry to subscribe to; must outlive this RRM instance. - void Initialize(Hardwares::VulkanDevice* device, Core::VFS::AssetRegistry* registry); + void Initialize(Hardwares::VulkanDevice* device, Core::VFS::AssetRegistry* registry); /// @brief Drain in-flight GPU work and release all GPU resources. - /// @details Calls vkQueueWaitAll, shuts down texture timelines, frees the global - /// geometry buffers, all image slots, and all generic buffer slots. - /// Must be called from the main thread before VulkanDevice teardown. - void Shutdown(); + /// @details Calls vkQueueWaitAll, shuts down texture timelines, and frees the + /// global geometry buffers and all generic buffer slots. Must be called + /// from the main thread before VulkanDevice teardown. + void Shutdown(); /// @brief Per-frame render-thread entry point. - /// @details Flushes pending mesh/texture uploads and pending hot-reload swaps that - /// were queued from the asset thread since the previous BeginFrame. - /// Called by AppRenderPipeline. + /// @details Flushes pending uploads, swaps, and texture reloads/releases queued + /// from other threads since the previous BeginFrame. Called by AppRenderPipeline. /// @param frame_index Current swapchain frame index (0 .. FRAMES_IN_FLIGHT-1). - void BeginFrame(uint32_t frame_index); + void BeginFrame(uint32_t frame_index); /// @brief Per-frame render-thread exit point. /// @details Advances the internal frame counter. Called by AppRenderPipeline after /// command buffer submission. /// @param frame_index Current swapchain frame index (0 .. FRAMES_IN_FLIGHT-1). - void EndFrame(uint32_t frame_index); + void EndFrame(uint32_t frame_index); /// @brief Upload a mesh asset to the packed global vertex and index buffers. /// @details Thread-safe: enqueues a pending upload consumed by BeginFrame. /// Vertices and indices are appended at the current watermark cursor. /// @param asset_handle Handle to a fully imported MeshAsset in the AssetManager. /// @return A valid BufferHandle on success; invalid if the asset is null or upload fails. - BufferHandle UploadMesh(Managers::AssetHandle asset_handle); - - /// @brief Upload a texture asset to a new device-local VkImage. - /// @details Thread-safe: enqueues a pending upload consumed by BeginFrame. - /// The image is transitioned to SHADER_READ_ONLY_OPTIMAL via timeline semaphore. - /// @param asset_handle Handle to a fully imported TextureAsset in the AssetManager. - /// @return A valid ImageHandle on success; invalid if the asset is null or upload fails. - ImageHandle UploadTexture(Managers::AssetHandle asset_handle); + BufferHandle UploadMesh(Managers::AssetHandle asset_handle); + + /// @brief Ingest a texture file, uploading (existing invalid) or reloading it in + /// place (existing valid) under the same handle. + /// @param uuid Asset UUID, used for logging only. + /// @param absolute_path Absolute filesystem path to the image file. + /// @param existing Current handle when reimporting; invalid for first ingest. + /// @return The texture's handle — same as existing when reimporting. + Rendering::Textures::TextureHandle IngestTexture(const uuids::uuid& uuid, const char* absolute_path, Rendering::Textures::TextureHandle existing = {}); + + /// @brief Schedule a hot-reload reimport for a texture, deduped by UUID. + /// @details Thread-safe; applied by the next FlushPendingTextureReloads. + void ScheduleTextureReload(const uuids::uuid& uuid); + + /// @brief Release a texture: patch referencing materials to the sentinel, then + /// timeline-gate the GPU-side teardown. + /// @details Thread-safe. The CPU-side patch always lands before the bindless slot + /// can be reused — see Device->DestroyTexture for the GPU-side gate. + void ReleaseTexture(const uuids::uuid& uuid); + + /// @brief Look up a texture by handle. + /// @return Pointer to the Texture, or nullptr if the handle is invalid or stale. + const Rendering::Textures::Texture* GetTexture(const Rendering::Textures::TextureHandle& handle) const; /// @brief Upload raw RGBA pixel data to an existing TextureHandle via the timeline path. /// @details Records a staging copy into an instant command buffer and enqueues the @@ -91,24 +109,26 @@ namespace ZEngine::Rendering /// @param handle Pre-allocated TextureHandle whose VkImage will receive the data. /// @param data RGBA pixel data; must remain valid until SubmitTextureJobs runs. /// @return The same handle on success; invalid handle if no free upload slot. - Rendering::Textures::TextureHandle UploadTextureBuffer(uint8_t frame_index, uint8_t thread_index, const Rendering::Textures::TextureHandle& handle, unsigned char* data); + Rendering::Textures::TextureHandle UploadTextureBuffer(uint8_t frame_index, uint8_t thread_index, const Rendering::Textures::TextureHandle& handle, unsigned char* data); // Upload the ImGui font atlas synchronously using m_upload_cmd/m_upload_fence. // Blocks until the GPU copy is complete so the texture is ready before the first // frame renders. Caller must enqueue the returned handle to // TextureHandleToUpdates for bindless descriptor registration. - Rendering::Textures::TextureHandle UploadFontAtlas(unsigned char* pixels, uint32_t width, uint32_t height); + Rendering::Textures::TextureHandle UploadFontAtlas(unsigned char* pixels, uint32_t width, uint32_t height); - /// @brief Load a texture file from disk, decode it on the thread pool, and upload. - /// @details Asynchronously reads and decodes the image; uploads via the timeline path. + /// @brief Decode a texture file on the thread pool and upload it, async. + /// @details When existing is valid, reconstructs that handle in place instead of + /// allocating a new one (used by IngestTexture's reimport path). /// @param frame_index Render frame index. /// @param thread_index Thread index within the per-frame pool. /// @param filename Absolute path to the image file on disk. + /// @param existing Handle to reconstruct in place; invalid to allocate a new one. /// @return A valid TextureHandle that will become readable once the upload drains. - Rendering::Textures::TextureHandle SubmitTextureFile(uint8_t frame_index, uint8_t thread_index, const char* filename); + Rendering::Textures::TextureHandle SubmitTextureFile(uint8_t frame_index, uint8_t thread_index, const char* filename, Rendering::Textures::TextureHandle existing = {}); /// @brief Return the (255, 20, 147) fallback TextureHandle for missing textures, creating it on first call. - Rendering::Textures::TextureHandle GetOrCreateFallbackTexture(); + Rendering::Textures::TextureHandle GetOrCreateFallbackTexture(); /// @brief Payload for a deferred texture upload. /// @@ -127,31 +147,31 @@ namespace ZEngine::Rendering /// @brief Enqueue a texture upload deferral for processing in the next BeginFrame. /// @param deferral Deferral to enqueue; if Slab is non-null, ownership of Pixels is transferred. - void EnqueueTextureDeferral(TextureDeferral&& deferral); + void EnqueueTextureDeferral(TextureDeferral&& deferral); /// @brief Drain all pending texture deferrals by dispatching UploadTextureBuffer. /// @details Called from AppRenderPipeline::BeginFrame. Processes every entry in /// m_tex_deferral_queue and submits the underlying staging copies. - void CompleteDeferrals(); + void CompleteDeferrals(); /// @brief Submit all pending timeline semaphore jobs to the GPU graphics queue. /// @details Called from AppRenderPipeline::EndFrame. Processes m_tex_job_queue. - void SubmitTextureJobs(); + void SubmitTextureJobs(); /// @brief Retire command buffers whose timeline fence has been signalled. /// @details Frees staging buffers associated with completed texture uploads for the /// given frame/thread pool. Called from AppRenderPipeline::BeginFrame. /// @param frame_index Render frame index. /// @param thread_index Thread index within the per-frame pool. - void RetireTextureSlots(uint8_t frame_index, uint8_t thread_index); + void RetireTextureSlots(uint8_t frame_index, uint8_t thread_index); /// @brief Cancel all queued timeline jobs without submitting them. /// @details Called on swapchain resize or recreate to discard stale uploads. - void ClearTextureJobs(); + void ClearTextureJobs(); /// @brief Reset all texture timeline semaphore counters after a swapchain recreate. /// @details Re-initialises per-pool signal values and retire arrays to zero. - void ResetTextureTimelines(); + void ResetTextureTimelines(); /// @brief Schedule a hot-reload swap for a mesh buffer. /// @details Thread-safe: enqueues onto m_pending_swaps, applied by the next @@ -163,16 +183,7 @@ namespace ZEngine::Rendering /// global buffer is append-only. /// @param old_handle The live BufferHandle to replace. /// @param new_asset AssetHandle for the new version of the mesh. - void ScheduleSwap(BufferHandle old_handle, Managers::AssetHandle new_asset); - - /// @brief Schedule a hot-reload swap for a texture image. - /// @details Thread-safe: enqueues onto m_pending_swaps, applied by the next - /// FlushPendingSwaps (render thread), which re-uploads new_asset and - /// defers-frees the old image's GPU memory. See ScheduleSwap(BufferHandle,...) - /// for why there's no frame-in-flight delay. - /// @param old_handle The live ImageHandle to replace. - /// @param new_asset AssetHandle for the new version of the texture. - void ScheduleSwap(ImageHandle old_handle, Managers::AssetHandle new_asset); + void ScheduleSwap(BufferHandle old_handle, Managers::AssetHandle new_asset); /// @brief Deferred release of a GPU buffer. /// @details For generic device-local buffers (handles returned by UploadBuffer) the @@ -180,12 +191,7 @@ namespace ZEngine::Rendering /// deferred-free queue. For mesh handles the slot is invalidated only — the /// packed global buffer is append-only and reclaimed on shutdown. /// @param handle Handle returned by UploadMesh or UploadBuffer. - void Release(BufferHandle handle); - - /// @brief Deferred release of a GPU image. - /// @details The VmaAllocation and VkImageView are freed after FRAMES_IN_FLIGHT frames. - /// @param handle Handle returned by UploadTexture. - void Release(ImageHandle handle); + void Release(BufferHandle handle); /// @brief Look up a generic device-local buffer by handle. /// @details Valid only for handles returned by UploadBuffer. Mesh handles must use @@ -193,19 +199,13 @@ namespace ZEngine::Rendering /// only — do not store it across BeginFrame calls. /// @param handle Handle returned by UploadBuffer. /// @return Pointer to the BufferView, or nullptr if the handle is invalid or stale. - const Core::Memory::BufferView* GetBuffer(BufferHandle handle) const; - - /// @brief Look up a GPU image by handle. - /// @details The returned pointer is valid for the current frame only. - /// @param handle Handle returned by UploadTexture. - /// @return Pointer to the BufferImage, or nullptr if the handle is invalid or stale. - const Core::Memory::BufferImage* GetImage(ImageHandle handle) const; + const Core::Memory::BufferView* GetBuffer(BufferHandle handle) const; /// @brief Return the shared device-local VkBuffer that holds all uploaded vertex data. /// @details All mesh uploads are appended sequentially at the watermark cursor. /// Bound once to the VertexSB descriptor; per-draw vertex offset is in DrawData. /// @return Pointer to the global vertex BufferView; always valid after Initialize. - const Core::Memory::BufferView* GetGlobalVertexBuffer() const + const Core::Memory::BufferView* GetGlobalVertexBuffer() const { return &m_global_vertex_buf; } @@ -319,36 +319,22 @@ namespace ZEngine::Rendering void EnqueueDeletion(Hardwares::DeferredFreeEntry entry); private: - enum class UploadKind : uint8_t - { - Mesh = 0, - Texture = 1, - }; - + // Textures never reach this queue — AssetManager::IngestTexture uploads them + // directly, and reimports are triggered via ScheduleTextureReload instead. struct PendingUpload { - UploadKind Kind; Managers::AssetHandle Asset; uuids::uuid UUID; }; - enum class SwapKind : uint8_t - { - Buffer = 0, - Image = 1, - }; - - // A hot-reload swap request, queued by ScheduleSwap (asset thread) and applied by - // FlushPendingSwaps (render thread, called from BeginFrame). Applied immediately on - // the next frame it's drained on — no frame-in-flight delay: nothing in this engine - // consumes RRM handles in a way that would need one (see the correction note on - // ScheduleSwap below). + // A hot-reload swap request for a mesh buffer, queued by ScheduleSwap (asset thread) + // and applied by FlushPendingSwaps (render thread, called from BeginFrame). Applied + // immediately on the next frame it's drained on — no frame-in-flight delay: nothing + // in this engine consumes RRM handles in a way that would need one. struct PendingSwap { - BufferHandle OldBuffer = {}; // valid when Kind == Buffer - ImageHandle OldImage = {}; // valid when Kind == Image + BufferHandle OldBuffer = {}; Managers::AssetHandle NewAsset = 0; - SwapKind Kind = SwapKind::Buffer; }; template @@ -359,7 +345,6 @@ namespace ZEngine::Rendering }; static constexpr uint32_t MAX_BUFFERS = 4096; - static constexpr uint32_t MAX_IMAGES = 4096; static constexpr uint32_t MAX_GENERIC_BUFS = 4096; // Generation tag: bit 31 = 1 marks a generic-buffer handle so Release() and // GetBuffer() can distinguish them from mesh handles (bit 31 = 0). @@ -374,75 +359,72 @@ namespace ZEngine::Rendering uint32_t IdxCount = 0; }; - BufferHandle DoUploadMesh(Managers::AssetHandle asset, uint32_t frame_index); - ImageHandle DoUploadTexture(Managers::AssetHandle asset); + BufferHandle DoUploadMesh(Managers::AssetHandle asset, uint32_t frame_index); /// @brief Append one mesh asset's vertex/index data to the global buffers. /// @details Shared by DoUploadMesh (allocates a new slot) and FlushPendingSwaps /// (reuses an existing slot). Returns a zero-VtxCount MeshSlot on failure. - MeshSlot AppendMeshData(Managers::AssetHandle asset, uint32_t frame_index); + MeshSlot AppendMeshData(Managers::AssetHandle asset, uint32_t frame_index); - void AppendToGlobalBuffer(Core::Memory::BufferView& global_buf, const void* data, size_t byte_size, VkDeviceSize byte_offset, uint32_t frame_index); + void AppendToGlobalBuffer(Core::Memory::BufferView& global_buf, const void* data, size_t byte_size, VkDeviceSize byte_offset, uint32_t frame_index); - void FlushPendingUploads(uint32_t frame_index); + void FlushPendingUploads(uint32_t frame_index); /// @brief Drain m_pending_swaps and apply each swap immediately (render thread only). - void FlushPendingSwaps(uint32_t frame_index); + void FlushPendingSwaps(uint32_t frame_index); - // Batch upload helpers — render-thread only - void BeginBatchUpload(); - void EndBatchUpload(); - void ResetGeometryBuffersInternal(); + /// @brief Drain m_pending_texture_reloads and reimport each one (render thread only). + void FlushPendingTextureReloads(); - Core::Memory::BufferImage* GetImageMutable(ImageHandle handle); + /// @brief Drain m_pending_texture_releases and call Device->DestroyTexture for each (render thread only). + void FlushPendingTextureReleases(); - void InitUploadPool(); - void InitGlobalBuffers(); - uint32_t AllocMeshSlot(); - uint32_t AllocImageSlot(); - uint32_t AllocGBufSlot(); + // Batch upload helpers — render-thread only + void BeginBatchUpload(); + void EndBatchUpload(); + void ResetGeometryBuffersInternal(); + + void InitUploadPool(); + void InitGlobalBuffers(); + uint32_t AllocMeshSlot(); + uint32_t AllocGBufSlot(); /// @brief Advance counter and return the next GBUF_GEN_TAG-tagged generation value. - static uint32_t NextGBufGeneration(uint32_t& counter); + static uint32_t NextGBufGeneration(uint32_t& counter); - Hardwares::VulkanDevice* m_device = nullptr; - Core::VFS::AssetRegistry* m_registry = nullptr; + Hardwares::VulkanDevice* m_device = nullptr; + Core::VFS::AssetRegistry* m_registry = nullptr; // Per-worker TLSF upload slabs — carved from Device->Arena at Initialize. // Each worker owns one slab exclusively via t_worker_slab (ThreadPool.h). // Sized to cover the worst-case decode buffer: equirect→cubemap ≈ 96 MB. - static constexpr size_t UPLOAD_SLAB_BYTES = 128 * 1024 * 1024; // 128 MB per worker - Core::Memory::TLSFSlab m_upload_slabs[Helpers::ThreadPool::MAX_WORKERS] = {}; - uint32_t m_upload_slab_count = 0; + static constexpr size_t UPLOAD_SLAB_BYTES = 128 * 1024 * 1024; // 128 MB per worker + Core::Memory::TLSFSlab m_upload_slabs[Helpers::ThreadPool::MAX_WORKERS] = {}; + uint32_t m_upload_slab_count = 0; - void InitUploadSlabs(uint32_t worker_count); + void InitUploadSlabs(uint32_t worker_count); // Global geometry buffers — all mesh vertices/indices packed together. - static constexpr VkDeviceSize GLOBAL_VTX_CAPACITY = 512 * 1024 * 1024; // 512 MB → ~16M DrawVertex - static constexpr VkDeviceSize GLOBAL_IDX_CAPACITY = 512 * 1024 * 1024; // 512 MB → ~128M uint32 - Core::Memory::BufferView m_global_vertex_buf = {}; - Core::Memory::BufferView m_global_index_buf = {}; - VkDeviceSize m_vtx_cursor = 0; // byte offset of next write - VkDeviceSize m_idx_cursor = 0; + static constexpr VkDeviceSize GLOBAL_VTX_CAPACITY = 512 * 1024 * 1024; // 512 MB → ~16M DrawVertex + static constexpr VkDeviceSize GLOBAL_IDX_CAPACITY = 512 * 1024 * 1024; // 512 MB → ~128M uint32 + Core::Memory::BufferView m_global_vertex_buf = {}; + Core::Memory::BufferView m_global_index_buf = {}; + VkDeviceSize m_vtx_cursor = 0; // byte offset of next write + VkDeviceSize m_idx_cursor = 0; // Mesh slot pool — stores per-mesh offsets into the global buffers. - Slot m_mesh_slots[MAX_BUFFERS] = {}; - uint32_t m_mesh_slot_count = 0; + Slot m_mesh_slots[MAX_BUFFERS] = {}; + uint32_t m_mesh_slot_count = 0; // Never reset by Release() — gives each slot a monotonic generation on reuse // instead of the deterministic idx+1 a released-then-reallocated slot would // otherwise get back, which is what gave stale handles no real ABA protection. - uint32_t m_mesh_slot_gen_counter[MAX_BUFFERS] = {}; - - // Image pool - Slot m_image_slots[MAX_IMAGES] = {}; - uint32_t m_image_slot_count = 0; - uint32_t m_image_slot_gen_counter[MAX_IMAGES] = {}; + uint32_t m_mesh_slot_gen_counter[MAX_BUFFERS] = {}; // Generic device-local buffer pool — for bone matrices, particle VBs, etc. // Handles carry GBUF_GEN_TAG in bit 31 to distinguish from mesh handles. - Slot m_gbuf_slots[MAX_GENERIC_BUFS] = {}; - uint32_t m_gbuf_slot_count = 0; - uint32_t m_gbuf_slot_gen_counter[MAX_GENERIC_BUFS] = {}; + Slot m_gbuf_slots[MAX_GENERIC_BUFS] = {}; + uint32_t m_gbuf_slot_count = 0; + uint32_t m_gbuf_slot_gen_counter[MAX_GENERIC_BUFS] = {}; // UUID → handle maps (for hot-reload swap lookup) // Written on first upload; read on OnAssetStale. Protected by m_uuid_map_mutex. @@ -451,52 +433,58 @@ namespace ZEngine::Rendering uuids::uuid UUID; BufferHandle Handle; }; - struct UUIDImagePair - { - uuids::uuid UUID; - ImageHandle Handle; - }; - static constexpr uint32_t MAX_UUID_MAP = 4096; - UUIDBufferPair m_uuid_to_buffer[MAX_UUID_MAP] = {}; - uint32_t m_uuid_to_buffer_count = 0; - UUIDImagePair m_uuid_to_image[MAX_UUID_MAP] = {}; - uint32_t m_uuid_to_image_count = 0; + static constexpr uint32_t MAX_UUID_MAP = 4096; + UUIDBufferPair m_uuid_to_buffer[MAX_UUID_MAP] = {}; + uint32_t m_uuid_to_buffer_count = 0; // Pending uploads — written from asset thread, flushed in BeginFrame - static constexpr uint32_t MAX_PENDING = 1024; - PendingUpload m_pending[MAX_PENDING] = {}; - uint32_t m_pending_count = 0; + static constexpr uint32_t MAX_PENDING = 1024; + PendingUpload m_pending[MAX_PENDING] = {}; + uint32_t m_pending_count = 0; // Pending hot-reload swaps — written from asset thread (ScheduleSwap), drained by // FlushPendingSwaps on the render thread. A dedicated mutex, not m_pending_mutex: // that lock is held across file I/O and a GPU call in SubmitTextureFile, and // swap enqueue/drain must not stall behind a concurrent texture load. - PendingSwap m_pending_swaps[MAX_PENDING] = {}; - uint32_t m_pending_swap_count = 0; + PendingSwap m_pending_swaps[MAX_PENDING] = {}; + uint32_t m_pending_swap_count = 0; + + // Pending texture reloads — written from any thread via ScheduleTextureReload + // (deduped by UUID at enqueue time), drained by FlushPendingTextureReloads on the + // render thread. + uuids::uuid m_pending_texture_reloads[MAX_PENDING] = {}; + uint32_t m_pending_texture_reload_count = 0; + std::mutex m_pending_texture_reload_mutex; + + // Pending texture releases — handle captured up front in ReleaseTexture, drained + // by FlushPendingTextureReleases, the only caller of Device->DestroyTexture. + Rendering::Textures::TextureHandle m_pending_texture_releases[MAX_PENDING] = {}; + uint32_t m_pending_texture_release_count = 0; + std::mutex m_pending_texture_release_mutex; // Geometry compaction request — set by asset thread, executed on render thread - std::atomic m_pending_reset = false; + std::atomic m_pending_reset = false; // Batch upload state — render-thread only, no locking needed - bool m_batch_mode = false; - Core::Memory::BufferView m_batch_stagings[MAX_PENDING * 2] = {}; - uint32_t m_batch_staging_count = 0; + bool m_batch_mode = false; + Core::Memory::BufferView m_batch_stagings[MAX_PENDING * 2] = {}; + uint32_t m_batch_staging_count = 0; - uint32_t m_current_frame = 0; + uint32_t m_current_frame = 0; // Dedicated command pools for geometry uploads — isolated from the swapchain // timeline semaphore chain. Geometry uploads must not share the graphics queue // submission path with Present; a private pool + fence ensures safe isolation. - Rendering::Pools::CommandPool* m_upload_pool = nullptr; - Hardwares::CommandBuffer* m_upload_cmd = nullptr; - VkFence m_upload_fence = VK_NULL_HANDLE; - Rendering::Pools::CommandPool* m_transfer_pool = nullptr; - Hardwares::CommandBuffer* m_transfer_cmd = nullptr; - VkFence m_transfer_fence = VK_NULL_HANDLE; + Rendering::Pools::CommandPool* m_upload_pool = nullptr; + Hardwares::CommandBuffer* m_upload_cmd = nullptr; + VkFence m_upload_fence = VK_NULL_HANDLE; + Rendering::Pools::CommandPool* m_transfer_pool = nullptr; + Hardwares::CommandBuffer* m_transfer_cmd = nullptr; + VkFence m_transfer_fence = VK_NULL_HANDLE; // Upper bound for texture timeline slot search — keeps textures out of the // geometry slots and caps the retire loop to the same range. - static constexpr uint32_t GEOMETRY_UPLOAD_SLOT = 15; + static constexpr uint32_t GEOMETRY_UPLOAD_SLOT = 15; struct TextureTimelineJob { diff --git a/ZEngine/ZEngine/Rendering/Renderers/RenderGraph.cpp b/ZEngine/ZEngine/Rendering/Renderers/RenderGraph.cpp index 863b75f99..6d89beb44 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/RenderGraph.cpp +++ b/ZEngine/ZEngine/Rendering/Renderers/RenderGraph.cpp @@ -42,7 +42,7 @@ namespace ZEngine::Rendering::Renderers auto* tex = device->GlobalTextures.Access(handle); if (!tex) return VK_NULL_HANDLE; - auto* img_buf = device->Image2DBufferManager.Access(tex->BufferHandle); + auto* img_buf = device->ImageBufferManager.Access(tex->BufferHandle); if (!img_buf) return VK_NULL_HANDLE; return img_buf->GetBuffer().Handle; @@ -274,13 +274,11 @@ namespace ZEngine::Rendering::Renderers // Do NOT call any vkDestroy* yet — new resources must be created first // so the driver cannot recycle these handles for new allocations. TransientPool.Clear(); - uint64_t timeline = Device->SwapchainPtr->RenderTimelineNextValue; + uint64_t timeline = Device->SwapchainPtr->RenderTimelineNextValue; - // Stack-local scratch for old handles (max 16 passes, max 32 transients). - VkFramebuffer old_fbs[16] = {}; - uint32_t old_fb_count = 0; - Core::Memory::BufferImage old_imgs[32] = {}; - uint32_t old_img_count = 0; + // Stack-local scratch for old framebuffer handles (max 16 passes). + VkFramebuffer old_fbs[16] = {}; + uint32_t old_fb_count = 0; for (auto& pass : Passes) { @@ -292,10 +290,9 @@ namespace ZEngine::Rendering::Renderers } } - // Swap the underlying Image2DBuffer in-place for each transient resource. - // TextureHandle and Image2DBufferManager slot are REUSED — no new slots, - // no slot exhaustion, handles stay stable so the editor's cached ImTextureID - // remains valid. Old VkImage/VkImageView data is saved for DeferFree. + // Reconstruct each transient resource in place — same TextureHandle, same slot, so + // the editor's cached ImTextureID stays valid. Old VkImage is defer-freed inside + // ReconstructTexture itself. for (auto& res : Resources) { if (res.External || !res.Transient) @@ -306,27 +303,10 @@ namespace ZEngine::Rendering::Renderers res.RuntimeState = {VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED}; if (!res.TextureHandle.Valid()) continue; - auto* tex = Device->GlobalTextures.Access(res.TextureHandle); - if (!tex) - continue; - auto* img = Device->Image2DBufferManager.Access(tex->BufferHandle); - if (!img) - continue; - // Save old VkImage/VkImageView for deferred destruction. - if (old_img_count < 32) - old_imgs[old_img_count++] = img->GetBuffer(); - // Reconstruct Image2DBuffer in the same slot with new dimensions. - // This creates new VkImage/VkImageView while old ones are still alive. - img->Specification.Width = width; - img->Specification.Height = height; - img->Construct(Device); - // Update Texture metadata. - tex->Width = width; - tex->Height = height; - tex->BufferSize = width * height * res.Spec.BytePerPixel * res.Spec.LayerCount; + Device->ReconstructTexture(res.TextureHandle, res.Spec); } - // Phase 2 — rebuild framebuffers and re-bind descriptors with new Image2DBuffers. + // Phase 2 — rebuild framebuffers and re-bind descriptors with new ImageBuffers. // All TextureHandles remain valid (in-place swap) so AllocateTransientResources // is a no-op for existing resources; call it only for safety (skips valid handles). @@ -393,14 +373,6 @@ namespace ZEngine::Rendering::Renderers e.Data.Vk = {reinterpret_cast(old_fbs[i]), Rendering::DeviceResourceType::FRAMEBUFFER, nullptr}; Device->DeferFree(e); } - for (uint32_t i = 0; i < old_img_count; ++i) - { - Hardwares::DeferredFreeEntry e; - e.EntryKind = Hardwares::DeferredFreeEntry::Kind::Image; - e.TimelineValue = timeline; - e.Data.Image = old_imgs[i]; - Device->DeferFree(e); - } } void RenderGraph::Dispose() @@ -412,7 +384,7 @@ namespace ZEngine::Rendering::Renderers auto* tex = Device->GlobalTextures.Access(res.TextureHandle); if (!tex) continue; - auto* img = Device->Image2DBufferManager.Access(tex->BufferHandle); + auto* img = Device->ImageBufferManager.Access(tex->BufferHandle); if (img) img->Dispose(); } @@ -843,7 +815,7 @@ namespace ZEngine::Rendering::Renderers auto* tex = Device->GlobalTextures.Access(handle); if (!tex) return; - auto* img = Device->Image2DBufferManager.Access(tex->BufferHandle); + auto* img = Device->ImageBufferManager.Access(tex->BufferHandle); if (!img) return; VkImageView view = img->GetImageViewHandle(); diff --git a/ZEngine/ZEngine/Rendering/Renderers/RenderPasses/RenderPass.cpp b/ZEngine/ZEngine/Rendering/Renderers/RenderPasses/RenderPass.cpp index cefc53720..edb1e1e47 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/RenderPasses/RenderPass.cpp +++ b/ZEngine/ZEngine/Rendering/Renderers/RenderPasses/RenderPass.cpp @@ -96,6 +96,8 @@ namespace ZEngine::Rendering::Renderers::RenderPasses void RenderPass::Dispose() { + // NOTE: dead code today. If wired up later, route through VulkanDevice::DestroyTexture + // instead — Remove() reclaims the slot with no timeline gate. for (auto& handle : Specification.ExternalOutputs) { m_device->GlobalTextures.Remove(handle); @@ -257,7 +259,7 @@ namespace ZEngine::Rendering::Renderers::RenderPasses auto frame_count = m_device->SwapchainPtr->BufferredFrameCount; auto tex_buf = m_device->GlobalTextures.Access(handle); - auto img_buf = m_device->Image2DBufferManager.Access(tex_buf->BufferHandle); + auto img_buf = m_device->ImageBufferManager.Access(tex_buf->BufferHandle); auto write_reqs = std::vector(frame_count); for (unsigned i = 0; i < frame_count; ++i) diff --git a/ZEngine/ZEngine/Rendering/Specifications/TextureSpecification.h b/ZEngine/ZEngine/Rendering/Specifications/TextureSpecification.h index 3ac9949a9..f69e35266 100644 --- a/ZEngine/ZEngine/Rendering/Specifications/TextureSpecification.h +++ b/ZEngine/ZEngine/Rendering/Specifications/TextureSpecification.h @@ -30,7 +30,7 @@ namespace ZEngine::Rendering::Specifications ARRAYOF_2D_IMAGE }; - struct Image2DBufferSpecification + struct ImageBufferSpecification { uint32_t Width; uint32_t Height; diff --git a/ZEngine/ZEngine/Rendering/Textures/Texture.h b/ZEngine/ZEngine/Rendering/Textures/Texture.h index d11649229..568f1d7ae 100644 --- a/ZEngine/ZEngine/Rendering/Textures/Texture.h +++ b/ZEngine/ZEngine/Rendering/Textures/Texture.h @@ -6,7 +6,7 @@ namespace ZEngine::Hardwares { - struct Image2DBuffer; + struct ImageBuffer; } namespace ZEngine::Rendering::Textures @@ -16,26 +16,19 @@ namespace ZEngine::Rendering::Textures Texture() = default; ~Texture(); - bool IsDepthTexture = false; - uint32_t Width = 1; - uint32_t Height = 1; - uint32_t BytePerPixel = 0; - VkDeviceSize BufferSize = 0; - Specifications::TextureSpecification Specification = {}; - Helpers::Handle BufferHandle = {}; + bool IsDepthTexture = false; + uint32_t Width = 1; + uint32_t Height = 1; + uint32_t BytePerPixel = 0; + VkDeviceSize BufferSize = 0; + Specifications::TextureSpecification Specification = {}; + Helpers::Handle BufferHandle = {}; - void Dispose(); + void Dispose(); }; using TextureHandle = Helpers::Handle; using TextureHandleManager = Helpers::HandleManager; - - /* - * To do : Should be deprecated - */ - Texture* CreateTexture(const char* path); - Texture* CreateTexture(unsigned int width, unsigned int height); - Texture* CreateTexture(unsigned int width, unsigned int height, float r, float g, float b, float a); } // namespace ZEngine::Rendering::Textures namespace ZEngine::Helpers diff --git a/ZEngine/ZEngine/Rendering/Textures/Texture2D.cpp b/ZEngine/ZEngine/Rendering/Textures/Texture2D.cpp deleted file mode 100644 index cbdab36fd..000000000 --- a/ZEngine/ZEngine/Rendering/Textures/Texture2D.cpp +++ /dev/null @@ -1,263 +0,0 @@ -#include -#include -#include -#include - -// #define STB_IMAGE_IMPLEMENTATION -// #ifdef __GNUC__ -// #define STBI_NO_SIMD -// #endif -#include - -// #define STB_IMAGE_WRITE_IMPLEMENTATION -// #define STB_IMAGE_RESIZE_IMPLEMENTATION -// #include -// #include - -using namespace ZEngine::Helpers; -using namespace ZEngine::Hardwares; - -namespace ZEngine::Rendering::Textures -{ - - Texture* CreateTexture(const char* path) - { - return nullptr; - } - - Texture* CreateTexture(unsigned int width, unsigned int height) - { - return nullptr; - } - - Texture* CreateTexture(unsigned int width, unsigned int height, float r, float g, float b, float a) - { - return nullptr; - } -} // namespace ZEngine::Rendering::Textures - -namespace ZEngine::Rendering::Textures -{ - // Ref Texture2D::Read(std::string_view filename) - //{ - // int width = 0, height = 0, channel = 0; - // // stbi_set_flip_vertically_on_load(1); - // stbi_uc* image_data = stbi_load(filename.data(), &width, &height, &channel, STBI_rgb_alpha); - - // if (!image_data) - // { - // ZENGINE_CORE_ERROR("Failed to load texture file : {0}", filename.data()) - // return Create(1, 1, 0, 0, 0, 0); - // } - // /* - // * post processing to convert the image from RGB to RBGA - // */ - // channel = (channel == STBI_rgb) ? STBI_rgb_alpha : channel; - // std::vector output_buffer(width * height * channel); - // // stbir_resize_uint8(image_data, width, height, 0, output_buffer.data(), width, height, 0, channel); - // stbi_image_free(image_data); - - // Specifications::TextureSpecification spec = {}; - // spec.Width = width; - // spec.Height = height; - // spec.Format = Specifications::ImageFormat::R8G8B8A8_SRGB; - // spec.BytePerPixel = - // Specifications::BytePerChannelMap[VALUE_FROM_SPEC_MAP(spec.Format)]; spec.Data = output_buffer.data(); auto - // texture = Create(spec); - - // return texture; - //} - - // Ref Texture2D::ReadCubemap(std::string_view filename) - //{ - // stbi_set_flip_vertically_on_load(1); - - // int width = 0, height = 0, channel = 0; - // const float* image_data = stbi_loadf(filename.data(), &width, &height, &channel, 4); - // /* - // * post processing to convert the image from RGB to RBGA - // */ - // channel = (channel == STBI_rgb) ? STBI_rgb_alpha : channel; - // std::vector output_buffer(width * height * channel); - // // stbir_resize_float(image_data, width, height, 0, output_buffer.data(), width, height, 0, channel); - // stbi_image_free((void*) image_data); - - // Buffers::Bitmap in = {width, height, 4, Buffers::BitmapFormat::FLOAT, - // output_buffer.data()}; Buffers::Bitmap vertical_cross = - // Buffers::Bitmap::EquirectangularMapToVerticalCross(in); Buffers::Bitmap cubemap = - // Buffers::Bitmap::VerticalCrossToCubemap(vertical_cross); - - // Specifications::TextureSpecification cubemap_texture_spec = {}; - // cubemap_texture_spec.IsCubemap = true; - // cubemap_texture_spec.Width = cubemap.Width; - // cubemap_texture_spec.Height = cubemap.Height; - // cubemap_texture_spec.Format = Specifications::ImageFormat::R32G32B32A32_SFLOAT; - // cubemap_texture_spec.BytePerPixel = - // Specifications::BytePerChannelMap[VALUE_FROM_SPEC_MAP(cubemap_texture_spec.Format)]; cubemap_texture_spec.Data - // = cubemap.Buffer.data(); cubemap_texture_spec.LayerCount = 6; - - // return Create(cubemap_texture_spec); - //} - - // std::future> Texture2D::ReadAsync(std::string_view filename) - //{ - // co_return Read(filename); - // } - - // BufferImage& Texture2D::GetBuffer() - //{ - // return m_image_2d_buffer->GetBuffer(); - // } - - // const BufferImage& Texture2D::GetBuffer() const - //{ - // return m_image_2d_buffer->GetBuffer(); - // } - - // Ref Texture2D::Create(const Specifications::TextureSpecification& spec) - //{ - // Ref texture = CreateRef(); - // // texture->m_specification = spec; - // FillAsVulkanImage(texture, spec); - // return texture; - // } - - // Ref Texture2D::Create(uint32_t width, uint32_t height) - //{ - // unsigned char image_data[] = {255, 255, 255, 255, '\0'}; - - // Specifications::TextureSpecification spec = {}; - // spec.Width = width; - // spec.Height = height; - // spec.Format = Specifications::ImageFormat::R8G8B8A8_SRGB; - // spec.BytePerPixel = - // Specifications::BytePerChannelMap[VALUE_FROM_SPEC_MAP(spec.Format)]; spec.Data = image_data; return - // Create(spec); - //} - - // Ref Texture2D::Create(uint32_t width, uint32_t height, float r, float g, float b, float a) - //{ - // unsigned char image_data[] = {0, 0, 0, 0, '\0'}; - // image_data[0] = static_cast(std::clamp(r, .0f, 255.0f)); - // image_data[1] = static_cast(std::clamp(g, .0f, 255.0f)); - // image_data[2] = static_cast(std::clamp(b, .0f, 255.0f)); - // image_data[3] = static_cast(std::clamp(a, .0f, 255.0f)); - // Specifications::TextureSpecification spec = {}; - // spec.Width = width; - // spec.Height = height; - // spec.Format = Specifications::ImageFormat::R8G8B8A8_SRGB; - // spec.BytePerPixel = - // Specifications::BytePerChannelMap[VALUE_FROM_SPEC_MAP(spec.Format)]; spec.Data = image_data; return - // Create(spec); - // } - - // Ref Texture2D::GetImage2DBuffer() const - //{ - // return m_image_2d_buffer; - // } - - // void Texture2D::Dispose() - //{ - // m_image_2d_buffer->Dispose(); - // } - - // void Texture2D::FillAsVulkanImage(Ref& texture, const Specifications::TextureSpecification& spec) - //{ - // // texture->m_byte_per_pixel = spec.BytePerPixel; - // // texture->m_buffer_size = spec.Width * spec.Height * spec.BytePerPixel * spec.LayerCount; - // // texture->m_width = spec.Width; - // // texture->m_height = spec.Height; - // // texture->m_is_depth = (spec.Format == Specifications::ImageFormat::DEPTH_STENCIL_FROM_DEVICE); - - // // auto device = Hardwares::VulkanDevice::GetNativeDeviceHandle(); - // // Hardwares::BufferView staging_buffer = - // // Hardwares::VulkanDevice::CreateBuffer(texture->m_buffer_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, - // VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT); - - // // Hardwares::VulkanDevice::MapAndCopyToMemory(staging_buffer, texture->m_buffer_size, spec.Data); - - // ///* Create VkImage */ - // // uint32_t storage_bit = spec.IsUsageStorage ? VK_IMAGE_USAGE_STORAGE_BIT : 0; - // // uint32_t transfert_bit = spec.IsUsageTransfert ? VK_IMAGE_USAGE_TRANSFER_DST_BIT : 0; - // // uint32_t sampled_bit = spec.IsUsageSampled ? VK_IMAGE_USAGE_SAMPLED_BIT : 0; - // // uint32_t image_aspect = (spec.Format == Specifications::ImageFormat::DEPTH_STENCIL_FROM_DEVICE) ? - // VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT; - // // uint32_t image_usage_attachment = - // // (spec.Format == Specifications::ImageFormat::DEPTH_STENCIL_FROM_DEVICE) ? - // VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; - - // // VkFormat image_format = (spec.Format == Specifications::ImageFormat::DEPTH_STENCIL_FROM_DEVICE) ? - // Hardwares::VulkanDevice::FindDepthFormat() - // // : - // Specifications::ImageFormatMap[static_cast(spec.Format)]; - // // Specifications::Image2DBufferSpecification buffer_spec; - // // buffer_spec.Width = texture->m_width; - // // buffer_spec.Height = texture->m_height; - // // buffer_spec.BufferUsageType = spec.IsCubemap ? Specifications::ImageBufferUsageType::CUBEMAP : - // Specifications::ImageBufferUsageType::SINGLE_2D_IMAGE; - // // buffer_spec.ImageFormat = image_format; - // // buffer_spec.ImageUsage = VkImageUsageFlagBits(image_usage_attachment | transfert_bit | sampled_bit | - // storage_bit); - // // buffer_spec.ImageAspectFlag = VkImageAspectFlagBits(image_aspect); - // // buffer_spec.LayerCount = spec.LayerCount; - - // // texture->m_image_2d_buffer = CreateRef(std::move(buffer_spec)); - - // // if (spec.PerformTransition) - // //{ - // // /*Transition Image from VK_IMAGE_LAYOUT_UNDEFINED to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL OR - // VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL and Copy buffer to - // // * image*/ - // // auto image_handle = - // texture->m_image_2d_buffer->GetHandle(); - // // auto& image_buffer = - // texture->m_image_2d_buffer->GetBuffer(); - // // Specifications::ImageMemoryBarrierSpecification barrier_spec_0 = {}; - // // barrier_spec_0.ImageHandle = image_handle; - // // barrier_spec_0.OldLayout = - // Specifications::ImageLayout::UNDEFINED; - // // barrier_spec_0.NewLayout = - // Specifications::ImageLayout::TRANSFER_DST_OPTIMAL; - // // barrier_spec_0.ImageAspectMask = VkImageAspectFlagBits(image_aspect); - // // barrier_spec_0.SourceAccessMask = 0; - // // barrier_spec_0.DestinationAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - // // barrier_spec_0.SourceStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; - // // barrier_spec_0.DestinationStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; - // // barrier_spec_0.LayerCount = spec.LayerCount; - // // Primitives::ImageMemoryBarrier barrier_0{barrier_spec_0}; - - // // Specifications::ImageMemoryBarrierSpecification barrier_spec_1 = {}; - // // barrier_spec_1.ImageHandle = image_handle; - // // barrier_spec_1.OldLayout = - // Specifications::ImageLayout::TRANSFER_DST_OPTIMAL; - // // barrier_spec_1.NewLayout = VkImageAspectFlagBits(image_aspect) == VK_IMAGE_ASPECT_DEPTH_BIT - // ? - // // Specifications::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL - // // : Specifications::ImageLayout::SHADER_READ_ONLY_OPTIMAL; - // // barrier_spec_1.ImageAspectMask = VkImageAspectFlagBits(image_aspect); - // // barrier_spec_1.SourceAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - // // barrier_spec_1.DestinationAccessMask = VK_ACCESS_SHADER_READ_BIT; - // // barrier_spec_1.SourceStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; - // // barrier_spec_1.DestinationStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; - // // barrier_spec_1.LayerCount = spec.LayerCount; - // // Primitives::ImageMemoryBarrier barrier_1{barrier_spec_1}; - - // // auto command_buffer = - // Hardwares::VulkanDevice::BeginInstantCommandBuffer(Rendering::QueueType::GRAPHIC_QUEUE); - // // command_buffer->TransitionImageLayout(barrier_0); - // // command_buffer->CopyBufferToImage(staging_buffer, image_buffer, texture->m_width, texture->m_height, - // spec.LayerCount, barrier_0.GetHandle().newLayout); - // // command_buffer->TransitionImageLayout(barrier_1); - // // Hardwares::VulkanDevice::EndInstantCommandBuffer(command_buffer); - // //} - - // ///* Cleanup resource */ - // // Hardwares::VulkanDevice::EnqueueBufferForDeletion(staging_buffer); - //} - - // Texture2D::~Texture2D() - //{ - // Dispose(); - // } - -} // namespace ZEngine::Rendering::Textures diff --git a/ZEngine/ZEngine/Rendering/Textures/Texture2D.h b/ZEngine/ZEngine/Rendering/Textures/Texture2D.h deleted file mode 100644 index 784b7d76f..000000000 --- a/ZEngine/ZEngine/Rendering/Textures/Texture2D.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once -#include -#include - -namespace ZEngine::Rendering::Textures -{ - - // class Texture2D : public Helpers::RefCounted /*: public Texture*/ - //{ - // public: - // Texture2D() = default; - // Texture2D(const Specifications::TextureSpecification& spec, const Helpers::Ref& - // buffer) - // { - // m_image_2d_buffer = buffer; - // // m_specification = spec; - // } - - // Texture2D(Specifications::TextureSpecification&& spec, Helpers::Ref&& buffer) - // { - // m_image_2d_buffer = std::move(buffer); - // // m_specification = std::move(spec); - // } - - // virtual ~Texture2D(); - - // static Helpers::Ref Create(const Specifications::TextureSpecification& spec); - // static Helpers::Ref Create(uint32_t width = 1, uint32_t height = 1); - // static Helpers::Ref Create(uint32_t width, uint32_t height, float r, float g, float b, - // float a); static Helpers::Ref Read(std::string_view filename); static - // Helpers::Ref ReadCubemap(std::string_view filename); static - // std::future> ReadAsync(std::string_view filename); - - // virtual Hardwares::BufferImage& GetBuffer() /*override*/; - // virtual const Hardwares::BufferImage& GetBuffer() const /*override*/; - // Helpers::Ref GetImage2DBuffer() const; - // virtual void Dispose() /*override*/; - - // protected: - // static void FillAsVulkanImage(Helpers::Ref& texture, const Specifications::TextureSpecification& - // specification); - - // private: - // Helpers::Ref m_image_2d_buffer; - // }; -} // namespace ZEngine::Rendering::Textures diff --git a/ZEngine/tests/Rendering/RenderResourceManagerTest.cpp b/ZEngine/tests/Rendering/RenderResourceManagerTest.cpp index 959da4128..a9d3ceb72 100644 --- a/ZEngine/tests/Rendering/RenderResourceManagerTest.cpp +++ b/ZEngine/tests/Rendering/RenderResourceManagerTest.cpp @@ -54,16 +54,15 @@ TEST(RenderHandle, EqualityRequiresBothFieldsToMatch) TEST(RenderHandle, DifferentTagTypesAreIncompatible) { - // Compile-time check: BufferHandle and ImageHandle are distinct types. - static_assert(!std::is_same_v, "BufferHandle and ImageHandle must be distinct types"); + // Compile-time check: BufferHandle and SamplerHandle are distinct types. static_assert(!std::is_same_v, "BufferHandle and SamplerHandle must be distinct types"); - static_assert(!std::is_same_v, "ImageHandle and PipelineHandle must be distinct types"); + static_assert(!std::is_same_v, "SamplerHandle and PipelineHandle must be distinct types"); // Runtime sanity: same {index, generation} in different handle types are not interchangeable. - BufferHandle buf{1, 2}; - ImageHandle img{1, 2}; - EXPECT_EQ(buf.Index, img.Index); - EXPECT_EQ(buf.Generation, img.Generation); + BufferHandle buf{1, 2}; + SamplerHandle smp{1, 2}; + EXPECT_EQ(buf.Index, smp.Index); + EXPECT_EQ(buf.Generation, smp.Generation); // They cannot be compared (different types) — confirmed by static_assert above. SUCCEED(); } @@ -71,7 +70,6 @@ TEST(RenderHandle, DifferentTagTypesAreIncompatible) TEST(RenderHandle, TriviallyCopiable) { static_assert(std::is_trivially_copyable_v, "BufferHandle must be trivially copyable"); - static_assert(std::is_trivially_copyable_v, "ImageHandle must be trivially copyable"); static_assert(std::is_trivially_copyable_v, "SamplerHandle must be trivially copyable"); static_assert(std::is_trivially_copyable_v, "PipelineHandle must be trivially copyable"); SUCCEED(); @@ -80,7 +78,6 @@ TEST(RenderHandle, TriviallyCopiable) TEST(RenderHandle, SizeFitsInEightBytes) { static_assert(sizeof(BufferHandle) == 8, "BufferHandle must fit in 8 bytes"); - static_assert(sizeof(ImageHandle) == 8, "ImageHandle must fit in 8 bytes"); static_assert(sizeof(SamplerHandle) == 8, "SamplerHandle must fit in 8 bytes"); static_assert(sizeof(PipelineHandle) == 8, "PipelineHandle must fit in 8 bytes"); SUCCEED(); diff --git a/ZEngine/tests/Rendering/TextureImporterTest.cpp b/ZEngine/tests/Rendering/TextureImporterTest.cpp new file mode 100644 index 000000000..ec9b9a76b --- /dev/null +++ b/ZEngine/tests/Rendering/TextureImporterTest.cpp @@ -0,0 +1,31 @@ +#include +#include + +using namespace ZEngine::Importers; + +// CanImport is a pure function on the extension string — no Initialize()/Arena needed. + +TEST(TextureImporterTest, CanImportClaimsAllEightRasterExtensions) +{ + TextureImporter importer; + const char* claimed[] = {"png", "jpg", "jpeg", "bmp", "tga", "gif", "psd", "pic"}; + for (const char* ext : claimed) + EXPECT_TRUE(importer.CanImport(ext)) << ext; +} + +TEST(TextureImporterTest, CanImportDoesNotClaimEnvironmentMapOrContainerFormats) +{ + TextureImporter importer; + // hdr/exr stay EnvironmentMapImporter's domain; ktx/ktx2 are recognized by + // AssetRegistry::InferTypeFromExtension but stb_image cannot decode them — neither + // should be claimed here, or ImportCoordinator's first-match routing gets ambiguous. + const char* unclaimed[] = {"hdr", "exr", "ktx", "ktx2", "zenvmap", "glb", "fbx", "obj"}; + for (const char* ext : unclaimed) + EXPECT_FALSE(importer.CanImport(ext)) << ext; +} + +TEST(TextureImporterTest, CanImportRejectsNull) +{ + TextureImporter importer; + EXPECT_FALSE(importer.CanImport(nullptr)); +} diff --git a/ZEngine/tests/VFS/AssetRegistryTest.cpp b/ZEngine/tests/VFS/AssetRegistryTest.cpp index 0af7923d4..517aa61da 100644 --- a/ZEngine/tests/VFS/AssetRegistryTest.cpp +++ b/ZEngine/tests/VFS/AssetRegistryTest.cpp @@ -281,6 +281,50 @@ TEST_F(AssetRegistryTest, StateTransitionAndCascadeMarksStale) EXPECT_EQ(m_registry.FindByUUID(mat)->State, AssetState::Stale); } +// InferTypeFromExtension — texture pipeline redesign: TextureImporter claims 8 raster +// formats (png/jpg/jpeg/bmp/tga/gif/psd/pic); this must classify all of them (plus the +// pre-existing hdr/exr/ktx/ktx2) as TEXTURE, or a file gets silently misrouted as MESH +// (AssetRegistry.cpp's fallback default) and hits type-confused handle access downstream. +TEST_F(AssetRegistryTest, InferTypeFromExtensionRecognizesAllRasterFormats) +{ + const char* texture_exts[] = {"/a.png", "/a.jpg", "/a.jpeg", "/a.bmp", "/a.tga", "/a.gif", "/a.psd", "/a.pic", "/a.hdr", "/a.exr", "/a.ktx", "/a.ktx2"}; + for (const char* p : texture_exts) + EXPECT_EQ(AssetRegistry::InferTypeFromExtension(P(p)), Managers::AssetType::TEXTURE) << p; + + EXPECT_EQ(AssetRegistry::InferTypeFromExtension(P("/a.zematerial")), Managers::AssetType::MATERIAL); + EXPECT_EQ(AssetRegistry::InferTypeFromExtension(P("/a.zemesh")), Managers::AssetType::MESH); +} + +// OnRemoved callback — fired by OnAssetDeleted, the trigger RenderResourceManager::ReleaseTexture +// is wired to for AssetType::TEXTURE. Mirrors OnStaleFiredByOnAssetModified's pattern. +TEST_F(AssetRegistryTest, OnAssetDeletedFiresRemovedCallbackWithType) +{ + struct Ctx + { + uuids::uuid uuid; + Managers::AssetType type = Managers::AssetType::MESH; + int count = 0; + }; + Ctx ctx{}; + + m_registry.SetOnRemovedCallback(&ctx, [](void* raw, const uuids::uuid& uuid, Managers::AssetType type) { + auto* c = static_cast(raw); + c->uuid = uuid; + c->type = type; + c->count++; + }); + + uuids::uuid uuid = MakeUUID(); + Reg(m_registry, uuid, Managers::AssetType::TEXTURE, "/p/removed.png"); + + m_registry.OnAssetDeleted(P("/p/removed.png")); + + EXPECT_EQ(ctx.count, 1); + EXPECT_EQ(ctx.uuid, uuid); + EXPECT_EQ(ctx.type, Managers::AssetType::TEXTURE); + EXPECT_EQ(m_registry.FindByUUID(uuid), nullptr) << "root record must be removed"; +} + // Test 11 — Registry initialises within the AssetManager budget (100 MB). // Regression guard: AssetRecord was once ~7 KB (full MetaFileData embedded), // making 4096 slots cost ~27 MB. Now AssetMetaSnapshot keeps it ~1.2 KB