diff --git a/CMakeLists.txt b/CMakeLists.txt index ddd92fe..db156ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -127,6 +127,8 @@ if(BUILD_SDL3_APP) src/services/impl/sdl_window_service.cpp src/services/impl/sdl_input_service.cpp src/services/impl/vulkan_device_service.cpp + src/services/impl/swapchain_service.cpp + src/services/impl/pipeline_service.cpp src/app/sdl3_app_core.cpp src/app/audio_player.cpp src/app/sdl3_app_device.cpp diff --git a/CMakeUserPresets.json b/CMakeUserPresets.json index ed4443e..889fff9 100644 --- a/CMakeUserPresets.json +++ b/CMakeUserPresets.json @@ -4,7 +4,6 @@ "conan": {} }, "include": [ - "build/Release/generators/CMakePresets.json", "build/build/Release/generators/CMakePresets.json" ] } \ No newline at end of file diff --git a/src/services/impl/pipeline_service.cpp b/src/services/impl/pipeline_service.cpp new file mode 100644 index 0000000..2362064 --- /dev/null +++ b/src/services/impl/pipeline_service.cpp @@ -0,0 +1,313 @@ +#include "pipeline_service.hpp" +#include "../../core/vertex.hpp" +#include "../../logging/logger.hpp" +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +PipelineService::PipelineService(std::shared_ptr deviceService) + : deviceService_(std::move(deviceService)) {} + +PipelineService::~PipelineService() { + if (pipelineLayout_ != VK_NULL_HANDLE || !pipelines_.empty()) { + Shutdown(); + } +} + +void PipelineService::RegisterShader(const std::string& key, const ShaderPaths& paths) { + logging::TraceGuard trace; + shaderPathMap_[key] = paths; + logging::Logger::GetInstance().Debug("Registered shader: " + key); +} + +void PipelineService::CompileAll(VkRenderPass renderPass, VkExtent2D extent) { + logging::TraceGuard trace; + + if (shaderPathMap_.empty()) { + throw std::runtime_error("No shader paths were registered before pipeline creation"); + } + + CreatePipelineLayout(); + CreatePipelinesInternal(renderPass, extent); + + logging::Logger::GetInstance().Info("Compiled " + std::to_string(pipelines_.size()) + " pipeline(s)"); +} + +void PipelineService::RecreatePipelines(VkRenderPass renderPass, VkExtent2D extent) { + logging::TraceGuard trace; + + CleanupPipelines(); + CreatePipelineLayout(); + CreatePipelinesInternal(renderPass, extent); + + logging::Logger::GetInstance().Info("Recreated " + std::to_string(pipelines_.size()) + " pipeline(s)"); +} + +void PipelineService::Cleanup() { + CleanupPipelines(); + + auto device = deviceService_->GetDevice(); + + if (pipelineLayout_ != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); + pipelineLayout_ = VK_NULL_HANDLE; + } +} + +void PipelineService::Shutdown() noexcept { + Cleanup(); +} + +VkPipeline PipelineService::GetPipeline(const std::string& key) const { + auto it = pipelines_.find(key); + if (it == pipelines_.end()) { + throw std::out_of_range("Pipeline not found: " + key); + } + return it->second; +} + +bool PipelineService::HasShader(const std::string& key) const { + return shaderPathMap_.find(key) != shaderPathMap_.end(); +} + +void PipelineService::CreatePipelineLayout() { + logging::TraceGuard trace; + + auto device = deviceService_->GetDevice(); + + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + pushRange.offset = 0; + pushRange.size = sizeof(core::PushConstants); + + VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; + pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + 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"); + } +} + +void PipelineService::CreatePipelinesInternal(VkRenderPass renderPass, VkExtent2D extent) { + logging::TraceGuard trace; + + auto device = deviceService_->GetDevice(); + + // Vertex input configuration + VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; + VkVertexInputBindingDescription bindingDescription{}; + bindingDescription.binding = 0; + bindingDescription.stride = sizeof(core::Vertex); + bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::array attributeDescriptions{}; + attributeDescriptions[0].binding = 0; + attributeDescriptions[0].location = 0; + attributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT; + attributeDescriptions[0].offset = offsetof(core::Vertex, position); + + attributeDescriptions[1].binding = 0; + attributeDescriptions[1].location = 1; + attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT; + attributeDescriptions[1].offset = offsetof(core::Vertex, color); + + vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + vertexInputInfo.vertexBindingDescriptionCount = 1; + vertexInputInfo.pVertexBindingDescriptions = &bindingDescription; + vertexInputInfo.vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()); + vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions.data(); + + // Input assembly + VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; + inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + inputAssembly.primitiveRestartEnable = VK_FALSE; + + // Viewport and scissor + VkViewport viewport{}; + viewport.x = 0.0f; + viewport.y = 0.0f; + viewport.width = static_cast(extent.width); + viewport.height = static_cast(extent.height); + viewport.minDepth = 0.0f; + viewport.maxDepth = 1.0f; + + VkRect2D scissor{}; + scissor.offset = {0, 0}; + scissor.extent = extent; + + VkPipelineViewportStateCreateInfo viewportState{}; + viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + viewportState.viewportCount = 1; + viewportState.pViewports = &viewport; + viewportState.scissorCount = 1; + viewportState.pScissors = &scissor; + + // Rasterization + VkPipelineRasterizationStateCreateInfo rasterizer{}; + rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rasterizer.depthClampEnable = VK_FALSE; + rasterizer.rasterizerDiscardEnable = VK_FALSE; + rasterizer.polygonMode = VK_POLYGON_MODE_FILL; + rasterizer.lineWidth = 1.0f; + rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; + rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE; + rasterizer.depthBiasEnable = VK_FALSE; + + // Multisampling + VkPipelineMultisampleStateCreateInfo multisampling{}; + multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + multisampling.sampleShadingEnable = VK_FALSE; + multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + + // Color blending + VkPipelineColorBlendAttachmentState colorBlendAttachment{}; + colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + colorBlendAttachment.blendEnable = VK_FALSE; + + VkPipelineColorBlendStateCreateInfo colorBlending{}; + colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + colorBlending.logicOpEnable = VK_FALSE; + colorBlending.attachmentCount = 1; + colorBlending.pAttachments = &colorBlendAttachment; + + // Base pipeline info + VkGraphicsPipelineCreateInfo pipelineInfo{}; + pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + pipelineInfo.pVertexInputState = &vertexInputInfo; + pipelineInfo.pInputAssemblyState = &inputAssembly; + pipelineInfo.pViewportState = &viewportState; + pipelineInfo.pRasterizationState = &rasterizer; + pipelineInfo.pMultisampleState = &multisampling; + pipelineInfo.pColorBlendState = &colorBlending; + pipelineInfo.layout = pipelineLayout_; + pipelineInfo.renderPass = renderPass; + pipelineInfo.subpass = 0; + + // Create pipeline for each registered shader + for (const auto& [key, paths] : shaderPathMap_) { + // Validate shader files exist + if (!std::filesystem::exists(paths.vertex)) { + throw std::runtime_error( + "Vertex shader not found: " + paths.vertex + + "\n\nShader key: " + key + + "\n\nPlease ensure shader files are compiled and present in the shaders directory."); + } + if (!std::filesystem::exists(paths.fragment)) { + throw std::runtime_error( + "Fragment shader not found: " + paths.fragment + + "\n\nShader key: " + key + + "\n\nPlease ensure shader files are compiled and present in the shaders directory."); + } + + auto vertShaderCode = ReadShaderFile(paths.vertex); + auto fragShaderCode = ReadShaderFile(paths.fragment); + + VkShaderModule vertShaderModule = CreateShaderModule(vertShaderCode); + VkShaderModule fragShaderModule = CreateShaderModule(fragShaderCode); + + VkPipelineShaderStageCreateInfo vertStageInfo{}; + vertStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + vertStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT; + vertStageInfo.module = vertShaderModule; + vertStageInfo.pName = "main"; + + VkPipelineShaderStageCreateInfo fragStageInfo{}; + fragStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + fragStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT; + fragStageInfo.module = fragShaderModule; + fragStageInfo.pName = "main"; + + VkPipelineShaderStageCreateInfo shaderStages[] = {vertStageInfo, fragStageInfo}; + + VkGraphicsPipelineCreateInfo pipelineCreateInfo = pipelineInfo; + pipelineCreateInfo.stageCount = 2; + pipelineCreateInfo.pStages = shaderStages; + + VkPipeline pipeline; + if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineCreateInfo, nullptr, + &pipeline) != VK_SUCCESS) { + vkDestroyShaderModule(device, fragShaderModule, nullptr); + vkDestroyShaderModule(device, vertShaderModule, nullptr); + throw std::runtime_error("Failed to create graphics pipeline for shader: " + key); + } + + pipelines_[key] = pipeline; + + vkDestroyShaderModule(device, fragShaderModule, nullptr); + vkDestroyShaderModule(device, vertShaderModule, nullptr); + + logging::Logger::GetInstance().Debug("Created pipeline: " + key); + } +} + +void PipelineService::CleanupPipelines() { + logging::TraceGuard trace; + + auto device = deviceService_->GetDevice(); + + for (auto& [key, pipeline] : pipelines_) { + vkDestroyPipeline(device, pipeline, nullptr); + } + pipelines_.clear(); +} + +VkShaderModule PipelineService::CreateShaderModule(const std::vector& code) { + logging::TraceGuard trace; + + auto device = deviceService_->GetDevice(); + + VkShaderModuleCreateInfo createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + createInfo.codeSize = code.size(); + createInfo.pCode = reinterpret_cast(code.data()); + + VkShaderModule shaderModule; + if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) { + throw std::runtime_error("Failed to create shader module"); + } + return shaderModule; +} + +std::vector PipelineService::ReadShaderFile(const std::string& path) { + logging::TraceGuard trace; + + if (!std::filesystem::exists(path)) { + throw std::runtime_error("Shader file not found: " + path + + "\n\nPlease ensure the file exists at this location."); + } + + if (!std::filesystem::is_regular_file(path)) { + throw std::runtime_error("Path is not a regular file: " + path); + } + + std::ifstream file(path, std::ios::ate | std::ios::binary); + if (!file) { + throw std::runtime_error("Failed to open shader file: " + path + + "\n\nCheck file permissions."); + } + + size_t fileSize = static_cast(file.tellg()); + std::vector buffer(fileSize); + + file.seekg(0); + file.read(buffer.data(), static_cast(fileSize)); + file.close(); + + logging::Logger::GetInstance().Debug("Read shader file: " + path + " (" + std::to_string(fileSize) + " bytes)"); + + return buffer; +} + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/pipeline_service.hpp b/src/services/impl/pipeline_service.hpp new file mode 100644 index 0000000..7a60c48 --- /dev/null +++ b/src/services/impl/pipeline_service.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include "../interfaces/i_pipeline_service.hpp" +#include "../interfaces/i_vulkan_device_service.hpp" +#include "../../di/lifecycle.hpp" +#include +#include +#include + +namespace sdl3cpp::services::impl { + +/** + * @brief Pipeline service implementation. + * + * Small, focused service (~200 lines) for Vulkan graphics pipeline management. + * Handles shader compilation, pipeline creation, and pipeline layout. + */ +class PipelineService : public IPipelineService, + public di::IShutdownable { +public: + explicit PipelineService(std::shared_ptr deviceService); + ~PipelineService() override; + + // IPipelineService interface + void RegisterShader(const std::string& key, const ShaderPaths& paths) override; + void CompileAll(VkRenderPass renderPass, VkExtent2D extent) override; + void RecreatePipelines(VkRenderPass renderPass, VkExtent2D extent) override; + void Cleanup() override; + + VkPipeline GetPipeline(const std::string& key) const override; + VkPipelineLayout GetPipelineLayout() const override { return pipelineLayout_; } + bool HasShader(const std::string& key) const override; + size_t GetShaderCount() const override { return shaderPathMap_.size(); } + + // IShutdownable interface + void Shutdown() noexcept override; + +private: + std::shared_ptr deviceService_; + + VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE; + std::unordered_map shaderPathMap_; + std::unordered_map pipelines_; + + // Helper methods + VkShaderModule CreateShaderModule(const std::vector& code); + std::vector ReadShaderFile(const std::string& path); + void CreatePipelineLayout(); + void CreatePipelinesInternal(VkRenderPass renderPass, VkExtent2D extent); + void CleanupPipelines(); +}; + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/swapchain_service.cpp b/src/services/impl/swapchain_service.cpp new file mode 100644 index 0000000..841b858 --- /dev/null +++ b/src/services/impl/swapchain_service.cpp @@ -0,0 +1,338 @@ +#include "swapchain_service.hpp" +#include "../../logging/logger.hpp" +#include +#include + +namespace sdl3cpp::services::impl { + +SwapchainService::SwapchainService(std::shared_ptr deviceService, + std::shared_ptr eventBus) + : deviceService_(std::move(deviceService)), eventBus_(std::move(eventBus)) { + // Subscribe to window resize events + eventBus_->Subscribe(events::EventType::WindowResized, + [this](const events::Event& event) { OnWindowResized(event); }); +} + +SwapchainService::~SwapchainService() { + if (swapchain_ != VK_NULL_HANDLE) { + Shutdown(); + } +} + +void SwapchainService::Initialize() { + logging::TraceGuard trace; + // Initialization happens in CreateSwapchain() +} + +void SwapchainService::CreateSwapchain(uint32_t width, uint32_t height) { + logging::TraceGuard trace; + + currentWidth_ = width; + currentHeight_ = height; + + auto physicalDevice = deviceService_->GetPhysicalDevice(); + auto surface = deviceService_->GetSurface(); + auto device = deviceService_->GetDevice(); + + SwapchainSupportDetails support = QuerySwapchainSupport(physicalDevice, surface); + + // Validate swap chain support + if (support.formats.empty()) { + throw std::runtime_error("No surface formats available for swap chain.\n" + "This may indicate GPU driver issues or incompatible surface."); + } + if (support.presentModes.empty()) { + throw std::runtime_error("No present modes available for swap chain.\n" + "This may indicate GPU driver issues or incompatible surface."); + } + + logging::Logger::GetInstance().Info("Creating swapchain with size: " + std::to_string(width) + "x" + std::to_string(height)); + + if (width == 0 || height == 0) { + logging::Logger::GetInstance().Error("Invalid dimensions (" + std::to_string(width) + "x" + std::to_string(height) + ")."); + throw std::runtime_error("Invalid dimensions (" + + std::to_string(width) + "x" + std::to_string(height) + ").\n" + + "Window may be minimized or invalid."); + } + + logging::Logger::GetInstance().Debug("Surface capabilities - Min extent: " + std::to_string(support.capabilities.minImageExtent.width) + "x" + std::to_string(support.capabilities.minImageExtent.height) + + ", Max extent: " + std::to_string(support.capabilities.maxImageExtent.width) + "x" + std::to_string(support.capabilities.maxImageExtent.height) + + ", Min images: " + std::to_string(support.capabilities.minImageCount) + + ", Max images: " + std::to_string(support.capabilities.maxImageCount)); + + VkSurfaceFormatKHR surfaceFormat = ChooseSurfaceFormat(support.formats); + VkPresentModeKHR presentMode = ChoosePresentMode(support.presentModes); + VkExtent2D extent = ChooseExtent(support.capabilities, width, height); + + uint32_t imageCount = support.capabilities.minImageCount + 1; + if (support.capabilities.maxImageCount > 0 && imageCount > support.capabilities.maxImageCount) { + imageCount = support.capabilities.maxImageCount; + } + logging::Logger::GetInstance().TraceVariable("imageCount", static_cast(imageCount)); + + VkSwapchainCreateInfoKHR createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; + createInfo.surface = surface; + createInfo.minImageCount = imageCount; + createInfo.imageFormat = surfaceFormat.format; + createInfo.imageColorSpace = surfaceFormat.colorSpace; + createInfo.imageExtent = extent; + createInfo.imageArrayLayers = 1; + createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + + QueueFamilyIndices indices = deviceService_->GetQueueFamilies(); + uint32_t queueFamilyIndices[] = {indices.graphicsFamily, indices.presentFamily}; + if (indices.graphicsFamily != indices.presentFamily) { + createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT; + createInfo.queueFamilyIndexCount = 2; + createInfo.pQueueFamilyIndices = queueFamilyIndices; + } else { + createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + } + + createInfo.preTransform = support.capabilities.currentTransform; + createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + createInfo.presentMode = presentMode; + createInfo.clipped = VK_TRUE; + + if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapchain_) != VK_SUCCESS) { + throw std::runtime_error("Failed to create swap chain"); + } + + vkGetSwapchainImagesKHR(device, swapchain_, &imageCount, nullptr); + images_.resize(imageCount); + vkGetSwapchainImagesKHR(device, swapchain_, &imageCount, images_.data()); + + imageFormat_ = surfaceFormat.format; + extent_ = extent; + + CreateImageViews(); + CreateRenderPass(); + CreateFramebuffers(); +} + +void SwapchainService::RecreateSwapchain(uint32_t width, uint32_t height) { + logging::TraceGuard trace; + + logging::Logger::GetInstance().Info("Recreating swapchain: " + std::to_string(width) + "x" + std::to_string(height)); + + deviceService_->WaitIdle(); + CleanupSwapchainInternal(); + CreateSwapchain(width, height); +} + +VkResult SwapchainService::AcquireNextImage(VkSemaphore semaphore, uint32_t& imageIndex) { + auto device = deviceService_->GetDevice(); + return vkAcquireNextImageKHR(device, swapchain_, UINT64_MAX, semaphore, + VK_NULL_HANDLE, &imageIndex); +} + +VkResult SwapchainService::Present(const std::vector& waitSemaphores, + uint32_t imageIndex) { + auto presentQueue = deviceService_->GetPresentQueue(); + + VkPresentInfoKHR presentInfo{}; + presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + presentInfo.waitSemaphoreCount = static_cast(waitSemaphores.size()); + presentInfo.pWaitSemaphores = waitSemaphores.data(); + presentInfo.swapchainCount = 1; + presentInfo.pSwapchains = &swapchain_; + presentInfo.pImageIndices = &imageIndex; + + return vkQueuePresentKHR(presentQueue, &presentInfo); +} + +void SwapchainService::CreateImageViews() { + logging::TraceGuard trace; + + auto device = deviceService_->GetDevice(); + + imageViews_.resize(images_.size()); + for (size_t i = 0; i < images_.size(); ++i) { + VkImageViewCreateInfo viewInfo{}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = images_[i]; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = imageFormat_; + viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, + VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + 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, &imageViews_[i]) != VK_SUCCESS) { + throw std::runtime_error("Failed to create image views"); + } + } +} + +void SwapchainService::CreateRenderPass() { + logging::TraceGuard trace; + + auto device = deviceService_->GetDevice(); + + VkAttachmentDescription colorAttachment{}; + colorAttachment.format = imageFormat_; + 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_UNDEFINED; + colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + + VkAttachmentReference colorAttachmentRef{}; + colorAttachmentRef.attachment = 0; + colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &colorAttachmentRef; + + VkSubpassDependency dependency{}; + dependency.srcSubpass = VK_SUBPASS_EXTERNAL; + dependency.dstSubpass = 0; + dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dependency.srcAccessMask = 0; + dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + + VkRenderPassCreateInfo renderPassInfo{}; + renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + renderPassInfo.attachmentCount = 1; + renderPassInfo.pAttachments = &colorAttachment; + renderPassInfo.subpassCount = 1; + renderPassInfo.pSubpasses = &subpass; + renderPassInfo.dependencyCount = 1; + renderPassInfo.pDependencies = &dependency; + + if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass_) != VK_SUCCESS) { + throw std::runtime_error("Failed to create render pass"); + } +} + +void SwapchainService::CreateFramebuffers() { + logging::TraceGuard trace; + + auto device = deviceService_->GetDevice(); + + framebuffers_.resize(imageViews_.size()); + for (size_t i = 0; i < imageViews_.size(); ++i) { + VkImageView attachments[] = {imageViews_[i]}; + + VkFramebufferCreateInfo framebufferInfo{}; + framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + framebufferInfo.renderPass = renderPass_; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = attachments; + framebufferInfo.width = extent_.width; + framebufferInfo.height = extent_.height; + framebufferInfo.layers = 1; + + if (vkCreateFramebuffer(device, &framebufferInfo, nullptr, &framebuffers_[i]) != VK_SUCCESS) { + throw std::runtime_error("Failed to create framebuffer"); + } + } +} + +void SwapchainService::CleanupSwapchainInternal() { + logging::TraceGuard trace; + + auto device = deviceService_->GetDevice(); + + for (auto framebuffer : framebuffers_) { + vkDestroyFramebuffer(device, framebuffer, nullptr); + } + framebuffers_.clear(); + + if (renderPass_ != VK_NULL_HANDLE) { + vkDestroyRenderPass(device, renderPass_, nullptr); + renderPass_ = VK_NULL_HANDLE; + } + + for (auto imageView : imageViews_) { + vkDestroyImageView(device, imageView, nullptr); + } + imageViews_.clear(); + + if (swapchain_ != VK_NULL_HANDLE) { + vkDestroySwapchainKHR(device, swapchain_, nullptr); + swapchain_ = VK_NULL_HANDLE; + } +} + +void SwapchainService::CleanupSwapchain() { + CleanupSwapchainInternal(); +} + +void SwapchainService::Shutdown() noexcept { + CleanupSwapchainInternal(); +} + +SwapchainService::SwapchainSupportDetails SwapchainService::QuerySwapchainSupport( + VkPhysicalDevice device, VkSurfaceKHR surface) { + logging::TraceGuard trace; + + SwapchainSupportDetails details; + vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities); + + uint32_t formatCount; + vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr); + if (formatCount != 0) { + details.formats.resize(formatCount); + vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data()); + } + + uint32_t presentModeCount; + vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr); + if (presentModeCount != 0) { + details.presentModes.resize(presentModeCount); + vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, + details.presentModes.data()); + } + + return details; +} + +VkSurfaceFormatKHR SwapchainService::ChooseSurfaceFormat( + const std::vector& availableFormats) { + logging::TraceGuard trace; + + for (const auto& availableFormat : availableFormats) { + if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && + availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { + return availableFormat; + } + } + return availableFormats[0]; +} + +VkPresentModeKHR SwapchainService::ChoosePresentMode( + const std::vector& availablePresentModes) { + logging::TraceGuard trace; + + for (const auto& availablePresentMode : availablePresentModes) { + if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) { + return availablePresentMode; + } + } + return VK_PRESENT_MODE_FIFO_KHR; +} + +VkExtent2D SwapchainService::ChooseExtent(const VkSurfaceCapabilitiesKHR& capabilities, + uint32_t width, uint32_t height) { + return VkExtent2D{ + std::clamp(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width), + std::clamp(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height) + }; +} + +void SwapchainService::OnWindowResized(const events::Event& event) { + logging::TraceGuard trace; + logging::Logger::GetInstance().Info("Window resized event received, swapchain recreation needed"); +} + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/swapchain_service.hpp b/src/services/impl/swapchain_service.hpp new file mode 100644 index 0000000..cd4538e --- /dev/null +++ b/src/services/impl/swapchain_service.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include "../interfaces/i_swapchain_service.hpp" +#include "../interfaces/i_vulkan_device_service.hpp" +#include "../../di/lifecycle.hpp" +#include "../../events/event_bus.hpp" +#include +#include + +namespace sdl3cpp::services::impl { + +/** + * @brief Swapchain service implementation. + * + * Small, focused service (~250 lines) for Vulkan swapchain management. + * Handles swapchain creation, recreation, image views, render pass, and framebuffers. + */ +class SwapchainService : public ISwapchainService, + public di::IInitializable, + public di::IShutdownable { +public: + explicit SwapchainService(std::shared_ptr deviceService, + std::shared_ptr eventBus); + ~SwapchainService() override; + + // ISwapchainService interface + void CreateSwapchain(uint32_t width, uint32_t height) override; + void RecreateSwapchain(uint32_t width, uint32_t height) override; + void CleanupSwapchain() override; + + VkResult AcquireNextImage(VkSemaphore semaphore, uint32_t& imageIndex) override; + VkResult Present(const std::vector& waitSemaphores, + uint32_t imageIndex) override; + + VkSwapchainKHR GetSwapchain() const override { return swapchain_; } + const std::vector& GetSwapchainImages() const override { return images_; } + const std::vector& GetSwapchainImageViews() const override { return imageViews_; } + const std::vector& GetSwapchainFramebuffers() const override { return framebuffers_; } + VkFormat GetSwapchainImageFormat() const override { return imageFormat_; } + VkExtent2D GetSwapchainExtent() const override { return extent_; } + VkRenderPass GetRenderPass() const override { return renderPass_; } + + // IInitializable interface + void Initialize() override; + + // IShutdownable interface + void Shutdown() noexcept override; + +private: + std::shared_ptr deviceService_; + std::shared_ptr eventBus_; + + VkSwapchainKHR swapchain_ = VK_NULL_HANDLE; + std::vector images_; + std::vector imageViews_; + VkFormat imageFormat_ = VK_FORMAT_UNDEFINED; + VkExtent2D extent_{}; + VkRenderPass renderPass_ = VK_NULL_HANDLE; + std::vector framebuffers_; + + uint32_t currentWidth_ = 0; + uint32_t currentHeight_ = 0; + + // Helper methods + struct SwapchainSupportDetails { + VkSurfaceCapabilitiesKHR capabilities{}; + std::vector formats; + std::vector presentModes; + }; + + SwapchainSupportDetails QuerySwapchainSupport(VkPhysicalDevice device, VkSurfaceKHR surface); + VkSurfaceFormatKHR ChooseSurfaceFormat(const std::vector& availableFormats); + VkPresentModeKHR ChoosePresentMode(const std::vector& availablePresentModes); + VkExtent2D ChooseExtent(const VkSurfaceCapabilitiesKHR& capabilities, uint32_t width, uint32_t height); + + void CreateImageViews(); + void CreateRenderPass(); + void CreateFramebuffers(); + void CleanupSwapchainInternal(); + + void OnWindowResized(const events::Event& event); +}; + +} // namespace sdl3cpp::services::impl