feat: add FXAA post-process anti-aliasing, combinable with MSAA

This commit is contained in:
Kelsi 2026-03-12 16:43:48 -07:00
parent 819a690c33
commit 6e95709b68
5 changed files with 495 additions and 2 deletions

View file

@ -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);
}

View file

@ -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;

View file

@ -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

View file

@ -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<float>(ext.width);
vp.height = static_cast<float>(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<int>(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<float>(ext.width),
1.0f / static_cast<float>(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;

View file

@ -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);