diff --git a/assets/shaders/fsr2_accumulate.comp.glsl b/assets/shaders/fsr2_accumulate.comp.glsl index a998b52c..7fb0cb27 100644 --- a/assets/shaders/fsr2_accumulate.comp.glsl +++ b/assets/shaders/fsr2_accumulate.comp.glsl @@ -2,25 +2,19 @@ layout(local_size_x = 8, local_size_y = 8) in; -// Inputs (internal resolution) layout(set = 0, binding = 0) uniform sampler2D sceneColor; layout(set = 0, binding = 1) uniform sampler2D depthBuffer; layout(set = 0, binding = 2) uniform sampler2D motionVectors; - -// History (display resolution) layout(set = 0, binding = 3) uniform sampler2D historyInput; - -// Output (display resolution) layout(set = 0, binding = 4, rgba16f) uniform writeonly image2D historyOutput; layout(push_constant) uniform PushConstants { vec4 internalSize; // xy = internal resolution, zw = 1/internal vec4 displaySize; // xy = display resolution, zw = 1/display - vec4 jitterOffset; // xy = current jitter (pixel-space), zw = unused + vec4 jitterOffset; // xy = current jitter (NDC-space), zw = unused vec4 params; // x = resetHistory (1=reset), y = sharpness, zw = unused } pc; -// RGB <-> YCoCg for neighborhood clamping vec3 rgbToYCoCg(vec3 rgb) { float y = 0.25 * rgb.r + 0.5 * rgb.g + 0.25 * rgb.b; float co = 0.5 * rgb.r - 0.5 * rgb.b; @@ -40,76 +34,52 @@ void main() { ivec2 outSize = ivec2(pc.displaySize.xy); if (outPixel.x >= outSize.x || outPixel.y >= outSize.y) return; - // Output UV in display space vec2 outUV = (vec2(outPixel) + 0.5) * pc.displaySize.zw; + vec3 currentColor = texture(sceneColor, outUV).rgb; - // Map display pixel to internal resolution UV (accounting for jitter) - vec2 internalUV = outUV; - - // Sample current frame color at internal resolution - vec3 currentColor = texture(sceneColor, internalUV).rgb; - - // Sample motion vector at internal resolution - vec2 inUV = outUV; // Approximate — display maps to internal via scale - vec2 motion = texture(motionVectors, inUV).rg; - - // Reproject: where was this pixel in the previous frame's history? - vec2 historyUV = outUV - motion; - - // History reset: on teleport / camera cut, just use current frame if (pc.params.x > 0.5) { imageStore(historyOutput, outPixel, vec4(currentColor, 1.0)); return; } - // Sample reprojected history + vec2 motion = texture(motionVectors, outUV).rg; + vec2 historyUV = outUV + motion; + + float historyValid = (historyUV.x >= 0.0 && historyUV.x <= 1.0 && + historyUV.y >= 0.0 && historyUV.y <= 1.0) ? 1.0 : 0.0; + vec3 historyColor = texture(historyInput, historyUV).rgb; - // Neighborhood clamping in YCoCg space to prevent ghosting - // Sample 3x3 neighborhood from current frame + // Neighborhood clamping in YCoCg space vec2 texelSize = pc.internalSize.zw; - vec3 samples[9]; - int idx = 0; - for (int dy = -1; dy <= 1; dy++) { - for (int dx = -1; dx <= 1; dx++) { - samples[idx] = rgbToYCoCg(texture(sceneColor, internalUV + vec2(dx, dy) * texelSize).rgb); - idx++; - } - } + vec3 s0 = rgbToYCoCg(currentColor); + vec3 s1 = rgbToYCoCg(texture(sceneColor, outUV + vec2(-texelSize.x, 0.0)).rgb); + vec3 s2 = rgbToYCoCg(texture(sceneColor, outUV + vec2( texelSize.x, 0.0)).rgb); + vec3 s3 = rgbToYCoCg(texture(sceneColor, outUV + vec2(0.0, -texelSize.y)).rgb); + vec3 s4 = rgbToYCoCg(texture(sceneColor, outUV + vec2(0.0, texelSize.y)).rgb); + vec3 s5 = rgbToYCoCg(texture(sceneColor, outUV + vec2(-texelSize.x, -texelSize.y)).rgb); + vec3 s6 = rgbToYCoCg(texture(sceneColor, outUV + vec2( texelSize.x, -texelSize.y)).rgb); + vec3 s7 = rgbToYCoCg(texture(sceneColor, outUV + vec2(-texelSize.x, texelSize.y)).rgb); + vec3 s8 = rgbToYCoCg(texture(sceneColor, outUV + vec2( texelSize.x, texelSize.y)).rgb); - // Compute AABB in YCoCg - vec3 boxMin = samples[0]; - vec3 boxMax = samples[0]; - for (int i = 1; i < 9; i++) { - boxMin = min(boxMin, samples[i]); - boxMax = max(boxMax, samples[i]); - } + vec3 m1 = s0 + s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8; + vec3 m2 = s0*s0 + s1*s1 + s2*s2 + s3*s3 + s4*s4 + s5*s5 + s6*s6 + s7*s7 + s8*s8; + vec3 mean = m1 / 9.0; + vec3 variance = max(m2 / 9.0 - mean * mean, vec3(0.0)); + vec3 stddev = sqrt(variance); - // Slightly expand the box to reduce flickering on edges - vec3 boxCenter = (boxMin + boxMax) * 0.5; - vec3 boxExtent = (boxMax - boxMin) * 0.5; - boxMin = boxCenter - boxExtent * 1.25; - boxMax = boxCenter + boxExtent * 1.25; + float gamma = 1.5; + vec3 boxMin = mean - gamma * stddev; + vec3 boxMax = mean + gamma * stddev; - // Clamp history to the neighborhood AABB vec3 historyYCoCg = rgbToYCoCg(historyColor); vec3 clampedHistory = clamp(historyYCoCg, boxMin, boxMax); historyColor = yCoCgToRgb(clampedHistory); - // Check if history UV is valid (within [0,1]) - float historyValid = (historyUV.x >= 0.0 && historyUV.x <= 1.0 && - historyUV.y >= 0.0 && historyUV.y <= 1.0) ? 1.0 : 0.0; - - // Blend factor: use more current frame for disoccluded regions - // Luminance difference between clamped history and original → confidence float clampDist = length(historyYCoCg - clampedHistory); - float blendFactor = mix(0.05, 0.3, clamp(clampDist * 4.0, 0.0, 1.0)); - - // If history is off-screen, use current frame entirely + float blendFactor = mix(0.05, 0.30, clamp(clampDist * 2.0, 0.0, 1.0)); blendFactor = mix(blendFactor, 1.0, 1.0 - historyValid); - // Final blend vec3 result = mix(historyColor, currentColor, blendFactor); - imageStore(historyOutput, outPixel, vec4(result, 1.0)); } diff --git a/assets/shaders/fsr2_accumulate.comp.spv b/assets/shaders/fsr2_accumulate.comp.spv index 4d31fba7..47529d75 100644 Binary files a/assets/shaders/fsr2_accumulate.comp.spv and b/assets/shaders/fsr2_accumulate.comp.spv differ diff --git a/assets/shaders/fsr2_motion.comp.glsl b/assets/shaders/fsr2_motion.comp.glsl index f4f68c2c..b0b39375 100644 --- a/assets/shaders/fsr2_motion.comp.glsl +++ b/assets/shaders/fsr2_motion.comp.glsl @@ -6,10 +6,8 @@ layout(set = 0, binding = 0) uniform sampler2D depthBuffer; layout(set = 0, binding = 1, rg16f) uniform writeonly image2D motionVectors; layout(push_constant) uniform PushConstants { - mat4 invViewProj; // Inverse of current jittered VP - mat4 prevViewProj; // Previous frame unjittered VP + mat4 reprojMatrix; // prevUnjitteredVP * inverse(currentUnjitteredVP) vec4 resolution; // xy = internal size, zw = 1/internal size - vec4 jitterOffset; // xy = current jitter (NDC), zw = previous jitter } pc; void main() { @@ -20,25 +18,18 @@ void main() { // Sample depth (Vulkan: 0 = near, 1 = far) float depth = texelFetch(depthBuffer, pixelCoord, 0).r; - // Pixel center in NDC [-1, 1] + // Pixel center in UV [0,1] and NDC [-1,1] vec2 uv = (vec2(pixelCoord) + 0.5) * pc.resolution.zw; vec2 ndc = uv * 2.0 - 1.0; - // Reconstruct world position from depth + // Clip-to-clip reprojection: current unjittered clip → previous unjittered clip vec4 clipPos = vec4(ndc, depth, 1.0); - vec4 worldPos = pc.invViewProj * clipPos; - worldPos /= worldPos.w; - - // Project into previous frame's clip space (unjittered) - vec4 prevClip = pc.prevViewProj * worldPos; + vec4 prevClip = pc.reprojMatrix * clipPos; vec2 prevNdc = prevClip.xy / prevClip.w; vec2 prevUV = prevNdc * 0.5 + 0.5; - // Remove jitter from current UV to get unjittered position - vec2 unjitteredUV = uv - pc.jitterOffset.xy * 0.5; - - // Motion = previous position - current unjittered position (in UV space) - vec2 motion = prevUV - unjitteredUV; + // Motion = previous position - current position (both unjittered, in UV space) + vec2 motion = prevUV - uv; imageStore(motionVectors, pixelCoord, vec4(motion, 0.0, 0.0)); } diff --git a/assets/shaders/fsr2_motion.comp.spv b/assets/shaders/fsr2_motion.comp.spv index 813c4b9d..faa3d836 100644 Binary files a/assets/shaders/fsr2_motion.comp.spv and b/assets/shaders/fsr2_motion.comp.spv differ diff --git a/assets/shaders/fsr2_sharpen.frag.glsl b/assets/shaders/fsr2_sharpen.frag.glsl index b4dd928b..2c649d22 100644 --- a/assets/shaders/fsr2_sharpen.frag.glsl +++ b/assets/shaders/fsr2_sharpen.frag.glsl @@ -10,16 +10,20 @@ layout(push_constant) uniform PushConstants { } pc; void main() { + // Undo the vertex shader Y flip (postprocess.vert flips for Vulkan overlay, + // but we need standard UV coords for texture sampling) + vec2 tc = vec2(TexCoord.x, 1.0 - TexCoord.y); + vec2 texelSize = pc.params.xy; float sharpness = pc.params.z; // RCAS: Robust Contrast-Adaptive Sharpening // 5-tap cross pattern - vec3 center = texture(inputImage, TexCoord).rgb; - vec3 north = texture(inputImage, TexCoord + vec2(0.0, -texelSize.y)).rgb; - vec3 south = texture(inputImage, TexCoord + vec2(0.0, texelSize.y)).rgb; - vec3 west = texture(inputImage, TexCoord + vec2(-texelSize.x, 0.0)).rgb; - vec3 east = texture(inputImage, TexCoord + vec2( texelSize.x, 0.0)).rgb; + vec3 center = texture(inputImage, tc).rgb; + vec3 north = texture(inputImage, tc + vec2(0.0, -texelSize.y)).rgb; + vec3 south = texture(inputImage, tc + vec2(0.0, texelSize.y)).rgb; + vec3 west = texture(inputImage, tc + vec2(-texelSize.x, 0.0)).rgb; + vec3 east = texture(inputImage, tc + vec2( texelSize.x, 0.0)).rgb; // Compute local contrast (min/max of neighborhood) vec3 minRGB = min(center, min(min(north, south), min(west, east))); diff --git a/assets/shaders/fsr2_sharpen.frag.spv b/assets/shaders/fsr2_sharpen.frag.spv index 99aba03a..f9d2394c 100644 Binary files a/assets/shaders/fsr2_sharpen.frag.spv and b/assets/shaders/fsr2_sharpen.frag.spv differ diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 13f77fe2..0058fbdd 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -408,7 +408,7 @@ private: VkPipelineLayout sharpenPipelineLayout = VK_NULL_HANDLE; VkDescriptorSetLayout sharpenDescSetLayout = VK_NULL_HANDLE; VkDescriptorPool sharpenDescPool = VK_NULL_HANDLE; - VkDescriptorSet sharpenDescSet = VK_NULL_HANDLE; + VkDescriptorSet sharpenDescSets[2] = {}; // Previous frame state for motion vector reprojection glm::mat4 prevViewProjection = glm::mat4(1.0f); diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 81686219..063bae9a 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -876,6 +876,9 @@ bool Renderer::isWaterRefractionEnabled() const { void Renderer::setMsaaSamples(VkSampleCountFlagBits samples) { if (!vkCtx) return; + // FSR2 requires non-MSAA render pass — block MSAA changes while FSR2 is active + if (fsr2_.enabled && samples > VK_SAMPLE_COUNT_1_BIT) return; + // Clamp to device maximum VkSampleCountFlagBits maxSamples = vkCtx->getMaxUsableSampleCount(); if (samples > maxSamples) samples = maxSamples; @@ -1178,7 +1181,7 @@ void Renderer::endFrame() { fsr2_.prevJitter = camera->getJitter(); camera->clearJitter(); fsr2_.currentHistory = 1 - fsr2_.currentHistory; - fsr2_.frameIndex++; + fsr2_.frameIndex = (fsr2_.frameIndex + 1) % 256; // Wrap to keep Halton values well-distributed } else if (fsr_.enabled && fsr_.sceneFramebuffer) { // End the off-screen scene render pass @@ -3782,7 +3785,7 @@ bool Renderer::initFSR2Resources() { VkPushConstantRange pc{}; pc.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; pc.offset = 0; - pc.size = 2 * sizeof(glm::mat4) + 2 * sizeof(glm::vec4); // 160 bytes + pc.size = sizeof(glm::mat4) + sizeof(glm::vec4); // 80 bytes VkPipelineLayoutCreateInfo plCI{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; plCI.setLayoutCount = 1; @@ -4005,20 +4008,21 @@ bool Renderer::initFSR2Resources() { return false; } - // Descriptor pool + set for sharpen pass (reads from history output) + // Descriptor pool + sets for sharpen pass (double-buffered to avoid race condition) VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2}; VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; - poolInfo.maxSets = 1; + poolInfo.maxSets = 2; poolInfo.poolSizeCount = 1; poolInfo.pPoolSizes = &poolSize; vkCreateDescriptorPool(device, &poolInfo, nullptr, &fsr2_.sharpenDescPool); + VkDescriptorSetLayout layouts[2] = {fsr2_.sharpenDescSetLayout, fsr2_.sharpenDescSetLayout}; VkDescriptorSetAllocateInfo dsAI{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; dsAI.descriptorPool = fsr2_.sharpenDescPool; - dsAI.descriptorSetCount = 1; - dsAI.pSetLayouts = &fsr2_.sharpenDescSetLayout; - vkAllocateDescriptorSets(device, &dsAI, &fsr2_.sharpenDescSet); - // Descriptor updated dynamically each frame to point at the correct history buffer + dsAI.descriptorSetCount = 2; + dsAI.pSetLayouts = layouts; + vkAllocateDescriptorSets(device, &dsAI, fsr2_.sharpenDescSets); + // Descriptors updated dynamically each frame to point at the correct history buffer } fsr2_.needsHistoryReset = true; @@ -4036,7 +4040,7 @@ void Renderer::destroyFSR2Resources() { if (fsr2_.sharpenPipeline) { vkDestroyPipeline(device, fsr2_.sharpenPipeline, nullptr); fsr2_.sharpenPipeline = VK_NULL_HANDLE; } if (fsr2_.sharpenPipelineLayout) { vkDestroyPipelineLayout(device, fsr2_.sharpenPipelineLayout, nullptr); fsr2_.sharpenPipelineLayout = VK_NULL_HANDLE; } - if (fsr2_.sharpenDescPool) { vkDestroyDescriptorPool(device, fsr2_.sharpenDescPool, nullptr); fsr2_.sharpenDescPool = VK_NULL_HANDLE; fsr2_.sharpenDescSet = VK_NULL_HANDLE; } + if (fsr2_.sharpenDescPool) { vkDestroyDescriptorPool(device, fsr2_.sharpenDescPool, nullptr); fsr2_.sharpenDescPool = VK_NULL_HANDLE; fsr2_.sharpenDescSets[0] = fsr2_.sharpenDescSets[1] = VK_NULL_HANDLE; } if (fsr2_.sharpenDescSetLayout) { vkDestroyDescriptorSetLayout(device, fsr2_.sharpenDescSetLayout, nullptr); fsr2_.sharpenDescSetLayout = VK_NULL_HANDLE; } if (fsr2_.accumulatePipeline) { vkDestroyPipeline(device, fsr2_.accumulatePipeline, nullptr); fsr2_.accumulatePipeline = VK_NULL_HANDLE; } @@ -4082,24 +4086,22 @@ void Renderer::dispatchMotionVectors() { vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_COMPUTE, fsr2_.motionVecPipelineLayout, 0, 1, &fsr2_.motionVecDescSet, 0, nullptr); - // Push constants: invViewProj, prevViewProj, resolution, jitterOffset + // Single reprojection matrix: prevUnjitteredVP * inv(currentUnjitteredVP) + // Both matrices are unjittered — jitter only affects sub-pixel sampling, + // not motion vector computation. This avoids numerical instability from + // jitter amplification through large world coordinates. struct { - glm::mat4 invViewProj; - glm::mat4 prevViewProj; + glm::mat4 reprojMatrix; // prevUnjitteredVP * inv(currentUnjitteredVP) glm::vec4 resolution; - glm::vec4 jitterOffset; } pc; - glm::mat4 currentVP = camera->getProjectionMatrix() * camera->getViewMatrix(); - pc.invViewProj = glm::inverse(currentVP); - pc.prevViewProj = fsr2_.prevViewProjection; + glm::mat4 currentUnjitteredVP = camera->getUnjitteredViewProjectionMatrix(); + pc.reprojMatrix = fsr2_.prevViewProjection * glm::inverse(currentUnjitteredVP); pc.resolution = glm::vec4( static_cast(fsr2_.internalWidth), static_cast(fsr2_.internalHeight), 1.0f / fsr2_.internalWidth, 1.0f / fsr2_.internalHeight); - glm::vec2 jitter = camera->getJitter(); - pc.jitterOffset = glm::vec4(jitter.x, jitter.y, fsr2_.prevJitter.x, fsr2_.prevJitter.y); vkCmdPushConstants(currentCmd, fsr2_.motionVecPipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(pc), &pc); @@ -4128,17 +4130,24 @@ void Renderer::dispatchTemporalAccumulate() { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - // Transition history input: GENERAL/UNDEFINED → SHADER_READ_ONLY + // History layout lifecycle: + // First frame: both in UNDEFINED + // Subsequent frames: both in SHADER_READ_ONLY (output was transitioned for sharpen, + // input was left in SHADER_READ_ONLY from its sharpen read) + VkImageLayout historyOldLayout = fsr2_.needsHistoryReset + ? VK_IMAGE_LAYOUT_UNDEFINED + : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + // Transition history input: SHADER_READ_ONLY → SHADER_READ_ONLY (barrier for sync) transitionImageLayout(currentCmd, fsr2_.history[inputIdx].image, - fsr2_.needsHistoryReset ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_GENERAL, - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + historyOldLayout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, // sharpen read in previous frame VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - // Transition history output: UNDEFINED → GENERAL + // Transition history output: SHADER_READ_ONLY → GENERAL (for compute write) transitionImageLayout(currentCmd, fsr2_.history[outputIdx].image, - VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, - VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + historyOldLayout, VK_IMAGE_LAYOUT_GENERAL, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); vkCmdBindPipeline(currentCmd, VK_PIPELINE_BIND_POINT_COMPUTE, fsr2_.accumulatePipeline); @@ -4179,6 +4188,10 @@ void Renderer::renderFSR2Sharpen() { VkExtent2D ext = vkCtx->getSwapchainExtent(); uint32_t outputIdx = fsr2_.currentHistory; + // Use per-frame descriptor set to avoid race with in-flight command buffers + uint32_t frameIdx = vkCtx->getCurrentFrame(); + VkDescriptorSet descSet = fsr2_.sharpenDescSets[frameIdx]; + // Update sharpen descriptor to point at current history output VkDescriptorImageInfo imgInfo{}; imgInfo.sampler = fsr2_.linearSampler; @@ -4186,7 +4199,7 @@ void Renderer::renderFSR2Sharpen() { imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; - write.dstSet = fsr2_.sharpenDescSet; + write.dstSet = descSet; write.dstBinding = 0; write.descriptorCount = 1; write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; @@ -4195,7 +4208,7 @@ void Renderer::renderFSR2Sharpen() { vkCmdBindPipeline(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, fsr2_.sharpenPipeline); vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, - fsr2_.sharpenPipelineLayout, 0, 1, &fsr2_.sharpenDescSet, 0, nullptr); + fsr2_.sharpenPipelineLayout, 0, 1, &descSet, 0, nullptr); glm::vec4 params(1.0f / ext.width, 1.0f / ext.height, fsr2_.sharpness, 0.0f); vkCmdPushConstants(currentCmd, fsr2_.sharpenPipelineLayout, @@ -4214,6 +4227,11 @@ void Renderer::setFSR2Enabled(bool enabled) { fsr_.enabled = false; fsr_.needsRecreate = true; } + // FSR2 requires non-MSAA render pass (its framebuffer has 2 attachments) + if (vkCtx && vkCtx->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT) { + pendingMsaaSamples_ = VK_SAMPLE_COUNT_1_BIT; + msaaChangePending_ = true; + } // Use FSR1's scale factor and sharpness as defaults fsr2_.scaleFactor = fsr_.scaleFactor; fsr2_.sharpness = fsr_.sharpness; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 96800895..eab00305 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -6281,7 +6281,13 @@ void GameScreen::renderSettingsWindow() { } { const char* aaLabels[] = { "Off", "2x MSAA", "4x MSAA", "8x MSAA" }; - if (ImGui::Combo("Anti-Aliasing", &pendingAntiAliasing, aaLabels, 4)) { + bool fsr2Active = renderer && renderer->isFSR2Enabled(); + if (fsr2Active) { + ImGui::BeginDisabled(); + int disabled = 0; + ImGui::Combo("Anti-Aliasing (FSR2)", &disabled, "Off (FSR2 active)\0", 1); + ImGui::EndDisabled(); + } else if (ImGui::Combo("Anti-Aliasing", &pendingAntiAliasing, aaLabels, 4)) { static const VkSampleCountFlagBits aaSamples[] = { VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT