From 5e7de35c52c7440a20e7308df8d8925fd4b7cba6 Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Fri, 12 Dec 2025 16:40:53 +0900 Subject: [PATCH] ADD: aspect ratio preserving completed --- shaders/present_letterbox.frag | 36 ++++++++++ src/core/device/images.cpp | 86 ++++++++++++++++++++++++ src/core/device/images.h | 10 +++ src/core/device/swapchain.cpp | 98 ++++++++------------------- src/core/device/swapchain.h | 3 + src/core/engine.cpp | 60 +++++++++++++---- src/core/engine.h | 4 ++ src/render/graph/graph.cpp | 117 ++++++++++++++++++++++++++++----- src/render/passes/fxaa.cpp | 17 ++++- src/render/passes/tonemap.cpp | 17 ++++- src/scene/vk_scene.cpp | 18 ++++- 11 files changed, 356 insertions(+), 110 deletions(-) create mode 100644 shaders/present_letterbox.frag diff --git a/shaders/present_letterbox.frag b/shaders/present_letterbox.frag new file mode 100644 index 0000000..78945d8 --- /dev/null +++ b/shaders/present_letterbox.frag @@ -0,0 +1,36 @@ +#version 450 + +layout(location=0) in vec2 inUV; +layout(location=0) out vec4 outColor; + +layout(set=0, binding=0) uniform sampler2D uSrc; + +layout(push_constant) uniform Push +{ + vec2 rect_min; // normalized (0..1) min corner in swapchain UV space + vec2 rect_size; // normalized size in swapchain UV space +} pc; + +void main() +{ + if (pc.rect_size.x <= 0.0 || pc.rect_size.y <= 0.0) + { + outColor = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + + vec2 rect_max = pc.rect_min + pc.rect_size; + vec2 uv = inUV; + + if (uv.x < pc.rect_min.x || uv.y < pc.rect_min.y || uv.x > rect_max.x || uv.y > rect_max.y) + { + outColor = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + + vec2 local = (uv - pc.rect_min) / pc.rect_size; + local = clamp(local, vec2(0.0), vec2(1.0)); + + outColor = texture(uSrc, local); +} + diff --git a/src/core/device/images.cpp b/src/core/device/images.cpp index 9a6d539..1131895 100644 --- a/src/core/device/images.cpp +++ b/src/core/device/images.cpp @@ -1,6 +1,9 @@ #include #include +#include +#include + #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" @@ -137,6 +140,89 @@ void vkutil::copy_image_to_image(VkCommandBuffer cmd, VkImage source, VkImage de vkCmdBlitImage2(cmd, &blitInfo); } //< copyimg + +VkRect2D vkutil::compute_letterbox_rect(VkExtent2D srcSize, VkExtent2D dstSize) +{ + VkRect2D rect{}; + rect.offset = {0, 0}; + rect.extent = dstSize; + if (srcSize.width == 0 || srcSize.height == 0 || dstSize.width == 0 || dstSize.height == 0) + { + return rect; + } + + const double srcAspect = double(srcSize.width) / double(srcSize.height); + const double dstAspect = double(dstSize.width) / double(dstSize.height); + + if (dstAspect > srcAspect) + { + // Fit by height, bars on left/right. + const double scale = double(dstSize.height) / double(srcSize.height); + uint32_t scaledWidth = static_cast(std::lround(double(srcSize.width) * scale)); + scaledWidth = std::min(scaledWidth, dstSize.width); + const uint32_t offsetX = (dstSize.width - scaledWidth) / 2u; + rect.offset = {static_cast(offsetX), 0}; + rect.extent = {scaledWidth, dstSize.height}; + } + else + { + // Fit by width, bars on top/bottom. + const double scale = double(dstSize.width) / double(srcSize.width); + uint32_t scaledHeight = static_cast(std::lround(double(srcSize.height) * scale)); + scaledHeight = std::min(scaledHeight, dstSize.height); + const uint32_t offsetY = (dstSize.height - scaledHeight) / 2u; + rect.offset = {0, static_cast(offsetY)}; + rect.extent = {dstSize.width, scaledHeight}; + } + + return rect; +} + +void vkutil::copy_image_to_image_letterboxed(VkCommandBuffer cmd, + VkImage source, + VkImage destination, + VkExtent2D srcSize, + VkExtent2D dstSize, + VkFilter filter) +{ + VkRect2D dstRect = compute_letterbox_rect(srcSize, dstSize); + + VkImageBlit2 blitRegion{ .sType = VK_STRUCTURE_TYPE_IMAGE_BLIT_2, .pNext = nullptr }; + + blitRegion.srcOffsets[1].x = static_cast(srcSize.width); + blitRegion.srcOffsets[1].y = static_cast(srcSize.height); + blitRegion.srcOffsets[1].z = 1; + + blitRegion.dstOffsets[0].x = dstRect.offset.x; + blitRegion.dstOffsets[0].y = dstRect.offset.y; + blitRegion.dstOffsets[0].z = 0; + + blitRegion.dstOffsets[1].x = dstRect.offset.x + static_cast(dstRect.extent.width); + blitRegion.dstOffsets[1].y = dstRect.offset.y + static_cast(dstRect.extent.height); + blitRegion.dstOffsets[1].z = 1; + + blitRegion.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + blitRegion.srcSubresource.baseArrayLayer = 0; + blitRegion.srcSubresource.layerCount = 1; + blitRegion.srcSubresource.mipLevel = 0; + + blitRegion.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + blitRegion.dstSubresource.baseArrayLayer = 0; + blitRegion.dstSubresource.layerCount = 1; + blitRegion.dstSubresource.mipLevel = 0; + + VkBlitImageInfo2 blitInfo{ .sType = VK_STRUCTURE_TYPE_BLIT_IMAGE_INFO_2, .pNext = nullptr }; + blitInfo.dstImage = destination; + blitInfo.dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + blitInfo.srcImage = source; + blitInfo.srcImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + blitInfo.filter = filter; + blitInfo.regionCount = 1; + blitInfo.pRegions = &blitRegion; + + vkCmdBlitImage2(cmd, &blitInfo); +} + //> mipgen static inline int compute_full_mip_count(VkExtent2D imageSize) { diff --git a/src/core/device/images.h b/src/core/device/images.h index 592dbe9..da0c2fd 100644 --- a/src/core/device/images.h +++ b/src/core/device/images.h @@ -5,7 +5,17 @@ namespace vkutil { void transition_image(VkCommandBuffer cmd, VkImage image, VkImageLayout currentLayout, VkImageLayout newLayout); + // Compute a letterboxed destination rect inside dstSize that preserves srcSize aspect ratio. + VkRect2D compute_letterbox_rect(VkExtent2D srcSize, VkExtent2D dstSize); + void copy_image_to_image(VkCommandBuffer cmd, VkImage source, VkImage destination, VkExtent2D srcSize, VkExtent2D dstSize); + // Blit source into a letterboxed rect in destination (preserves aspect ratio). + void copy_image_to_image_letterboxed(VkCommandBuffer cmd, + VkImage source, + VkImage destination, + VkExtent2D srcSize, + VkExtent2D dstSize, + VkFilter filter = VK_FILTER_LINEAR); void generate_mipmaps(VkCommandBuffer cmd, VkImage image, VkExtent2D imageSize); // Variant that generates exactly mipLevels levels (starting at base level 0). void generate_mipmaps_levels(VkCommandBuffer cmd, VkImage image, VkExtent2D imageSize, int mipLevels); diff --git a/src/core/device/swapchain.cpp b/src/core/device/swapchain.cpp index 85900aa..1a7bd01 100644 --- a/src/core/device/swapchain.cpp +++ b/src/core/device/swapchain.cpp @@ -23,9 +23,9 @@ void SwapchainManager::init_swapchain() // On creation we also push a cleanup lambda to _deletionQueue for final shutdown. // On resize we will flush that queue first to destroy previous resources. - // depth/draw/gbuffer sized to current window extent + // depth/draw/gbuffer sized to fixed logical render extent (letterboxed) auto create_frame_images = [this]() { - VkExtent3D drawImageExtent = { _windowExtent.width, _windowExtent.height, 1 }; + VkExtent3D drawImageExtent = { kRenderWidth, kRenderHeight, 1 }; // Draw HDR target _drawImage.imageFormat = VK_FORMAT_R16G16B16A16_SFLOAT; @@ -118,7 +118,7 @@ void SwapchainManager::create_swapchain(uint32_t width, uint32_t height) //use vsync present mode .set_desired_present_mode(VK_PRESENT_MODE_FIFO_KHR) .set_desired_extent(width, height) - .add_image_usage_flags(VK_IMAGE_USAGE_TRANSFER_DST_BIT) + .add_image_usage_flags(VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) .build() .value(); @@ -127,6 +127,7 @@ void SwapchainManager::create_swapchain(uint32_t width, uint32_t height) _swapchain = vkbSwapchain.swapchain; _swapchainImages = vkbSwapchain.get_images().value(); _swapchainImageViews = vkbSwapchain.get_image_views().value(); + _swapchainImageLayouts.assign(_swapchainImages.size(), VK_IMAGE_LAYOUT_UNDEFINED); } void SwapchainManager::destroy_swapchain() const @@ -142,83 +143,36 @@ void SwapchainManager::destroy_swapchain() const void SwapchainManager::resize_swapchain(struct SDL_Window *window) { + int w, h; + // HiDPI-aware drawable size for correct pixel dimensions + SDL_Vulkan_GetDrawableSize(window, &w, &h); + if (w <= 0 || h <= 0) + { + // Window may be minimized or in a transient resize state; keep current swapchain. + resize_requested = true; + return; + } + vkDeviceWaitIdle(_deviceManager->device()); destroy_swapchain(); - // Destroy per-frame images before recreating them - _deletionQueue.flush(); - - int w, h; - // HiDPI-aware drawable size for correct pixel dimensions - SDL_Vulkan_GetDrawableSize(window, &w, &h); _windowExtent.width = w; _windowExtent.height = h; create_swapchain(_windowExtent.width, _windowExtent.height); - // Recreate frame images at the new size - // (duplicate the same logic used at init time) - VkExtent3D drawImageExtent = { _windowExtent.width, _windowExtent.height, 1 }; - - _drawImage.imageFormat = VK_FORMAT_R16G16B16A16_SFLOAT; - _drawImage.imageExtent = drawImageExtent; - - VkImageUsageFlags drawImageUsages{}; - drawImageUsages |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; - drawImageUsages |= VK_IMAGE_USAGE_STORAGE_BIT; - drawImageUsages |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; - drawImageUsages |= VK_IMAGE_USAGE_SAMPLED_BIT; - - VkImageCreateInfo rimg_info = vkinit::image_create_info(_drawImage.imageFormat, drawImageUsages, drawImageExtent); - VmaAllocationCreateInfo rimg_allocinfo = {}; - rimg_allocinfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; - rimg_allocinfo.requiredFlags = static_cast(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - vmaCreateImage(_deviceManager->allocator(), &rimg_info, &rimg_allocinfo, &_drawImage.image, &_drawImage.allocation, - nullptr); - - VkImageViewCreateInfo rview_info = vkinit::imageview_create_info(_drawImage.imageFormat, _drawImage.image, - VK_IMAGE_ASPECT_COLOR_BIT); - VK_CHECK(vkCreateImageView(_deviceManager->device(), &rview_info, nullptr, &_drawImage.imageView)); - - _depthImage.imageFormat = VK_FORMAT_D32_SFLOAT; - _depthImage.imageExtent = drawImageExtent; - VkImageUsageFlags depthImageUsages{}; - depthImageUsages |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; - VkImageCreateInfo dimg_info = vkinit::image_create_info(_depthImage.imageFormat, depthImageUsages, drawImageExtent); - vmaCreateImage(_deviceManager->allocator(), &dimg_info, &rimg_allocinfo, &_depthImage.image, - &_depthImage.allocation, nullptr); - - VkImageViewCreateInfo dview_info = vkinit::imageview_create_info(_depthImage.imageFormat, _depthImage.image, - VK_IMAGE_ASPECT_DEPTH_BIT); - VK_CHECK(vkCreateImageView(_deviceManager->device(), &dview_info, nullptr, &_depthImage.imageView)); - - _gBufferPosition = _resourceManager->create_image(drawImageExtent, VK_FORMAT_R16G16B16A16_SFLOAT, - VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); - _gBufferNormal = _resourceManager->create_image(drawImageExtent, VK_FORMAT_R16G16B16A16_SFLOAT, - VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); - _gBufferAlbedo = _resourceManager->create_image(drawImageExtent, VK_FORMAT_R8G8B8A8_UNORM, - VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); - _gBufferExtra = _resourceManager->create_image(drawImageExtent, VK_FORMAT_R16G16B16A16_SFLOAT, - VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); - _idBuffer = _resourceManager->create_image(drawImageExtent, VK_FORMAT_R32_UINT, - VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | - VK_IMAGE_USAGE_TRANSFER_SRC_BIT | - VK_IMAGE_USAGE_SAMPLED_BIT); - - _deletionQueue.push_function([=]() { - vkDestroyImageView(_deviceManager->device(), _drawImage.imageView, nullptr); - vmaDestroyImage(_deviceManager->allocator(), _drawImage.image, _drawImage.allocation); - - vkDestroyImageView(_deviceManager->device(), _depthImage.imageView, nullptr); - vmaDestroyImage(_deviceManager->allocator(), _depthImage.image, _depthImage.allocation); - - _resourceManager->destroy_image(_gBufferPosition); - _resourceManager->destroy_image(_gBufferNormal); - _resourceManager->destroy_image(_gBufferAlbedo); - _resourceManager->destroy_image(_gBufferExtra); - _resourceManager->destroy_image(_idBuffer); - }); - resize_requested = false; } + +VkImageLayout SwapchainManager::swapchain_image_layout(uint32_t index) const +{ + if (index >= _swapchainImageLayouts.size()) return VK_IMAGE_LAYOUT_UNDEFINED; + return _swapchainImageLayouts[index]; +} + +void SwapchainManager::set_swapchain_image_layout(uint32_t index, VkImageLayout layout) +{ + if (index >= _swapchainImageLayouts.size()) return; + _swapchainImageLayouts[index] = layout; +} diff --git a/src/core/device/swapchain.h b/src/core/device/swapchain.h index 6c7fc4b..e0410d6 100644 --- a/src/core/device/swapchain.h +++ b/src/core/device/swapchain.h @@ -24,6 +24,8 @@ public: VkExtent2D swapchainExtent() const { return _swapchainExtent; } const std::vector &swapchainImages() const { return _swapchainImages; } const std::vector &swapchainImageViews() const { return _swapchainImageViews; } + VkImageLayout swapchain_image_layout(uint32_t index) const; + void set_swapchain_image_layout(uint32_t index, VkImageLayout layout); AllocatedImage drawImage() const { return _drawImage; } AllocatedImage depthImage() const { return _depthImage; } @@ -47,6 +49,7 @@ private: std::vector _swapchainImages; std::vector _swapchainImageViews; + std::vector _swapchainImageLayouts; AllocatedImage _drawImage = {}; AllocatedImage _depthImage = {}; diff --git a/src/core/engine.cpp b/src/core/engine.cpp index 3959876..66c6f55 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -759,19 +759,32 @@ void VulkanEngine::draw() uint32_t swapchainImageIndex; - VkResult e = vkAcquireNextImageKHR(_deviceManager->device(), _swapchainManager->swapchain(), 1000000000, + VkResult e = vkAcquireNextImageKHR(_deviceManager->device(), + _swapchainManager->swapchain(), + 1000000000, get_current_frame()._swapchainSemaphore, - nullptr, &swapchainImageIndex); + nullptr, + &swapchainImageIndex); if (e == VK_ERROR_OUT_OF_DATE_KHR) { resize_requested = true; return; } + if (e == VK_SUBOPTIMAL_KHR) + { + // Acquire succeeded and signaled the semaphore. Keep rendering this frame + // so the semaphore gets waited on, but schedule a resize soon. + resize_requested = true; + } + else + { + VK_CHECK(e); + } - _drawExtent.height = std::min(_swapchainManager->swapchainExtent().height, - _swapchainManager->drawImage().imageExtent.height) * renderScale; - _drawExtent.width = std::min(_swapchainManager->swapchainExtent().width, - _swapchainManager->drawImage().imageExtent.width) * renderScale; + // Fixed logical render resolution (letterboxed): draw extent is derived + // from the engine's logical render size instead of the swapchain/window. + _drawExtent.width = static_cast(static_cast(_logicalRenderExtent.width) * renderScale); + _drawExtent.height = static_cast(static_cast(_logicalRenderExtent.height) * renderScale); VK_CHECK(vkResetFences(_deviceManager->device(), 1, &get_current_frame()._renderFence)); @@ -1041,7 +1054,11 @@ void VulkanEngine::draw() presentInfo.pImageIndices = &swapchainImageIndex; VkResult presentResult = vkQueuePresentKHR(_deviceManager->graphicsQueue(), &presentInfo); - if (presentResult == VK_ERROR_OUT_OF_DATE_KHR) + if (_swapchainManager) + { + _swapchainManager->set_swapchain_image_layout(swapchainImageIndex, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + } + if (presentResult == VK_ERROR_OUT_OF_DATE_KHR || presentResult == VK_SUBOPTIMAL_KHR) { resize_requested = true; } @@ -1065,13 +1082,23 @@ void VulkanEngine::run() if (e.type == SDL_QUIT) bQuit = true; if (e.type == SDL_WINDOWEVENT) { - if (e.window.event == SDL_WINDOWEVENT_MINIMIZED) + switch (e.window.event) { - freeze_rendering = true; - } - if (e.window.event == SDL_WINDOWEVENT_RESTORED) - { - freeze_rendering = false; + case SDL_WINDOWEVENT_MINIMIZED: + freeze_rendering = true; + break; + case SDL_WINDOWEVENT_RESTORED: + freeze_rendering = false; + resize_requested = true; + _last_resize_event_ms = SDL_GetTicks(); + break; + case SDL_WINDOWEVENT_SIZE_CHANGED: + case SDL_WINDOWEVENT_RESIZED: + resize_requested = true; + _last_resize_event_ms = SDL_GetTicks(); + break; + default: + break; } } if (e.type == SDL_MOUSEMOTION) @@ -1190,7 +1217,12 @@ void VulkanEngine::run() } if (resize_requested) { - _swapchainManager->resize_swapchain(_window); + const uint32_t now_ms = SDL_GetTicks(); + if (now_ms - _last_resize_event_ms >= RESIZE_DEBOUNCE_MS) + { + _swapchainManager->resize_swapchain(_window); + resize_requested = false; + } } // Begin frame: wait for the GPU, resolve pending ID-buffer picks, diff --git a/src/core/engine.h b/src/core/engine.h index 0c9b1a9..7080bac 100644 --- a/src/core/engine.h +++ b/src/core/engine.h @@ -251,4 +251,8 @@ private: void init_mesh_pipeline(); void init_default_data(); + + // Debounce swapchain recreation during live window resizing. + uint32_t _last_resize_event_ms{0}; + static constexpr uint32_t RESIZE_DEBOUNCE_MS = 150; }; diff --git a/src/render/graph/graph.cpp b/src/render/graph/graph.cpp index b921003..449cdf8 100644 --- a/src/render/graph/graph.cpp +++ b/src/render/graph/graph.cpp @@ -2,12 +2,18 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include #include #include +#include #include #include @@ -17,6 +23,8 @@ #include "core/device/device.h" #include +#include "assets/manager.h" + void RenderGraph::init(EngineContext *ctx) { _context = ctx; @@ -694,11 +702,17 @@ void RenderGraph::execute(VkCommandBuffer cmd) VkRenderingAttachmentInfo depthInfo{}; bool hasDepth = false; - // Choose renderArea as the min of all attachment extents and the desired draw extent - VkExtent2D chosenExtent{_context->getDrawExtent()}; + // Choose renderArea as the min of all attachment extents. + // Do not pre-clamp to drawExtent here: swapchain passes (ImGui, present) + // should be able to use the full window extent. + VkExtent2D chosenExtent{0, 0}; auto clamp_min = [](VkExtent2D a, VkExtent2D b) { return VkExtent2D{std::min(a.width, b.width), std::min(a.height, b.height)}; }; + auto set_or_clamp = [&](VkExtent2D e) { + if (chosenExtent.width == 0 || chosenExtent.height == 0) chosenExtent = e; + else chosenExtent = clamp_min(chosenExtent, e); + }; // Resolve color attachments VkExtent2D firstColorExtent{0,0}; @@ -714,7 +728,7 @@ void RenderGraph::execute(VkCommandBuffer cmd) VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); if (!a.store) info.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; colorInfos.push_back(info); - if (rec->extent.width && rec->extent.height) chosenExtent = clamp_min(chosenExtent, rec->extent); + if (rec->extent.width && rec->extent.height) set_or_clamp(rec->extent); if (firstColorExtent.width == 0 && firstColorExtent.height == 0) { firstColorExtent = rec->extent; @@ -748,10 +762,15 @@ void RenderGraph::execute(VkCommandBuffer cmd) else depthInfo.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; if (!p.depthAttachment.store) depthInfo.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; hasDepth = true; - if (rec->extent.width && rec->extent.height) chosenExtent = clamp_min(chosenExtent, rec->extent); + if (rec->extent.width && rec->extent.height) set_or_clamp(rec->extent); } } + if (chosenExtent.width == 0 || chosenExtent.height == 0) + { + chosenExtent = _context->getDrawExtent(); + } + VkRenderingInfo ri{}; ri.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; ri.renderArea = VkRect2D{VkOffset2D{0, 0}, chosenExtent}; @@ -800,17 +819,86 @@ void RenderGraph::add_present_chain(RGImageHandle sourceDraw, if (!sourceDraw.valid() || !targetSwapchain.valid()) return; add_pass( - "CopyToSwapchain", - RGPassType::Transfer, + "PresentLetterbox", + RGPassType::Graphics, [sourceDraw, targetSwapchain](RGPassBuilder &builder, EngineContext *) { - builder.read(sourceDraw, RGImageUsage::TransferSrc); - builder.write(targetSwapchain, RGImageUsage::TransferDst); + builder.read(sourceDraw, RGImageUsage::SampledFragment); + VkClearValue clear{}; + clear.color = {{0.f, 0.f, 0.f, 1.f}}; + builder.write_color(targetSwapchain, true, clear); }, [sourceDraw, targetSwapchain](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx) { - VkImage src = res.image(sourceDraw); - VkImage dst = res.image(targetSwapchain); - if (src == VK_NULL_HANDLE || dst == VK_NULL_HANDLE) return; - vkutil::copy_image_to_image(cmd, src, dst, ctx->getDrawExtent(), ctx->getSwapchain()->swapchainExtent()); + if (!ctx || !ctx->currentFrame || !ctx->pipelines) return; + + VkImageView srcView = res.image_view(sourceDraw); + VkImageView dstView = res.image_view(targetSwapchain); + if (srcView == VK_NULL_HANDLE || dstView == VK_NULL_HANDLE) return; + + VkPipeline pipeline = VK_NULL_HANDLE; + VkPipelineLayout layout = VK_NULL_HANDLE; + if (!ctx->pipelines->getGraphics("present_letterbox", pipeline, layout)) + { + GraphicsPipelineCreateInfo info{}; + info.vertexShaderPath = ctx->getAssets()->shaderPath("fullscreen.vert.spv"); + info.fragmentShaderPath = ctx->getAssets()->shaderPath("present_letterbox.frag.spv"); + info.setLayouts = { ctx->getDescriptorLayouts()->singleImageLayout() }; + + VkPushConstantRange pcr{}; + pcr.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + pcr.offset = 0; + pcr.size = sizeof(glm::vec4); + info.pushConstants = { pcr }; + + VkFormat swapFmt = ctx->getSwapchain()->swapchainImageFormat(); + info.configure = [swapFmt](PipelineBuilder &b) { + b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); + b.set_polygon_mode(VK_POLYGON_MODE_FILL); + b.set_cull_mode(VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE); + b.set_multisampling_none(); + b.disable_depthtest(); + b.disable_blending(); + b.set_color_attachment_format(swapFmt); + }; + + if (!ctx->pipelines->createGraphicsPipeline("present_letterbox", info)) + { + return; + } + if (!ctx->pipelines->getGraphics("present_letterbox", pipeline, layout)) + { + return; + } + } + + VkDevice device = ctx->getDevice()->device(); + VkDescriptorSetLayout setLayout = ctx->getDescriptorLayouts()->singleImageLayout(); + VkDescriptorSet set = ctx->currentFrame->_frameDescriptors.allocate(device, setLayout); + DescriptorWriter writer; + writer.write_image(0, srcView, ctx->getSamplers()->defaultLinear(), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + writer.update_set(device, set); + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &set, 0, nullptr); + + VkExtent2D srcSize = ctx->getDrawExtent(); + VkExtent2D dstSize = ctx->getSwapchain()->swapchainExtent(); + VkRect2D dstRect = vkutil::compute_letterbox_rect(srcSize, dstSize); + + float minX = dstSize.width > 0 ? float(dstRect.offset.x) / float(dstSize.width) : 0.f; + float minY = dstSize.height > 0 ? float(dstRect.offset.y) / float(dstSize.height) : 0.f; + float sizeX = dstSize.width > 0 ? float(dstRect.extent.width) / float(dstSize.width) : 1.f; + float sizeY = dstSize.height > 0 ? float(dstRect.extent.height) / float(dstSize.height) : 1.f; + + glm::vec4 pc{minX, minY, sizeX, sizeY}; + vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(glm::vec4), &pc); + + VkViewport vp{0.f, 0.f, float(dstSize.width), float(dstSize.height), 0.f, 1.f}; + VkRect2D sc{{0, 0}, dstSize}; + vkCmdSetViewport(cmd, 0, 1, &vp); + vkCmdSetScissor(cmd, 0, 1, &sc); + vkCmdDraw(cmd, 3, 1, 0, 0); }); if (appendExtra) @@ -990,9 +1078,8 @@ RGImageHandle RenderGraph::import_swapchain_image(uint32_t index) d.imageView = views[index]; d.format = _context->getSwapchain()->swapchainImageFormat(); d.extent = _context->getSwapchain()->swapchainExtent(); - // On first use after swapchain creation, images are in UNDEFINED layout. - // Start from UNDEFINED so the graph inserts the necessary transition. - d.currentLayout = VK_IMAGE_LAYOUT_UNDEFINED; + // Track actual layout across frames. After present, images are in PRESENT_SRC_KHR. + d.currentLayout = _context->getSwapchain()->swapchain_image_layout(index); return import_image(d); } diff --git a/src/render/passes/fxaa.cpp b/src/render/passes/fxaa.cpp index 833febc..3b939d5 100644 --- a/src/render/passes/fxaa.cpp +++ b/src/render/passes/fxaa.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,11 @@ void FxaaPass::init(EngineContext *context) _inputSetLayout = _context->getDescriptorLayouts()->singleImageLayout(); + const VkFormat ldrFormat = + (_context && _context->getSwapchain()) + ? _context->getSwapchain()->swapchainImageFormat() + : VK_FORMAT_B8G8R8A8_UNORM; + GraphicsPipelineCreateInfo info{}; info.vertexShaderPath = _context->getAssets()->shaderPath("fullscreen.vert.spv"); info.fragmentShaderPath = _context->getAssets()->shaderPath("fxaa.frag.spv"); @@ -34,14 +40,14 @@ void FxaaPass::init(EngineContext *context) pcr.size = sizeof(FxaaPush); info.pushConstants = { pcr }; - info.configure = [this](PipelineBuilder &b) { + info.configure = [ldrFormat](PipelineBuilder &b) { b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); b.set_polygon_mode(VK_POLYGON_MODE_FILL); b.set_cull_mode(VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE); b.set_multisampling_none(); b.disable_depthtest(); b.disable_blending(); - b.set_color_attachment_format(VK_FORMAT_R8G8B8A8_UNORM); + b.set_color_attachment_format(ldrFormat); }; _context->pipelines->createGraphicsPipeline("fxaa", info); @@ -70,9 +76,14 @@ RGImageHandle FxaaPass::register_graph(RenderGraph *graph, RGImageHandle ldrInpu return ldrInput; } + const VkFormat ldrFormat = + (_context && _context->getSwapchain()) + ? _context->getSwapchain()->swapchainImageFormat() + : VK_FORMAT_B8G8R8A8_UNORM; + RGImageDesc desc{}; desc.name = "ldr.fxaa"; - desc.format = VK_FORMAT_R8G8B8A8_UNORM; + desc.format = ldrFormat; desc.extent = _context->getDrawExtent(); desc.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT diff --git a/src/render/passes/tonemap.cpp b/src/render/passes/tonemap.cpp index 88dc806..052af71 100644 --- a/src/render/passes/tonemap.cpp +++ b/src/render/passes/tonemap.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,11 @@ void TonemapPass::init(EngineContext *context) _inputSetLayout = _context->getDescriptorLayouts()->singleImageLayout(); + const VkFormat ldrFormat = + (_context && _context->getSwapchain()) + ? _context->getSwapchain()->swapchainImageFormat() + : VK_FORMAT_B8G8R8A8_UNORM; + GraphicsPipelineCreateInfo info{}; info.vertexShaderPath = _context->getAssets()->shaderPath("fullscreen.vert.spv"); info.fragmentShaderPath = _context->getAssets()->shaderPath("tonemap.frag.spv"); @@ -39,14 +45,14 @@ void TonemapPass::init(EngineContext *context) pcr.size = sizeof(TonemapPush); info.pushConstants = { pcr }; - info.configure = [this](PipelineBuilder &b) { + info.configure = [ldrFormat](PipelineBuilder &b) { b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); b.set_polygon_mode(VK_POLYGON_MODE_FILL); b.set_cull_mode(VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE); b.set_multisampling_none(); b.disable_depthtest(); b.disable_blending(); - b.set_color_attachment_format(VK_FORMAT_R8G8B8A8_UNORM); + b.set_color_attachment_format(ldrFormat); }; _context->pipelines->createGraphicsPipeline("tonemap", info); @@ -71,9 +77,14 @@ RGImageHandle TonemapPass::register_graph(RenderGraph *graph, RGImageHandle hdrI { if (!graph || !hdrInput.valid()) return {}; + const VkFormat ldrFormat = + (_context && _context->getSwapchain()) + ? _context->getSwapchain()->swapchainImageFormat() + : VK_FORMAT_B8G8R8A8_UNORM; + RGImageDesc desc{}; desc.name = "ldr.tonemap"; - desc.format = VK_FORMAT_R8G8B8A8_UNORM; + desc.format = ldrFormat; desc.extent = _context->getDrawExtent(); desc.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index 0d9b3ae..e25c37c 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -265,10 +265,22 @@ void SceneManager::update_scene() }; // Keep projection FOV in sync with the camera so that CPU ray picking - // matches what is rendered on-screen. + // matches what is rendered inside the fixed logical render area (letterboxed). const float fov = glm::radians(mainCamera.fovDegrees); - const float aspect = (float) _context->getSwapchain()->windowExtent().width / - (float) _context->getSwapchain()->windowExtent().height; + + // Derive aspect ratio from the fixed logical render size instead of the window/swapchain. + VkExtent2D logicalExtent{ kRenderWidth, kRenderHeight }; + if (_context) + { + VkExtent2D ctxExtent = _context->getLogicalRenderExtent(); + if (ctxExtent.width > 0 && ctxExtent.height > 0) + { + logicalExtent = ctxExtent; + } + } + + const float aspect = static_cast(logicalExtent.width) / + static_cast(logicalExtent.height); const float nearPlane = 0.1f; glm::mat4 projection = makeReversedInfinitePerspective(fov, aspect, nearPlane); // Vulkan NDC has inverted Y.