From 51a8cf565f578f58d8e70bd60133bf604af6553e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 8 Mar 2026 19:56:52 -0700 Subject: [PATCH] Integrate AMD FSR2 backend and document SDK bootstrap --- BUILD_INSTRUCTIONS.md | 5 + CMakeLists.txt | 41 +++++- README.md | 11 ++ build.ps1 | 19 +++ build.sh | 18 +++ include/rendering/camera.hpp | 3 + include/rendering/renderer.hpp | 12 ++ include/third_party/ffx_fsr2_compat.h | 15 ++ rebuild.ps1 | 19 +++ rebuild.sh | 18 +++ src/rendering/renderer.cpp | 196 ++++++++++++++++++++++---- 11 files changed, 329 insertions(+), 28 deletions(-) create mode 100644 include/third_party/ffx_fsr2_compat.h diff --git a/BUILD_INSTRUCTIONS.md b/BUILD_INSTRUCTIONS.md index 54f2041c..eecbccaf 100644 --- a/BUILD_INSTRUCTIONS.md +++ b/BUILD_INSTRUCTIONS.md @@ -217,3 +217,8 @@ You can also specify an expansion: `.\extract_assets.ps1 "C:\Games\WoW\Data" wot ```bash git submodule update --init --recursive ``` +- AMD FSR2 SDK is fetched automatically by `build.sh` / `rebuild.sh` / `build.ps1` / `rebuild.ps1` from: + - `https://github.com/GPUOpen-Effects/FidelityFX-FSR2.git` + - target path: `extern/FidelityFX-FSR2` +- AMD backend is enabled only when SDK headers and generated Vulkan permutation headers are present. + If generated permutation headers are missing, the build uses the internal FSR2 fallback path. diff --git a/CMakeLists.txt b/CMakeLists.txt index 94c57484..f0003be2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,18 +27,49 @@ option(WOWEE_ENABLE_AMD_FSR2 "Enable AMD FidelityFX FSR2 backend when SDK is pre # AMD FidelityFX FSR2 SDK detection (drop-in under extern/FidelityFX-FSR2) set(WOWEE_AMD_FSR2_DIR ${CMAKE_SOURCE_DIR}/extern/FidelityFX-FSR2) set(WOWEE_AMD_FSR2_HEADER ${WOWEE_AMD_FSR2_DIR}/src/ffx-fsr2-api/ffx_fsr2.h) -if(WOWEE_ENABLE_AMD_FSR2 AND EXISTS ${WOWEE_AMD_FSR2_HEADER}) +set(WOWEE_AMD_FSR2_VK_PERM_HEADER ${WOWEE_AMD_FSR2_DIR}/src/ffx-fsr2-api/vk/shaders/ffx_fsr2_accumulate_pass_permutations.h) +if(WOWEE_ENABLE_AMD_FSR2 AND EXISTS ${WOWEE_AMD_FSR2_HEADER} AND EXISTS ${WOWEE_AMD_FSR2_VK_PERM_HEADER}) message(STATUS "AMD FSR2 SDK detected at ${WOWEE_AMD_FSR2_DIR}") add_compile_definitions(WOWEE_HAS_AMD_FSR2=1) - include_directories( + add_compile_definitions(FFX_GCC=1) + + # AMD FSR2 Vulkan backend sources (official SDK implementation) + set(WOWEE_AMD_FSR2_SOURCES + ${WOWEE_AMD_FSR2_DIR}/src/ffx-fsr2-api/ffx_assert.cpp + ${WOWEE_AMD_FSR2_DIR}/src/ffx-fsr2-api/ffx_fsr2.cpp + ${WOWEE_AMD_FSR2_DIR}/src/ffx-fsr2-api/vk/ffx_fsr2_vk.cpp + ${WOWEE_AMD_FSR2_DIR}/src/ffx-fsr2-api/vk/shaders/ffx_fsr2_shaders_vk.cpp + ) + add_library(wowee_fsr2_amd_vk STATIC ${WOWEE_AMD_FSR2_SOURCES}) + set_target_properties(wowee_fsr2_amd_vk PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + ) + target_include_directories(wowee_fsr2_amd_vk PUBLIC ${WOWEE_AMD_FSR2_DIR}/src ${WOWEE_AMD_FSR2_DIR}/src/ffx-fsr2-api ${WOWEE_AMD_FSR2_DIR}/src/ffx-fsr2-api/vk + ${WOWEE_AMD_FSR2_DIR}/src/ffx-fsr2-api/vk/shaders ) + set(WOWEE_FFX_COMPAT_HEADER ${CMAKE_SOURCE_DIR}/include/third_party/ffx_fsr2_compat.h) + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(wowee_fsr2_amd_vk PRIVATE + "-include${WOWEE_FFX_COMPAT_HEADER}" + ) + elseif(MSVC) + target_compile_options(wowee_fsr2_amd_vk PRIVATE + "/FI${WOWEE_FFX_COMPAT_HEADER}" + ) + endif() + target_link_libraries(wowee_fsr2_amd_vk PUBLIC Vulkan::Vulkan) else() add_compile_definitions(WOWEE_HAS_AMD_FSR2=0) if(WOWEE_ENABLE_AMD_FSR2) - message(WARNING "AMD FSR2 SDK not found at ${WOWEE_AMD_FSR2_DIR}; using internal fallback implementation.") + if(NOT EXISTS ${WOWEE_AMD_FSR2_HEADER}) + message(WARNING "AMD FSR2 SDK not found at ${WOWEE_AMD_FSR2_DIR}; using internal fallback implementation.") + elseif(NOT EXISTS ${WOWEE_AMD_FSR2_VK_PERM_HEADER}) + message(WARNING "AMD FSR2 SDK found, but generated Vulkan permutation headers are missing (e.g. ${WOWEE_AMD_FSR2_VK_PERM_HEADER}); using internal fallback implementation.") + endif() endif() endif() @@ -556,6 +587,10 @@ if(TARGET vk-bootstrap) target_link_libraries(wowee PRIVATE vk-bootstrap) endif() +if(TARGET wowee_fsr2_amd_vk) + target_link_libraries(wowee PRIVATE wowee_fsr2_amd_vk) +endif() + # Link Unicorn if available if(HAVE_UNICORN) target_link_libraries(wowee PRIVATE ${UNICORN_LIBRARY}) diff --git a/README.md b/README.md index 1196da24..757412a0 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,17 @@ make -j$(nproc) ./bin/wowee ``` +### AMD FSR2 SDK (External) + +- Build scripts (`build.sh`, `rebuild.sh`, `build.ps1`, `rebuild.ps1`) auto-fetch the AMD SDK to: + - `extern/FidelityFX-FSR2` +- Source URL: + - `https://github.com/GPUOpen-Effects/FidelityFX-FSR2.git` +- The renderer enables the AMD backend only when both are present: + - `extern/FidelityFX-FSR2/src/ffx-fsr2-api/ffx_fsr2.h` + - `extern/FidelityFX-FSR2/src/ffx-fsr2-api/vk/shaders/ffx_fsr2_accumulate_pass_permutations.h` +- If SDK files or generated Vulkan permutation headers are missing, CMake falls back to the internal non-AMD FSR2 path automatically. + ## Controls ### Camera & Movement diff --git a/build.ps1 b/build.ps1 index 1f35f0f1..6e965219 100644 --- a/build.ps1 +++ b/build.ps1 @@ -12,7 +12,26 @@ $ErrorActionPreference = "Stop" $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path Set-Location $ScriptDir +function Ensure-Fsr2Sdk { + $sdkDir = Join-Path $ScriptDir "extern\FidelityFX-FSR2" + $sdkHeader = Join-Path $sdkDir "src\ffx-fsr2-api\ffx_fsr2.h" + if (Test-Path $sdkHeader) { return } + + if (-not (Get-Command git -ErrorAction SilentlyContinue)) { + Write-Warning "git not found; cannot auto-fetch AMD FSR2 SDK." + return + } + + Write-Host "Fetching AMD FidelityFX FSR2 SDK into $sdkDir ..." + New-Item -ItemType Directory -Path (Join-Path $ScriptDir "extern") -Force | Out-Null + & git clone --depth 1 https://github.com/GPUOpen-Effects/FidelityFX-FSR2.git $sdkDir + if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to clone AMD FSR2 SDK. Build will use internal fallback path." + } +} + Write-Host "Building wowee..." +Ensure-Fsr2Sdk # Create build directory if it doesn't exist if (-not (Test-Path "build")) { diff --git a/build.sh b/build.sh index f5a2d8f1..a234bb72 100755 --- a/build.sh +++ b/build.sh @@ -5,7 +5,25 @@ set -e # Exit on error cd "$(dirname "$0")" +ensure_fsr2_sdk() { + local sdk_dir="extern/FidelityFX-FSR2" + local sdk_header="$sdk_dir/src/ffx-fsr2-api/ffx_fsr2.h" + if [ -f "$sdk_header" ]; then + return + fi + if ! command -v git >/dev/null 2>&1; then + echo "Warning: git not found; cannot auto-fetch AMD FSR2 SDK." + return + fi + echo "Fetching AMD FidelityFX FSR2 SDK into $sdk_dir ..." + mkdir -p extern + git clone --depth 1 https://github.com/GPUOpen-Effects/FidelityFX-FSR2.git "$sdk_dir" || { + echo "Warning: failed to clone AMD FSR2 SDK. Build will use internal fallback path." + } +} + echo "Building wowee..." +ensure_fsr2_sdk # Create build directory if it doesn't exist mkdir -p build diff --git a/include/rendering/camera.hpp b/include/rendering/camera.hpp index 99a4879a..ee58c8f2 100644 --- a/include/rendering/camera.hpp +++ b/include/rendering/camera.hpp @@ -27,6 +27,9 @@ public: glm::mat4 getViewProjectionMatrix() const { return projectionMatrix * viewMatrix; } glm::mat4 getUnjitteredViewProjectionMatrix() const { return unjitteredProjectionMatrix * viewMatrix; } float getAspectRatio() const { return aspectRatio; } + float getFovDegrees() const { return fov; } + float getNearPlane() const { return nearPlane; } + float getFarPlane() const { return farPlane; } // Sub-pixel jitter for temporal upscaling (FSR 2) void setJitter(float jx, float jy); diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index af9191e0..cc17efc4 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -11,6 +11,10 @@ #include "rendering/vk_frame_data.hpp" #include "rendering/vk_utils.hpp" #include "rendering/sky_system.hpp" +#if WOWEE_HAS_AMD_FSR2 +#include "ffx_fsr2.h" +#include "ffx_fsr2_vk.h" +#endif namespace wowee { namespace core { class Window; } @@ -420,6 +424,13 @@ private: glm::vec2 prevJitter = glm::vec2(0.0f); uint32_t frameIndex = 0; bool needsHistoryReset = true; + bool useAmdBackend = false; +#if WOWEE_HAS_AMD_FSR2 + FfxFsr2Context amdContext{}; + FfxFsr2Interface amdInterface{}; + void* amdScratchBuffer = nullptr; + size_t amdScratchBufferSize = 0; +#endif // Convergent accumulation: jitter for N frames then freeze int convergenceFrame = 0; @@ -431,6 +442,7 @@ private: void destroyFSR2Resources(); void dispatchMotionVectors(); void dispatchTemporalAccumulate(); + void dispatchAmdFsr2(); void renderFSR2Sharpen(); static float halton(uint32_t index, uint32_t base); diff --git a/include/third_party/ffx_fsr2_compat.h b/include/third_party/ffx_fsr2_compat.h new file mode 100644 index 00000000..81be76ed --- /dev/null +++ b/include/third_party/ffx_fsr2_compat.h @@ -0,0 +1,15 @@ +#pragma once + +// Compatibility shim for building AMD FSR2 SDK sources on non-MSVC toolchains. +#include +#include +#include +#include + +#ifndef _countof +#define _countof(x) (sizeof(x) / sizeof((x)[0])) +#endif + +#ifndef wcscpy_s +#define wcscpy_s(dst, src) wcscpy((dst), (src)) +#endif diff --git a/rebuild.ps1 b/rebuild.ps1 index b365e3b4..9b1efc4e 100644 --- a/rebuild.ps1 +++ b/rebuild.ps1 @@ -12,7 +12,26 @@ $ErrorActionPreference = "Stop" $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path Set-Location $ScriptDir +function Ensure-Fsr2Sdk { + $sdkDir = Join-Path $ScriptDir "extern\FidelityFX-FSR2" + $sdkHeader = Join-Path $sdkDir "src\ffx-fsr2-api\ffx_fsr2.h" + if (Test-Path $sdkHeader) { return } + + if (-not (Get-Command git -ErrorAction SilentlyContinue)) { + Write-Warning "git not found; cannot auto-fetch AMD FSR2 SDK." + return + } + + Write-Host "Fetching AMD FidelityFX FSR2 SDK into $sdkDir ..." + New-Item -ItemType Directory -Path (Join-Path $ScriptDir "extern") -Force | Out-Null + & git clone --depth 1 https://github.com/GPUOpen-Effects/FidelityFX-FSR2.git $sdkDir + if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to clone AMD FSR2 SDK. Build will use internal fallback path." + } +} + Write-Host "Clean rebuilding wowee..." +Ensure-Fsr2Sdk # Remove build directory completely if (Test-Path "build") { diff --git a/rebuild.sh b/rebuild.sh index dce16e92..17026d74 100755 --- a/rebuild.sh +++ b/rebuild.sh @@ -5,7 +5,25 @@ set -e # Exit on error cd "$(dirname "$0")" +ensure_fsr2_sdk() { + local sdk_dir="extern/FidelityFX-FSR2" + local sdk_header="$sdk_dir/src/ffx-fsr2-api/ffx_fsr2.h" + if [ -f "$sdk_header" ]; then + return + fi + if ! command -v git >/dev/null 2>&1; then + echo "Warning: git not found; cannot auto-fetch AMD FSR2 SDK." + return + fi + echo "Fetching AMD FidelityFX FSR2 SDK into $sdk_dir ..." + mkdir -p extern + git clone --depth 1 https://github.com/GPUOpen-Effects/FidelityFX-FSR2.git "$sdk_dir" || { + echo "Warning: failed to clone AMD FSR2 SDK. Build will use internal fallback path." + } +} + echo "Clean rebuilding wowee..." +ensure_fsr2_sdk # Remove build directory completely if [ -d "build" ]; then diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 293127f5..6ff37ddd 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -1022,32 +1022,48 @@ void Renderer::beginFrame() { return; } - // FSR2 jitter pattern for temporal accumulation. - constexpr bool kFsr2TemporalEnabled = false; + // FSR2 jitter pattern. if (fsr2_.enabled && fsr2_.sceneFramebuffer && camera) { - if (!kFsr2TemporalEnabled) { + if (!fsr2_.useAmdBackend) { camera->setJitter(0.0f, 0.0f); } else { - glm::mat4 currentVP = camera->getViewProjectionMatrix(); + glm::mat4 currentVP = camera->getViewProjectionMatrix(); - // Reset history only for clear camera movement. - bool cameraMoved = false; - for (int i = 0; i < 4 && !cameraMoved; i++) { - for (int j = 0; j < 4 && !cameraMoved; j++) { - if (std::abs(currentVP[i][j] - fsr2_.lastStableVP[i][j]) > 1e-3f) { - cameraMoved = true; + // Reset history only for clear camera movement. + bool cameraMoved = false; + for (int i = 0; i < 4 && !cameraMoved; i++) { + for (int j = 0; j < 4 && !cameraMoved; j++) { + if (std::abs(currentVP[i][j] - fsr2_.lastStableVP[i][j]) > 1e-3f) { + cameraMoved = true; + } } } - } - if (cameraMoved) { - fsr2_.lastStableVP = currentVP; - fsr2_.needsHistoryReset = true; - } + if (cameraMoved) { + fsr2_.lastStableVP = currentVP; + fsr2_.needsHistoryReset = true; + } - const float jitterScale = 0.5f; - float jx = (halton(fsr2_.frameIndex + 1, 2) - 0.5f) * 2.0f * jitterScale / static_cast(fsr2_.internalWidth); - float jy = (halton(fsr2_.frameIndex + 1, 3) - 0.5f) * 2.0f * jitterScale / static_cast(fsr2_.internalHeight); - camera->setJitter(jx, jy); +#if WOWEE_HAS_AMD_FSR2 + // AMD-recommended jitter sequence in pixel space, converted to NDC projection offset. + int32_t phaseCount = ffxFsr2GetJitterPhaseCount( + static_cast(fsr2_.internalWidth), + static_cast(vkCtx->getSwapchainExtent().width)); + float jitterX = 0.0f; + float jitterY = 0.0f; + if (phaseCount > 0 && + ffxFsr2GetJitterOffset(&jitterX, &jitterY, static_cast(fsr2_.frameIndex % static_cast(phaseCount)), phaseCount) == FFX_OK) { + float ndcJx = (2.0f * jitterX) / static_cast(fsr2_.internalWidth); + float ndcJy = (2.0f * jitterY) / static_cast(fsr2_.internalHeight); + camera->setJitter(ndcJx, ndcJy); + } else { + camera->setJitter(0.0f, 0.0f); + } +#else + const float jitterScale = 0.5f; + float jx = (halton(fsr2_.frameIndex + 1, 2) - 0.5f) * 2.0f * jitterScale / static_cast(fsr2_.internalWidth); + float jy = (halton(fsr2_.frameIndex + 1, 3) - 0.5f) * 2.0f * jitterScale / static_cast(fsr2_.internalHeight); + camera->setJitter(jx, jy); +#endif } } @@ -1152,14 +1168,13 @@ void Renderer::endFrame() { if (!vkCtx || currentCmd == VK_NULL_HANDLE) return; if (fsr2_.enabled && fsr2_.sceneFramebuffer) { - constexpr bool kFsr2TemporalEnabled = false; // End the off-screen scene render pass vkCmdEndRenderPass(currentCmd); - if (kFsr2TemporalEnabled) { + if (fsr2_.useAmdBackend) { // Compute passes: motion vectors -> temporal accumulation dispatchMotionVectors(); - dispatchTemporalAccumulate(); + dispatchAmdFsr2(); // Transition history output: GENERAL -> SHADER_READ_ONLY for sharpen pass transitionImageLayout(currentCmd, fsr2_.history[fsr2_.currentHistory].image, @@ -1209,7 +1224,7 @@ void Renderer::endFrame() { fsr2_.prevViewProjection = camera->getViewProjectionMatrix(); fsr2_.prevJitter = camera->getJitter(); camera->clearJitter(); - if (kFsr2TemporalEnabled) { + if (fsr2_.useAmdBackend) { fsr2_.currentHistory = 1 - fsr2_.currentHistory; } fsr2_.frameIndex = (fsr2_.frameIndex + 1) % 256; // Wrap to keep Halton values well-distributed @@ -3739,6 +3754,7 @@ bool Renderer::initFSR2Resources() { LOG_INFO("FSR2: initializing at ", fsr2_.internalWidth, "x", fsr2_.internalHeight, " -> ", swapExtent.width, "x", swapExtent.height, " (scale=", fsr2_.scaleFactor, ")"); + fsr2_.useAmdBackend = false; #if WOWEE_HAS_AMD_FSR2 LOG_INFO("FSR2: AMD FidelityFX SDK detected at build time."); #else @@ -3802,6 +3818,51 @@ bool Renderer::initFSR2Resources() { samplerInfo.magFilter = VK_FILTER_NEAREST; vkCreateSampler(device, &samplerInfo, nullptr, &fsr2_.nearestSampler); +#if WOWEE_HAS_AMD_FSR2 + // Initialize AMD FSR2 context; fall back to internal path on any failure. + fsr2_.amdScratchBufferSize = ffxFsr2GetScratchMemorySizeVK(vkCtx->getPhysicalDevice()); + if (fsr2_.amdScratchBufferSize > 0) { + fsr2_.amdScratchBuffer = std::malloc(fsr2_.amdScratchBufferSize); + } + if (!fsr2_.amdScratchBuffer) { + LOG_WARNING("FSR2 AMD: failed to allocate scratch buffer, using internal fallback."); + } else { + FfxErrorCode ifaceErr = ffxFsr2GetInterfaceVK( + &fsr2_.amdInterface, + fsr2_.amdScratchBuffer, + fsr2_.amdScratchBufferSize, + vkCtx->getPhysicalDevice(), + vkGetDeviceProcAddr); + if (ifaceErr != FFX_OK) { + LOG_WARNING("FSR2 AMD: ffxFsr2GetInterfaceVK failed (", static_cast(ifaceErr), "), using internal fallback."); + std::free(fsr2_.amdScratchBuffer); + fsr2_.amdScratchBuffer = nullptr; + fsr2_.amdScratchBufferSize = 0; + } else { + FfxFsr2ContextDescription ctxDesc{}; + ctxDesc.flags = FFX_FSR2_ENABLE_AUTO_EXPOSURE | FFX_FSR2_ENABLE_MOTION_VECTORS_JITTER_CANCELLATION; + ctxDesc.maxRenderSize.width = fsr2_.internalWidth; + ctxDesc.maxRenderSize.height = fsr2_.internalHeight; + ctxDesc.displaySize.width = swapExtent.width; + ctxDesc.displaySize.height = swapExtent.height; + ctxDesc.callbacks = fsr2_.amdInterface; + ctxDesc.device = ffxGetDeviceVK(vkCtx->getDevice()); + ctxDesc.fpMessage = nullptr; + + FfxErrorCode ctxErr = ffxFsr2ContextCreate(&fsr2_.amdContext, &ctxDesc); + if (ctxErr == FFX_OK) { + fsr2_.useAmdBackend = true; + LOG_INFO("FSR2 AMD: context created successfully."); + } else { + LOG_WARNING("FSR2 AMD: context creation failed (", static_cast(ctxErr), "), using internal fallback."); + std::free(fsr2_.amdScratchBuffer); + fsr2_.amdScratchBuffer = nullptr; + fsr2_.amdScratchBufferSize = 0; + } + } + } +#endif + // --- Motion Vector Compute Pipeline --- { // Descriptor set layout: binding 0 = depth (sampler), binding 1 = motion vectors (storage image) @@ -4078,6 +4139,18 @@ void Renderer::destroyFSR2Resources() { vkDeviceWaitIdle(device); +#if WOWEE_HAS_AMD_FSR2 + if (fsr2_.useAmdBackend) { + ffxFsr2ContextDestroy(&fsr2_.amdContext); + fsr2_.useAmdBackend = false; + } + if (fsr2_.amdScratchBuffer) { + std::free(fsr2_.amdScratchBuffer); + fsr2_.amdScratchBuffer = nullptr; + } + fsr2_.amdScratchBufferSize = 0; +#endif + 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_.sharpenDescSets[0] = fsr2_.sharpenDescSets[1] = VK_NULL_HANDLE; } @@ -4220,9 +4293,82 @@ void Renderer::dispatchTemporalAccumulate() { fsr2_.needsHistoryReset = false; } +void Renderer::dispatchAmdFsr2() { + if (currentCmd == VK_NULL_HANDLE || !camera) return; +#if WOWEE_HAS_AMD_FSR2 + if (!fsr2_.useAmdBackend) return; + + VkExtent2D swapExtent = vkCtx->getSwapchainExtent(); + uint32_t outputIdx = fsr2_.currentHistory; + + transitionImageLayout(currentCmd, fsr2_.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_COMPUTE_SHADER_BIT); + transitionImageLayout(currentCmd, fsr2_.motionVectors.image, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + transitionImageLayout(currentCmd, fsr2_.sceneDepth.image, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + transitionImageLayout(currentCmd, fsr2_.history[outputIdx].image, + fsr2_.needsHistoryReset ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_GENERAL, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + FfxFsr2DispatchDescription desc{}; + desc.commandList = ffxGetCommandListVK(currentCmd); + desc.color = ffxGetTextureResourceVK(&fsr2_.amdContext, + fsr2_.sceneColor.image, fsr2_.sceneColor.imageView, + fsr2_.internalWidth, fsr2_.internalHeight, vkCtx->getSwapchainFormat(), + L"FSR2_InputColor", FFX_RESOURCE_STATE_COMPUTE_READ); + desc.depth = ffxGetTextureResourceVK(&fsr2_.amdContext, + fsr2_.sceneDepth.image, fsr2_.sceneDepth.imageView, + fsr2_.internalWidth, fsr2_.internalHeight, vkCtx->getDepthFormat(), + L"FSR2_InputDepth", FFX_RESOURCE_STATE_COMPUTE_READ); + desc.motionVectors = ffxGetTextureResourceVK(&fsr2_.amdContext, + fsr2_.motionVectors.image, fsr2_.motionVectors.imageView, + fsr2_.internalWidth, fsr2_.internalHeight, VK_FORMAT_R16G16_SFLOAT, + L"FSR2_InputMotionVectors", FFX_RESOURCE_STATE_COMPUTE_READ); + desc.output = ffxGetTextureResourceVK(&fsr2_.amdContext, + fsr2_.history[outputIdx].image, fsr2_.history[outputIdx].imageView, + swapExtent.width, swapExtent.height, VK_FORMAT_R16G16B16A16_SFLOAT, + L"FSR2_Output", FFX_RESOURCE_STATE_UNORDERED_ACCESS); + + // Camera jitter is stored as NDC projection offsets; convert to render-pixel offsets. + glm::vec2 jitterNdc = camera->getJitter(); + desc.jitterOffset.x = jitterNdc.x * 0.5f * static_cast(fsr2_.internalWidth); + desc.jitterOffset.y = jitterNdc.y * 0.5f * static_cast(fsr2_.internalHeight); + desc.motionVectorScale.x = static_cast(fsr2_.internalWidth); + desc.motionVectorScale.y = static_cast(fsr2_.internalHeight); + desc.renderSize.width = fsr2_.internalWidth; + desc.renderSize.height = fsr2_.internalHeight; + desc.enableSharpening = false; // Keep existing RCAS post pass. + desc.sharpness = 0.0f; + desc.frameTimeDelta = glm::max(0.001f, lastDeltaTime_ * 1000.0f); + desc.preExposure = 1.0f; + desc.reset = fsr2_.needsHistoryReset; + desc.cameraNear = camera->getNearPlane(); + desc.cameraFar = camera->getFarPlane(); + desc.cameraFovAngleVertical = glm::radians(camera->getFovDegrees()); + desc.viewSpaceToMetersFactor = 1.0f; + desc.enableAutoReactive = false; + + FfxErrorCode dispatchErr = ffxFsr2ContextDispatch(&fsr2_.amdContext, &desc); + if (dispatchErr != FFX_OK) { + LOG_WARNING("FSR2 AMD: dispatch failed (", static_cast(dispatchErr), "), forcing history reset."); + fsr2_.needsHistoryReset = true; + } else { + fsr2_.needsHistoryReset = false; + } +#endif +} + void Renderer::renderFSR2Sharpen() { if (!fsr2_.sharpenPipeline || currentCmd == VK_NULL_HANDLE) return; - constexpr bool kFsr2TemporalEnabled = false; VkExtent2D ext = vkCtx->getSwapchainExtent(); uint32_t outputIdx = fsr2_.currentHistory; @@ -4234,7 +4380,7 @@ void Renderer::renderFSR2Sharpen() { // Update sharpen descriptor to point at current history output VkDescriptorImageInfo imgInfo{}; imgInfo.sampler = fsr2_.linearSampler; - imgInfo.imageView = kFsr2TemporalEnabled + imgInfo.imageView = fsr2_.useAmdBackend ? fsr2_.history[outputIdx].imageView : fsr2_.sceneColor.imageView; imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;