From d47af7e8d54e548c7397ddc2209d07ab1d6876e4 Mon Sep 17 00:00:00 2001 From: Jean Philippe Date: Fri, 21 Aug 2026 12:06:15 +0900 Subject: [PATCH 1/3] fix(rendering): GBuffer pipeline, second-launch asset reload, HashMap rewrite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rendering: - Register GbufferPass in render graph; was outputting black via dead BasePass - Fix magenta background: shaderInt64 device feature not enabled, GbufferPass Execute was returning before BeginRenderPass when no mesh in scene - Split vertex_common.glsl / fragment_common.glsl into geometry_bindings.glsl, texture_bindings.glsl, material.glsl, draw.glsl, draw_types.glsl, surface.glsl to stop Int64/sampler extensions leaking into geometry-only shader stages - Redesign RenderPass API: binding-number reflection, BindingsByName O(1) map, arena-safe SetName/SetPipelineName, pipeline+shader name in error messages - Fix ImGUIRenderer: remove stale SetSampler("LinearWrapSampler") call Asset reload (second launch): - IsAssetExtension: add .zemesh and .zematerial so VFSScanner processes them - InferTypeFromExtension: map .zematerial -> MATERIAL (was falling through to MESH) - ImguiLayer: set Scanner.SetAssetRegistry and post startup TriggerScan - Add AssetManager::ReloadFromDisk — deserializes each unloaded MATERIAL/MESH record from disk and calls IngestMaterial/IngestMesh - Fix pre-existing buffer overflow in IngestMesh: sizeof(AssetNodeHierarchy) used instead of sizeof(NodeHierarchy) for the Hierarchies memcpy, overflowing 308 KB past the allocated block and corrupting the NodeNames hash map Containers: - Rewrite UnorderedHashMap: power-of-2 capacity, linear probing with bitmask, Array::init(cap,cap) + secure_memset(0), guard_load assertion at 75% - Fix secure_memset arg order: (dst, value, count, dst_size) was swapped, leaving all entry state bytes as garbage instead of EntryState::Empty - Rewrite HashMap (ordered): same fixes; per-slot default construction since prev/next must be size_type(-1), not 0 - 55 edge-case tests: power-of-2, tombstone reuse, collision cluster, large batch, ordered insertion invariants, sort_keys idempotent --- Resources/Shaders/bindings.glsl | 11 + Resources/Shaders/composite.frag | 5 +- Resources/Shaders/deferred_lighting.frag | 3 +- Resources/Shaders/deferred_lighting.vert | 2 +- Resources/Shaders/depth_prepass_scene.vert | 2 +- Resources/Shaders/draw.glsl | 38 ++ Resources/Shaders/draw_types.glsl | 30 + Resources/Shaders/fragment_common.glsl | 120 ---- Resources/Shaders/g_buffer.frag | 2 +- Resources/Shaders/g_buffer.vert | 2 +- Resources/Shaders/geometry_bindings.glsl | 35 ++ Resources/Shaders/light_types.glsl | 37 ++ Resources/Shaders/material.glsl | 18 + Resources/Shaders/material_types.glsl | 21 + Resources/Shaders/surface.glsl | 44 ++ Resources/Shaders/texture_bindings.glsl | 8 + Resources/Shaders/vertex_common.glsl | 95 ---- .../Components/AssetImporterUIComponent.h | 2 +- Tetragrama/Layers/ImguiLayer.cpp | 16 +- ZEngine/ZEngine/Core/Containers/HashMap.h | 538 +++++++----------- .../Core/Containers/UnorderedHashMap.h | 481 ++++++---------- .../Core/VFS/Registry/AssetRegistry.cpp | 14 +- .../ZEngine/Core/VFS/Registry/AssetRegistry.h | 1 + ZEngine/ZEngine/Core/VFS/VFSScanner.cpp | 2 +- ZEngine/ZEngine/Hardwares/DeviceSwapchain.cpp | 4 +- ZEngine/ZEngine/Hardwares/VulkanDevice.cpp | 2 + ZEngine/ZEngine/Hardwares/VulkanDevice.h | 15 +- .../ZEngine/Helpers/SerializerCommonHelper.h | 2 +- ZEngine/ZEngine/Managers/AssetManager.cpp | 78 ++- ZEngine/ZEngine/Managers/AssetManager.h | 9 +- .../Rendering/Renderers/GraphicRenderer.cpp | 60 +- .../Rendering/Renderers/ImGUIRenderer.cpp | 5 +- .../Renderers/RenderPasses/RenderPass.cpp | 138 ++--- .../Renderers/RenderPasses/RenderPass.h | 41 +- .../Rendering/Renderers/RendererPasses.cpp | 165 +----- .../Rendering/Renderers/RendererPasses.h | 6 +- ZEngine/ZEngine/Rendering/Shaders/Shader.cpp | 68 +-- ZEngine/ZEngine/Rendering/Shaders/Shader.h | 1 + ZEngine/tests/Containers/hashset_test.cpp | 2 +- .../tests/Containers/ordered_hashmap_test.cpp | 427 +++++++++----- .../tests/Containers/ordered_hashset_test.cpp | 2 +- .../Containers/unordered_hashmap_test.cpp | 381 ++++++++----- 42 files changed, 1431 insertions(+), 1502 deletions(-) create mode 100644 Resources/Shaders/bindings.glsl create mode 100644 Resources/Shaders/draw.glsl create mode 100644 Resources/Shaders/draw_types.glsl delete mode 100644 Resources/Shaders/fragment_common.glsl create mode 100644 Resources/Shaders/geometry_bindings.glsl create mode 100644 Resources/Shaders/light_types.glsl create mode 100644 Resources/Shaders/material.glsl create mode 100644 Resources/Shaders/material_types.glsl create mode 100644 Resources/Shaders/surface.glsl create mode 100644 Resources/Shaders/texture_bindings.glsl delete mode 100644 Resources/Shaders/vertex_common.glsl diff --git a/Resources/Shaders/bindings.glsl b/Resources/Shaders/bindings.glsl new file mode 100644 index 000000000..ff45174b9 --- /dev/null +++ b/Resources/Shaders/bindings.glsl @@ -0,0 +1,11 @@ +// Full bindings — geometry pipeline + global bindless textures + sampler. +// Use this in shaders that sample from TextureArray (e.g. g_buffer.frag). +// For passes that only need geometry (e.g. depth pre-pass vertex), use geometry_bindings.glsl. +#extension GL_EXT_nonuniform_qualifier : require + +#include "geometry_bindings.glsl" + +#define INVALID_MAP_HANDLE 0xFFFFFFFFu + +layout(set = 1, binding = 0) uniform texture2D TextureArray[]; +layout(set = 1, binding = 1) uniform sampler LinearWrapSampler; diff --git a/Resources/Shaders/composite.frag b/Resources/Shaders/composite.frag index d9425d3f1..dc5cd57d1 100644 --- a/Resources/Shaders/composite.frag +++ b/Resources/Shaders/composite.frag @@ -2,13 +2,12 @@ layout(location = 0) in vec2 outUV; -layout(set = 0, binding = 0) uniform texture2D sharedRTAsTex; +layout(set = 0, binding = 10) uniform texture2D sharedRTAsTex; layout(set = 1, binding = 1) uniform sampler LinearWrapSampler; layout(location = 0) out vec4 outColor; void main() { - vec4 color = texture(sampler2D(sharedRTAsTex, LinearWrapSampler), outUV); - outColor = color; + outColor = texture(sampler2D(sharedRTAsTex, LinearWrapSampler), outUV); } diff --git a/Resources/Shaders/deferred_lighting.frag b/Resources/Shaders/deferred_lighting.frag index 6d513b7fc..f17db3211 100644 --- a/Resources/Shaders/deferred_lighting.frag +++ b/Resources/Shaders/deferred_lighting.frag @@ -1,6 +1,7 @@ #version 460 #extension GL_GOOGLE_include_directive : require -#include "fragment_common.glsl" +#include "bindings.glsl" +#include "light_types.glsl" layout(location = 0) in vec2 TexCoord; layout(location = 1) in vec4 ViewPos; diff --git a/Resources/Shaders/deferred_lighting.vert b/Resources/Shaders/deferred_lighting.vert index 3d90e3d0a..1f081de8b 100644 --- a/Resources/Shaders/deferred_lighting.vert +++ b/Resources/Shaders/deferred_lighting.vert @@ -1,6 +1,6 @@ #version 460 #extension GL_GOOGLE_include_directive : require -#include "vertex_common.glsl" +#include "draw.glsl" layout(location = 0) out vec2 TexCoord; layout(location = 1) out vec4 ViewPos; diff --git a/Resources/Shaders/depth_prepass_scene.vert b/Resources/Shaders/depth_prepass_scene.vert index 709874818..6717b52c3 100644 --- a/Resources/Shaders/depth_prepass_scene.vert +++ b/Resources/Shaders/depth_prepass_scene.vert @@ -1,6 +1,6 @@ #version 460 #extension GL_GOOGLE_include_directive : require -#include "vertex_common.glsl" +#include "draw.glsl" void main() { diff --git a/Resources/Shaders/draw.glsl b/Resources/Shaders/draw.glsl new file mode 100644 index 000000000..4e583d254 --- /dev/null +++ b/Resources/Shaders/draw.glsl @@ -0,0 +1,38 @@ +// Pull-render helpers — assembles per-vertex data from the global geometry buffers. +// Uses geometry_bindings.glsl only — safe for all vertex stages including depth pre-pass. +#include "geometry_bindings.glsl" + +DrawDataView GetDrawDataView() +{ + DrawDataView dataView; + + DrawData dd = DrawDataBuffer.Data[gl_BaseInstance]; + uint refIdx = dd.IndexOffset + gl_VertexIndex; + uint verIdx = IndexBuffer.Data[refIdx] + dd.VertexOffset; + DrawVertex v = VertexBuffer.Data[verIdx]; + + dataView.Vertex = vec4(v.x, v.y, v.z, 1.0); + dataView.Normal = vec3(v.nx, v.ny, v.nz); + dataView.TexCoord = vec2(v.u, v.v); + dataView.Transform = TransformBuffer.Data[dd.TransformIndex]; + dataView.MaterialId = dd.MaterialIndex; + return dataView; +} + +DrawData FetchDrawData() +{ + return DrawDataBuffer.Data[gl_BaseInstance]; +} + +DrawVertex FetchVertexData() +{ + DrawData dd = FetchDrawData(); + uint refIdx = dd.IndexOffset + gl_VertexIndex; + uint verIdx = IndexBuffer.Data[refIdx] + dd.VertexOffset; + return VertexBuffer.Data[verIdx]; +} + +mat4 FetchTransform() +{ + return TransformBuffer.Data[FetchDrawData().TransformIndex]; +} diff --git a/Resources/Shaders/draw_types.glsl b/Resources/Shaders/draw_types.glsl new file mode 100644 index 000000000..de2e55045 --- /dev/null +++ b/Resources/Shaders/draw_types.glsl @@ -0,0 +1,30 @@ +// Vertex and draw-call data structures used by the pull-render pipeline. +// No descriptor bindings — include this wherever the structs are needed. + +struct DrawVertex +{ + float x, y, z; + float nx, ny, nz; + float u, v; +}; + +struct DrawData +{ + uint VertexOffset; + uint VertexCount; + uint IndexOffset; + uint IndexCount; + uint AllocationCount; + uint InstanceCount; + uint TransformIndex; + uint MaterialIndex; +}; + +struct DrawDataView +{ + uint MaterialId; + mat4 Transform; + vec4 Vertex; + vec3 Normal; + vec2 TexCoord; +}; diff --git a/Resources/Shaders/fragment_common.glsl b/Resources/Shaders/fragment_common.glsl deleted file mode 100644 index 7f125f25c..000000000 --- a/Resources/Shaders/fragment_common.glsl +++ /dev/null @@ -1,120 +0,0 @@ -#extension GL_EXT_nonuniform_qualifier : require -#extension GL_EXT_shader_explicit_arithmetic_types_int64 : enable - -struct MaterialData -{ - vec4 Ambient; - vec4 Emissive; - vec4 Albedo; - vec4 Specular; - vec4 Roughness; - vec4 Factors; // {x : transparency, y : Metallic, z : AlphaTest, w : _padding} - - uint64_t EmissiveMap; - uint64_t AlbedoMap; - uint64_t SpecularMap; - uint64_t NormalMap; - uint64_t OpacityMap; - uint64_t _padding; -}; - -struct DirectionalLight -{ - vec4 Direction; - vec4 Ambient; - vec4 Diffuse; - vec4 Specular; -}; - -struct PointLight -{ - vec4 Position; - vec4 Ambient; - vec4 Diffuse; - vec4 Specular; - - float Constant; - float Linear; - float Quadratic; - float _padding; -}; - -struct SpotLight -{ - vec4 Position; - vec4 Direction; - vec4 Ambient; - vec4 Diffuse; - vec4 Specular; - - float CutOff; - float Constant; - float Linear; - float Quadratic; -}; - -#define INVALID_MAP_HANDLE 0xFFFFFFFFu - -layout(std140, set = 0, binding = 5) readonly buffer MatSB -{ - MaterialData Data[]; -} -MaterialDataBuffer; - -layout(set = 1, binding = 0) uniform texture2D TextureArray[]; -layout(set = 1, binding = 1) uniform sampler LinearWrapSampler; - -MaterialData FetchMaterial(uint dataIndex) -{ - return MaterialDataBuffer.Data[dataIndex]; -} - -// http://www.thetenthplanet.de/archives/1180 -// modified to fix handedness of the resulting cotangent frame -mat3 cotangentFrame(vec3 N, vec3 p, vec2 uv) -{ - // get edge vectors of the pixel triangle - vec3 dp1 = dFdx(p); - vec3 dp2 = dFdy(p); - vec2 duv1 = dFdx(uv); - vec2 duv2 = dFdy(uv); - - // solve the linear system - vec3 dp2perp = cross(dp2, N); - vec3 dp1perp = cross(N, dp1); - vec3 T = dp2perp * duv1.x + dp1perp * duv2.x; - vec3 B = dp2perp * duv1.y + dp1perp * duv2.y; - - // construct a scale-invariant frame - float invmax = inversesqrt(max(dot(T, T), dot(B, B))); - - // calculate handedness of the resulting cotangent frame - float w = (dot(cross(N, T), B) < 0.0) ? -1.0 : 1.0; - - // adjust tangent if needed - T = T * w; - - return mat3(T * invmax, B * invmax, N); -} - -vec3 perturbNormal(vec3 n, vec3 v, vec3 normalSample, vec2 uv) -{ - vec3 map = normalize(2.0 * normalSample - vec3(1.0)); - mat3 TBN = cotangentFrame(n, v, uv); - return normalize(TBN * map); -} - -void runAlphaTest(float alpha, float alphaThreshold) -{ - if (alphaThreshold > 0.0) - { - // http://alex-charlton.com/posts/Dithering_on_the_GPU/ - // https://forums.khronos.org/showthread.php/5091-screen-door-transparency - mat4 thresholdMatrix = mat4(1.0 / 17.0, 9.0 / 17.0, 3.0 / 17.0, 11.0 / 17.0, 13.0 / 17.0, 5.0 / 17.0, 15.0 / 17.0, 7.0 / 17.0, 4.0 / 17.0, 12.0 / 17.0, 2.0 / 17.0, 10.0 / 17.0, 16.0 / 17.0, 8.0 / 17.0, 14.0 / 17.0, 6.0 / 17.0); - - alpha = clamp(alpha - 0.5 * thresholdMatrix[int(mod(gl_FragCoord.x, 4.0))][int(mod(gl_FragCoord.y, 4.0))], 0.0, 1.0); - - if (alpha < alphaThreshold) - discard; - } -} diff --git a/Resources/Shaders/g_buffer.frag b/Resources/Shaders/g_buffer.frag index 0fad4f354..2d0e403ea 100644 --- a/Resources/Shaders/g_buffer.frag +++ b/Resources/Shaders/g_buffer.frag @@ -1,6 +1,6 @@ #version 460 #extension GL_GOOGLE_include_directive : require -#include "fragment_common.glsl" +#include "material.glsl" layout(location = 0) in vec2 TexCoord; layout(location = 1) in vec3 WorldNormal; diff --git a/Resources/Shaders/g_buffer.vert b/Resources/Shaders/g_buffer.vert index 5ba8ae0f4..0e5e0f141 100644 --- a/Resources/Shaders/g_buffer.vert +++ b/Resources/Shaders/g_buffer.vert @@ -1,6 +1,6 @@ #version 460 #extension GL_GOOGLE_include_directive : require -#include "vertex_common.glsl" +#include "draw.glsl" layout(location = 0) out vec2 TexCoord; layout(location = 1) out vec3 WorldNormal; diff --git a/Resources/Shaders/geometry_bindings.glsl b/Resources/Shaders/geometry_bindings.glsl new file mode 100644 index 000000000..1bd47e661 --- /dev/null +++ b/Resources/Shaders/geometry_bindings.glsl @@ -0,0 +1,35 @@ +// Geometry pipeline bindings — camera, vertex, index, draw data, transform. +// No material, no textures, no samplers — safe to include in any pass. +#include "draw_types.glsl" + +layout(set = 0, binding = 0) uniform UBCamera +{ + mat4 View; + mat4 Projection; + vec4 Position; +} +Camera; + +layout(set = 0, binding = 1) readonly buffer VertexSB +{ + DrawVertex Data[]; +} +VertexBuffer; + +layout(set = 0, binding = 2) readonly buffer IndexSB +{ + uint Data[]; +} +IndexBuffer; + +layout(set = 0, binding = 3) readonly buffer DrawDataSB +{ + DrawData Data[]; +} +DrawDataBuffer; + +layout(set = 0, binding = 4) readonly buffer TransformSB +{ + mat4 Data[]; +} +TransformBuffer; diff --git a/Resources/Shaders/light_types.glsl b/Resources/Shaders/light_types.glsl new file mode 100644 index 000000000..699d177f2 --- /dev/null +++ b/Resources/Shaders/light_types.glsl @@ -0,0 +1,37 @@ +// Light data structures — composable by any pass that needs lighting. +// No descriptor bindings. + +struct DirectionalLight +{ + vec4 Direction; + vec4 Ambient; + vec4 Diffuse; + vec4 Specular; +}; + +struct PointLight +{ + vec4 Position; + vec4 Ambient; + vec4 Diffuse; + vec4 Specular; + + float Constant; + float Linear; + float Quadratic; + float _padding; +}; + +struct SpotLight +{ + vec4 Position; + vec4 Direction; + vec4 Ambient; + vec4 Diffuse; + vec4 Specular; + + float CutOff; + float Constant; + float Linear; + float Quadratic; +}; diff --git a/Resources/Shaders/material.glsl b/Resources/Shaders/material.glsl new file mode 100644 index 000000000..d1974baec --- /dev/null +++ b/Resources/Shaders/material.glsl @@ -0,0 +1,18 @@ +// Material bindings and accessor — fragment-stage only. +// Declares set=1 (TextureArray, LinearWrapSampler) and MatSB. +// Does NOT pull in geometry buffers so no duplicate-binding conflicts with vertex stage. +#extension GL_EXT_shader_explicit_arithmetic_types_int64 : enable + +#include "material_types.glsl" +#include "texture_bindings.glsl" + +layout(std140, set = 0, binding = 5) readonly buffer MatSB +{ + MaterialData Data[]; +} +MaterialDataBuffer; + +MaterialData FetchMaterial(uint dataIndex) +{ + return MaterialDataBuffer.Data[dataIndex]; +} diff --git a/Resources/Shaders/material_types.glsl b/Resources/Shaders/material_types.glsl new file mode 100644 index 000000000..9dd0970c5 --- /dev/null +++ b/Resources/Shaders/material_types.glsl @@ -0,0 +1,21 @@ +// Material data structure. +// No descriptor bindings — include wherever MaterialData is needed. +// Note: MaterialData uses uint64_t; include material.glsl (not this file directly) +// in shader stages that need the full material pipeline — it enables Int64 there. + +struct MaterialData +{ + vec4 Ambient; + vec4 Emissive; + vec4 Albedo; + vec4 Specular; + vec4 Roughness; + vec4 Factors; // {x : transparency, y : Metallic, z : AlphaTest, w : _padding} + + uint64_t EmissiveMap; + uint64_t AlbedoMap; + uint64_t SpecularMap; + uint64_t NormalMap; + uint64_t OpacityMap; + uint64_t _padding; +}; diff --git a/Resources/Shaders/surface.glsl b/Resources/Shaders/surface.glsl new file mode 100644 index 000000000..f38198cf2 --- /dev/null +++ b/Resources/Shaders/surface.glsl @@ -0,0 +1,44 @@ +// Screen-space surface utilities — normal mapping and alpha test. +// No descriptor bindings. + +// http://www.thetenthplanet.de/archives/1180 +// Modified to fix handedness of the resulting cotangent frame. +mat3 cotangentFrame(vec3 N, vec3 p, vec2 uv) +{ + vec3 dp1 = dFdx(p); + vec3 dp2 = dFdy(p); + vec2 duv1 = dFdx(uv); + vec2 duv2 = dFdy(uv); + + vec3 dp2perp = cross(dp2, N); + vec3 dp1perp = cross(N, dp1); + vec3 T = dp2perp * duv1.x + dp1perp * duv2.x; + vec3 B = dp2perp * duv1.y + dp1perp * duv2.y; + + float invmax = inversesqrt(max(dot(T, T), dot(B, B))); + float w = (dot(cross(N, T), B) < 0.0) ? -1.0 : 1.0; + T = T * w; + + return mat3(T * invmax, B * invmax, N); +} + +vec3 perturbNormal(vec3 n, vec3 v, vec3 normalSample, vec2 uv) +{ + vec3 map = normalize(2.0 * normalSample - vec3(1.0)); + mat3 TBN = cotangentFrame(n, v, uv); + return normalize(TBN * map); +} + +void runAlphaTest(float alpha, float alphaThreshold) +{ + if (alphaThreshold > 0.0) + { + // http://alex-charlton.com/posts/Dithering_on_the_GPU/ + mat4 thresholdMatrix = mat4(1.0 / 17.0, 9.0 / 17.0, 3.0 / 17.0, 11.0 / 17.0, 13.0 / 17.0, 5.0 / 17.0, 15.0 / 17.0, 7.0 / 17.0, 4.0 / 17.0, 12.0 / 17.0, 2.0 / 17.0, 10.0 / 17.0, 16.0 / 17.0, 8.0 / 17.0, 14.0 / 17.0, 6.0 / 17.0); + + alpha = clamp(alpha - 0.5 * thresholdMatrix[int(mod(gl_FragCoord.x, 4.0))][int(mod(gl_FragCoord.y, 4.0))], 0.0, 1.0); + + if (alpha < alphaThreshold) + discard; + } +} diff --git a/Resources/Shaders/texture_bindings.glsl b/Resources/Shaders/texture_bindings.glsl new file mode 100644 index 000000000..2d0dc0712 --- /dev/null +++ b/Resources/Shaders/texture_bindings.glsl @@ -0,0 +1,8 @@ +// Set=1 bindings — global bindless texture array and sampler. +// Safe to include in fragment shaders without pulling in geometry buffers. +#extension GL_EXT_nonuniform_qualifier : require + +#define INVALID_MAP_HANDLE 0xFFFFFFFFu + +layout(set = 1, binding = 0) uniform texture2D TextureArray[]; +layout(set = 1, binding = 1) uniform sampler LinearWrapSampler; diff --git a/Resources/Shaders/vertex_common.glsl b/Resources/Shaders/vertex_common.glsl deleted file mode 100644 index fa1d9d276..000000000 --- a/Resources/Shaders/vertex_common.glsl +++ /dev/null @@ -1,95 +0,0 @@ -struct DrawVertex -{ - float x, y, z; - float nx, ny, nz; - float u, v; -}; - -struct DrawData -{ - uint VertexOffset; - uint VertexCount; - uint IndexOffset; - uint IndexCount; - uint AllocationCount; - uint InstanceCount; - uint TransformIndex; - uint MaterialIndex; -}; - -struct DrawDataView -{ - uint MaterialId; - mat4 Transform; - vec4 Vertex; - vec3 Normal; - vec2 TexCoord; -}; - -layout(set = 0, binding = 0) uniform UBCamera -{ - mat4 View; - mat4 Projection; - vec4 Position; -} -Camera; - -layout(set = 0, binding = 1) readonly buffer VertexSB -{ - DrawVertex Data[]; -} -VertexBuffer; -layout(set = 0, binding = 2) readonly buffer IndexSB -{ - uint Data[]; -} -IndexBuffer; -layout(set = 0, binding = 3) readonly buffer DrawDataSB -{ - DrawData Data[]; -} -DrawDataBuffer; -layout(set = 0, binding = 4) readonly buffer TransformSB -{ - mat4 Data[]; -} -TransformBuffer; - -DrawDataView GetDrawDataView() -{ - DrawDataView dataView; - - DrawData dd = DrawDataBuffer.Data[gl_BaseInstance]; - uint refIdx = dd.IndexOffset + gl_VertexIndex; - uint verIdx = IndexBuffer.Data[refIdx] + dd.VertexOffset; - DrawVertex v = VertexBuffer.Data[verIdx]; - - dataView.Vertex = vec4(v.x, v.y, v.z, 1.0); - dataView.Normal = vec3(v.nx, v.ny, v.nz); - dataView.TexCoord = vec2(v.u, v.v); - dataView.Transform = TransformBuffer.Data[dd.TransformIndex]; - dataView.MaterialId = dd.MaterialIndex; - return dataView; -} - -DrawData FetchDrawData() -{ - return DrawDataBuffer.Data[gl_BaseInstance]; -} - -DrawVertex FetchVertexData() -{ - DrawData dd = FetchDrawData(); - - uint refIdx = dd.IndexOffset + gl_VertexIndex; - uint verIdx = IndexBuffer.Data[refIdx] + dd.VertexOffset; - DrawVertex v = VertexBuffer.Data[verIdx]; - - return v; -} - -mat4 FetchTransform() -{ - DrawData dd = FetchDrawData(); - return TransformBuffer.Data[dd.TransformIndex]; -} \ No newline at end of file diff --git a/Tetragrama/Components/AssetImporterUIComponent.h b/Tetragrama/Components/AssetImporterUIComponent.h index 8462c13ca..679518bef 100644 --- a/Tetragrama/Components/AssetImporterUIComponent.h +++ b/Tetragrama/Components/AssetImporterUIComponent.h @@ -32,6 +32,7 @@ namespace Tetragrama::Components void Initialize(Layers::ImguiLayer* parent = nullptr, cstring name = "Asset Importer", bool visibility = true, bool closed = false) override; void Update(ZEngine::Core::TimeStep dt) override; virtual void Render(ZEngine::Rendering::Renderers::GraphicRenderer* const renderer, ZEngine::Hardwares::CommandBuffer* const command_buffer) override; + void TriggerScan(); // main-thread only private: PaddedAtomic m_state{}; // default = Idle (0) @@ -90,7 +91,6 @@ namespace Tetragrama::Components void PushLog(cstring text, const float color[4]); void PushHistory(cstring name, bool success, cstring msg); - void TriggerScan(); // main-thread only void StartImport(); void BrowseFile(); void RenderIdle(); diff --git a/Tetragrama/Layers/ImguiLayer.cpp b/Tetragrama/Layers/ImguiLayer.cpp index 10b39adb5..da5d0e5d9 100644 --- a/Tetragrama/Layers/ImguiLayer.cpp +++ b/Tetragrama/Layers/ImguiLayer.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include #include @@ -33,6 +35,15 @@ namespace Tetragrama::Layers arena->CreateSubArena(ZMega(64), &LocalArena); Scanner.Initialize(arena); + Scanner.SetAssetRegistry(ZEngine::Managers::AssetManager::Instance()->Registry); + Scanner.SetOnScanComplete(this, [](void* ctx, ZEngine::Core::VFS::ScanStats) { + ZEngine::Core::MainThreadScheduler::Post(ctx, [](void* p) { + auto* layer = static_cast(p); + auto scratch = ZGetScratch(&layer->LocalArena); + ZEngine::Managers::AssetManager::ReloadFromDisk(scratch.Arena); + ZReleaseScratch(scratch); + }); + }); Cache.Initialize(arena); NodeHierarchies.init(arena, 10, 0); @@ -82,9 +93,6 @@ namespace Tetragrama::Layers dockspace_cmp->ChildrenCount = dockspace_cmp->Children.size(); AddUIComponent(dockspace_cmp, -1, 0); - /* - * Register Dockspace Component - */ IMessenger::Register>(dockspace_cmp, EDITOR_COMPONENT_DOCKSPACE_REQUEST_OPENSCENE, [=](void* const message) -> std::future { auto message_ptr = reinterpret_cast*>(message); const auto& value = message_ptr->GetValue(); @@ -96,6 +104,8 @@ namespace Tetragrama::Layers const auto& value = message_ptr->GetValue(); return dockspace_cmp->OnOpenMeshRequestAsync(value.c_str()); }); + + ZEngine::Core::MainThreadScheduler::Post(importer_cmp, [](void* ctx) { reinterpret_cast(ctx)->TriggerScan(); }); } void ImguiLayer::Deinitialize() diff --git a/ZEngine/ZEngine/Core/Containers/HashMap.h b/ZEngine/ZEngine/Core/Containers/HashMap.h index 44f04068b..b7527a1bc 100644 --- a/ZEngine/ZEngine/Core/Containers/HashMap.h +++ b/ZEngine/ZEngine/Core/Containers/HashMap.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include #include @@ -11,20 +11,30 @@ #include #include +// Insertion-ordered open-addressing hash map. +// +// Same capacity contract as UnorderedHashMap: +// - Capacity always a power of 2; init() rounds up to next pow2 ≥ 16. +// - Pass at least 2× expected entry count so load stays ≤50%. +// - Linear probing with bitmask — visits every slot for any table size. +// Insertion order is preserved via a doubly-linked list threaded through +// the slot array (prev/next indices; size_type(-1) = end sentinel). + namespace ZEngine::Core::Containers { - enum class EntryState + // EntryState is also defined in UnorderedHashMap.h — keep both identical. + enum class EntryState : uint8_t { - Empty, - Occupied, - Deleted + Empty = 0, + Occupied = 1, + Deleted = 2 }; template struct OrderedHashEntry { - K key; - V value; + K key = {}; + V value = {}; EntryState state = EntryState::Empty; std::size_t prev = std::size_t(-1); std::size_t next = std::size_t(-1); @@ -42,55 +52,35 @@ namespace ZEngine::Core::Containers using iterator_category = std::forward_iterator_tag; using difference_type = std::ptrdiff_t; - // Constructs an iterator for the hash map's entries, - // @param entries Pointer to the hash table slot array. - // @param index Current slot index; OrderedHashMapIterator(EntryPointer entries, std::size_t index) : m_entries(entries), m_index(index) {} + OrderedHashMapIterator& operator++() { m_index = m_entries[m_index].next; return *this; } - // Checks if two iterators are not equal based on their index. - // @param other The iterator to compare with. - // @return True if the iterators point to different indices, false otherwise. - bool operator!=(const OrderedHashMapIterator& other) const + bool operator!=(const OrderedHashMapIterator& o) const { - return m_index != other.m_index; + return m_index != o.m_index; } - - // Checks if two iterators are equal based on their index. - // @param other The iterator to compare with. - // @return True if the iterators point to the same index, false otherwise. - bool operator==(const OrderedHashMapIterator& other) const + bool operator==(const OrderedHashMapIterator& o) const { - return m_index == other.m_index; + return m_index == o.m_index; } - // Dereferences the iterator to return a key-value pair for the current entry. - // @return A pair containing references to the key and value (const or non-const based on IsConst). value_type operator*() const { - const auto& entry = m_entries[m_index]; if constexpr (IsConst) - { - return {entry.key, entry.value}; - } + return {m_entries[m_index].key, m_entries[m_index].value}; else - { - return {entry.key, const_cast(entry.value)}; - } + return {m_entries[m_index].key, const_cast(m_entries[m_index].value)}; } - // Provides pointer-like access to the current key-value pair. - // @return A pointer to a temporary key-value pair. pointer operator->() const { return std::addressof(**this); } - - // Returns a const reference to the key at the current position. const K& key() const { return m_entries[m_index].key; @@ -110,160 +100,122 @@ namespace ZEngine::Core::Containers using iterator = OrderedHashMapIterator; using const_iterator = OrderedHashMapIterator; - // Initializes the hash map with an allocator, initial capacity, and load factor. - // @param allocator Pointer to the arena allocator for memory management. - // @param initial_capacity Initial number of slots (default: 16). - // @param load_factor Maximum load factor before resizing (default: 0.75). - void init(Memory::ArenaAllocator* allocator, size_type initial_capacity = 16) - { - m_allocator = allocator; - m_load_factor = 0.75f; - m_entries.init(m_allocator, initial_capacity); - for (size_type i = 0; i < initial_capacity; ++i) - { - m_entries.push({}); - } + // Pre-allocate capacity slots (rounded to next power of 2 ≥ 16). + // Uses Array::init(arena, cap, cap) + per-slot default construction so + // prev/next are correctly set to size_type(-1) — cannot use memset(0). + void init(Memory::ArenaAllocator* allocator, size_type slot_capacity = 16) + { + m_allocator = allocator; + size_type cap = next_pow2(slot_capacity < 16 ? 16 : slot_capacity); + m_capacity_mask = cap - 1; + m_entries.init(m_allocator, cap, cap); + for (size_type i = 0; i < cap; ++i) + m_entries[i] = Entry{}; m_size = 0; m_head = size_type(-1); m_tail = size_type(-1); } - // Inserts a key-value pair into the hash map, updating the value if the key exists. - // Resizes the map if the load factor would be exceeded. - // @param key The key to insert. - // @param value The value to associate with the key. - // @throws std::runtime_error if the table is full and cannot be resized. void insert(const K& key, const V& value) requires std::is_copy_assignable_v { - maybe_grow(); - - size_type index = probe_for_insert(key); - auto& entry = m_entries[index]; - - if (entry.state == EntryState::Empty || entry.state == EntryState::Deleted) + guard_load(); + size_type idx = probe_insert(key); + Entry& e = m_entries[idx]; + if (e.state != EntryState::Occupied) { - entry.key = key; - entry.value = value; - entry.state = EntryState::Occupied; - link_tail(index); + e.key = key; + e.value = value; + e.state = EntryState::Occupied; + link_tail(idx); ++m_size; } else { - entry.value = value; + e.value = value; } } void insert(const K& key, V&& value) { - maybe_grow(); - - size_type index = probe_for_insert(key); - auto& entry = m_entries[index]; - - if (entry.state == EntryState::Empty || entry.state == EntryState::Deleted) + guard_load(); + size_type idx = probe_insert(key); + Entry& e = m_entries[idx]; + if (e.state != EntryState::Occupied) { - entry.key = key; - entry.value = std::move(value); - entry.state = EntryState::Occupied; - link_tail(index); + e.key = key; + e.value = std::move(value); + e.state = EntryState::Occupied; + link_tail(idx); ++m_size; } else { - entry.value = std::move(value); + e.value = std::move(value); } } V& operator[](const K& key) { - maybe_grow(); - - size_type index = probe_for_insert(key); - auto& entry = m_entries[index]; - - if (entry.state == EntryState::Empty || entry.state == EntryState::Deleted) + guard_load(); + size_type idx = probe_insert(key); + Entry& e = m_entries[idx]; + if (e.state != EntryState::Occupied) { - entry.key = key; - entry.value = V{}; - entry.state = EntryState::Occupied; - link_tail(index); + e.key = key; + e.value = V{}; + e.state = EntryState::Occupied; + link_tail(idx); ++m_size; } - return entry.value; + return e.value; } - // Retrieves a const reference to the value associated with a key. - // @param key The key to look up. - // @return Const reference to the value. - // @throws std::out_of_range if the key is not found. const V& at(const K& key) const { - size_type index = probe_for_key(key); - if (index == size_type(-1)) - { - throw std::out_of_range("Key not found in HashMap"); - } - return m_entries[index].value; + size_type idx = probe_find(key); + ZENGINE_VALIDATE_ASSERT(idx != npos, "HashMap::at: key not found") + return m_entries[idx].value; } - // Finds the value associated with a key. - // @param key The key to look up. - // @return Pointer to the value if found, nullptr otherwise. V* find(const K& key) { - size_type index = probe_for_key(key); - return (index != size_type(-1)) ? &m_entries[index].value : nullptr; + size_type i = probe_find(key); + return i != npos ? &m_entries[i].value : nullptr; } - - // Finds the value associated with a key (const version). - // @param key The key to look up. - // @return Const pointer to the value if found, nullptr otherwise. const V* find(const K& key) const { - size_type index = probe_for_key(key); - return (index != size_type(-1)) ? &m_entries[index].value : nullptr; + size_type i = probe_find(key); + return i != npos ? &m_entries[i].value : nullptr; } - // Returns a pointer to the stored key if found, nullptr otherwise. - // Useful for callers that need a stable pointer into the map's key storage. const K* find_key(const K& key) const { - size_type index = probe_for_key(key); - return (index != size_type(-1)) ? &m_entries[index].key : nullptr; + size_type i = probe_find(key); + return i != npos ? &m_entries[i].key : nullptr; } - // Checks if a key exists in the hash map. - // @param key The key to check. - // @return True if the key exists, false otherwise. bool contains(const K& key) const { - return find(key) != nullptr; + return probe_find(key) != npos; } - // Removes the entry for a key, preserving insertion order of remaining entries. - // @note Marks the slot as Deleted; does not shrink the table. void remove(const K& key) { - size_type index = probe_for_key(key); - if (index != size_type(-1)) + size_type idx = probe_find(key); + if (idx != npos) { - unlink(index); - m_entries[index].state = EntryState::Deleted; + unlink(idx); + m_entries[idx].state = EntryState::Deleted; --m_size; } } - // Clears all entries, resetting insertion-order state without changing capacity. void clear() { - for (size_type i = 0; i < m_entries.size(); ++i) - { - m_entries[i].state = EntryState::Empty; - m_entries[i].prev = size_type(-1); - m_entries[i].next = size_type(-1); - } + size_type cap = capacity(); + for (size_type i = 0; i < cap; ++i) + m_entries[i] = Entry{}; m_size = 0; m_head = size_type(-1); m_tail = size_type(-1); @@ -272,315 +224,223 @@ namespace ZEngine::Core::Containers void sort_keys() { if (m_size < 2) - { return; - } auto scratch = ZGetScratch(m_allocator); - Array indices; indices.init(scratch.Arena, m_size); - size_type cur = m_head; - while (cur != size_type(-1)) - { + for (size_type cur = m_head; cur != npos; cur = m_entries[cur].next) indices.push(cur); - cur = m_entries[cur].next; - } - // Sort indices by key. std::sort(indices.data(), indices.data() + indices.size(), [this](size_type a, size_type b) { return key_less(m_entries[a].key, m_entries[b].key); }); - // Rebuild linked list in the new order. m_head = indices[0]; m_tail = indices[indices.size() - 1]; - for (size_type i = 0; i < indices.size(); ++i) { - m_entries[indices[i]].prev = (i > 0) ? indices[i - 1] : size_type(-1); - m_entries[indices[i]].next = (i + 1 < indices.size()) ? indices[i + 1] : size_type(-1); + m_entries[indices[i]].prev = (i > 0) ? indices[i - 1] : npos; + m_entries[indices[i]].next = (i + 1 < indices.size()) ? indices[i + 1] : npos; } ZReleaseScratch(scratch); } - // Checks if the hash map is empty. - // @return True if the map contains no key-value pairs, false otherwise. bool empty() const { return m_size == 0; } - - // Returns the number of key-value pairs in the hash map. - // @return The number of occupied entries. size_type size() const { return m_size; } - - // Returns the current capacity of the hash map. - // @return The number of slots in the underlying array. size_type capacity() const { return m_entries.size(); } - // Returns an iterator to the first occupied entry. - // @return Iterator pointing to the first key-value pair or end() if empty. - // @note Iterators are invalidated by insert, remove, or reserve operations. - iterator begin() + // Grow to next_pow2(new_count) — allocates a new Array block, abandons old. + // Call sparingly; pre-size correctly via init() to avoid this entirely. + void reserve(size_type new_count) { - return iterator(m_entries.data(), m_head); + size_type new_cap = next_pow2(new_count); + if (new_cap > capacity()) + rehash(new_cap); } - // Returns an iterator to the end of the hash map. - // @return Iterator representing the past-the-end position. + iterator begin() + { + return {m_entries.data(), m_head}; + } iterator end() { - return iterator(m_entries.data(), size_type(-1)); + return {m_entries.data(), npos}; } - - // Returns a const iterator to the first occupied entry. - // @return Const iterator pointing to the first key-value pair or end() if empty. - // @note Iterators are invalidated by insert, remove, or reserve operations. const_iterator begin() const { - return const_iterator(m_entries.data(), m_head); + return {m_entries.data(), m_head}; } - - // Returns a const iterator to the end of the hash map. - // @return Const iterator representing the past-the-end position. const_iterator end() const { - return const_iterator(m_entries.data(), size_type(-1)); + return {m_entries.data(), npos}; } - - // Returns a const iterator to the first occupied entry (alias for begin() const). - // @return Const iterator pointing to the first key-value pair or end() if empty. const_iterator cbegin() const { return begin(); } - - // Returns a const iterator to the end of the hash map (alias for end() const). - // @return Const iterator representing the past-the-end position. const_iterator cend() const { return end(); } - // Ensures the hash map has at least the specified capacity. - // @param new_capacity Desired minimum number of slots. - // @note Rehashes the map if the new capacity is greater than the current capacity. - void reserve(size_type new_capacity) - { - if (new_capacity > m_entries.size()) - { - rehash(new_capacity); - } - } - private: - // Appends slot index to the tail of the insertion-order linked list. - void link_tail(size_type index) - { - m_entries[index].prev = m_tail; - m_entries[index].next = size_type(-1); - if (m_tail != size_type(-1)) - { - m_entries[m_tail].next = index; - } - else - { - m_head = index; - } - m_tail = index; - } + static constexpr size_type npos = size_type(-1); - // Removes slot index from the insertion-order linked list. - void unlink(size_type index) + static size_type next_pow2(size_type n) { - auto& entry = m_entries[index]; - if (entry.prev != size_type(-1)) - { - m_entries[entry.prev].next = entry.next; - } - else - { - m_head = entry.next; - } - if (entry.next != size_type(-1)) - { - m_entries[entry.next].prev = entry.prev; - } - else - { - m_tail = entry.prev; - } - entry.prev = size_type(-1); - entry.next = size_type(-1); + if (n == 0) + return 1; + --n; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 8; + n |= n >> 16; + if constexpr (sizeof(size_type) > 4) + n |= n >> 32; + return n + 1; } - void maybe_grow() - { - if (static_cast(m_size + 1) / m_entries.size() > m_load_factor) - { - size_type new_capacity = std::max(16, static_cast(m_entries.size() * 1.5f)); - rehash(new_capacity); - } - } - - // Compare keys, specialized for const char* bool key_equals(const K& a, const K& b) const { if constexpr (std::is_same_v) - { return Helpers::secure_strcmp(a, b) == 0; - } else - { return a == b; - } } bool key_less(const K& a, const K& b) const { if constexpr (std::is_same_v) - { return Helpers::secure_strcmp(a, b) < 0; - } else - { return a < b; - } } - // Rehashes the hash map to a new capacity, reinserting all occupied entries. - // @param new_capacity The new number of slots. - // @note Moves the old entries to avoid copying and skips Deleted entries. - void rehash(size_type new_capacity) + size_type hash_of(const K& key) const { - Array old_entries = std::move(m_entries); - size_type old_head = m_head; + if constexpr (std::is_same_v) + return rapidhash(key, Helpers::secure_strlen(key)); + else + return rapidhash(&key, sizeof(K)); + } - m_entries = Array{}; - m_entries.init(m_allocator, new_capacity); - for (size_type i = 0; i < new_capacity; ++i) + size_type probe_find(const K& key) const + { + if (m_entries.empty()) + return npos; + size_type idx = hash_of(key) & m_capacity_mask; + for (size_type i = 0; i <= m_capacity_mask; ++i) { - m_entries.push({}); + const Entry& e = m_entries[idx]; + if (e.state == EntryState::Empty) + return npos; + if (e.state == EntryState::Occupied && key_equals(e.key, key)) + return idx; + idx = (idx + 1) & m_capacity_mask; } - m_size = 0; - - m_head = size_type(-1); - m_tail = size_type(-1); + return npos; + } - size_type cur = old_head; - while (cur != size_type(-1)) + size_type probe_insert(const K& key) + { + ZENGINE_VALIDATE_ASSERT(!m_entries.empty(), "HashMap: call init() before insert") + size_type idx = hash_of(key) & m_capacity_mask; + size_type tombstone = npos; + for (size_type i = 0; i <= m_capacity_mask; ++i) { - size_type old_next = old_entries[cur].next; - size_type index = probe_for_insert(old_entries[cur].key); - auto& entry = m_entries[index]; - entry.key = std::move(old_entries[cur].key); - entry.value = std::move(old_entries[cur].value); - entry.state = EntryState::Occupied; - link_tail(index); - ++m_size; - cur = old_next; + Entry& e = m_entries[idx]; + if (e.state == EntryState::Occupied && key_equals(e.key, key)) + return idx; + if (e.state == EntryState::Empty) + return tombstone != npos ? tombstone : idx; + if (e.state == EntryState::Deleted && tombstone == npos) + tombstone = idx; + idx = (idx + 1) & m_capacity_mask; } + if (tombstone != npos) + return tombstone; + ZENGINE_VALIDATE_ASSERT(false, "HashMap: table full — pre-size with init(arena, 2*count)") + return npos; } - // Probes for a key using quadratic probing. - // @param key The key to look up. - // @return Index of the key if found, or size_type(-1) if not found. - size_type probe_for_key(const K& key) const + void guard_load() { - size_type index = hash(key) % m_entries.size(); - size_type i = 0; - - do - { - const auto& entry = m_entries[index]; - if (entry.state == EntryState::Empty) - { - return size_type(-1); - } - if (entry.state == EntryState::Occupied && key_equals(entry.key, key)) - { - return index; - } - ++i; - index = (index + i) % m_entries.size(); - } while (i < m_entries.size()); - - return size_type(-1); - } - - // Probes for a slot to insert a key, preferring deleted slots if available. - // @param key The key to insert. - // @return Index of the slot to use for insertion or the existing key. - // @throws std::runtime_error if the table is full and no slot is found. - size_type probe_for_insert(const K& key) - { - size_type index = hash(key) % m_entries.size(); - size_type first_deleted = size_type(-1); - size_type i = 0; - - do - { - auto& entry = m_entries[index]; - if (entry.state == EntryState::Occupied && key_equals(entry.key, key)) - { - return index; - } - - if (entry.state == EntryState::Empty) - { - return (first_deleted != size_type(-1)) ? first_deleted : index; - } - - if (entry.state == EntryState::Deleted && first_deleted == size_type(-1)) - { - first_deleted = index; - } - - ++i; - index = (index + i) % m_entries.size(); - } while (i < m_entries.size()); - - if (first_deleted != size_type(-1)) - { - return first_deleted; - } + if (m_entries.empty()) + return; + ZENGINE_VALIDATE_ASSERT(static_cast(m_size + 1) / static_cast(capacity()) <= 0.75f, "HashMap: load > 75% — call reserve() or increase init() capacity") + } - throw std::runtime_error("HashMap probe failed: table full"); + void link_tail(size_type idx) + { + m_entries[idx].prev = m_tail; + m_entries[idx].next = npos; + if (m_tail != npos) + m_entries[m_tail].next = idx; + else + m_head = idx; + m_tail = idx; } - // Computes the hash value for a key using the provided hasher. - // @param key The key to hash. - // @return The hash value. - size_type hash(const K& key) const + void unlink(size_type idx) { - if constexpr (std::is_same_v) - { - return rapidhash(key, Helpers::secure_strlen(key)); - } + auto& e = m_entries[idx]; + if (e.prev != npos) + m_entries[e.prev].next = e.next; else + m_head = e.next; + if (e.next != npos) + m_entries[e.next].prev = e.prev; + else + m_tail = e.prev; + e.prev = npos; + e.next = npos; + } + + void rehash(size_type new_cap) + { + Array old = std::move(m_entries); + size_type old_head = m_head; + + m_capacity_mask = new_cap - 1; + m_entries.init(m_allocator, new_cap, new_cap); + for (size_type i = 0; i < new_cap; ++i) + m_entries[i] = Entry{}; + m_size = 0; + m_head = npos; + m_tail = npos; + + for (size_type cur = old_head; cur != npos;) { - return rapidhash(&key, sizeof(K)); + size_type next = old[cur].next; + size_type idx = probe_insert(old[cur].key); + m_entries[idx].key = std::move(old[cur].key); + m_entries[idx].value = std::move(old[cur].value); + m_entries[idx].state = EntryState::Occupied; + link_tail(idx); + ++m_size; + cur = next; } } Memory::ArenaAllocator* m_allocator = nullptr; Array m_entries; - size_type m_size = 0; - float m_load_factor = 0.75f; - size_type m_head = size_type(-1); - size_type m_tail = size_type(-1); + size_type m_capacity_mask = 0; + size_type m_size = 0; + size_type m_head = npos; + size_type m_tail = npos; }; - // Computes a hash value for a C-string using rapidhash. - // @param str The null-terminated string to hash. - // @return The hash value. inline uint64_t hash_compute(const char* str) { return rapidhash(str, Helpers::secure_strlen(str)); diff --git a/ZEngine/ZEngine/Core/Containers/UnorderedHashMap.h b/ZEngine/ZEngine/Core/Containers/UnorderedHashMap.h index 42ad49f4f..7653c2651 100644 --- a/ZEngine/ZEngine/Core/Containers/UnorderedHashMap.h +++ b/ZEngine/ZEngine/Core/Containers/UnorderedHashMap.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include #include @@ -7,21 +7,32 @@ #include #include #include +#include + +// Open-addressing hash map backed by Array. +// +// Capacity contract: +// - Always a power of 2. init() rounds the requested slot count up to the next +// power of 2 (minimum 16) so (index + 1) & mask visits every slot. +// - Pass at least 2× the number of entries you intend to insert so the load +// stays ≤50% and rehash is never needed. +// - Capacity is pre-allocated via Array::init(arena, cap, cap) — no push loop, +// all slots are zeroed (EntryState::Empty = 0) and no Arena waste on grow. namespace ZEngine::Core::Containers { - enum class EntryState + enum class EntryState : uint8_t { - Empty, - Occupied, - Deleted + Empty = 0, + Occupied = 1, + Deleted = 2, }; template struct HashEntry { - K key; - V value; + K key = {}; + V value = {}; EntryState state = EntryState::Empty; }; @@ -37,80 +48,50 @@ namespace ZEngine::Core::Containers using iterator_category = std::input_iterator_tag; using difference_type = std::ptrdiff_t; - // Constructs an iterator for the hash map's entries, starting at the given index. - // @param entries Value to the array of hash map entries. - // @param index Starting index for iteration. - HashMapIterator(EntryPointer entries, std::size_t index, std::size_t size) : m_entries(entries), m_index(index), m_size(size) + HashMapIterator(EntryPointer entries, std::size_t index, std::size_t capacity) : m_entries(entries), m_index(index), m_capacity(capacity) { - advance_to_valid(); + skip_to_occupied(); } - // Advances the iterator to the next occupied entry. - // @return Reference to the incremented iterator. HashMapIterator& operator++() { ++m_index; - advance_to_valid(); + skip_to_occupied(); return *this; } - // Checks if two iterators are not equal based on their index. - // @param other The iterator to compare with. - // @return True if the iterators point to different indices, false otherwise. - bool operator!=(const HashMapIterator& other) const + bool operator!=(const HashMapIterator& o) const { - return m_index != other.m_index; + return m_index != o.m_index; } - - // Checks if two iterators are equal based on their index. - // @param other The iterator to compare with. - // @return True if the iterators point to the same index, false otherwise. - bool operator==(const HashMapIterator& other) const + bool operator==(const HashMapIterator& o) const { - return m_index == other.m_index; + return m_index == o.m_index; } - // Dereferences the iterator to return a key-value pair for the current entry. - // @return A pair containing references to the key and value (const or non-const based on IsConst). value_type operator*() const { - const auto& entry = m_entries[m_index]; if constexpr (IsConst) - { - return {entry.key, entry.value}; - } + return {m_entries[m_index].key, m_entries[m_index].value}; else - { - return {entry.key, const_cast(entry.value)}; - } - } - - // Provides pointer-like access to the current key-value pair. - // @return A pointer to a temporary key-value pair. - pointer operator->() const - { - return std::addressof(**this); + return {m_entries[m_index].key, const_cast(m_entries[m_index].value)}; } - // Returns a const reference to the key at the current position. const K& key() const { return m_entries[m_index].key; } private: - // Advances the iterator to the next occupied entry, skipping empty or deleted entries. - void advance_to_valid() + void skip_to_occupied() { - while (m_index < m_size && m_entries[m_index].state != EntryState::Occupied) - { + while (m_index < m_capacity && m_entries[m_index].state != EntryState::Occupied) ++m_index; - } } EntryPointer m_entries; std::size_t m_index; - std::size_t m_size; + std::size_t m_capacity; }; template @@ -122,374 +103,262 @@ namespace ZEngine::Core::Containers using iterator = HashMapIterator; using const_iterator = HashMapIterator; - // Initializes the hash map with an allocator, initial capacity, and load factor. - // @param allocator Pointer to the arena allocator for memory management. - // @param initial_capacity Initial number of slots (default: 16). - // @param load_factor Maximum load factor before resizing (default: 0.75). - void init(Memory::ArenaAllocator* allocator, size_type initial_capacity = 16) + // Pre-allocate capacity slots (rounded to next power of 2 ≥ 16). + // Uses Array::init(arena, cap, cap) so the full block is reserved up-front; + // all entries are zeroed — EntryState::Empty = 0 needs no explicit loop. + void init(Memory::ArenaAllocator* arena, size_type slot_capacity = 16) { - m_allocator = allocator; - m_load_factor = 0.75f; - m_entries.init(m_allocator, initial_capacity); - for (size_type i = 0; i < initial_capacity; ++i) - { - m_entries.push({}); - } + m_allocator = arena; + size_type cap = next_pow2(slot_capacity < 16 ? 16 : slot_capacity); + m_capacity_mask = cap - 1; + m_entries.init(arena, cap, cap); + Helpers::secure_memset(m_entries.data(), 0, cap * sizeof(Entry), cap * sizeof(Entry)); m_size = 0; } - // Inserts a key-value pair into the hash map, updating the value if the key exists. - // Resizes the map if the load factor would be exceeded. - // @param key The key to insert. - // @param value The value to associate with the key. - // @throws std::runtime_error if the table is full and cannot be resized. void insert(const K& key, const V& value) - requires std::is_copy_assignable_v { - maybe_grow(); - - size_type index = probe_for_insert(key); - auto& entry = m_entries[index]; - - if (entry.state == EntryState::Empty || entry.state == EntryState::Deleted) - { - entry.key = key; - entry.value = value; - entry.state = EntryState::Occupied; - ++m_size; - } - else - { - entry.value = value; - } + upsert(key, value); } - void insert(const K& key, V&& value) { - maybe_grow(); - - size_type index = probe_for_insert(key); - auto& entry = m_entries[index]; - - if (entry.state == EntryState::Empty || entry.state == EntryState::Deleted) - { - entry.key = key; - entry.value = std::move(value); - entry.state = EntryState::Occupied; - ++m_size; - } - else - { - entry.value = std::move(value); - } + upsert(key, std::move(value)); } V& operator[](const K& key) { - maybe_grow(); - - size_type index = probe_for_insert(key); - auto& entry = m_entries[index]; - - if (entry.state == EntryState::Empty || entry.state == EntryState::Deleted) + guard_load(); + size_type idx = probe_insert(key); + Entry& e = m_entries[idx]; + if (e.state != EntryState::Occupied) { - entry.key = key; - entry.value = V{}; - entry.state = EntryState::Occupied; + e.key = key; + e.value = V{}; + e.state = EntryState::Occupied; ++m_size; } - return entry.value; + return e.value; } - // Retrieves a const reference to the value associated with a key. - // @param key The key to look up. - // @return Const reference to the value. - // @throws std::out_of_range if the key is not found. - const V& at(const K& key) const - { - size_type index = probe_for_key(key); - if (index == size_type(-1)) - { - throw std::out_of_range("Key not found in UnorderedHashMap"); - } - return m_entries[index].value; - } - - // Finds the value associated with a key. - // @param key The key to look up. - // @return Pointer to the value if found, nullptr otherwise. V* find(const K& key) { - size_type index = probe_for_key(key); - return (index != size_type(-1)) ? &m_entries[index].value : nullptr; + size_type i = probe_find(key); + return i != npos ? &m_entries[i].value : nullptr; } - - // Finds the value associated with a key (const version). - // @param key The key to look up. - // @return Const pointer to the value if found, nullptr otherwise. const V* find(const K& key) const { - size_type index = probe_for_key(key); - return (index != size_type(-1)) ? &m_entries[index].value : nullptr; + size_type i = probe_find(key); + return i != npos ? &m_entries[i].value : nullptr; } - // Returns a pointer to the stored key if found, nullptr otherwise. const K* find_key(const K& key) const { - size_type index = probe_for_key(key); - return (index != size_type(-1)) ? &m_entries[index].key : nullptr; + size_type i = probe_find(key); + return i != npos ? &m_entries[i].key : nullptr; } - // Checks if a key exists in the hash map. - // @param key The key to check. - // @return True if the key exists, false otherwise. bool contains(const K& key) const { - return find(key) != nullptr; + return probe_find(key) != npos; + } + + const V& at(const K& key) const + { + size_type i = probe_find(key); + ZENGINE_VALIDATE_ASSERT(i != npos, "UnorderedHashMap::at: key not found") + return m_entries[i].value; } - // Removes a key-value pair from the hash map. - // @param key The key to remove. - // @note Marks the entry as Deleted; does not shrink the table. void remove(const K& key) { - size_type index = probe_for_key(key); - if (index != size_type(-1)) + size_type i = probe_find(key); + if (i != npos) { - m_entries[index].state = EntryState::Deleted; + m_entries[i].state = EntryState::Deleted; --m_size; } } - // Clears all entries in the hash map, resetting it to an empty state. - // @note Sets all entries to Empty; does not change capacity. void clear() { - for (auto& entry : m_entries) - { - entry.state = EntryState::Empty; - } + Helpers::secure_memset(m_entries.data(), 0, m_entries.size() * sizeof(Entry), m_entries.size() * sizeof(Entry)); m_size = 0; } - // Checks if the hash map is empty. - // @return True if the map contains no key-value pairs, false otherwise. - bool empty() const - { - return m_size == 0; - } - - // Returns the number of key-value pairs in the hash map. - // @return The number of occupied entries. size_type size() const { return m_size; } - - // Returns the current capacity of the hash map. - // @return The number of slots in the underlying array. size_type capacity() const { return m_entries.size(); } + bool empty() const + { + return m_size == 0; + } - // Returns an iterator to the first occupied entry. - // @return Iterator pointing to the first key-value pair or end() if empty. - // @note Iterators are invalidated by insert, remove, or reserve operations. - iterator begin() + // Grow to next_pow2(new_count) — allocates a new Array block, abandons old. + // Call sparingly; pre-size correctly via init() to avoid this entirely. + void reserve(size_type new_count) { - return iterator(m_entries.data(), 0, m_entries.size()); + size_type new_cap = next_pow2(new_count); + if (new_cap > capacity()) + rehash(new_cap); } - // Returns an iterator to the end of the hash map. - // @return Iterator representing the past-the-end position. + iterator begin() + { + return {m_entries.data(), 0, capacity()}; + } iterator end() { - return iterator(m_entries.data(), m_entries.size(), m_entries.size()); + return {m_entries.data(), capacity(), capacity()}; } - - // Returns a const iterator to the first occupied entry. - // @return Const iterator pointing to the first key-value pair or end() if empty. - // @note Iterators are invalidated by insert, remove, or reserve operations. const_iterator begin() const { - return const_iterator(m_entries.data(), 0, m_entries.size()); + return {m_entries.data(), 0, capacity()}; } - - // Returns a const iterator to the end of the hash map. - // @return Const iterator representing the past-the-end position. const_iterator end() const { - return const_iterator(m_entries.data(), m_entries.size(), m_entries.size()); + return {m_entries.data(), capacity(), capacity()}; } - - // Returns a const iterator to the first occupied entry (alias for begin() const). - // @return Const iterator pointing to the first key-value pair or end() if empty. const_iterator cbegin() const { return begin(); } - - // Returns a const iterator to the end of the hash map (alias for end() const). - // @return Const iterator representing the past-the-end position. const_iterator cend() const { return end(); } - // Ensures the hash map has at least the specified capacity. - // @param new_capacity Desired minimum number of slots. - // @note Rehashes the map if the new capacity is greater than the current capacity. - void reserve(size_type new_capacity) - { - if (new_capacity > m_entries.size()) - { - rehash(new_capacity); - } - } - private: - // Checks if the hash map needs to grow based on the load factor and resizes if necessary. - // @note Triggers rehashing if (m_size + 1) / capacity > load_factor. - void maybe_grow() + static constexpr size_type npos = size_type(-1); + + static size_type next_pow2(size_type n) { - if (static_cast(m_size + 1) / m_entries.size() > m_load_factor) - { - size_type new_capacity = std::max(16, static_cast(m_entries.size() * 1.5f)); // Growth factor 1.5 - rehash(new_capacity); - } + if (n == 0) + return 1; + --n; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 8; + n |= n >> 16; + if constexpr (sizeof(size_type) > 4) + n |= n >> 32; + return n + 1; } - // Compare keys, specialized for const char* - bool key_equals(const K& a, const K& b) const + bool key_eq(const K& a, const K& b) const { if constexpr (std::is_same_v) - { return Helpers::secure_strcmp(a, b) == 0; - } else - { return a == b; - } } - // Rehashes the hash map to a new capacity, reinserting all occupied entries. - // @param new_capacity The new number of slots. - // @note Moves the old entries to avoid copying and skips Deleted entries. - void rehash(size_type new_capacity) + size_type hash_of(const K& key) const { - Array old_entries = std::move(m_entries); - m_entries = Array{}; - m_entries.init(m_allocator, new_capacity); - for (size_type i = 0; i < new_capacity; ++i) - { - m_entries.push({}); - } - m_size = 0; + if constexpr (std::is_same_v) + return rapidhash(key, Helpers::secure_strlen(key)); + else + return rapidhash(&key, sizeof(K)); + } - for (size_type i = 0; i < old_entries.size(); ++i) + size_type probe_find(const K& key) const + { + if (m_entries.empty()) + return npos; + size_type idx = hash_of(key) & m_capacity_mask; + for (size_type i = 0; i <= m_capacity_mask; ++i) { - if (old_entries[i].state == EntryState::Occupied) - { - size_type index = probe_for_insert(old_entries[i].key); - m_entries[index] = std::move(old_entries[i]); - ++m_size; - } + const Entry& e = m_entries[idx]; + if (e.state == EntryState::Empty) + return npos; + if (e.state == EntryState::Occupied && key_eq(e.key, key)) + return idx; + idx = (idx + 1) & m_capacity_mask; } + return npos; } - // Probes for a key using quadratic probing. - // @param key The key to look up. - // @return Index of the key if found, or size_type(-1) if not found. - size_type probe_for_key(const K& key) const + size_type probe_insert(const K& key) { - size_type index = hash(key) % m_entries.size(); - size_type i = 0; - - do + ZENGINE_VALIDATE_ASSERT(!m_entries.empty(), "UnorderedHashMap: call init() before insert") + size_type idx = hash_of(key) & m_capacity_mask; + size_type tombstone = npos; + for (size_type i = 0; i <= m_capacity_mask; ++i) { - const auto& entry = m_entries[index]; - if (entry.state == EntryState::Empty) - { - return size_type(-1); - } - if (entry.state == EntryState::Occupied && key_equals(entry.key, key)) - { - return index; - } - ++i; - index = (index + i) % m_entries.size(); - } while (i < m_entries.size()); - - return size_type(-1); + Entry& e = m_entries[idx]; + if (e.state == EntryState::Occupied && key_eq(e.key, key)) + return idx; + if (e.state == EntryState::Empty) + return tombstone != npos ? tombstone : idx; + if (e.state == EntryState::Deleted && tombstone == npos) + tombstone = idx; + idx = (idx + 1) & m_capacity_mask; + } + if (tombstone != npos) + return tombstone; + ZENGINE_VALIDATE_ASSERT(false, "UnorderedHashMap: table full — pre-size with init(arena, 2*count)") + return npos; } - // Probes for a slot to insert a key, preferring deleted slots if available. - // @param key The key to insert. - // @return Index of the slot to use for insertion or the existing key. - // @throws std::runtime_error if the table is full and no slot is found. - size_type probe_for_insert(const K& key) + void guard_load() { - size_type index = hash(key) % m_entries.size(); - size_type first_deleted = size_type(-1); - size_type i = 0; - - do - { - auto& entry = m_entries[index]; - if (entry.state == EntryState::Occupied && key_equals(entry.key, key)) - { - return index; - } - - if (entry.state == EntryState::Empty) - { - return (first_deleted != size_type(-1)) ? first_deleted : index; - } - - if (entry.state == EntryState::Deleted && first_deleted == size_type(-1)) - { - first_deleted = index; - } - - ++i; - index = (index + i) % m_entries.size(); - } while (i < m_entries.size()); + if (m_entries.empty()) + return; + ZENGINE_VALIDATE_ASSERT(static_cast(m_size + 1) / static_cast(capacity()) <= 0.75f, "UnorderedHashMap: load > 75% — call reserve() or increase init() capacity") + } - if (first_deleted != size_type(-1)) + template + void upsert(const K& key, Val&& val) + { + guard_load(); + size_type idx = probe_insert(key); + if (idx == npos) + return; + Entry& e = m_entries[idx]; + if (e.state != EntryState::Occupied) { - return first_deleted; + e.key = key; + e.state = EntryState::Occupied; + ++m_size; } - - throw std::runtime_error("UnorderedHashMap probe failed: table full"); + e.value = std::forward(val); } - // Computes the hash value for a key using the provided hasher. - // @param key The key to hash. - // @return The hash value. - size_type hash(const K& key) const + void rehash(size_type new_cap) { - if constexpr (std::is_same_v) - { - return rapidhash(key, Helpers::secure_strlen(key)); - } - else + Array old = std::move(m_entries); + size_type old_cap = old.size(); + + m_capacity_mask = new_cap - 1; + m_entries.init(m_allocator, new_cap, new_cap); + Helpers::secure_memset(m_entries.data(), 0, new_cap * sizeof(Entry), new_cap * sizeof(Entry)); + m_size = 0; + + for (size_type i = 0; i < old_cap; ++i) { - return rapidhash(&key, sizeof(K)); + if (old[i].state == EntryState::Occupied) + { + size_type idx = probe_insert(old[i].key); + m_entries[idx] = std::move(old[i]); + ++m_size; + } } } - Memory::ArenaAllocator* m_allocator = nullptr; - Array m_entries; - size_type m_size = 0; - float m_load_factor = 0.75f; + Memory::ArenaAllocator* m_allocator = nullptr; + Array m_entries = {}; + size_type m_capacity_mask = 0; + size_type m_size = 0; }; - // Computes a hash value for a C-string using rapidhash. - // @param str The null-terminated string to hash. - // @return The hash value. inline uint64_t hash_compute(const char* str) { return rapidhash(str, Helpers::secure_strlen(str)); } + } // namespace ZEngine::Core::Containers diff --git a/ZEngine/ZEngine/Core/VFS/Registry/AssetRegistry.cpp b/ZEngine/ZEngine/Core/VFS/Registry/AssetRegistry.cpp index 24aa00671..57137fa7c 100644 --- a/ZEngine/ZEngine/Core/VFS/Registry/AssetRegistry.cpp +++ b/ZEngine/ZEngine/Core/VFS/Registry/AssetRegistry.cpp @@ -127,6 +127,11 @@ namespace ZEngine::Core::VFS return h.Valid() ? m_index.Access(h) : nullptr; } + AssetRecord* AssetRegistry::Access(Helpers::Handle handle) + { + return handle.Valid() ? m_index.Access(handle) : nullptr; + } + void AssetRegistry::SetHotReloadCallback(void* ctx, void (*cb)(void*, std::span)) { m_reload_cb_ctx = ctx; @@ -336,12 +341,7 @@ namespace ZEngine::Core::VFS return; Core::Containers::Array deps; - // We need an arena here — use a small stack buffer via a temporary array - // that is left uninitialized and just enumerated from the graph. - // For DOT output we accept O(n) per node; this is a debug-only path. dc->self->m_graph.CopyDependents(rec.UUID, deps); - // DOT output intentionally omitted for brevity of this debug helper. - // Full implementation would write edges between node names. }); int tail = std::snprintf(out_buf + dot.pos, out_len - dot.pos, "}\n"); @@ -358,6 +358,10 @@ namespace ZEngine::Core::VFS if (ext.Equals(".png") || ext.Equals(".jpg") || ext.Equals(".jpeg") || ext.Equals(".hdr") || ext.Equals(".ktx") || ext.Equals(".ktx2")) return Managers::AssetType::TEXTURE; + if (ext.Equals(".zematerial")) + return Managers::AssetType::MATERIAL; + if (ext.Equals(".zemesh")) + return Managers::AssetType::MESH; return Managers::AssetType::MESH; } diff --git a/ZEngine/ZEngine/Core/VFS/Registry/AssetRegistry.h b/ZEngine/ZEngine/Core/VFS/Registry/AssetRegistry.h index 72c434b83..b733456c7 100644 --- a/ZEngine/ZEngine/Core/VFS/Registry/AssetRegistry.h +++ b/ZEngine/ZEngine/Core/VFS/Registry/AssetRegistry.h @@ -39,6 +39,7 @@ namespace ZEngine::Core::VFS const AssetRecord* FindByUUID(const uuids::uuid& uuid) const; AssetRecord* FindByPath(const Core::VFS::VFSPath& path); const AssetRecord* FindByPath(const Core::VFS::VFSPath& path) const; + AssetRecord* Access(Helpers::Handle handle); // Query struct QueryFilter diff --git a/ZEngine/ZEngine/Core/VFS/VFSScanner.cpp b/ZEngine/ZEngine/Core/VFS/VFSScanner.cpp index f64824965..729f964d2 100644 --- a/ZEngine/ZEngine/Core/VFS/VFSScanner.cpp +++ b/ZEngine/ZEngine/Core/VFS/VFSScanner.cpp @@ -10,7 +10,7 @@ namespace ZEngine::Core::VFS { static bool IsAssetExtension(const VFSPath& path) { - const char* exts[] = {".glb", ".gltf", ".fbx", ".png", ".jpg", ".jpeg", ".hdr", ".ktx"}; + const char* exts[] = {".glb", ".gltf", ".fbx", ".png", ".jpg", ".jpeg", ".hdr", ".ktx", ".zemesh", ".zematerial"}; VFSPathComponent ext = path.Extension(); for (const char* candidate : exts) if (ext.Equals(candidate)) diff --git a/ZEngine/ZEngine/Hardwares/DeviceSwapchain.cpp b/ZEngine/ZEngine/Hardwares/DeviceSwapchain.cpp index 1e3f6a080..5750f39fa 100644 --- a/ZEngine/ZEngine/Hardwares/DeviceSwapchain.cpp +++ b/ZEngine/ZEngine/Hardwares/DeviceSwapchain.cpp @@ -342,9 +342,9 @@ namespace ZEngine::Hardwares auto scratch = ZGetScratch(&Arena); { Array write_descriptor_sets = {}; - write_descriptor_sets.init(scratch.Arena, Device->WriteBindlessDescriptorSetRequests.size()); + write_descriptor_sets.init(scratch.Arena, Device->BindlessTextureSlotRequests.size()); - for (auto& req : Device->WriteBindlessDescriptorSetRequests) + for (auto& req : Device->BindlessTextureSlotRequests) { write_descriptor_sets.push( VkWriteDescriptorSet{ diff --git a/ZEngine/ZEngine/Hardwares/VulkanDevice.cpp b/ZEngine/ZEngine/Hardwares/VulkanDevice.cpp index f5b24fe01..a08f5b0ee 100644 --- a/ZEngine/ZEngine/Hardwares/VulkanDevice.cpp +++ b/ZEngine/ZEngine/Hardwares/VulkanDevice.cpp @@ -353,6 +353,8 @@ namespace ZEngine::Hardwares device_features_2.features.drawIndirectFirstInstance = PhysicalDeviceFeature.features.drawIndirectFirstInstance; device_features_2.features.multiDrawIndirect = PhysicalDeviceFeature.features.multiDrawIndirect; device_features_2.features.samplerAnisotropy = PhysicalDeviceFeature.features.samplerAnisotropy; + // Required for MaterialData.AlbedoMap / NormalMap etc. (uint64_t handles in g_buffer.frag) + device_features_2.features.shaderInt64 = PhysicalDeviceFeature.features.shaderInt64; if (PhysicalDeviceSupportSampledImageBindless || PhysicalDeviceSupportStorageBufferBindless) { diff --git a/ZEngine/ZEngine/Hardwares/VulkanDevice.h b/ZEngine/ZEngine/Hardwares/VulkanDevice.h index dae88d54b..8448aa43d 100644 --- a/ZEngine/ZEngine/Hardwares/VulkanDevice.h +++ b/ZEngine/ZEngine/Hardwares/VulkanDevice.h @@ -56,7 +56,6 @@ namespace ZEngine::Hardwares using Core::Memory::GpuMemoryDomain; struct WriteDescriptorSetRequestKey; - struct WriteDescriptorSetRequest; struct CommandBufferManager; struct AsyncGPUOperation; struct AsyncGPUOperationHandle; @@ -224,18 +223,6 @@ namespace ZEngine::Hardwares } }; - struct WriteDescriptorSetRequest - { - bool Updated = false; - int Handle; - uint32_t FrameIndex; - VkDescriptorSet DstSet; - uint32_t Binding; - uint32_t DstArrayElement; - uint32_t DescriptorCount; - VkDescriptorType DescriptorType; - }; - /* * Async GPU operation handle and definition */ @@ -300,7 +287,7 @@ namespace ZEngine::Hardwares Core::Containers::UnorderedHashMap> ShaderReservedDescriptorSetMap = {}; //> Core::Containers::UnorderedHashMap ShaderReservedDescriptorSetLayoutMap = {}; // Core::Containers::UnorderedHashMap> ShaderReservedLayoutBindingSpecificationMap = {}; - std::set WriteBindlessDescriptorSetRequests = {}; + std::set BindlessTextureSlotRequests = {}; std::unordered_set ShaderReservedBindingSets = {}; Rendering::Textures::TextureHandleManager GlobalTextures = {}; Helpers::HandleManager Image2DBufferManager = {}; diff --git a/ZEngine/ZEngine/Helpers/SerializerCommonHelper.h b/ZEngine/ZEngine/Helpers/SerializerCommonHelper.h index 98ed627cc..a9798e1b1 100644 --- a/ZEngine/ZEngine/Helpers/SerializerCommonHelper.h +++ b/ZEngine/ZEngine/Helpers/SerializerCommonHelper.h @@ -138,7 +138,7 @@ namespace ZEngine::Helpers { size_t size = 0; ReadBinary(in, size); - map.init(arena, (size > 32) ? size : 32); + map.init(arena, size * 2 + 16); // 2× gives ≤50% load — init rounds to next pow2 for (uint32_t i = 0; i < size; ++i) { diff --git a/ZEngine/ZEngine/Managers/AssetManager.cpp b/ZEngine/ZEngine/Managers/AssetManager.cpp index 4a4b49f29..69a0f9a9b 100644 --- a/ZEngine/ZEngine/Managers/AssetManager.cpp +++ b/ZEngine/ZEngine/Managers/AssetManager.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +50,7 @@ namespace ZEngine::Managers s_Instance->Textures.init(&s_Instance->Arena, 5000); s_Instance->UUIDToTextureHandle.init(&s_Instance->Arena, 5000); s_Instance->MeshToHierarchySlot.init(&s_Instance->Arena, 5000); + s_Instance->UUIDToMaterialSlot.init(&s_Instance->Arena, 5000); static Core::VFS::AssetRegistry s_registry; s_registry.Initialize(&s_Instance->Arena); @@ -78,8 +80,6 @@ namespace ZEngine::Managers return handle; } - // ── Direct ingest methods — called from ImportCoordinator thread ───────────── - bool AssetManager::IsRegistered(const uuids::uuid& id) { return s_Instance && s_Instance->Registry && s_Instance->Registry->FindByUUID(id) != nullptr; @@ -90,12 +90,12 @@ namespace ZEngine::Managers if (!s_Instance) return; std::lock_guard lock(s_Instance->IngestMutex); - // Use GetAsset (not IsRegistered) — VFSScanner pre-registers UUIDs in the registry - // without populating the Meshes array, so IsRegistered gives a false positive. - if (GetAsset(mesh.MeshUUID) != nullptr) + // Use MeshToHierarchySlot — populated only after data is actually ingested. + // IsRegistered / GetAsset both give false positives because VFSScanner + // pre-registers UUIDs with SlotHandle=0 before any mesh data exists. + if (s_Instance->MeshToHierarchySlot.find(mesh.MeshUUID) != nullptr) return; - // Mesh auto mesh_slot = static_cast(s_Instance->Meshes.size()); auto& m = s_Instance->Meshes.push_use({}); m.MeshUUID = mesh.MeshUUID; @@ -108,7 +108,6 @@ namespace ZEngine::Managers m.SubMeshes.push(sub); RegisterAsset(AssetType::MESH, m.MeshUUID, mesh_slot); - // Hierarchy auto hier_slot = static_cast(s_Instance->NodeHierarchies.size()); auto& h = s_Instance->NodeHierarchies.push_use({}); h.NodeHierarchyUUID = hierarchy.NodeHierarchyUUID; @@ -123,7 +122,7 @@ namespace ZEngine::Managers h.NodeMeshes.init(&s_Instance->Arena, hierarchy.NodeMeshes.size() > 32 ? hierarchy.NodeMeshes.size() * 2 : 64); h.NodeMaterials.init(&s_Instance->Arena, hierarchy.NodeMaterials.size() > 32 ? hierarchy.NodeMaterials.size() * 2 : 64); - Helpers::secure_memcpy(h.Hierarchies.data(), h.Hierarchies.size() * sizeof(AssetNodeHierarchy), hierarchy.Hierarchies.data(), hierarchy.Hierarchies.size() * sizeof(AssetNodeHierarchy)); + Helpers::secure_memcpy(h.Hierarchies.data(), h.Hierarchies.size() * sizeof(Helpers::NodeHierarchy), hierarchy.Hierarchies.data(), hierarchy.Hierarchies.size() * sizeof(Helpers::NodeHierarchy)); Helpers::secure_memcpy(h.LocalTransforms.data(), h.LocalTransforms.size() * sizeof(Core::Maths::Mat4f), hierarchy.LocalTransforms.data(), hierarchy.LocalTransforms.size() * sizeof(Core::Maths::Mat4f)); Helpers::secure_memcpy(h.GlobalTransforms.data(), h.GlobalTransforms.size() * sizeof(Core::Maths::Mat4f), hierarchy.GlobalTransforms.data(), hierarchy.GlobalTransforms.size() * sizeof(Core::Maths::Mat4f)); @@ -210,12 +209,17 @@ namespace ZEngine::Managers if (!s_Instance) return; std::lock_guard lock(s_Instance->IngestMutex); - if (GetAsset(mat.MaterialUUID) != nullptr) + // Use UUIDToMaterialSlot — populated only after data is actually ingested. + // GetAsset gives false positives: VFSScanner pre-registers all .zematerial UUIDs + // with SlotHandle=0, so GetAsset(uuid_N) returns Materials[0] + // (the first ingested material) for every subsequent material → all skipped. + if (s_Instance->UUIDToMaterialSlot.find(mat.MaterialUUID) != nullptr) return; auto slot = static_cast(s_Instance->Materials.size()); s_Instance->Materials.push(mat); RegisterAsset(AssetType::MATERIAL, mat.MaterialUUID, slot); + s_Instance->UUIDToMaterialSlot.insert(mat.MaterialUUID, slot); Rendering::Meshes::MeshMaterial& gpu_mat = s_Instance->GPUMeshMaterials.push_use({}); gpu_mat.AlbedoColor = mat.AlbedoColor; @@ -245,8 +249,6 @@ namespace ZEngine::Managers gpu_mat.SpecularMap = tex_handle(mat.SpecularTexUUID, mat.SpecularTexPath); } - // ── CPU buffer accessors ────────────────────────────────────────────────────── - Importers::AssetMesh* AssetManager::GetMeshAsset(const uuids::uuid& id) { if (!Registry) @@ -274,6 +276,60 @@ namespace ZEngine::Managers return rec ? rec->SlotHandle : 0; } + void AssetManager::ReloadFromDisk(Core::Memory::ArenaAllocator* scratch) + { + if (!s_Instance || !s_Instance->Registry || !scratch) + return; + // Materials — deserialize each .zematerial that has not been ingested yet. + { + auto result = s_Instance->Registry->Query({.Type = AssetType::MATERIAL}, 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->UUIDToMaterialSlot.find(rec->UUID) != nullptr) + continue; + + char native[MAX_FILE_PATH_COUNT] = {}; + rec->Path.ResolveNative(s_Instance->CurrentWorkingSpacePath, native, sizeof(native)); + + AssetMaterial mat = {}; + Importers::AssetCodec::DeserializeMaterialAssetFile(scratch, native, mat); + if (!mat.MaterialUUID.is_nil()) + { + ZENGINE_LOG_ASSET_INFO("Reloading material from disk: {}", native) + IngestMaterial(std::move(mat)); + } + } + } + + // Meshes — deserialize each .zemesh that has not been ingested yet. + { + auto result = s_Instance->Registry->Query({.Type = AssetType::MESH}, 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->MeshToHierarchySlot.find(rec->UUID) != nullptr) + continue; + + char native[MAX_FILE_PATH_COUNT] = {}; + rec->Path.ResolveNative(s_Instance->CurrentWorkingSpacePath, native, sizeof(native)); + + AssetMesh mesh = {}; + AssetNodeHierarchy hier = {}; + Importers::AssetCodec::DeserializeMeshAssetFile(scratch, native, mesh, hier); + if (!mesh.MeshUUID.is_nil()) + { + ZENGINE_LOG_ASSET_INFO("Reloading mesh from disk: {}", native) + IngestMesh(std::move(mesh), std::move(hier)); + } + } + } + } + uuids::uuid AssetManager::GetOrCreateUUID(Core::VFS::IVFSContext& ctx, const Core::VFS::VFSPath& asset_path, const char* importer_name) { auto hash = Core::VFS::MetaFileIO::ComputeHash(ctx, asset_path); diff --git a/ZEngine/ZEngine/Managers/AssetManager.h b/ZEngine/ZEngine/Managers/AssetManager.h index 8fa1bf1c6..a634eefe9 100644 --- a/ZEngine/ZEngine/Managers/AssetManager.h +++ b/ZEngine/ZEngine/Managers/AssetManager.h @@ -34,6 +34,10 @@ namespace ZEngine::Managers // Mesh UUID → NodeHierarchy slot — O(1) lookup replacing the old linear scan. Core::Containers::UnorderedHashMap MeshToHierarchySlot = {}; + // Populated only when asset data is actually ingested — not by VFSScanner + // pre-registration (which sets SlotHandle=0, causing GetAsset to return a + // false match for any material at slot 0). + Core::Containers::UnorderedHashMap UUIDToMaterialSlot = {}; // (255, 20, 147) fallback handle used when a texture file cannot be resolved. Rendering::Textures::TextureHandle FallbackTextureHandle = {}; @@ -44,7 +48,6 @@ namespace ZEngine::Managers Hardwares::VulkanDevice* Device = nullptr; ::ZEngine::Core::VFS::AssetRegistry* Registry = nullptr; - // CPU buffer accessors Importers::AssetMesh* GetMeshAsset(const uuids::uuid& id); Importers::AssetNodeHierarchy* GetMeshNodeHierarchy(const uuids::uuid& mesh_id); AssetHandle GetMeshNodeHierarchyHandle(const uuids::uuid& id); @@ -73,6 +76,10 @@ namespace ZEngine::Managers 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 + // but not yet ingested (second launch / project reopen). Safe to call every scan. + static void ReloadFromDisk(Core::Memory::ArenaAllocator* scratch); + template static T* GetAsset(K key) { diff --git a/ZEngine/ZEngine/Rendering/Renderers/GraphicRenderer.cpp b/ZEngine/ZEngine/Rendering/Renderers/GraphicRenderer.cpp index ab0b35d92..9744ed01f 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/GraphicRenderer.cpp +++ b/ZEngine/ZEngine/Rendering/Renderers/GraphicRenderer.cpp @@ -35,8 +35,9 @@ namespace ZEngine::Rendering::Renderers /* * Renderer Passes */ - auto base_pass = ZPushStructCtor(Device->Arena, BasePass); auto scene_depth_prepass = ZPushStructCtor(Device->Arena, DepthPrePass); + auto gbuffer_pass = ZPushStructCtor(Device->Arena, GbufferPass); + auto composite_pass = ZPushStructCtor(Device->Arena, CompositePass); auto skybox_pass = ZPushStructCtor(Device->Arena, SkyboxPass); auto grid_pass = ZPushStructCtor(Device->Arena, GridPass); @@ -54,9 +55,12 @@ namespace ZEngine::Rendering::Renderers RenderGraph->ResourceBuilder->AttachRenderTarget(RendererResourceName::FrameDepthRenderTargetName, FrameDepthRenderTarget); RenderGraph->ResourceBuilder->AttachRenderTarget(RendererResourceName::FrameColorRenderTargetName, FrameColorRenderTarget); - // Skybox starts disabled; ApplySkyConfig enables it when a scene with an HDRI sky loads. - RenderGraph->AddCallbackPass("Base Pass", base_pass); RenderGraph->AddCallbackPass("Depth Pre-Pass", scene_depth_prepass); + RenderGraph->AddCallbackPass("G-Buffer Pass", gbuffer_pass); + // TODO(deferred-lighting): re-add CompositePass when GbufferPass uses separate + // G-buffer targets and LightingPass is ready to composite them. + // RenderGraph->AddCallbackPass("Composite Pass", composite_pass); + // Skybox starts disabled; ApplySkyConfig enables it when a scene with an HDRI sky loads. RenderGraph->AddCallbackPass("Skybox Pass", skybox_pass, false); RenderGraph->AddCallbackPass("Grid Pass", grid_pass); @@ -84,26 +88,25 @@ namespace ZEngine::Rendering::Renderers if (!scene) return; - // Bind static per-scene buffers once (TransformSB, DrawDataSB, MatSB). - // These are HOST_VISIBLE BufferViews — descriptor binding is set once, - // data is written directly via vmaCopyMemoryToAllocation each frame. + // Bind static per-scene buffers once. HOST_VISIBLE — descriptor set once, + // data written each frame via vmaCopyMemoryToAllocation. if (!m_static_buffers_bound && scene->TransformBuffer.Handle) { - auto& depth_node = RenderGraph->NodeMap["Depth Pre-Pass"]; - auto& base_node = RenderGraph->NodeMap["Base Pass"]; + auto& depth_node = RenderGraph->NodeMap["Depth Pre-Pass"]; + auto& gbuffer_node = RenderGraph->NodeMap["G-Buffer Pass"]; if (depth_node.Handle) { - depth_node.Handle->SetInput("TransformSB", &scene->TransformBuffer); - depth_node.Handle->SetInput("DrawDataSB", &scene->RenderDataBuffer); + depth_node.Handle->SetStorageBuffer("TransformSB", &scene->TransformBuffer); + depth_node.Handle->SetStorageBuffer("DrawDataSB", &scene->RenderDataBuffer); } - if (base_node.Handle) + if (gbuffer_node.Handle) { - base_node.Handle->SetInput("TransformSB", &scene->TransformBuffer); - base_node.Handle->SetInput("DrawDataSB", &scene->RenderDataBuffer); - base_node.Handle->SetInput("MatSB", &scene->MaterialBuffer); + gbuffer_node.Handle->SetStorageBuffer("TransformSB", &scene->TransformBuffer); + gbuffer_node.Handle->SetStorageBuffer("DrawDataSB", &scene->RenderDataBuffer); + gbuffer_node.Handle->SetStorageBuffer("MatSB", &scene->MaterialBuffer); } m_static_buffers_bound = true; - ZENGINE_CORE_INFO("[GraphicRenderer] Bound TransformSB/DrawDataSB/MatSB to RMM buffers") + ZENGINE_CORE_INFO("[GraphicRenderer] Bound TransformSB/DrawDataSB/MatSB to geometry passes") } if (!Device->RRM) @@ -111,25 +114,26 @@ namespace ZEngine::Rendering::Renderers auto* rrm = reinterpret_cast(Device->RRM); - // Global vertex + index buffers — bind once when they become ready. - // All meshes share these two VkBuffers; per-mesh offsets are in DrawDataSB. + // Global vertex + index buffers — bind to both geometry passes once ready. if (!m_global_buffers_bound && rrm->GlobalBuffersReady()) { - const auto* vtx_buf = rrm->GetGlobalVertexBuffer(); - const auto* idx_buf = rrm->GetGlobalIndexBuffer(); - auto& depth_node = RenderGraph->NodeMap["Depth Pre-Pass"]; - auto& base_node = RenderGraph->NodeMap["Base Pass"]; - // VertexSB = set 0, binding 1 — IndexSB = set 0, binding 2 (vertex_common.glsl). - // Use SetInputByBinding to bypass ValidateInput (name lookup inconsistency). - // Only bind to passes that include vertex_common.glsl (Depth Pre-Pass). - // Base Pass uses a stub shader with no bindings — skip it. + const auto* vtx_buf = rrm->GetGlobalVertexBuffer(); + const auto* idx_buf = rrm->GetGlobalIndexBuffer(); + auto& depth_node = RenderGraph->NodeMap["Depth Pre-Pass"]; + auto& gbuffer_node = RenderGraph->NodeMap["G-Buffer Pass"]; if (depth_node.Handle) { - depth_node.Handle->SetInputByBinding(0, 1, vtx_buf); - depth_node.Handle->SetInputByBinding(0, 2, idx_buf); + depth_node.Handle->SetStorageBuffer("VertexSB", vtx_buf); + depth_node.Handle->SetStorageBuffer("IndexSB", idx_buf); + } + if (gbuffer_node.Handle) + { + gbuffer_node.Handle->SetStorageBuffer("VertexSB", vtx_buf); + gbuffer_node.Handle->SetStorageBuffer("IndexSB", idx_buf); + gbuffer_node.Handle->UseTextureArray("TextureArray"); } m_global_buffers_bound = true; - ZENGINE_CORE_INFO("[GraphicRenderer] Bound global VertexSB/IndexSB to packed geometry buffers") + ZENGINE_CORE_INFO("[GraphicRenderer] Bound global VertexSB/IndexSB to geometry passes") } } diff --git a/ZEngine/ZEngine/Rendering/Renderers/ImGUIRenderer.cpp b/ZEngine/ZEngine/Rendering/Renderers/ImGUIRenderer.cpp index 20649b9aa..deee80fc0 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/ImGUIRenderer.cpp +++ b/ZEngine/ZEngine/Rendering/Renderers/ImGUIRenderer.cpp @@ -141,9 +141,8 @@ namespace ZEngine::Rendering::Renderers .UseSwapchainAsRenderTarget(); UIPass = Device->CreateRenderPass(pass_builder->Detach()); - UIPass->SetBindlessInput("TextureArray"); - UIPass->SetInput("LinearWrapSampler", Device->GlobalLinearWrapSamplerImageInfo); - UIPass->SetInput("LinearClampSampler", Device->GlobalLinearClampToEdgeSamplerImageInfo); + UIPass->UseTextureArray("TextureArray"); + UIPass->SetSampler("LinearClampSampler", Device->GlobalLinearClampToEdgeSamplerImageInfo); UIPass->Verify(); UIPass->Bake(); } diff --git a/ZEngine/ZEngine/Rendering/Renderers/RenderPasses/RenderPass.cpp b/ZEngine/ZEngine/Rendering/Renderers/RenderPasses/RenderPass.cpp index 18006dc6c..cefc53720 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/RenderPasses/RenderPass.cpp +++ b/ZEngine/ZEngine/Rendering/Renderers/RenderPasses/RenderPass.cpp @@ -149,7 +149,7 @@ namespace ZEngine::Rendering::Renderers::RenderPasses return verify; } - void RenderPass::SetInputFromHeap(std::string_view key_name, VkDeviceSize range) + void RenderPass::SetDynamicUniform(std::string_view key_name, VkDeviceSize range) { auto validity_output = ValidateInput(key_name); if (!validity_output.first) @@ -162,7 +162,7 @@ namespace ZEngine::Rendering::Renderers::RenderPasses set_array = m_device->ShaderReservedDescriptorSetMap.find(spec.Set); if (!set_array) { - ZENGINE_CORE_ERROR("SetInputFromHeap: descriptor set {} not found for key '{}'", spec.Set, key_name.data()) + ZENGINE_CORE_ERROR("SetDynamicUniform: descriptor set {} not found for key '{}'", spec.Set, key_name.data()) return; } @@ -192,11 +192,11 @@ namespace ZEngine::Rendering::Renderers::RenderPasses Inputs.insert(key_name.data()); } - void RenderPass::SetInput(std::string_view key_name, const Core::Memory::BufferView* buffer) + void RenderPass::SetStorageBuffer(std::string_view key_name, const Core::Memory::BufferView* buffer) { if (!buffer || !buffer->Handle) { - ZENGINE_CORE_WARN("SetInput(BufferView): null buffer for key '{}'", key_name.data()) + ZENGINE_CORE_WARN("SetStorageBuffer: null buffer for key '{}'", key_name.data()) return; } @@ -211,7 +211,7 @@ namespace ZEngine::Rendering::Renderers::RenderPasses set_array = m_device->ShaderReservedDescriptorSetMap.find(spec.Set); if (!set_array) { - ZENGINE_CORE_ERROR("SetInput(BufferView): descriptor set {} not found for key '{}'", spec.Set, key_name.data()) + ZENGINE_CORE_ERROR("SetStorageBuffer: descriptor set {} not found for key '{}'", spec.Set, key_name.data()) return; } @@ -234,46 +234,11 @@ namespace ZEngine::Rendering::Renderers::RenderPasses Inputs.insert(key_name.data()); } - void RenderPass::SetInputByBinding(uint32_t set, uint32_t binding, const Core::Memory::BufferView* buffer) - { - if (!buffer || !buffer->Handle) - return; - - auto shader = Pipeline->Shader; - const auto* set_array = shader->DescriptorSetMap.find(set); - if (!set_array) - set_array = m_device->ShaderReservedDescriptorSetMap.find(set); - if (!set_array) - { - ZENGINE_CORE_ERROR("SetInputByBinding: descriptor set {} not found", set) - return; - } - - auto frame_count = m_device->SwapchainPtr->BufferredFrameCount; - VkDescriptorBufferInfo buf_info = {.buffer = buffer->Handle, .offset = 0, .range = VK_WHOLE_SIZE}; - std::vector write_reqs(frame_count); - for (unsigned i = 0; i < frame_count; ++i) - { - write_reqs[i] = VkWriteDescriptorSet{ - .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, - .dstSet = (*set_array)[i], - .dstBinding = binding, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, - .pBufferInfo = &buf_info, - }; - } - vkUpdateDescriptorSets(m_device->LogicalDevice, (uint32_t) write_reqs.size(), write_reqs.data(), 0, nullptr); - } - - void RenderPass::SetInput(std::string_view key_name, const Textures::TextureHandle& handle) + void RenderPass::SetTexture(std::string_view key_name, const Textures::TextureHandle& handle) { auto validity_output = ValidateInput(key_name); if (!validity_output.first) - { return; - } const auto& spec = validity_output.second; auto shader = Pipeline->Shader; @@ -282,33 +247,42 @@ namespace ZEngine::Rendering::Renderers::RenderPasses set_array = m_device->ShaderReservedDescriptorSetMap.find(spec.Set); if (!set_array) { - ZENGINE_CORE_ERROR("SetInput(Texture): descriptor set {} not found for key '{}'", spec.Set, key_name.data()) + ZENGINE_CORE_ERROR("SetTexture: descriptor set {} not found for key '{}'", spec.Set, key_name.data()) return; } - 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 write_reqs = std::vector(frame_count); + + // Use the descriptor type declared in the shader (SAMPLED_IMAGE or COMBINED_IMAGE_SAMPLER) + // rather than hardcoding — avoids type mismatches with the pipeline layout. + const VkDescriptorType vk_type = Specifications::DescriptorTypeMap[VALUE_FROM_SPEC_MAP(spec.DescriptorTypeValue)]; + + 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 write_reqs = std::vector(frame_count); for (unsigned i = 0; i < frame_count; ++i) { - auto set = (*set_array)[i]; auto& image_info = img_buf->GetDescriptorImageInfo(); - - write_reqs[i] = VkWriteDescriptorSet{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .pNext = nullptr, .dstSet = set, .dstBinding = spec.Binding, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, .pImageInfo = &(image_info), .pBufferInfo = nullptr, .pTexelBufferView = nullptr}; + write_reqs[i] = VkWriteDescriptorSet{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .pNext = nullptr, + .dstSet = (*set_array)[i], + .dstBinding = spec.Binding, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk_type, + .pImageInfo = &(image_info), + }; } - vkUpdateDescriptorSets(m_device->LogicalDevice, write_reqs.size(), write_reqs.data(), 0, nullptr); - + vkUpdateDescriptorSets(m_device->LogicalDevice, (uint32_t) write_reqs.size(), write_reqs.data(), 0, nullptr); Inputs.insert(key_name.data()); } - void RenderPass::SetInput(cstring key_name, const VkDescriptorImageInfo& sampler_info) + void RenderPass::SetSampler(cstring key_name, const VkDescriptorImageInfo& sampler_info) { auto validity_output = ValidateInput(key_name); if (!validity_output.first) - { return; - } const auto& spec = validity_output.second; auto shader = Pipeline->Shader; @@ -317,7 +291,7 @@ namespace ZEngine::Rendering::Renderers::RenderPasses set_array = m_device->ShaderReservedDescriptorSetMap.find(spec.Set); if (!set_array) { - ZENGINE_CORE_ERROR("SetInput(Sampler): descriptor set {} not found for key '{}'", spec.Set, key_name) + ZENGINE_CORE_ERROR("SetSampler: descriptor set {} not found for key '{}'", spec.Set, key_name) return; } auto frame_count = m_device->SwapchainPtr->BufferredFrameCount; @@ -325,40 +299,48 @@ namespace ZEngine::Rendering::Renderers::RenderPasses for (unsigned i = 0; i < frame_count; ++i) { - auto set = (*set_array)[i]; - - write_reqs[i] = VkWriteDescriptorSet{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .pNext = nullptr, .dstSet = set, .dstBinding = spec.Binding, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER, .pImageInfo = &(sampler_info), .pBufferInfo = nullptr, .pTexelBufferView = nullptr}; + write_reqs[i] = VkWriteDescriptorSet{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .pNext = nullptr, + .dstSet = (*set_array)[i], + .dstBinding = spec.Binding, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER, + .pImageInfo = &(sampler_info), + }; } - vkUpdateDescriptorSets(m_device->LogicalDevice, write_reqs.size(), write_reqs.data(), 0, nullptr); - + vkUpdateDescriptorSets(m_device->LogicalDevice, (uint32_t) write_reqs.size(), write_reqs.data(), 0, nullptr); Inputs.insert(key_name); } - void RenderPass::SetBindlessInput(std::string_view key_name) + void RenderPass::UseTextureArray(std::string_view key_name) { auto validity_output = ValidateInput(key_name); if (!validity_output.first) - { return; - } + const auto& binding_spec = validity_output.second; - auto shader = Pipeline->Shader; - const auto* set_array = shader->DescriptorSetMap.find(binding_spec.Set); + + // Only SAMPLED_IMAGE arrays belong in BindlessTextureSlotRequests. + // Samplers are compile-time resources — use SetSampler() for them. + ZENGINE_VALIDATE_ASSERT(binding_spec.DescriptorTypeValue == Specifications::DescriptorType::SAMPLED_IMAGE, "UseTextureArray: binding is not a SAMPLED_IMAGE array — use SetSampler() for samplers") + + auto shader = Pipeline->Shader; + const auto* set_array = shader->DescriptorSetMap.find(binding_spec.Set); if (!set_array) set_array = m_device->ShaderReservedDescriptorSetMap.find(binding_spec.Set); if (!set_array) { - ZENGINE_CORE_ERROR("SetBindlessInput: descriptor set {} not found for key '{}'", binding_spec.Set, key_name.data()) + ZENGINE_CORE_ERROR("UseTextureArray: descriptor set {} not found for key '{}'", binding_spec.Set, key_name.data()) return; } - auto frame_count = m_device->SwapchainPtr->BufferredFrameCount; + auto frame_count = m_device->SwapchainPtr->BufferredFrameCount; for (unsigned i = 0; i < frame_count; ++i) { - auto set = (*set_array)[i]; - Hardwares::WriteDescriptorSetRequestKey key = {.Binding = binding_spec.Binding, .DstSet = set}; - auto& reqs = m_device->WriteBindlessDescriptorSetRequests; - reqs.insert(key); + Hardwares::WriteDescriptorSetRequestKey key = {.Binding = binding_spec.Binding, .DstSet = (*set_array)[i]}; + m_device->BindlessTextureSlotRequests.insert(key); } Inputs.insert(key_name.data()); @@ -368,7 +350,7 @@ namespace ZEngine::Rendering::Renderers::RenderPasses { for (const auto& [binding_name, texture] : Specification.InputTextures) { - SetInput(binding_name, texture); + SetTexture(binding_name, texture); } } @@ -454,7 +436,9 @@ namespace ZEngine::Rendering::Renderers::RenderPasses auto binding_spec = shader->GetLayoutBindingSpecification(key.data()); if ((binding_spec.Set == 0xFFFFFFFF) && (binding_spec.Binding == 0xFFFFFFFF)) { - ZENGINE_CORE_ERROR("Shader input not found : {}", key.data()) + const auto* pipeline_name = Specification.PipelineSpecification.DebugName; + const auto* shader_name = shader->m_specification.Name; + ZENGINE_CORE_ERROR("[{}] Shader input not found: '{}' (shader: {})", pipeline_name ? pipeline_name : "?", key.data(), shader_name ? shader_name : "?") valid = false; } return {valid, binding_spec}; @@ -470,13 +454,17 @@ namespace ZEngine::Rendering::Renderers::RenderPasses RenderPassBuilder& RenderPassBuilder::SetName(std::string_view name) { - m_spec.DebugName = name.data(); + auto buf = ZPushString(Arena, name.size() + 1); + Helpers::secure_strcpy(buf, name.size() + 1, name.data()); + m_spec.DebugName = buf; return *this; } RenderPassBuilder& RenderPassBuilder::SetPipelineName(std::string_view name) { - m_spec.PipelineSpecification.DebugName = name.data(); + auto buf = ZPushString(Arena, name.size() + 1); + Helpers::secure_strcpy(buf, name.size() + 1, name.data()); + m_spec.PipelineSpecification.DebugName = buf; return *this; } diff --git a/ZEngine/ZEngine/Rendering/Renderers/RenderPasses/RenderPass.h b/ZEngine/ZEngine/Rendering/Renderers/RenderPasses/RenderPass.h index 95dce9cb2..929fc513d 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/RenderPasses/RenderPass.h +++ b/ZEngine/ZEngine/Rendering/Renderers/RenderPasses/RenderPass.h @@ -12,16 +12,6 @@ namespace ZEngine::Rendering::Renderers::RenderPasses { - enum PassInputType - { - UNIFORM_BUFFER_SET, - STORAGE_BUFFER_SET, - BINDLESS_TEXTURE, - UNIFORM_BUFFER, - STORAGE_BUFFER, - TEXTURE - }; - struct RenderPass { RenderPass() {} @@ -40,17 +30,25 @@ namespace ZEngine::Rendering::Renderers::RenderPasses void Dispose(); void Bake(); bool Verify(); - // Binds a single device-local BufferView to ALL frame descriptor sets. - void SetInput(std::string_view key_name, const Core::Memory::BufferView* buffer); - // Direct binding by (set, binding) index — bypasses name lookup for well-known bindings. - void SetInputByBinding(uint32_t set, uint32_t binding, const Core::Memory::BufferView* buffer); - // Bind a heap-allocated resource as DYNAMIC_UNIFORM_BUFFER. - // The heap VkBuffer covers all frames; dynamic offsets are supplied at draw time. - void SetInputFromHeap(std::string_view key_name, VkDeviceSize range); - void SetInput(std::string_view key_name, const Textures::TextureHandle& texture); - void SetBindlessInput(std::string_view key_name); - // Todo : This is a temporary solution, we should have a more abstract sampler resource in the future - void SetInput(cstring key_name, const VkDescriptorImageInfo& sampler_info); + + // Bind a storage buffer (STORAGE_BUFFER) to all frame descriptor sets by name. + void SetStorageBuffer(std::string_view name, const Core::Memory::BufferView* buffer); + + // Bind a per-frame dynamic uniform (UNIFORM_BUFFER_DYNAMIC) from the FrameHeap. + void SetDynamicUniform(std::string_view name, VkDeviceSize range); + + // Bind a single texture as SAMPLED_IMAGE (or the type declared in the shader). + void SetTexture(std::string_view name, const Textures::TextureHandle& texture); + + // Bind a sampler at compile time (SAMPLER). Use for LinearWrapSampler etc. + void SetSampler(cstring name, const VkDescriptorImageInfo& sampler_info); + + // Connects this pass to the engine's global bindless TextureArray + // (set=1, binding=0, 600 slots). Registers descriptor sets for per-frame + // texture slot updates via DeviceSwapchain::Present(). + // Asserts that the named binding is VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE. + void UseTextureArray(std::string_view name); + void UpdateInputBinding(); ZRawPtr(Renderers::RenderPasses::Attachment) GetAttachment() const; void UpdateRenderTargets(); @@ -61,7 +59,6 @@ namespace ZEngine::Rendering::Renderers::RenderPasses std::pair ValidateInput(std::string_view key); private: - bool m_perform_update{false}; Hardwares::VulkanDevice* m_device; }; ZDEFINE_PTR(RenderPass); diff --git a/ZEngine/ZEngine/Rendering/Renderers/RendererPasses.cpp b/ZEngine/ZEngine/Rendering/Renderers/RendererPasses.cpp index 09abd749a..a89b9e928 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/RendererPasses.cpp +++ b/ZEngine/ZEngine/Rendering/Renderers/RendererPasses.cpp @@ -10,40 +10,35 @@ using namespace ZEngine::Core::Containers; namespace ZEngine::Rendering::Renderers { - void BasePass::Setup(Hardwares::VulkanDevicePtr const device, cstring name, RenderGraphResourceBuilderPtr const res_builder, RenderGraphResourceInspectorPtr res_inspector) + void CompositePass::Setup(Hardwares::VulkanDevicePtr const device, cstring name, RenderGraphResourceBuilderPtr const res_builder, RenderGraphResourceInspectorPtr res_inspector) { RenderGraphRenderPassCreation pass_node = {.Name = name}; pass_node.Inputs.init(device->Arena, 1); pass_node.Outputs.init(device->Arena, 1); - pass_node.Inputs.push(RenderGraphRenderPassInputOutputInfo{.Name = RendererResourceName::FrameDepthRenderTargetName}); + // Read the GbufferPass albedo output; write to the final swapchain color target. + pass_node.Inputs.push(RenderGraphRenderPassInputOutputInfo{.Name = "gbuffer_albedo_render_target", .BindingInputKeyName = "sharedRTAsTex", .Type = RenderGraphResourceType::TEXTURE}); pass_node.Outputs.push(RenderGraphRenderPassInputOutputInfo{.Name = RendererResourceName::FrameColorRenderTargetName}); res_builder->CreateRenderPassNode(std::move(pass_node)); } - void BasePass::Compile(Hardwares::VulkanDevicePtr const device, Rendering::Scenes::SceneDataPtr const scene, RenderPasses::RenderPassBuilder* pass_builder, RenderGraphResourceInspectorPtr res_inspector, RenderPasses::RenderPass** const output_pass) + void CompositePass::Compile(Hardwares::VulkanDevicePtr const device, Rendering::Scenes::SceneDataPtr const scene, RenderPasses::RenderPassBuilder* pass_builder, RenderGraphResourceInspectorPtr res_inspector, RenderPasses::RenderPass** const output_pass) { CHECK_AND_ESCAPE_NULL(output_pass) if (output_pass && !(*output_pass)) { - auto pass_spec = pass_builder->SetPipelineName("Base-Pipeline") - .SetInputBindingCount(0) - .EnablePipelineDepthTest(true) - - .UseShader("base") - .Detach(); - // clang-format off - *output_pass = device->CreateRenderPass(std::move(pass_spec)); - // clang-format on + auto pass_spec = pass_builder->SetPipelineName("Composite-Pipeline").SetInputBindingCount(0).EnablePipelineDepthTest(false).UseShader("composite").Detach(); + *output_pass = device->CreateRenderPass(std::move(pass_spec)); (*output_pass)->Bake(); } + (*output_pass)->SetSampler("LinearWrapSampler", device->GlobalLinearWrapSamplerImageInfo); (*output_pass)->Verify(); } - void BasePass::Execute(Hardwares::VulkanDevicePtr const device, RenderGraphResourceInspectorPtr res_inspector, Rendering::Scenes::SceneDataPtr const scene, RenderPasses::RenderPass* const pass, Buffers::FramebufferVNext* const framebuffer, Hardwares::CommandBufferPtr const command_buffer) + void CompositePass::Execute(Hardwares::VulkanDevicePtr const device, RenderGraphResourceInspectorPtr res_inspector, Rendering::Scenes::SceneDataPtr const scene, RenderPasses::RenderPass* const pass, Buffers::FramebufferVNext* const framebuffer, Hardwares::CommandBufferPtr const command_buffer) { command_buffer->BeginRenderPass(pass, framebuffer->Handle, false); { @@ -53,7 +48,7 @@ namespace ZEngine::Rendering::Renderers command_buffer->SetScissor(w, h); } command_buffer->BindPipeline(Specifications::PipelineBindPoint::GRAPHIC, pass->Pipeline); - command_buffer->BindDescriptorSets(device->SwapchainPtr->CurrentFrame->Index, scene ? &scene->CameraHeapOffset : nullptr, scene ? 1u : 0u); + command_buffer->BindDescriptorSets(device->SwapchainPtr->CurrentFrame->Index, nullptr, 0u); command_buffer->Draw(3, 1, 0, 0); command_buffer->EndRenderPass(); } @@ -88,7 +83,7 @@ namespace ZEngine::Rendering::Renderers if (scene) { - (*output_pass)->SetInputFromHeap("UBCamera", sizeof(Contracts::UBOCameraLayout)); + (*output_pass)->SetDynamicUniform("UBCamera", sizeof(Contracts::UBOCameraLayout)); // VertexSB/IndexSB/DrawDataSB/TransformSB bound by UpdateRMMBindings via BufferView*. (*output_pass)->Verify(); } @@ -193,9 +188,9 @@ namespace ZEngine::Rendering::Renderers if (scene && m_env_map.Valid()) { - (*output_pass)->SetInputFromHeap("UBCamera", sizeof(Contracts::UBOCameraLayout)); - (*output_pass)->SetInput("EnvMap", m_env_map); - (*output_pass)->SetInput("LinearClampToEdgeSampler", device->GlobalLinearClampToEdgeSamplerImageInfo); + (*output_pass)->SetDynamicUniform("UBCamera", sizeof(Contracts::UBOCameraLayout)); + (*output_pass)->SetTexture("EnvMap", m_env_map); + (*output_pass)->SetSampler("LinearClampToEdgeSampler", device->GlobalLinearClampToEdgeSamplerImageInfo); (*output_pass)->Verify(); } } @@ -277,7 +272,7 @@ namespace ZEngine::Rendering::Renderers if (scene) { - (*output_pass)->SetInputFromHeap("UBCamera", sizeof(Contracts::UBOCameraLayout)); + (*output_pass)->SetDynamicUniform("UBCamera", sizeof(Contracts::UBOCameraLayout)); } (*output_pass)->Verify(); } @@ -303,29 +298,15 @@ namespace ZEngine::Rendering::Renderers void GbufferPass::Setup(Hardwares::VulkanDevicePtr const device, cstring name, RenderGraphResourceBuilderPtr const res_builder, RenderGraphResourceInspectorPtr res_inspector) { - uint32_t rt_w = device->SwapchainPtr->SwapchainImageWidth; - uint32_t rt_h = device->SwapchainPtr->SwapchainImageHeight; - Specifications::TextureSpecification normal_output_spec = {.IsUsageStorage = true, .Width = rt_w, .Height = rt_h, .Format = ImageFormat::R16G16B16A16_SFLOAT}; - Specifications::TextureSpecification position_output_spec = {.IsUsageStorage = true, .Width = rt_w, .Height = rt_h, .Format = ImageFormat::R16G16B16A16_SFLOAT}; - Specifications::TextureSpecification specular_output_spec = {.IsUsageStorage = true, .Width = rt_w, .Height = rt_h, .Format = ImageFormat::R8G8B8A8_UNORM}; - Specifications::TextureSpecification colour_output_spec = {.IsUsageStorage = true, .Width = rt_w, .Height = rt_h, .Format = ImageFormat::R8G8B8A8_UNORM}; - - auto& gbuffer_albedo = res_builder->CreateRenderTarget("gbuffer_albedo_render_target", colour_output_spec); - auto& gbuffer_specular = res_builder->CreateRenderTarget("gbuffer_specular_render_target", specular_output_spec); - auto& gbuffer_normals = res_builder->CreateRenderTarget("gbuffer_normals_render_target", normal_output_spec); - auto& gbuffer_position = res_builder->CreateRenderTarget("gbuffer_position_render_target", position_output_spec); - - RenderGraphRenderPassCreation pass_node = {.Name = name}; + uint32_t rt_w = device->SwapchainPtr->SwapchainImageWidth; + uint32_t rt_h = device->SwapchainPtr->SwapchainImageHeight; + RenderGraphRenderPassCreation pass_node = {.Name = name}; - pass_node.Inputs.init(device->Arena, 2); - pass_node.Outputs.init(device->Arena, 4); + pass_node.Inputs.init(device->Arena, 1); + pass_node.Outputs.init(device->Arena, 1); pass_node.Inputs.push(RenderGraphRenderPassInputOutputInfo{.Name = RendererResourceName::FrameDepthRenderTargetName}); - - pass_node.Outputs.push({.Name = gbuffer_albedo.Name}); - pass_node.Outputs.push({.Name = gbuffer_specular.Name}); - pass_node.Outputs.push({.Name = gbuffer_normals.Name}); - pass_node.Outputs.push({.Name = gbuffer_position.Name}); + pass_node.Outputs.push(RenderGraphRenderPassInputOutputInfo{.Name = RendererResourceName::FrameColorRenderTargetName}); res_builder->CreateRenderPassNode(std::move(pass_node)); } @@ -342,10 +323,11 @@ namespace ZEngine::Rendering::Renderers if (scene) { - (*output_pass)->SetInputFromHeap("UBCamera", sizeof(Contracts::UBOCameraLayout)); + (*output_pass)->SetDynamicUniform("UBCamera", sizeof(Contracts::UBOCameraLayout)); // VertexSB / IndexSB bound by GraphicRenderer::UpdateRMMBindings (RMM path). // DrawDataSB/TransformSB/MatSB bound by UpdateRMMBindings via BufferView*. - (*output_pass)->SetBindlessInput("TextureArray"); + (*output_pass)->UseTextureArray("TextureArray"); + (*output_pass)->SetSampler("LinearWrapSampler", device->GlobalLinearWrapSamplerImageInfo); (*output_pass)->Verify(); } } @@ -353,111 +335,24 @@ namespace ZEngine::Rendering::Renderers void GbufferPass::Execute(Hardwares::VulkanDevicePtr const device, RenderGraphResourceInspectorPtr res_inspector, Rendering::Scenes::SceneDataPtr const scene, RenderPasses::RenderPass* const pass, Buffers::FramebufferVNext* const framebuffer, Hardwares::CommandBufferPtr const command_buffer) { CHECK_AND_ESCAPE_NULL(scene) - if (scene->IndirectCommandCount == 0 || !scene->RMMVertexHandle.IsValid()) - return; command_buffer->BeginRenderPass(pass, framebuffer->Handle, false); + if (scene->IndirectCommandCount > 0 && scene->RMMVertexHandle.IsValid()) { uint32_t w = pass->GetRenderAreaWidth(); uint32_t h = pass->GetRenderAreaHeight(); command_buffer->SetViewport(w, h); command_buffer->SetScissor(w, h); + command_buffer->BindPipeline(Specifications::PipelineBindPoint::GRAPHIC, pass->Pipeline); + command_buffer->BindDescriptorSets(device->SwapchainPtr->CurrentFrame->Index, &scene->CameraHeapOffset, 1u); + command_buffer->DrawIndirect(device->FrameHeaps[device->SwapchainPtr->CurrentFrame->Index].Handle, scene->IndirectHeapOffset, scene->IndirectCommandCount); } - command_buffer->BindPipeline(Specifications::PipelineBindPoint::GRAPHIC, pass->Pipeline); - command_buffer->BindDescriptorSets(device->SwapchainPtr->CurrentFrame->Index, scene ? &scene->CameraHeapOffset : nullptr, scene ? 1u : 0u); - command_buffer->DrawIndirect(device->FrameHeaps[device->SwapchainPtr->CurrentFrame->Index].Handle, scene->IndirectHeapOffset, scene->IndirectCommandCount); command_buffer->EndRenderPass(); } - void LightingPass::Setup(Hardwares::VulkanDevicePtr const device, cstring name, RenderGraphResourceBuilderPtr const res_builder, RenderGraphResourceInspectorPtr res_inspector) - { - // auto& builder = graph->Builder; - // auto& renderer = graph->Renderer; - - // Specifications::TextureSpecification lighting_output_spec = {.Width = 1280, .Height = 780, .Format = ImageFormat::R8G8B8A8_UNORM}; - // auto& lighting_output = builder->CreateRenderTarget("lighting_render_target", lighting_output_spec); - // RenderGraphRenderPassCreation pass_node = {.Name = name.data()}; - - // pass_node.Inputs.init(graph->Renderer->Device->Arena, 5); - // pass_node.Outputs.init(graph->Renderer->Device->Arena, 1); + void LightingPass::Setup(Hardwares::VulkanDevicePtr const, cstring, RenderGraphResourceBuilderPtr const, RenderGraphResourceInspectorPtr) {} - // pass_node.Inputs.push(RenderGraphRenderPassInputOutputInfo{.Name = renderer->FrameDepthRenderTargetName}); - // pass_node.Inputs.push({.Name = "gbuffer_albedo_render_target", .BindingInputKeyName = "AlbedoSampler", .Type = RenderGraphResourceType::TEXTURE}); - // pass_node.Inputs.push({.Name = "gbuffer_position_render_target", .BindingInputKeyName = "PositionSampler", .Type = RenderGraphResourceType::TEXTURE}); - // pass_node.Inputs.push({.Name = "gbuffer_normals_render_target", .BindingInputKeyName = "NormalSampler", .Type = RenderGraphResourceType::TEXTURE}); - // pass_node.Inputs.push({.Name = "gbuffer_specular_render_target", .BindingInputKeyName = "SpecularSampler", .Type = RenderGraphResourceType::TEXTURE}); - // pass_node.Outputs.push(RenderGraphRenderPassInputOutputInfo{.Name = lighting_output.Name}); + void LightingPass::Compile(Hardwares::VulkanDevicePtr const, Rendering::Scenes::SceneDataPtr const, RenderPasses::RenderPassBuilder*, RenderGraphResourceInspectorPtr, RenderPasses::RenderPass** const) {} - // builder->CreateRenderPassNode(pass_node); - } - - void LightingPass::Compile(Hardwares::VulkanDevicePtr const device, Rendering::Scenes::SceneDataPtr const scene, RenderPasses::RenderPassBuilder* pass_builder, RenderGraphResourceInspectorPtr res_inspector, RenderPasses::RenderPass** const output_pass) - { - // if (!pass) - //{ - // return; - // } - - // auto& builder = graph->RenderPassBuilder; - // auto& renderer = graph->Renderer; - - // if (pass && !(*pass)) - //{ - // auto pass_spec = builder->SetPipelineName("Deferred-lighting-Pipeline").EnablePipelineDepthTest(true).UseShader("deferred_lighting").Detach(); - - // *pass = renderer->CreateRenderPass(pass_spec); - // (*pass)->Bake(); - //} - - //(*pass)->SetInput("UBCamera", renderer->SceneCameraBufferHandle); - //(*pass)->SetInput("VertexSB", scene->VertexBufferHandle); - //(*pass)->SetInput("IndexSB", scene->IndexBufferHandle); - //(*pass)->SetInput("DrawDataSB", scene->IndirectDataDrawBufferHandle); - //(*pass)->SetInput("TransformSB", scene->TransformBufferHandle); - //(*pass)->SetInput("MatSB", scene->MaterialBufferHandle); - - // auto directional_light_buffer = graph->GetStorageBufferSet("g_scene_directional_light_buffer"); - // auto point_light_buffer = graph->GetStorageBufferSet("g_scene_point_light_buffer"); - // auto spot_light_buffer = graph->GetStorageBufferSet("g_scene_spot_light_buffer"); - - //(*pass)->SetInput("DirectionalLightSB", directional_light_buffer); - //(*pass)->SetInput("PointLightSB", point_light_buffer); - //(*pass)->SetInput("SpotLightSB", spot_light_buffer); - - //(*pass)->Verify(); - } - - void LightingPass::Execute(Hardwares::VulkanDevicePtr const device, RenderGraphResourceInspectorPtr res_inspector, Rendering::Scenes::SceneDataPtr const scene, RenderPasses::RenderPass* const pass, Buffers::FramebufferVNext* const framebuffer, Hardwares::CommandBufferPtr const command_buffer) - { - // auto directional_light_buffer_handle = graph->GetStorageBufferSet("g_scene_directional_light_buffer"); - // auto point_light_buffer_handle = graph->GetStorageBufferSet("g_scene_point_light_buffer"); - // auto spot_light_buffer_handle = graph->GetStorageBufferSet("g_scene_spot_light_buffer"); - ///* - // * Composing Light Data - // */ - // auto directional_light_buffer = graph->Renderer->Device->StorageBufferSetManager.Access(directional_light_buffer_handle); - // auto point_light_buffer = graph->Renderer->Device->StorageBufferSetManager.Access(point_light_buffer_handle); - // auto spot_light_buffer = graph->Renderer->Device->StorageBufferSetManager.Access(spot_light_buffer_handle); - - // auto dir_light_data = Lights::CreateLightBuffer(scene_data->DirectionalLights); - // auto point_light_data = Lights::CreateLightBuffer(scene_data->PointLights); - // auto spot_light_data = Lights::CreateLightBuffer(scene_data->SpotLights); - - // directional_light_buffer->SetData(frame_index, dir_light_data); - // point_light_buffer->SetData(frame_index, point_light_data); - // spot_light_buffer->SetData(frame_index, spot_light_data); - - // if (!scene->IndirectBufferHandle) - //{ - // return; - // } - - // auto renderer = graph->Renderer; - // auto indirect_buffer = renderer->Device->IndirectBufferSetManager.Access(scene->IndirectBufferHandle); - - // command_buffer->BeginRenderPass(pass, framebuffer->Handle); - // command_buffer->BindDescriptorSets(scene->FrameIndex); - // command_buffer->DrawIndirect(*indirect_buffer->At(scene->FrameIndex)); - // command_buffer->EndRenderPass(); - } + void LightingPass::Execute(Hardwares::VulkanDevicePtr const, RenderGraphResourceInspectorPtr, Rendering::Scenes::SceneDataPtr const, RenderPasses::RenderPass* const, Buffers::FramebufferVNext* const, Hardwares::CommandBufferPtr const) {} } // namespace ZEngine::Rendering::Renderers diff --git a/ZEngine/ZEngine/Rendering/Renderers/RendererPasses.h b/ZEngine/ZEngine/Rendering/Renderers/RendererPasses.h index 586e090bf..06d04a37f 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/RendererPasses.h +++ b/ZEngine/ZEngine/Rendering/Renderers/RendererPasses.h @@ -9,7 +9,11 @@ namespace ZEngine::Rendering::Renderers { - struct BasePass : public IRenderGraphCallbackPass + // Temporary albedo passthrough until LightingPass is implemented. + // Reads gbuffer_albedo_render_target and blits it to FrameColorRenderTarget + // using the "composite" full-screen shader. Replace with LightingPass when + // deferred lighting (DirectionalLightSB / PointLightSB / SpotLightSB) is ready. + struct CompositePass : public IRenderGraphCallbackPass { virtual void Setup(Hardwares::VulkanDevicePtr const device, cstring name, RenderGraphResourceBuilderPtr const res_builder, RenderGraphResourceInspectorPtr res_inspector) override; virtual void Compile(Hardwares::VulkanDevicePtr const device, Rendering::Scenes::SceneDataPtr const scene, RenderPasses::RenderPassBuilder* pass_builder, RenderGraphResourceInspectorPtr res_inspector, RenderPasses::RenderPass** const output_pass) override; diff --git a/ZEngine/ZEngine/Rendering/Shaders/Shader.cpp b/ZEngine/ZEngine/Rendering/Shaders/Shader.cpp index 068be65ad..3356e698d 100644 --- a/ZEngine/ZEngine/Rendering/Shaders/Shader.cpp +++ b/ZEngine/ZEngine/Rendering/Shaders/Shader.cpp @@ -87,6 +87,13 @@ namespace ZEngine::Rendering::Shaders } } + BindingsByName.init(&LocalArena, static_cast(LayoutBindingSpecifications.size() * 2) + 4); + for (const auto& spec : LayoutBindingSpecifications) + { + if (spec.Name) + BindingsByName[spec.Name] = spec; + } + // We remove the Set to avoid double release from the Device and Shader owned resource for (const auto [set, _] : m_device->ShaderReservedDescriptorSetLayoutMap) { @@ -267,7 +274,8 @@ namespace ZEngine::Rendering::Shaders for (const auto& SI_resource : fragment_resources.sampled_images) { - uint32_t set = spirv_compiler->get_decoration(SI_resource.id, spv::DecorationDescriptorSet); + uint32_t set = spirv_compiler->get_decoration(SI_resource.id, spv::DecorationDescriptorSet); + uint32_t binding = spirv_compiler->get_decoration(SI_resource.id, spv::DecorationBinding); if (m_device->ShaderReservedLayoutBindingSpecificationMap.contains(set)) { @@ -275,10 +283,9 @@ namespace ZEngine::Rendering::Shaders LayoutBindingSpecification binding_spec = {}; for (size_t i = 0; i < binding_specifications.size(); ++i) { - const auto& spec = binding_specifications[i]; - if (Helpers::secure_strcmp(spec.Name, SI_resource.name.c_str()) == 0) + if (binding_specifications[i].Binding == binding) { - binding_spec = spec; + binding_spec = binding_specifications[i]; break; } } @@ -292,11 +299,9 @@ namespace ZEngine::Rendering::Shaders continue; } - uint32_t binding = spirv_compiler->get_decoration(SI_resource.id, spv::DecorationBinding); - - const auto& type = spirv_compiler->get_type(SI_resource.type_id); - uint32_t count = std::min(type.array.empty() ? 1 : type.array[0], 256u); + const auto& type = spirv_compiler->get_type(SI_resource.type_id); + uint32_t count = std::min(type.array.empty() ? 1 : type.array[0], 256u); if (LayoutBindingSpecificationMap[set].capacity() <= 0) { @@ -311,7 +316,8 @@ namespace ZEngine::Rendering::Shaders for (const auto& SI_resource : fragment_resources.separate_images) { - uint32_t set = spirv_compiler->get_decoration(SI_resource.id, spv::DecorationDescriptorSet); + uint32_t set = spirv_compiler->get_decoration(SI_resource.id, spv::DecorationDescriptorSet); + uint32_t binding = spirv_compiler->get_decoration(SI_resource.id, spv::DecorationBinding); if (m_device->ShaderReservedLayoutBindingSpecificationMap.contains(set)) { @@ -319,10 +325,9 @@ namespace ZEngine::Rendering::Shaders LayoutBindingSpecification binding_spec = {}; for (size_t i = 0; i < binding_specifications.size(); ++i) { - const auto& spec = binding_specifications[i]; - if (Helpers::secure_strcmp(spec.Name, SI_resource.name.c_str()) == 0) + if (binding_specifications[i].Binding == binding) { - binding_spec = spec; + binding_spec = binding_specifications[i]; break; } } @@ -336,11 +341,10 @@ namespace ZEngine::Rendering::Shaders continue; } - uint32_t binding = spirv_compiler->get_decoration(SI_resource.id, spv::DecorationBinding); - const auto& type = spirv_compiler->get_type(SI_resource.type_id); + const auto& type = spirv_compiler->get_type(SI_resource.type_id); - uint32_t count = std::min(type.array.empty() ? 1 : type.array[0], 256u); + uint32_t count = std::min(type.array.empty() ? 1 : type.array[0], 256u); if (LayoutBindingSpecificationMap[set].capacity() <= 0) { @@ -355,7 +359,8 @@ namespace ZEngine::Rendering::Shaders for (const auto& SI_resource : fragment_resources.separate_samplers) { - uint32_t set = spirv_compiler->get_decoration(SI_resource.id, spv::DecorationDescriptorSet); + uint32_t set = spirv_compiler->get_decoration(SI_resource.id, spv::DecorationDescriptorSet); + uint32_t binding = spirv_compiler->get_decoration(SI_resource.id, spv::DecorationBinding); if (m_device->ShaderReservedLayoutBindingSpecificationMap.contains(set)) { @@ -363,10 +368,9 @@ namespace ZEngine::Rendering::Shaders LayoutBindingSpecification binding_spec = {}; for (size_t i = 0; i < binding_specifications.size(); ++i) { - const auto& spec = binding_specifications[i]; - if (Helpers::secure_strcmp(spec.Name, SI_resource.name.c_str()) == 0) + if (binding_specifications[i].Binding == binding) { - binding_spec = spec; + binding_spec = binding_specifications[i]; break; } } @@ -380,11 +384,9 @@ namespace ZEngine::Rendering::Shaders continue; } - uint32_t binding = spirv_compiler->get_decoration(SI_resource.id, spv::DecorationBinding); - const auto& type = spirv_compiler->get_type(SI_resource.type_id); - - uint32_t count = std::min(type.array.empty() ? 1 : type.array[0], 256u); + const auto& type = spirv_compiler->get_type(SI_resource.type_id); + uint32_t count = std::min(type.array.empty() ? 1 : type.array[0], 256u); if (LayoutBindingSpecificationMap[set].capacity() <= 0) { @@ -401,25 +403,11 @@ namespace ZEngine::Rendering::Shaders Specifications::LayoutBindingSpecification Shader::GetLayoutBindingSpecification(cstring name) { - LayoutBindingSpecification binding_spec = {}; - if (!Helpers::secure_strlen(name)) - { - return binding_spec; - } - - for (const auto& layout_binding : LayoutBindingSpecificationMap) - { - const auto& binding_specification_collection = layout_binding.second; - auto find_it = std::find_if(binding_specification_collection.begin(), binding_specification_collection.end(), [&](const LayoutBindingSpecification& spec) { return Helpers::secure_strcmp(spec.Name, name) == 0; }); + return {}; - if (find_it != std::end(binding_specification_collection)) - { - binding_spec = *find_it; - break; - } - } - return binding_spec; + const auto* spec = BindingsByName.find(name); + return spec ? *spec : LayoutBindingSpecification{}; } void Shader::Dispose() diff --git a/ZEngine/ZEngine/Rendering/Shaders/Shader.h b/ZEngine/ZEngine/Rendering/Shaders/Shader.h index 8a465eb37..231b8a399 100644 --- a/ZEngine/ZEngine/Rendering/Shaders/Shader.h +++ b/ZEngine/ZEngine/Rendering/Shaders/Shader.h @@ -25,6 +25,7 @@ namespace ZEngine::Rendering::Shaders Core::Containers::Array ShaderModules = {}; Core::Containers::Array SetLayouts = {}; Core::Containers::Array LayoutBindingSpecifications = {}; + Core::Containers::UnorderedHashMap BindingsByName = {}; Core::Containers::Array PushConstants = {}; Core::Containers::UnorderedHashMap> DescriptorSetMap = {}; //> Core::Containers::UnorderedHashMap InternalDescriptorSetLayoutMap = {}; // diff --git a/ZEngine/tests/Containers/hashset_test.cpp b/ZEngine/tests/Containers/hashset_test.cpp index c7bdda833..4df624e9d 100644 --- a/ZEngine/tests/Containers/hashset_test.cpp +++ b/ZEngine/tests/Containers/hashset_test.cpp @@ -26,7 +26,7 @@ TEST_F(UnorderedHashSetTest, InitialState) UnorderedHashSet set; set.init(&manager.MainArena, 10); EXPECT_EQ(set.size(), 0); - EXPECT_EQ(set.capacity(), 10); + EXPECT_GE(set.capacity(), 10); EXPECT_TRUE(set.empty()); } diff --git a/ZEngine/tests/Containers/ordered_hashmap_test.cpp b/ZEngine/tests/Containers/ordered_hashmap_test.cpp index 63c35d8e8..484ed0b18 100644 --- a/ZEngine/tests/Containers/ordered_hashmap_test.cpp +++ b/ZEngine/tests/Containers/ordered_hashmap_test.cpp @@ -13,7 +13,7 @@ class OrderedHashMapTest : public ::testing::Test protected: void SetUp() override { - manager.Initialize(2000, {}); + manager.Initialize(ZMega(4), {}); } void TearDown() override { @@ -22,19 +22,32 @@ class OrderedHashMapTest : public ::testing::Test MemoryManager manager; }; +// Basic API + TEST_F(OrderedHashMapTest, InitialState) { HashMap map; map.init(&manager.MainArena, 10); - EXPECT_EQ(map.size(), 0); - EXPECT_EQ(map.capacity(), 10); + EXPECT_EQ(map.size(), 0u); EXPECT_TRUE(map.empty()); + EXPECT_GE(map.capacity(), 16u); +} + +TEST_F(OrderedHashMapTest, CapacityAlwaysPowerOfTwo) +{ + for (int req : {1, 2, 3, 5, 7, 10, 17, 100, 906}) + { + HashMap m; + m.init(&manager.MainArena, req); + size_t cap = m.capacity(); + EXPECT_EQ(cap & (cap - 1), 0u) << "capacity " << cap << " is not power-of-2"; + } } TEST_F(OrderedHashMapTest, Contains) { HashMap map; - map.init(&manager.MainArena, 10); + map.init(&manager.MainArena, 16); map.insert(1, 10); EXPECT_TRUE(map.contains(1)); EXPECT_FALSE(map.contains(2)); @@ -43,13 +56,11 @@ TEST_F(OrderedHashMapTest, Contains) TEST_F(OrderedHashMapTest, BracketOperator) { HashMap map; - map.init(&manager.MainArena, 10); + map.init(&manager.MainArena, 16); map[1] = 10; EXPECT_EQ(map[1], 10); - map[1] = 20; EXPECT_EQ(map[1], 20); - EXPECT_EQ(map[2], 0); EXPECT_TRUE(map.contains(2)); } @@ -57,12 +68,11 @@ TEST_F(OrderedHashMapTest, BracketOperator) TEST_F(OrderedHashMapTest, Remove) { HashMap map; - map.init(&manager.MainArena, 10); + map.init(&manager.MainArena, 16); map.insert(1, 10); map.insert(2, 20); - EXPECT_EQ(map.size(), 2); map.remove(1); - EXPECT_EQ(map.size(), 1); + EXPECT_EQ(map.size(), 1u); EXPECT_FALSE(map.contains(1)); EXPECT_TRUE(map.contains(2)); } @@ -70,239 +80,266 @@ TEST_F(OrderedHashMapTest, Remove) TEST_F(OrderedHashMapTest, Find) { HashMap map; - map.init(&manager.MainArena, 10); + map.init(&manager.MainArena, 16); map.insert(1, 10); - int* value = map.find(1); - ASSERT_NE(value, nullptr); - EXPECT_EQ(*value, 10); - int* non_existent = map.find(2); - EXPECT_EQ(non_existent, nullptr); + ASSERT_NE(map.find(1), nullptr); + EXPECT_EQ(*map.find(1), 10); + EXPECT_EQ(map.find(2), nullptr); } TEST_F(OrderedHashMapTest, Clear) { HashMap map; - map.init(&manager.MainArena, 10); - + map.init(&manager.MainArena, 16); map.insert(1, 10); map.insert(2, 20); map.insert(3, 30); - - EXPECT_EQ(map.size(), 3); - map.clear(); - - EXPECT_EQ(map.size(), 0); + EXPECT_EQ(map.size(), 0u); EXPECT_TRUE(map.empty()); - EXPECT_FALSE(map.contains(1)); EXPECT_FALSE(map.contains(2)); EXPECT_FALSE(map.contains(3)); } -TEST_F(OrderedHashMapTest, Resize) -{ - HashMap map; - map.init(&manager.MainArena, 2); - - for (int i = 0; i < 10; ++i) - { - map.insert(i, i * 10); - } - - EXPECT_EQ(map.size(), 10); - - for (int i = 0; i < 10; ++i) - { - EXPECT_TRUE(map.contains(i)); - EXPECT_EQ(map[i], i * 10); - } - - EXPECT_GT(map.capacity(), 2); -} - TEST_F(OrderedHashMapTest, OverwriteValue) { HashMap map; - map.init(&manager.MainArena, 10); - - String str1; - str1.init(&manager.MainArena, "first"); - - String str2; - str2.init(&manager.MainArena, "updated"); - - map.insert(1, str1); - EXPECT_STREQ(map[1].c_str(), str1.c_str()); - - map.insert(1, str2); - EXPECT_STREQ(map[1].c_str(), str2.c_str()); + map.init(&manager.MainArena, 16); + String s1; + s1.init(&manager.MainArena, "first"); + String s2; + s2.init(&manager.MainArena, "updated"); + map.insert(1, s1); + map.insert(1, s2); + EXPECT_STREQ(map[1].c_str(), "updated"); } -TEST_F(OrderedHashMapTest, CollisionHandling) +TEST_F(OrderedHashMapTest, ExplicitReserveAndBulkInsert) { HashMap map; - map.init(&manager.MainArena, 2); - - map.insert(1, 10); - map.insert(3, 30); - map.insert(5, 50); - - EXPECT_TRUE(map.contains(1)); - EXPECT_TRUE(map.contains(3)); - EXPECT_TRUE(map.contains(5)); - - EXPECT_EQ(map[1], 10); - EXPECT_EQ(map[3], 30); - EXPECT_EQ(map[5], 50); + map.init(&manager.MainArena, 32); + map.reserve(200); + for (int i = 0; i < 100; ++i) + map.insert(i, i * 3); + EXPECT_EQ(map.size(), 100u); + for (int i = 0; i < 100; ++i) + EXPECT_EQ(*map.find(i), i * 3); } -// --- Insertion-order tests --- +// Insertion order TEST_F(OrderedHashMapTest, InsertionOrderPreserved) { HashMap map; map.init(&manager.MainArena, 16); - map.insert(10, 100); map.insert(20, 200); map.insert(30, 300); - std::vector keys; - for (auto [key, value] : map) - { - keys.push_back(key); - } - + for (auto [k, v] : map) + keys.push_back(k); ASSERT_EQ(keys.size(), 3u); EXPECT_EQ(keys[0], 10); EXPECT_EQ(keys[1], 20); EXPECT_EQ(keys[2], 30); } -TEST_F(OrderedHashMapTest, InsertionOrderAfterRemove) +TEST_F(OrderedHashMapTest, InsertionOrderAfterRemoveMiddle) { HashMap map; map.init(&manager.MainArena, 16); - map.insert(1, 10); map.insert(2, 20); map.insert(3, 30); map.remove(2); map.insert(4, 40); - std::vector keys; - for (auto [key, value] : map) - { - keys.push_back(key); - } - + for (auto [k, v] : map) + keys.push_back(k); ASSERT_EQ(keys.size(), 3u); EXPECT_EQ(keys[0], 1); EXPECT_EQ(keys[1], 3); EXPECT_EQ(keys[2], 4); } -TEST_F(OrderedHashMapTest, InsertionOrderPreservedAfterResize) +TEST_F(OrderedHashMapTest, RemoveHead_OrderCorrect) { HashMap map; - map.init(&manager.MainArena, 2); - - for (int i = 0; i < 10; ++i) - map.insert(i, i * 10); - + map.init(&manager.MainArena, 16); + map.insert(1, 10); + map.insert(2, 20); + map.insert(3, 30); + map.remove(1); std::vector keys; - for (auto [key, value] : map) - keys.push_back(key); + for (auto [k, v] : map) + keys.push_back(k); + ASSERT_EQ(keys.size(), 2u); + EXPECT_EQ(keys[0], 2); + EXPECT_EQ(keys[1], 3); +} - ASSERT_EQ(keys.size(), 10u); - for (int i = 0; i < 10; ++i) - EXPECT_EQ(keys[i], i); +TEST_F(OrderedHashMapTest, RemoveTail_OrderCorrect) +{ + HashMap map; + map.init(&manager.MainArena, 16); + map.insert(1, 10); + map.insert(2, 20); + map.insert(3, 30); + map.remove(3); + std::vector keys; + for (auto [k, v] : map) + keys.push_back(k); + ASSERT_EQ(keys.size(), 2u); + EXPECT_EQ(keys[0], 1); + EXPECT_EQ(keys[1], 2); } TEST_F(OrderedHashMapTest, UpdateDoesNotChangeOrder) { HashMap map; map.init(&manager.MainArena, 16); - map.insert(1, 10); map.insert(2, 20); map.insert(3, 30); map.insert(2, 999); - std::vector keys; - for (auto [key, value] : map) - keys.push_back(key); - + for (auto [k, v] : map) + keys.push_back(k); ASSERT_EQ(keys.size(), 3u); EXPECT_EQ(keys[0], 1); EXPECT_EQ(keys[1], 2); EXPECT_EQ(keys[2], 3); - EXPECT_EQ(map[2], 999); + EXPECT_EQ(*map.find(2), 999); } -// --- sort_keys() tests --- - -TEST_F(OrderedHashMapTest, SortKeysAscending) +TEST_F(OrderedHashMapTest, InsertionOrderPreservedAfterReserve) { HashMap map; - map.init(&manager.MainArena, 16); - map.insert(30, 300); - map.insert(10, 100); - map.insert(20, 200); - - map.sort_keys(); - + map.init(&manager.MainArena, 32); + for (int i = 0; i < 20; ++i) + map.insert(i, i * 10); + map.reserve(256); std::vector keys; - for (auto [key, value] : map) - keys.push_back(key); + for (auto [k, v] : map) + keys.push_back(k); + ASSERT_EQ(keys.size(), 20u); + for (int i = 0; i < 20; ++i) + EXPECT_EQ(keys[i], i); +} +TEST_F(OrderedHashMapTest, ReinsertDeletedKeyAppendsToTail) +{ + HashMap map; + map.init(&manager.MainArena, 32); + map.insert(1, 10); + map.insert(2, 20); + map.insert(3, 30); + map.remove(1); + map.insert(1, 100); + std::vector keys; + for (auto [k, v] : map) + keys.push_back(k); ASSERT_EQ(keys.size(), 3u); - EXPECT_EQ(keys[0], 10); - EXPECT_EQ(keys[1], 20); - EXPECT_EQ(keys[2], 30); + EXPECT_EQ(keys[0], 2); + EXPECT_EQ(keys[1], 3); + EXPECT_EQ(keys[2], 1); // appended at tail + EXPECT_EQ(*map.find(1), 100); +} + +TEST_F(OrderedHashMapTest, ClearThenRebuildOrder) +{ + HashMap map; + map.init(&manager.MainArena, 32); + for (int i = 0; i < 5; ++i) + map.insert(i, i); + map.clear(); + for (int i = 10; i < 15; ++i) + map.insert(i, i * 2); + std::vector keys; + for (auto [k, v] : map) + keys.push_back(k); + ASSERT_EQ(keys.size(), 5u); + for (int i = 0; i < 5; ++i) + EXPECT_EQ(keys[i], 10 + i); } -TEST_F(OrderedHashMapTest, SortKeysPreservesValues) +TEST_F(OrderedHashMapTest, IteratorOnEmptyMap) { HashMap map; map.init(&manager.MainArena, 16); - map.insert(3, 300); - map.insert(1, 100); - map.insert(2, 200); + int count = 0; + for (auto [k, v] : map) + ++count; + EXPECT_EQ(count, 0); +} - map.sort_keys(); +// sort_keys +TEST_F(OrderedHashMapTest, SortKeysAscending) +{ + HashMap map; + map.init(&manager.MainArena, 16); + map.insert(30, 300); + map.insert(10, 100); + map.insert(20, 200); + map.sort_keys(); std::vector> entries; - for (auto [key, value] : map) - { - entries.push_back({key, value}); - } - + for (auto [k, v] : map) + entries.push_back({k, v}); ASSERT_EQ(entries.size(), 3u); - EXPECT_EQ(entries[0].first, 1); + EXPECT_EQ(entries[0].first, 10); EXPECT_EQ(entries[0].second, 100); - EXPECT_EQ(entries[1].first, 2); + EXPECT_EQ(entries[1].first, 20); EXPECT_EQ(entries[1].second, 200); - EXPECT_EQ(entries[2].first, 3); + EXPECT_EQ(entries[2].first, 30); EXPECT_EQ(entries[2].second, 300); } -TEST_F(OrderedHashMapTest, SortKeysDoesNotAffectLookup) +TEST_F(OrderedHashMapTest, SortKeysPreservesLookup) { HashMap map; map.init(&manager.MainArena, 16); map.insert(30, 300); map.insert(10, 100); map.insert(20, 200); - map.sort_keys(); - EXPECT_EQ(*map.find(10), 100); EXPECT_EQ(*map.find(20), 200); EXPECT_EQ(*map.find(30), 300); } +TEST_F(OrderedHashMapTest, SortKeysIdempotent) +{ + HashMap map; + map.init(&manager.MainArena, 16); + map.insert(3, 30); + map.insert(1, 10); + map.insert(2, 20); + map.sort_keys(); + map.sort_keys(); + std::vector keys; + for (auto [k, v] : map) + keys.push_back(k); + ASSERT_EQ(keys.size(), 3u); + EXPECT_EQ(keys[0], 1); + EXPECT_EQ(keys[1], 2); + EXPECT_EQ(keys[2], 3); +} + +TEST_F(OrderedHashMapTest, SortKeysSingleEntry) +{ + HashMap map; + map.init(&manager.MainArena, 16); + map.insert(42, 420); + map.sort_keys(); + std::vector keys; + for (auto [k, v] : map) + keys.push_back(k); + ASSERT_EQ(keys.size(), 1u); + EXPECT_EQ(keys[0], 42); +} + TEST_F(OrderedHashMapTest, SortKeysOnEmptyMap) { HashMap map; @@ -314,21 +351,121 @@ TEST_F(OrderedHashMapTest, SortKeysOnEmptyMap) TEST_F(OrderedHashMapTest, InsertAfterSortAppendsTail) { HashMap map; - map.init(&manager.MainArena, 16); + map.init(&manager.MainArena, 32); map.insert(30, 300); map.insert(10, 100); map.sort_keys(); // order: 10, 30 + map.insert(20, 200); + std::vector keys; + for (auto [k, v] : map) + keys.push_back(k); + ASSERT_EQ(keys.size(), 3u); + EXPECT_EQ(keys[0], 10); + EXPECT_EQ(keys[1], 30); + EXPECT_EQ(keys[2], 20); // appended, not sorted +} - map.insert(20, 200); // appended after sort - +TEST_F(OrderedHashMapTest, SortAfterRemoveAndReinsert) +{ + HashMap map; + map.init(&manager.MainArena, 32); + map.insert(5, 50); + map.insert(3, 30); + map.insert(4, 40); + map.remove(3); + map.insert(1, 10); // order: 5, 4, 1 + map.sort_keys(); std::vector keys; - for (auto [key, value] : map) + for (auto [k, v] : map) + keys.push_back(k); + ASSERT_EQ(keys.size(), 3u); + EXPECT_EQ(keys[0], 1); + EXPECT_EQ(keys[1], 4); + EXPECT_EQ(keys[2], 5); +} + +// Edge cases + +TEST_F(OrderedHashMapTest, RemoveNonExistentIsNoop) +{ + HashMap map; + map.init(&manager.MainArena, 16); + map.insert(1, 10); + map.remove(999); + EXPECT_EQ(map.size(), 1u); + EXPECT_TRUE(map.contains(1)); +} + +TEST_F(OrderedHashMapTest, DuplicateInsertDoesNotChangeSize) +{ + HashMap map; + map.init(&manager.MainArena, 16); + map.insert(7, 70); + map.insert(7, 71); + map.insert(7, 72); + EXPECT_EQ(map.size(), 1u); + EXPECT_EQ(*map.find(7), 72); +} + +TEST_F(OrderedHashMapTest, LargeBatch_906Entries) +{ + HashMap map; + map.init(&manager.MainArena, 906 * 2 + 16); + for (uint32_t i = 0; i < 906; ++i) + map.insert(i, i * 5); + EXPECT_EQ(map.size(), 906u); + + // Check all present + for (uint32_t i = 0; i < 906; ++i) + EXPECT_EQ(*map.find(i), i * 5); + + // Check insertion order + uint32_t expected = 0; + for (auto [k, v] : map) { - keys.push_back(key); + EXPECT_EQ(k, expected); + ++expected; } + EXPECT_EQ(expected, 906u); +} - ASSERT_EQ(keys.size(), 3u); - EXPECT_EQ(keys[0], 10); - EXPECT_EQ(keys[1], 30); - EXPECT_EQ(keys[2], 20); +TEST_F(OrderedHashMapTest, CStringKeyContentEquality) +{ + HashMap map; + map.init(&manager.MainArena, 16); + char a[] = "hello"; + char b[] = "hello"; + map.insert(a, 99); + EXPECT_TRUE(map.contains(b)); + EXPECT_EQ(*map.find(b), 99); +} + +TEST_F(OrderedHashMapTest, FindKeyReturnsStablePointer) +{ + HashMap map; + map.init(&manager.MainArena, 16); + map.insert(5, 50); + const int* kp = map.find_key(5); + ASSERT_NE(kp, nullptr); + EXPECT_EQ(*kp, 5); + EXPECT_EQ(map.find_key(999), nullptr); +} + +TEST_F(OrderedHashMapTest, RemoveAllEntriesAndIteratorIsEmpty) +{ + HashMap map; + map.init(&manager.MainArena, 32); + for (int i = 0; i < 10; ++i) + map.insert(i, i); + for (int i = 0; i < 10; ++i) + map.remove(i); + int count = 0; + for (auto [k, v] : map) + ++count; + EXPECT_EQ(count, 0); + EXPECT_EQ(map.size(), 0u); + // Reinsertion works after removing all + map.insert(42, 420); + EXPECT_EQ(map.size(), 1u); + EXPECT_EQ(*map.find(42), 420); } diff --git a/ZEngine/tests/Containers/ordered_hashset_test.cpp b/ZEngine/tests/Containers/ordered_hashset_test.cpp index 6ee2851db..5fd9c0a22 100644 --- a/ZEngine/tests/Containers/ordered_hashset_test.cpp +++ b/ZEngine/tests/Containers/ordered_hashset_test.cpp @@ -26,7 +26,7 @@ TEST_F(OrderedHashSetTest, InitialState) HashSet set; set.init(&manager.MainArena, 10); EXPECT_EQ(set.size(), 0); - EXPECT_EQ(set.capacity(), 10); + EXPECT_GE(set.capacity(), 10); EXPECT_TRUE(set.empty()); } diff --git a/ZEngine/tests/Containers/unordered_hashmap_test.cpp b/ZEngine/tests/Containers/unordered_hashmap_test.cpp index ff5b31eec..9ed6d85a4 100644 --- a/ZEngine/tests/Containers/unordered_hashmap_test.cpp +++ b/ZEngine/tests/Containers/unordered_hashmap_test.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include using namespace ZEngine::Core::Containers; using namespace ZEngine::Core::Memory; @@ -12,7 +14,7 @@ class HashMapTest : public ::testing::Test protected: void SetUp() override { - manager.Initialize(2000, {}); + manager.Initialize(ZMega(4), {}); } void TearDown() override { @@ -21,19 +23,40 @@ class HashMapTest : public ::testing::Test MemoryManager manager; }; +// Basic API + TEST_F(HashMapTest, InitialState) { - UnorderedHashMap array; - array.init(&manager.MainArena, 10); - EXPECT_EQ(array.size(), 0); - EXPECT_EQ(array.capacity(), 10); - EXPECT_TRUE(array.empty()); + UnorderedHashMap map; + map.init(&manager.MainArena, 10); + EXPECT_EQ(map.size(), 0u); + EXPECT_TRUE(map.empty()); + EXPECT_GE(map.capacity(), 10u); +} + +TEST_F(HashMapTest, CapacityAlwaysPowerOfTwo) +{ + for (int req : {1, 2, 3, 5, 7, 10, 17, 100, 906}) + { + UnorderedHashMap m; + m.init(&manager.MainArena, req); + size_t cap = m.capacity(); + EXPECT_GT(cap, 0u); + EXPECT_EQ(cap & (cap - 1), 0u) << "capacity " << cap << " is not power-of-2"; + } +} + +TEST_F(HashMapTest, MinimumCapacityIs16) +{ + UnorderedHashMap m; + m.init(&manager.MainArena, 1); + EXPECT_GE(m.capacity(), 16u); } TEST_F(HashMapTest, Contains) { UnorderedHashMap map; - map.init(&manager.MainArena, 10); + map.init(&manager.MainArena, 16); map.insert(1, 10); EXPECT_TRUE(map.contains(1)); EXPECT_FALSE(map.contains(2)); @@ -42,26 +65,23 @@ TEST_F(HashMapTest, Contains) TEST_F(HashMapTest, BracketOperator) { UnorderedHashMap map; - map.init(&manager.MainArena, 10); + map.init(&manager.MainArena, 16); map[1] = 10; EXPECT_EQ(map[1], 10); - map[1] = 20; EXPECT_EQ(map[1], 20); - - EXPECT_EQ(map[2], 0); + EXPECT_EQ(map[2], 0); // default-inserts EXPECT_TRUE(map.contains(2)); } TEST_F(HashMapTest, Remove) { UnorderedHashMap map; - map.init(&manager.MainArena, 10); + map.init(&manager.MainArena, 16); map.insert(1, 10); map.insert(2, 20); - EXPECT_EQ(map.size(), 2); map.remove(1); - EXPECT_EQ(map.size(), 1); + EXPECT_EQ(map.size(), 1u); EXPECT_FALSE(map.contains(1)); EXPECT_TRUE(map.contains(2)); } @@ -69,194 +89,267 @@ TEST_F(HashMapTest, Remove) TEST_F(HashMapTest, Find) { UnorderedHashMap map; - map.init(&manager.MainArena, 10); + map.init(&manager.MainArena, 16); map.insert(1, 10); - int* value = map.find(1); - ASSERT_NE(value, nullptr); - EXPECT_EQ(*value, 10); - int* non_existent = map.find(2); - EXPECT_EQ(non_existent, nullptr); + int* v = map.find(1); + ASSERT_NE(v, nullptr); + EXPECT_EQ(*v, 10); + EXPECT_EQ(map.find(2), nullptr); } TEST_F(HashMapTest, Clear) { UnorderedHashMap map; - map.init(&manager.MainArena, 10); - - // Insert multiple elements + map.init(&manager.MainArena, 16); map.insert(1, 10); map.insert(2, 20); map.insert(3, 30); - - EXPECT_EQ(map.size(), 3); - map.clear(); - - EXPECT_EQ(map.size(), 0); + EXPECT_EQ(map.size(), 0u); EXPECT_TRUE(map.empty()); - EXPECT_FALSE(map.contains(1)); EXPECT_FALSE(map.contains(2)); EXPECT_FALSE(map.contains(3)); } -TEST_F(HashMapTest, Resize) +TEST_F(HashMapTest, OverwriteValue) +{ + UnorderedHashMap map; + map.init(&manager.MainArena, 16); + String s1; + s1.init(&manager.MainArena, "first"); + String s2; + s2.init(&manager.MainArena, "updated"); + map.insert(1, s1); + map.insert(1, s2); + EXPECT_STREQ(map[1].c_str(), "updated"); +} + +TEST_F(HashMapTest, ViewIteration) { UnorderedHashMap map; - map.init(&manager.MainArena, 2); + map.init(&manager.MainArena, 16); + map.insert(10, 100); + map.insert(20, 200); + map.insert(30, 300); - for (int i = 0; i < 10; ++i) + std::unordered_map expected = { + {10, 100}, + {20, 200}, + {30, 300} + }; + for (auto [k, v] : map) { - map.insert(i, i * 10); + auto it = expected.find(k); + ASSERT_NE(it, expected.end()); + EXPECT_EQ(v, it->second); + expected.erase(it); } + EXPECT_TRUE(expected.empty()); +} - EXPECT_EQ(map.size(), 10); +TEST_F(HashMapTest, ExplicitReserveAndBulkInsert) +{ + UnorderedHashMap map; + map.init(&manager.MainArena, 32); + map.reserve(200); - for (int i = 0; i < 10; ++i) + for (int i = 0; i < 100; ++i) + map.insert(i, i * 2); + + EXPECT_EQ(map.size(), 100u); + for (int i = 0; i < 100; ++i) { EXPECT_TRUE(map.contains(i)); - EXPECT_EQ(map[i], i * 10); + EXPECT_EQ(*map.find(i), i * 2); } - - EXPECT_GT(map.capacity(), 2); } -TEST_F(HashMapTest, OverwriteValue) -{ - UnorderedHashMap map; - map.init(&manager.MainArena, 10); - - String str1; - str1.init(&manager.MainArena, "first"); +// Edge cases - String str2; - str2.init(&manager.MainArena, "updated"); +TEST_F(HashMapTest, RemoveNonExistentIsNoop) +{ + UnorderedHashMap map; + map.init(&manager.MainArena, 16); + map.insert(1, 10); + map.remove(999); + EXPECT_EQ(map.size(), 1u); + EXPECT_TRUE(map.contains(1)); +} - map.insert(1, str1); - EXPECT_STREQ(map[1].c_str(), str1.c_str()); +TEST_F(HashMapTest, RemoveAndReinsertSameKey) +{ + UnorderedHashMap map; + map.init(&manager.MainArena, 32); + map.insert(42, 100); + map.remove(42); + EXPECT_FALSE(map.contains(42)); + EXPECT_EQ(map.size(), 0u); + map.insert(42, 200); + EXPECT_TRUE(map.contains(42)); + EXPECT_EQ(*map.find(42), 200); + EXPECT_EQ(map.size(), 1u); +} - map.insert(1, str2); - EXPECT_STREQ(map[1].c_str(), str2.c_str()); +TEST_F(HashMapTest, TombstoneReuse_RemoveAllThenReinsert) +{ + UnorderedHashMap map; + map.init(&manager.MainArena, 64); + for (int i = 0; i < 20; ++i) + map.insert(i, i); + for (int i = 0; i < 20; ++i) + map.remove(i); + EXPECT_EQ(map.size(), 0u); + + for (int i = 0; i < 20; ++i) + map.insert(i + 100, i + 100); + + EXPECT_EQ(map.size(), 20u); + for (int i = 0; i < 20; ++i) + EXPECT_EQ(*map.find(i + 100), i + 100); } -TEST_F(HashMapTest, CollisionHandling) +TEST_F(HashMapTest, DuplicateInsertDoesNotChangeSize) { UnorderedHashMap map; - map.init(&manager.MainArena, 2); + map.init(&manager.MainArena, 16); + map.insert(7, 70); + map.insert(7, 71); + map.insert(7, 72); + EXPECT_EQ(map.size(), 1u); + EXPECT_EQ(*map.find(7), 72); +} - map.insert(1, 10); - map.insert(3, 30); +TEST_F(HashMapTest, FindKeyReturnsStablePointer) +{ + UnorderedHashMap map; + map.init(&manager.MainArena, 16); map.insert(5, 50); - - EXPECT_TRUE(map.contains(1)); - EXPECT_TRUE(map.contains(3)); - EXPECT_TRUE(map.contains(5)); - - EXPECT_EQ(map[1], 10); - EXPECT_EQ(map[3], 30); - EXPECT_EQ(map[5], 50); + const int* kp = map.find_key(5); + ASSERT_NE(kp, nullptr); + EXPECT_EQ(*kp, 5); + EXPECT_EQ(map.find_key(999), nullptr); } -TEST_F(HashMapTest, ViewIteration) +TEST_F(HashMapTest, ClearThenReuseMap) { UnorderedHashMap map; - map.init(&manager.MainArena, 8); + map.init(&manager.MainArena, 32); + for (int i = 0; i < 10; ++i) + map.insert(i, i); + map.clear(); + for (int i = 100; i < 110; ++i) + map.insert(i, i * 2); + EXPECT_EQ(map.size(), 10u); + for (int i = 100; i < 110; ++i) + EXPECT_EQ(*map.find(i), i * 2); + for (int i = 0; i < 10; ++i) + EXPECT_FALSE(map.contains(i)); +} - map.insert(10, 100); - map.insert(20, 200); - map.insert(30, 300); +TEST_F(HashMapTest, IteratorOnEmptyMap) +{ + UnorderedHashMap map; + map.init(&manager.MainArena, 16); + int count = 0; + for (auto [k, v] : map) + ++count; + EXPECT_EQ(count, 0); +} - std::unordered_map expected = { - {10, 100}, - {20, 200}, - {30, 300} - }; +TEST_F(HashMapTest, IteratorSkipsDeletedSlots) +{ + UnorderedHashMap map; + map.init(&manager.MainArena, 32); + for (int i = 0; i < 10; ++i) + map.insert(i, i); + for (int i = 0; i < 10; i += 2) + map.remove(i); - for (auto [key, value] : map) + std::unordered_set seen; + for (auto [k, v] : map) { - auto it = expected.find(key); - ASSERT_NE(it, expected.end()); - EXPECT_EQ(value, it->second); - expected.erase(it); + seen.insert(k); + EXPECT_EQ(v, k); } + EXPECT_EQ(seen.size(), 5u); + for (int i = 1; i < 10; i += 2) + EXPECT_TRUE(seen.count(i)); +} - EXPECT_TRUE(expected.empty()); +TEST_F(HashMapTest, CStringKeyContentEquality) +{ + UnorderedHashMap map; + map.init(&manager.MainArena, 16); + + char a[] = "hello"; + char b[] = "hello"; // same content, different pointer + map.insert(a, 42); + EXPECT_TRUE(map.contains(b)); + EXPECT_EQ(*map.find(b), 42); } -TEST_F(HashMapTest, UserDefinedStructViewIterations) +TEST_F(HashMapTest, LargeBatch_906Entries) { - struct Person - { - String name; - int age; + UnorderedHashMap map; + map.init(&manager.MainArena, 906 * 2 + 16); - bool operator==(const Person& other) const - { - return name == other.name && age == other.age; - } - }; + for (uint32_t i = 0; i < 906; ++i) + map.insert(i, i * 3); - UnorderedHashMap map; - map.init(&manager.MainArena, 8); - - String str1; - str1.init(&manager.MainArena, "Alice"); - String str2; - str2.init(&manager.MainArena, "Bob"); - String str3; - str3.init(&manager.MainArena, "Carol"); - String str4; - str4.init(&manager.MainArena, "Engineer"); - String str5; - str5.init(&manager.MainArena, "Designer"); - String str6; - str6.init(&manager.MainArena, "Artist"); - - Person alice{str1, 30}; - Person bob{str2, 25}; - Person carol{str3, 28}; - - map.insert(alice, str4); - map.insert(bob, str5); - map.insert(carol, str6); - - EXPECT_TRUE(map.contains(alice)); - EXPECT_TRUE(map.contains(bob)); - - EXPECT_EQ(map[alice], str4); - EXPECT_EQ(map[bob], str5); - - struct ExpectedEntry + EXPECT_EQ(map.size(), 906u); + for (uint32_t i = 0; i < 906; ++i) { - Person key; - String value; - bool matched = false; - }; + EXPECT_TRUE(map.contains(i)); + EXPECT_EQ(*map.find(i), i * 3); + } +} - ExpectedEntry expected[] = { - {alice, str4}, - { bob, str5}, - {carol, str6} - }; +TEST_F(HashMapTest, CollisionCluster_AllHashToSameBucket) +{ + // keys 0, 16, 32, 48 all hash to bucket 0 (mod 64 with capacity=64) + UnorderedHashMap map; + map.init(&manager.MainArena, 128); - size_t matched_count = 0; + const int stride = 64; + for (int i = 0; i < 20; ++i) + map.insert(i * stride, i); + + for (int i = 0; i < 20; ++i) + { + EXPECT_TRUE(map.contains(i * stride)); + EXPECT_EQ(*map.find(i * stride), i); + } +} - for (auto [key, value] : map) +TEST_F(HashMapTest, UserDefinedStructKey) +{ + struct Point { - bool found = false; - for (auto& entry : expected) + int x, y; + bool operator==(const Point& o) const { - if (!entry.matched && entry.key == key) - { - EXPECT_EQ(value, entry.value); - entry.matched = true; - found = true; - matched_count++; - break; - } + return x == o.x && y == o.y; } - ASSERT_TRUE(found); - } + }; + UnorderedHashMap map; + map.init(&manager.MainArena, 32); + map.insert({1, 2}, 12); + map.insert({3, 4}, 34); + EXPECT_EQ(*map.find({1, 2}), 12); + EXPECT_EQ(*map.find({3, 4}), 34); + EXPECT_EQ(map.find({5, 6}), nullptr); +} - EXPECT_EQ(matched_count, 3); +TEST_F(HashMapTest, RemoveHeadOfCollisionChain) +{ + UnorderedHashMap map; + map.init(&manager.MainArena, 64); + // keys 0 and 64 both hash to bucket 0 (assuming capacity=64) + map.insert(0, 1000); + map.insert(64, 6400); + map.remove(0); + EXPECT_FALSE(map.contains(0)); + EXPECT_TRUE(map.contains(64)); + EXPECT_EQ(*map.find(64), 6400); } From e52590eee61aa0602fc0ff088758b8256fbfcd38 Mon Sep 17 00:00:00 2001 From: Jean Philippe Date: Fri, 21 Aug 2026 13:22:11 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(rendering):=20retire=20mesh=20geometry?= =?UTF-8?q?=20slot=20on=20actor=20delete=20=E2=80=94=20closes=20#639?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a mesh actor was deleted from the outliner RemoveMeshInstance removed the draw-indirect entry but never freed the MeshSlot in the RRM, so the slot index was permanently consumed for the session. - RenderResourceManager::ReleaseMeshGeometry(uuid): frees the MeshSlot (sets Generation=0 so AllocMeshSlot can reuse it) and removes the entry from m_uuid_to_buffer. The VB/IB bytes are not reclaimed — the global geometry buffer is append-only — but the slot is available for reuse. - RenderScene::RemoveMeshInstance: checks if any remaining instance shares the same MeshUUID before releasing; only calls ReleaseMeshGeometry when the deleted instance was the last reference to that mesh. - HierarchyViewUIComponent: passes ctx->RenderResourceManager to RemoveMeshInstance so the slot is freed at delete time. - RenderScene.h: forward-declares RenderResourceManager to avoid pulling the full header into every TU that includes RenderScene.h. --- .../Components/HierarchyViewUIComponent.cpp | 2 +- .../Rendering/RenderResourceManager.cpp | 23 +++++++++++++++++++ .../ZEngine/Rendering/RenderResourceManager.h | 12 ++++++---- .../ZEngine/Rendering/Scenes/RenderScene.cpp | 23 ++++++++++++++++++- .../ZEngine/Rendering/Scenes/RenderScene.h | 7 +++++- 5 files changed, 60 insertions(+), 7 deletions(-) diff --git a/Tetragrama/Components/HierarchyViewUIComponent.cpp b/Tetragrama/Components/HierarchyViewUIComponent.cpp index a67125dc7..7ac09d2ce 100644 --- a/Tetragrama/Components/HierarchyViewUIComponent.cpp +++ b/Tetragrama/Components/HierarchyViewUIComponent.cpp @@ -162,7 +162,7 @@ namespace Tetragrama::Components { auto* mc = actor->GetComponent(); if (mc && mc->RenderInstanceId != UINT32_MAX) - current_scene->RemoveMeshInstance(mc->RenderInstanceId); + current_scene->RemoveMeshInstance(mc->RenderInstanceId, ctx->RenderResourceManager); if (selected) current_scene->SelectedActorHandle = {}; pending_delete = h; diff --git a/ZEngine/ZEngine/Rendering/RenderResourceManager.cpp b/ZEngine/ZEngine/Rendering/RenderResourceManager.cpp index 896c4e3ad..b4a27fe9e 100644 --- a/ZEngine/ZEngine/Rendering/RenderResourceManager.cpp +++ b/ZEngine/ZEngine/Rendering/RenderResourceManager.cpp @@ -617,6 +617,29 @@ namespace ZEngine::Rendering return {}; } + void RenderResourceManager::ReleaseMeshGeometry(const uuids::uuid& uuid) + { + std::lock_guard lock(m_uuid_map_mutex); + + // Find and invalidate the slot + for (uint32_t i = 0; i < m_uuid_to_buffer_count; ++i) + { + if (m_uuid_to_buffer[i].UUID == uuid) + { + BufferHandle h = m_uuid_to_buffer[i].Handle; + + // Free the mesh slot (geometry bytes stay in VB/IB — append-only) + if (h.IsValid() && !(h.Generation & GBUF_GEN_TAG) && h.Index < m_mesh_slot_count) + m_mesh_slots[h.Index].Generation = 0; + + // Remove from UUID map (swap with last entry) + m_uuid_to_buffer[i] = m_uuid_to_buffer[--m_uuid_to_buffer_count]; + ZENGINE_CORE_INFO("[RRM] Released mesh geometry slot for UUID {}", uuids::to_string(uuid)) + return; + } + } + } + void RenderResourceManager::Release(BufferHandle handle) { if (!handle.IsValid()) diff --git a/ZEngine/ZEngine/Rendering/RenderResourceManager.h b/ZEngine/ZEngine/Rendering/RenderResourceManager.h index e2eb5cb1e..80e943a01 100644 --- a/ZEngine/ZEngine/Rendering/RenderResourceManager.h +++ b/ZEngine/ZEngine/Rendering/RenderResourceManager.h @@ -240,12 +240,16 @@ namespace ZEngine::Rendering void ResetGeometryBuffers(); /// @brief Find the BufferHandle registered for a mesh asset by UUID. - /// @details Returns an invalid handle if the mesh has not been uploaded yet or - /// the UUID is not in the uuid-to-buffer map. - /// @param uuid Asset UUID from the meta file. - /// @return Valid BufferHandle if found; invalid otherwise. BufferHandle FindMeshBuffer(const uuids::uuid& uuid) const; + /// @brief Release the geometry slot for a mesh and unregister its UUID. + /// @details Frees the MeshSlot so it can be reused by a future upload. + /// The VB/IB bytes are not reclaimed (append-only buffer) but the + /// slot index becomes available for the next UploadMesh call. + /// No-op if the UUID is not registered. + /// @param uuid Asset UUID of the mesh to release. + void ReleaseMeshGeometry(const uuids::uuid& uuid); + /// @brief Write CPU data into an existing HOST_VISIBLE BufferView. /// @details Uses the ring allocator for staging; falls back to a one-shot staging /// buffer and RecordAndSubmit. Render-thread only. diff --git a/ZEngine/ZEngine/Rendering/Scenes/RenderScene.cpp b/ZEngine/ZEngine/Rendering/Scenes/RenderScene.cpp index d754370e1..0fdb625d3 100644 --- a/ZEngine/ZEngine/Rendering/Scenes/RenderScene.cpp +++ b/ZEngine/ZEngine/Rendering/Scenes/RenderScene.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -35,20 +36,40 @@ namespace ZEngine::Rendering::Scenes return id; } - void RenderScene::RemoveMeshInstance(uint32_t id) + void RenderScene::RemoveMeshInstance(uint32_t id, Rendering::RenderResourceManager* rrm) { + uuids::uuid freed_uuid; + SeqBeginWrite(); for (uint32_t i = 0; i < Instances.size(); ++i) { if (Instances[i].Id == id) { + freed_uuid = Instances[i].MeshUUID; Instances.erase(i); break; } } SeqEndWrite(); + + // Release geometry if no other instance references the same mesh UUID. + if (rrm && !freed_uuid.is_nil()) + { + bool still_used = false; + for (uint32_t i = 0; i < Instances.size(); ++i) + { + if (Instances[i].MeshUUID == freed_uuid) + { + still_used = true; + break; + } + } + if (!still_used) + rrm->ReleaseMeshGeometry(freed_uuid); + } + MarkInstancesDirty(); } diff --git a/ZEngine/ZEngine/Rendering/Scenes/RenderScene.h b/ZEngine/ZEngine/Rendering/Scenes/RenderScene.h index 6431090c1..3b52b2cd1 100644 --- a/ZEngine/ZEngine/Rendering/Scenes/RenderScene.h +++ b/ZEngine/ZEngine/Rendering/Scenes/RenderScene.h @@ -8,6 +8,11 @@ #include #include +namespace ZEngine::Rendering +{ + class RenderResourceManager; +} + namespace ZEngine::Rendering::Scenes { struct GridConfig @@ -110,7 +115,7 @@ namespace ZEngine::Rendering::Scenes // --- Main-thread-only write operations --- uint32_t AddMeshInstance(const uuids::uuid& uuid, const char* name); - void RemoveMeshInstance(uint32_t id); + void RemoveMeshInstance(uint32_t id, ZEngine::Rendering::RenderResourceManager* rrm = nullptr); void SetInstanceTransform(uint32_t id, const Core::Maths::Mat4f& t); void MarkInstancesDirty(); From 0f848debd7f859383278490105a186885920ddae Mon Sep 17 00:00:00 2001 From: Jean Philippe Date: Fri, 21 Aug 2026 13:32:53 +0900 Subject: [PATCH 3/3] style: remove --- separator comments --- Tetragrama/Components/ProjectViewUIComponent.cpp | 3 --- ZEngine/ZEngine/Core/VFS/Meta/MetaFileIO.cpp | 2 -- .../ZEngine/Core/VFS/Registry/DependencyGraph.cpp | 4 ---- ZEngine/ZEngine/Rendering/Cameras/FlyCamera.cpp | 14 -------------- ZEngine/ZEngine/Rendering/Scenes/RenderScene.h | 2 -- 5 files changed, 25 deletions(-) diff --git a/Tetragrama/Components/ProjectViewUIComponent.cpp b/Tetragrama/Components/ProjectViewUIComponent.cpp index 30f13ec80..65ca8d388 100644 --- a/Tetragrama/Components/ProjectViewUIComponent.cpp +++ b/Tetragrama/Components/ProjectViewUIComponent.cpp @@ -278,7 +278,6 @@ namespace Tetragrama::Components char name[MAX_FILE_PATH_COUNT]; entry.Path.CopyFilename(name, sizeof(name)); - // --- UE thumbnail-first card layout --- // Icon fills the top portion, name overlaid on a semi-transparent footer strip. const float sz = m_thumbnail_size; const float pad = 6.0f; @@ -324,7 +323,6 @@ namespace Tetragrama::Components ImGui::EndPopup(); } - // ---- DrawList rendering ---- ImDrawList* dl = ImGui::GetWindowDrawList(); ImVec2 icon_end = {origin.x + card_w, origin.y + sz}; @@ -344,7 +342,6 @@ namespace Tetragrama::Components if (!dark_theme) dl->AddRect(origin, card_end, card_border, rounding, 0, 1.0f); - // --- Icon (vector, centered in the thumbnail area) --- // When a per-asset thumbnail is ready, call // dl->AddImage((ImTextureID)(intptr_t)thumb.Index, ixo, {ixo.x + ic, ixo.y + ic * 0.92f}) // instead of DrawContentIcon(). diff --git a/ZEngine/ZEngine/Core/VFS/Meta/MetaFileIO.cpp b/ZEngine/ZEngine/Core/VFS/Meta/MetaFileIO.cpp index 1d24dbbde..9f9381845 100644 --- a/ZEngine/ZEngine/Core/VFS/Meta/MetaFileIO.cpp +++ b/ZEngine/ZEngine/Core/VFS/Meta/MetaFileIO.cpp @@ -27,9 +27,7 @@ namespace ZEngine::Core::VFS } } // namespace - // ------------------------------------------------------------------------- // MetaFileIO - // ------------------------------------------------------------------------- VFSPath MetaFileIO::MetaPathFor(const VFSPath& asset_path) { diff --git a/ZEngine/ZEngine/Core/VFS/Registry/DependencyGraph.cpp b/ZEngine/ZEngine/Core/VFS/Registry/DependencyGraph.cpp index 0a42fd06f..78a2f1c58 100644 --- a/ZEngine/ZEngine/Core/VFS/Registry/DependencyGraph.cpp +++ b/ZEngine/ZEngine/Core/VFS/Registry/DependencyGraph.cpp @@ -4,9 +4,7 @@ namespace ZEngine::Core::VFS { - // ------------------------------------------------------------------------- // AdjacencyList - // ------------------------------------------------------------------------- bool AdjacencyList::Contains(const uuids::uuid& uuid) const { @@ -70,9 +68,7 @@ namespace ZEngine::Core::VFS return false; } - // ------------------------------------------------------------------------- // DependencyGraph - // ------------------------------------------------------------------------- void DependencyGraph::Initialize(Core::Memory::ArenaAllocator* arena) { diff --git a/ZEngine/ZEngine/Rendering/Cameras/FlyCamera.cpp b/ZEngine/ZEngine/Rendering/Cameras/FlyCamera.cpp index fa6824608..40fe404c9 100644 --- a/ZEngine/ZEngine/Rendering/Cameras/FlyCamera.cpp +++ b/ZEngine/ZEngine/Rendering/Cameras/FlyCamera.cpp @@ -44,9 +44,7 @@ namespace ZEngine::Rendering::Cameras UpdateMatrices(); } - // --------------------------------------------------------------------------- // Public accessors - // --------------------------------------------------------------------------- Quaternion FlyCamera::GetOrientation() const { @@ -73,9 +71,7 @@ namespace ZEngine::Rendering::Cameras return rotate(GetOrientation(), Vec3f(0.0f, 1.0f, 0.0f)); } - // --------------------------------------------------------------------------- // Configuration - // --------------------------------------------------------------------------- void FlyCamera::SetViewportSize(float logicalW, float logicalH) { @@ -98,9 +94,7 @@ namespace ZEngine::Rendering::Cameras m_viewDirty = true; } - // --------------------------------------------------------------------------- // OnUpdate — main entry point called once per frame by the controller - // --------------------------------------------------------------------------- void FlyCamera::OnUpdate(float dt) { @@ -191,9 +185,7 @@ namespace ZEngine::Rendering::Cameras Input.FlushDeltas(); } - // --------------------------------------------------------------------------- // Private update methods - // --------------------------------------------------------------------------- void FlyCamera::UpdateFree(float dt) { @@ -305,9 +297,7 @@ namespace ZEngine::Rendering::Cameras } } - // --------------------------------------------------------------------------- // Focus / bookmarks - // --------------------------------------------------------------------------- void FlyCamera::FocusOn(Vec3f center, float radius) { @@ -376,9 +366,7 @@ namespace ZEngine::Rendering::Cameras State = FlyCameraState::Animating; } - // --------------------------------------------------------------------------- // Ray unprojection - // --------------------------------------------------------------------------- FlyCamera::Ray FlyCamera::GetRayFromViewport(float viewportX, float viewportY) const { @@ -400,9 +388,7 @@ namespace ZEngine::Rendering::Cameras return {Position, mag > 0.0001f ? dir / mag : f}; } - // --------------------------------------------------------------------------- // Private helpers - // --------------------------------------------------------------------------- Vec3f FlyCamera::KeyboardMoveDir() const { diff --git a/ZEngine/ZEngine/Rendering/Scenes/RenderScene.h b/ZEngine/ZEngine/Rendering/Scenes/RenderScene.h index 3b52b2cd1..9c1b59fc3 100644 --- a/ZEngine/ZEngine/Rendering/Scenes/RenderScene.h +++ b/ZEngine/ZEngine/Rendering/Scenes/RenderScene.h @@ -113,13 +113,11 @@ namespace ZEngine::Rendering::Scenes PaddedAtomic GridDirty[3] = {}; GridConfig Grid = {}; - // --- Main-thread-only write operations --- uint32_t AddMeshInstance(const uuids::uuid& uuid, const char* name); void RemoveMeshInstance(uint32_t id, ZEngine::Rendering::RenderResourceManager* rrm = nullptr); void SetInstanceTransform(uint32_t id, const Core::Maths::Mat4f& t); void MarkInstancesDirty(); - // --- Render-thread read (seqlock snapshot) --- // Fills `out` with a consistent copy; retries if a write was in progress. void GetInstancesSnapshot(Core::Memory::ArenaAllocator* scratch, Core::Containers::Array& out) const;