diff --git a/scripts/cube_logic.lua b/scripts/cube_logic.lua index 62abc57..845b769 100644 --- a/scripts/cube_logic.lua +++ b/scripts/cube_logic.lua @@ -237,6 +237,125 @@ local function resolve_color3(value, fallback) return {fallback[1], fallback[2], fallback[3]} end +local function resolve_number_optional(value) + if type(value) == "number" then + return value + end + return nil +end + +local function resolve_color3_optional(value) + if type(value) == "table" then + local r = tonumber(value[1]) + local g = tonumber(value[2]) + local b = tonumber(value[3]) + if r and g and b then + return {r, g, b} + end + end + return nil +end + +local function format_optional_number(value) + if type(value) == "number" then + return string_format("%.3f", value) + end + return "default" +end + +local function format_optional_color(value) + if type(value) == "table" + and type(value[1]) == "number" + and type(value[2]) == "number" + and type(value[3]) == "number" then + return string_format("{%.2f, %.2f, %.2f}", value[1], value[2], value[3]) + end + return "default" +end + +local function build_shader_parameter_overrides() + if type(config) ~= "table" or type(config.atmospherics) ~= "table" then + return {} + end + + local atmospherics = config.atmospherics + local ambient_strength = resolve_number_optional(atmospherics.ambient_strength) + local light_intensity = resolve_number_optional(atmospherics.light_intensity) + local key_intensity = resolve_number_optional(atmospherics.key_light_intensity) + local fill_intensity = resolve_number_optional(atmospherics.fill_light_intensity) + local light_color = resolve_color3_optional(atmospherics.light_color) + local pbr_roughness = resolve_number_optional(atmospherics.pbr_roughness) + local pbr_metallic = resolve_number_optional(atmospherics.pbr_metallic) + + local parameters = {} + + local function apply_common(key) + if ambient_strength == nil and light_intensity == nil and light_color == nil then + return + end + local entry = {} + if ambient_strength ~= nil then + entry.ambient_strength = ambient_strength + end + if light_intensity ~= nil then + entry.light_intensity = light_intensity + end + if light_color ~= nil then + entry.light_color = light_color + end + parameters[key] = entry + end + + apply_common("solid") + apply_common("floor") + apply_common("wall") + apply_common("ceiling") + + if ambient_strength ~= nil or key_intensity ~= nil or fill_intensity ~= nil or light_intensity ~= nil then + local entry = {} + if ambient_strength ~= nil then + entry.ambient_strength = ambient_strength + end + if key_intensity ~= nil then + entry.key_intensity = key_intensity + elseif light_intensity ~= nil then + entry.key_intensity = light_intensity + end + if fill_intensity ~= nil then + entry.fill_intensity = fill_intensity + elseif light_intensity ~= nil then + entry.fill_intensity = light_intensity * 0.45 + end + parameters.default = entry + end + + if light_intensity ~= nil or light_color ~= nil or pbr_roughness ~= nil or pbr_metallic ~= nil then + local entry = {} + if light_intensity ~= nil then + entry.light_intensity = light_intensity + end + if light_color ~= nil then + entry.light_color = light_color + end + if pbr_roughness ~= nil then + entry.material_roughness = pbr_roughness + end + if pbr_metallic ~= nil then + entry.material_metallic = pbr_metallic + end + parameters.pbr = entry + end + + if next(parameters) ~= nil then + log_debug("Shader lighting overrides: ambient=%s light_intensity=%s light_color=%s", + format_optional_number(ambient_strength), + format_optional_number(light_intensity), + format_optional_color(light_color)) + end + + return parameters +end + local function apply_skybox_color_from_config() if type(config) ~= "table" then return @@ -335,6 +454,7 @@ end local function build_shader_variants() apply_skybox_color_from_config() + local shader_parameters = build_shader_parameter_overrides() local ok, toolkit = pcall(require, "shader_toolkit") if not ok then @@ -345,7 +465,7 @@ local function build_shader_variants() local output_mode = "source" local compile = false local ok_generate, generated = pcall(toolkit.generate_cube_demo_variants, - {compile = compile, output_mode = output_mode}) + {compile = compile, output_mode = output_mode, parameters = shader_parameters}) if not ok_generate then log_debug("Shader generation failed: %s", tostring(generated)) return build_static_shader_variants() diff --git a/src/services/impl/graphics_service.cpp b/src/services/impl/graphics_service.cpp index 79f72da..63a39ee 100644 --- a/src/services/impl/graphics_service.cpp +++ b/src/services/impl/graphics_service.cpp @@ -112,6 +112,9 @@ void GraphicsService::SetRenderGraphDefinition(const RenderGraphDefinition& defi ", passes=" + std::to_string(definition.passes.size())); renderGraphDefinition_ = definition; + if (backend_) { + backend_->SetRenderGraphDefinition(definition); + } } const RenderGraphDefinition& GraphicsService::GetRenderGraphDefinition() const { diff --git a/src/services/impl/gxm_graphics_backend.cpp b/src/services/impl/gxm_graphics_backend.cpp index 5796c45..c44ea0c 100644 --- a/src/services/impl/gxm_graphics_backend.cpp +++ b/src/services/impl/gxm_graphics_backend.cpp @@ -463,6 +463,10 @@ void GxmGraphicsBackend::SetViewProjection(const std::array& viewProj // Matrix will be set when drawing with specific pipeline } +void GxmGraphicsBackend::SetRenderGraphDefinition(const RenderGraphDefinition& definition) { + (void)definition; +} + void GxmGraphicsBackend::Draw(GraphicsDeviceHandle device, GraphicsPipelineHandle pipeline, GraphicsBufferHandle vertexBuffer, GraphicsBufferHandle indexBuffer, uint32_t indexCount, const std::array& modelMatrix) { diff --git a/src/services/impl/gxm_graphics_backend.hpp b/src/services/impl/gxm_graphics_backend.hpp index 4e49216..1a5b4b5 100644 --- a/src/services/impl/gxm_graphics_backend.hpp +++ b/src/services/impl/gxm_graphics_backend.hpp @@ -37,6 +37,7 @@ public: bool EndFrame(GraphicsDeviceHandle device) override; void SetViewProjection(const std::array& viewProj) override; + void SetRenderGraphDefinition(const RenderGraphDefinition& definition) override; void Draw(GraphicsDeviceHandle device, GraphicsPipelineHandle pipeline, GraphicsBufferHandle vertexBuffer, GraphicsBufferHandle indexBuffer, diff --git a/src/services/impl/pipeline_service.cpp b/src/services/impl/pipeline_service.cpp index ee5b38a..ed6457e 100644 --- a/src/services/impl/pipeline_service.cpp +++ b/src/services/impl/pipeline_service.cpp @@ -105,6 +105,10 @@ void PipelineService::Cleanup() { vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); pipelineLayout_ = VK_NULL_HANDLE; } + if (descriptorSetLayout_ != VK_NULL_HANDLE) { + vkDestroyDescriptorSetLayout(device, descriptorSetLayout_, nullptr); + descriptorSetLayout_ = VK_NULL_HANDLE; + } } void PipelineService::Shutdown() noexcept { @@ -131,21 +135,42 @@ void PipelineService::CreatePipelineLayout() { auto device = deviceService_->GetDevice(); + if (descriptorSetLayout_ != VK_NULL_HANDLE) { + vkDestroyDescriptorSetLayout(device, descriptorSetLayout_, nullptr); + descriptorSetLayout_ = VK_NULL_HANDLE; + } + if (pipelineLayout_ != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); + pipelineLayout_ = VK_NULL_HANDLE; + } + + VkDescriptorSetLayoutBinding samplerBinding{}; + samplerBinding.binding = 0; + samplerBinding.descriptorCount = 1; + samplerBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + samplerBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + VkDescriptorSetLayoutCreateInfo setLayoutInfo{}; + setLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + setLayoutInfo.bindingCount = 1; + setLayoutInfo.pBindings = &samplerBinding; + + if (vkCreateDescriptorSetLayout(device, &setLayoutInfo, nullptr, &descriptorSetLayout_) != VK_SUCCESS) { + throw std::runtime_error("Failed to create descriptor set layout"); + } + VkPushConstantRange pushRange{}; - pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; pushRange.offset = 0; pushRange.size = sizeof(core::PushConstants); VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + pipelineLayoutInfo.setLayoutCount = 1; + pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout_; pipelineLayoutInfo.pushConstantRangeCount = 1; pipelineLayoutInfo.pPushConstantRanges = &pushRange; - if (pipelineLayout_ != VK_NULL_HANDLE) { - vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); - pipelineLayout_ = VK_NULL_HANDLE; - } - if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout_) != VK_SUCCESS) { throw std::runtime_error("Failed to create pipeline layout"); } diff --git a/src/services/impl/pipeline_service.hpp b/src/services/impl/pipeline_service.hpp index 9ec1760..4a03307 100644 --- a/src/services/impl/pipeline_service.hpp +++ b/src/services/impl/pipeline_service.hpp @@ -33,6 +33,10 @@ public: logger_->Trace("PipelineService", "GetPipelineLayout"); return pipelineLayout_; } + VkDescriptorSetLayout GetDescriptorSetLayout() const override { + logger_->Trace("PipelineService", "GetDescriptorSetLayout"); + return descriptorSetLayout_; + } bool HasShader(const std::string& key) const override; size_t GetShaderCount() const override { logger_->Trace("PipelineService", "GetShaderCount"); @@ -47,6 +51,7 @@ private: std::shared_ptr logger_; VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE; + VkDescriptorSetLayout descriptorSetLayout_ = VK_NULL_HANDLE; std::unordered_map shaderPathMap_; std::unordered_map pipelines_; diff --git a/src/services/impl/render_command_service.cpp b/src/services/impl/render_command_service.cpp index 9bc3bfe..a06cf29 100644 --- a/src/services/impl/render_command_service.cpp +++ b/src/services/impl/render_command_service.cpp @@ -1,5 +1,6 @@ #include "render_command_service.hpp" #include "../../core/vertex.hpp" +#include #include #include @@ -46,6 +47,8 @@ void RenderCommandService::Cleanup() { logger_->Trace("RenderCommandService", "Cleanup"); CleanupCommandResources(); CleanupSyncObjects(); + CleanupRenderGraphResources(); + CleanupDescriptorResources(); } void RenderCommandService::Shutdown() noexcept { @@ -113,6 +116,10 @@ void RenderCommandService::RecordCommands(uint32_t imageIndex, VkCommandBufferBeginInfo beginInfo{}; beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; vkBeginCommandBuffer(commandBuffer, &beginInfo); + EnsureDummyImageLayout(commandBuffer); + + EnsureDescriptorResources(); + EnsureDummyImageLayout(commandBuffer); // Begin render pass auto framebuffers = swapchainService_->GetSwapchainFramebuffers(); @@ -161,6 +168,14 @@ void RenderCommandService::RecordCommands(uint32_t imageIndex, if (pipelineLayout == VK_NULL_HANDLE) { logger_->Error("RenderCommandService: Pipeline layout is not initialized"); } else { + if (defaultDescriptorSet_ == VK_NULL_HANDLE) { + logger_->Error("RenderCommandService: Default descriptor set not available"); + vkCmdEndRenderPass(commandBuffer); + vkEndCommandBuffer(commandBuffer); + return; + } + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + pipelineLayout, 0, 1, &defaultDescriptorSet_, 0, nullptr); if (logger_) { logger_->Trace("RenderCommandService", "RecordCommands", "drawing commands=" + std::to_string(commands.size())); @@ -215,7 +230,8 @@ void RenderCommandService::RecordCommands(uint32_t imageIndex, pushConstants.enableFog = 1; // Enable fog for PBR } - vkCmdPushConstants(commandBuffer, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, + vkCmdPushConstants(commandBuffer, pipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(core::PushConstants), &pushConstants); vkCmdDrawIndexed(commandBuffer, command.indexCount, 1, command.indexOffset, command.vertexOffset, 0); @@ -254,6 +270,308 @@ void RenderCommandService::RecordCommands(uint32_t imageIndex, vkEndCommandBuffer(commandBuffer); } +void RenderCommandService::RecordRenderGraph(uint32_t imageIndex, + const RenderGraphDefinition& graph, + const std::vector& commands, + const std::array& viewProj) { + logger_->Trace("RenderCommandService", "RecordRenderGraph", + "imageIndex=" + std::to_string(imageIndex) + + ", commands.size=" + std::to_string(commands.size()) + + ", resources=" + std::to_string(graph.resources.size()) + + ", passes=" + std::to_string(graph.passes.size())); + + EnsureRenderGraphResources(graph); + EnsureDescriptorResources(); + + VkCommandBuffer commandBuffer = commandBuffers_[imageIndex]; + vkResetCommandBuffer(commandBuffer, 0); + + VkCommandBufferBeginInfo beginInfo{}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + vkBeginCommandBuffer(commandBuffer, &beginInfo); + + VkPipelineLayout pipelineLayout = pipelineService_->GetPipelineLayout(); + if (pipelineLayout == VK_NULL_HANDLE) { + logger_->Error("RenderCommandService: Pipeline layout is not initialized"); + vkEndCommandBuffer(commandBuffer); + return; + } + + VkBuffer vertexBuffer = bufferService_->GetVertexBuffer(); + VkBuffer indexBuffer = bufferService_->GetIndexBuffer(); + bool sceneBuffersReady = vertexBuffer != VK_NULL_HANDLE && indexBuffer != VK_NULL_HANDLE; + if (sceneBuffersReady) { + VkDeviceSize offsets[] = {0}; + vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vertexBuffer, offsets); + vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT16); + } else { + logger_->Warn("RenderCommandService: Vertex or index buffer not initialized for render graph"); + } + + const auto& config = configService_->GetConfig(); + const auto& skyColor = config.atmospherics.skyColor; + + std::array sceneClear{}; + sceneClear[0].color = {{skyColor[0], skyColor[1], skyColor[2], 1.0f}}; + sceneClear[1].depthStencil = {1.0f, 0}; + + std::array postClear{}; + postClear[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; + postClear[1].depthStencil = {1.0f, 0}; + + auto framebuffers = swapchainService_->GetSwapchainFramebuffers(); + auto swapchainRenderPass = swapchainService_->GetRenderPass(); + auto extent = swapchainService_->GetSwapchainExtent(); + + bool swapchainUsed = false; + + for (const auto& pass : graph.passes) { + if (!IsPassEnabled(pass)) { + if (logger_) { + logger_->Trace("RenderCommandService", "RecordRenderGraph", + "Skipping disabled pass=" + pass.name); + } + continue; + } + if (pass.shader.empty()) { + if (logger_) { + logger_->Warn("RenderCommandService: Render graph pass '" + pass.name + "' has no shader"); + } + continue; + } + + std::string outputName = ResolvePassOutput(pass); + if (outputName.empty()) { + if (logger_) { + logger_->Warn("RenderCommandService: Render graph pass '" + pass.name + "' has no output"); + } + continue; + } + + bool outputSwapchain = outputName == "swapchain"; + VkRenderPass renderPass = outputSwapchain ? swapchainRenderPass : offscreenRenderPass_; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + VkExtent2D targetExtent = extent; + RenderGraphImage* target = nullptr; + + if (!outputSwapchain && renderPass == VK_NULL_HANDLE) { + if (logger_) { + logger_->Warn("RenderCommandService: Offscreen render pass is not initialized"); + } + continue; + } + + if (outputSwapchain) { + if (imageIndex >= framebuffers.size()) { + logger_->Error("RenderCommandService: Swapchain framebuffer index out of range"); + continue; + } + framebuffer = framebuffers[imageIndex]; + targetExtent = extent; + swapchainUsed = true; + } else { + target = FindRenderTarget(outputName); + if (!target || target->framebuffer == VK_NULL_HANDLE) { + if (logger_) { + logger_->Warn("RenderCommandService: Render target not found for '" + outputName + "'"); + } + continue; + } + framebuffer = target->framebuffer; + targetExtent = target->extent; + TransitionImageLayout(commandBuffer, *target, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_ASPECT_COLOR_BIT); + RenderGraphImage* depthTarget = FindDepthTarget(outputName); + if (depthTarget) { + TransitionImageLayout(commandBuffer, *depthTarget, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + VK_IMAGE_ASPECT_DEPTH_BIT); + } else if (logger_) { + logger_->Warn("RenderCommandService: Depth target not found for '" + outputName + "'"); + } + } + + VkRenderPassBeginInfo renderPassInfo{}; + renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + renderPassInfo.renderPass = renderPass; + renderPassInfo.framebuffer = framebuffer; + renderPassInfo.renderArea.offset = {0, 0}; + renderPassInfo.renderArea.extent = targetExtent; + + const auto& clearValues = IsScenePass(pass) ? sceneClear : postClear; + renderPassInfo.clearValueCount = static_cast(clearValues.size()); + renderPassInfo.pClearValues = clearValues.data(); + + vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE); + + bool scenePass = IsScenePass(pass); + if (!scenePass) { + if (!pipelineService_->HasShader(pass.shader)) { + if (logger_) { + logger_->Warn("RenderCommandService: Missing pipeline for shader key: " + pass.shader); + } + vkCmdEndRenderPass(commandBuffer); + continue; + } + + VkPipeline pipeline = pipelineService_->GetPipeline(pass.shader); + vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + } + + if (scenePass) { + if (defaultDescriptorSet_ == VK_NULL_HANDLE) { + if (logger_) { + logger_->Warn("RenderCommandService: Default descriptor set not available"); + } + vkCmdEndRenderPass(commandBuffer); + continue; + } + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + pipelineLayout, 0, 1, &defaultDescriptorSet_, 0, nullptr); + + if (!sceneBuffersReady) { + if (logger_) { + logger_->Warn("RenderCommandService: Skipping scene pass due to missing buffers"); + } + } else if (commands.empty()) { + if (logger_) { + logger_->Trace("RenderCommandService", "RecordRenderGraph", + "No scene commands for pass=" + pass.name); + } + } else { + size_t drawIndex = 0; + for (const auto& command : commands) { + if (!pipelineService_->HasShader(command.shaderKey)) { + logger_->Error("RenderCommandService: Missing pipeline for shader key: " + command.shaderKey); + continue; + } + + if (logger_) { + logger_->Trace("RenderCommandService", "RecordRenderGraph", + "pass=" + pass.name + + ", draw=" + std::to_string(drawIndex) + + ", shaderKey=" + command.shaderKey); + } + + VkPipeline scenePipeline = pipelineService_->GetPipeline(command.shaderKey); + vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, scenePipeline); + + core::PushConstants pushConstants{}; + pushConstants.model = command.modelMatrix; + pushConstants.viewProj = viewProj; + + if (command.shaderKey.find("pbr") != std::string::npos) { + pushConstants.view = {1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f}; + pushConstants.proj = pushConstants.view; + pushConstants.lightViewProj = pushConstants.view; + pushConstants.cameraPos = {0.0f, 0.0f, 0.0f}; + pushConstants.time = 0.0f; + pushConstants.ambientStrength = config.atmospherics.ambientStrength; + pushConstants.fogDensity = config.atmospherics.fogDensity; + pushConstants.fogStart = 0.0f; + pushConstants.fogEnd = 100.0f; + pushConstants.fogColor = config.atmospherics.fogColor; + pushConstants.gamma = config.atmospherics.gamma; + pushConstants.exposure = config.atmospherics.exposure; + pushConstants.enableShadows = config.atmospherics.enableShadows ? 1 : 0; + pushConstants.enableFog = 1; + } + + vkCmdPushConstants(commandBuffer, pipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, + sizeof(core::PushConstants), &pushConstants); + vkCmdDrawIndexed(commandBuffer, command.indexCount, 1, + command.indexOffset, command.vertexOffset, 0); + ++drawIndex; + } + } + } else { + std::string inputName = ResolvePassInput(pass); + if (inputName == "swapchain" && outputSwapchain) { + if (logger_) { + logger_->Trace("RenderCommandService", "RecordRenderGraph", + "Skipping feedback pass=" + pass.name); + } + vkCmdEndRenderPass(commandBuffer); + continue; + } + + bool hasInput = true; + VkImageView inputView = VK_NULL_HANDLE; + if (inputName.empty()) { + hasInput = false; + if (logger_) { + logger_->Warn("RenderCommandService: Pass '" + pass.name + "' missing input"); + } + } else if (inputName == "swapchain") { + hasInput = false; + if (logger_) { + logger_->Warn("RenderCommandService: Pass '" + pass.name + + "' cannot sample from swapchain output"); + } + } else { + RenderGraphImage* inputTarget = FindRenderTarget(inputName); + if (inputTarget) { + TransitionImageLayout(commandBuffer, *inputTarget, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_ASPECT_COLOR_BIT); + inputView = inputTarget->view; + } else { + hasInput = false; + if (logger_) { + logger_->Warn("RenderCommandService: Input target not found for '" + inputName + "'"); + } + } + } + + if (!hasInput || inputView == VK_NULL_HANDLE) { + vkCmdEndRenderPass(commandBuffer); + continue; + } + + if (postProcessDescriptorSet_ == VK_NULL_HANDLE) { + if (logger_) { + logger_->Warn("RenderCommandService: Post-process descriptor set not available"); + } + vkCmdEndRenderPass(commandBuffer); + continue; + } + + UpdateDescriptorSet(postProcessDescriptorSet_, inputView); + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + pipelineLayout, 0, 1, &postProcessDescriptorSet_, 0, nullptr); + + core::PushConstants pushConstants = BuildFullscreenConstants(pass); + vkCmdPushConstants(commandBuffer, pipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, + sizeof(core::PushConstants), &pushConstants); + vkCmdDraw(commandBuffer, 3, 1, 0, 0); + } + + if (outputSwapchain && guiRendererService_) { + const auto& images = swapchainService_->GetSwapchainImages(); + if (imageIndex < images.size() && images[imageIndex] != VK_NULL_HANDLE) { + guiRendererService_->RenderToSwapchain(commandBuffer, images[imageIndex]); + } + } + + vkCmdEndRenderPass(commandBuffer); + + if (!outputSwapchain && target) { + TransitionImageLayout(commandBuffer, *target, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_ASPECT_COLOR_BIT); + } + } + + if (!swapchainUsed && logger_) { + logger_->Warn("RenderCommandService: Render graph did not output to swapchain"); + } + + vkEndCommandBuffer(commandBuffer); +} + bool RenderCommandService::EndFrame(uint32_t imageIndex) { logger_->Trace("RenderCommandService", "EndFrame", "imageIndex=" + std::to_string(imageIndex)); @@ -414,4 +732,756 @@ void RenderCommandService::CleanupSyncObjects() { } } +void RenderCommandService::EnsureDescriptorResources() { + if (descriptorPool_ != VK_NULL_HANDLE) { + return; + } + if (logger_) { + logger_->Trace("RenderCommandService", "EnsureDescriptorResources"); + } + CreateDescriptorPool(); + CreateSampler(); + CreateDummyImage(); + AllocateDescriptorSets(); + UpdateDescriptorSet(defaultDescriptorSet_, dummyImageView_); + UpdateDescriptorSet(postProcessDescriptorSet_, dummyImageView_); +} + +void RenderCommandService::CleanupDescriptorResources() { + logger_->Trace("RenderCommandService", "CleanupDescriptorResources"); + + auto device = deviceService_->GetDevice(); + + if (dummyImageView_ != VK_NULL_HANDLE) { + vkDestroyImageView(device, dummyImageView_, nullptr); + dummyImageView_ = VK_NULL_HANDLE; + } + if (dummyImage_ != VK_NULL_HANDLE) { + vkDestroyImage(device, dummyImage_, nullptr); + dummyImage_ = VK_NULL_HANDLE; + } + if (dummyImageMemory_ != VK_NULL_HANDLE) { + vkFreeMemory(device, dummyImageMemory_, nullptr); + dummyImageMemory_ = VK_NULL_HANDLE; + } + if (sampler_ != VK_NULL_HANDLE) { + vkDestroySampler(device, sampler_, nullptr); + sampler_ = VK_NULL_HANDLE; + } + if (descriptorPool_ != VK_NULL_HANDLE) { + vkDestroyDescriptorPool(device, descriptorPool_, nullptr); + descriptorPool_ = VK_NULL_HANDLE; + } + + defaultDescriptorSet_ = VK_NULL_HANDLE; + postProcessDescriptorSet_ = VK_NULL_HANDLE; + dummyImageLayout_ = VK_IMAGE_LAYOUT_UNDEFINED; +} + +void RenderCommandService::CreateDescriptorPool() { + logger_->Trace("RenderCommandService", "CreateDescriptorPool"); + + auto device = deviceService_->GetDevice(); + + VkDescriptorPoolSize poolSize{}; + poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + poolSize.descriptorCount = 8; + + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + poolInfo.maxSets = 4; + + if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool_) != VK_SUCCESS) { + throw std::runtime_error("Failed to create descriptor pool"); + } +} + +void RenderCommandService::CreateSampler() { + logger_->Trace("RenderCommandService", "CreateSampler"); + + auto device = deviceService_->GetDevice(); + + VkSamplerCreateInfo samplerInfo{}; + samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerInfo.magFilter = VK_FILTER_LINEAR; + samplerInfo.minFilter = VK_FILTER_LINEAR; + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + samplerInfo.maxLod = 1.0f; + + if (vkCreateSampler(device, &samplerInfo, nullptr, &sampler_) != VK_SUCCESS) { + throw std::runtime_error("Failed to create sampler"); + } +} + +void RenderCommandService::CreateDummyImage() { + logger_->Trace("RenderCommandService", "CreateDummyImage"); + + auto device = deviceService_->GetDevice(); + + VkImageCreateInfo imageInfo{}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.extent.width = 1; + imageInfo.extent.height = 1; + imageInfo.extent.depth = 1; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateImage(device, &imageInfo, nullptr, &dummyImage_) != VK_SUCCESS) { + throw std::runtime_error("Failed to create dummy image"); + } + + VkMemoryRequirements memRequirements; + vkGetImageMemoryRequirements(device, dummyImage_, &memRequirements); + + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = deviceService_->FindMemoryType( + memRequirements.memoryTypeBits, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + + if (vkAllocateMemory(device, &allocInfo, nullptr, &dummyImageMemory_) != VK_SUCCESS) { + throw std::runtime_error("Failed to allocate dummy image memory"); + } + + vkBindImageMemory(device, dummyImage_, dummyImageMemory_, 0); + + VkImageViewCreateInfo viewInfo{}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = dummyImage_; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM; + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + viewInfo.subresourceRange.baseMipLevel = 0; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.baseArrayLayer = 0; + viewInfo.subresourceRange.layerCount = 1; + + if (vkCreateImageView(device, &viewInfo, nullptr, &dummyImageView_) != VK_SUCCESS) { + throw std::runtime_error("Failed to create dummy image view"); + } + + dummyImageLayout_ = VK_IMAGE_LAYOUT_UNDEFINED; +} + +void RenderCommandService::AllocateDescriptorSets() { + logger_->Trace("RenderCommandService", "AllocateDescriptorSets"); + + VkDescriptorSetLayout layout = pipelineService_->GetDescriptorSetLayout(); + if (layout == VK_NULL_HANDLE) { + logger_->Error("RenderCommandService: Descriptor set layout is not initialized"); + return; + } + + std::array layouts = {layout, layout}; + VkDescriptorSetAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = descriptorPool_; + allocInfo.descriptorSetCount = static_cast(layouts.size()); + allocInfo.pSetLayouts = layouts.data(); + + std::array sets{}; + if (vkAllocateDescriptorSets(deviceService_->GetDevice(), &allocInfo, sets.data()) != VK_SUCCESS) { + throw std::runtime_error("Failed to allocate descriptor sets"); + } + + defaultDescriptorSet_ = sets[0]; + postProcessDescriptorSet_ = sets[1]; +} + +void RenderCommandService::UpdateDescriptorSet(VkDescriptorSet set, VkImageView view) { + if (set == VK_NULL_HANDLE || view == VK_NULL_HANDLE || sampler_ == VK_NULL_HANDLE) { + return; + } + + VkDescriptorImageInfo imageInfo{}; + imageInfo.sampler = sampler_; + imageInfo.imageView = view; + imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + VkWriteDescriptorSet descriptorWrite{}; + descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrite.dstSet = set; + descriptorWrite.dstBinding = 0; + descriptorWrite.dstArrayElement = 0; + descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + descriptorWrite.descriptorCount = 1; + descriptorWrite.pImageInfo = &imageInfo; + + vkUpdateDescriptorSets(deviceService_->GetDevice(), 1, &descriptorWrite, 0, nullptr); +} + +void RenderCommandService::EnsureDummyImageLayout(VkCommandBuffer commandBuffer) { + if (dummyImage_ == VK_NULL_HANDLE || dummyImageLayout_ == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) { + return; + } + + VkImageMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = dummyImageLayout_; + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = dummyImage_; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + vkCmdPipelineBarrier(commandBuffer, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &barrier); + + dummyImageLayout_ = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; +} + +void RenderCommandService::EnsureRenderGraphResources(const RenderGraphDefinition& graph) { + if (graph.resources.empty() || graph.passes.empty()) { + return; + } + + VkExtent2D extent = swapchainService_->GetSwapchainExtent(); + if (renderGraphResourceCount_ == graph.resources.size() && + renderGraphPassCount_ == graph.passes.size() && + renderGraphExtent_.width == extent.width && + renderGraphExtent_.height == extent.height) { + return; + } + + CleanupRenderGraphResources(); + RegisterRenderGraphShaders(graph); + + renderGraphResourceCount_ = graph.resources.size(); + renderGraphPassCount_ = graph.passes.size(); + renderGraphExtent_ = extent; + + VkFormat colorFormat = swapchainService_->GetSwapchainImageFormat(); + VkFormat depthFormat = swapchainService_->GetDepthFormat(); + + VkAttachmentDescription colorAttachment{}; + colorAttachment.format = colorFormat; + colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; + colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + colorAttachment.initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + colorAttachment.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkAttachmentDescription depthAttachment{}; + depthAttachment.format = depthFormat; + depthAttachment.samples = VK_SAMPLE_COUNT_1_BIT; + depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + depthAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + depthAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + depthAttachment.initialLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + depthAttachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + + VkAttachmentReference colorRef{}; + colorRef.attachment = 0; + colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkAttachmentReference depthRef{}; + depthRef.attachment = 1; + depthRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &colorRef; + subpass.pDepthStencilAttachment = &depthRef; + + std::array attachments = {colorAttachment, depthAttachment}; + VkRenderPassCreateInfo renderPassInfo{}; + renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + renderPassInfo.attachmentCount = static_cast(attachments.size()); + renderPassInfo.pAttachments = attachments.data(); + renderPassInfo.subpassCount = 1; + renderPassInfo.pSubpasses = &subpass; + + if (vkCreateRenderPass(deviceService_->GetDevice(), &renderPassInfo, nullptr, &offscreenRenderPass_) != VK_SUCCESS) { + throw std::runtime_error("Failed to create offscreen render pass"); + } + + for (const auto& resource : graph.resources) { + if (resource.type != "color" || resource.name == "swapchain") { + continue; + } + if (resource.layers > 1 || resource.mips > 1) { + if (logger_) { + logger_->Warn("RenderCommandService: Skipping layered/mipped resource '" + resource.name + "'"); + } + continue; + } + + VkExtent2D targetExtent = ResolveExtent(resource); + if (targetExtent.width != extent.width || targetExtent.height != extent.height) { + if (logger_) { + logger_->Warn("RenderCommandService: Skipping resource '" + resource.name + + "' due to unsupported extent"); + } + continue; + } + VkFormat targetFormat = ResolveColorFormat(resource.format); + if (targetFormat != colorFormat && logger_) { + logger_->Trace("RenderCommandService", "EnsureRenderGraphResources", + "Resource '" + resource.name + "' format overridden to swapchain format"); + } + + RenderGraphImage color{}; + CreateRenderGraphImage(color, colorFormat, targetExtent, + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + VK_IMAGE_ASPECT_COLOR_BIT); + + RenderGraphImage depth{}; + CreateRenderGraphImage(depth, depthFormat, targetExtent, + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, + VK_IMAGE_ASPECT_DEPTH_BIT); + + std::array framebufferAttachments = {color.view, depth.view}; + VkFramebufferCreateInfo framebufferInfo{}; + framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + framebufferInfo.renderPass = offscreenRenderPass_; + framebufferInfo.attachmentCount = static_cast(framebufferAttachments.size()); + framebufferInfo.pAttachments = framebufferAttachments.data(); + framebufferInfo.width = targetExtent.width; + framebufferInfo.height = targetExtent.height; + framebufferInfo.layers = 1; + + if (vkCreateFramebuffer(deviceService_->GetDevice(), &framebufferInfo, nullptr, &color.framebuffer) != VK_SUCCESS) { + throw std::runtime_error("Failed to create render graph framebuffer for " + resource.name); + } + + renderGraphTargets_.emplace(resource.name, color); + renderGraphDepth_.emplace(resource.name, depth); + } +} + +void RenderCommandService::CleanupRenderGraphResources() { + logger_->Trace("RenderCommandService", "CleanupRenderGraphResources"); + + auto device = deviceService_->GetDevice(); + + for (auto& [name, image] : renderGraphTargets_) { + if (image.framebuffer != VK_NULL_HANDLE) { + vkDestroyFramebuffer(device, image.framebuffer, nullptr); + image.framebuffer = VK_NULL_HANDLE; + } + if (image.view != VK_NULL_HANDLE) { + vkDestroyImageView(device, image.view, nullptr); + image.view = VK_NULL_HANDLE; + } + if (image.image != VK_NULL_HANDLE) { + vkDestroyImage(device, image.image, nullptr); + image.image = VK_NULL_HANDLE; + } + if (image.memory != VK_NULL_HANDLE) { + vkFreeMemory(device, image.memory, nullptr); + image.memory = VK_NULL_HANDLE; + } + } + renderGraphTargets_.clear(); + + for (auto& [name, image] : renderGraphDepth_) { + if (image.view != VK_NULL_HANDLE) { + vkDestroyImageView(device, image.view, nullptr); + image.view = VK_NULL_HANDLE; + } + if (image.image != VK_NULL_HANDLE) { + vkDestroyImage(device, image.image, nullptr); + image.image = VK_NULL_HANDLE; + } + if (image.memory != VK_NULL_HANDLE) { + vkFreeMemory(device, image.memory, nullptr); + image.memory = VK_NULL_HANDLE; + } + } + renderGraphDepth_.clear(); + + if (offscreenRenderPass_ != VK_NULL_HANDLE) { + vkDestroyRenderPass(device, offscreenRenderPass_, nullptr); + offscreenRenderPass_ = VK_NULL_HANDLE; + } + + renderGraphResourceCount_ = 0; + renderGraphPassCount_ = 0; + renderGraphExtent_ = {}; +} + +void RenderCommandService::RegisterRenderGraphShaders(const RenderGraphDefinition& graph) { + static const char* kFullscreenVertex = R"( +#version 450 + +layout(location = 0) out vec2 fragTexCoord; + +vec2 positions[3] = vec2[]( + vec2(-1.0, -1.0), + vec2(3.0, -1.0), + vec2(-1.0, 3.0) +); + +vec2 uvs[3] = vec2[]( + vec2(0.0, 0.0), + vec2(2.0, 0.0), + vec2(0.0, 2.0) +); + +void main() { + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); + fragTexCoord = uvs[gl_VertexIndex]; +} +)"; + + static const char* kPostProcessFragment = R"( +#version 450 + +layout(location = 0) in vec2 fragTexCoord; +layout(location = 0) out vec4 outColor; + +layout(set = 0, binding = 0) uniform sampler2D inputTexture; + +layout(push_constant) uniform PushConstants { + mat4 model; + mat4 viewProj; + mat4 view; + mat4 proj; + mat4 lightViewProj; + vec3 cameraPos; + float time; + float ambientStrength; + float fogDensity; + float fogStart; + float fogEnd; + vec3 fogColor; + float gamma; + float exposure; + int enableShadows; + int enableFog; +} pc; + +void main() { + vec3 color = texture(inputTexture, fragTexCoord).rgb; + outColor = vec4(color, 1.0); +} +)"; + + static const char* kTonemapFragment = R"( +#version 450 + +layout(location = 0) in vec2 fragTexCoord; +layout(location = 0) out vec4 outColor; + +layout(set = 0, binding = 0) uniform sampler2D inputTexture; + +layout(push_constant) uniform PushConstants { + mat4 model; + mat4 viewProj; + mat4 view; + mat4 proj; + mat4 lightViewProj; + vec3 cameraPos; + float time; + float ambientStrength; + float fogDensity; + float fogStart; + float fogEnd; + vec3 fogColor; + float gamma; + float exposure; + int enableShadows; + int enableFog; +} pc; + +void main() { + vec3 color = texture(inputTexture, fragTexCoord).rgb; + color *= pc.exposure; + color = color / (color + vec3(1.0)); + color = pow(color, vec3(1.0 / max(pc.gamma, 0.01))); + outColor = vec4(color, 1.0); +} +)"; + + bool registered = false; + for (const auto& pass : graph.passes) { + if (pass.shader.empty()) { + continue; + } + if (IsScenePass(pass)) { + continue; + } + if (pipelineService_->HasShader(pass.shader)) { + continue; + } + + ShaderPaths paths{}; + paths.vertexSource = kFullscreenVertex; + if (pass.shader == "tonemap") { + paths.fragmentSource = kTonemapFragment; + } else { + paths.fragmentSource = kPostProcessFragment; + } + pipelineService_->RegisterShader(pass.shader, paths); + registered = true; + } + + if (registered) { + VkRenderPass renderPass = swapchainService_->GetRenderPass(); + VkExtent2D extent = swapchainService_->GetSwapchainExtent(); + pipelineService_->RecreatePipelines(renderPass, extent); + CleanupDescriptorResources(); + } +} + +VkFormat RenderCommandService::ResolveColorFormat(const std::string& format) const { + (void)format; + return swapchainService_->GetSwapchainImageFormat(); +} + +VkFormat RenderCommandService::ResolveDepthFormat(const std::string& format) const { + (void)format; + return swapchainService_->GetDepthFormat(); +} + +VkExtent2D RenderCommandService::ResolveExtent(const RenderGraphResource& resource) const { + VkExtent2D swapchainExtent = swapchainService_->GetSwapchainExtent(); + if (resource.hasExplicitSize) { + return {std::max(1u, resource.explicitSize[0]), + std::max(1u, resource.explicitSize[1])}; + } + if (resource.size == "half") { + return {std::max(1u, swapchainExtent.width / 2), + std::max(1u, swapchainExtent.height / 2)}; + } + return swapchainExtent; +} + +RenderGraphImage* RenderCommandService::FindRenderTarget(const std::string& name) { + auto it = renderGraphTargets_.find(name); + if (it == renderGraphTargets_.end()) { + return nullptr; + } + return &it->second; +} + +RenderGraphImage* RenderCommandService::FindDepthTarget(const std::string& name) { + auto it = renderGraphDepth_.find(name); + if (it == renderGraphDepth_.end()) { + return nullptr; + } + return &it->second; +} + +void RenderCommandService::CreateRenderGraphImage(RenderGraphImage& image, VkFormat format, + VkExtent2D extent, VkImageUsageFlags usage, + VkImageAspectFlags aspectMask) { + auto device = deviceService_->GetDevice(); + + VkImageCreateInfo imageInfo{}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.extent.width = extent.width; + imageInfo.extent.height = extent.height; + imageInfo.extent.depth = 1; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.format = format; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imageInfo.usage = usage; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateImage(device, &imageInfo, nullptr, &image.image) != VK_SUCCESS) { + throw std::runtime_error("Failed to create render graph image"); + } + + VkMemoryRequirements memRequirements; + vkGetImageMemoryRequirements(device, image.image, &memRequirements); + + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = deviceService_->FindMemoryType( + memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + + if (vkAllocateMemory(device, &allocInfo, nullptr, &image.memory) != VK_SUCCESS) { + throw std::runtime_error("Failed to allocate render graph image memory"); + } + + vkBindImageMemory(device, image.image, image.memory, 0); + + VkImageViewCreateInfo viewInfo{}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = image.image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = format; + viewInfo.subresourceRange.aspectMask = aspectMask; + viewInfo.subresourceRange.baseMipLevel = 0; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.baseArrayLayer = 0; + viewInfo.subresourceRange.layerCount = 1; + + if (vkCreateImageView(device, &viewInfo, nullptr, &image.view) != VK_SUCCESS) { + throw std::runtime_error("Failed to create render graph image view"); + } + + image.format = format; + image.extent = extent; + image.layout = VK_IMAGE_LAYOUT_UNDEFINED; +} + +void RenderCommandService::TransitionImageLayout(VkCommandBuffer commandBuffer, RenderGraphImage& image, + VkImageLayout newLayout, VkImageAspectFlags aspectMask) { + if (image.layout == newLayout || image.image == VK_NULL_HANDLE) { + return; + } + + VkImageMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = image.layout; + barrier.newLayout = newLayout; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image.image; + barrier.subresourceRange.aspectMask = aspectMask; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + + VkPipelineStageFlags srcStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkPipelineStageFlags dstStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + + if (newLayout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) { + barrier.srcAccessMask = (image.layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + ? VK_ACCESS_SHADER_READ_BIT : 0; + barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + srcStage = (image.layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + ? VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + dstStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + } else if (newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) { + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + srcStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + dstStage = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + } else if (newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) { + barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + srcStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dstStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + } + + vkCmdPipelineBarrier(commandBuffer, srcStage, dstStage, 0, 0, nullptr, 0, nullptr, 1, &barrier); + image.layout = newLayout; +} + +bool RenderCommandService::IsScenePass(const RenderGraphPass& pass) const { + return pass.kind == "gbuffer" || pass.kind == "forward_plus" || pass.kind == "transparent"; +} + +bool RenderCommandService::IsPassEnabled(const RenderGraphPass& pass) const { + auto it = pass.settings.find("enabled"); + if (it != pass.settings.end() && it->second.type == RenderGraphValue::Type::Boolean) { + return it->second.boolean; + } + return true; +} + +std::string RenderCommandService::ResolvePassOutput(const RenderGraphPass& pass) const { + if (!pass.output.empty()) { + return pass.output; + } + if (!pass.outputs.empty()) { + auto sceneIt = pass.outputs.find("scene"); + if (sceneIt != pass.outputs.end()) { + return sceneIt->second; + } + auto albedoIt = pass.outputs.find("albedo"); + if (albedoIt != pass.outputs.end()) { + return albedoIt->second; + } + auto colorIt = pass.outputs.find("color"); + if (colorIt != pass.outputs.end()) { + return colorIt->second; + } + return pass.outputs.begin()->second; + } + return {}; +} + +std::string RenderCommandService::ResolvePassInput(const RenderGraphPass& pass) const { + if (!pass.inputs.empty()) { + auto sceneIt = pass.inputs.find("scene"); + if (sceneIt != pass.inputs.end()) { + return sceneIt->second; + } + auto inputIt = pass.inputs.find("input"); + if (inputIt != pass.inputs.end()) { + return inputIt->second; + } + auto albedoIt = pass.inputs.find("albedo"); + if (albedoIt != pass.inputs.end()) { + return albedoIt->second; + } + auto colorIt = pass.inputs.find("color"); + if (colorIt != pass.inputs.end()) { + return colorIt->second; + } + return pass.inputs.begin()->second; + } + return {}; +} + +core::PushConstants RenderCommandService::BuildFullscreenConstants(const RenderGraphPass& pass) const { + core::PushConstants constants{}; + const std::array identity = {1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f}; + constants.model = identity; + constants.viewProj = identity; + constants.view = identity; + constants.proj = identity; + constants.lightViewProj = identity; + constants.cameraPos = {0.0f, 0.0f, 0.0f}; + + const auto& config = configService_->GetConfig(); + constants.gamma = config.atmospherics.gamma; + constants.exposure = config.atmospherics.exposure; + constants.ambientStrength = config.atmospherics.ambientStrength; + constants.fogDensity = config.atmospherics.fogDensity; + constants.fogColor = config.atmospherics.fogColor; + constants.enableFog = 0; + constants.enableShadows = 0; + + auto applySetting = [&](const std::string& key, float& target) { + auto it = pass.settings.find(key); + if (it != pass.settings.end() && it->second.type == RenderGraphValue::Type::Number) { + target = static_cast(it->second.number); + } + }; + + applySetting("gamma", constants.gamma); + applySetting("exposure", constants.exposure); + + return constants; +} + } // namespace sdl3cpp::services::impl diff --git a/src/services/impl/render_command_service.hpp b/src/services/impl/render_command_service.hpp index 86ac4e0..0ef7084 100644 --- a/src/services/impl/render_command_service.hpp +++ b/src/services/impl/render_command_service.hpp @@ -11,6 +11,7 @@ #include "../interfaces/i_logger.hpp" #include "../../di/lifecycle.hpp" #include +#include #include namespace sdl3cpp::services::impl { @@ -40,6 +41,10 @@ public: void RecordCommands(uint32_t imageIndex, const std::vector& commands, const std::array& viewProj) override; + void RecordRenderGraph(uint32_t imageIndex, + const RenderGraphDefinition& graph, + const std::vector& commands, + const std::array& viewProj) override; bool EndFrame(uint32_t imageIndex) override; VkCommandBuffer GetCurrentCommandBuffer() const override; @@ -57,6 +62,16 @@ public: void Shutdown() noexcept override; private: + struct RenderGraphImage { + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + VkFormat format = VK_FORMAT_UNDEFINED; + VkExtent2D extent{}; + VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED; + }; + std::shared_ptr deviceService_; std::shared_ptr swapchainService_; std::shared_ptr pipelineService_; @@ -73,6 +88,22 @@ private: VkSemaphore renderFinishedSemaphore_ = VK_NULL_HANDLE; VkFence inFlightFence_ = VK_NULL_HANDLE; + VkDescriptorPool descriptorPool_ = VK_NULL_HANDLE; + VkDescriptorSet defaultDescriptorSet_ = VK_NULL_HANDLE; + VkDescriptorSet postProcessDescriptorSet_ = VK_NULL_HANDLE; + VkSampler sampler_ = VK_NULL_HANDLE; + VkImage dummyImage_ = VK_NULL_HANDLE; + VkDeviceMemory dummyImageMemory_ = VK_NULL_HANDLE; + VkImageView dummyImageView_ = VK_NULL_HANDLE; + VkImageLayout dummyImageLayout_ = VK_IMAGE_LAYOUT_UNDEFINED; + + VkRenderPass offscreenRenderPass_ = VK_NULL_HANDLE; + std::unordered_map renderGraphTargets_; + std::unordered_map renderGraphDepth_; + VkExtent2D renderGraphExtent_{}; + size_t renderGraphResourceCount_ = 0; + size_t renderGraphPassCount_ = 0; + uint32_t currentFrame_ = 0; uint32_t maxFramesInFlight_ = 1; // Single frame in flight for simplicity @@ -82,6 +113,33 @@ private: void CreateSyncObjects(); void CleanupCommandResources(); void CleanupSyncObjects(); + + void EnsureDescriptorResources(); + void CleanupDescriptorResources(); + void CreateDescriptorPool(); + void CreateSampler(); + void CreateDummyImage(); + void AllocateDescriptorSets(); + void UpdateDescriptorSet(VkDescriptorSet set, VkImageView view); + void EnsureDummyImageLayout(VkCommandBuffer commandBuffer); + + void EnsureRenderGraphResources(const RenderGraphDefinition& graph); + void CleanupRenderGraphResources(); + void RegisterRenderGraphShaders(const RenderGraphDefinition& graph); + VkFormat ResolveColorFormat(const std::string& format) const; + VkFormat ResolveDepthFormat(const std::string& format) const; + VkExtent2D ResolveExtent(const RenderGraphResource& resource) const; + RenderGraphImage* FindRenderTarget(const std::string& name); + RenderGraphImage* FindDepthTarget(const std::string& name); + void CreateRenderGraphImage(RenderGraphImage& image, VkFormat format, VkExtent2D extent, + VkImageUsageFlags usage, VkImageAspectFlags aspectMask); + void TransitionImageLayout(VkCommandBuffer commandBuffer, RenderGraphImage& image, + VkImageLayout newLayout, VkImageAspectFlags aspectMask); + bool IsScenePass(const RenderGraphPass& pass) const; + bool IsPassEnabled(const RenderGraphPass& pass) const; + std::string ResolvePassOutput(const RenderGraphPass& pass) const; + std::string ResolvePassInput(const RenderGraphPass& pass) const; + core::PushConstants BuildFullscreenConstants(const RenderGraphPass& pass) const; }; } // namespace sdl3cpp::services::impl diff --git a/src/services/impl/swapchain_service.hpp b/src/services/impl/swapchain_service.hpp index efdcbb6..efd374a 100644 --- a/src/services/impl/swapchain_service.hpp +++ b/src/services/impl/swapchain_service.hpp @@ -54,6 +54,10 @@ public: logger_->Trace("SwapchainService", "GetSwapchainImageFormat"); return imageFormat_; } + VkFormat GetDepthFormat() const override { + logger_->Trace("SwapchainService", "GetDepthFormat"); + return depthFormat_; + } VkExtent2D GetSwapchainExtent() const override { logger_->Trace("SwapchainService", "GetSwapchainExtent"); return extent_; diff --git a/src/services/impl/vulkan_graphics_backend.cpp b/src/services/impl/vulkan_graphics_backend.cpp index 1d2675b..92a3e93 100644 --- a/src/services/impl/vulkan_graphics_backend.cpp +++ b/src/services/impl/vulkan_graphics_backend.cpp @@ -220,7 +220,12 @@ bool VulkanGraphicsBackend::EndFrame(GraphicsDeviceHandle device) { logger_->Trace("VulkanGraphicsBackend", "EndFrame"); // Record all accumulated commands - renderCommandService_->RecordCommands(currentImageIndex_, frameCommands_, currentViewProj_); + if (renderGraphEnabled_) { + renderCommandService_->RecordRenderGraph(currentImageIndex_, renderGraphDefinition_, + frameCommands_, currentViewProj_); + } else { + renderCommandService_->RecordCommands(currentImageIndex_, frameCommands_, currentViewProj_); + } // End the frame return renderCommandService_->EndFrame(currentImageIndex_); @@ -231,6 +236,14 @@ void VulkanGraphicsBackend::SetViewProjection(const std::array& viewP currentViewProj_ = viewProj; } +void VulkanGraphicsBackend::SetRenderGraphDefinition(const RenderGraphDefinition& definition) { + logger_->Trace("VulkanGraphicsBackend", "SetRenderGraphDefinition", + "resources=" + std::to_string(definition.resources.size()) + + ", passes=" + std::to_string(definition.passes.size())); + renderGraphDefinition_ = definition; + renderGraphEnabled_ = !definition.passes.empty(); +} + void VulkanGraphicsBackend::Draw(GraphicsDeviceHandle device, GraphicsPipelineHandle pipeline, GraphicsBufferHandle vertexBuffer, GraphicsBufferHandle indexBuffer, uint32_t indexCount, const std::array& modelMatrix) { diff --git a/src/services/impl/vulkan_graphics_backend.hpp b/src/services/impl/vulkan_graphics_backend.hpp index d103d36..f98dae4 100644 --- a/src/services/impl/vulkan_graphics_backend.hpp +++ b/src/services/impl/vulkan_graphics_backend.hpp @@ -48,6 +48,7 @@ public: bool EndFrame(GraphicsDeviceHandle device) override; void SetViewProjection(const std::array& viewProj) override; + void SetRenderGraphDefinition(const RenderGraphDefinition& definition) override; void Draw(GraphicsDeviceHandle device, GraphicsPipelineHandle pipeline, GraphicsBufferHandle vertexBuffer, GraphicsBufferHandle indexBuffer, @@ -72,6 +73,8 @@ private: uint32_t currentImageIndex_; std::vector frameCommands_; std::array currentViewProj_; + RenderGraphDefinition renderGraphDefinition_{}; + bool renderGraphEnabled_ = false; std::unordered_map pipelineToShaderKey_; }; diff --git a/src/services/interfaces/i_graphics_backend.hpp b/src/services/interfaces/i_graphics_backend.hpp index 58ccbeb..b0359f3 100644 --- a/src/services/interfaces/i_graphics_backend.hpp +++ b/src/services/interfaces/i_graphics_backend.hpp @@ -146,6 +146,13 @@ public: */ virtual void SetViewProjection(const std::array& viewProj) = 0; + /** + * @brief Set the render graph definition (optional). + * + * @param definition Render graph definition + */ + virtual void SetRenderGraphDefinition(const RenderGraphDefinition& definition) = 0; + /** * @brief Draw with a pipeline. * diff --git a/src/services/interfaces/i_pipeline_service.hpp b/src/services/interfaces/i_pipeline_service.hpp index e059fff..5aa6019 100644 --- a/src/services/interfaces/i_pipeline_service.hpp +++ b/src/services/interfaces/i_pipeline_service.hpp @@ -66,6 +66,13 @@ public: */ virtual VkPipelineLayout GetPipelineLayout() const = 0; + /** + * @brief Get the descriptor set layout used by pipelines. + * + * @return VkDescriptorSetLayout handle + */ + virtual VkDescriptorSetLayout GetDescriptorSetLayout() const = 0; + /** * @brief Check if a shader exists. * diff --git a/src/services/interfaces/i_render_command_service.hpp b/src/services/interfaces/i_render_command_service.hpp index 76f4271..045b73d 100644 --- a/src/services/interfaces/i_render_command_service.hpp +++ b/src/services/interfaces/i_render_command_service.hpp @@ -45,6 +45,19 @@ public: const std::vector& commands, const std::array& viewProj) = 0; + /** + * @brief Record rendering commands using a render graph. + * + * @param imageIndex Swapchain image index + * @param graph Render graph definition + * @param commands Render commands to execute for scene passes + * @param viewProj View-projection matrix + */ + virtual void RecordRenderGraph(uint32_t imageIndex, + const RenderGraphDefinition& graph, + const std::vector& commands, + const std::array& viewProj) = 0; + /** * @brief End the frame and present. * diff --git a/src/services/interfaces/i_swapchain_service.hpp b/src/services/interfaces/i_swapchain_service.hpp index afe8cac..68b200f 100644 --- a/src/services/interfaces/i_swapchain_service.hpp +++ b/src/services/interfaces/i_swapchain_service.hpp @@ -104,6 +104,13 @@ public: */ virtual VkFormat GetSwapchainImageFormat() const = 0; + /** + * @brief Get the depth buffer format. + * + * @return VkFormat of depth images + */ + virtual VkFormat GetDepthFormat() const = 0; + /** * @brief Get the swapchain extent (size). *