diff --git a/include/rendering/character_preview.hpp b/include/rendering/character_preview.hpp index 079c299c..7ff5352c 100644 --- a/include/rendering/character_preview.hpp +++ b/include/rendering/character_preview.hpp @@ -89,7 +89,7 @@ private: bool modelLoaded_ = false; bool compositeRequested_ = false; bool compositeRendered_ = false; // True after first successful compositePass - float modelYaw_ = 180.0f; + float modelYaw_ = 90.0f; // Cached info from loadCharacter() for later recompositing. game::Race race_ = game::Race::HUMAN; diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index 2c73129a..9ee98a7d 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -45,7 +45,8 @@ public: ~CharacterRenderer(); bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* am, - VkRenderPass renderPassOverride = VK_NULL_HANDLE); + VkRenderPass renderPassOverride = VK_NULL_HANDLE, + VkSampleCountFlagBits msaaSamples = VK_SAMPLE_COUNT_1_BIT); void shutdown(); void setAssetManager(pipeline::AssetManager* am) { assetManager = am; } @@ -230,6 +231,7 @@ public: private: VkContext* vkCtx_ = nullptr; VkRenderPass renderPassOverride_ = VK_NULL_HANDLE; + VkSampleCountFlagBits msaaSamplesOverride_ = VK_SAMPLE_COUNT_1_BIT; pipeline::AssetManager* assetManager = nullptr; // Vulkan pipelines (one per blend mode) diff --git a/include/rendering/vk_render_target.hpp b/include/rendering/vk_render_target.hpp index 3acbc561..ffa1cd4f 100644 --- a/include/rendering/vk_render_target.hpp +++ b/include/rendering/vk_render_target.hpp @@ -13,6 +13,7 @@ class VkContext; /** * Off-screen render target encapsulating VkRenderPass + VkFramebuffer + color VkImage. * Used for minimap compositing, world map compositing, and other off-screen passes. + * Supports optional MSAA with automatic resolve. */ class VkRenderTarget { public: @@ -26,9 +27,11 @@ public: * Create the render target with given dimensions and format. * Creates: color image, image view, sampler, render pass, framebuffer. * When withDepth is true, also creates a D32_SFLOAT depth attachment. + * When msaaSamples > 1, creates multisampled images and a resolve attachment. */ bool create(VkContext& ctx, uint32_t width, uint32_t height, - VkFormat format = VK_FORMAT_R8G8B8A8_UNORM, bool withDepth = false); + VkFormat format = VK_FORMAT_R8G8B8A8_UNORM, bool withDepth = false, + VkSampleCountFlagBits msaaSamples = VK_SAMPLE_COUNT_1_BIT); /** * Destroy all Vulkan resources. @@ -48,14 +51,15 @@ public: */ void endPass(VkCommandBuffer cmd); - // Accessors - VkImage getColorImage() const { return colorImage_.image; } - VkImageView getColorImageView() const { return colorImage_.imageView; } + // Accessors - always return the resolved (single-sample) image for reading + VkImage getColorImage() const { return resolveImage_.image ? resolveImage_.image : colorImage_.image; } + VkImageView getColorImageView() const { return resolveImage_.imageView ? resolveImage_.imageView : colorImage_.imageView; } VkSampler getSampler() const { return sampler_; } VkRenderPass getRenderPass() const { return renderPass_; } VkExtent2D getExtent() const { return { colorImage_.extent.width, colorImage_.extent.height }; } VkFormat getFormat() const { return colorImage_.format; } bool isValid() const { return framebuffer_ != VK_NULL_HANDLE; } + VkSampleCountFlagBits getSampleCount() const { return msaaSamples_; } /** * Descriptor info for binding the color attachment as a texture in a shader. @@ -63,9 +67,11 @@ public: VkDescriptorImageInfo descriptorInfo() const; private: - AllocatedImage colorImage_{}; + AllocatedImage colorImage_{}; // MSAA color (or single-sample if no MSAA) + AllocatedImage resolveImage_{}; // Single-sample resolve target (only when MSAA) AllocatedImage depthImage_{}; bool hasDepth_ = false; + VkSampleCountFlagBits msaaSamples_ = VK_SAMPLE_COUNT_1_BIT; VkSampler sampler_ = VK_NULL_HANDLE; VkRenderPass renderPass_ = VK_NULL_HANDLE; VkFramebuffer framebuffer_ = VK_NULL_HANDLE; diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index 8288f81b..964357f4 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -58,7 +58,8 @@ bool CharacterPreview::initialize(pipeline::AssetManager* am) { // Initialize CharacterRenderer with our off-screen render pass charRenderer_ = std::make_unique(); - if (!charRenderer_->initialize(vkCtx_, perFrameLayout, am, renderTarget_->getRenderPass())) { + if (!charRenderer_->initialize(vkCtx_, perFrameLayout, am, renderTarget_->getRenderPass(), + renderTarget_->getSampleCount())) { LOG_ERROR("CharacterPreview: failed to initialize CharacterRenderer"); return false; } @@ -103,7 +104,8 @@ void CharacterPreview::createFBO() { // 1. Create off-screen render target with depth renderTarget_ = std::make_unique(); - if (!renderTarget_->create(*vkCtx_, fboWidth_, fboHeight_, VK_FORMAT_R8G8B8A8_UNORM, true)) { + if (!renderTarget_->create(*vkCtx_, fboWidth_, fboHeight_, VK_FORMAT_R8G8B8A8_UNORM, true, + VK_SAMPLE_COUNT_4_BIT)) { LOG_ERROR("CharacterPreview: failed to create render target"); renderTarget_.reset(); return; diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index ad5b0868..522f6a48 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -113,13 +113,15 @@ CharacterRenderer::~CharacterRenderer() { bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* am, - VkRenderPass renderPassOverride) { + VkRenderPass renderPassOverride, + VkSampleCountFlagBits msaaSamples) { core::Logger::getInstance().info("Initializing character renderer (Vulkan)..."); vkCtx_ = ctx; assetManager = am; perFrameLayout_ = perFrameLayout; renderPassOverride_ = renderPassOverride; + msaaSamplesOverride_ = msaaSamples; const unsigned hc = std::thread::hardware_concurrency(); const size_t availableCores = (hc > 1u) ? static_cast(hc - 1u) : 1ull; // Character updates run alongside M2/WMO work; default to a smaller share. @@ -224,7 +226,7 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram } VkRenderPass mainPass = renderPassOverride_ ? renderPassOverride_ : vkCtx_->getImGuiRenderPass(); - VkSampleCountFlagBits samples = renderPassOverride_ ? VK_SAMPLE_COUNT_1_BIT : vkCtx_->getMsaaSamples(); + VkSampleCountFlagBits samples = renderPassOverride_ ? msaaSamplesOverride_ : vkCtx_->getMsaaSamples(); // --- Vertex input --- // CharVertexGPU: vec3 pos(12) + uint8[4] boneWeights(4) + uint8[4] boneIndices(4) + @@ -2946,7 +2948,7 @@ void CharacterRenderer::recreatePipelines() { } VkRenderPass mainPass = renderPassOverride_ ? renderPassOverride_ : vkCtx_->getImGuiRenderPass(); - VkSampleCountFlagBits samples = renderPassOverride_ ? VK_SAMPLE_COUNT_1_BIT : vkCtx_->getMsaaSamples(); + VkSampleCountFlagBits samples = renderPassOverride_ ? msaaSamplesOverride_ : vkCtx_->getMsaaSamples(); // --- Vertex input --- VkVertexInputBindingDescription charBinding{}; diff --git a/src/rendering/vk_render_target.cpp b/src/rendering/vk_render_target.cpp index 338133e0..48e3a50e 100644 --- a/src/rendering/vk_render_target.cpp +++ b/src/rendering/vk_render_target.cpp @@ -9,24 +9,39 @@ VkRenderTarget::~VkRenderTarget() { // Must call destroy() explicitly with device/allocator before destruction } -bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height, VkFormat format, bool withDepth) { +bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height, + VkFormat format, bool withDepth, VkSampleCountFlagBits msaaSamples) { VkDevice device = ctx.getDevice(); VmaAllocator allocator = ctx.getAllocator(); hasDepth_ = withDepth; + msaaSamples_ = msaaSamples; + bool useMSAA = msaaSamples != VK_SAMPLE_COUNT_1_BIT; - // Create color image (COLOR_ATTACHMENT + SAMPLED for reading in subsequent passes) + // Create color image (multisampled if MSAA) colorImage_ = createImage(device, allocator, width, height, format, - VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | (useMSAA ? VkImageUsageFlags(0) : VK_IMAGE_USAGE_SAMPLED_BIT), + msaaSamples); if (!colorImage_.image) { LOG_ERROR("VkRenderTarget: failed to create color image (", width, "x", height, ")"); return false; } - // Create depth image if requested + // Create resolve image for MSAA (single-sample, sampled for reading) + if (useMSAA) { + resolveImage_ = createImage(device, allocator, width, height, format, + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); + if (!resolveImage_.image) { + LOG_ERROR("VkRenderTarget: failed to create resolve image (", width, "x", height, ")"); + destroy(device, allocator); + return false; + } + } + + // Create depth image if requested (multisampled to match color) if (withDepth) { depthImage_ = createImage(device, allocator, width, height, - VK_FORMAT_D32_SFLOAT, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + VK_FORMAT_D32_SFLOAT, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, msaaSamples); if (!depthImage_.image) { LOG_ERROR("VkRenderTarget: failed to create depth image (", width, "x", height, ")"); destroy(device, allocator); @@ -53,102 +68,185 @@ bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height, VkF } // Create render pass - VkAttachmentDescription attachments[2]{}; - // Color attachment: UNDEFINED -> COLOR_ATTACHMENT_OPTIMAL -> SHADER_READ_ONLY_OPTIMAL - attachments[0].format = format; - attachments[0].samples = VK_SAMPLE_COUNT_1_BIT; - attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; - attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; - attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - attachments[0].finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + if (useMSAA) { + // MSAA render pass: color(MSAA) + resolve(1x) + optional depth(MSAA) + // Attachment 0: MSAA color (rendered into, not stored after resolve) + // Attachment 1: resolve color (stores final resolved result) + // Attachment 2: MSAA depth (optional) + VkAttachmentDescription attachments[3]{}; - // Depth attachment (only used when withDepth) - attachments[1].format = VK_FORMAT_D32_SFLOAT; - attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; - attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; - attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + // MSAA color + attachments[0].format = format; + attachments[0].samples = msaaSamples; + attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; // resolved, don't need MSAA data + attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[0].finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - VkAttachmentReference colorRef{}; - colorRef.attachment = 0; - colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + // Resolve color + attachments[1].format = format; + attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[1].finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - VkAttachmentReference depthRef{}; - depthRef.attachment = 1; - depthRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + // MSAA depth + attachments[2].format = VK_FORMAT_D32_SFLOAT; + attachments[2].samples = msaaSamples; + attachments[2].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachments[2].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[2].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[2].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[2].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[2].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; - VkSubpassDescription subpass{}; - subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = 1; - subpass.pColorAttachments = &colorRef; - if (withDepth) subpass.pDepthStencilAttachment = &depthRef; + VkAttachmentReference colorRef{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkAttachmentReference resolveRef{1, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkAttachmentReference depthRef{2, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}; - // Dependencies - VkSubpassDependency dependencies[2]{}; - uint32_t depCount = 1; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &colorRef; + subpass.pResolveAttachments = &resolveRef; + if (withDepth) subpass.pDepthStencilAttachment = &depthRef; - // Input dependency: wait for previous fragment shader reads before writing - dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL; - dependencies[0].dstSubpass = 0; - dependencies[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; - dependencies[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - dependencies[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT; - dependencies[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + VkSubpassDependency dep{}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + if (withDepth) { + dep.dstStageMask |= VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dep.dstAccessMask |= VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + } - if (withDepth) { - dependencies[0].dstStageMask |= VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; - dependencies[0].dstAccessMask |= VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + VkRenderPassCreateInfo rpInfo{}; + rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + rpInfo.attachmentCount = withDepth ? 3u : 2u; + rpInfo.pAttachments = attachments; + rpInfo.subpassCount = 1; + rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dep; - // Output dependency (depth targets only): ensure writes complete before fragment reads - dependencies[1].srcSubpass = 0; - dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL; - dependencies[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | - VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; - dependencies[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; - dependencies[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | - VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; - dependencies[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - depCount = 2; + if (vkCreateRenderPass(device, &rpInfo, nullptr, &renderPass_) != VK_SUCCESS) { + LOG_ERROR("VkRenderTarget: failed to create MSAA render pass"); + destroy(device, allocator); + return false; + } + + // Create framebuffer: MSAA color + resolve + optional MSAA depth + VkImageView fbAttachments[3] = { colorImage_.imageView, resolveImage_.imageView, depthImage_.imageView }; + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = renderPass_; + fbInfo.attachmentCount = withDepth ? 3u : 2u; + fbInfo.pAttachments = fbAttachments; + fbInfo.width = width; + fbInfo.height = height; + fbInfo.layers = 1; + + if (vkCreateFramebuffer(device, &fbInfo, nullptr, &framebuffer_) != VK_SUCCESS) { + LOG_ERROR("VkRenderTarget: failed to create MSAA framebuffer"); + destroy(device, allocator); + return false; + } + } else { + // Non-MSAA render pass (original path) + VkAttachmentDescription attachments[2]{}; + attachments[0].format = format; + attachments[0].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[0].finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + attachments[1].format = VK_FORMAT_D32_SFLOAT; + attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + + VkAttachmentReference colorRef{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkAttachmentReference depthRef{1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}; + + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &colorRef; + if (withDepth) subpass.pDepthStencilAttachment = &depthRef; + + VkSubpassDependency dependencies[2]{}; + uint32_t depCount = 1; + dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL; + dependencies[0].dstSubpass = 0; + dependencies[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + dependencies[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dependencies[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + dependencies[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + + if (withDepth) { + dependencies[0].dstStageMask |= VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dependencies[0].dstAccessMask |= VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + dependencies[1].srcSubpass = 0; + dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL; + dependencies[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + dependencies[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + dependencies[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + dependencies[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + depCount = 2; + } + + VkRenderPassCreateInfo rpInfo{}; + rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + rpInfo.attachmentCount = withDepth ? 2u : 1u; + rpInfo.pAttachments = attachments; + rpInfo.subpassCount = 1; + rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = depCount; + rpInfo.pDependencies = dependencies; + + if (vkCreateRenderPass(device, &rpInfo, nullptr, &renderPass_) != VK_SUCCESS) { + LOG_ERROR("VkRenderTarget: failed to create render pass"); + destroy(device, allocator); + return false; + } + + VkImageView fbAttachments[2] = { colorImage_.imageView, depthImage_.imageView }; + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = renderPass_; + fbInfo.attachmentCount = withDepth ? 2u : 1u; + fbInfo.pAttachments = fbAttachments; + fbInfo.width = width; + fbInfo.height = height; + fbInfo.layers = 1; + + if (vkCreateFramebuffer(device, &fbInfo, nullptr, &framebuffer_) != VK_SUCCESS) { + LOG_ERROR("VkRenderTarget: failed to create framebuffer"); + destroy(device, allocator); + return false; + } } - VkRenderPassCreateInfo rpInfo{}; - rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; - rpInfo.attachmentCount = withDepth ? 2u : 1u; - rpInfo.pAttachments = attachments; - rpInfo.subpassCount = 1; - rpInfo.pSubpasses = &subpass; - rpInfo.dependencyCount = depCount; - rpInfo.pDependencies = dependencies; - - if (vkCreateRenderPass(device, &rpInfo, nullptr, &renderPass_) != VK_SUCCESS) { - LOG_ERROR("VkRenderTarget: failed to create render pass"); - destroy(device, allocator); - return false; - } - - // Create framebuffer - VkImageView fbAttachments[2] = { colorImage_.imageView, depthImage_.imageView }; - VkFramebufferCreateInfo fbInfo{}; - fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; - fbInfo.renderPass = renderPass_; - fbInfo.attachmentCount = withDepth ? 2u : 1u; - fbInfo.pAttachments = fbAttachments; - fbInfo.width = width; - fbInfo.height = height; - fbInfo.layers = 1; - - if (vkCreateFramebuffer(device, &fbInfo, nullptr, &framebuffer_) != VK_SUCCESS) { - LOG_ERROR("VkRenderTarget: failed to create framebuffer"); - destroy(device, allocator); - return false; - } - - LOG_INFO("VkRenderTarget created (", width, "x", height, withDepth ? ", with depth)" : ")"); + LOG_INFO("VkRenderTarget created (", width, "x", height, + withDepth ? ", depth" : "", + useMSAA ? ", MSAAx" : "", useMSAA ? std::to_string(msaaSamples) : "", ")"); return true; } @@ -165,9 +263,11 @@ void VkRenderTarget::destroy(VkDevice device, VmaAllocator allocator) { vkDestroySampler(device, sampler_, nullptr); sampler_ = VK_NULL_HANDLE; } + destroyImage(device, allocator, resolveImage_); destroyImage(device, allocator, depthImage_); destroyImage(device, allocator, colorImage_); hasDepth_ = false; + msaaSamples_ = VK_SAMPLE_COUNT_1_BIT; } void VkRenderTarget::beginPass(VkCommandBuffer cmd, const VkClearColorValue& clear) { @@ -178,10 +278,18 @@ void VkRenderTarget::beginPass(VkCommandBuffer cmd, const VkClearColorValue& cle rpBegin.renderArea.offset = {0, 0}; rpBegin.renderArea.extent = getExtent(); - VkClearValue clearValues[2]{}; - clearValues[0].color = clear; - clearValues[1].depthStencil = {1.0f, 0}; - rpBegin.clearValueCount = hasDepth_ ? 2u : 1u; + VkClearValue clearValues[3]{}; + clearValues[0].color = clear; // MSAA color (or single-sample color) + clearValues[1].color = clear; // resolve (only used for MSAA) + clearValues[2].depthStencil = {1.0f, 0}; // depth + + bool useMSAA = msaaSamples_ != VK_SAMPLE_COUNT_1_BIT; + if (useMSAA) { + rpBegin.clearValueCount = hasDepth_ ? 3u : 2u; + } else { + clearValues[1].depthStencil = {1.0f, 0}; // depth is attachment 1 in non-MSAA + rpBegin.clearValueCount = hasDepth_ ? 2u : 1u; + } rpBegin.pClearValues = clearValues; vkCmdBeginRenderPass(cmd, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); @@ -210,7 +318,8 @@ void VkRenderTarget::endPass(VkCommandBuffer cmd) { VkDescriptorImageInfo VkRenderTarget::descriptorInfo() const { VkDescriptorImageInfo info{}; info.sampler = sampler_; - info.imageView = colorImage_.imageView; + // Always return the resolved (single-sample) image for shader reads + info.imageView = resolveImage_.imageView ? resolveImage_.imageView : colorImage_.imageView; info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; return info; }