From 6e95709b689a2062c8b26b1d500834595bb2145d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 16:43:48 -0700 Subject: [PATCH] feat: add FXAA post-process anti-aliasing, combinable with MSAA --- assets/shaders/fxaa.frag.glsl | 132 ++++++++++++++ include/rendering/renderer.hpp | 29 ++++ include/ui/game_screen.hpp | 2 + src/rendering/renderer.cpp | 308 ++++++++++++++++++++++++++++++++- src/ui/game_screen.cpp | 26 +++ 5 files changed, 495 insertions(+), 2 deletions(-) create mode 100644 assets/shaders/fxaa.frag.glsl diff --git a/assets/shaders/fxaa.frag.glsl b/assets/shaders/fxaa.frag.glsl new file mode 100644 index 00000000..df35aaa0 --- /dev/null +++ b/assets/shaders/fxaa.frag.glsl @@ -0,0 +1,132 @@ +#version 450 + +// FXAA 3.11 — Fast Approximate Anti-Aliasing post-process pass. +// Reads the resolved scene color and outputs a smoothed result. +// Push constant: rcpFrame = vec2(1/width, 1/height). + +layout(set = 0, binding = 0) uniform sampler2D uScene; + +layout(location = 0) in vec2 TexCoord; +layout(location = 0) out vec4 outColor; + +layout(push_constant) uniform PC { + vec2 rcpFrame; +} pc; + +// Quality tuning +#define FXAA_EDGE_THRESHOLD (1.0/8.0) // minimum edge contrast to process +#define FXAA_EDGE_THRESHOLD_MIN (1.0/24.0) // ignore very dark regions +#define FXAA_SEARCH_STEPS 12 +#define FXAA_SEARCH_THRESHOLD (1.0/4.0) +#define FXAA_SUBPIX 0.75 +#define FXAA_SUBPIX_TRIM (1.0/4.0) +#define FXAA_SUBPIX_TRIM_SCALE (1.0/(1.0 - FXAA_SUBPIX_TRIM)) +#define FXAA_SUBPIX_CAP (3.0/4.0) + +float luma(vec3 c) { + return dot(c, vec3(0.299, 0.587, 0.114)); +} + +void main() { + vec2 uv = TexCoord; + vec2 rcp = pc.rcpFrame; + + // --- Centre and cardinal neighbours --- + vec3 rgbM = texture(uScene, uv).rgb; + vec3 rgbN = texture(uScene, uv + vec2( 0.0, -1.0) * rcp).rgb; + vec3 rgbS = texture(uScene, uv + vec2( 0.0, 1.0) * rcp).rgb; + vec3 rgbE = texture(uScene, uv + vec2( 1.0, 0.0) * rcp).rgb; + vec3 rgbW = texture(uScene, uv + vec2(-1.0, 0.0) * rcp).rgb; + + float lumaN = luma(rgbN); + float lumaS = luma(rgbS); + float lumaE = luma(rgbE); + float lumaW = luma(rgbW); + float lumaM = luma(rgbM); + + float lumaMin = min(lumaM, min(min(lumaN, lumaS), min(lumaE, lumaW))); + float lumaMax = max(lumaM, max(max(lumaN, lumaS), max(lumaE, lumaW))); + float range = lumaMax - lumaMin; + + // Early exit on smooth regions + if (range < max(FXAA_EDGE_THRESHOLD_MIN, lumaMax * FXAA_EDGE_THRESHOLD)) { + outColor = vec4(rgbM, 1.0); + return; + } + + // --- Diagonal neighbours --- + vec3 rgbNW = texture(uScene, uv + vec2(-1.0, -1.0) * rcp).rgb; + vec3 rgbNE = texture(uScene, uv + vec2( 1.0, -1.0) * rcp).rgb; + vec3 rgbSW = texture(uScene, uv + vec2(-1.0, 1.0) * rcp).rgb; + vec3 rgbSE = texture(uScene, uv + vec2( 1.0, 1.0) * rcp).rgb; + + float lumaNW = luma(rgbNW); + float lumaNE = luma(rgbNE); + float lumaSW = luma(rgbSW); + float lumaSE = luma(rgbSE); + + // --- Sub-pixel blend factor --- + float lumaL = (lumaN + lumaS + lumaE + lumaW) * 0.25; + float rangeL = abs(lumaL - lumaM); + float blendL = max(0.0, (rangeL / range) - FXAA_SUBPIX_TRIM) * FXAA_SUBPIX_TRIM_SCALE; + blendL = min(FXAA_SUBPIX_CAP, blendL) * FXAA_SUBPIX; + + // --- Edge orientation (horizontal vs. vertical) --- + float edgeHorz = + abs(-2.0*lumaW + lumaNW + lumaSW) + + 2.0*abs(-2.0*lumaM + lumaN + lumaS) + + abs(-2.0*lumaE + lumaNE + lumaSE); + float edgeVert = + abs(-2.0*lumaS + lumaSW + lumaSE) + + 2.0*abs(-2.0*lumaM + lumaW + lumaE) + + abs(-2.0*lumaN + lumaNW + lumaNE); + + bool horzSpan = (edgeHorz >= edgeVert); + float lengthSign = horzSpan ? rcp.y : rcp.x; + + float luma1 = horzSpan ? lumaN : lumaW; + float luma2 = horzSpan ? lumaS : lumaE; + float grad1 = abs(luma1 - lumaM); + float grad2 = abs(luma2 - lumaM); + lengthSign = (grad1 >= grad2) ? -lengthSign : lengthSign; + + // --- Edge search --- + vec2 posB = uv; + vec2 offNP = horzSpan ? vec2(rcp.x, 0.0) : vec2(0.0, rcp.y); + if (!horzSpan) posB.x += lengthSign * 0.5; + if ( horzSpan) posB.y += lengthSign * 0.5; + + float lumaMLSS = lumaM - (luma1 + luma2) * 0.5; + float gradientScaled = max(grad1, grad2) * 0.25; + + vec2 posN = posB - offNP; + vec2 posP = posB + offNP; + bool done1 = false, done2 = false; + float lumaEnd1 = 0.0, lumaEnd2 = 0.0; + + for (int i = 0; i < FXAA_SEARCH_STEPS; ++i) { + if (!done1) lumaEnd1 = luma(texture(uScene, posN).rgb) - lumaMLSS; + if (!done2) lumaEnd2 = luma(texture(uScene, posP).rgb) - lumaMLSS; + done1 = done1 || (abs(lumaEnd1) >= gradientScaled * FXAA_SEARCH_THRESHOLD); + done2 = done2 || (abs(lumaEnd2) >= gradientScaled * FXAA_SEARCH_THRESHOLD); + if (done1 && done2) break; + if (!done1) posN -= offNP; + if (!done2) posP += offNP; + } + + float dstN = horzSpan ? (uv.x - posN.x) : (uv.y - posN.y); + float dstP = horzSpan ? (posP.x - uv.x) : (posP.y - uv.y); + bool dirN = (dstN < dstP); + float lumaEndFinal = dirN ? lumaEnd1 : lumaEnd2; + + float spanLength = dstN + dstP; + float pixelOffset = (dirN ? dstN : dstP) / spanLength; + bool goodSpan = ((lumaEndFinal < 0.0) != (lumaMLSS < 0.0)); + float pixelOffsetFinal = max(goodSpan ? pixelOffset : 0.0, blendL); + + vec2 finalUV = uv; + if ( horzSpan) finalUV.y += pixelOffsetFinal * lengthSign; + if (!horzSpan) finalUV.x += pixelOffsetFinal * lengthSign; + + outColor = vec4(texture(uScene, finalUV).rgb, 1.0); +} diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 93bbed03..07d8091f 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -271,6 +271,10 @@ public: float getShadowDistance() const { return shadowDistance_; } void setMsaaSamples(VkSampleCountFlagBits samples); + // FXAA post-process anti-aliasing (combinable with MSAA) + void setFXAAEnabled(bool enabled); + bool isFXAAEnabled() const { return fxaa_.enabled; } + // FSR (FidelityFX Super Resolution) upscaling void setFSREnabled(bool enabled); bool isFSREnabled() const { return fsr_.enabled; } @@ -398,6 +402,31 @@ private: void destroyFSRResources(); void renderFSRUpscale(); + // FXAA post-process state + struct FXAAState { + bool enabled = false; + bool needsRecreate = false; + + // Off-screen scene target (same resolution as swapchain — no scaling) + AllocatedImage sceneColor{}; // 1x resolved color target + AllocatedImage sceneDepth{}; // Depth (matches MSAA sample count) + AllocatedImage sceneMsaaColor{}; // MSAA color target (when MSAA > 1x) + AllocatedImage sceneDepthResolve{}; // Depth resolve (MSAA + depth resolve) + VkFramebuffer sceneFramebuffer = VK_NULL_HANDLE; + VkSampler sceneSampler = VK_NULL_HANDLE; + + // FXAA fullscreen pipeline + VkPipeline pipeline = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkDescriptorSetLayout descSetLayout = VK_NULL_HANDLE; + VkDescriptorPool descPool = VK_NULL_HANDLE; + VkDescriptorSet descSet = VK_NULL_HANDLE; + }; + FXAAState fxaa_; + bool initFXAAResources(); + void destroyFXAAResources(); + void renderFXAAPass(); + // FSR 2.2 temporal upscaling state struct FSR2State { bool enabled = false; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 58984256..a6dd4920 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -204,6 +204,7 @@ private: float pendingLeftBarOffsetY = 0.0f; // Vertical offset from screen center int pendingGroundClutterDensity = 100; int pendingAntiAliasing = 0; // 0=Off, 1=2x, 2=4x, 3=8x + bool pendingFXAA = false; // FXAA post-process (combinable with MSAA) bool pendingNormalMapping = true; // on by default float pendingNormalMapStrength = 0.8f; // 0.0-2.0 bool pendingPOM = true; // on by default @@ -238,6 +239,7 @@ private: bool minimapSettingsApplied_ = false; bool volumeSettingsApplied_ = false; // True once saved volume settings applied to audio managers bool msaaSettingsApplied_ = false; // True once saved MSAA setting applied to renderer + bool fxaaSettingsApplied_ = false; // True once saved FXAA setting applied to renderer bool waterRefractionApplied_ = false; bool normalMapSettingsApplied_ = false; // True once saved normal map/POM settings applied diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 7618a345..20e2d472 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -858,6 +858,7 @@ void Renderer::shutdown() { destroyFSRResources(); destroyFSR2Resources(); + destroyFXAAResources(); destroyPerFrameResources(); zoneManager.reset(); @@ -960,8 +961,9 @@ void Renderer::applyMsaaChange() { VkDevice device = vkCtx->getDevice(); if (selCirclePipeline) { vkDestroyPipeline(device, selCirclePipeline, nullptr); selCirclePipeline = VK_NULL_HANDLE; } if (overlayPipeline) { vkDestroyPipeline(device, overlayPipeline, nullptr); overlayPipeline = VK_NULL_HANDLE; } - if (fsr_.sceneFramebuffer) destroyFSRResources(); // Will be lazily recreated in beginFrame() + if (fsr_.sceneFramebuffer) destroyFSRResources(); // Will be lazily recreated in beginFrame() if (fsr2_.sceneFramebuffer) destroyFSR2Resources(); + if (fxaa_.sceneFramebuffer) destroyFXAAResources(); // Will be lazily recreated in beginFrame() // Reinitialize ImGui Vulkan backend with new MSAA sample count ImGui_ImplVulkan_Shutdown(); @@ -1017,6 +1019,19 @@ void Renderer::beginFrame() { } } + // FXAA resource management (disabled when FSR2 is active — FSR2 has its own AA) + if (fxaa_.needsRecreate && fxaa_.sceneFramebuffer) { + destroyFXAAResources(); + fxaa_.needsRecreate = false; + if (!fxaa_.enabled) LOG_INFO("FXAA: disabled"); + } + if (fxaa_.enabled && !fsr2_.enabled && !fsr_.enabled && !fxaa_.sceneFramebuffer) { + if (!initFXAAResources()) { + LOG_ERROR("FXAA: initialization failed, disabling"); + fxaa_.enabled = false; + } + } + // Handle swapchain recreation if needed if (vkCtx->isSwapchainDirty()) { vkCtx->recreateSwapchain(window->getWidth(), window->getHeight()); @@ -1033,6 +1048,11 @@ void Renderer::beginFrame() { destroyFSR2Resources(); initFSR2Resources(); } + // Recreate FXAA resources for new swapchain dimensions + if (fxaa_.enabled && !fsr2_.enabled && !fsr_.enabled) { + destroyFXAAResources(); + initFXAAResources(); + } } // Acquire swapchain image and begin command buffer @@ -1122,6 +1142,9 @@ void Renderer::beginFrame() { } else if (fsr_.enabled && fsr_.sceneFramebuffer) { rpInfo.framebuffer = fsr_.sceneFramebuffer; renderExtent = { fsr_.internalWidth, fsr_.internalHeight }; + } else if (fxaa_.enabled && fxaa_.sceneFramebuffer) { + rpInfo.framebuffer = fxaa_.sceneFramebuffer; + renderExtent = vkCtx->getSwapchainExtent(); // native resolution — no downscaling } else { rpInfo.framebuffer = vkCtx->getSwapchainFramebuffers()[currentImageIndex]; renderExtent = vkCtx->getSwapchainExtent(); @@ -1298,10 +1321,50 @@ void Renderer::endFrame() { // Draw FSR upscale fullscreen quad renderFSRUpscale(); + + } else if (fxaa_.enabled && fxaa_.sceneFramebuffer) { + // End the off-screen scene render pass + vkCmdEndRenderPass(currentCmd); + + // Transition resolved scene color: PRESENT_SRC_KHR → SHADER_READ_ONLY + transitionImageLayout(currentCmd, fxaa_.sceneColor.image, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + + // Begin swapchain render pass (1x — no MSAA on the output pass) + VkRenderPassBeginInfo rpInfo{}; + rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rpInfo.renderPass = vkCtx->getImGuiRenderPass(); + rpInfo.framebuffer = vkCtx->getSwapchainFramebuffers()[currentImageIndex]; + rpInfo.renderArea.offset = {0, 0}; + rpInfo.renderArea.extent = vkCtx->getSwapchainExtent(); + // The swapchain render pass always has 2 attachments when MSAA is off; + // FXAA output goes to the non-MSAA swapchain directly. + VkClearValue fxaaClear[2]{}; + fxaaClear[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; + fxaaClear[1].depthStencil = {1.0f, 0}; + rpInfo.clearValueCount = 2; + rpInfo.pClearValues = fxaaClear; + + vkCmdBeginRenderPass(currentCmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE); + + VkExtent2D ext = vkCtx->getSwapchainExtent(); + VkViewport vp{}; + vp.width = static_cast(ext.width); + vp.height = static_cast(ext.height); + vp.maxDepth = 1.0f; + vkCmdSetViewport(currentCmd, 0, 1, &vp); + VkRect2D sc{}; + sc.extent = ext; + vkCmdSetScissor(currentCmd, 0, 1, &sc); + + // Draw FXAA pass + renderFXAAPass(); } // ImGui rendering — must respect subpass contents mode - if (!fsr_.enabled && !fsr2_.enabled && parallelRecordingEnabled_) { + if (!fsr_.enabled && !fsr2_.enabled && !fxaa_.enabled && parallelRecordingEnabled_) { // Scene pass was begun with VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS, // so ImGui must be recorded into a secondary command buffer. VkCommandBuffer imguiCmd = beginSecondary(SEC_IMGUI); @@ -4698,6 +4761,247 @@ void Renderer::setAmdFsr3FramegenEnabled(bool enabled) { // ========================= End FSR 2.2 ========================= +// ========================= FXAA Post-Process ========================= + +bool Renderer::initFXAAResources() { + if (!vkCtx) return false; + + VkDevice device = vkCtx->getDevice(); + VmaAllocator alloc = vkCtx->getAllocator(); + VkExtent2D ext = vkCtx->getSwapchainExtent(); + VkSampleCountFlagBits msaa = vkCtx->getMsaaSamples(); + bool useMsaa = (msaa > VK_SAMPLE_COUNT_1_BIT); + bool useDepthResolve = (vkCtx->getDepthResolveImageView() != VK_NULL_HANDLE); + + LOG_INFO("FXAA: initializing at ", ext.width, "x", ext.height, + " (MSAA=", static_cast(msaa), "x)"); + + VkFormat colorFmt = vkCtx->getSwapchainFormat(); + VkFormat depthFmt = vkCtx->getDepthFormat(); + + // sceneColor: 1x resolved color target — FXAA reads from here + fxaa_.sceneColor = createImage(device, alloc, ext.width, ext.height, + colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); + if (!fxaa_.sceneColor.image) { + LOG_ERROR("FXAA: failed to create scene color image"); + return false; + } + + // sceneDepth: depth buffer at current MSAA sample count + fxaa_.sceneDepth = createImage(device, alloc, ext.width, ext.height, + depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, msaa); + if (!fxaa_.sceneDepth.image) { + LOG_ERROR("FXAA: failed to create scene depth image"); + destroyFXAAResources(); + return false; + } + + if (useMsaa) { + fxaa_.sceneMsaaColor = createImage(device, alloc, ext.width, ext.height, + colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, msaa); + if (!fxaa_.sceneMsaaColor.image) { + LOG_ERROR("FXAA: failed to create MSAA color image"); + destroyFXAAResources(); + return false; + } + if (useDepthResolve) { + fxaa_.sceneDepthResolve = createImage(device, alloc, ext.width, ext.height, + depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + if (!fxaa_.sceneDepthResolve.image) { + LOG_ERROR("FXAA: failed to create depth resolve image"); + destroyFXAAResources(); + return false; + } + } + } + + // Framebuffer — same attachment layout as main render pass + VkImageView fbAttachments[4]{}; + uint32_t fbCount; + if (useMsaa) { + fbAttachments[0] = fxaa_.sceneMsaaColor.imageView; + fbAttachments[1] = fxaa_.sceneDepth.imageView; + fbAttachments[2] = fxaa_.sceneColor.imageView; // resolve target + fbCount = 3; + if (useDepthResolve) { + fbAttachments[3] = fxaa_.sceneDepthResolve.imageView; + fbCount = 4; + } + } else { + fbAttachments[0] = fxaa_.sceneColor.imageView; + fbAttachments[1] = fxaa_.sceneDepth.imageView; + fbCount = 2; + } + + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = vkCtx->getImGuiRenderPass(); + fbInfo.attachmentCount = fbCount; + fbInfo.pAttachments = fbAttachments; + fbInfo.width = ext.width; + fbInfo.height = ext.height; + fbInfo.layers = 1; + if (vkCreateFramebuffer(device, &fbInfo, nullptr, &fxaa_.sceneFramebuffer) != VK_SUCCESS) { + LOG_ERROR("FXAA: failed to create scene framebuffer"); + destroyFXAAResources(); + return false; + } + + // Sampler + VkSamplerCreateInfo samplerInfo{}; + samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerInfo.minFilter = VK_FILTER_LINEAR; + samplerInfo.magFilter = 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; + if (vkCreateSampler(device, &samplerInfo, nullptr, &fxaa_.sceneSampler) != VK_SUCCESS) { + LOG_ERROR("FXAA: failed to create sampler"); + destroyFXAAResources(); + return false; + } + + // Descriptor set layout: binding 0 = combined image sampler + VkDescriptorSetLayoutBinding binding{}; + binding.binding = 0; + binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + VkDescriptorSetLayoutCreateInfo layoutInfo{}; + layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + layoutInfo.bindingCount = 1; + layoutInfo.pBindings = &binding; + vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &fxaa_.descSetLayout); + + VkDescriptorPoolSize poolSize{}; + poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + poolSize.descriptorCount = 1; + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.maxSets = 1; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + vkCreateDescriptorPool(device, &poolInfo, nullptr, &fxaa_.descPool); + + VkDescriptorSetAllocateInfo dsAllocInfo{}; + dsAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + dsAllocInfo.descriptorPool = fxaa_.descPool; + dsAllocInfo.descriptorSetCount = 1; + dsAllocInfo.pSetLayouts = &fxaa_.descSetLayout; + vkAllocateDescriptorSets(device, &dsAllocInfo, &fxaa_.descSet); + + // Bind the resolved 1x sceneColor + VkDescriptorImageInfo imgInfo{}; + imgInfo.sampler = fxaa_.sceneSampler; + imgInfo.imageView = fxaa_.sceneColor.imageView; + imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = fxaa_.descSet; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imgInfo; + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + + // Pipeline layout — push constant holds vec2 rcpFrame + VkPushConstantRange pc{}; + pc.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + pc.offset = 0; + pc.size = 8; // vec2 + VkPipelineLayoutCreateInfo plCI{}; + plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + plCI.setLayoutCount = 1; + plCI.pSetLayouts = &fxaa_.descSetLayout; + plCI.pushConstantRangeCount = 1; + plCI.pPushConstantRanges = &pc; + vkCreatePipelineLayout(device, &plCI, nullptr, &fxaa_.pipelineLayout); + + // FXAA pipeline — fullscreen triangle into the swapchain render pass + // Uses VK_SAMPLE_COUNT_1_BIT: it always runs after MSAA resolve. + VkShaderModule vertMod, fragMod; + if (!vertMod.loadFromFile(device, "assets/shaders/postprocess.vert.spv") || + !fragMod.loadFromFile(device, "assets/shaders/fxaa.frag.spv")) { + LOG_ERROR("FXAA: failed to load shaders"); + destroyFXAAResources(); + return false; + } + + fxaa_.pipeline = PipelineBuilder() + .setShaders(vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({}, {}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setNoDepthTest() + .setColorBlendAttachment(PipelineBuilder::blendDisabled()) + .setMultisample(VK_SAMPLE_COUNT_1_BIT) // swapchain pass is always 1x + .setLayout(fxaa_.pipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device); + + vertMod.destroy(); + fragMod.destroy(); + + if (!fxaa_.pipeline) { + LOG_ERROR("FXAA: failed to create pipeline"); + destroyFXAAResources(); + return false; + } + + LOG_INFO("FXAA: initialized successfully"); + return true; +} + +void Renderer::destroyFXAAResources() { + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + VmaAllocator alloc = vkCtx->getAllocator(); + vkDeviceWaitIdle(device); + + if (fxaa_.pipeline) { vkDestroyPipeline(device, fxaa_.pipeline, nullptr); fxaa_.pipeline = VK_NULL_HANDLE; } + if (fxaa_.pipelineLayout) { vkDestroyPipelineLayout(device, fxaa_.pipelineLayout, nullptr); fxaa_.pipelineLayout = VK_NULL_HANDLE; } + if (fxaa_.descPool) { vkDestroyDescriptorPool(device, fxaa_.descPool, nullptr); fxaa_.descPool = VK_NULL_HANDLE; fxaa_.descSet = VK_NULL_HANDLE; } + if (fxaa_.descSetLayout) { vkDestroyDescriptorSetLayout(device, fxaa_.descSetLayout, nullptr); fxaa_.descSetLayout = VK_NULL_HANDLE; } + if (fxaa_.sceneFramebuffer) { vkDestroyFramebuffer(device, fxaa_.sceneFramebuffer, nullptr); fxaa_.sceneFramebuffer = VK_NULL_HANDLE; } + if (fxaa_.sceneSampler) { vkDestroySampler(device, fxaa_.sceneSampler, nullptr); fxaa_.sceneSampler = VK_NULL_HANDLE; } + destroyImage(device, alloc, fxaa_.sceneDepthResolve); + destroyImage(device, alloc, fxaa_.sceneMsaaColor); + destroyImage(device, alloc, fxaa_.sceneDepth); + destroyImage(device, alloc, fxaa_.sceneColor); +} + +void Renderer::renderFXAAPass() { + if (!fxaa_.pipeline || currentCmd == VK_NULL_HANDLE) return; + VkExtent2D ext = vkCtx->getSwapchainExtent(); + + vkCmdBindPipeline(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, fxaa_.pipeline); + vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + fxaa_.pipelineLayout, 0, 1, &fxaa_.descSet, 0, nullptr); + + // Push rcpFrame = vec2(1/width, 1/height) + float rcpFrame[2] = { + 1.0f / static_cast(ext.width), + 1.0f / static_cast(ext.height) + }; + vkCmdPushConstants(currentCmd, fxaa_.pipelineLayout, + VK_SHADER_STAGE_FRAGMENT_BIT, 0, 8, rcpFrame); + + vkCmdDraw(currentCmd, 3, 1, 0, 0); // fullscreen triangle +} + +void Renderer::setFXAAEnabled(bool enabled) { + if (fxaa_.enabled == enabled) return; + fxaa_.enabled = enabled; + if (!enabled) { + fxaa_.needsRecreate = true; // defer destruction to next beginFrame() + } +} + +// ========================= End FXAA ========================= + void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { (void)world; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 7f14109d..bf02b2b4 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -512,6 +512,15 @@ void GameScreen::render(game::GameHandler& gameHandler) { msaaSettingsApplied_ = true; } + // Apply saved FXAA setting once when renderer is available + if (!fxaaSettingsApplied_) { + auto* renderer = core::Application::getInstance().getRenderer(); + if (renderer) { + renderer->setFXAAEnabled(pendingFXAA); + fxaaSettingsApplied_ = true; + } + } + // Apply saved water refraction setting once when renderer is available if (!waterRefractionApplied_) { auto* renderer = core::Application::getInstance().getRenderer(); @@ -13969,6 +13978,21 @@ void GameScreen::renderSettingsWindow() { updateGraphicsPresetFromCurrentSettings(); saveSettings(); } + // FXAA — post-process, combinable with any MSAA level (disabled with FSR2) + if (fsr2Active) { + ImGui::BeginDisabled(); + bool fxaaOff = false; + ImGui::Checkbox("FXAA (disabled with FSR3)", &fxaaOff); + ImGui::EndDisabled(); + } else { + if (ImGui::Checkbox("FXAA (post-process)", &pendingFXAA)) { + if (renderer) renderer->setFXAAEnabled(pendingFXAA); + updateGraphicsPresetFromCurrentSettings(); + saveSettings(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("FXAA smooths jagged edges as a post-process pass.\nCan be combined with MSAA for extra quality."); + } } // FSR Upscaling { @@ -16474,6 +16498,7 @@ void GameScreen::saveSettings() { out << "shadow_distance=" << pendingShadowDistance << "\n"; out << "water_refraction=" << (pendingWaterRefraction ? 1 : 0) << "\n"; out << "antialiasing=" << pendingAntiAliasing << "\n"; + out << "fxaa=" << (pendingFXAA ? 1 : 0) << "\n"; out << "normal_mapping=" << (pendingNormalMapping ? 1 : 0) << "\n"; out << "normal_map_strength=" << pendingNormalMapStrength << "\n"; out << "pom=" << (pendingPOM ? 1 : 0) << "\n"; @@ -16608,6 +16633,7 @@ void GameScreen::loadSettings() { else if (key == "shadow_distance") pendingShadowDistance = std::clamp(std::stof(val), 40.0f, 500.0f); else if (key == "water_refraction") pendingWaterRefraction = (std::stoi(val) != 0); else if (key == "antialiasing") pendingAntiAliasing = std::clamp(std::stoi(val), 0, 3); + else if (key == "fxaa") pendingFXAA = (std::stoi(val) != 0); else if (key == "normal_mapping") pendingNormalMapping = (std::stoi(val) != 0); else if (key == "normal_map_strength") pendingNormalMapStrength = std::clamp(std::stof(val), 0.0f, 2.0f); else if (key == "pom") pendingPOM = (std::stoi(val) != 0);