diff --git a/Resources/Shaders/deferred_lighting.frag b/Resources/Shaders/deferred_lighting.frag index f17db3211..b2616df74 100644 --- a/Resources/Shaders/deferred_lighting.frag +++ b/Resources/Shaders/deferred_lighting.frag @@ -1,136 +1,144 @@ #version 460 -#extension GL_GOOGLE_include_directive : require -#include "bindings.glsl" -#include "light_types.glsl" -layout(location = 0) in vec2 TexCoord; -layout(location = 1) in vec4 ViewPos; - -layout(std140, set = 0, binding = 6) readonly buffer DirectionalLightSB +layout(set = 0, binding = 0) uniform UBCamera { - uint Count; - DirectionalLight Data[]; + mat4 View; + mat4 Projection; + vec4 Position; + mat4 InvViewProj; } -DirectionalLightBuffer; +Camera; -layout(std140, set = 0, binding = 7) readonly buffer PointLightSB -{ - uint Count; - PointLight Data[]; -} -PointLightBuffer; +layout(set = 2, binding = 0) uniform texture2D GBufferAlbedoAO; +layout(set = 2, binding = 1) uniform texture2D GBufferNormalRoughness; +layout(set = 2, binding = 2) uniform texture2D GBufferMetallicEmissive; +layout(set = 2, binding = 3) uniform texture2D GBufferDepth; +layout(set = 2, binding = 5) uniform sampler GBufferSampler; -layout(std140, set = 0, binding = 8) readonly buffer SpotLightSB +struct GpuDirectionalLight { - uint Count; - SpotLight Data[]; + vec4 Direction; + vec4 Color; + float Intensity; + float _p0; + float _p1; + float _p2; +}; +struct GpuPointLight +{ + vec4 Position; + vec4 Color; + float Intensity; + float Radius; + float _p0; + float _p1; +}; + +layout(std430, set = 2, binding = 4) readonly buffer LightSB +{ + GpuDirectionalLight DirectionalLights[4]; + GpuPointLight PointLights[8]; + uint DirectionalCount; + uint PointCount; + uint _pad[2]; } -SpotLightBuffer; - -layout(set = 0, binding = 10) uniform sampler2D AlbedoSampler; -layout(set = 0, binding = 11) uniform sampler2D PositionSampler; -layout(set = 0, binding = 12) uniform sampler2D NormalSampler; -layout(set = 0, binding = 13) uniform sampler2D SpecularSampler; +LightBuffer; +layout(location = 0) in vec2 TexCoord; layout(location = 0) out vec4 OutColor; -vec3 ComputeDirectionalLight(DirectionalLight light, vec3 normal, vec3 viewDir, vec3 albedoMap, vec3 specularMap) -{ - vec3 direction = light.Direction.xyz; - - vec3 lightDir = normalize(direction); - vec3 ambient = light.Ambient.xyz * albedoMap; - - float diff = max(dot(normal, lightDir), 0.0); - vec3 diffuse = diff * light.Diffuse.xyz * albedoMap; +const float PI = 3.14159265359; - vec3 reflectDir = reflect(-lightDir, normal); - float spec = pow(max(dot(viewDir, reflectDir), 0.0), 16); - vec3 specular = spec * light.Specular.xyz * specularMap; - - return vec3(ambient + diffuse + specular); +vec3 reconstruct_position(vec2 uv, float depth) +{ + vec4 ndc = vec4(uv * 2.0 - 1.0, depth, 1.0); + vec4 world = Camera.InvViewProj * ndc; + return world.xyz / world.w; } -vec3 ComputePointLight(PointLight light, vec3 normal, vec3 viewDir, vec4 fragPos, vec3 albedoMap, vec3 specularMap) +float distribution_ggx(vec3 N, vec3 H, float roughness) { - float dist = length(light.Position.xyz - fragPos.xyz); - float attenuation = 1.0 / (light.Constant + (light.Linear * dist) + (light.Quadratic * (dist * dist))); - - vec3 lightDir = normalize(light.Position.xyz - fragPos.xyz); - vec3 ambient = light.Ambient.xyz * albedoMap; - - float diff = max(dot(normal, lightDir), 0.0); - vec3 diffuse = diff * light.Diffuse.xyz * albedoMap; - - vec3 reflectDir = reflect(-lightDir, normal); - float spec = pow(max(dot(viewDir, reflectDir), 0.0), 16); // todo : 16 should be replaced by material.shininess - vec3 specular = spec * light.Specular.xyz * specularMap; - - ambient *= attenuation; - diffuse *= attenuation; - specular *= attenuation; - - return vec3(ambient + diffuse + specular); + float a = roughness * roughness; + float a2 = a * a; + float NdH = max(dot(N, H), 0.0); + float d = NdH * NdH * (a2 - 1.0) + 1.0; + return a2 / (PI * d * d); } -vec3 ComputeSpotLight(SpotLight light, vec3 normal, vec3 viewDir, vec3 fragPos, vec3 albedoMap, vec3 specularMap) +float geometry_schlick_ggx(float NdV, float roughness) { - vec3 lightDir = normalize(light.Position.xyz - fragPos); - vec3 direction = normalize(light.Direction.xyz); - float theta = dot(lightDir, direction); // check if lighting is inside the spotlight cone - - if (theta > light.CutOff) - { - vec3 ambient = light.Ambient.xyz * albedoMap; - - float diff = max(dot(normal, lightDir), 0.0); - vec3 diffuse = diff * light.Diffuse.xyz * albedoMap; + float r = roughness + 1.0; + float k = (r * r) / 8.0; + return NdV / (NdV * (1.0 - k) + k); +} - vec3 reflectDir = reflect(-lightDir, normal); - float spec = pow(max(dot(viewDir, reflectDir), 0.0), 16); // todo : 16 should be replaced by material.shininess - vec3 specular = spec * light.Specular.xyz * specularMap; +float geometry_smith(vec3 N, vec3 V, vec3 L, float roughness) +{ + return geometry_schlick_ggx(max(dot(N, V), 0.0), roughness) * geometry_schlick_ggx(max(dot(N, L), 0.0), roughness); +} - float dist = length(light.Position.xyz - fragPos); - float attenuation = 1.0 / (light.Constant + (light.Linear * dist) + (light.Quadratic * (dist * dist))); +vec3 fresnel_schlick(float cosTheta, vec3 F0) +{ + return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); +} - diffuse *= attenuation; - specular *= attenuation; +vec3 pbr_directional(GpuDirectionalLight light, vec3 N, vec3 V, vec3 albedo, float roughness, float metallic) +{ + vec3 F0 = mix(vec3(0.04), albedo, metallic); + vec3 L = normalize(-light.Direction.xyz); + vec3 H = normalize(V + L); + vec3 radiance = light.Color.rgb * light.Intensity; + float NDF = distribution_ggx(N, H, roughness); + float G = geometry_smith(N, V, L, roughness); + vec3 F = fresnel_schlick(max(dot(H, V), 0.0), F0); + vec3 spec = (NDF * G * F) / (4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001); + vec3 kD = (1.0 - F) * (1.0 - metallic); + return (kD * albedo / PI + spec) * radiance * max(dot(N, L), 0.0); +} - return vec3(ambient + diffuse + specular); - } - else - { - return vec3(light.Ambient.xyz * albedoMap); - } +vec3 pbr_point(GpuPointLight light, vec3 WorldPos, vec3 N, vec3 V, vec3 albedo, float roughness, float metallic) +{ + vec3 F0 = mix(vec3(0.04), albedo, metallic); + vec3 L = normalize(light.Position.xyz - WorldPos); + vec3 H = normalize(V + L); + float dist = length(light.Position.xyz - WorldPos); + float attenuation = 1.0 / (dist * dist); + vec3 radiance = light.Color.rgb * light.Intensity * attenuation; + float NDF = distribution_ggx(N, H, roughness); + float G = geometry_smith(N, V, L, roughness); + vec3 F = fresnel_schlick(max(dot(H, V), 0.0), F0); + vec3 spec = (NDF * G * F) / (4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001); + vec3 kD = (1.0 - F) * (1.0 - metallic); + return (kD * albedo / PI + spec) * radiance * max(dot(N, L), 0.0); } void main() { - vec3 norm = texture(NormalSampler, TexCoord).rgb; - vec4 fragPos = texture(PositionSampler, TexCoord); - vec3 albedo = texture(AlbedoSampler, TexCoord).rgb; - vec3 specular = texture(SpecularSampler, TexCoord).rgb; - - vec3 viewDir = normalize(ViewPos.xyz - fragPos.xyz); - - vec3 lighting = vec3(0.0); - for (uint i = 0; i < DirectionalLightBuffer.Count; ++i) - { - DirectionalLight directionalLight = DirectionalLightBuffer.Data[i]; - lighting += ComputeDirectionalLight(directionalLight, norm, viewDir, albedo, specular); - } - - for (uint i = 0; i < PointLightBuffer.Count; ++i) - { - PointLight pointLight = PointLightBuffer.Data[i]; - lighting += ComputePointLight(pointLight, norm, viewDir, fragPos, albedo, specular); - } - - for (uint i = 0; i < SpotLightBuffer.Count; ++i) - { - SpotLight spotLight = SpotLightBuffer.Data[i]; - lighting += ComputeSpotLight(spotLight, norm, viewDir, fragPos.xyz, albedo, specular); - } - - OutColor = vec4(lighting, 1.0); + vec4 sAlbedoAO = texture(sampler2D(GBufferAlbedoAO, GBufferSampler), TexCoord); + vec4 sNormalRough = texture(sampler2D(GBufferNormalRoughness, GBufferSampler), TexCoord); + vec4 sMetEmit = texture(sampler2D(GBufferMetallicEmissive, GBufferSampler), TexCoord); + float depth = texture(sampler2D(GBufferDepth, GBufferSampler), TexCoord).r; + + vec3 albedo = sAlbedoAO.rgb; + float ao = sAlbedoAO.a; + vec3 N = normalize(sNormalRough.rgb * 2.0 - 1.0); + float roughness = max(sNormalRough.a, 0.04); + float metallic = sMetEmit.r; + float emissive = sMetEmit.g; + + vec3 WorldPos = reconstruct_position(TexCoord, depth); + vec3 V = normalize(Camera.Position.xyz - WorldPos); + + vec3 Lo = vec3(0.0); + for (uint i = 0; i < LightBuffer.DirectionalCount; ++i) + Lo += pbr_directional(LightBuffer.DirectionalLights[i], N, V, albedo, roughness, metallic); + for (uint i = 0; i < LightBuffer.PointCount; ++i) + Lo += pbr_point(LightBuffer.PointLights[i], WorldPos, N, V, albedo, roughness, metallic); + + vec3 ambient = vec3(0.03) * albedo * ao; + vec3 color = ambient + Lo + albedo * emissive; + + color = color / (color + vec3(1.0)); + color = pow(color, vec3(1.0 / 2.2)); + OutColor = vec4(color, 1.0); } \ No newline at end of file diff --git a/Resources/Shaders/deferred_lighting.vert b/Resources/Shaders/deferred_lighting.vert index 1f081de8b..7251e6a4c 100644 --- a/Resources/Shaders/deferred_lighting.vert +++ b/Resources/Shaders/deferred_lighting.vert @@ -1,17 +1,10 @@ #version 460 -#extension GL_GOOGLE_include_directive : require -#include "draw.glsl" layout(location = 0) out vec2 TexCoord; -layout(location = 1) out vec4 ViewPos; void main() { - DrawDataView dataView = GetDrawDataView(); - - vec4 worldPos = dataView.Transform * dataView.Vertex; - ViewPos = Camera.Position; - gl_Position = Camera.Projection * Camera.View * worldPos; - // Convert gl_Position from NDC [-1, 1] to texture coordinates [0, 1] - TexCoord = (gl_Position.xy / gl_Position.w) * 0.5 + 0.5; + vec2 uv = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); + TexCoord = uv; + gl_Position = vec4(uv * 2.0 - 1.0, 0.0, 1.0); } \ No newline at end of file diff --git a/Resources/Shaders/g_buffer.frag b/Resources/Shaders/g_buffer.frag index 2d0e403ea..d6dcfaa66 100644 --- a/Resources/Shaders/g_buffer.frag +++ b/Resources/Shaders/g_buffer.frag @@ -4,38 +4,46 @@ layout(location = 0) in vec2 TexCoord; layout(location = 1) in vec3 WorldNormal; -layout(location = 2) in vec4 FragPos; -layout(location = 3) in flat uint MaterialIdx; +layout(location = 2) in flat uint MaterialIdx; -layout(location = 0) out vec4 OutAlbedo; -layout(location = 1) out vec4 OutSpecular; -layout(location = 2) out vec3 OutNormal; -layout(location = 3) out vec4 OutPosition; +layout(location = 0) out vec4 OutAlbedoAO; +layout(location = 1) out vec4 OutNormalRoughness; +layout(location = 2) out vec4 OutMetallicEmissive; void main() { - MaterialData material = FetchMaterial(MaterialIdx); + MaterialData material = FetchMaterial(MaterialIdx); - OutNormal = normalize(WorldNormal); - OutSpecular = material.Specular; - OutAlbedo = material.Albedo; - OutPosition = FragPos; + vec3 albedo = material.Albedo.rgb; + float ao = 1.0; + vec3 normal = normalize(WorldNormal); + float roughness = material.Roughness.y; + float metallic = material.Roughness.x; + float emissive = material.Emissive.r; if (material.AlbedoMap < INVALID_MAP_HANDLE) { uint texId = uint(material.AlbedoMap); - OutAlbedo = texture(sampler2D(TextureArray[nonuniformEXT(texId)], LinearWrapSampler), TexCoord); + albedo = texture(sampler2D(TextureArray[nonuniformEXT(texId)], LinearWrapSampler), TexCoord).rgb; } if (material.SpecularMap < INVALID_MAP_HANDLE) { - uint texId = uint(material.SpecularMap); - OutSpecular = texture(sampler2D(TextureArray[nonuniformEXT(texId)], LinearWrapSampler), TexCoord); + // glTF metallicRoughnessTexture: R=occlusion, G=roughness, B=metallic + uint texId = uint(material.SpecularMap); + vec3 orm = texture(sampler2D(TextureArray[nonuniformEXT(texId)], LinearWrapSampler), TexCoord).rgb; + ao = orm.r; + roughness = orm.g; + metallic = orm.b; } - if (material.NormalMap < INVALID_MAP_HANDLE) + if (material.EmissiveMap < INVALID_MAP_HANDLE) { - uint texId = uint(material.NormalMap); - OutNormal = texture(sampler2D(TextureArray[nonuniformEXT(texId)], LinearWrapSampler), TexCoord).rgb; + uint texId = uint(material.EmissiveMap); + emissive = texture(sampler2D(TextureArray[nonuniformEXT(texId)], LinearWrapSampler), TexCoord).r; } + + OutAlbedoAO = vec4(albedo, ao); + OutNormalRoughness = vec4(normal * 0.5 + 0.5, roughness); + OutMetallicEmissive = vec4(metallic, emissive, 0.0, 0.0); } diff --git a/Resources/Shaders/g_buffer.vert b/Resources/Shaders/g_buffer.vert index 0e5e0f141..bceaef351 100644 --- a/Resources/Shaders/g_buffer.vert +++ b/Resources/Shaders/g_buffer.vert @@ -4,17 +4,14 @@ layout(location = 0) out vec2 TexCoord; layout(location = 1) out vec3 WorldNormal; -layout(location = 2) out vec4 FragPos; -layout(location = 3) out flat uint MaterialIdx; +layout(location = 2) out flat uint MaterialIdx; void main() { DrawDataView dataView = GetDrawDataView(); - vec4 worldPos = dataView.Transform * dataView.Vertex; WorldNormal = transpose(inverse(mat3(dataView.Transform))) * dataView.Normal; TexCoord = dataView.TexCoord; MaterialIdx = dataView.MaterialId; - FragPos = worldPos; gl_Position = Camera.Projection * Camera.View * worldPos; } \ No newline at end of file diff --git a/Resources/Shaders/geometry_bindings.glsl b/Resources/Shaders/geometry_bindings.glsl index 1bd47e661..b10eb7440 100644 --- a/Resources/Shaders/geometry_bindings.glsl +++ b/Resources/Shaders/geometry_bindings.glsl @@ -7,6 +7,7 @@ layout(set = 0, binding = 0) uniform UBCamera mat4 View; mat4 Projection; vec4 Position; + mat4 InvViewProj; } Camera; diff --git a/ZEngine/ZEngine/Applications/AppRenderPipeline.cpp b/ZEngine/ZEngine/Applications/AppRenderPipeline.cpp index 5d2960ad5..3da5988d2 100644 --- a/ZEngine/ZEngine/Applications/AppRenderPipeline.cpp +++ b/ZEngine/ZEngine/Applications/AppRenderPipeline.cpp @@ -41,10 +41,7 @@ namespace ZEngine::Applications void AppRenderPipeline::ResizeRenderTarget(uint32_t w, uint32_t h) { if (SceneRenderer && SceneRenderer->RenderGraph) - { - auto rendergraph = SceneRenderer->RenderGraph; - rendergraph->Resize(w, h); - } + SceneRenderer->RenderGraph->Resize(w, h); } bool AppRenderPipeline::BeginFrame() diff --git a/ZEngine/ZEngine/Core/Memory/MemoryManager.h b/ZEngine/ZEngine/Core/Memory/MemoryManager.h index f54e81014..e9a490083 100644 --- a/ZEngine/ZEngine/Core/Memory/MemoryManager.h +++ b/ZEngine/ZEngine/Core/Memory/MemoryManager.h @@ -62,7 +62,7 @@ namespace ZEngine::Core::Memory cfg.Logging = {"Logging", ZMega(8ULL)}; cfg.VirtualFS = {"VirtualFS", ZMega(64ULL)}; cfg.VulkanDevice = {"VulkanDevice", ZGiga(1ULL)}; - cfg.ImportPipeline = {"ImportPipeline", ZGiga(1ULL)}; // engine importers (414 MB) + editor importers (414 MB) + coordinator + cfg.ImportPipeline = {"ImportPipeline", ZGiga(1ULL)}; // glTF 64 + Assimp 128 + envmap 32 + editor ~414 MB; each carves directly cfg.UIContext = {"UIContext", ZMega(64ULL)}; cfg.Swapchain = {"Swapchain", ZMega(8ULL)}; cfg.ShaderCache = {"ShaderCache", ZMega(64ULL)}; diff --git a/ZEngine/ZEngine/Engine.cpp b/ZEngine/ZEngine/Engine.cpp index 0ce6681ea..ce1b8d1ad 100644 --- a/ZEngine/ZEngine/Engine.cpp +++ b/ZEngine/ZEngine/Engine.cpp @@ -85,9 +85,9 @@ namespace ZEngine g_engine_ctx->WorldTick = ZPushStructCtor(&g_engine_ctx->ECSArena, ECS::WorldTick); g_engine_ctx->WorldTick->Initialize(&g_engine_ctx->ECSArena); - // ImportPipeline arena: covers engine importers (414 MB) + editor importers (414 MB) + coordinator overhead. - // Editor's AssetImporterUIComponent carves its own importer arenas from this same budget - // via Engine::GetContext()->ImportPipelineArena so all import memory is budget-tracked. + // ImportPipeline arena: each importer carves its own sub-arena directly from this + // parent (glTF 64 MB + Assimp 128 MB + envmap 32 MB + editor ~414 MB). + // Each Import() call ends with Arena.Clear() so the sub-arena is reused, not consumed. memory->CreateBudgetedArena(memory->Budget.ImportPipeline, &g_engine_ctx->ImportPipelineArena); g_engine_ctx->ImportCoordinator = ZPushStructCtor(&g_engine_ctx->AssetArena, Importers::ImportCoordinator); g_engine_ctx->ImportCoordinator->Initialize(&g_engine_ctx->AssetArena, g_engine_ctx->VFS, Managers::AssetManager::Instance()->Registry); @@ -95,15 +95,9 @@ namespace ZEngine static Importers::GltfImporter s_gltf_importer; static Importers::AssimpImporter s_assimp_importer; static Importers::EnvironmentMapImporter s_env_map_importer; - static Core::Memory::ArenaAllocator s_gltf_arena; - static Core::Memory::ArenaAllocator s_assimp_arena; - static Core::Memory::ArenaAllocator s_envmap_arena; - g_engine_ctx->ImportPipelineArena.CreateSubArena(ZMega(64), &s_gltf_arena); - g_engine_ctx->ImportPipelineArena.CreateSubArena(ZMega(350), &s_assimp_arena); - g_engine_ctx->ImportPipelineArena.CreateSubArena(ZMega(32), &s_envmap_arena); - s_gltf_importer.Initialize(&s_gltf_arena); - s_assimp_importer.Initialize(&s_assimp_arena); - s_env_map_importer.Initialize(&s_envmap_arena); + s_gltf_importer.Initialize(&g_engine_ctx->ImportPipelineArena); + s_assimp_importer.Initialize(&g_engine_ctx->ImportPipelineArena); + s_env_map_importer.Initialize(&g_engine_ctx->ImportPipelineArena); g_engine_ctx->ImportCoordinator->RegisterImporter(&s_gltf_importer); g_engine_ctx->ImportCoordinator->RegisterImporter(&s_assimp_importer); g_engine_ctx->ImportCoordinator->RegisterImporter(&s_env_map_importer); diff --git a/ZEngine/ZEngine/Importers/AssimpImporter.cpp b/ZEngine/ZEngine/Importers/AssimpImporter.cpp index cfbc60077..5c4b3e012 100644 --- a/ZEngine/ZEngine/Importers/AssimpImporter.cpp +++ b/ZEngine/ZEngine/Importers/AssimpImporter.cpp @@ -32,7 +32,7 @@ namespace ZEngine::Importers void AssimpImporter::Initialize(Core::Memory::ArenaAllocator* arena) { - arena->CreateSubArena(ZMega(350), &Arena); + arena->CreateSubArena(ZMega(128), &Arena); } bool AssimpImporter::CanImport(const char* extension) const diff --git a/ZEngine/ZEngine/Managers/AssetManager.cpp b/ZEngine/ZEngine/Managers/AssetManager.cpp index 30d5ad49a..8c325dae1 100644 --- a/ZEngine/ZEngine/Managers/AssetManager.cpp +++ b/ZEngine/ZEngine/Managers/AssetManager.cpp @@ -37,23 +37,23 @@ namespace ZEngine::Managers void AssetManager::Initialize(Core::Memory::ArenaAllocator* arena, Hardwares::VulkanDevice* device, cstring working_space_path) { - s_Instance = ZPushStructCtor(arena, AssetManager); - arena->CreateSubArena(ZMega(400), &s_Instance->Arena); + s_Instance = ZPushStructCtor(arena, AssetManager); + s_Instance->Arena = arena; s_Instance->Device = device; s_Instance->CurrentWorkingSpacePath = working_space_path; - s_Instance->NodeHierarchies.init(&s_Instance->Arena, 5000); - s_Instance->Meshes.init(&s_Instance->Arena, 5000); - s_Instance->Materials.init(&s_Instance->Arena, 5000); - s_Instance->GPUMeshMaterials.init(&s_Instance->Arena, 5000); - 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); + s_Instance->NodeHierarchies.init(s_Instance->Arena, 5000); + s_Instance->Meshes.init(s_Instance->Arena, 5000); + s_Instance->Materials.init(s_Instance->Arena, 5000); + s_Instance->GPUMeshMaterials.init(s_Instance->Arena, 5000); + 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); + s_registry.Initialize(s_Instance->Arena); s_Instance->Registry = &s_registry; } @@ -99,9 +99,9 @@ namespace ZEngine::Managers auto mesh_slot = static_cast(s_Instance->Meshes.size()); auto& m = s_Instance->Meshes.push_use({}); m.MeshUUID = mesh.MeshUUID; - m.SubMeshes.init(&s_Instance->Arena, mesh.SubMeshes.size()); - m.Vertices.init(&s_Instance->Arena, mesh.Vertices.size(), mesh.Vertices.size()); - m.Indices.init(&s_Instance->Arena, mesh.Indices.size(), mesh.Indices.size()); + m.SubMeshes.init(s_Instance->Arena, mesh.SubMeshes.size()); + m.Vertices.init(s_Instance->Arena, mesh.Vertices.size(), mesh.Vertices.size()); + m.Indices.init(s_Instance->Arena, mesh.Indices.size(), mesh.Indices.size()); Helpers::secure_memcpy(m.Vertices.data(), m.Vertices.size() * sizeof(float), mesh.Vertices.data(), mesh.Vertices.size() * sizeof(float)); Helpers::secure_memcpy(m.Indices.data(), m.Indices.size() * sizeof(uint32_t), mesh.Indices.data(), mesh.Indices.size() * sizeof(uint32_t)); for (auto& sub : mesh.SubMeshes) @@ -113,14 +113,14 @@ namespace ZEngine::Managers h.NodeHierarchyUUID = hierarchy.NodeHierarchyUUID; h.MeshUUID = hierarchy.MeshUUID; - h.Hierarchies.init(&s_Instance->Arena, hierarchy.Hierarchies.size(), hierarchy.Hierarchies.size()); - h.LocalTransforms.init(&s_Instance->Arena, hierarchy.LocalTransforms.size(), hierarchy.LocalTransforms.size()); - h.GlobalTransforms.init(&s_Instance->Arena, hierarchy.GlobalTransforms.size(), hierarchy.GlobalTransforms.size()); - h.Names.init(&s_Instance->Arena, hierarchy.Names.size()); - h.MaterialNames.init(&s_Instance->Arena, hierarchy.MaterialNames.size()); - h.NodeNames.init(&s_Instance->Arena, hierarchy.NodeNames.size() > 32 ? hierarchy.NodeNames.size() * 2 : 64); - 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); + h.Hierarchies.init(s_Instance->Arena, hierarchy.Hierarchies.size(), hierarchy.Hierarchies.size()); + h.LocalTransforms.init(s_Instance->Arena, hierarchy.LocalTransforms.size(), hierarchy.LocalTransforms.size()); + h.GlobalTransforms.init(s_Instance->Arena, hierarchy.GlobalTransforms.size(), hierarchy.GlobalTransforms.size()); + h.Names.init(s_Instance->Arena, hierarchy.Names.size()); + h.MaterialNames.init(s_Instance->Arena, hierarchy.MaterialNames.size()); + h.NodeNames.init(s_Instance->Arena, hierarchy.NodeNames.size() > 32 ? hierarchy.NodeNames.size() * 2 : 64); + 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(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)); @@ -129,12 +129,12 @@ namespace ZEngine::Managers for (auto& name : hierarchy.Names) { auto& n = h.Names.push_use({}); - n.init(&s_Instance->Arena, name.c_str()); + n.init(s_Instance->Arena, name.c_str()); } for (auto& mat_name : hierarchy.MaterialNames) { auto& n = h.MaterialNames.push_use({}); - n.init(&s_Instance->Arena, mat_name.c_str()); + n.init(s_Instance->Arena, mat_name.c_str()); } for (const auto& [k, v] : hierarchy.NodeNames) h.NodeNames.insert(k, v); @@ -169,7 +169,7 @@ namespace ZEngine::Managers auto slot = static_cast(s_Instance->Textures.size()); auto& new_tex = s_Instance->Textures.push_use({}); new_tex.TextureUUID = uuid; - new_tex.Path.init(&s_Instance->Arena, path.c_str()); + new_tex.Path.init(s_Instance->Arena, path.c_str()); if (!new_tex.Path.empty() && s_Instance->Device && s_Instance->Device->RRM) { diff --git a/ZEngine/ZEngine/Managers/AssetManager.h b/ZEngine/ZEngine/Managers/AssetManager.h index a634eefe9..ba568ff08 100644 --- a/ZEngine/ZEngine/Managers/AssetManager.h +++ b/ZEngine/ZEngine/Managers/AssetManager.h @@ -17,7 +17,7 @@ namespace ZEngine::Managers { struct AssetManager { - Core::Memory::ArenaAllocator Arena = {}; + Core::Memory::ArenaAllocator* Arena = nullptr; cstring CurrentWorkingSpacePath = ""; // CPU-side import buffers — owned by the import pipeline. diff --git a/ZEngine/ZEngine/Rendering/Buffers/FrameBuffer.cpp b/ZEngine/ZEngine/Rendering/Buffers/FrameBuffer.cpp index d1880c062..efa0a00d0 100644 --- a/ZEngine/ZEngine/Rendering/Buffers/FrameBuffer.cpp +++ b/ZEngine/ZEngine/Rendering/Buffers/FrameBuffer.cpp @@ -11,6 +11,8 @@ namespace ZEngine::Rendering::Buffers Create(); } + FramebufferVNext::FramebufferVNext(Hardwares::VulkanDevice* device) : m_device(device) {} + FramebufferVNext::~FramebufferVNext() { Dispose(); @@ -66,6 +68,13 @@ namespace ZEngine::Rendering::Buffers Create(); } + void FramebufferVNext::Reset(VkFramebuffer handle, uint32_t width, uint32_t height) + { + Handle = handle; + m_specification.Width = width; + m_specification.Height = height; + } + void FramebufferVNext::Dispose() { if (Handle) diff --git a/ZEngine/ZEngine/Rendering/Buffers/Framebuffer.h b/ZEngine/ZEngine/Rendering/Buffers/Framebuffer.h index 88900029a..c3bb398b3 100644 --- a/ZEngine/ZEngine/Rendering/Buffers/Framebuffer.h +++ b/ZEngine/ZEngine/Rendering/Buffers/Framebuffer.h @@ -11,6 +11,7 @@ namespace ZEngine::Rendering::Buffers struct FramebufferVNext { FramebufferVNext(Hardwares::VulkanDevice* device, Specifications::FrameBufferSpecificationVNext&&); + explicit FramebufferVNext(Hardwares::VulkanDevice* device); ~FramebufferVNext(); VkFramebuffer Handle{VK_NULL_HANDLE}; @@ -18,6 +19,7 @@ namespace ZEngine::Rendering::Buffers void Create(); void Resize(uint32_t width = 1, uint32_t height = 1); void Dispose(); + void Reset(VkFramebuffer handle, uint32_t width, uint32_t height); uint32_t GetWidth() const; uint32_t GetHeight() const; Specifications::FrameBufferSpecificationVNext& GetSpecification(); diff --git a/ZEngine/ZEngine/Rendering/Renderers/Contracts/RendererDataContract.h b/ZEngine/ZEngine/Rendering/Renderers/Contracts/RendererDataContract.h index eb49245ba..4da24cb02 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/Contracts/RendererDataContract.h +++ b/ZEngine/ZEngine/Rendering/Renderers/Contracts/RendererDataContract.h @@ -5,9 +5,10 @@ namespace ZEngine::Rendering::Renderers::Contracts { struct UBOCameraLayout { - alignas(16) ZEngine::Core::Maths::Mat4f View = ZEngine::Core::Maths::Identity(); - alignas(16) ZEngine::Core::Maths::Mat4f Projection = ZEngine::Core::Maths::Identity(); - alignas(16) ZEngine::Core::Maths::Vec4f Position = ZEngine::Core::Maths::Vec4f(0.0f, 0.0f, 0.0f, 1.0f); + alignas(16) ZEngine::Core::Maths::Mat4f View = ZEngine::Core::Maths::Identity(); + alignas(16) ZEngine::Core::Maths::Mat4f Projection = ZEngine::Core::Maths::Identity(); + alignas(16) ZEngine::Core::Maths::Vec4f Position = ZEngine::Core::Maths::Vec4f(0.0f, 0.0f, 0.0f, 1.0f); + alignas(16) ZEngine::Core::Maths::Mat4f InvViewProj = ZEngine::Core::Maths::Identity(); }; struct UBOModelLayout diff --git a/ZEngine/ZEngine/Rendering/Renderers/GraphicRenderer.cpp b/ZEngine/ZEngine/Rendering/Renderers/GraphicRenderer.cpp index 9744ed01f..f45cbd50a 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/GraphicRenderer.cpp +++ b/ZEngine/ZEngine/Rendering/Renderers/GraphicRenderer.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -31,55 +32,42 @@ namespace ZEngine::Rendering::Renderers RenderSceneData->TransformBuffer = Device->GpuMem.AllocateBuffer(DefaultBufferSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, Core::Memory::GpuMemoryDomain::HostUniform, TransformBufferName); RenderSceneData->RenderDataBuffer = Device->GpuMem.AllocateBuffer(DefaultBufferSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, Core::Memory::GpuMemoryDomain::HostUniform, RenderDataBufferName); RenderSceneData->MaterialBuffer = Device->GpuMem.AllocateBuffer(DefaultBufferSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, Core::Memory::GpuMemoryDomain::HostUniform, MaterialBufferName); + RenderSceneData->LightBuffer = Device->GpuMem.AllocateBuffer(sizeof(Scenes::LightArrayUBO), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, Core::Memory::GpuMemoryDomain::HostUniform, LightBufferName); /* * Renderer Passes */ - 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); - - uint32_t rt_w = Device->SwapchainPtr->SwapchainImageWidth; - uint32_t rt_h = Device->SwapchainPtr->SwapchainImageHeight; - FrameColorRenderTarget = Device->CreateTexture({.PerformTransition = false, .Width = rt_w, .Height = rt_h, .Format = ImageFormat::R8G8B8A8_UNORM}); - FrameDepthRenderTarget = Device->CreateTexture({.PerformTransition = false, .Width = rt_w, .Height = rt_h, .Format = ImageFormat::DEPTH_STENCIL_FROM_DEVICE}); - - Device->TextureHandleToUpdates.Enqueue(FrameColorRenderTarget); - /* - * Render Graph definition - */ - RenderGraph->Initialize(Device, RenderSceneData); + auto scene_depth_prepass = ZPushStructCtor(Device->Arena, DepthPrePass); + auto gbuffer_pass = ZPushStructCtor(Device->Arena, GbufferPass); + auto lighting_pass = ZPushStructCtor(Device->Arena, LightingPass); + auto skybox_pass = ZPushStructCtor(Device->Arena, SkyboxPass); + auto grid_pass = ZPushStructCtor(Device->Arena, GridPass); - RenderGraph->ResourceBuilder->AttachRenderTarget(RendererResourceName::FrameDepthRenderTargetName, FrameDepthRenderTarget); - RenderGraph->ResourceBuilder->AttachRenderTarget(RendererResourceName::FrameColorRenderTargetName, FrameColorRenderTarget); + RenderGraph->Initialize(Device, RenderSceneData); 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); + RenderGraph->AddCallbackPass("Lighting Pass", lighting_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); RenderGraph->Setup(); RenderGraph->Compile(); + + // Register FrameColor for bindless access now that the graph has allocated it. + Device->TextureHandleToUpdates.Enqueue(RenderGraph->ResourceInspector->GetRenderTarget(RendererResourceName::FrameColorRenderTargetName)); } void GraphicRenderer::Deinitialize() { RenderGraph->Dispose(); - // Queue render targets for proper GPU memory release — TextureHandleToDispose - // is drained in VulkanDevice::Deinitialize which runs after this call. - Device->TextureHandleToDispose.Enqueue(FrameColorRenderTarget); - Device->TextureHandleToDispose.Enqueue(FrameDepthRenderTarget); if (RenderSceneData) { Device->GpuMem.FreeBuffer(RenderSceneData->TransformBuffer); Device->GpuMem.FreeBuffer(RenderSceneData->RenderDataBuffer); Device->GpuMem.FreeBuffer(RenderSceneData->MaterialBuffer); + Device->GpuMem.FreeBuffer(RenderSceneData->LightBuffer); } } @@ -92,21 +80,24 @@ namespace ZEngine::Rendering::Renderers // data written each frame via vmaCopyMemoryToAllocation. if (!m_static_buffers_bound && scene->TransformBuffer.Handle) { - auto& depth_node = RenderGraph->NodeMap["Depth Pre-Pass"]; - auto& gbuffer_node = RenderGraph->NodeMap["G-Buffer Pass"]; - if (depth_node.Handle) + auto* depth_pass = RenderGraph->GetPass("Depth Pre-Pass"); + auto* gbuffer_pass = RenderGraph->GetPass("G-Buffer Pass"); + auto* light_pass = RenderGraph->GetPass("Lighting Pass"); + if (depth_pass && depth_pass->Handle) { - depth_node.Handle->SetStorageBuffer("TransformSB", &scene->TransformBuffer); - depth_node.Handle->SetStorageBuffer("DrawDataSB", &scene->RenderDataBuffer); + depth_pass->Handle->SetStorageBuffer("TransformSB", &scene->TransformBuffer); + depth_pass->Handle->SetStorageBuffer("DrawDataSB", &scene->RenderDataBuffer); } - if (gbuffer_node.Handle) + if (gbuffer_pass && gbuffer_pass->Handle) { - gbuffer_node.Handle->SetStorageBuffer("TransformSB", &scene->TransformBuffer); - gbuffer_node.Handle->SetStorageBuffer("DrawDataSB", &scene->RenderDataBuffer); - gbuffer_node.Handle->SetStorageBuffer("MatSB", &scene->MaterialBuffer); + gbuffer_pass->Handle->SetStorageBuffer("TransformSB", &scene->TransformBuffer); + gbuffer_pass->Handle->SetStorageBuffer("DrawDataSB", &scene->RenderDataBuffer); + gbuffer_pass->Handle->SetStorageBuffer("MatSB", &scene->MaterialBuffer); } + if (light_pass && light_pass->Handle && scene->LightBuffer.Handle) + light_pass->Handle->SetStorageBuffer("LightSB", &scene->LightBuffer); m_static_buffers_bound = true; - ZENGINE_CORE_INFO("[GraphicRenderer] Bound TransformSB/DrawDataSB/MatSB to geometry passes") + ZENGINE_CORE_INFO("[GraphicRenderer] Bound TransformSB/DrawDataSB/MatSB/LightSB to passes") } if (!Device->RRM) @@ -119,18 +110,18 @@ namespace ZEngine::Rendering::Renderers { 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) + auto* depth_pass = RenderGraph->GetPass("Depth Pre-Pass"); + auto* gbuffer_pass = RenderGraph->GetPass("G-Buffer Pass"); + if (depth_pass && depth_pass->Handle) { - depth_node.Handle->SetStorageBuffer("VertexSB", vtx_buf); - depth_node.Handle->SetStorageBuffer("IndexSB", idx_buf); + depth_pass->Handle->SetStorageBuffer("VertexSB", vtx_buf); + depth_pass->Handle->SetStorageBuffer("IndexSB", idx_buf); } - if (gbuffer_node.Handle) + if (gbuffer_pass && gbuffer_pass->Handle) { - gbuffer_node.Handle->SetStorageBuffer("VertexSB", vtx_buf); - gbuffer_node.Handle->SetStorageBuffer("IndexSB", idx_buf); - gbuffer_node.Handle->UseTextureArray("TextureArray"); + gbuffer_pass->Handle->SetStorageBuffer("VertexSB", vtx_buf); + gbuffer_pass->Handle->SetStorageBuffer("IndexSB", idx_buf); + gbuffer_pass->Handle->UseTextureArray("TextureArray"); } m_global_buffers_bound = true; ZENGINE_CORE_INFO("[GraphicRenderer] Bound global VertexSB/IndexSB to geometry passes") @@ -140,7 +131,8 @@ namespace ZEngine::Rendering::Renderers void GraphicRenderer::DrawScene(uint8_t frame_index, uint8_t thread_index, Hardwares::CommandBufferPtr const cb, Cameras::CameraPtr const camera) { auto asset_manager = Managers::AssetManager::Instance(); - auto ubo_camera_data = UBOCameraLayout{.View = camera->GetView(), .Projection = camera->GetProjection(), .Position = Vec4f(camera->GetPosition(), 1.0f)}; + auto view_proj = camera->GetProjection() * camera->GetView(); + auto ubo_camera_data = UBOCameraLayout{.View = camera->GetView(), .Projection = camera->GetProjection(), .Position = Vec4f(camera->GetPosition(), 1.0f), .InvViewProj = view_proj.Inverse()}; if (Device->RRM && RenderSceneData->MaterialBuffer.Handle) { @@ -148,6 +140,22 @@ namespace ZEngine::Rendering::Renderers rrm->UpdateBuffer(RenderSceneData->MaterialBuffer, asset_manager->GPUMeshMaterials.data(), asset_manager->GPUMeshMaterials.size() * sizeof(asset_manager->GPUMeshMaterials[0])); } + if (Device->RRM && RenderSceneData->LightBuffer.Handle) + { + auto* rrm = reinterpret_cast(Device->RRM); + Scenes::LightArrayUBO lights = {}; + lights.DirectionalLights[0].Direction.x = 0.5f; + lights.DirectionalLights[0].Direction.y = -1.0f; + lights.DirectionalLights[0].Direction.z = 0.5f; + lights.DirectionalLights[0].Color.x = 1.0f; + lights.DirectionalLights[0].Color.y = 1.0f; + lights.DirectionalLights[0].Color.z = 1.0f; + lights.DirectionalLights[0].Color.w = 1.0f; + lights.DirectionalLights[0].Intensity = 3.0f; + lights.DirectionalCount = 1; + rrm->UpdateBuffer(RenderSceneData->LightBuffer, &lights, sizeof(lights)); + } + // Push camera data into the per-frame heap; store offset for dynamic descriptor binding auto& heap = Device->FrameHeaps[Device->SwapchainPtr->CurrentFrame->Index]; auto camera_alloc = heap.Push(&ubo_camera_data, sizeof(UBOCameraLayout), Device->MinUniformBufferOffsetAlignment()); @@ -163,18 +171,16 @@ namespace ZEngine::Rendering::Renderers void GraphicRenderer::ApplySkyConfig(const Scenes::SkyConfig& sky) { - auto& node = RenderGraph->NodeMap["Skybox Pass"]; - if (!sky.IsHDRI()) { - node.Enabled = false; + RenderGraph->SetPassEnabled("Skybox Pass", false); return; } auto env_path = sky.EnvironmentMap.c_str(); if (!env_path || env_path[0] == '\0') { - node.Enabled = false; + RenderGraph->SetPassEnabled("Skybox Pass", false); return; } @@ -182,7 +188,7 @@ namespace ZEngine::Rendering::Renderers if (!vfs) { ZENGINE_CORE_ERROR("[Renderer] VFS not available — cannot resolve environment map: {}", env_path) - node.Enabled = false; + RenderGraph->SetPassEnabled("Skybox Pass", false); return; } @@ -191,23 +197,28 @@ namespace ZEngine::Rendering::Renderers if (exists_result.Failed() || !exists_result.Value()) { ZENGINE_CORE_ERROR("[Renderer] Environment map not found in VFS: {}", env_path) - node.Enabled = false; + RenderGraph->SetPassEnabled("Skybox Pass", false); return; } - auto* skybox_pass = static_cast(node.CallbackPass); - skybox_pass->EnvMapPath = env_path; - node.Enabled = true; + auto* pass = RenderGraph->GetPass("Skybox Pass"); + if (pass) + { + static_cast(pass->Callback)->EnvMapPath = env_path; + pass->Enabled = true; + } } void GraphicRenderer::ApplyGridConfig(const Scenes::GridConfig& cfg) { - auto& node = RenderGraph->NodeMap["Grid Pass"]; - node.Enabled = cfg.Enabled; + auto* pass = RenderGraph->GetPass("Grid Pass"); + if (!pass) + return; + pass->Enabled = cfg.Enabled; if (!cfg.Enabled) return; - auto& p = static_cast(node.CallbackPass)->PushData; + auto& p = static_cast(pass->Callback)->PushData; p.CellSize = cfg.CellSize; p.FadeRadius = cfg.FadeRadius; p.FadeStrength = cfg.FadeStrength; diff --git a/ZEngine/ZEngine/Rendering/Renderers/GraphicRenderer.h b/ZEngine/ZEngine/Rendering/Renderers/GraphicRenderer.h index 057a1148f..747d4cc4f 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/GraphicRenderer.h +++ b/ZEngine/ZEngine/Rendering/Renderers/GraphicRenderer.h @@ -10,15 +10,12 @@ namespace ZEngine::Rendering::Renderers GraphicRenderer(); ~GraphicRenderer(); - cstring VertexBufferName = "VertexStorageBuffer"; - cstring IndexBufferName = "IndexStorageBuffer"; - cstring TransformBufferName = "TransformStorageBuffer"; - cstring RenderDataBufferName = "RenderDataStorageBuffer"; - cstring MaterialBufferName = "MaterialStorageBuffer"; - - Textures::TextureHandle FrameSharedRenderTarget = {}; - Textures::TextureHandle FrameColorRenderTarget = {}; - Textures::TextureHandle FrameDepthRenderTarget = {}; + cstring VertexBufferName = "VertexStorageBuffer"; + cstring IndexBufferName = "IndexStorageBuffer"; + cstring TransformBufferName = "TransformStorageBuffer"; + cstring RenderDataBufferName = "RenderDataStorageBuffer"; + cstring MaterialBufferName = "MaterialStorageBuffer"; + cstring LightBufferName = "LightStorageBuffer"; void Initialize(Hardwares::VulkanDevicePtr device) override; void Deinitialize() override; diff --git a/ZEngine/ZEngine/Rendering/Renderers/IRenderer.h b/ZEngine/ZEngine/Rendering/Renderers/IRenderer.h index ead62d7f5..b59ef7c6e 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/IRenderer.h +++ b/ZEngine/ZEngine/Rendering/Renderers/IRenderer.h @@ -53,6 +53,10 @@ namespace ZEngine::Rendering::Renderers inline static cstring FrameSharedRenderTargetName = "g_frame_shared_render_target"; inline static cstring FrameColorRenderTargetName = "g_frame_color_render_target"; + inline static cstring GBufferAlbedoAOName = "g_gbuffer_albedo_ao"; + inline static cstring GBufferNormalRoughnessName = "g_gbuffer_normal_roughness"; + inline static cstring GBufferMetallicEmissiveName = "g_gbuffer_metallic_emissive"; + inline static cstring SceneCameraBufferName = "SceneCamera"; }; diff --git a/ZEngine/ZEngine/Rendering/Renderers/RenderGraph.cpp b/ZEngine/ZEngine/Rendering/Renderers/RenderGraph.cpp index eb4cb7304..7c1cccf08 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/RenderGraph.cpp +++ b/ZEngine/ZEngine/Rendering/Renderers/RenderGraph.cpp @@ -1,545 +1,756 @@ -#include -#include #include #include -#include using namespace ZEngine::Core::Containers; using namespace ZEngine::Helpers; namespace ZEngine::Rendering::Renderers { + // kAccessTable — stage + access + layout for every RGAccess value. + static constexpr struct + { + VkPipelineStageFlags Stage; + VkAccessFlags Access; + VkImageLayout Layout; + } kAccessTable[static_cast(RGAccess::Count_)] = { + // None + { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED}, + // ColorWrite + { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}, + // DepthWrite + {VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}, + // DepthRead — stays in DEPTH_STENCIL_ATTACHMENT_OPTIMAL; depthWrite=false in pipeline + { VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}, + // ShaderRead + { VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}, + // ShaderReadWrite + { VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_IMAGE_LAYOUT_GENERAL}, + // TransferRead + { VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL}, + // TransferWrite + { VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL}, + // Present + { VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR}, + }; + + static VkImage GetVkImage(Hardwares::VulkanDevice* device, Textures::TextureHandle handle) + { + if (!handle.Valid()) + return VK_NULL_HANDLE; + auto* tex = device->GlobalTextures.Access(handle); + if (!tex) + return VK_NULL_HANDLE; + auto* img_buf = device->Image2DBufferManager.Access(tex->BufferHandle); + if (!img_buf) + return VK_NULL_HANDLE; + return img_buf->GetBuffer().Handle; + } + + static VkImageSubresourceRange FullSubresourceRange(const RGResource& res, Hardwares::VulkanDevice* device) + { + bool depth = (res.Spec.Format == Rendering::Specifications::ImageFormat::DEPTH_STENCIL_FROM_DEVICE); + // For imported (external) resources the spec may be empty; check the actual texture. + if (!depth && res.TextureHandle.Valid()) + { + auto* tex = device->GlobalTextures.Access(res.TextureHandle); + if (tex && tex->IsDepthTexture) + depth = true; + } + VkImageSubresourceRange r = {}; + r.aspectMask = depth ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT; + r.baseMipLevel = 0; + r.levelCount = VK_REMAINING_MIP_LEVELS; + r.baseArrayLayer = 0; + r.layerCount = VK_REMAINING_ARRAY_LAYERS; + return r; + } + + void RGTransientPool::Initialize(Core::Memory::ArenaAllocator* arena) + { + Slots.init(arena, 32); + } + + Textures::TextureHandle RGTransientPool::TryAlias(const Specifications::TextureSpecification& spec, uint32_t first_pass) + { + for (auto& slot : Slots) + { + if (slot.FreeAfterPass >= first_pass) + continue; + const auto& s = slot.Spec; + if (s.Format != spec.Format || s.Width != spec.Width || s.Height != spec.Height || s.LayerCount != spec.LayerCount) + continue; + return slot.Handle; + } + return {}; + } + + void RGTransientPool::Register(Textures::TextureHandle handle, const Specifications::TextureSpecification& spec, uint32_t last_pass) + { + auto& slot = Slots.push_use({}); + slot.Handle = handle; + slot.Spec = spec; + slot.FreeAfterPass = last_pass; + } + + void RGTransientPool::MarkInUse(Textures::TextureHandle handle, uint32_t last_pass) + { + for (auto& slot : Slots) + { + if (slot.Handle.Index == handle.Index && slot.Handle.Generation == handle.Generation) + { + slot.FreeAfterPass = last_pass; + return; + } + } + } + + void RGTransientPool::Clear() + { + Slots.clear(); + } + void RenderGraph::Initialize(Hardwares::VulkanDevicePtr device, Scenes::SceneDataPtr data) { - Device = device; - SceneData = data; + Device = device; + SceneData = data; + + Passes.init(Device->Arena, 16); + Resources.init(Device->Arena, 32); + SortedPassIndices.init(Device->Arena, 16); + ResourceIndex.init(Device->Arena, 64); + PassIndex.init(Device->Arena, 32); + TransientPool.Initialize(Device->Arena); + ResourceBuilder = ZPushStruct(Device->Arena, RenderGraphResourceBuilder); ResourceInspector = ZPushStruct(Device->Arena, RenderGraphResourceInspector); RenderPassBuilder = ZPushStructCtorArgs(Device->Arena, RenderPasses::RenderPassBuilder); - SortedNodesMap.init(Device->Arena, 16); - NodeMap.init(Device->Arena); - ResourceMap.init(Device->Arena); RenderPassBuilder->Initialize(Device->Arena); - ResourceBuilder->Initialize(this); ResourceInspector->Initialize(this); } - void RenderGraph::AddCallbackPass(cstring pass_name, IRenderGraphCallbackPass* const pass_callback, bool enabled) + void RenderGraph::AddCallbackPass(cstring pass_name, IRenderGraphCallbackPass* const cb, bool enabled) { - NodeMap[pass_name].Enabled = enabled; - NodeMap[pass_name].CallbackPass = pass_callback; + uint32_t idx = static_cast(Passes.size()); + auto& p = Passes.push_use({}); + p.Name = pass_name; + p.Enabled = enabled; + p.Callback = cb; + p.Reads.init(Device->Arena, 8); + p.Writes.init(Device->Arena, 8); + p.ImageBarriers.init(Device->Arena, 8); + + PassIndex[pass_name] = idx; } void RenderGraph::Setup() { - for (auto [name, _] : NodeMap) // Todo UnorderedHashMap needs to support for (auto& [key, val]) {....} + for (uint32_t i = 0; i < Passes.size(); ++i) { - NodeMap[name].EdgeNodes.init(Device->Arena); - NodeMap[name].CallbackPass->Setup(Device, name, ResourceBuilder, ResourceInspector); + ResourceBuilder->CurrentPass = i; + Passes[i].Callback->Setup(Device, Passes[i].Name, ResourceBuilder, ResourceInspector); } + ResourceBuilder->CurrentPass = UINT32_MAX; } void RenderGraph::Compile() { - for (auto pass : NodeMap) + BuildLifetimes(); + AllocateTransientResources(); + BuildBarriers(); + BuildTopology(); + + for (uint32_t i = 0; i < SortedPassIndices.size(); ++i) { - for (uint32_t i = 0; i < pass.second.Creation.Inputs.size(); ++i) + uint32_t pi = SortedPassIndices[i]; + RGPass& pass = Passes[pi]; + if (!pass.Enabled || !pass.Callback) + continue; + + // Pre-populate the builder with resolved resource handles so the pass's + // Compile() can call SetPipelineName()...Detach() and get a spec that + // already has the correct input attachments and render targets. + // Only DepthRead reads become VkRenderPass input attachments. + // ShaderRead reads are bound via SetTexture() in the pass's Compile(). + for (const auto& r : pass.Reads) { - if (ResourceMap.contains(pass.second.Creation.Inputs[i].Name)) - { - RenderGraphResource& resource = ResourceMap[pass.second.Creation.Inputs[i].Name]; - if (NodeMap.contains(resource.ProducerNodeName)) - { - RenderGraphNode& producer_node = NodeMap[resource.ProducerNodeName]; - producer_node.EdgeNodes.insert(pass.first); - } - } + if (!r.Handle.Valid()) + continue; + if (r.Access != RGAccess::DepthRead && r.Access != RGAccess::DepthWrite) + continue; + const auto& res = Resources[r.Handle.Index]; + if (res.TextureHandle.Valid()) + RenderPassBuilder->AddInputAttachment(res.TextureHandle); + } + for (const auto& w : pass.Writes) + { + if (!w.Handle.Valid()) + continue; + const auto& res = Resources[w.Handle.Index]; + if (res.TextureHandle.Valid()) + RenderPassBuilder->UseRenderTarget(res.TextureHandle); } - } - // ToDo: Potentially remove Node that have no Edges from the graph...? + pass.Callback->Compile(Device, SceneData, RenderPassBuilder, ResourceInspector, &pass.Handle); + } - /* - * Topological Sorting - */ - auto scratch = ZGetScratch(Device->Arena); + AllocateFramebuffers(); + } - Array sorted_nodes = {}; - UnorderedHashSet processed_nodes = {}; - UnorderedHashSet visited_nodes = {}; - UnorderedHashSet in_stack_nodes = {}; - std::stack stack = {}; + void RenderGraph::Execute(Hardwares::CommandBufferPtr const cb) + { + cb->ClearColor(0.11f, 0.11f, 0.11f, 1.0f); + cb->ClearDepth(1.0f, 0); - sorted_nodes.init(scratch.Arena, NodeMap.size()); - visited_nodes.init(scratch.Arena); - processed_nodes.init(scratch.Arena); - in_stack_nodes.init(scratch.Arena); + auto scratch = ZGetScratch(Device->Arena); - for (const auto& [name, _] : NodeMap) + for (uint32_t i = 0; i < SortedPassIndices.size(); ++i) { - if (processed_nodes.contains(name)) - { + RGPass& pass = Passes[SortedPassIndices[i]]; + if (!pass.Enabled) continue; - } - stack.push(name); + // Build barriers for this pass fresh each frame so layout transitions + // correctly track the actual per-frame resource states. + Core::Containers::Array barriers; + barriers.init(scratch.Arena, 8); + VkPipelineStageFlags src_stage = 0; + VkPipelineStageFlags dst_stage = 0; + + auto emit_barrier = [&](const RGPassResource& pr) { + if (!pr.Handle.Valid() || pr.Access == RGAccess::None) + return; + RGResource& res = Resources[pr.Handle.Index]; + const auto& dst = kAccessTable[static_cast(pr.Access)]; + if (res.RuntimeState.Layout == dst.Layout && res.RuntimeState.Access == dst.Access) + return; + + VkImageMemoryBarrier b = {}; + b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + b.oldLayout = res.RuntimeState.Layout; + b.newLayout = dst.Layout; + b.srcAccessMask = res.RuntimeState.Access; + b.dstAccessMask = dst.Access; + b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.image = GetVkImage(Device, res.TextureHandle); + b.subresourceRange = FullSubresourceRange(res, Device); + if (b.image == VK_NULL_HANDLE) + return; + + barriers.push(b); + src_stage |= res.RuntimeState.Stage; + dst_stage |= dst.Stage; + res.RuntimeState = {dst.Stage, dst.Access, dst.Layout}; + }; - while (!stack.empty()) - { - // Copy, not reference: pushing to the stack can invalidate a top() ref. - const auto top = stack.top(); + for (const auto& w : pass.Writes) + emit_barrier(w); + for (const auto& r : pass.Reads) + emit_barrier(r); - if (!visited_nodes.contains(top)) - { - visited_nodes.insert(top); - in_stack_nodes.insert(top); - // Re-push self below its children so it is emitted only after all - // descendants are processed (post-order). - stack.push(top); - - for (auto edge : NodeMap[top].EdgeNodes) - { - if (in_stack_nodes.contains(edge)) - { - ZENGINE_CORE_ERROR("RenderGraph: cycle detected between '{}' and '{}'", top, edge) - continue; - } - if (!visited_nodes.contains(edge)) - { - stack.push(edge); - } - } - } - else - { - stack.pop(); - - if (!processed_nodes.contains(top)) - { - sorted_nodes.push(top); - processed_nodes.insert(top); - in_stack_nodes.remove(top); - } - } + if (!barriers.empty()) + { + vkCmdPipelineBarrier(cb->GetHandle(), src_stage ? src_stage : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, dst_stage ? dst_stage : VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, static_cast(barriers.size()), barriers.data()); } - } - auto begin = sorted_nodes.begin(); - auto end = std::prev(sorted_nodes.end()); - while (end >= begin) - { - SortedNodesMap.push(*end); - end = std::prev(end); + if (!pass.Framebuffer || !pass.Framebuffer->Handle) + continue; + pass.Callback->Execute(Device, ResourceInspector, SceneData, pass.Handle, pass.Framebuffer, cb); } ZReleaseScratch(scratch); + } - /* - * Reading sorting graph node in reverse order and Create resource and RenderPass Node - */ - for (cstring node_name : SortedNodesMap) + void RenderGraph::Resize(uint32_t width, uint32_t height) + { + // Phase 1 — collect old Vulkan handles that need to be freed. + // Do NOT call any vkDestroy* yet — new resources must be created first + // so the driver cannot recycle these handles for new allocations. + TransientPool.Clear(); + uint64_t timeline = Device->SwapchainPtr->RenderTimelineNextValue; + + // Stack-local scratch for old handles (max 16 passes, max 32 transients). + VkFramebuffer old_fbs[16] = {}; + uint32_t old_fb_count = 0; + Core::Memory::BufferImage old_imgs[32] = {}; + uint32_t old_img_count = 0; + + for (auto& pass : Passes) { - auto& node = NodeMap[node_name]; - - RenderPassBuilder->SetName(node.Creation.Name); - - for (auto& output : node.Creation.Outputs) + if (pass.Framebuffer && pass.Framebuffer->Handle) { - auto& resource = ResourceMap[output.Name]; + if (old_fb_count < 16) + old_fbs[old_fb_count++] = pass.Framebuffer->Handle; + pass.Framebuffer->Handle = VK_NULL_HANDLE; + } + } - if (resource.ResourceInfo.External) - { - RenderPassBuilder->UseRenderTarget(resource.ResourceInfo.TextureHandle); - continue; - } + // Swap the underlying Image2DBuffer in-place for each transient resource. + // TextureHandle and Image2DBufferManager slot are REUSED — no new slots, + // no slot exhaustion, handles stay stable so the editor's cached ImTextureID + // remains valid. Old VkImage/VkImageView data is saved for DeferFree. + for (auto& res : Resources) + { + if (res.External || !res.Transient) + continue; + res.Spec.Width = width; + res.Spec.Height = height; + res.CurrentState = {VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED}; + res.RuntimeState = {VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED}; + if (!res.TextureHandle.Valid()) + continue; + auto* tex = Device->GlobalTextures.Access(res.TextureHandle); + if (!tex) + continue; + auto* img = Device->Image2DBufferManager.Access(tex->BufferHandle); + if (!img) + continue; + // Save old VkImage/VkImageView for deferred destruction. + if (old_img_count < 32) + old_imgs[old_img_count++] = img->GetBuffer(); + // Reconstruct Image2DBuffer in the same slot with new dimensions. + // This creates new VkImage/VkImageView while old ones are still alive. + img->Specification.Width = width; + img->Specification.Height = height; + img->Construct(Device); + // Update Texture metadata. + tex->Width = width; + tex->Height = height; + tex->BufferSize = width * height * res.Spec.BytePerPixel * res.Spec.LayerCount; + } - if (output.Type == RenderGraphResourceType::ATTACHMENT) - { - resource.ResourceInfo.TextureSpec.PerformTransition = false; - resource.ResourceInfo.TextureHandle = Device->CreateTexture(resource.ResourceInfo.TextureSpec); + // Phase 2 — rebuild framebuffers and re-bind descriptors with new Image2DBuffers. + // All TextureHandles remain valid (in-place swap) so AllocateTransientResources + // is a no-op for existing resources; call it only for safety (skips valid handles). - RenderPassBuilder->UseRenderTarget(resource.ResourceInfo.TextureHandle); - } + if (const auto* idx = ResourceIndex.find(RendererResourceName::FrameColorRenderTargetName)) + Device->TextureHandleToUpdates.Enqueue(Resources[*idx].TextureHandle); - else if (output.Type == RenderGraphResourceType::REFERENCE) + // Sync pass Specification.Inputs and ExternalOutputs to the new handles so + // CommandBuffer::BeginRenderPass builds clear values from valid pointers. + for (auto& pass : Passes) + { + if (!pass.Handle) + continue; + uint32_t out_idx = 0; + for (const auto& w : pass.Writes) + { + if (w.Handle.Valid() && out_idx < pass.Handle->Specification.ExternalOutputs.size()) { - RenderPassBuilder->UseRenderTarget(resource.ResourceInfo.TextureHandle); + const auto& res = Resources[w.Handle.Index]; + if (res.TextureHandle.Valid()) + pass.Handle->Specification.ExternalOutputs[out_idx] = res.TextureHandle; } + ++out_idx; } - - for (auto& input : node.Creation.Inputs) + uint32_t in_idx = 0; + for (const auto& r : pass.Reads) { - auto& resource = ResourceMap[input.Name]; - - if (input.Type == RenderGraphResourceType::ATTACHMENT) - { - RenderPassBuilder->AddInputAttachment(resource.ResourceInfo.TextureHandle); - } - else if (input.Type == RenderGraphResourceType::TEXTURE) + if (!r.Handle.Valid()) + continue; + if (r.Access != RGAccess::DepthRead && r.Access != RGAccess::DepthWrite) + continue; + if (in_idx < pass.Handle->Specification.Inputs.size()) { - RenderPassBuilder->AddInputTexture(input.BindingInputKeyName, resource.ResourceInfo.TextureHandle); + const auto& res = Resources[r.Handle.Index]; + if (res.TextureHandle.Valid()) + pass.Handle->Specification.Inputs[in_idx] = res.TextureHandle; } + ++in_idx; } - - node.CallbackPass->Compile(Device, SceneData, RenderPassBuilder, ResourceInspector, &(node.Handle)); } - for (cstring name : SortedNodesMap) + AllocateFramebuffers(); + + for (auto& pass : Passes) { - auto& node = NodeMap[name]; - if (node.Handle && (node.Handle->Specification.Type != Specifications::RenderPassType::GRAPHIC)) - { + if (!pass.Handle) continue; + for (const auto& r : pass.Reads) + { + if (!r.Handle.Valid() || !r.BindingKey) + continue; + const auto& res = Resources[r.Handle.Index]; + if (res.TextureHandle.Valid()) + pass.Handle->SetTexture(r.BindingKey, res.TextureHandle); } + } - Specifications::FrameBufferSpecificationVNext framebuffer_spec = { - .Width = node.Handle->RenderAreaWidth, - .Height = node.Handle->RenderAreaHeight, - .RenderTargets = node.Handle->RenderTargets, - .Attachment = node.Handle->Attachment, - }; - node.Framebuffer = ZPushStructCtorArgs(Device->Arena, Buffers::FramebufferVNext, Device, std::move(framebuffer_spec)); + // Phase 3 — now that new resources are live, schedule old ones for GPU-safe deletion. + // Framebuffers must be enqueued before their referenced image views. + for (uint32_t i = 0; i < old_fb_count; ++i) + { + Hardwares::DeferredFreeEntry e; + e.EntryKind = Hardwares::DeferredFreeEntry::Kind::VkHandle; + e.TimelineValue = timeline; + e.Data.Vk = {reinterpret_cast(old_fbs[i]), Rendering::DeviceResourceType::FRAMEBUFFER, nullptr}; + Device->DeferFree(e); + } + for (uint32_t i = 0; i < old_img_count; ++i) + { + Hardwares::DeferredFreeEntry e; + e.EntryKind = Hardwares::DeferredFreeEntry::Kind::Image; + e.TimelineValue = timeline; + e.Data.Image = old_imgs[i]; + Device->DeferFree(e); + } + } + + void RenderGraph::Dispose() + { + for (auto& res : Resources) + { + if (res.External || !res.Transient || !res.TextureHandle.Valid()) + continue; + auto* tex = Device->GlobalTextures.Access(res.TextureHandle); + if (!tex) + continue; + auto* img = Device->Image2DBufferManager.Access(tex->BufferHandle); + if (img) + img->Dispose(); } + + for (auto& pass : Passes) + { + if (pass.Callback) + pass.Callback->Deinitialize(Device); + } + } + + RGResourceHandle RenderGraph::ImportRenderTarget(cstring name, Textures::TextureHandle handle) + { + if (auto* idx = ResourceIndex.find(name)) + { + Resources[*idx].TextureHandle = handle; + return {*idx, 0}; + } + uint32_t idx = static_cast(Resources.size()); + auto& res = Resources.push_use({}); + res.Name = name; + res.Kind = RGResourceKind::Attachment; + res.External = true; + res.Transient = false; + res.TextureHandle = handle; + ResourceIndex[name] = idx; + return {idx, 0}; } - void RenderGraph::Execute(Hardwares::CommandBufferPtr const command_buffer) + RGPass* RenderGraph::GetPass(cstring name) { - ZENGINE_VALIDATE_ASSERT(command_buffer, "Command Buffer can't be null") + if (auto* idx = PassIndex.find(name)) + return &Passes[*idx]; + return nullptr; + } - command_buffer->ClearColor(0.11f, 0.11f, 0.11f, 1.0f); // #1C1C1C dark carbon - command_buffer->ClearDepth(1.0f, 0); + void RenderGraph::SetPassEnabled(cstring name, bool enabled) + { + if (auto* idx = PassIndex.find(name)) + Passes[*idx].Enabled = enabled; + } - for (auto& node_name : SortedNodesMap) + void RenderGraph::BuildLifetimes() + { + for (auto& res : Resources) { - auto& node = NodeMap[node_name]; + res.FirstPassIndex = UINT32_MAX; + res.LastPassIndex = 0; + res.CurrentState = {VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED}; + res.RuntimeState = {VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED}; + } - if (!node.Enabled) + for (uint32_t i = 0; i < Passes.size(); ++i) + { + const auto& pass = Passes[i]; + for (const auto& w : pass.Writes) { - continue; + if (!w.Handle.Valid()) + continue; + auto& res = Resources[w.Handle.Index]; + if (i < res.FirstPassIndex) + res.FirstPassIndex = i; + if (i > res.LastPassIndex) + res.LastPassIndex = i; } - - for (auto& input : node.Creation.Inputs) + for (const auto& r : pass.Reads) { - if (input.Type == RenderGraphResourceType::TEXTURE) - { - auto& resource = ResourceMap[input.Name]; - /* - * The input texture can from an attachment that should read as Shader Sampler2D data - * So we need ensure the right config for transition - */ - bool is_resource_attachment = resource.Type == RenderGraphResourceType::ATTACHMENT; - - auto texture = Device->GlobalTextures.Access(resource.ResourceInfo.TextureHandle); - auto img_buf = Device->Image2DBufferManager.Access(texture->BufferHandle); - auto& buffer = img_buf->GetBuffer(); - - Specifications::ImageMemoryBarrierSpecification barrier_spec = {}; - barrier_spec.ImageHandle = buffer.Handle; - barrier_spec.OldLayout = is_resource_attachment ? Specifications::ImageLayout::COLOR_ATTACHMENT_OPTIMAL : Specifications::ImageLayout::UNDEFINED; - barrier_spec.NewLayout = Specifications::ImageLayout::SHADER_READ_ONLY_OPTIMAL; - barrier_spec.ImageAspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - barrier_spec.SourceAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - barrier_spec.DestinationAccessMask = VK_ACCESS_SHADER_READ_BIT; - barrier_spec.SourceStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - barrier_spec.DestinationStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; - barrier_spec.LayerCount = 1; - - Primitives::ImageMemoryBarrier barrier{barrier_spec}; - command_buffer->TransitionImageLayout(barrier); - img_buf->Layout = barrier_spec.NewLayout; - } + if (!r.Handle.Valid()) + continue; + auto& res = Resources[r.Handle.Index]; + if (i < res.FirstPassIndex) + res.FirstPassIndex = i; + if (i > res.LastPassIndex) + res.LastPassIndex = i; } + } + } - for (auto& output : node.Creation.Outputs) + void RenderGraph::AllocateTransientResources() + { + for (auto& res : Resources) + { + if (res.External || !res.Transient) + continue; + if (res.FirstPassIndex == UINT32_MAX) + continue; + if (res.TextureHandle.Valid()) + continue; + auto aliased = TransientPool.TryAlias(res.Spec, res.FirstPassIndex); + if (aliased.Valid()) { - if (output.Type == RenderGraphResourceType::REFERENCE) - { - continue; - } - - auto& resource = ResourceMap[output.Name]; - - if (resource.Type == RenderGraphResourceType::ATTACHMENT) + res.TextureHandle = aliased; + TransientPool.MarkInUse(aliased, res.LastPassIndex); + } + else + { + // Derive usage from access declarations and set appropriate spec flags. + for (const auto& pass : Passes) { - auto texture = Device->GlobalTextures.Access(resource.ResourceInfo.TextureHandle); - auto img_buf = Device->Image2DBufferManager.Access(texture->BufferHandle); - auto& buffer = img_buf->GetBuffer(); - - Specifications::ImageMemoryBarrierSpecification barrier_spec = {}; - if (texture->IsDepthTexture) - { - barrier_spec.ImageHandle = buffer.Handle; - barrier_spec.OldLayout = img_buf->Layout; - barrier_spec.NewLayout = Specifications::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL; - barrier_spec.ImageAspectMask = VkImageAspectFlagBits(VK_IMAGE_ASPECT_DEPTH_BIT /*| VK_IMAGE_ASPECT_STENCIL_BIT*/); // Todo : To consider Stencil - // buffer, we want to extend - // Texture spec to introduce - // HasStencil bit - barrier_spec.SourceAccessMask = 0; - barrier_spec.DestinationAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT; - barrier_spec.SourceStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; - barrier_spec.DestinationStageMask = VkPipelineStageFlagBits(VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT); - barrier_spec.LayerCount = 1; - - Primitives::ImageMemoryBarrier barrier{barrier_spec}; - command_buffer->TransitionImageLayout(barrier); - img_buf->Layout = barrier_spec.NewLayout; - } - else - { - barrier_spec.ImageHandle = buffer.Handle; - barrier_spec.OldLayout = img_buf->Layout; - barrier_spec.NewLayout = Specifications::ImageLayout::COLOR_ATTACHMENT_OPTIMAL; - barrier_spec.ImageAspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - barrier_spec.SourceAccessMask = 0; - barrier_spec.DestinationAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - barrier_spec.SourceStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; - barrier_spec.DestinationStageMask = VkPipelineStageFlagBits(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT); - barrier_spec.LayerCount = 1; - - Primitives::ImageMemoryBarrier barrier{barrier_spec}; - command_buffer->TransitionImageLayout(barrier); - img_buf->Layout = barrier_spec.NewLayout; - } + for (const auto& w : pass.Writes) + if (w.Handle.Valid() && w.Handle.Index == res.FirstPassIndex && w.Access == RGAccess::ShaderReadWrite) + res.Spec.IsUsageStorage = true; } + res.TextureHandle = Device->CreateTexture(res.Spec); + TransientPool.Register(res.TextureHandle, res.Spec, res.LastPassIndex); } - - node.CallbackPass->Execute(Device, ResourceInspector, SceneData, node.Handle, node.Framebuffer, command_buffer); } } - void RenderGraph::Resize(uint32_t width, uint32_t height) + void RenderGraph::BuildBarriers() { - for (auto& node_name : SortedNodesMap) + for (uint32_t i = 0; i < Passes.size(); ++i) { - auto& node = NodeMap[node_name]; - - auto& pass_spec = node.Handle->Specification; + RGPass& pass = Passes[i]; + pass.ImageBarriers.clear(); + pass.BarrierSrcStage = 0; + pass.BarrierDstStage = 0; + + auto emit = [&](const RGPassResource& pr) { + if (!pr.Handle.Valid() || pr.Access == RGAccess::None) + return; + RGResource& res = Resources[pr.Handle.Index]; + const auto& dst = kAccessTable[static_cast(pr.Access)]; + + if (res.CurrentState.Layout == dst.Layout && res.CurrentState.Access == dst.Access) + return; + + VkImageMemoryBarrier b = {}; + b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + // Use UNDEFINED only for transient resources on first use (discard previous alias contents). + // External/persistent resources must specify the actual old layout so the barrier + // declares the correct dependency across frames. + bool discard = (res.Transient && !res.External && res.FirstPassIndex == i); + b.oldLayout = discard ? VK_IMAGE_LAYOUT_UNDEFINED : res.CurrentState.Layout; + b.newLayout = dst.Layout; + b.srcAccessMask = res.CurrentState.Access; + b.dstAccessMask = dst.Access; + b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.image = GetVkImage(Device, res.TextureHandle); + b.subresourceRange = FullSubresourceRange(res, Device); + + if (b.image == VK_NULL_HANDLE) + return; + + pass.ImageBarriers.push(b); + pass.BarrierSrcStage |= res.CurrentState.Stage; + pass.BarrierDstStage |= dst.Stage; + res.CurrentState = {dst.Stage, dst.Access, dst.Layout}; + }; - if ((pass_spec.Type != Specifications::RenderPassType::GRAPHIC) && (pass_spec.Type != Specifications::RenderPassType::COMPUTE)) - { - continue; - } + for (const auto& w : pass.Writes) + emit(w); + for (const auto& r : pass.Reads) + emit(r); + } + } - pass_spec.ExternalOutputs.clear(); - pass_spec.Inputs.clear(); - pass_spec.InputTextures.clear(); + void RenderGraph::BuildTopology() + { + // Simple insertion-order execution for now — passes execute in the order they were added. + // A full DFS topological sort can replace this once producer/consumer edges are tracked. + SortedPassIndices.clear(); + for (uint32_t i = 0; i < Passes.size(); ++i) + SortedPassIndices.push(i); + } - for (auto& output : node.Creation.Outputs) - { - auto& resource = ResourceMap[output.Name]; + void RenderGraph::AllocateFramebuffers() + { + for (uint32_t i = 0; i < Passes.size(); ++i) + { + RGPass& pass = Passes[i]; + if (!pass.Handle) + continue; + if (pass.Handle->Specification.Type == Specifications::RenderPassType::COMPUTE) + continue; - if (output.Type == RenderGraphResourceType::REFERENCE) + // Stack-local view array avoids aliasing between the scratch arena and + // the Device->Arena allocations inside the same loop body. + VkImageView view_buf[16] = {}; + uint32_t view_count = 0; + uint32_t w = 0; + uint32_t h = 0; + + auto push_view = [&](Textures::TextureHandle handle) { + if (view_count >= 16) + return; + auto* tex = Device->GlobalTextures.Access(handle); + if (!tex) + return; + auto* img = Device->Image2DBufferManager.Access(tex->BufferHandle); + if (!img) + return; + VkImageView view = img->GetImageViewHandle(); + if (view == VK_NULL_HANDLE) + return; + view_buf[view_count++] = view; + if (w == 0) { - pass_spec.ExternalOutputs.push(resource.ResourceInfo.TextureHandle); - continue; + w = tex->Width; + h = tex->Height; } + }; - auto temp_handle = Device->GlobalTextures.Create(); - auto texture_to_dispose = Device->GlobalTextures.Access(resource.ResourceInfo.TextureHandle); - Device->GlobalTextures.Update(temp_handle, *texture_to_dispose); - Device->TextureHandleToDispose.Enqueue(temp_handle); - - // We invalidate ResourceInfo.TextureHandle, so it can be recycle for another texture allocation - Device->GlobalTextures.Remove(resource.ResourceInfo.TextureHandle); - resource.ResourceInfo.TextureSpec.Width = width; - resource.ResourceInfo.TextureSpec.Height = height; - resource.ResourceInfo.TextureHandle = Device->CreateTexture(resource.ResourceInfo.TextureSpec); - - if ((output.Name == RendererResourceName::FrameColorRenderTargetName) || (output.Name == RendererResourceName::FrameDepthRenderTargetName)) - { - Device->TextureHandleToUpdates.Enqueue(resource.ResourceInfo.TextureHandle); - } - pass_spec.ExternalOutputs.push(resource.ResourceInfo.TextureHandle); + for (const auto& r : pass.Reads) + { + if (!r.Handle.Valid()) + continue; + if (r.Access != RGAccess::DepthRead && r.Access != RGAccess::DepthWrite) + continue; + push_view(Resources[r.Handle.Index].TextureHandle); } - - for (auto& input : node.Creation.Inputs) + for (const auto& wr : pass.Writes) { - auto& resource = ResourceMap[input.Name]; - - if (resource.Type == RenderGraphResourceType::ATTACHMENT && input.Type == RenderGraphResourceType::ATTACHMENT) - { - pass_spec.Inputs.push(resource.ResourceInfo.TextureHandle); - } - /* - * The resource is an attachment from a RenderPass output, but the current node consumes it as Image for - * sampling operation - */ - else if (resource.Type == RenderGraphResourceType::ATTACHMENT && input.Type == RenderGraphResourceType::TEXTURE) - { - pass_spec.InputTextures[input.BindingInputKeyName] = resource.ResourceInfo.TextureHandle; - } + if (!wr.Handle.Valid()) + continue; + push_view(Resources[wr.Handle.Index].TextureHandle); } - node.Handle->UpdateRenderTargets(); - node.Handle->UpdateInputBinding(); + if (view_count == 0 || w == 0) + continue; - Specifications::FrameBufferSpecificationVNext framebuffer_spec = { - .Width = node.Handle->RenderAreaWidth, - .Height = node.Handle->RenderAreaHeight, - .RenderTargets = node.Handle->RenderTargets, - .Attachment = node.Handle->Attachment, - }; - if (node.Framebuffer) - node.Framebuffer->Dispose(); - node.Framebuffer = ZPushStructCtorArgs(Device->Arena, Buffers::FramebufferVNext, Device, std::move(framebuffer_spec)); - } - } + pass.Handle->RenderAreaWidth = w; + pass.Handle->RenderAreaHeight = h; - void RenderGraph::Dispose() - { - for (auto& node_name : SortedNodesMap) - { - auto& node = NodeMap[node_name]; - node.CallbackPass->Deinitialize(Device); - node.Handle->Dispose(); - if (node.Handle->Specification.Type == Specifications::RenderPassType::GRAPHIC) - { - node.Framebuffer->Dispose(); - } - } + VkRenderPass rp = pass.Handle->GetAttachment()->GetHandle(); + VkFramebuffer vk_fb = Device->CreateFramebuffer(Core::Containers::ArrayView{view_buf, view_count}, rp, w, h); - for (const auto& resource : ResourceMap) - { - auto& value = ResourceMap[resource.first]; - if (value.ResourceInfo.External) + if (vk_fb == VK_NULL_HANDLE) { + ZENGINE_CORE_ERROR("[RenderGraph] AllocateFramebuffers: CreateFramebuffer returned null for pass '{}' (views={} rp={} w={} h={})", pass.Name ? pass.Name : "?", view_count, (void*) rp, w, h) continue; } - if (value.Type == RenderGraphResourceType::ATTACHMENT || value.Type == RenderGraphResourceType::TEXTURE) - { - if (value.ResourceInfo.TextureHandle.Valid()) - Device->TextureHandleToDispose.Enqueue(value.ResourceInfo.TextureHandle); - } + if (!pass.Framebuffer) + pass.Framebuffer = ZPushStructCtorArgs(Device->Arena, Buffers::FramebufferVNext, Device); + pass.Framebuffer->Reset(vk_fb, w, h); } } - RenderGraphResource& RenderGraphResourceBuilder::AttachTexture(cstring name, const Textures::TextureHandle& handle) + void RenderGraphResourceBuilder::Initialize(RenderGraph* graph) { - auto texture = Graph->Device->GlobalTextures.Access(handle); - Graph->ResourceMap[name].Name = name; - Graph->ResourceMap[name].Type = RenderGraphResourceType::TEXTURE; - Graph->ResourceMap[name].ResourceInfo.TextureHandle = handle; - Graph->ResourceMap[name].ResourceInfo.TextureSpec = texture->Specification; - Graph->ResourceMap[name].ResourceInfo.External = true; - return Graph->ResourceMap[name]; + Graph = graph; } - RenderGraphResource& RenderGraphResourceBuilder::AttachRenderTarget(cstring name, const Textures::TextureHandle& handle) + static uint32_t GetOrCreateResource(RenderGraph* graph, cstring name, RGResourceKind kind, bool external, const Specifications::TextureSpecification& spec) { - auto texture = Graph->Device->GlobalTextures.Access(handle); - Graph->ResourceMap[name].Name = name; - Graph->ResourceMap[name].Type = RenderGraphResourceType::ATTACHMENT; - Graph->ResourceMap[name].ResourceInfo.TextureHandle = handle; - Graph->ResourceMap[name].ResourceInfo.TextureSpec = texture->Specification; - Graph->ResourceMap[name].ResourceInfo.External = true; - return Graph->ResourceMap[name]; + if (auto* idx = graph->ResourceIndex.find(name)) + return *idx; + + uint32_t idx = static_cast(graph->Resources.size()); + auto& res = graph->Resources.push_use({}); + res.Name = name; + res.Kind = kind; + res.External = external; + res.Transient = !external; + res.Spec = spec; + graph->ResourceIndex[name] = idx; + return idx; } - void RenderGraphResourceBuilder::Initialize(RenderGraphPtr graph) + static void RecordAccess(RenderGraph* graph, uint32_t pass_idx, uint32_t res_idx, RGAccess access, cstring binding_key, bool is_write) { - Graph = graph; + if (pass_idx == UINT32_MAX) + return; + RGPass& pass = graph->Passes[pass_idx]; + RGPassResource pr; + pr.Handle = {res_idx, 0}; + pr.Access = access; + pr.BindingKey = binding_key; + if (is_write) + pass.Writes.push(pr); + else + pass.Reads.push(pr); } - RenderGraphResource& RenderGraphResourceBuilder::CreateTexture(cstring name, const Specifications::TextureSpecification& spec) + RGResourceHandle RenderGraphResourceBuilder::WriteColorAttachment(cstring name, const Specifications::TextureSpecification& spec) { - Graph->ResourceMap[name].Name = name; - Graph->ResourceMap[name].Type = RenderGraphResourceType::TEXTURE; - Graph->ResourceMap[name].ResourceInfo.TextureSpec = spec; - return Graph->ResourceMap[name]; + uint32_t idx = GetOrCreateResource(Graph, name, RGResourceKind::Attachment, false, spec); + RecordAccess(Graph, CurrentPass, idx, RGAccess::ColorWrite, nullptr, true); + return {idx, 0}; } - RenderGraphResource& RenderGraphResourceBuilder::CreateTexture(cstring name, cstring filename) + RGResourceHandle RenderGraphResourceBuilder::WriteDepthAttachment(cstring name, const Specifications::TextureSpecification& spec) { - Graph->ResourceMap[name].Name = name; - Graph->ResourceMap[name].Type = RenderGraphResourceType::TEXTURE; - Graph->ResourceMap[name].ResourceInfo.TextureHandle = Graph->Device->RRM ? static_cast(Graph->Device->RRM)->SubmitTextureFile(0, 0, filename) : Rendering::Textures::TextureHandle{}; - return Graph->ResourceMap[name]; + uint32_t idx = GetOrCreateResource(Graph, name, RGResourceKind::Attachment, false, spec); + RecordAccess(Graph, CurrentPass, idx, RGAccess::DepthWrite, nullptr, true); + return {idx, 0}; } - RenderGraphResource& RenderGraphResourceBuilder::CreateRenderTarget(cstring name, const Specifications::TextureSpecification& spec) + RGResourceHandle RenderGraphResourceBuilder::ReadTexture(cstring name, cstring binding_key) { - Graph->ResourceMap[name].Name = name; - Graph->ResourceMap[name].Type = RenderGraphResourceType::ATTACHMENT; - Graph->ResourceMap[name].ResourceInfo.TextureSpec = spec; - return Graph->ResourceMap[name]; + Specifications::TextureSpecification empty_spec = {}; + uint32_t idx = GetOrCreateResource(Graph, name, RGResourceKind::Texture, true, empty_spec); + RecordAccess(Graph, CurrentPass, idx, RGAccess::ShaderRead, binding_key, false); + return {idx, 0}; } - void RenderGraphResourceBuilder::CreateRenderPassNode(RenderGraphRenderPassCreation creation) + RGResourceHandle RenderGraphResourceBuilder::ReadDepth(cstring name) { - cstring name = creation.Name; - for (const auto& output : creation.Outputs) - { - if (output.Type == RenderGraphResourceType::ATTACHMENT) - { - RenderGraphResource& resource = Graph->ResourceMap[output.Name]; - resource.ProducerNodeName = name; - } - } - Graph->NodeMap[name].Creation = std::move(creation); + Specifications::TextureSpecification empty_spec = {}; + uint32_t idx = GetOrCreateResource(Graph, name, RGResourceKind::Attachment, true, empty_spec); + RecordAccess(Graph, CurrentPass, idx, RGAccess::DepthRead, nullptr, false); + return {idx, 0}; } - void RenderGraphResourceInspector::Initialize(RenderGraphPtr graph) + RGResourceHandle RenderGraphResourceBuilder::ImportRenderTarget(cstring name, Textures::TextureHandle handle) { - Graph = graph; + Specifications::TextureSpecification empty_spec = {}; + uint32_t idx = GetOrCreateResource(Graph, name, RGResourceKind::Attachment, true, empty_spec); + Graph->Resources[idx].TextureHandle = handle; + Graph->Resources[idx].External = true; + Graph->Resources[idx].Transient = false; + return {idx, 0}; } - RenderGraphResource& RenderGraphResourceInspector::GetResource(cstring name) + RGResourceHandle RenderGraphResourceBuilder::AttachRenderTarget(cstring name, const Textures::TextureHandle& texture) { - if (!Graph->ResourceMap.contains(name)) - { - Graph->ResourceMap[name].Name = name; - } - return Graph->ResourceMap[name]; + return ImportRenderTarget(name, texture); } - Textures::TextureHandle RenderGraphResourceInspector::GetRenderTarget(cstring name) + void RenderGraphResourceInspector::Initialize(RenderGraph* graph) { - Textures::TextureHandle output = {}; - if (!Graph->ResourceMap.contains(name)) - { - Graph->ResourceMap[name].Name = name; - } - if (Graph->ResourceMap[name].Type != RenderGraphResourceType::ATTACHMENT) - { - ZENGINE_CORE_WARN("{} isn't a valid Attachement Resource", name) - } - - auto handle = Graph->ResourceMap[name].ResourceInfo.TextureHandle; - if (handle.Valid()) - { - output = handle; - } - return output; + Graph = graph; } - Textures::TextureHandle RenderGraphResourceInspector::GetTexture(cstring name) + Textures::TextureHandle RenderGraphResourceInspector::GetTextureHandle(RGResourceHandle handle) const { - Textures::TextureHandle output = {}; - - if (!Graph->ResourceMap.contains(name)) - { - Graph->ResourceMap[name].Name = name; - } - if (Graph->ResourceMap[name].Type != RenderGraphResourceType::TEXTURE) - { - ZENGINE_CORE_WARN("{} isn't a valid Texture Resource", name) - } + if (!handle.Valid() || handle.Index >= Graph->Resources.size()) + return {}; + return Graph->Resources[handle.Index].TextureHandle; + } - auto handle = Graph->ResourceMap[name].ResourceInfo.TextureHandle; - if (handle.Valid()) - { - output = handle; - } - return output; + Textures::TextureHandle RenderGraphResourceInspector::GetRenderTarget(cstring name) const + { + if (auto* idx = Graph->ResourceIndex.find(name)) + return Graph->Resources[*idx].TextureHandle; + return {}; } - RenderGraphNode& RenderGraphResourceInspector::GetNode(cstring name) + Textures::TextureHandle RenderGraphResourceInspector::GetTexture(cstring name) const { - ZENGINE_VALIDATE_ASSERT(Graph->NodeMap.contains(name), "Node Pass should be created first") - return Graph->NodeMap[name]; + return GetRenderTarget(name); } + } // namespace ZEngine::Rendering::Renderers diff --git a/ZEngine/ZEngine/Rendering/Renderers/RenderGraph.h b/ZEngine/ZEngine/Rendering/Renderers/RenderGraph.h index a80467ed2..6b6adfddc 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/RenderGraph.h +++ b/ZEngine/ZEngine/Rendering/Renderers/RenderGraph.h @@ -1,7 +1,7 @@ #pragma once #include #include -#include +#include #include #include #include @@ -9,12 +9,12 @@ #include #include #include +#include namespace ZEngine::Rendering::Renderers { struct RenderGraphResourceBuilder; struct RenderGraphResourceInspector; - struct RenderGraphNode; struct RenderGraph; struct IRenderGraphCallbackPass; @@ -23,47 +23,101 @@ namespace ZEngine::Rendering::Renderers ZDEFINE_PTR(RenderGraph); ZDEFINE_PTR(IRenderGraphCallbackPass); - enum RenderGraphResourceType + // Typed index into RenderGraph::Resources[]. No string on the execute hot path. + struct RGResourceHandle { - UNDEFINED = -1, - BUFFER = 0, - ATTACHMENT, - TEXTURE, - REFERENCE + uint32_t Index = UINT32_MAX; + uint32_t Version = 0; + + bool Valid() const + { + return Index != UINT32_MAX; + } }; - struct RenderGraphResourceInfo + enum class RGResourceKind : uint8_t { - bool External = false; - Specifications::TextureSpecification TextureSpec; - union - { - Textures::TextureHandle TextureHandle; - }; + Attachment, + Texture, + Buffer, + }; + + // How a pass uses a resource — drives barrier stage/access/layout derivation. + enum class RGAccess : uint8_t + { + None, + ColorWrite, + DepthWrite, + DepthRead, + ShaderRead, + ShaderReadWrite, + TransferRead, + TransferWrite, + Present, + Count_, + }; + + struct RGResourceState + { + VkPipelineStageFlags Stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags Access = 0; + VkImageLayout Layout = VK_IMAGE_LAYOUT_UNDEFINED; + }; + + struct RGResource + { + cstring Name = nullptr; + RGResourceKind Kind = RGResourceKind::Attachment; + bool External = false; + Textures::TextureHandle TextureHandle = {}; + RGResourceState CurrentState = {}; // compile-time simulation + RGResourceState RuntimeState = {}; // per-frame Execute tracking + uint32_t FirstPassIndex = UINT32_MAX; + uint32_t LastPassIndex = 0; + bool Transient = true; + Specifications::TextureSpecification Spec = {}; + }; + + struct RGPassResource + { + RGResourceHandle Handle = {}; + RGAccess Access = RGAccess::None; + cstring BindingKey = nullptr; }; - struct RenderGraphResource + struct RGPass { - cstring Name; - cstring ProducerNodeName; - RenderGraphResourceType Type; - RenderGraphResourceInfo ResourceInfo; + cstring Name = nullptr; + bool Enabled = true; + IRenderGraphCallbackPass* Callback = nullptr; + RenderPasses::RenderPass* Handle = nullptr; + ZRawPtr(Buffers::FramebufferVNext) Framebuffer = nullptr; + Core::Containers::Array Reads = {}; + Core::Containers::Array Writes = {}; + Core::Containers::Array ImageBarriers = {}; + VkPipelineStageFlags BarrierSrcStage = 0; + VkPipelineStageFlags BarrierDstStage = 0; }; - struct RenderGraphRenderPassInputOutputInfo + struct RGTransientSlot { - cstring Name; - cstring BindingInputKeyName; - RenderGraphResourceType Type = RenderGraphResourceType::ATTACHMENT; + Textures::TextureHandle Handle = {}; + Specifications::TextureSpecification Spec = {}; + uint32_t FreeAfterPass = 0; }; - struct RenderGraphRenderPassCreation + struct RGTransientPool { - cstring Name; - Core::Containers::Array Inputs; - Core::Containers::Array Outputs; + Core::Containers::Array Slots; + + void Initialize(Core::Memory::ArenaAllocator* arena); + Textures::TextureHandle TryAlias(const Specifications::TextureSpecification& spec, uint32_t first_pass); + void Register(Textures::TextureHandle handle, const Specifications::TextureSpecification& spec, uint32_t last_pass); + void MarkInUse(Textures::TextureHandle handle, uint32_t last_pass); + void Clear(); }; + // Unchanged interface — all existing pass implementations compile without modification. struct IRenderGraphCallbackPass { virtual void Setup(Hardwares::VulkanDevicePtr const device, cstring name, RenderGraphResourceBuilderPtr const res_builder, RenderGraphResourceInspectorPtr res_inspector) = 0; @@ -72,66 +126,90 @@ namespace ZEngine::Rendering::Renderers virtual void Deinitialize(Hardwares::VulkanDevicePtr const device) {} }; - struct RenderGraphNode + struct RenderGraph { - bool Enabled = true; - RenderGraphRenderPassCreation Creation = {}; - Core::Containers::UnorderedHashSet EdgeNodes = {}; - RenderPasses::RenderPassPtr Handle = nullptr; - ZRawPtr(Buffers::FramebufferVNext) Framebuffer = nullptr; - IRenderGraphCallbackPassPtr CallbackPass = nullptr; + RenderGraph() = default; + ~RenderGraph() = default; + + Hardwares::VulkanDevicePtr Device = nullptr; + Scenes::SceneDataPtr SceneData = nullptr; + + Core::Containers::Array Passes; + Core::Containers::Array Resources; + Core::Containers::Array SortedPassIndices; + + // String → index: used only in Setup/Compile, not in Execute. + Core::Containers::UnorderedHashMap ResourceIndex; + Core::Containers::UnorderedHashMap PassIndex; + + RenderGraphResourceBuilderPtr ResourceBuilder = nullptr; + RenderGraphResourceInspectorPtr ResourceInspector = nullptr; + RenderPasses::RenderPassBuilder* RenderPassBuilder = nullptr; + + RGTransientPool TransientPool; + + void Initialize(Hardwares::VulkanDevicePtr device, Scenes::SceneDataPtr data = nullptr); + void AddCallbackPass(cstring pass_name, IRenderGraphCallbackPass* const cb, bool enabled = true); + void Setup(); + void Compile(); + void Execute(Hardwares::CommandBufferPtr const cb); + void Resize(uint32_t width, uint32_t height); + void Dispose(); + + RGResourceHandle ImportRenderTarget(cstring name, Textures::TextureHandle handle); + + // Access a pass by name — O(1) lookup via PassIndex; setup/config only, not Execute. + RGPass* GetPass(cstring name); + void SetPassEnabled(cstring name, bool enabled); + + private: + void BuildLifetimes(); + void AllocateTransientResources(); + void BuildBarriers(); + void BuildTopology(); + void AllocateFramebuffers(); }; - struct RenderGraph + // Pass-facing API — replaces RenderGraphResourceBuilder call sites in Setup(). + struct RenderGraphResourceBuilder { - RenderGraph() {} - ~RenderGraph() {} + RenderGraph* Graph = nullptr; + uint32_t CurrentPass = UINT32_MAX; + + void Initialize(RenderGraph* graph); - Hardwares::VulkanDevicePtr Device = nullptr; + // Declare that the current pass writes a transient color attachment. + RGResourceHandle WriteColorAttachment(cstring name, const Specifications::TextureSpecification& spec); - Core::Containers::Array SortedNodesMap = {}; - Core::Containers::UnorderedHashMap NodeMap = {}; - Core::Containers::UnorderedHashMap ResourceMap = {}; + // Declare that the current pass writes a transient depth attachment. + RGResourceHandle WriteDepthAttachment(cstring name, const Specifications::TextureSpecification& spec); - RenderGraphResourceBuilderPtr ResourceBuilder = nullptr; - RenderGraphResourceInspectorPtr ResourceInspector = nullptr; - RenderPasses::RenderPassBuilder* RenderPassBuilder = nullptr; + // Declare that the current pass reads a resource as a sampled texture. + RGResourceHandle ReadTexture(cstring name, cstring binding_key = nullptr); - Scenes::SceneDataPtr SceneData = nullptr; + // Declare that the current pass reads a depth resource (read-only). + RGResourceHandle ReadDepth(cstring name); - void Initialize(Hardwares::VulkanDevicePtr device, Scenes::SceneDataPtr data = nullptr); + // Import an externally-managed render target (not owned by the graph). + RGResourceHandle ImportRenderTarget(cstring name, Textures::TextureHandle handle); - void Setup(); - void Compile(); - void Execute(Hardwares::CommandBufferPtr const command_buffer); - void Resize(uint32_t width, uint32_t height); - void Dispose(); - void AddCallbackPass(cstring pass_name, IRenderGraphCallbackPass* const pass_callback, bool enabled = true); + // Attach an already-imported render target by name — looks up by name only. + RGResourceHandle AttachRenderTarget(cstring name, const Textures::TextureHandle& texture); }; + // Pass-facing read API — replaces RenderGraphResourceInspector call sites in Execute(). struct RenderGraphResourceInspector { - RenderGraphPtr Graph = nullptr; + RenderGraph* Graph = nullptr; - void Initialize(RenderGraphPtr graph); + void Initialize(RenderGraph* graph); - RenderGraphResource& GetResource(cstring name); - Textures::TextureHandle GetRenderTarget(cstring name); - Textures::TextureHandle GetTexture(cstring name); - RenderGraphNode& GetNode(cstring name); - }; - - struct RenderGraphResourceBuilder - { - RenderGraphPtr Graph = nullptr; - - void Initialize(RenderGraphPtr graph); + // Retrieve a texture handle by RGResourceHandle (O(1), no string). + Textures::TextureHandle GetTextureHandle(RGResourceHandle handle) const; - RenderGraphResource& CreateTexture(cstring name, const Specifications::TextureSpecification& spec); - RenderGraphResource& CreateTexture(cstring name, cstring filename); - RenderGraphResource& CreateRenderTarget(cstring name, const Specifications::TextureSpecification& spec); - RenderGraphResource& AttachTexture(cstring name, const Textures::TextureHandle& texture); - RenderGraphResource& AttachRenderTarget(cstring name, const Textures::TextureHandle& texture); - void CreateRenderPassNode(RenderGraphRenderPassCreation creation); + // String-keyed overloads — preserved for existing Execute() call sites. + Textures::TextureHandle GetRenderTarget(cstring name) const; + Textures::TextureHandle GetTexture(cstring name) const; }; -} // namespace ZEngine::Rendering::Renderers \ No newline at end of file + +} // namespace ZEngine::Rendering::Renderers diff --git a/ZEngine/ZEngine/Rendering/Renderers/RendererPasses.cpp b/ZEngine/ZEngine/Rendering/Renderers/RendererPasses.cpp index a89b9e928..a4840bee2 100644 --- a/ZEngine/ZEngine/Rendering/Renderers/RendererPasses.cpp +++ b/ZEngine/ZEngine/Rendering/Renderers/RendererPasses.cpp @@ -12,16 +12,8 @@ namespace ZEngine::Rendering::Renderers 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); - - // 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)); + res_builder->ReadTexture("gbuffer_albedo_render_target", "sharedRTAsTex"); + res_builder->WriteColorAttachment(RendererResourceName::FrameColorRenderTargetName, {}); } void CompositePass::Compile(Hardwares::VulkanDevicePtr const device, Rendering::Scenes::SceneDataPtr const scene, RenderPasses::RenderPassBuilder* pass_builder, RenderGraphResourceInspectorPtr res_inspector, RenderPasses::RenderPass** const output_pass) @@ -55,13 +47,9 @@ namespace ZEngine::Rendering::Renderers void DepthPrePass::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.Outputs.push(RenderGraphRenderPassInputOutputInfo{.Name = RendererResourceName::FrameDepthRenderTargetName}); - - res_builder->CreateRenderPassNode(std::move(pass_node)); + uint32_t w = device->SwapchainPtr->SwapchainImageWidth; + uint32_t h = device->SwapchainPtr->SwapchainImageHeight; + res_builder->WriteDepthAttachment(RendererResourceName::FrameDepthRenderTargetName, {.Width = w, .Height = h, .Format = Specifications::ImageFormat::DEPTH_STENCIL_FROM_DEVICE}); } void DepthPrePass::Compile(Hardwares::VulkanDevicePtr const device, Rendering::Scenes::SceneDataPtr const scene, RenderPasses::RenderPassBuilder* pass_builder, RenderGraphResourceInspectorPtr res_inspector, RenderPasses::RenderPass** const output_pass) @@ -143,16 +131,13 @@ namespace ZEngine::Rendering::Renderers if (env_map_available) { - auto env_map_res = res_builder->CreateTexture("skybox_env_map", EnvMapPath); - m_env_map = env_map_res.ResourceInfo.TextureHandle; + auto* rrm = ZEngine::Engine::GetContext()->RenderResourceManager; + if (rrm) + m_env_map = rrm->SubmitTextureFile(0, 0, EnvMapPath); } - RenderGraphRenderPassCreation pass_node = {.Name = name}; - pass_node.Inputs.init(device->Arena, 2); - pass_node.Outputs.init(device->Arena, 1); - pass_node.Inputs.push(RenderGraphRenderPassInputOutputInfo{.Name = RendererResourceName::FrameDepthRenderTargetName}); - pass_node.Inputs.push(RenderGraphRenderPassInputOutputInfo{.Name = RendererResourceName::FrameColorRenderTargetName}); - res_builder->CreateRenderPassNode(std::move(pass_node)); + res_builder->ReadDepth(RendererResourceName::FrameDepthRenderTargetName); + res_builder->WriteColorAttachment(RendererResourceName::FrameColorRenderTargetName, {}); } void SkyboxPass::Compile(Hardwares::VulkanDevicePtr const device, Rendering::Scenes::SceneDataPtr const scene, RenderPasses::RenderPassBuilder* pass_builder, RenderGraphResourceInspectorPtr res_inspector, RenderPasses::RenderPass** const output_pass) @@ -230,12 +215,8 @@ namespace ZEngine::Rendering::Renderers ZENGINE_VALIDATE_ASSERT(rrm, "GridPass::Setup: RenderResourceManager not available") rrm->RegisterBuiltinGeometry(verts, sizeof(verts), idxs, 6, m_vtx_offset, m_idx_offset); - RenderGraphRenderPassCreation pass_node = {.Name = name}; - pass_node.Inputs.init(device->Arena, 2); - pass_node.Outputs.init(device->Arena, 1); - pass_node.Inputs.push(RenderGraphRenderPassInputOutputInfo{.Name = RendererResourceName::FrameDepthRenderTargetName}); - pass_node.Inputs.push(RenderGraphRenderPassInputOutputInfo{.Name = RendererResourceName::FrameColorRenderTargetName}); - res_builder->CreateRenderPassNode(std::move(pass_node)); + res_builder->ReadDepth(RendererResourceName::FrameDepthRenderTargetName); + res_builder->WriteColorAttachment(RendererResourceName::FrameColorRenderTargetName, {}); } void GridPass::Compile(Hardwares::VulkanDevicePtr const device, Rendering::Scenes::SceneDataPtr const scene, RenderPasses::RenderPassBuilder* pass_builder, RenderGraphResourceInspectorPtr res_inspector, RenderPasses::RenderPass** const output_pass) @@ -298,16 +279,12 @@ 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; - 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}); - pass_node.Outputs.push(RenderGraphRenderPassInputOutputInfo{.Name = RendererResourceName::FrameColorRenderTargetName}); - res_builder->CreateRenderPassNode(std::move(pass_node)); + uint32_t w = device->SwapchainPtr->SwapchainImageWidth; + uint32_t h = device->SwapchainPtr->SwapchainImageHeight; + res_builder->ReadDepth(RendererResourceName::FrameDepthRenderTargetName); + res_builder->WriteColorAttachment(RendererResourceName::GBufferAlbedoAOName, {.Width = w, .Height = h, .Format = Specifications::ImageFormat::R8G8B8A8_UNORM}); + res_builder->WriteColorAttachment(RendererResourceName::GBufferNormalRoughnessName, {.Width = w, .Height = h, .Format = Specifications::ImageFormat::R16G16B16A16_SFLOAT}); + res_builder->WriteColorAttachment(RendererResourceName::GBufferMetallicEmissiveName, {.Width = w, .Height = h, .Format = Specifications::ImageFormat::R8G8B8A8_UNORM}); } void GbufferPass::Compile(Hardwares::VulkanDevicePtr const device, Rendering::Scenes::SceneDataPtr const scene, RenderPasses::RenderPassBuilder* pass_builder, RenderGraphResourceInspectorPtr res_inspector, RenderPasses::RenderPass** const output_pass) @@ -350,9 +327,63 @@ namespace ZEngine::Rendering::Renderers command_buffer->EndRenderPass(); } - void LightingPass::Setup(Hardwares::VulkanDevicePtr const, cstring, RenderGraphResourceBuilderPtr const, RenderGraphResourceInspectorPtr) {} + void LightingPass::Setup(Hardwares::VulkanDevicePtr const device, cstring name, RenderGraphResourceBuilderPtr const res_builder, RenderGraphResourceInspectorPtr res_inspector) + { + uint32_t w = device->SwapchainPtr->SwapchainImageWidth; + uint32_t h = device->SwapchainPtr->SwapchainImageHeight; + res_builder->ReadTexture(RendererResourceName::GBufferAlbedoAOName, "GBufferAlbedoAO"); + res_builder->ReadTexture(RendererResourceName::GBufferNormalRoughnessName, "GBufferNormalRoughness"); + res_builder->ReadTexture(RendererResourceName::GBufferMetallicEmissiveName, "GBufferMetallicEmissive"); + res_builder->ReadTexture(RendererResourceName::FrameDepthRenderTargetName, "GBufferDepth"); + res_builder->WriteColorAttachment(RendererResourceName::FrameColorRenderTargetName, {.Width = w, .Height = h, .Format = Specifications::ImageFormat::R8G8B8A8_UNORM, .LoadOp = LoadOperation::LOAD}); + } + + void LightingPass::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("Deferred-Lighting-Pipeline").SetInputBindingCount(0).EnablePipelineDepthTest(false).UseShader("deferred_lighting").Detach(); + *output_pass = device->CreateRenderPass(std::move(pass_spec)); + (*output_pass)->Bake(); + } + + (*output_pass)->SetDynamicUniform("UBCamera", sizeof(Contracts::UBOCameraLayout)); - void LightingPass::Compile(Hardwares::VulkanDevicePtr const, Rendering::Scenes::SceneDataPtr const, RenderPasses::RenderPassBuilder*, RenderGraphResourceInspectorPtr, RenderPasses::RenderPass** const) {} + auto albedo_ao_handle = res_inspector->GetRenderTarget(RendererResourceName::GBufferAlbedoAOName); + auto normal_rough_handle = res_inspector->GetRenderTarget(RendererResourceName::GBufferNormalRoughnessName); + auto metallic_emit_handle = res_inspector->GetRenderTarget(RendererResourceName::GBufferMetallicEmissiveName); + auto depth_handle = res_inspector->GetRenderTarget(RendererResourceName::FrameDepthRenderTargetName); - void LightingPass::Execute(Hardwares::VulkanDevicePtr const, RenderGraphResourceInspectorPtr, Rendering::Scenes::SceneDataPtr const, RenderPasses::RenderPass* const, Buffers::FramebufferVNext* const, Hardwares::CommandBufferPtr const) {} + if (albedo_ao_handle.Valid()) + (*output_pass)->SetTexture("GBufferAlbedoAO", albedo_ao_handle); + if (normal_rough_handle.Valid()) + (*output_pass)->SetTexture("GBufferNormalRoughness", normal_rough_handle); + if (metallic_emit_handle.Valid()) + (*output_pass)->SetTexture("GBufferMetallicEmissive", metallic_emit_handle); + if (depth_handle.Valid()) + (*output_pass)->SetTexture("GBufferDepth", depth_handle); + + if (scene && scene->LightBuffer.Handle) + (*output_pass)->SetStorageBuffer("LightSB", &scene->LightBuffer); + + (*output_pass)->SetSampler("GBufferSampler", device->GlobalLinearWrapSamplerImageInfo); + (*output_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) + { + command_buffer->BeginRenderPass(pass, framebuffer->Handle, false); + { + 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 ? &scene->CameraHeapOffset : nullptr, scene ? 1u : 0u); + command_buffer->Draw(3, 1, 0, 0); + command_buffer->EndRenderPass(); + } } // namespace ZEngine::Rendering::Renderers diff --git a/ZEngine/ZEngine/Rendering/Scenes/RenderScene.h b/ZEngine/ZEngine/Rendering/Scenes/RenderScene.h index 9c1b59fc3..108bfeaaa 100644 --- a/ZEngine/ZEngine/Rendering/Scenes/RenderScene.h +++ b/ZEngine/ZEngine/Rendering/Scenes/RenderScene.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -51,6 +52,32 @@ namespace ZEngine::Rendering::Scenes } }; + struct GpuDirectionalLight + { + gpuvec4 Direction = {}; + gpuvec4 Color = {}; + float Intensity = 0.f; + float _pad[3] = {}; + }; + + struct GpuPointLight + { + gpuvec4 Position = {}; + gpuvec4 Color = {}; + float Intensity = 0.f; + float Radius = 0.f; + float _pad[2] = {}; + }; + + struct LightArrayUBO + { + GpuDirectionalLight DirectionalLights[4] = {}; + GpuPointLight PointLights[8] = {}; + uint32_t DirectionalCount = 0; + uint32_t PointCount = 0; + uint32_t _pad[2] = {}; + }; + struct SceneData { // Camera UBO — migrated to PerFrameUploadHeap; offset updated each frame in DrawScene @@ -67,6 +94,7 @@ namespace ZEngine::Rendering::Scenes Core::Memory::BufferView TransformBuffer = {}; Core::Memory::BufferView MaterialBuffer = {}; Core::Memory::BufferView RenderDataBuffer = {}; + Core::Memory::BufferView LightBuffer = {}; // RRM vertex buffer handle — index buffer is paired via RRM::GetIndexBuffer(RMMVertexHandle). Rendering::BufferHandle RMMVertexHandle = {}; diff --git a/ZEngine/ZEngine/Rendering/Shaders/Shader.cpp b/ZEngine/ZEngine/Rendering/Shaders/Shader.cpp index 3356e698d..e45616941 100644 --- a/ZEngine/ZEngine/Rendering/Shaders/Shader.cpp +++ b/ZEngine/ZEngine/Rendering/Shaders/Shader.cpp @@ -237,7 +237,7 @@ namespace ZEngine::Rendering::Shaders uint32_t set = spirv_compiler->get_decoration(SB_resource.id, spv::DecorationDescriptorSet); uint32_t binding = spirv_compiler->get_decoration(SB_resource.id, spv::DecorationBinding); - if (LayoutBindingSpecificationMap.at(set).capacity() <= 0) + if (LayoutBindingSpecificationMap[set].capacity() <= 0) { LayoutBindingSpecificationMap[set].init(m_device->Arena, 10); } @@ -459,11 +459,21 @@ namespace ZEngine::Rendering::Shaders continue; } + layout_binding_collection.clear(); for (uint32_t i = 0; i < layout_binding_set.second.size(); ++i) { layout_binding_collection.push(VkDescriptorSetLayoutBinding{.binding = layout_binding_set.second[i].Binding, .descriptorType = DescriptorTypeMap[static_cast(layout_binding_set.second[i].DescriptorTypeValue)], .descriptorCount = layout_binding_set.second[i].Count, .stageFlags = ShaderStageFlagsMap[static_cast(layout_binding_set.second[i].Flags)], .pImmutableSamplers = nullptr}); } + for (const auto& lb : layout_binding_collection) + { + auto it = std::find_if(pool_size_collection.begin(), pool_size_collection.end(), [&](const VkDescriptorPoolSize& ps) { return ps.type == lb.descriptorType; }); + if (it == pool_size_collection.end()) + pool_size_collection.push(VkDescriptorPoolSize{.type = lb.descriptorType, .descriptorCount = lb.descriptorCount}); + else + it->descriptorCount += lb.descriptorCount; + } + /* * Binding flag extension */ @@ -532,20 +542,6 @@ namespace ZEngine::Rendering::Shaders InternalDescriptorSetLayoutMap[binding_set] = std::move(descriptor_set_layout); } - /* - * Packing PoolSize - */ - for (const auto& layout_binding : layout_binding_collection) - { - auto find_pool_size_it = std::find_if(pool_size_collection.begin(), pool_size_collection.end(), [&](const VkDescriptorPoolSize& pool_size) { return (layout_binding.descriptorType == pool_size.type); }); - - if (find_pool_size_it == std::end(pool_size_collection)) - { - pool_size_collection.push(VkDescriptorPoolSize{.type = layout_binding.descriptorType, .descriptorCount = layout_binding.descriptorCount}); - continue; - } - find_pool_size_it->descriptorCount += layout_binding.descriptorCount; - } /* * Ensure correctness with number of frame count */ @@ -576,10 +572,17 @@ namespace ZEngine::Rendering::Shaders return; } + uint32_t non_reserved_set_count = 0; + for (const auto& layout : InternalDescriptorSetLayoutMap) + { + if (!m_device->ShaderReservedDescriptorSetMap.contains(layout.first)) + ++non_reserved_set_count; + } + VkDescriptorPoolCreateInfo pool_info = {}; pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; pool_info.flags = pool_needs_update_after_bind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0; - pool_info.maxSets = m_device->SwapchainPtr->BufferredFrameCount; + pool_info.maxSets = non_reserved_set_count * m_device->SwapchainPtr->BufferredFrameCount; pool_info.poolSizeCount = pool_size_collection.size(); pool_info.pPoolSizes = pool_size_collection.data(); diff --git a/ZEngine/docs/future-plan/next-year-plans/deferred-rendering.md b/ZEngine/docs/future-plan/next-year-plans/deferred-rendering.md index 4b2a01c7c..3abbb3676 100644 --- a/ZEngine/docs/future-plan/next-year-plans/deferred-rendering.md +++ b/ZEngine/docs/future-plan/next-year-plans/deferred-rendering.md @@ -24,7 +24,7 @@ The two rendering paths differ in when lighting is evaluated relative to visibil | Material variety | Unlimited | Must fit in G-buffer layout | | Best for | Mobile, outdoor, few lights, high transparency | Indoor/architectural, 50+ lights, complex scenes | -**Default path:** Forward rendering with tile-based light culling (`light-culling.md`). Forward handles up to ~100 lights comfortably with culling. Deferred is activated only when a scene has 50+ lights or when the project explicitly sets `RenderingMode::Deferred`. +**Default path:** Forward rendering with tile-based light culling (`light-culling.md`). Forward handles up to ~100 lights comfortably with culling. Deferred is activated only when a scene has 50+ lights or when the project explicitly enables it. --- @@ -56,39 +56,68 @@ The G-buffer is four render targets. All targets share the same dimensions as th ```cpp class GBufferPass final : public IRenderGraphCallbackPass { public: - void Setup(RenderGraphBuilder& builder) override; - void Compile(RenderGraphInspector& inspector) override; - void Execute(VkCommandBuffer cmd, RenderGraphInspector& inspector) override; - -private: - VkPipeline m_pipeline = VK_NULL_HANDLE; - VkPipelineLayout m_layout = VK_NULL_HANDLE; - VkRenderPass m_rp = VK_NULL_HANDLE; + void Setup(Hardwares::VulkanDevicePtr const device, cstring name, + RenderGraphResourceBuilderPtr const res_builder, + RenderGraphResourceInspectorPtr res_inspector) override; + + void Compile(Hardwares::VulkanDevicePtr const device, + Rendering::Scenes::SceneDataPtr const scene, + RenderPasses::RenderPassBuilder* pass_builder, + RenderGraphResourceInspectorPtr res_inspector, + RenderPasses::RenderPass** const output_pass) override; + + void 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) override; + + void Deinitialize(Hardwares::VulkanDevicePtr const device) override; }; ``` **Setup:** ```cpp -void GBufferPass::Setup(RenderGraphBuilder& builder) { - builder.WriteColorAttachment("gbuffer_albedo_ao", - RGTextureDesc{ .format = VK_FORMAT_R8G8B8A8_UNORM, - .usage = IMAGE_USAGE_COLOR_ATTACHMENT | IMAGE_USAGE_SAMPLED }); - - builder.WriteColorAttachment("gbuffer_normals_rough", - RGTextureDesc{ .format = VK_FORMAT_R16G16B16A16_SFLOAT, - .usage = IMAGE_USAGE_COLOR_ATTACHMENT | IMAGE_USAGE_SAMPLED }); - - builder.WriteColorAttachment("gbuffer_metallic_emissive", - RGTextureDesc{ .format = VK_FORMAT_R8G8B8A8_UNORM, - .usage = IMAGE_USAGE_COLOR_ATTACHMENT | IMAGE_USAGE_SAMPLED }); - - builder.WriteDepthAttachment("hdr_depth", - RGTextureDesc{ .format = VK_FORMAT_D32_SFLOAT, - .usage = IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT | IMAGE_USAGE_SAMPLED }); +void GBufferPass::Setup(Hardwares::VulkanDevicePtr const device, cstring name, + RenderGraphResourceBuilderPtr const res_builder, + RenderGraphResourceInspectorPtr res_inspector) { + TextureSpecification albedo_spec = {}; + albedo_spec.Format = VK_FORMAT_R8G8B8A8_UNORM; + res_builder->WriteColorAttachment("gbuffer_albedo_ao", albedo_spec); + + TextureSpecification normals_spec = {}; + normals_spec.Format = VK_FORMAT_R16G16B16A16_SFLOAT; + res_builder->WriteColorAttachment("gbuffer_normals_rough", normals_spec); + + TextureSpecification metallic_spec = {}; + metallic_spec.Format = VK_FORMAT_R8G8B8A8_UNORM; + res_builder->WriteColorAttachment("gbuffer_metallic_emissive", metallic_spec); + + TextureSpecification depth_spec = {}; + depth_spec.Format = VK_FORMAT_D32_SFLOAT; + res_builder->WriteDepthAttachment("hdr_depth", depth_spec); } ``` +Before `Compile` is called, the RenderGraph pre-populates the `RenderPassBuilder` with one `UseRenderTarget` call per declared write. `Compile` receives this pre-populated builder and extends it with pipeline state: + +```cpp +void GBufferPass::Compile(Hardwares::VulkanDevicePtr const device, + Rendering::Scenes::SceneDataPtr const scene, + RenderPasses::RenderPassBuilder* pass_builder, + RenderGraphResourceInspectorPtr res_inspector, + RenderPasses::RenderPass** const output_pass) { + pass_builder->SetPipelineName("gbuffer") + .EnablePipelineDepthTest(true) + .UseShader("gbuffer.vert", "gbuffer.frag") + .Detach(output_pass); +} +``` + +`Execute` binds the per-mesh material descriptor sets and records the draw calls for all opaque meshes in the scene. + **Vertex shader:** standard MVP transform. No changes from the forward path. **Fragment shader (`gbuffer.frag.glsl`):** samples albedo, normal map, roughness/metallic textures from the material. Transforms normals to view space. Packs outputs into the four G-buffer attachment locations: @@ -121,41 +150,69 @@ void main() { ```cpp class DeferredLightingPass final : public IRenderGraphCallbackPass { public: - void Setup(RenderGraphBuilder& builder) override; - void Compile(RenderGraphInspector& inspector) override; - void Execute(VkCommandBuffer cmd, RenderGraphInspector& inspector) override; - -private: - VkPipeline m_pipeline = VK_NULL_HANDLE; - VkPipelineLayout m_layout = VK_NULL_HANDLE; + void Setup(Hardwares::VulkanDevicePtr const device, cstring name, + RenderGraphResourceBuilderPtr const res_builder, + RenderGraphResourceInspectorPtr res_inspector) override; + + void Compile(Hardwares::VulkanDevicePtr const device, + Rendering::Scenes::SceneDataPtr const scene, + RenderPasses::RenderPassBuilder* pass_builder, + RenderGraphResourceInspectorPtr res_inspector, + RenderPasses::RenderPass** const output_pass) override; + + void 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) override; + + void Deinitialize(Hardwares::VulkanDevicePtr const device) override; }; ``` **Setup:** ```cpp -void DeferredLightingPass::Setup(RenderGraphBuilder& builder) { - // Read G-buffer - builder.ReadTexture("gbuffer_albedo_ao", IMAGE_USAGE_SAMPLED); - builder.ReadTexture("gbuffer_normals_rough", IMAGE_USAGE_SAMPLED); - builder.ReadTexture("gbuffer_metallic_emissive", IMAGE_USAGE_SAMPLED); - builder.ReadTexture("hdr_depth", IMAGE_USAGE_SAMPLED); - - // Read shadow maps - builder.ReadTexture("shadow_map_directional", IMAGE_USAGE_SAMPLED); - - // Read light culling output - builder.ReadBuffer("light_grid", BUFFER_USAGE_STORAGE_READ); - builder.ReadBuffer("light_index_list", BUFFER_USAGE_STORAGE_READ); - builder.ReadBuffer("light_buffer", BUFFER_USAGE_UNIFORM_READ); - - // Write HDR output - builder.WriteColorAttachment("hdr_lit", - RGTextureDesc{ .format = VK_FORMAT_B10G11R11_UFLOAT_PACK32, - .usage = IMAGE_USAGE_COLOR_ATTACHMENT | IMAGE_USAGE_SAMPLED }); +void DeferredLightingPass::Setup(Hardwares::VulkanDevicePtr const device, cstring name, + RenderGraphResourceBuilderPtr const res_builder, + RenderGraphResourceInspectorPtr res_inspector) { + res_builder->ReadTexture("gbuffer_albedo_ao"); + res_builder->ReadTexture("gbuffer_normals_rough"); + res_builder->ReadTexture("gbuffer_metallic_emissive"); + res_builder->ReadDepth("hdr_depth"); + + // Shadow map read — depends on ShadowPass completing first. + res_builder->ReadTexture("shadow_map_directional"); + + // Light culling buffer reads are declared here when LightCullPass is implemented. + // See light-culling.md for the light_grid and light_index_list resource names. + + TextureSpecification hdr_spec = {}; + hdr_spec.Format = VK_FORMAT_B10G11R11_UFLOAT_PACK32; + res_builder->WriteColorAttachment("hdr_lit", hdr_spec); } ``` +Depth is read in `VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL` (MoltenVK compatible). `depthWriteEnable` is set to false in the pipeline spec. + +Before `Compile` is called, the RenderGraph pre-populates the `RenderPassBuilder` with one `AddInputAttachment` call per declared read and one `UseRenderTarget` call for the declared write. `Compile` extends the builder with pipeline state: + +```cpp +void DeferredLightingPass::Compile(Hardwares::VulkanDevicePtr const device, + Rendering::Scenes::SceneDataPtr const scene, + RenderPasses::RenderPassBuilder* pass_builder, + RenderGraphResourceInspectorPtr res_inspector, + RenderPasses::RenderPass** const output_pass) { + pass_builder->SetPipelineName("deferred_lighting") + .EnablePipelineDepthTest(false) + .UseShader("fullscreen_triangle.vert", "deferred_lighting.frag") + .Detach(output_pass); +} +``` + +`Execute` retrieves texture handles via `res_inspector->GetTextureHandle(handle)` and binds them to the lighting descriptor set before recording the full-screen triangle draw. + **Fragment shader (`deferred_lighting.frag.glsl`):** Reconstructs world position from depth + inverse VP matrix. Reads G-buffer. Evaluates directional light (always applied, not tile-culled). Iterates tile-assigned point and spot lights via `light_grid`/`light_index_list`. Applies shadow map lookups. Applies lightmap if `LightmapComponent` data is packed into RT2 reserved bits. Normal reconstruction from G-buffer (view-space to world-space): @@ -188,7 +245,7 @@ vec3 reconstruct_position(vec2 uv, float depth) { } ``` -Note: "depth must be in Vulkan range [0,1]. If reversed-Z optimization is enabled, pass (1.0 - depth) instead." +Note: depth must be in Vulkan range [0,1]. If reversed-Z optimization is enabled, pass `(1.0 - depth)` instead. **NDC convention:** Vulkan depth is [0,1] natively. The reconstruction shader uses depth directly without conversion. If the project uses reversed-Z (depth_near=1, depth_far=0), pass `(1.0 - depth)` instead. @@ -212,41 +269,63 @@ Alpha-cutout materials (masked) that do not require blending can be rendered in ## 6. RenderGraph Integration -**`RenderingMode` enum:** +`GraphicRenderer::Initialize` is the orchestrator for pass registration. Passes are registered via `AddCallbackPass` and toggled with `SetPassEnabled`. The deferred path is disabled by default; enabling it requires disabling the forward geometry pass and enabling the deferred passes. ```cpp -enum class RenderingMode : uint8_t { - Forward, // default; GeometryPass + LightingPass - Deferred, // GBufferPass + DeferredLightingPass + TransparentForwardPass -}; -``` +void GraphicRenderer::Initialize(/* ... */) { + // Forward path (enabled by default) + m_render_graph->AddCallbackPass("DepthPrePass", &m_depth_pre_pass, true); + m_render_graph->AddCallbackPass("GeometryPass", &m_geometry_pass, true); + m_render_graph->AddCallbackPass("LightingPass", &m_lighting_pass, true); -`AppRenderPipeline` holds a `RenderingMode m_mode` field. At `Compile()` time, the pipeline registers the appropriate pass set: + // Deferred path (disabled by default) + m_render_graph->AddCallbackPass("GBufferPass", &m_gbuffer_pass, false); + m_render_graph->AddCallbackPass("DeferredLightingPass", &m_deferred_lighting_pass, false); + m_render_graph->AddCallbackPass("TransparentForwardPass", &m_transparent_pass, false); -```cpp -void AppRenderPipeline::Compile(RenderGraphBuilder& builder) { // Common passes (both modes) - builder.AddPass(); - builder.AddPass(); - - if (m_mode == RenderingMode::Deferred) { - builder.AddPass(); - builder.AddPass(); - builder.AddPass(); - } else { - builder.AddPass(); - builder.AddPass(); - builder.AddPass(); - } - - // Common post-processing (both modes) - builder.AddPass(); - builder.AddPass(); - builder.AddPass(); + m_render_graph->AddCallbackPass("BloomPass", &m_bloom_pass, true); + m_render_graph->AddCallbackPass("TonemapPass", &m_tonemap_pass, true); + m_render_graph->AddCallbackPass("UIPass", &m_ui_pass, true); } ``` -The RenderGraph resolves resource dependencies from the declared `Setup` inputs/outputs, so pass ordering is handled automatically after the registration order establishes the topology. +To switch to the deferred path at runtime, disable the forward geometry group and enable the deferred group: + +```cpp +m_render_graph->SetPassEnabled("DepthPrePass", false); +m_render_graph->SetPassEnabled("GeometryPass", false); +m_render_graph->SetPassEnabled("LightingPass", false); + +m_render_graph->SetPassEnabled("GBufferPass", true); +m_render_graph->SetPassEnabled("DeferredLightingPass", true); +m_render_graph->SetPassEnabled("TransparentForwardPass", true); +``` + +A full RenderGraph recompile is triggered on pass enable/disable changes. This should not be done during gameplay. + +**`ZENGINE_DEFERRED` CMake flag:** not yet wired. When implemented, it will control whether the deferred passes are registered at all. Until then, deferred pass registration is controlled entirely through `SetPassEnabled`. + +### 6.3 Descriptor Set Layout (DeferredLightingPass) + +``` +Set 0 — per-frame UBO: + binding 0: CameraData { mat4 view; mat4 proj; mat4 inv_view; mat4 inv_view_proj; vec3 cam_pos; } + binding 1: LightBuffer { uint light_count; Light lights[MAX_LIGHTS]; } + +Set 1 — G-buffer textures (sampler2D): + binding 0: gbuffer_albedo_ao + binding 1: gbuffer_normals_rough + binding 2: gbuffer_metallic_emissive + binding 3: hdr_depth + +Set 2 — shadow maps: + binding 0: shadow_map_directional + +Set 3 — light culling (storage buffers, when LightCullPass is implemented): + binding 0: light_grid (readonly SSBO) + binding 1: light_index_list (readonly SSBO) +``` --- @@ -264,7 +343,7 @@ The depth buffer is shared with the forward depth pre-pass and is not an additio At 4K, the G-buffer costs 96 MB of VRAM. This must be accounted for in the project's memory budget. Cross-reference: `memory-budget.md` should reserve a `RENDER_GBUFFER` budget line of 128 MB (worst-case 4K with some headroom). -If VRAM is constrained, `RenderingMode::Deferred` should be restricted to PC configurations with 8GB+ VRAM. Console targets with unified memory budgets require separate analysis. +If VRAM is constrained, the deferred path should be restricted to PC configurations with 8GB+ VRAM. Console targets with unified memory budgets require separate analysis. --- @@ -291,9 +370,9 @@ Motion vectors for TAA require writing a `"motion_vectors"` render target in `GB The deferred path is additive. Existing forward shaders are not modified. New deferred-path shaders (`gbuffer.vert.glsl`, `gbuffer.frag.glsl`, `deferred_lighting.frag.glsl`) are added alongside them. -**CMake flag:** `-DZENGINE_DEFERRED=ON` enables the deferred rendering path. When disabled (default), the deferred pass files are compiled but `AppRenderPipeline` defaults to `RenderingMode::Forward`. +`GraphicRenderer::Initialize` registers both the forward and deferred pass groups. Switching between them is a matter of toggling pass enabled state via `SetPassEnabled`, which triggers a RenderGraph recompile. See §6 for the full switching pattern. -**Runtime switching:** `RenderingMode` can be changed at runtime (for editor tooling and RenderDoc debugging) if the RenderGraph is rebuilt. A full RenderGraph recompile is triggered on mode change. This should not be done during gameplay. +**`ZENGINE_DEFERRED` CMake flag:** not yet wired. See §6. **Shader variants:** G-buffer fragment shaders are separate files, not `#ifdef` variants of the forward fragment shader. This avoids a combinatorial explosion of shader permutations and keeps both paths readable independently. @@ -301,8 +380,10 @@ The deferred path is additive. Existing forward shaders are not modified. New de ## 10. File Layout +The layout below is the target directory structure for the deferred rendering feature. It does not exist yet. Current rendering passes are located in `ZEngine/ZEngine/Rendering/Renderers/RendererPasses.h` and `ZEngine/ZEngine/Rendering/Renderers/RendererPasses.cpp`. + ``` -ZEngine/Rendering/Deferred/ +ZEngine/Rendering/Deferred/ -- target layout; not yet created GBufferPass.h GBufferPass.cpp DeferredLightingPass.h @@ -315,10 +396,6 @@ ZEngine/Rendering/Deferred/ deferred_lighting.frag.glsl deferred_common.glsl -- position reconstruction, G-buffer unpack helpers transparent_forward.frag.glsl - -ZEngine/Rendering/ - AppRenderPipeline.h -- RenderingMode enum, Compile() switch (modified) - AppRenderPipeline.cpp ``` --- @@ -330,8 +407,8 @@ ZEngine/Rendering/ - [ ] `DeferredLightingPass.h` / `DeferredLightingPass.cpp` — full-screen lighting with tile-culled light iteration - [ ] `deferred_lighting.frag.glsl` — G-buffer unpack, position reconstruction, PBR evaluation - [ ] `TransparentForwardPass.h` / `TransparentForwardPass.cpp` — forward pass for alpha-blended objects -- [ ] `RenderingMode` enum and `AppRenderPipeline::Compile()` switch -- [ ] `ZENGINE_DEFERRED` CMake flag +- [ ] `RenderingMode` enum and `GraphicRenderer::Initialize` deferred registration +- [ ] `ZENGINE_DEFERRED` CMake flag wired to pass registration - [ ] `memory-budget.md` updated with `RENDER_GBUFFER` budget line - [ ] FXAA post-process pass (or stub placeholder) - [ ] Integration test: scene with 50 point lights renders correctly in deferred mode diff --git a/ZEngine/docs/future-plan/render-graph-integration.md b/ZEngine/docs/future-plan/render-graph-integration.md index ec03ec41f..229eeb6ae 100644 --- a/ZEngine/docs/future-plan/render-graph-integration.md +++ b/ZEngine/docs/future-plan/render-graph-integration.md @@ -1,110 +1,133 @@ # ZEngine — Render Graph Integration Guide -**Priority:** P0 — Post-processing, shadows, UI, text, particles, and animation skinning all depend on this -**Status:** Design — documents the existing RenderGraph and specifies integration patterns for all new passes -**Based on:** Existing `RenderGraph.h/.cpp` (live, production-quality) +**Priority:** P0 — post-processing, shadows, UI, text, and particles all depend on this +**Status:** Partially implemented — render graph core complete; passes DepthPrePass, GbufferPass, SkyboxPass, GridPass implemented; LightingPass and post-process chain not started +**Files:** +``` +ZEngine/ZEngine/Rendering/Renderers/RenderGraph.h +ZEngine/ZEngine/Rendering/Renderers/RenderGraph.cpp +ZEngine/ZEngine/Rendering/Renderers/GraphicRenderer.h +ZEngine/ZEngine/Rendering/Renderers/GraphicRenderer.cpp +ZEngine/ZEngine/Rendering/Renderers/RendererPasses.h +ZEngine/ZEngine/Rendering/Renderers/RendererPasses.cpp +``` --- -## 1. What Already Exists +## 1. What Exists -`RenderGraph` lives at `ZEngine/Rendering/Renderers/RenderGraph.h` and `RenderGraph.cpp`. It is -production code, not a stub. The following capabilities are fully implemented and in use today. +`RenderGraph` is production code. The following capabilities are fully implemented. ### 1.1 Topological Sort -`RenderGraph::Compile()` builds a directed acyclic graph (DAG) from resource producer/consumer -relationships and runs a DFS post-order topological sort to produce `SortedNodesMap` — an -`Array` of pass names in dependency order. The sort detects cycles and logs an error -via `ZENGINE_CORE_ERROR` without crashing. `SortedNodesMap` is then traversed in-order for -Vulkan object creation (passes) and again in-order during `Execute()`. +`RenderGraph::Compile()` builds a DAG from resource producer/consumer relationships and runs a +DFS post-order topological sort to produce an execution order. The sort detects cycles and logs +an error via `ZENGINE_CORE_ERROR` without crashing. Passes are executed in sorted order during +`Execute()`. -The edge graph is built from resource declarations: if pass B lists resource `"hdr_color"` as -an input and the `ResourceMap` records `"hdr_color"` as produced by pass A, `Compile()` inserts -`B` into `A.EdgeNodes`. Pass A therefore precedes pass B in the sorted output. +Edges are built from resource declarations: if pass B declares a read on `"hdr_color"` and pass +A declared it as a write, `Compile()` places A before B in execution order. -### 1.2 Resource Declaration via ResourceBuilder +### 1.2 Resource Declaration via RenderGraphResourceBuilder (Setup phase only) -`RenderGraphResourceBuilder` is the write side. It populates `Graph->ResourceMap` and -`Graph->NodeMap` during the Setup phase. The following methods are implemented: +`RenderGraphResourceBuilderPtr` is the write side. All declarations must happen inside +`IRenderGraphCallbackPass::Setup()`. -| Method | Type stored | External flag | -|---|---|---| -| `CreateRenderTarget(name, TextureSpec)` | `ATTACHMENT` | false — graph owns the texture | -| `CreateTexture(name, TextureSpec)` | `TEXTURE` | false — graph owns the texture | -| `CreateTexture(name, filename)` | `TEXTURE` | false — async loaded | -| `AttachRenderTarget(name, TextureHandle)` | `ATTACHMENT` | true — caller owns | -| `AttachTexture(name, TextureHandle)` | `TEXTURE` | true — caller owns | -| `AttachBuffer(name, StorageBufferSetHandle)` | `BUFFER_SET` | true — caller owns | -| `AttachBuffer(name, UniformBufferSetHandle)` | `BUFFER_SET` | true — caller owns | -| `CreateBufferSet(name, BufferSetCreationType)` | `BUFFER_SET` | false — graph owns | -| `CreateRenderPassNode(RenderGraphRenderPassCreation)` | writes `NodeMap[name].Creation` | — | - -`CreateRenderPassNode` is the only method that writes into `NodeMap`; all others write -`ResourceMap`. A pass must call `CreateRenderPassNode` in Setup or the node will have no -`Creation` and `Compile()` will not build edges for it. - -Resource entries with `External = true` are not allocated or freed by the graph. External -resources survive `Dispose()` intact. - -### 1.3 Resource Query via ResourceInspector - -`RenderGraphResourceInspector` is the read side. All query methods auto-create a placeholder -entry if the name is not yet in `ResourceMap`, so they are safe to call before a resource is -fully declared (though the returned handle will be invalid until Setup is complete for the -producing pass). The available queries: +| Method | Description | +|---|---| +| `WriteColorAttachment(name, TextureSpecification)` → `RGResourceHandle` | Declares a transient color attachment written by this pass. Graph owns the texture. | +| `WriteDepthAttachment(name, TextureSpecification)` → `RGResourceHandle` | Declares a transient depth attachment written by this pass. Graph owns the texture. | +| `ReadTexture(name, binding_key = nullptr)` → `RGResourceHandle` | Declares a sampled texture read by this pass. | +| `ReadDepth(name)` → `RGResourceHandle` | Declares a depth resource read (depth test, no write). | +| `ImportRenderTarget(name, TextureHandle)` → `RGResourceHandle` | Registers an externally-owned render target. Graph does not own or free it. | +| `AttachRenderTarget(name, TextureHandle)` → `RGResourceHandle` | Attaches an already-imported RT by name. | -``` -GetRenderTarget(name) -> TextureHandle -GetTexture(name) -> TextureHandle -GetStorageBufferSet(name) -> StorageBufferSetHandle -GetVertexBufferSet(name) -> VertexBufferSetHandle -GetIndexBufferSet(name) -> IndexBufferSetHandle -GetBufferUniformSet(name) -> UniformBufferSetHandle -GetIndirectBufferSet(name) -> IndirectBufferSetHandle -GetResource(name) -> RenderGraphResource& -GetNode(name) -> RenderGraphNode& (asserts the node exists) -``` +`RGResourceHandle` is a typed index (`uint32_t Index`, `uint32_t Version`). Call `.Valid()` to +check before use. -### 1.4 Three-Phase Pass Lifecycle +External resources (imported or attached) survive `Dispose()` intact. + +### 1.3 Resource Query via RenderGraphResourceInspector (Compile + Execute) + +`RenderGraphResourceInspectorPtr` is the read side. -Every pass registered with `AddCallbackPass` goes through three phases driven by the graph: +| Method | Cost | Notes | +|---|---|---| +| `GetTextureHandle(RGResourceHandle)` → `TextureHandle` | O(1) | Preferred in Execute — no string lookup | +| `GetRenderTarget(cstring)` → `TextureHandle` | string lookup | Use in Compile for handles not stored from Setup | +| `GetTexture(cstring)` → `TextureHandle` | string lookup | | + +### 1.4 Three-Phase Pass Lifecycle ``` -RenderGraph::Setup() -> calls pass->Setup() for every registered node -RenderGraph::Compile() -> builds edge graph, sorts, then calls pass->Compile() for each node -RenderGraph::Execute() -> calls pass->Execute() for each enabled node in sorted order +RenderGraph::Setup() calls pass->Setup() for every registered pass +RenderGraph::Compile() builds edge graph, sorts, then calls pass->Compile() per pass +RenderGraph::Execute() calls pass->Execute() per enabled pass in sorted order ``` -The graph does not re-run Setup or Compile between frames unless `Resize()` is called. -`Execute()` is the only method called per-frame. +`Execute()` is the only method called per frame. `Resize()` triggers a full re-Compile. ### 1.5 Automatic Barrier Insertion -`RenderGraph::Execute()` inserts `VkImageMemoryBarrier` commands automatically before -invoking each pass's `Execute()`: +`Execute()` inserts `VkImageMemoryBarrier` commands before each pass based on `RGAccess` +entries in `kAccessTable`. `RuntimeState` tracks actual per-frame image layout starting from +UNDEFINED on frame 0; `CurrentState` is compile-time simulation only. -- Output attachments (`ATTACHMENT` type): transitioned to - `COLOR_ATTACHMENT_OPTIMAL` (color) or `DEPTH_STENCIL_ATTACHMENT_OPTIMAL` (depth). -- Input textures (`TEXTURE` type in `Inputs`): transitioned from - `COLOR_ATTACHMENT_OPTIMAL` (if the resource was produced as an attachment) or `UNDEFINED` - to `SHADER_READ_ONLY_OPTIMAL`. +- Color write attachments: transitioned to `COLOR_ATTACHMENT_OPTIMAL`. +- Depth write attachments: transitioned to `DEPTH_STENCIL_ATTACHMENT_OPTIMAL`. +- `DepthRead`: stays in `VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL` + (`depthWriteEnable=false` in pipeline). This is MoltenVK-compatible. +- Shader-read textures: transitioned to `SHADER_READ_ONLY_OPTIMAL`. -Passes must not insert redundant barriers for these resources. Passes that use resources in -ways the graph cannot infer (e.g., storage image writes in compute passes) must insert their -own barriers. +Passes must not insert redundant barriers for resources they declared in Setup. Passes that use +resources in ways the graph cannot infer (e.g., storage image writes in compute) must insert +their own barriers. -### 1.6 AddCallbackPass +### 1.6 RenderGraph Public API ```cpp -void RenderGraph::AddCallbackPass(cstring pass_name, - IRenderGraphCallbackPass* const pass_callback, - bool enabled = true); +void Initialize(VulkanDevicePtr device, SceneDataPtr data = nullptr); +void AddCallbackPass(cstring name, IRenderGraphCallbackPass* cb, bool enabled = true); +RGPass* GetPass(cstring name); // O(1) name lookup; use for config/enable, not Execute +void SetPassEnabled(cstring name, bool enabled); +RGResourceHandle ImportRenderTarget(cstring name, TextureHandle handle); +void Setup(); +void Compile(); +void Execute(CommandBufferPtr cb); +void Resize(uint32_t w, uint32_t h); +void Dispose(); ``` -Registers `pass_callback` under `pass_name` in `NodeMap`. The `Enabled` flag is stored in -`RenderGraphNode::Enabled` and checked each frame in `Execute()`. Passes may be registered -in any order; `Compile()` determines execution order from the resource graph. +### 1.7 Key Types + +```cpp +struct RGResourceHandle { uint32_t Index = UINT32_MAX; uint32_t Version = 0; bool Valid() const; }; + +enum class RGAccess : uint8_t { + None, ColorWrite, DepthWrite, DepthRead, + ShaderRead, ShaderReadWrite, TransferRead, TransferWrite, Present +}; + +struct RGPass { + cstring Name; + bool Enabled; + IRenderGraphCallbackPass* Callback; + RenderPass* Handle; + FramebufferVNext* Framebuffer; + Array Reads; + Array Writes; +}; + +struct RGResource { + cstring Name; + RGResourceKind Kind; + bool External; + TextureHandle TextureHandle; + RGResourceState CurrentState; // compile-time simulation + RGResourceState RuntimeState; // actual per-frame layout + TextureSpecification Spec; +}; +``` --- @@ -112,160 +135,118 @@ in any order; `Compile()` determines execution order from the resource graph. ### 2.1 Setup -Called once: `RenderGraph::Setup()` iterates `NodeMap` and calls each pass's `Setup()`. +Called once: `RenderGraph::Setup()` calls each pass's `Setup()`. A pass's `Setup()` must: -- Call `builder->CreateRenderPassNode(RenderGraphRenderPassCreation{...})` to register its - name, inputs, and outputs with the graph. This is mandatory — without it, no edges are - built and the pass may execute in arbitrary order. -- Declare every output resource it produces via `builder->CreateRenderTarget`, - `builder->CreateTexture`, or `builder->CreateBufferSet`. Resources declared here will be - allocated by the graph in `Compile()`. -- Attach external resources it consumes via `builder->AttachBuffer` or - `builder->AttachRenderTarget` if those resources are owned outside the graph. +- Declare every resource it writes via `WriteColorAttachment` or `WriteDepthAttachment`. +- Declare every resource it reads via `ReadTexture` or `ReadDepth`. +- Attach external resources via `ImportRenderTarget` or `AttachRenderTarget`. +- Store returned `RGResourceHandle` values as members for use in Compile and Execute. A pass's `Setup()` must not: -- Create any Vulkan objects (`VkPipeline`, `VkRenderPass`, `VkFramebuffer`, `VkDescriptorSet`, - or any `vk*` handle). The graph has not yet sorted or allocated resources. -- Call `inspector->GetRenderTarget()` and use the returned handle as a valid texture — it - will be invalid until `Compile()` runs. -- Allocate memory from the heap. Use `ZPushStruct` / `ZPushArray` on the device arena if - pass-lifetime allocations are needed. - -Arena allocation macros used in passes: +- Create any Vulkan objects (`VkPipeline`, `VkRenderPass`, `VkFramebuffer`, + `VkDescriptorSet`, or any `vk*` handle). +- Use returned handles as valid textures — allocation happens in `Compile()`. + +Arena allocation macros for pass-lifetime data: ``` -ZPushStruct(arena, Type) — allocates sizeof(Type), default-constructs -ZPushStructCtor(arena, Type) — allocates and zero-inits -ZPushStructCtorArgs(arena, Type, ...) — allocates and constructs with args -ZPushArray(arena, Type, count) — allocates count × sizeof(Type) +ZPushStruct(arena, Type) +ZPushStructCtor(arena, Type) +ZPushStructCtorArgs(arena, Type, ...) +ZPushArray(arena, Type, count) ``` - -All defined in `ZEngineDef.h`. Pass lifetime data goes on `Device->Arena`. -Per-frame data goes on a scratch arena obtained via `ZGetScratch(Device->Arena)`. +All defined in `ZEngineDef.h`. Pass-lifetime allocations go on `Device->Arena`; +per-frame scratch allocations use `ZGetScratch(Device->Arena)`. ### 2.2 Compile -Called once (and again after `Resize()`): after sorting, `RenderGraph::Compile()` iterates -`SortedNodesMap` in order and calls each pass's `Compile()`. +Called once (and again after `Resize()`): after sorting, the graph calls each pass's +`Compile()`. -By the time `Compile()` is called for a given pass, all passes that produce the pass's inputs -have already had their resources allocated (because they precede this pass in sorted order). -This makes `inspector->GetRenderTarget("hdr_lit")` safe and guaranteed to return a valid handle -for any input that was declared in a preceding pass's Setup. +Before calling `pass->Compile()`, the graph pre-populates `RenderPassBuilder`: +- Each declared Write → `pass_builder->UseRenderTarget(handle)` +- Each declared Read → `pass_builder->AddInputAttachment(handle)` + +The `pass_builder` parameter is already populated when `Compile()` is entered. Passes call +`pass_builder->SetPipelineName(...)`, `pass_builder->UseShader(...)`, etc. on it. A pass's `Compile()` must: -- Read input resource handles from `inspector` to wire framebuffer attachments, descriptor set - bindings, and pipeline layout bindings. -- Create all Vulkan objects this pass owns: `VkRenderPass`, `VkFramebuffer`, - `VkDescriptorSetLayout`, `VkPipelineLayout`, `VkPipeline`. -- Store those handles as private members (allocated from the device arena, not `new`). -- Write the compiled `RenderPass` pointer to `*output_pass`. The graph reads this pointer to - build `node.Handle`, which it subsequently uses for framebuffer creation. +- Read input handles from `res_inspector` when they were not stored in Setup. +- Create all Vulkan objects this pass owns: render pass, framebuffer, descriptor sets, + pipeline layout, pipeline. Store them as arena-allocated members. +- Write the compiled `RenderPass` pointer to `*output_pass`. A pass's `Compile()` must not: -- Call `builder->CreateRenderTarget` or any other builder method. The declaration phase is over. +- Call any builder methods. The declaration phase is over. - Record Vulkan commands. -- Block on GPU completion (`vkDeviceWaitIdle`, `vkQueueWaitIdle`). +- Block on GPU completion. ### 2.3 Execute -Called once per frame: `RenderGraph::Execute()` iterates `SortedNodesMap`, inserts barriers, -and calls each enabled pass's `Execute()`. +Called once per frame: the graph inserts barriers and calls each enabled pass's `Execute()`. A pass's `Execute()` must: -- Record all Vulkan commands needed for this pass into `command_buffer`. -- Call `vkCmdBeginRenderPass` / `vkCmdEndRenderPass` (or the ZEngine command buffer wrapper - equivalents) around draw calls for graphic passes. -- Use `inspector->GetRenderTarget()` or `inspector->GetStorageBufferSet()` to read the - current frame's resource handles. Do not cache handles across frames if they could be - invalidated by a resize. +- Record all Vulkan commands into `command_buffer`. +- Call begin/end render pass around draw calls for graphic passes. +- Use `res_inspector->GetTextureHandle(handle)` (preferred) or + `res_inspector->GetRenderTarget(name)` to get the current frame's texture handles. A pass's `Execute()` must not: -- Create or destroy Vulkan objects. Resource creation belongs in Compile. +- Create or destroy Vulkan objects. - Call any builder methods. -- Access `SortedNodesMap` or `NodeMap` directly. --- -## 3. The Canonical Frame Pass Order +## 3. Canonical Frame Pass Order -The following table lists every pass that will be registered in `AppRenderPipeline::Initialize`, -in the order that satisfies dependencies. `Compile()` enforces this order via the DAG regardless -of `AddCallbackPass` call order, but the table is the canonical reference for what produces what. +The table below lists every pass in dependency order. Columns note current implementation +status. Passes with no data dependency on each other (e.g., shadow passes) may be reordered +by the graph within their tier. ``` -Pass name string Category Produces Consumes -────────────────────────────────────────────────────────────────────────────────────────────────── -"DepthPrePass" Geometry hdr_depth scene geometry -"ShadowPassDir_0" Shadow (CSM) shadow_dir_0 scene geometry -"ShadowPassDir_1" Shadow (CSM) shadow_dir_1 scene geometry -"ShadowPassDir_2" Shadow (CSM) shadow_dir_2 scene geometry -"ShadowPassDir_3" Shadow (CSM) shadow_dir_3 scene geometry -"ShadowPassSpot_0" Shadow (spot) shadow_spot_0 scene geometry -"ShadowPassSpot_1" Shadow (spot) shadow_spot_1 scene geometry -"ShadowPassSpot_2" Shadow (spot) shadow_spot_2 scene geometry -"ShadowPassSpot_3" Shadow (spot) shadow_spot_3 scene geometry -"ShadowPassPoint_0" Shadow (point) shadow_point_0 scene geometry -"ShadowPassPoint_1" Shadow (point) shadow_point_1 scene geometry -"SkinningUploadPass" Animation bone_matrix_buffers CPU animation data -"GeometryPass" Scene geometry hdr_color, hdr_normals hdr_depth -"LightingPass" Deferred lighting hdr_lit hdr_color, hdr_normals, +Pass name string Category Produces Consumes Status +"Depth Pre-Pass" Geometry FrameDepth scene geometry Implemented +"G-Buffer Pass" Scene geometry FrameColor, gbuffer_normals FrameDepth Implemented +"Skybox Pass" Sky FrameColor (in-place) FrameDepth Implemented (disabled at startup) +"Grid Pass" Editor FrameColor (in-place) FrameDepth Implemented +"ShadowPassDir_0" Shadow (CSM) shadow_dir_0 scene geometry Not started +"ShadowPassDir_1" Shadow (CSM) shadow_dir_1 scene geometry Not started +"ShadowPassDir_2" Shadow (CSM) shadow_dir_2 scene geometry Not started +"ShadowPassDir_3" Shadow (CSM) shadow_dir_3 scene geometry Not started +"ShadowPassSpot_0" Shadow (spot) shadow_spot_0 scene geometry Not started +"ShadowPassSpot_1" Shadow (spot) shadow_spot_1 scene geometry Not started +"ShadowPassSpot_2" Shadow (spot) shadow_spot_2 scene geometry Not started +"ShadowPassSpot_3" Shadow (spot) shadow_spot_3 scene geometry Not started +"ShadowPassPoint_0" Shadow (point) shadow_point_0 scene geometry Not started +"ShadowPassPoint_1" Shadow (point) shadow_point_1 scene geometry Not started +"SkinningUploadPass" Animation bone_matrix_buffers CPU animation data Not started +"LightingPass" Deferred lighting hdr_lit hdr_color, hdr_normals, Not started hdr_depth, shadow_dir_0..3, shadow_spot_0..3, shadow_point_0..1 -"SSAOPass" Post-process ssao hdr_depth, hdr_normals -"BloomThresholdPass" Post-process bloom_threshold hdr_lit -"BloomDownsample_0" Post-process bloom_mip_0 bloom_threshold -"BloomDownsample_1" Post-process bloom_mip_1 bloom_mip_0 -"BloomDownsample_2" Post-process bloom_mip_2 bloom_mip_1 -"BloomDownsample_3" Post-process bloom_mip_3 bloom_mip_2 -"BloomDownsample_4" Post-process bloom_mip_4 bloom_mip_3 -"BloomUpsample_4" Post-process bloom_upsample_4 bloom_mip_4 -"BloomUpsample_3" Post-process bloom_upsample_3 bloom_upsample_4 -"BloomUpsample_2" Post-process bloom_upsample_2 bloom_upsample_3 -"BloomUpsample_1" Post-process bloom_upsample_1 bloom_upsample_2 -"BloomUpsample_0" Post-process bloom_upsample_0 bloom_upsample_1 -"ToneMappingPass" Post-process ldr_color hdr_lit, - bloom_upsample_0, +"SSAOPass" Post-process ssao hdr_depth, hdr_normals Not started +"BloomThresholdPass" Post-process bloom_threshold hdr_lit Not started +"BloomDownsample_0..4" Post-process bloom_mip_0..4 bloom_threshold/prev Not started +"BloomUpsample_0..4" Post-process bloom_upsample_0..4 bloom_mip/prev Not started +"ToneMappingPass" Post-process ldr_color hdr_lit, bloom_upsample_0, Not started ssao -"FXAAPass" Post-process ldr_fxaa ldr_color -"UIPass" UI ldr_final ldr_fxaa -"TextPass" Text ldr_final (in-place) ldr_final -"OverlayPass" ImGui / editor swapchain image ldr_final +"FXAAPass" Post-process ldr_fxaa ldr_color Not started +"UIPass" UI ldr_final ldr_fxaa Not started +"TextPass" Text ldr_final (in-place) ldr_final Not started +"OverlayPass" ImGui / editor swapchain image ldr_final Not started ``` -The depth pre-pass, shadow passes, and skinning upload pass have no data dependency on each -other and the graph may reorder them within their tier. The ordering constraint that matters is: -`GeometryPass` must follow `DepthPrePass`; `LightingPass` must follow all shadow passes and -`GeometryPass`; tone mapping must follow SSAO and all bloom upsample passes. - -### Resource consumption map (all produced resources must have a consumer) - -``` -hdr_depth → GeometryPass (depth test), SSAOPass (occlusion), LightingPass -hdr_color → LightingPass (albedo input) -hdr_normals → LightingPass (normal input), SSAOPass -hdr_lit → BloomThresholdPass, ToneMappingPass -shadow_dir_{0..3} → LightingPass (CSM shadow lookup) -shadow_spot_{0..3} → LightingPass (spot shadow lookup) -shadow_point_{0..1}→ LightingPass (point shadow lookup) -bone_matrix_buffers→ GeometryPass (skinned mesh vertex shader reads bone matrices) -ssao → LightingPass (multiplied into ambient term) -bloom_threshold → BloomDownsample_0 -bloom_mip_{0..4} → BloomDownsample_{n+1} or BloomUpsample_0 -bloom_upsample_{0..4} → BloomUpsample_{n+1} or ToneMappingPass (bloom composite) -ldr_color → FXAAPass -ldr_fxaa → UIPass -ldr_final → OverlayPass → swapchain present -``` +Ordering constraints: `G-Buffer Pass` must follow `Depth Pre-Pass`; `LightingPass` must follow +all shadow passes and `G-Buffer Pass`; tone mapping must follow SSAO and all bloom upsample +passes. --- ## 4. Resource Naming Conventions All passes must use exactly these names when declaring or consuming shared resources. The graph -is string-keyed; a typo creates a second disconnected resource entry rather than a compile error. +is string-keyed; a typo creates a disconnected resource entry rather than a compile error. | Name | Format | Notes | |---|---|---| @@ -276,7 +257,7 @@ is string-keyed; a typo creates a second disconnected resource entry rather than | `"ldr_color"` | `VK_FORMAT_R8G8B8A8_UNORM` | After tone mapping, full resolution | | `"ldr_fxaa"` | `VK_FORMAT_R8G8B8A8_UNORM` | After FXAA, full resolution | | `"ldr_final"` | `VK_FORMAT_R8G8B8A8_UNORM` | After UI and text, full resolution | -| `"shadow_dir_0"` .. `"shadow_dir_3"` | `VK_FORMAT_D32_SFLOAT` | CSM cascades 0-3, 2048x2048 each | +| `"shadow_dir_0"` .. `"shadow_dir_3"` | `VK_FORMAT_D32_SFLOAT` | CSM cascades, 2048x2048 each | | `"shadow_spot_0"` .. `"shadow_spot_3"` | `VK_FORMAT_D32_SFLOAT` | Spot shadow maps, 1024x1024 each | | `"shadow_point_0"` .. `"shadow_point_1"` | `VK_FORMAT_D32_SFLOAT` | Point light cube maps, 512x512 per face | | `"ssao"` | `VK_FORMAT_R8_UNORM` | SSAO occlusion, full resolution | @@ -285,25 +266,18 @@ is string-keyed; a typo creates a second disconnected resource entry rather than | `"bloom_upsample_0"` .. `"bloom_upsample_4"` | `VK_FORMAT_R16G16B16A16_SFLOAT` | Upsample chain | | `"bone_matrix_buffers"` | `BUFFER_SET / STORAGE` | Per-bone world matrices for skinning | -Shadow map texture specs must set `Width` and `Height` to their fixed sizes (2048, 1024, 512) -rather than 0. Only render targets that should track the window size use `Width = 0, Height = 0`. - -### Resource name collision +Shadow map specs must set `Width` and `Height` to their fixed sizes (2048, 1024, 512) rather +than 0. Only render targets that track the window size use `Width = 0, Height = 0`. -Resource names are globally unique within a `RenderGraph` instance. All canonical -names (Section 4) are reserved. Custom passes must use unique names; a recommended -convention is: `"_"` e.g. `"mymod_custom_bloom"`. - -Duplicate resource names cause `ZENGINE_VALIDATE_ASSERT` in debug builds during -`RenderGraph::Compile()`. In release builds, the behavior is undefined (second -declaration silently overwrites the first). +Resource names are globally unique within a `RenderGraph` instance. Custom passes must use +unique names; recommended convention: `"_"`, e.g. `"mymod_custom_bloom"`. --- ## 5. How to Write a New Pass -The following is the complete, minimal skeleton for a new pass. Replace type and member names -with pass-specific names; do not copy-paste the comment strings. +Minimal skeleton for a new pass. `m_color_handle` and `m_input_handle` are stored from Setup +and reused in Compile and Execute. ```cpp // MyPass.h @@ -315,129 +289,112 @@ namespace ZEngine::Rendering::Renderers { struct MyCustomPass final : public IRenderGraphCallbackPass { - // --- Phase 1: Setup -------------------------------------------------- void Setup( Hardwares::VulkanDevicePtr const device, cstring name, - RenderGraphResourceBuilderPtr const builder, - RenderGraphResourceInspectorPtr inspector) override + RenderGraphResourceBuilderPtr const res_builder, + RenderGraphResourceInspectorPtr res_inspector) override { - // 1a. Declare outputs this pass produces. - builder->CreateRenderTarget("ldr_color", Specifications::TextureSpecification{ - .Width = 0, // 0 = match swapchain size; tracked by Resize() - .Height = 0, - .Format = VK_FORMAT_R8G8B8A8_UNORM, - .Usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, - }); - - // 1b. Register the render pass node (inputs + outputs). - builder->CreateRenderPassNode(RenderGraphRenderPassCreation{ - .Name = name, - .Inputs = { - { .Name = "hdr_lit", - .Type = RenderGraphResourceType::TEXTURE }, - }, - .Outputs = { - { .Name = "ldr_color", - .Type = RenderGraphResourceType::ATTACHMENT }, - }, - }); + m_color_handle = res_builder->WriteColorAttachment("ldr_color", + Specifications::TextureSpecification{ + .Width = 0, + .Height = 0, + .Format = VK_FORMAT_R8G8B8A8_UNORM, + .Usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT + | VK_IMAGE_USAGE_SAMPLED_BIT, + }); + m_input_handle = res_builder->ReadTexture("hdr_lit"); } - // --- Phase 2: Compile ------------------------------------------------ void Compile( Hardwares::VulkanDevicePtr const device, - Scenes::SceneDataPtr const scene, + Rendering::Scenes::SceneDataPtr const scene, RenderPasses::RenderPassBuilder* pass_builder, - RenderGraphResourceInspectorPtr inspector, + RenderGraphResourceInspectorPtr res_inspector, RenderPasses::RenderPass** const output_pass) override { - // 2a. Read input handles — safe here because producer passes ran first. - m_input_handle = inspector->GetRenderTarget("hdr_lit"); - - // 2b. Wire the pass builder (already pre-wired by RenderGraph::Compile - // before this call — see note below). - - // 2c. Build the render pass, pipeline, descriptor sets. - // Store handles as arena-allocated members, never raw new/delete. - // Write the RenderPass pointer to output_pass when done. - // *output_pass = ; + // pass_builder is pre-populated: UseRenderTarget called for each Write, + // AddInputAttachment called for each Read. Configure the rest here. + pass_builder->SetPipelineName("my_pass_pipeline"); + pass_builder->UseShader("my_pass.vert", "my_pass.frag"); + + // Build render pass, pipeline, descriptor sets. + // Store handles as arena-allocated members. + // *output_pass = ; } - // --- Phase 3: Execute ------------------------------------------------ void Execute( Hardwares::VulkanDevicePtr const device, - RenderGraphResourceInspectorPtr inspector, - Scenes::SceneDataPtr const scene, + RenderGraphResourceInspectorPtr res_inspector, + Rendering::Scenes::SceneDataPtr const scene, RenderPasses::RenderPass* const pass, Buffers::FramebufferVNext* const framebuffer, - Hardwares::CommandBufferPtr const cmd) override + Hardwares::CommandBufferPtr const command_buffer) override { - // 3a. Record commands. - cmd->BeginRenderPass(pass, framebuffer); - { - cmd->SetViewport(pass->GetRenderAreaWidth(), pass->GetRenderAreaHeight()); - cmd->SetScissor(pass->GetRenderAreaWidth(), pass->GetRenderAreaHeight()); - cmd->BindPipeline(Specifications::PipelineBindPoint::GRAPHIC, pass->Pipeline); - cmd->BindDescriptorSets(device->SwapchainPtr->CurrentFrame->Index); + // O(1) handle lookup — prefer GetTextureHandle over GetTexture in Execute. + Textures::TextureHandle input = res_inspector->GetTextureHandle(m_input_handle); - // Full-screen triangle: 3 vertices, no vertex buffer. - cmd->Draw(3, 1, 0, 0); + command_buffer->BeginRenderPass(pass, framebuffer); + { + command_buffer->SetViewport(pass->GetRenderAreaWidth(), pass->GetRenderAreaHeight()); + command_buffer->SetScissor(pass->GetRenderAreaWidth(), pass->GetRenderAreaHeight()); + command_buffer->BindPipeline(Specifications::PipelineBindPoint::GRAPHIC, pass->Pipeline); + command_buffer->BindDescriptorSets(device->SwapchainPtr->CurrentFrame->Index); + command_buffer->Draw(3, 1, 0, 0); // full-screen triangle } - cmd->EndRenderPass(); + command_buffer->EndRenderPass(); } + void Deinitialize(Hardwares::VulkanDevicePtr const device) override {} + private: - Textures::TextureHandle m_input_handle = {}; - // Pipeline, descriptor set layout, etc. stored as arena pointers. + RGResourceHandle m_color_handle = {}; + RGResourceHandle m_input_handle = {}; }; } ``` -Note on the builder pre-wiring: `RenderGraph::Compile()` calls -`RenderPassBuilder->UseRenderTarget()` and `RenderPassBuilder->AddInputTexture()` for each -output and input declared in the node's `Creation` before calling `pass->Compile()`. The -`RenderPassBuilder` is therefore already populated when `Compile()` is entered. Passes should -read from it through the `pass_builder` parameter rather than querying the inspector redundantly -for attachment handles that are already wired. - --- ## 6. The Main Lighting Pass -`LightingPass` is the central deferred lighting pass. It is not yet implemented but is -referenced by every post-process and shadow design doc. This section is the authoritative spec. +`LightingPass` is not yet implemented. This section is the authoritative spec. **Pass name string:** `"LightingPass"` ### 6.1 Inputs -| Input name | RenderGraphResourceType | Descriptor set | Binding | -|---|---|---|---| -| `"hdr_color"` | `TEXTURE` | set 3 | binding 0 | -| `"hdr_normals"` | `TEXTURE` | set 3 | binding 1 | -| `"hdr_depth"` | `TEXTURE` | set 3 | binding 2 | -| `"shadow_dir_0"` | `TEXTURE` | set 2 | binding 0 | -| `"shadow_dir_1"` | `TEXTURE` | set 2 | binding 1 | -| `"shadow_dir_2"` | `TEXTURE` | set 2 | binding 2 | -| `"shadow_dir_3"` | `TEXTURE` | set 2 | binding 3 | -| `"shadow_spot_0"` | `TEXTURE` | set 2 | binding 4 | -| `"shadow_spot_1"` | `TEXTURE` | set 2 | binding 5 | -| `"shadow_spot_2"` | `TEXTURE` | set 2 | binding 6 | -| `"shadow_spot_3"` | `TEXTURE` | set 2 | binding 7 | -| `"shadow_point_0"` | `TEXTURE` | set 2 | binding 8 | -| `"shadow_point_1"` | `TEXTURE` | set 2 | binding 9 | - -### 6.2 Outputs - -| Output name | RenderGraphResourceType | Format | +| Input name | Descriptor set | Binding | |---|---|---| -| `"hdr_lit"` | `ATTACHMENT` | `VK_FORMAT_R16G16B16A16_SFLOAT` | +| `"hdr_color"` | set 3 | binding 0 | +| `"hdr_normals"` | set 3 | binding 1 | +| `"hdr_depth"` | set 3 | binding 2 | +| `"shadow_dir_0"` | set 2 | binding 0 | +| `"shadow_dir_1"` | set 2 | binding 1 | +| `"shadow_dir_2"` | set 2 | binding 2 | +| `"shadow_dir_3"` | set 2 | binding 3 | +| `"shadow_spot_0"` | set 2 | binding 4 | +| `"shadow_spot_1"` | set 2 | binding 5 | +| `"shadow_spot_2"` | set 2 | binding 6 | +| `"shadow_spot_3"` | set 2 | binding 7 | +| `"shadow_point_0"` | set 2 | binding 8 | +| `"shadow_point_1"` | set 2 | binding 9 | + +All inputs are declared via `ReadTexture(name)` in Setup. Depth resources are declared via +`ReadDepth(name)` and stay in `DEPTH_STENCIL_ATTACHMENT_OPTIMAL` at runtime. + +### 6.2 Output + +| Output name | Format | +|---|---| +| `"hdr_lit"` | `VK_FORMAT_R16G16B16A16_SFLOAT` | + +Declared via `WriteColorAttachment("hdr_lit", spec)` in Setup. ### 6.3 Descriptor Set Layout ``` -Set 0 — Scene UBO (updated once per frame) +Set 0 — Scene UBO (once per frame) Binding 0: VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER struct SceneUBO { mat4 View; @@ -450,7 +407,7 @@ Set 0 — Scene UBO (updated once per frame) float FarPlane; }; -Set 1 — Light array UBO (updated when scene lights change) +Set 1 — Light array UBO (when scene lights change) Binding 0: VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER struct GpuDirectionLight { vec4 DirectionWS; vec4 Color; float Intensity; uint32_t CascadeCount; float _pad[2]; }; struct GpuPointLight { vec4 PositionWS; vec4 Color; float Intensity; float Radius; float _pad[2]; }; @@ -468,22 +425,22 @@ Set 1 — Light array UBO (updated when scene lights change) Set 2 — Shadow UBO + shadow map samplers Binding 0: VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER - struct CSMData { mat4 LightSpaceMatrices[4]; float CascadeSplits[4]; }; - struct SpotShadowData{ mat4 LightSpaceMatrix; }; + struct CSMData { mat4 LightSpaceMatrices[4]; float CascadeSplits[4]; }; + struct SpotShadowData { mat4 LightSpaceMatrix; }; struct PointShadowData{ float FarPlane; float _pad[3]; }; struct ShadowUBO { - CSMData Directional; - SpotShadowData Spot[4]; + CSMData Directional; + SpotShadowData Spot[4]; PointShadowData Point[2]; }; - Binding 1..4: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER — shadow_dir_0..3 - (VK_COMPARE_OP_LESS, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, - border color = (1,1,1,1)) - Binding 5..8: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER — shadow_spot_0..3 - Binding 9..10: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER — shadow_point_0..1 - (cube sampler, VK_IMAGE_VIEW_TYPE_CUBE) - -Set 3 — G-buffer textures (updated after GeometryPass) + Binding 1..4: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER — shadow_dir_0..3 + (VK_COMPARE_OP_LESS, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, + border color = (1,1,1,1)) + Binding 5..8: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER — shadow_spot_0..3 + Binding 9..10: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER — shadow_point_0..1 + (cube sampler, VK_IMAGE_VIEW_TYPE_CUBE) + +Set 3 — G-buffer textures (after G-Buffer Pass) Binding 0: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER — hdr_color Binding 1: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER — hdr_normals Binding 2: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER — hdr_depth @@ -492,11 +449,10 @@ Set 3 — G-buffer textures (updated after GeometryPass) ### 6.4 Draw Call -Full-screen triangle, no vertex buffer. The vertex shader generates clip-space positions -and UVs from `gl_VertexIndex` using the identity: +Full-screen triangle, no vertex buffer. The vertex shader generates clip-space positions and +UVs from `gl_VertexIndex`: ```glsl -// vertex shader void main() { vec2 uv = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); gl_Position = vec4(uv * 2.0 - 1.0, 0.0, 1.0); @@ -504,171 +460,68 @@ void main() { } ``` -`vkCmdDraw(cmd, 3, 1, 0, 0)` — three vertices, one instance, no index buffer, no vertex -buffer binding. +`vkCmdDraw(cmd, 3, 1, 0, 0)` — three vertices, one instance, no index buffer, no vertex buffer +binding. --- -## 6b. OverlayPass (Editor / ImGui) - -Currently implemented as `AppRenderPipeline::RenderOverlay()` which bypasses the -RenderGraph entirely. Migration into the graph is a v2 deliverable. - -When migrated: -- Name: `"OverlayPass"` -- Inputs: `"ldr_final"` (`TEXTURE`, read-only) -- Outputs: none (renders directly to swapchain image; declared as `REFERENCE` to swapchain) -- Descriptor set 0: ImGui font atlas (`sampler2D`) -- Draw calls: ImGui-generated vertex/index buffers, one draw per ImGui draw list -- Pipeline: alpha-blend enabled, no depth test, no depth write -- Only compiled and added to the graph in `ZENGINE_EDITOR` builds +## 7. How GraphicRenderer Registers Passes -Until migration is complete: `OverlayPass` is excluded from the canonical table -ordering. The graph executes all passes through `"ldr_final"`, then `RenderOverlay()` -runs as a manual step after graph `Execute()` in `AppRenderPipeline::EndFrame()`. - ---- - -## 7. How AppRenderPipeline Wires Passes Together - -`AppRenderPipeline::Initialize` constructs the `GraphicRenderer` which owns and initializes -the `RenderGraph`. All pass registrations happen inside `GraphicRenderer::Initialize` (or a -dedicated `RegisterPasses` helper called from there). The sequence is: +`GraphicRenderer::Initialize` is the orchestrator. It attaches external render targets and +registers passes before calling `Setup()` and `Compile()`: ```cpp -void GraphicRenderer::RegisterPasses(RenderGraphPtr render_graph) -{ - // Depth and shadow passes — no dependency on each other. - render_graph->AddCallbackPass("DepthPrePass", ZNew(DepthPrePass)); - render_graph->AddCallbackPass("ShadowPassDir_0", ZNew(ShadowPassDir, 0)); - render_graph->AddCallbackPass("ShadowPassDir_1", ZNew(ShadowPassDir, 1)); - render_graph->AddCallbackPass("ShadowPassDir_2", ZNew(ShadowPassDir, 2)); - render_graph->AddCallbackPass("ShadowPassDir_3", ZNew(ShadowPassDir, 3)); - render_graph->AddCallbackPass("ShadowPassSpot_0", ZNew(ShadowPassSpot, 0)); - render_graph->AddCallbackPass("ShadowPassSpot_1", ZNew(ShadowPassSpot, 1)); - render_graph->AddCallbackPass("ShadowPassSpot_2", ZNew(ShadowPassSpot, 2)); - render_graph->AddCallbackPass("ShadowPassSpot_3", ZNew(ShadowPassSpot, 3)); - render_graph->AddCallbackPass("ShadowPassPoint_0", ZNew(ShadowPassPoint, 0)); - render_graph->AddCallbackPass("ShadowPassPoint_1", ZNew(ShadowPassPoint, 1)); - render_graph->AddCallbackPass("SkinningUploadPass", ZNew(SkinningUploadPass)); - - // Geometry and deferred lighting. - render_graph->AddCallbackPass("GeometryPass", ZNew(GeometryPass)); - render_graph->AddCallbackPass("LightingPass", ZNew(LightingPass)); - - // Post-process chain. - // Passes are owned by PostProcessStack, not registered directly with the - // RenderGraph via AddCallbackPass. PostProcessStack::Compile() calls - // AddCallbackPass internally for each enabled pass in Order-sorted sequence. - // GraphicRenderer holds a PostProcessStack member (or pointer) initialised - // during engine startup. - m_post_process_stack.AddPass(MakeSSAOPass (arena, device, SSAOParams{})); - m_post_process_stack.AddPass(MakeBloomPass (arena, device, BloomParams{})); - m_post_process_stack.AddPass(MakeToneMappingPass (arena, device, ToneMappingParams{})); - m_post_process_stack.AddPass(MakeFXAAPass (arena, device, FXAAParams{})); - // Optional passes — disabled at startup; editor can enable at runtime. - m_post_process_stack.AddPass(MakeColorGradingPass (arena, device, ColorGradingParams{})); - m_post_process_stack.AddPass(MakeChromaticAberrationPass (arena, device, ChromaticAberrationParams{})); - m_post_process_stack.AddPass(MakeVignettePass (arena, device, VignetteParams{})); - m_post_process_stack.Compile(); - - // UI, text, editor overlay. - render_graph->AddCallbackPass("UIPass", ZNew(UIPass)); - render_graph->AddCallbackPass("TextPass", ZNew(TextPass)); - render_graph->AddCallbackPass("OverlayPass", ZNew(OverlayPass)); // ImGui - - render_graph->Setup(); - render_graph->Compile(); -} +RenderGraph->ResourceBuilder->AttachRenderTarget("FrameDepth", FrameDepthRenderTarget); +RenderGraph->ResourceBuilder->AttachRenderTarget("FrameColor", FrameColorRenderTarget); +RenderGraph->AddCallbackPass("Depth Pre-Pass", scene_depth_prepass); +RenderGraph->AddCallbackPass("G-Buffer Pass", gbuffer_pass); +RenderGraph->AddCallbackPass("Skybox Pass", skybox_pass, false); // disabled until sky config +RenderGraph->AddCallbackPass("Grid Pass", grid_pass); +RenderGraph->Setup(); +RenderGraph->Compile(); ``` -The call order to `AddCallbackPass` is irrelevant to execution order. `Compile()` determines -the actual execution order from the Inputs/Outputs declared by each pass in `Setup()`. The -listing above is alphabetically grouped by category only for readability. - -`AppRenderPipeline::RenderScene` calls `SceneRenderer->DrawScene(...)` which internally calls -`RenderGraph::Execute(command_buffer)`. The ImGui overlay bypasses the graph entirely today -(see `AppRenderPipeline::RenderOverlay`); the `OverlayPass` entry in the table above -represents a future migration of that code into the graph. +Registration order in `AddCallbackPass` is irrelevant to execution order; `Compile()` determines +order from the resource graph. --- ## 8. Pass Enable/Disable at Runtime -Post-process passes are toggled through `PostProcessStack`, not directly through the -`RenderGraph`. The stack owns pass lifetime and communicates enable state to the graph via -the `PostProcessPassData::Enabled` flag, which `Compile()` propagates to the corresponding -`AddCallbackPass` node. - -To disable a post-process pass at startup, set `Enabled = false` in the factory params or -call `SetEnabled` before `Compile()`: - -```cpp -// Option A — pass disabled params to the factory. -auto entry = MakeSSAOPass(arena, device, SSAOParams{}); -entry.Data.Enabled = false; -m_post_process_stack.AddPass(entry); - -// Option B — disable after AddPass, before Compile(). -m_post_process_stack.SetEnabled(StringHash("SSAOPass"), false); -``` - -To toggle a post-process pass after the graph is compiled (runtime): - -```cpp -m_post_process_stack.SetEnabled(StringHash("FXAAPass"), false); -// Takes effect on the next PostProcessStack::Execute() call — no re-compile needed. -``` - -For non-post-process passes (geometry, shadow, UI) the RenderGraph node can be toggled -directly via the node map: +Use `SetPassEnabled` and `GetPass` to toggle or configure passes after the graph is compiled: ```cpp -render_graph->NodeMap["DepthPrePass"].Enabled = false; +RenderGraph->SetPassEnabled("Skybox Pass", true); +auto* pass = RenderGraph->GetPass("Skybox Pass"); +if (pass) { + static_cast(pass->Callback)->EnvMapPath = path; +} ``` -Disabled passes are skipped in `Execute()`: their barriers are not emitted and their -`Execute()` callback is not called. Their resource producers still run normally. This means a -disabled pass's output resource exists and has been transitioned to the correct layout by its -preceding barriers; downstream passes that read from it will still work. +`GetPass` is O(1) by name and is intended for configuration, not for calling Execute. -Do not disable a pass whose output is consumed by multiple downstream passes unless all -consumers are also disabled. The resource will exist but contain stale data from a previous -frame (or the initial clear value). +Disabled passes are skipped in `Execute()`: their barriers are not emitted and their `Execute()` +callback is not called. Their declared output resources still exist in the graph. Do not disable +a pass whose output is consumed by downstream passes unless those consumers are also disabled. --- ## 9. Resize Handling -`RenderGraph::Resize(uint32_t width, uint32_t height)` is the only entry point for window -resize events. `AppRenderPipeline::ResizeRenderTarget` calls it: +`RenderGraph::Resize(uint32_t width, uint32_t height)` is the only entry point for window resize +events. For every registered pass: -```cpp -void AppRenderPipeline::ResizeRenderTarget(uint32_t w, uint32_t h) -{ - if (SceneRenderer && SceneRenderer->RenderGraph) - SceneRenderer->RenderGraph->Resize(w, h); -} -``` +1. Resources declared with `Width = 0, Height = 0` in their `TextureSpecification` are + re-created at the new `(width, height)`. +2. Resources declared with fixed dimensions (e.g., shadow maps at 2048) are recreated at their + fixed sizes. +3. The graph updates `FramebufferVNext` in place. +4. A full re-Compile runs after all resources are reallocated. -`Resize()` does the following for every node in `SortedNodesMap`: -1. Clears the node's input and output attachment lists on its `RenderPass::Specification`. -2. For each output with type `ATTACHMENT` (not `REFERENCE`): enqueues the old texture handle - for deferred disposal, allocates a new texture at the new dimensions using the stored - `TextureSpec`, and writes the new handle back into `ResourceMap`. Resources declared with - `Width = 0, Height = 0` are re-created at the new `(width, height)`. -3. Re-binds all input attachments and input textures on the `RenderPass::Specification`. -4. Calls `node.Handle->UpdateRenderTargets()` and `node.Handle->UpdateInputBinding()`. -5. Recreates the `FramebufferVNext` in place (arena-allocated, so no free needed). - -Shadow map resources use fixed `Width`/`Height` in their `TextureSpec` (e.g., 2048, 1024) -so they are recreated at their fixed sizes, not the window size. This is correct behaviour. - -Passes that hold their own copies of `VkFramebuffer` or `VkImageView` outside the -`FramebufferVNext` managed by the graph must detect the resize and recreate those objects. -The recommended pattern is to compare the stored handle against -`inspector->GetRenderTarget(name)` at the start of `Execute()` and rebuild if it differs. -Alternatively, subscribe to a resize callback emitted by `AppRenderPipeline`. +Passes that cache `VkFramebuffer` or `VkImageView` handles outside the graph-managed +`FramebufferVNext` must detect the resize. Recommended pattern: compare the stored handle +against `res_inspector->GetTextureHandle(m_handle)` at the start of `Execute()` and rebuild if +it differs. Alternatively, subscribe to the resize callback from `AppRenderPipeline`. --- @@ -677,72 +530,41 @@ Alternatively, subscribe to a resize callback emitted by `AppRenderPipeline`. `RenderGraph` has no internal synchronization. All of the following must be called from the render thread only: -- `AddCallbackPass` -- `Setup` -- `Compile` -- `Execute` -- `Resize` -- `Dispose` -- Any `ResourceInspector` or `ResourceBuilder` method - -The main thread (game logic thread) communicates with the render thread exclusively through -the `RenderPayload` mailbox in `AppRenderPipeline`. The mailbox is a three-slot ring buffer -protected by `PaddedAtomic` head/tail indices. The render thread reads the latest -committed payload at the start of each frame; the game thread writes into the next available -slot and advances the tail. - -Implication for new systems: if an ECS system or animation system wants to enable/disable a -render graph pass (e.g., enable particles when a particle emitter is active), it must write -that intent into the `RenderPayload` struct, not call `GetNode(...).Enabled` directly from -the game thread. +- `AddCallbackPass`, `Setup`, `Compile`, `Execute`, `Resize`, `Dispose` +- Any `RenderGraphResourceInspector` or `RenderGraphResourceBuilder` method ---- +The main thread communicates with the render thread exclusively through the `RenderPayload` +mailbox in `AppRenderPipeline`. The mailbox is a three-slot ring buffer protected by +`PaddedAtomic` head/tail indices. -## 11. File Layout - -No new files are needed to use the render graph. The existing files are: - -``` -ZEngine/Rendering/Renderers/RenderGraph.h — all public types and interfaces -ZEngine/Rendering/Renderers/RenderGraph.cpp — full implementation -ZEngine/Applications/AppRenderPipeline.h — pipeline, RenderPayload mailbox -ZEngine/Applications/AppRenderPipeline.cpp — BeginFrame / RenderScene / EndFrame -ZEngine/Rendering/Renderers/GraphicRenderer.h — owns RenderGraph, owns pass instances -``` - -New passes are added as new `.h`/`.cpp` files inside `ZEngine/Rendering/Renderers/` or a -subdirectory (e.g., `Renderers/PostProcess/`, `Renderers/Shadow/`). They implement -`IRenderGraphCallbackPass` and are registered in `GraphicRenderer::RegisterPasses`. - -This document is the missing specification for the graph itself. Individual passes are -specified in their own design docs listed in the deliverables checklist below. +If an ECS system or animation system wants to enable or disable a render graph pass, it must +write that intent into `RenderPayload`, not call `SetPassEnabled` from the game thread. --- -## 12. Deliverables Checklist - -The following passes need to be designed and implemented. Each links to its own design doc -where it exists. +## 11. Deliverables Checklist | Pass | Design doc | Status | |---|---|---| -| `DepthPrePass` | (no separate doc — trivial depth-only draw) | Not started | -| `ShadowPassDir_0..3` (CSM) | `shadows.md` | Designed | -| `ShadowPassSpot_0..3` | `shadows.md` | Designed | -| `ShadowPassPoint_0..1` | `shadows.md` | Designed | -| `SkinningUploadPass` | `animation-system.md` | Designed | -| `GeometryPass` | (currently implemented as part of `GraphicRenderer`) | Exists, needs migration | +| `Depth Pre-Pass` | — | Implemented | +| `G-Buffer Pass` | — | Implemented | +| `Skybox Pass` | — | Implemented (disabled at startup) | +| `Grid Pass` | — | Implemented | +| `ShadowPassDir_0..3` (CSM) | `shadows.md` | Not started | +| `ShadowPassSpot_0..3` | `shadows.md` | Not started | +| `ShadowPassPoint_0..1` | `shadows.md` | Not started | +| `SkinningUploadPass` | `animation-system.md` | Not started | | `LightingPass` | this document (section 6) | Not started | -| `SSAOPass` | `post-processing.md` | Designed | -| `BloomThresholdPass` | `post-processing.md` | Designed | -| `BloomDownsample_0..4` | `post-processing.md` | Designed | -| `BloomUpsample_0..4` | `post-processing.md` | Designed | -| `ToneMappingPass` | `post-processing.md` | Designed | -| `FXAAPass` | `post-processing.md` | Designed | -| `UIPass` | `ui-system.md` | Designed | -| `TextPass` | `text-rendering.md` | Designed | -| `OverlayPass` (ImGui migration) | (migration of existing `ImGUIRenderer`) | Not started | - -Implementation order recommendation: `LightingPass` first (unblocks all visual output), -then shadow passes (unblocks lighting quality), then SSAO and bloom (unblocks visual -polish), then `UIPass` and `TextPass` (unblocks in-engine UI), then `OverlayPass` migration. +| `SSAOPass` | `post-processing.md` | Not started | +| `BloomThresholdPass` | `post-processing.md` | Not started | +| `BloomDownsample_0..4` | `post-processing.md` | Not started | +| `BloomUpsample_0..4` | `post-processing.md` | Not started | +| `ToneMappingPass` | `post-processing.md` | Not started | +| `FXAAPass` | `post-processing.md` | Not started | +| `UIPass` | `ui-system.md` | Not started | +| `TextPass` | `text-rendering.md` | Not started | +| `OverlayPass` (ImGui migration) | — | Not started | + +Implementation order recommendation: `LightingPass` first (unblocks all visual output), then +shadow passes (unblocks lighting quality), then SSAO and bloom (unblocks visual polish), then +`UIPass` and `TextPass`, then `OverlayPass` migration.