From ea97a113b0f9cadf519fbcff315cc539915a3acd Mon Sep 17 00:00:00 2001 From: Rys Sommefeldt Date: Mon, 5 Sep 2022 13:36:43 +0100 Subject: [PATCH] FidelityFX FSR v2.1.0 --- .gitlab-ci.yml | 4 + CMakeLists.txt | 1 + LICENSE.txt | 2 + README.md | 155 +-- changelog.md | 19 + libs/cauldron | 2 +- media/checkerboard.dds | Bin 0 -> 87508 bytes media/composition_text.dds | Bin 0 -> 1398144 bytes media/lion.jpg | Bin 0 -> 357550 bytes release_notes.txt | 19 +- src/DX12/AnimatedTexture.cpp | 183 ++++ src/DX12/AnimatedTexture.h | 56 ++ src/DX12/AnimatedTexture.hlsl | 129 +++ src/DX12/CMakeLists.txt | 40 +- src/DX12/FSR2Sample.cpp | 6 +- src/DX12/FSR2Sample.h | 1 - src/DX12/Renderer.cpp | 162 ++- src/DX12/Renderer.h | 32 +- src/DX12/UI.cpp | 29 +- src/DX12/UI.h | 31 +- src/DX12/UpscaleContext_FSR2_API.cpp | 39 +- src/DX12/UpscaleContext_Spatial.cpp | 2 +- src/GpuParticleShaders/Globals.h | 92 ++ src/GpuParticleShaders/ParallelSortCS.hlsl | 123 +++ src/GpuParticleShaders/ParticleEmit.hlsl | 101 ++ src/GpuParticleShaders/ParticleHelpers.h | 36 + src/GpuParticleShaders/ParticleRender.hlsl | 263 +++++ .../ParticleSimulation.hlsl | 313 ++++++ src/GpuParticleShaders/ParticleStructs.h | 54 + src/GpuParticleShaders/RenderScene.hlsl | 109 ++ src/GpuParticleShaders/ShaderConstants.h | 26 + src/GpuParticleShaders/SimulationBindings.h | 121 +++ src/GpuParticleShaders/fp16util.h | 169 ++++ src/GpuParticles/ParticleHelpers.h | 36 + src/GpuParticles/ParticleSystem.h | 93 ++ src/GpuParticles/ParticleSystemInternal.h | 154 +++ src/GpuParticles/dx12/GPUParticleSystem.cpp | 745 ++++++++++++++ src/GpuParticles/dx12/ParallelSort.cpp | 524 ++++++++++ src/GpuParticles/dx12/ParallelSort.h | 102 ++ src/GpuParticles/vk/BufferHelper.h | 179 ++++ src/GpuParticles/vk/GPUParticleSystem.cpp | 944 ++++++++++++++++++ src/GpuParticles/vk/ParallelSort.cpp | 559 +++++++++++ src/GpuParticles/vk/ParallelSort.h | 101 ++ src/VK/AnimatedTexture.cpp | 294 ++++++ src/VK/AnimatedTexture.h | 57 ++ src/VK/AnimatedTexture.hlsl | 128 +++ src/VK/CMakeLists.txt | 24 + src/VK/FSR2Sample.cpp | 5 +- src/VK/FSR2Sample.h | 1 - src/VK/Renderer.cpp | 225 ++++- src/VK/Renderer.h | 31 + src/VK/UI.cpp | 34 +- src/VK/UI.h | 40 +- src/VK/UpscaleContext_FSR2_API.cpp | 38 +- src/VK/stdafx.h | 2 +- src/ffx-fsr2-api/CMakeLists.txt | 17 +- src/ffx-fsr2-api/dx12/CMakeLists.txt | 2 +- src/ffx-fsr2-api/dx12/ffx_fsr2_dx12.cpp | 106 +- src/ffx-fsr2-api/dx12/ffx_fsr2_dx12.h | 2 +- .../dx12/shaders/ffx_fsr2_shaders_dx12.cpp | 74 +- .../dx12/shaders/ffx_fsr2_shaders_dx12.h | 9 +- src/ffx-fsr2-api/ffx_assert.cpp | 6 +- src/ffx-fsr2-api/ffx_assert.h | 4 +- src/ffx-fsr2-api/ffx_fsr2.cpp | 183 ++-- src/ffx-fsr2-api/ffx_fsr2.h | 6 +- src/ffx-fsr2-api/ffx_fsr2_interface.h | 83 +- src/ffx-fsr2-api/ffx_fsr2_maximum_bias.h | 4 +- src/ffx-fsr2-api/ffx_types.h | 74 +- src/ffx-fsr2-api/shaders/ffx_common_types.h | 13 +- src/ffx-fsr2-api/shaders/ffx_core_cpu.h | 4 + src/ffx-fsr2-api/shaders/ffx_core_hlsl.h | 385 +++---- src/ffx-fsr2-api/shaders/ffx_fsr1.h | 4 + .../shaders/ffx_fsr2_accumulate.h | 189 ++-- .../shaders/ffx_fsr2_accumulate_pass.glsl | 21 +- .../shaders/ffx_fsr2_accumulate_pass.hlsl | 11 +- .../ffx_fsr2_autogen_reactive_pass.glsl | 12 +- .../ffx_fsr2_autogen_reactive_pass.hlsl | 4 +- .../shaders/ffx_fsr2_callbacks_glsl.h | 313 +++--- .../shaders/ffx_fsr2_callbacks_hlsl.h | 413 ++++---- src/ffx-fsr2-api/shaders/ffx_fsr2_common.h | 338 ++++--- ...x_fsr2_compute_luminance_pyramid_pass.glsl | 2 +- ...x_fsr2_compute_luminance_pyramid_pass.hlsl | 4 +- .../shaders/ffx_fsr2_depth_clip.h | 19 +- .../shaders/ffx_fsr2_depth_clip_pass.glsl | 4 +- .../shaders/ffx_fsr2_depth_clip_pass.hlsl | 8 +- src/ffx-fsr2-api/shaders/ffx_fsr2_lock.h | 69 +- .../shaders/ffx_fsr2_lock_pass.glsl | 16 +- .../shaders/ffx_fsr2_lock_pass.hlsl | 6 - .../ffx_fsr2_postprocess_lock_status.h | 43 +- .../shaders/ffx_fsr2_prepare_input_color.h | 21 +- .../ffx_fsr2_prepare_input_color_pass.glsl | 5 +- .../ffx_fsr2_prepare_input_color_pass.hlsl | 2 +- .../shaders/ffx_fsr2_rcas_pass.glsl | 2 +- ...ruct_dilated_velocity_and_previous_depth.h | 129 ++- ..._fsr2_reconstruct_previous_depth_pass.glsl | 16 +- ..._fsr2_reconstruct_previous_depth_pass.hlsl | 14 +- src/ffx-fsr2-api/shaders/ffx_fsr2_reproject.h | 66 +- src/ffx-fsr2-api/shaders/ffx_fsr2_resources.h | 3 +- src/ffx-fsr2-api/shaders/ffx_fsr2_sample.h | 374 ++++--- src/ffx-fsr2-api/shaders/ffx_fsr2_upsample.h | 176 +++- src/ffx-fsr2-api/vk/CMakeLists.txt | 2 +- src/ffx-fsr2-api/vk/ffx_fsr2_vk.cpp | 98 +- src/ffx-fsr2-api/vk/ffx_fsr2_vk.h | 3 +- .../vk/shaders/ffx_fsr2_shaders_vk.cpp | 69 +- .../vk/shaders/ffx_fsr2_shaders_vk.h | 54 +- src/ffx-parallelsort/FFX_ParallelSort.h | 514 ++++++++++ 106 files changed, 8747 insertions(+), 1884 deletions(-) create mode 100644 changelog.md create mode 100644 media/checkerboard.dds create mode 100644 media/composition_text.dds create mode 100644 media/lion.jpg create mode 100644 src/DX12/AnimatedTexture.cpp create mode 100644 src/DX12/AnimatedTexture.h create mode 100644 src/DX12/AnimatedTexture.hlsl create mode 100644 src/GpuParticleShaders/Globals.h create mode 100644 src/GpuParticleShaders/ParallelSortCS.hlsl create mode 100644 src/GpuParticleShaders/ParticleEmit.hlsl create mode 100644 src/GpuParticleShaders/ParticleHelpers.h create mode 100644 src/GpuParticleShaders/ParticleRender.hlsl create mode 100644 src/GpuParticleShaders/ParticleSimulation.hlsl create mode 100644 src/GpuParticleShaders/ParticleStructs.h create mode 100644 src/GpuParticleShaders/RenderScene.hlsl create mode 100644 src/GpuParticleShaders/ShaderConstants.h create mode 100644 src/GpuParticleShaders/SimulationBindings.h create mode 100644 src/GpuParticleShaders/fp16util.h create mode 100644 src/GpuParticles/ParticleHelpers.h create mode 100644 src/GpuParticles/ParticleSystem.h create mode 100644 src/GpuParticles/ParticleSystemInternal.h create mode 100644 src/GpuParticles/dx12/GPUParticleSystem.cpp create mode 100644 src/GpuParticles/dx12/ParallelSort.cpp create mode 100644 src/GpuParticles/dx12/ParallelSort.h create mode 100644 src/GpuParticles/vk/BufferHelper.h create mode 100644 src/GpuParticles/vk/GPUParticleSystem.cpp create mode 100644 src/GpuParticles/vk/ParallelSort.cpp create mode 100644 src/GpuParticles/vk/ParallelSort.h create mode 100644 src/VK/AnimatedTexture.cpp create mode 100644 src/VK/AnimatedTexture.h create mode 100644 src/VK/AnimatedTexture.hlsl create mode 100644 src/ffx-parallelsort/FFX_ParallelSort.h diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 044cb9f..dfe5c49 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -59,6 +59,10 @@ package_sample: - "media/cauldron-media/color_ramp_bt2020_dcip3/" - "media/cauldron-media/readme.md" - "media/cauldron-media/screenshot.png" + - "media/atlas.dds" + - "media/checkerboard.dds" + - "media/composition_text.dds" + - "media/lion.jpg" - "README.md" - "LICENSE.txt" - "%SampleName%_DX12.bat" diff --git a/CMakeLists.txt b/CMakeLists.txt index aacf16f..9a5424c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,7 @@ cmake_minimum_required(VERSION 3.12.1) option (GFX_API_DX12 "Build with DX12" ON) +option (GFX_API_VK "Build with Vulkan" ON) if(NOT DEFINED GFX_API) project (FSR2_Sample) diff --git a/LICENSE.txt b/LICENSE.txt index 699b8a3..19b21ff 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,3 +1,5 @@ +FidelityFX Super Resolution 2.1 +================================= Copyright (c) 2022 Advanced Micro Devices, Inc. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/README.md b/README.md index e1b64dc..9f43d44 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# FidelityFX Super Resolution 2.0.1 (FSR 2.0) +# FidelityFX Super Resolution 2.1 (FSR 2.1) Copyright (c) 2022 Advanced Micro Devices, Inc. All rights reserved. @@ -21,11 +21,11 @@ THE SOFTWARE. ![Screenshot](screenshot.png) -AMD FidelityFX Super Resolution 2.0 (FSR 2) is an open source, high-quality solution for producing high resolution frames from lower resolution inputs. +AMD FidelityFX Super Resolution 2 (FSR 2) is an open source, high-quality solution for producing high resolution frames from lower resolution inputs. You can find the binaries for FidelityFX FSR in the release section on GitHub. -# Super Resolution 2.0 +# Super Resolution 2 ### Table of contents @@ -50,6 +50,7 @@ You can find the binaries for FidelityFX FSR in the release section on GitHub. - [Camera jitter](#camera-jitter) - [Camera jump cuts](#camera-jump-cuts) - [Mipmap biasing](#mipmap-biasing) + - [Frame Time Delta Input](#frame-time-delta-input) - [HDR support](#hdr-support) - [Falling back to 32bit floating point](#falling-back-to-32bit-floating-point) - [64-wide wavefronts](#64-wide-wavefronts) @@ -63,12 +64,12 @@ You can find the binaries for FidelityFX FSR in the release section on GitHub. - [Reproject & accumulate](#reproject-accumulate) - [Robust Contrast Adaptive Sharpening (RCAS)](#robust-contrast-adaptive-sharpening-rcas) - [Building the sample](#building-the-sample) +- [Limitations](#limitations) - [Version history](#version-history) -- [Limitations](release_notes.txt) - [References](#references) # Introduction -**FidelityFX Super Resolution 2.0** (or **FSR2** for short) is a cutting-edge upscaling technique developed from the ground up to produce high resolution frames from lower resolution inputs. +**FidelityFX Super Resolution 2** (or **FSR2** for short) is a cutting-edge upscaling technique developed from the ground up to produce high resolution frames from lower resolution inputs. ![alt text](docs/media/super-resolution-temporal/overview.svg "A diagram showing the input resources to the super resolution (temporal) algorithm.") @@ -100,19 +101,19 @@ To use FSR2 you should follow the steps below: 8. Create a backend for your target API. E.g. for DirectX12 you should call [`ffxFsr2GetInterfaceDX12`](src/ffx-fsr2-api/dx12/ffx_fsr2_dx12.h#L55). A scratch buffer should be allocated of the size returned by calling [`ffxFsr2GetScratchMemorySizeDX12`](src/ffx-fsr2-api/dx12/ffx_fsr2_dx12.h#L40) and the pointer to that buffer passed to [`ffxFsr2GetInterfaceDX12`](src/ffx-fsr2-api/dx12/ffx_fsr2_dx12.h#L55). -9. Create a FSR2 context by calling [`ffxFsr2ContextCreate`](src/ffx-fsr2-api/ffx_fsr2.h#L213). The parameters structure should be filled out matching the configuration of your application. See the API reference documentation for more details. +9. Create a FSR2 context by calling [`ffxFsr2ContextCreate`](src/ffx-fsr2-api/ffx_fsr2.h#L215). The parameters structure should be filled out matching the configuration of your application. See the API reference documentation for more details. -10. Each frame you should call [`ffxFsr2ContextDispatch`](src/ffx-fsr2-api/ffx_fsr2.h#L254) to launch FSR2 workloads. The parameters structure should be filled out matching the configuration of your application. See the API reference documentation for more details. +10. Each frame you should call [`ffxFsr2ContextDispatch`](src/ffx-fsr2-api/ffx_fsr2.h#L256) to launch FSR2 workloads. The parameters structure should be filled out matching the configuration of your application. See the API reference documentation for more details, and ensure the [`frameTimeDelta` field is provided in milliseconds](#frame-time-delta-input). -11. When your application is terminating (or you wish to destroy the context for another reason) you should call [`ffxFsr2ContextDestroy`](src/ffx-fsr2-api/ffx_fsr2.h#L277). The GPU should be idle before calling this function. +11. When your application is terminating (or you wish to destroy the context for another reason) you should call [`ffxFsr2ContextDestroy`](src/ffx-fsr2-api/ffx_fsr2.h#L279). The GPU should be idle before calling this function. -12. Sub-pixel jittering should be applied to your application's projection matrix. This should be done when performing the main rendering of your application. You should use the [`ffxFsr2GetJitterOffset`](src/ffx-fsr2-api/ffx_fsr2.h#L422) function to compute the precise jitter offsets. See [Camera jitter](#camera-jitter) section for more details. +12. Sub-pixel jittering should be applied to your application's projection matrix. This should be done when performing the main rendering of your application. You should use the [`ffxFsr2GetJitterOffset`](src/ffx-fsr2-api/ffx_fsr2.h#L424) function to compute the precise jitter offsets. See [Camera jitter](#camera-jitter) section for more details. -13. For the best upscaling quality it is strongly advised that you populate the [Reactive mask](#reactive-mask) and [Transparency & composition mask](#transparency-and-composition-mask) according to our guidelines. You can also use [`ffxFsr2ContextGenerateReactiveMask`](src/ffx-fsr2-api/ffx_fsr2.h#L265) as a starting point. +13. For the best upscaling quality it is strongly advised that you populate the [Reactive mask](#reactive-mask) and [Transparency & composition mask](#transparency-and-composition-mask) according to our guidelines. You can also use [`ffxFsr2ContextGenerateReactiveMask`](src/ffx-fsr2-api/ffx_fsr2.h#L267) as a starting point. 14. Applications should expose [scaling modes](#scaling-modes), in their user interface in the following order: Quality, Balanced, Performance, and (optionally) Ultra Performance. -15. Applications should also expose a sharpening slider to allow end users to acheive additional quality. +15. Applications should also expose a sharpening slider to allow end users to achieve additional quality. # Integration guidelines @@ -131,24 +132,24 @@ We strongly recommend that applications adopt consistent naming and scaling rati ## Performance Depending on your target hardware and operating configuration FSR2 will operate at different performance levels. -The table below summarizes the measured performance of FSR2 on a variety of hardware. +The table below summarizes the measured performance of FSR2 on a variety of hardware in DX12. | Target resolution | Quality | RX 6950 XT | RX 6900 XT | RX 6800 XT | RX 6800 | RX 6700 XT | RX 6600 XT | RX 5700 XT | RX Vega 56 | RX 590 | |-------------------|------------------|------------|------------|------------|---------|------------|------------|------------|------------|--------| -| 3840x2160 | Quality (1.5x) | 1.1ms | 1.2ms | 1.3ms | 1.6ms | 1.8ms | 3.0ms | 2.4ms | 3.7ms | 5.6ms | -| | Balanced (1.7x) | 1.0ms | 1.1ms | 1.1ms | 1.4ms | 1.7ms | 2.7ms | 2.2ms | 3.3ms | 5.3ms | -| | Performance (2x) | 0.9ms | 1.0ms | 1.0ms | 1.4ms | 1.5ms | 2.3ms | 2.0ms | 3.1ms | 4.9ms | -| | Ultra perf. (3x) | 0.8ms | 0.9ms | 0.9ms | 1.2ms | 1.4ms | 1.8ms | 1.7ms | 2.7ms | 4.3ms | -| 2560x1440 | Quality (1.5x) | 0.5ms | 0.5ms | 0.5ms | 0.7ms | 0.8ms | 1.2ms | 1.0ms | 1.6ms | 2.5ms | -| | Balanced (1.7x) | 0.4ms | 0.5ms | 0.5ms | 0.6ms | 0.8ms | 1.0ms | 1.0ms | 1.5ms | 2.4ms | -| | Performance (2x) | 0.4ms | 0.4ms | 0.4ms | 0.5ms | 0.7ms | 0.9ms | 0.9ms | 1.4ms | 2.2ms | -| | Ultra perf. (3x) | 0.3ms | 0.4ms | 0.4ms | 0.5ms | 0.6ms | 0.8ms | 0.7ms | 1.2ms | 1.9ms | -| 1920x1080 | Quality (1.5x) | 0.3ms | 0.3ms | 0.3ms | 0.3ms | 0.5ms | 0.6ms | 0.6ms | 0.9ms | 1.4ms | -| | Balanced (1.7x) | 0.2ms | 0.2ms | 0.3ms | 0.3ms | 0.4ms | 0.6ms | 0.5ms | 0.8ms | 1.3ms | -| | Performance (2x) | 0.2ms | 0.2ms | 0.2ms | 0.3ms | 0.4ms | 0.5ms | 0.5ms | 0.8ms | 1.3ms | -| | Ultra perf. (3x) | 0.2ms | 0.2ms | 0.2ms | 0.3ms | 0.4ms | 0.4ms | 0.4ms | 0.7ms | 1.1ms | +| 3840x2160 | Quality (1.5x) | 1.1ms | 1.2ms | 1.2ms | 1.3ms | 1.8ms | 3.0ms | 2.4ms | 4.8ms | 5.3ms | +| | Balanced (1.7x) | 1.0ms | 1.0ms | 1.1ms | 1.2ms | 1.6ms | 2.7ms | 2.1ms | 4.3ms | 4.8ms | +| | Performance (2x) | 0.8ms | 0.9ms | 0.9ms | 1.1ms | 1.5ms | 2.3ms | 1.9ms | 3.5ms | 4.2ms | +| | Ultra perf. (3x) | 0.7ms | 0.7ms | 0.7ms | 1.0ms | 1.3ms | 1.7ms | 1.6ms | 2.8ms | 3.5ms | +| 2560x1440 | Quality (1.5x) | 0.4ms | 0.4ms | 0.5ms | 0.6ms | 0.8ms | 1.2ms | 1.0ms | 1.8ms | 2.3ms | +| | Balanced (1.7x) | 0.4ms | 0.4ms | 0.4ms | 0.5ms | 0.7ms | 1.0ms | 0.9ms | 1.7ms | 2.1ms | +| | Performance (2x) | 0.4ms | 0.4ms | 0.4ms | 0.5ms | 0.7ms | 0.9ms | 0.8ms | 1.4ms | 1.9ms | +| | Ultra perf. (3x) | 0.3ms | 0.3ms | 0.3ms | 0.4ms | 0.6ms | 0.7ms | 0.7ms | 1.2ms | 1.6ms | +| 1920x1080 | Quality (1.5x) | 0.3ms | 0.3ms | 0.3ms | 0.3ms | 0.4ms | 0.6ms | 0.6ms | 1.0ms | 1.3ms | +| | Balanced (1.7x) | 0.2ms | 0.2ms | 0.2ms | 0.3ms | 0.4ms | 0.6ms | 0.5ms | 0.9ms | 1.2ms | +| | Performance (2x) | 0.2ms | 0.2ms | 0.2ms | 0.3ms | 0.4ms | 0.5ms | 0.5ms | 0.8ms | 1.1ms | +| | Ultra perf. (3x) | 0.2ms | 0.2ms | 0.2ms | 0.2ms | 0.3ms | 0.4ms | 0.4ms | 0.7ms | 0.9ms | -Figures are rounded to the nearest 0.1ms and are without [`enableSharpening`](src/ffx-fsr2-api/ffx_fsr2.h#L127) set. +Figures are rounded to the nearest 0.1ms and are without additional [`sharpness`](src/ffx-fsr2-api/ffx_fsr2.h#L129). ## Memory requirements Using FSR2 requires some additional GPU local memory to be allocated for consumption by the GPU. When using the FSR2 API, this memory is allocated when the FSR2 context is created, and is done so via the series of callbacks which comprise the backend interface. This memory is used to store intermediate surfaces which are computed by the FSR2 algorithm as well as surfaces which are persistent across many frames of the application. The table below includes the amount of memory used by FSR2 under various operating conditions. The "Working set" column indicates the total amount of memory used by FSR2 as the algorithm is executing on the GPU; this is the amount of memory FSR2 will require to run. The "Persistent memory" column indicates how much of the "Working set" column is required to be left intact for subsequent frames of the application; this memory stores the temporal data consumed by FSR2. The "Aliasable memory" column indicates how much of the "Working set" column may be aliased by surfaces or other resources used by the application outside of the operating boundaries of FSR2. @@ -157,41 +158,43 @@ You can take control of resource creation in FSR2 by overriding the resource cre | Resolution | Quality | Working set (MB) | Persistent memory (MB) | Aliasable memory (MB) | | -----------|------------------------|------------------|------------------------|-------------------------| -| 3840x2160 | Quality (1.5x) | 293.53MB | 94.92MB | 198.61MB | -| | Balanced (1.7x) | 274.03MB | 94.92MB | 179.11MB | -| | Performance (2x) | 255.68MB | 94.92MB | 160.76MB | -| | Ultra performance (3x) | 227.11MB | 94.92MB | 132.19MB | -| 2560x1440 | Quality (1.5x) | 136.41MB | 84.37MB | 52.04MB | -| | Balanced (1.7x) | 126.97MB | 84.37MB | 42.60MB | -| | Performance (2x) | 117.53MB | 84.37MB | 33.16MB | -| | Ultra performance (3x) | 104.95MB | 84.37MB | 20.58MB | -| 1920x1080 | Quality (1.5x) | 76.46MB | 47.46MB | 29.18MB | -| | Balanced (1.7x) | 71.75MB | 47.46MB | 23.68MB | -| | Performance (2x) | 67.81MB | 47.46MB | 20.79MB | -| | Ultra performance (3x) | 58.38MB | 47.46MB | 11.09MB | +| 3840x2160 | Quality (1.5x) | 302MB | 218MB | 85MB | +| | Balanced (1.7x) | 279MB | 214MB | 65MB | +| | Performance (2x) | 260MB | 211MB | 49MB | +| | Ultra performance (3x) | 228MB | 206MB | 22MB | +| 2560x1440 | Quality (1.5x) | 140MB | 100MB | 40MB | +| | Balanced (1.7x) | 129MB | 98MB | 33MB | +| | Performance (2x) | 119MB | 97MB | 24MB | +| | Ultra performance (3x) | 105MB | 95MB | 10MB | +| 1920x1080 | Quality (1.5x) | 78MB | 56MB | 22MB | +| | Balanced (1.7x) | 73MB | 55MB | 18MB | +| | Performance (2x) | 69MB | 54MB | 15MB | +| | Ultra performance (3x) | 59MB | 53MB | 6MB | + +Figures are rounded up to nearest MB and are without additional [`sharpness`](src/ffx-fsr2-api/ffx_fsr2.h#L129). Figures are approximations using an RX 6700XT GPU in DX12 and are subject to change. For details on how to manage FSR2's memory requirements please refer to the section of this document dealing with [Memory management](#memory-management). ## Input resources FSR2 is a temporal algorithm, and therefore requires access to data from both the current and previous frame. The following table enumerates all external inputs required by FSR2. -> The resolution column indicates if the data should be at 'rendered' resolution or 'presentation' resolution. 'Rendered' resolution indicates that the resource should match the resolution at which the application is performing its rendering. Conversely, 'presentation' indicates that the resolution of the target should match that which is to be presented to the user. All resources are from the current rendered frame, for DirectX(R)12 and Vulkan(R) applications all input resources should be transitioned to [`D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE`](https://docs.microsoft.com/en-us/windows/win32/api/d3d12/ne-d3d12-d3d12_resource_states) and [`VK_ACCESS_SHADER_READ_BIT`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkAccessFlagBits.html) respectively before calling [`ffxFsr2ContextDispatch`](src/ffx-fsr2-api/ffx_fsr2.h#L254). +> The resolution column indicates if the data should be at 'rendered' resolution or 'presentation' resolution. 'Rendered' resolution indicates that the resource should match the resolution at which the application is performing its rendering. Conversely, 'presentation' indicates that the resolution of the target should match that which is to be presented to the user. All resources are from the current rendered frame, for DirectX(R)12 and Vulkan(R) applications all input resources should be transitioned to [`D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE`](https://docs.microsoft.com/en-us/windows/win32/api/d3d12/ne-d3d12-d3d12_resource_states) and [`VK_ACCESS_SHADER_READ_BIT`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkAccessFlagBits.html) respectively before calling [`ffxFsr2ContextDispatch`](src/ffx-fsr2-api/ffx_fsr2.h#L256). | Name | Resolution | Format | Type | Notes | | ----------------|------------------------------|------------------------------------|-----------|------------------------------------------------| -| Color buffer | Render | `APPLICATION SPECIFIED` | Texture | The render resolution color buffer for the current frame provided by the application. If the contents of the color buffer are in high dynamic range (HDR), then the [`FFX_FSR2_ENABLE_HIGH_DYNAMIC_RANGE`](src/ffx-fsr2-api/ffx_fsr2.h#L87) flag should be set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L103) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L101) structure. | -| Depth buffer | Render | `APPLICATION SPECIFIED (1x FLOAT)` | Texture | The render resolution depth buffer for the current frame provided by the application. The data should be provided as a single floating point value, the precision of which is under the application's control. The configuration of the depth should be communicated to FSR2 via the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L103) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L101) structure when creating the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L164). You should set the [`FFX_FSR2_ENABLE_DEPTH_INVERTED`](src/ffx-fsr2-api/ffx_fsr2.h#L90) flag if your depth buffer is inverted (that is [1..0] range), and you should set the [`FFX_FSR2_ENABLE_DEPTH_INFINITE`](src/ffx-fsr2-api/ffx_fsr2.h#L91) flag if your depth buffer has an infinite far plane. If the application provides the depth buffer in `D32S8` format, then FSR2 will ignore the stencil component of the buffer, and create an `R32_FLOAT` resource to address the depth buffer. On GCN and RDNA hardware, depth buffers are stored separately from stencil buffers. | -| Motion vectors | Render or presentation | `APPLICATION SPECIFIED (2x FLOAT)` | Texture | The 2D motion vectors for the current frame provided by the application in [**(<-width, -height>**..****] range. If your application renders motion vectors with a different range, you may use the [`motionVectorScale`](src/ffx-fsr2-api/ffx_fsr2.h#L125) field of the [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L114) structure to adjust them to match the expected range for FSR2. Internally, FSR2 uses 16-bit quantities to represent motion vectors in many cases, which means that while motion vectors with greater precision can be provided, FSR2 will not benefit from the increased precision. The resolution of the motion vector buffer should be equal to the render resolution, unless the [`FFX_FSR2_ENABLE_DISPLAY_RESOLUTION_MOTION_VECTORS`](src/ffx-fsr2-api/ffx_fsr2.h#L88) flag is set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L103) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L101) structure when creating the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L164), in which case it should be equal to the presentation resolution. | +| Color buffer | Render | `APPLICATION SPECIFIED` | Texture | The render resolution color buffer for the current frame provided by the application. If the contents of the color buffer are in high dynamic range (HDR), then the [`FFX_FSR2_ENABLE_HIGH_DYNAMIC_RANGE`](src/ffx-fsr2-api/ffx_fsr2.h#L88) flag should be set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L104) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L102) structure. | +| Depth buffer | Render | `APPLICATION SPECIFIED (1x FLOAT)` | Texture | The render resolution depth buffer for the current frame provided by the application. The data should be provided as a single floating point value, the precision of which is under the application's control. The configuration of the depth should be communicated to FSR2 via the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L104) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L102) structure when creating the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L166). You should set the [`FFX_FSR2_ENABLE_DEPTH_INVERTED`](src/ffx-fsr2-api/ffx_fsr2.h#L91) flag if your depth buffer is inverted (that is [1..0] range), and you should set the [`FFX_FSR2_ENABLE_DEPTH_INFINITE`](src/ffx-fsr2-api/ffx_fsr2.h#L92) flag if your depth buffer has an infinite far plane. If the application provides the depth buffer in `D32S8` format, then FSR2 will ignore the stencil component of the buffer, and create an `R32_FLOAT` resource to address the depth buffer. On GCN and RDNA hardware, depth buffers are stored separately from stencil buffers. | +| Motion vectors | Render or presentation | `APPLICATION SPECIFIED (2x FLOAT)` | Texture | The 2D motion vectors for the current frame provided by the application in [**(<-width, -height>**..****] range. If your application renders motion vectors with a different range, you may use the [`motionVectorScale`](src/ffx-fsr2-api/ffx_fsr2.h#L126) field of the [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L115) structure to adjust them to match the expected range for FSR2. Internally, FSR2 uses 16-bit quantities to represent motion vectors in many cases, which means that while motion vectors with greater precision can be provided, FSR2 will not benefit from the increased precision. The resolution of the motion vector buffer should be equal to the render resolution, unless the [`FFX_FSR2_ENABLE_DISPLAY_RESOLUTION_MOTION_VECTORS`](src/ffx-fsr2-api/ffx_fsr2.h#L89) flag is set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L104) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L102) structure when creating the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L166), in which case it should be equal to the presentation resolution. | | Reactive mask | Render | `R8_UNORM` | Texture | As some areas of a rendered image do not leave a footprint in the depth buffer or include motion vectors, FSR2 provides support for a reactive mask texture which can be used to indicate to FSR2 where such areas are. Good examples of these are particles, or alpha-blended objects which do not write depth or motion vectors. If this resource is not set, then FSR2's shading change detection logic will handle these cases as best it can, but for optimal results, this resource should be set. For more information on the reactive mask please refer to the [Reactive mask](#reactive-mask) section. | -| Exposure | 1x1 | `R32_FLOAT` | Texture | A 1x1 texture containing the exposure value computed for the current frame. This resource is optional, and may be omitted if the [`FFX_FSR2_ENABLE_AUTO_EXPOSURE`](src/ffx-fsr2-api/ffx_fsr2.h#L92) flag is set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L103) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L101) structure when creating the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L164). | +| Exposure | 1x1 | `R32_FLOAT` | Texture | A 1x1 texture containing the exposure value computed for the current frame. This resource is optional, and may be omitted if the [`FFX_FSR2_ENABLE_AUTO_EXPOSURE`](src/ffx-fsr2-api/ffx_fsr2.h#L93) flag is set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L104) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L102) structure when creating the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L166). | ## Depth buffer configurations It is strongly recommended that an inverted, infinite depth buffer is used with FSR2. However, alternative depth buffer configurations are supported. An application should inform the FSR2 API of its depth buffer configuration by setting the appropriate flags during the creation of the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L164). The table below contains the appropriate flags. | FSR2 flag | Note | |----------------------------------|--------------------------------------------------------------------------------------------| -| [`FFX_FSR2_ENABLE_DEPTH_INVERTED`](src/ffx-fsr2-api/ffx_fsr2.h#L90) | A bit indicating that the input depth buffer data provided is inverted [max..0]. | -| [`FFX_FSR2_ENABLE_DEPTH_INFINITE`](src/ffx-fsr2-api/ffx_fsr2.h#L91) | A bit indicating that the input depth buffer data provided is using an infinite far plane. | +| [`FFX_FSR2_ENABLE_DEPTH_INVERTED`](src/ffx-fsr2-api/ffx_fsr2.h#L91) | A bit indicating that the input depth buffer data provided is inverted [max..0]. | +| [`FFX_FSR2_ENABLE_DEPTH_INFINITE`](src/ffx-fsr2-api/ffx_fsr2.h#L92) | A bit indicating that the input depth buffer data provided is using an infinite far plane. | ## Providing motion vectors @@ -201,11 +204,11 @@ A key part of a temporal algorithm (be it antialiasing or upscaling) is the prov ![alt text](docs/media/super-resolution-temporal/motion-vectors.svg "A diagram showing a 2D motion vector.") -If your application computes motion vectors in another space - for example normalized device coordinate space - then you may use the [`motionVectorScale`](src/ffx-fsr2-api/ffx_fsr2.h#L125) field of the [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L114) structure to instruct FSR2 to adjust them to match the expected range for FSR2. The code examples below illustrate how motion vectors may be scaled to screen space. The example HLSL and C++ code below illustrates how NDC-space motion vectors can be scaled using the FSR2 host API. +If your application computes motion vectors in another space - for example normalized device coordinate space - then you may use the [`motionVectorScale`](src/ffx-fsr2-api/ffx_fsr2.h#L126) field of the [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L115) structure to instruct FSR2 to adjust them to match the expected range for FSR2. The code examples below illustrate how motion vectors may be scaled to screen space. The example HLSL and C++ code below illustrates how NDC-space motion vectors can be scaled using the FSR2 host API. ```HLSL // GPU: Example of application NDC motion vector computation -float2 motionVector = (currentPosition.xy / currentPosition.w) - (previousPosition.xy / previousPosition.w); +float2 motionVector = (previousPosition.xy / previousPosition.w) - (currentPosition.xy / currentPosition.w); // CPU: Matching FSR 2.0 motionVectorScale configuration dispatchParameters.motionVectorScale.x = (float)renderWidth; @@ -213,43 +216,45 @@ dispatchParameters.motionVectorScale.y = (float)renderHeight; ``` ### Precision & resolution -Internally, FSR2 uses 16bit quantities to represent motion vectors in many cases, which means that while motion vectors with greater precision can be provided, FSR2 will not currently benefit from the increased precision. The resolution of the motion vector buffer should be equal to the render resolution, unless the [`FFX_FSR2_ENABLE_DISPLAY_RESOLUTION_MOTION_VECTORS`](src/ffx-fsr2-api/ffx_fsr2.h#L88) flag is set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L103) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L114) structure when creating the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L164), in which case it should be equal to the presentation resolution. +Internally, FSR2 uses 16bit quantities to represent motion vectors in many cases, which means that while motion vectors with greater precision can be provided, FSR2 will not currently benefit from the increased precision. The resolution of the motion vector buffer should be equal to the render resolution, unless the [`FFX_FSR2_ENABLE_DISPLAY_RESOLUTION_MOTION_VECTORS`](src/ffx-fsr2-api/ffx_fsr2.h#L89) flag is set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L104) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L115) structure when creating the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L166), in which case it should be equal to the presentation resolution. ### Coverage FSR2 will perform better quality upscaling when more objects provide their motion vectors. It is therefore advised that all opaque, alpha-tested and alpha-blended objects should write their motion vectors for all covered pixels. If vertex shader effects are applied - such as scrolling UVs - these calculations should also be factored into the calculation of motion for the best results. For alpha-blended objects it is also strongly advised that the alpha value of each covered pixel is stored to the corresponding pixel in the [reactive mask](#reactive-mask). This will allow FSR2 to perform better handling of alpha-blended objects during upscaling. The reactive mask is especially important for alpha-blended objects where writing motion vectors might be prohibitive, such as particles. ## Reactive mask -In the context of FSR2, the term "reactivity" means how much influence the samples rendered for the current frame have over the production of the final upscaled image. Typically, samples rendered for the current frame contribute a relatively modest amount to the result computed by FSR2; however, there are exceptions. To produce the best results for fast moving, alpha-blended objects, FSR2 requires the [Reproject & accumulate](#reproject-accumulate) stage to become more reactive for such pixels. As there is no good way to determine from either color, depth or motion vectors which pixels have been rendered using alpha blending, FSR2 performs best when applications explicity mark such areas. +In the context of FSR2, the term "reactivity" means how much influence the samples rendered for the current frame have over the production of the final upscaled image. Typically, samples rendered for the current frame contribute a relatively modest amount to the result computed by FSR2; however, there are exceptions. To produce the best results for fast moving, alpha-blended objects, FSR2 requires the [Reproject & accumulate](#reproject-accumulate) stage to become more reactive for such pixels. As there is no good way to determine from either color, depth or motion vectors which pixels have been rendered using alpha blending, FSR2 performs best when applications explicitly mark such areas. Therefore, it is strongly encouraged that applications provide a reactive mask to FSR2. The reactive mask guides FSR2 on where it should reduce its reliance on historical information when compositing the current pixel, and instead allow the current frame's samples to contribute more to the final result. The reactive mask allows the application to provide a value from [0..1] where 0 indicates that the pixel is not at all reactive (and should use the default FSR2 composition strategy), and a value of 1 indicates the pixel should be fully reactive. While there are other applications for the reactive mask, the primary application for the reactive mask is producing better results of upscaling images which include alpha-blended objects. A good proxy for reactiveness is actually the alpha value used when compositing an alpha-blended object into the scene, therefore, applications should write `alpha` to the reactive mask. It should be noted that it is unlikely that a reactive value of close to 1 will ever produce good results. Therefore, we recommend clamping the maximum reactive value to around 0.9. -If a [Reactive mask](#reactive-mask) is not provided to FSR2 (by setting the [`reactive`](src/ffx-fsr2-api/ffx_fsr2.h#L121) field of [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L114) to `NULL`) then an internally generated 1x1 texture with a cleared reactive value will be used. +If a [Reactive mask](#reactive-mask) is not provided to FSR2 (by setting the [`reactive`](src/ffx-fsr2-api/ffx_fsr2.h#L122) field of [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L115) to `NULL`) then an internally generated 1x1 texture with a cleared reactive value will be used. ## Transparency & composition mask In addition to the [Reactive mask](#reactive-mask), FSR2 provides for the application to denote areas of other specialist rendering which should be accounted for during the upscaling process. Examples of such special rendering include areas of raytraced reflections or animated textures. While the [Reactive mask](#reactive-mask) adjusts the accumulation balance, the [Transparency & composition mask](#transparency-and-composition-mask) adjusts the pixel locks created by FSR2. A pixel with a value of 0 in the [Transparency & composition mask](#ttransparency-and-composition-mask) does not perform any additional modification to the lock for that pixel. Conversely, a value of 1 denotes that the lock for that pixel should be completely removed. -If a [Transparency & composition mask](#transparency-and-composition-mask) is not provided to FSR2 (by setting the [`transparencyAndComposition`](#src/ffx-fsr2-api/ffx_fsr2.h#L122) field of [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L114) to `NULL`) then an internally generated 1x1 texture with a cleared transparency and composition value will be used. +If a [Transparency & composition mask](#transparency-and-composition-mask) is not provided to FSR2 (by setting the [`transparencyAndComposition`](#src/ffx-fsr2-api/ffx_fsr2.h#L123) field of [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L115) to `NULL`) then an internally generated 1x1 texture with a cleared transparency and composition value will be used. ## Automatically generating reactivity To help applications generate the [Reactive mask](#reactive-mask) and the [Transparency & composition mask](#transparency-and-composition-mask), FSR2 provides an optional helper API. Under the hood, the API launches a compute shader which computes these values for each pixel using a luminance-based heuristic. -Applications wishing to do this can call the [`ffxFsr2ContextGenerateReactiveMask`](src/ffx-fsr2-api/ffx_fsr2.h#L265) function and should pass two versions of the color buffer, one containing opaque only geometry, and the other containing both opaque and alpha-blended objects. +Applications wishing to do this can call the [`ffxFsr2ContextGenerateReactiveMask`](src/ffx-fsr2-api/ffx_fsr2.h#L267) function and should pass two versions of the color buffer, one containing opaque only geometry, and the other containing both opaque and alpha-blended objects. + +In version 2.1, this helper changed slightly in order to give developers more options when items such as decals were used, which may have resulted in shimmer on certain surfaces. A "binaryValue" can now be set in the FfxFsr2GenerateReactiveDescription struct, to provide a specific value to be written into the reactive mask instead of 1.0f, which can be too high. ## Exposure FSR2 provides two values which control the exposure used when performing upscaling. They are as follows: 1. **Pre-exposure** a value by which we divide the input signal to get back to the original signal produced by the game before any packing into lower precision render targets. -2. **Expsoure** a value which is multiplied against the result of the pre-exposed color value. +2. **Exposure** a value which is multiplied against the result of the pre-exposed color value. The exposure value should match that which the application uses during any subsequent tonemapping passes performed by the application. This means FSR2 will operate consistently with what is likely to be visible in the final tonemapped image. > In various stages of the FSR2 algorithm described in this document, FSR2 will compute its own exposure value for internal use. It is worth noting that all outputs from FSR2 will have this internal tonemapping reversed before the final output is written. Meaning that FSR2 returns results in the same domain as the original input signal. -Poorly selected exposure values can have a drastic impact on the final quality of FSR2's upscaling. Therefore, it is recommended that [`FFX_FSR2_ENABLE_AUTO_EXPOSURE`](src/ffx-fsr2-api/ffx_fsr2.h#L92) is used by the application, unless there is a particular reason not to. When [`FFX_FSR2_ENABLE_AUTO_EXPOSURE`](src/ffx-fsr2-api/ffx_fsr2.h#L92) is set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L103) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L101) structure, the exposure calculation shown in the HLSL code below is used to compute the exposure value, this matches the exposure response of ISO 100 film stock. +Poorly selected exposure values can have a drastic impact on the final quality of FSR2's upscaling. Therefore, it is recommended that [`FFX_FSR2_ENABLE_AUTO_EXPOSURE`](src/ffx-fsr2-api/ffx_fsr2.h#L93) is used by the application, unless there is a particular reason not to. When [`FFX_FSR2_ENABLE_AUTO_EXPOSURE`](src/ffx-fsr2-api/ffx_fsr2.h#L93) is set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L104) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L102) structure, the exposure calculation shown in the HLSL code below is used to compute the exposure value, this matches the exposure response of ISO 100 film stock. ```HLSL float ComputeAutoExposureFromAverageLog(float averageLogLuminance) @@ -274,7 +279,7 @@ With any image upscaling approach is it important to understand how to place oth | Post processing A | Post processing B | |--------------------------------|----------------------| | Screenspace reflections | Film grain | -| Screenspace ambient occlusion | Chromatic abberation | +| Screenspace ambient occlusion | Chromatic aberration | | Denoisers (shadow, reflections)| Vignette | | Exposure (optional) | Tonemapping | | | Bloom | @@ -327,7 +332,7 @@ Out of the box, the FSR2 API will compile into multiple libraries following the ## Memory management If the FSR2 API is used with one of the supplied backends (e.g: DirectX(R)12 or Vulkan(R)) then all the resources required by FSR2 are created as committed resources directly using the graphics device provided by the host application. However, by overriding the create and destroy family of functions present in the backend interface it is possible for an application to more precisely control the memory management of FSR2. -To do this, you can either provide a full custom backend to FSR2 via the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L101) structure passed to [`ffxFsr2ContextCreate`](src/ffx-fsr2-api/ffx_fsr2.h#L213) function, or you can retrieve the backend for your desired API and override the resource creation and destruction functions to handle them yourself. To do this, simply overwrite the [`fpCreateResource`](src/ffx-fsr2-api/ffx_fsr2_interface.h#L399) and [`fpDestroyResource`](src/ffx-fsr2-api/ffx_fsr2_interface.h#L403) function pointers. +To do this, you can either provide a full custom backend to FSR2 via the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L102) structure passed to [`ffxFsr2ContextCreate`](src/ffx-fsr2-api/ffx_fsr2.h#L215) function, or you can retrieve the backend for your desired API and override the resource creation and destruction functions to handle them yourself. To do this, simply overwrite the [`fpCreateResource`](src/ffx-fsr2-api/ffx_fsr2_interface.h#L360) and [`fpDestroyResource`](src/ffx-fsr2-api/ffx_fsr2_interface.h#L364) function pointers. ``` CPP // Setup DX12 interface. @@ -372,7 +377,7 @@ Internally, these function implement a Halton[2,3] sequence [[Halton](#reference ![alt text](docs/media/super-resolution-temporal/jitter-space.svg "A diagram showing how to map sub-pixel jitter offsets to projection offsets.") -It is important to understand that the values returned from the [`ffxFsr2GetJitterOffset`](src/ffx-fsr2-api/ffx_fsr2.h#L422) are in unit pixel space, and in order to composite this correctly into a projection matrix we must convert them into projection offsets. The diagram above shows a single pixel in unit pixel space, and in projection space. The code listing below shows how to correctly composite the sub-pixel jitter offset value into a projection matrix. +It is important to understand that the values returned from the [`ffxFsr2GetJitterOffset`](src/ffx-fsr2-api/ffx_fsr2.h#L424) are in unit pixel space, and in order to composite this correctly into a projection matrix we must convert them into projection offsets. The diagram above shows a single pixel in unit pixel space, and in projection space. The code listing below shows how to correctly composite the sub-pixel jitter offset value into a projection matrix. ``` CPP const int32_t jitterPhaseCount = ffxFsr2GetJitterPhaseCount(renderWidth, displayWidth); @@ -390,7 +395,7 @@ const Matrix4 jitteredProjectionMatrix = jitterTranslationMatrix * projectionMat Jitter should be applied to *all* rendering. This includes opaque, alpha transparent, and raytraced objects. For rasterized objects, the sub-pixel jittering values calculated by the [`ffxFsr2GetJitterOffset`](src/ffx-fsr2-api/ffx_fsr2.h#L422) function can be applied to the camera projection matrix which is ultimately used to perform transformations during vertex shading. For raytraced rendering, the sub-pixel jitter should be applied to the ray's origin - often the camera's position. -Whether you elect to use the recommended [`ffxFsr2GetJitterOffset`](src/ffx-fsr2-api/ffx_fsr2.h#L422) function or your own sequence generator, you must set the [`jitterOffset`](src/ffx-fsr2-api/ffx_fsr2.h#L124) field of the [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L114) structure to inform FSR2 of the jitter offset that has been applied in order to render each frame. Moreover, if not using the recommended [`ffxFsr2GetJitterOffset`](src/ffx-fsr2-api/ffx_fsr2.h#L422) function, care should be taken that your jitter sequence never generates a null vector; that is value of 0 in both the X and Y dimensions. +Whether you elect to use the recommended [`ffxFsr2GetJitterOffset`](src/ffx-fsr2-api/ffx_fsr2.h#L424) function or your own sequence generator, you must set the [`jitterOffset`](src/ffx-fsr2-api/ffx_fsr2.h#L125) field of the [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L115) structure to inform FSR2 of the jitter offset that has been applied in order to render each frame. Moreover, if not using the recommended [`ffxFsr2GetJitterOffset`](src/ffx-fsr2-api/ffx_fsr2.h#L424) function, care should be taken that your jitter sequence never generates a null vector; that is value of 0 in both the X and Y dimensions. The table below shows the jitter sequence length for each of the default quality modes. @@ -403,7 +408,7 @@ The table below shows the jitter sequence length for each of the default quality | Custom | [1..n]x (per dimension) | `ceil(8 * n^2)` | ## Camera jump cuts -Most applications with real-time rendering have a large degree of temporal consistency between any two consecutive frames. However, there are cases where a change to a camera's transformation might cause an abrupt change in what is rendered. In such cases, FSR2 is unlikely to be able to reuse any data it has accumulated from previous frames, and should clear this data such to exclude it from consideration in the compositing process. In order to indicate to FSR2 that a jump cut has occurred with the camera you should set the [`reset`](src/ffx-fsr2-api/ffx_fsr2.h#L131) field of the [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L114) structure to `true` for the first frame of the discontinuous camera transformation. +Most applications with real-time rendering have a large degree of temporal consistency between any two consecutive frames. However, there are cases where a change to a camera's transformation might cause an abrupt change in what is rendered. In such cases, FSR2 is unlikely to be able to reuse any data it has accumulated from previous frames, and should clear this data such to exclude it from consideration in the compositing process. In order to indicate to FSR2 that a jump cut has occurred with the camera you should set the [`reset`](src/ffx-fsr2-api/ffx_fsr2.h#L132) field of the [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L115) structure to `true` for the first frame of the discontinuous camera transformation. Rendering performance may be slightly less than typical frame-to-frame operation when using the reset flag, as FSR2 will clear some additional internal resources. @@ -425,8 +430,13 @@ The following table illustrates the mipmap biasing factor which results from eva | Performance | 2.0X (per dimension) | -2.0 | | Ultra performance | 3.0X (per dimension) | -2.58 | +## Frame Time Delta Input +The FSR2 API requires [`frameTimeDelta`](src/ffx-fsr2-api/ffx_fsr2.h#L130) be provided by the application through the [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L115) structure. This value is in __milliseconds__: if running at 60fps, the value passed should be around __16.6f__. + +The value is used within the temporal component of the FSR 2 auto-exposure feature. This allows for tuning of the history accumulation for quality purposes. + ## HDR support -High dynamic range images are supported in FSR2. To enable this, you should set the [`FFX_FSR2_ENABLE_HIGH_DYNAMIC_RANGE`](src/ffx-fsr2-api/ffx_fsr2.h#L87) bit in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L103) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L101) structure. Images should be provided to FSR2 in linear color space. +High dynamic range images are supported in FSR2. To enable this, you should set the [`FFX_FSR2_ENABLE_HIGH_DYNAMIC_RANGE`](src/ffx-fsr2-api/ffx_fsr2.h#L88) bit in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L104) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L102) structure. Images should be provided to FSR2 in linear color space. > Support for additional color spaces might be provided in a future revision of FSR2. @@ -473,7 +483,7 @@ Each pass stage of the algorithm is laid out in the sections following this one, The compute luminance pyramid stage has two responsibilities: 1. To produce a lower resolution version of the input color's luminance. This is used by shading change detection in the accumulation pass. -2. To produce a 1x1 exposure texture which is optionally used by the exposure calculations of the [Adjust input color](#adjust-input-color) stage to apply tonemapping, and the [Reproject & Accumulate](#project-and-accumulate) stage for reversing local tonemapping ahead of producing an ouput from FSR2. +2. To produce a 1x1 exposure texture which is optionally used by the exposure calculations of the [Adjust input color](#adjust-input-color) stage to apply tonemapping, and the [Reproject & Accumulate](#project-and-accumulate) stage for reversing local tonemapping ahead of producing an output from FSR2. ### Resource inputs @@ -483,7 +493,7 @@ The following table contains all resources consumed by the [Compute luminance py | Name | Temporal layer | Resolution | Format | Type | Notes | | ----------------|-----------------|--------------|-------------------------|-----------|----------------------------------------------| -| Color buffer | Current frame | Render | `APPLICATION SPECIFIED` | Texture | The render resolution color buffer for the current frame provided by the application. If the contents of the color buffer are in high dynamic range (HDR), then the [`FFX_FSR2_ENABLE_HIGH_DYNAMIC_RANGE`](src/ffx-fsr2-api/ffx_fsr2.h#L87) flag should be set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L103) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L101) structure. | +| Color buffer | Current frame | Render | `APPLICATION SPECIFIED` | Texture | The render resolution color buffer for the current frame provided by the application. If the contents of the color buffer are in high dynamic range (HDR), then the [`FFX_FSR2_ENABLE_HIGH_DYNAMIC_RANGE`](src/ffx-fsr2-api/ffx_fsr2.h#L87) flag should be set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L104) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L102) structure. | ### Resource outputs The following table contains all resources produced or modified by the [Compute luminance pyramid](#compute-luminance-pyramid) stage. @@ -492,11 +502,11 @@ The following table contains all resources produced or modified by the [Compute | Name | Temporal layer | Resolution | Format | Type | Notes | | ----------------------------|-----------------|------------------|-------------------------|-----------|----------------------------------------------| -| Exposure | Current frame | 1x1 | `R32_FLOAT` | Texture | A 1x1 texture containing the exposure value computed for the current frame. This resource is optional, and may be omitted if the [`FFX_FSR2_ENABLE_AUTO_EXPOSURE`](src/ffx-fsr2-api/ffx_fsr2.h#L92) flag is set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L103) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L101) structure when creating the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L164). | +| Exposure | Current frame | 1x1 | `R32_FLOAT` | Texture | A 1x1 texture containing the exposure value computed for the current frame. This resource is optional, and may be omitted if the [`FFX_FSR2_ENABLE_AUTO_EXPOSURE`](src/ffx-fsr2-api/ffx_fsr2.h#L92) flag is set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L104) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L102) structure when creating the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L166). | | Current luminance | Current frame | `Render * 0.5` | `R16_FLOAT` | Texture | A texture at 50% of render resolution texture which contains the luminance of the current frame. | ### Description -The [Compute luminance pyramid](#compute-luminance-pyramid) stage is implemented using FidelityFX [Single Pass Downsampler](single-pass-downsampler.md), an optimized technique for producing mipmap chains using a single compute shader dispatch. Instead of the conventional (full) pyramidal approach, SPD provides a mechanism to produce a specific set of mipmap levels for an arbitrary input texture, as well as performing arbitrary calculations on that data as we store it to the target location in memory. In FSR2, we are interested in producing in upto two intermediate resources depending on the configuration of the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L164). The first resource is a low-resolution representation of the current luminance, this is used later in FSR2 to attempt to detect shading changes. The second is the exposure value, and while it is always computed, it is only used by subsequent stages if the [`FFX_FSR2_ENABLE_AUTO_EXPOSURE`](src/ffx-fsr2-api/ffx_fsr2.h#L92) flag is set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L103) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L101) structure upon context creation. The exposure value - either from the application, or the [Compute luminance pyramid](#compute-luminance-pyramid) stage - is used in the [Adjust input color](#adjust-input-color) stage of FSR2, as well as by the [Reproject & Accumulate](#project-and-accumulate) stage. +The [Compute luminance pyramid](#compute-luminance-pyramid) stage is implemented using FidelityFX [Single Pass Downsampler](single-pass-downsampler.md), an optimized technique for producing mipmap chains using a single compute shader dispatch. Instead of the conventional (full) pyramidal approach, SPD provides a mechanism to produce a specific set of mipmap levels for an arbitrary input texture, as well as performing arbitrary calculations on that data as we store it to the target location in memory. In FSR2, we are interested in producing in upto two intermediate resources depending on the configuration of the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L166). The first resource is a low-resolution representation of the current luminance, this is used later in FSR2 to attempt to detect shading changes. The second is the exposure value, and while it is always computed, it is only used by subsequent stages if the [`FFX_FSR2_ENABLE_AUTO_EXPOSURE`](src/ffx-fsr2-api/ffx_fsr2.h#L93) flag is set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L104) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L102) structure upon context creation. The exposure value - either from the application, or the [Compute luminance pyramid](#compute-luminance-pyramid) stage - is used in the [Adjust input color](#adjust-input-color) stage of FSR2, as well as by the [Reproject & Accumulate](#project-and-accumulate) stage. ![alt text](docs/media/super-resolution-temporal/auto-exposure.svg "A diagram showing the mipmap levels written by auto-exposure.") @@ -542,8 +552,8 @@ The following table contains all resources consumed by the [Adjust input color]( | Name | Temporal layer | Resolution | Format | Type | Notes | | ----------------|-----------------|--------------|---------------------------|-----------|----------------------------------------------| -| Color buffer | Current frame | Render | `APPLICATION SPECIFIED` | Texture | The render resolution color buffer for the current frame provided by the application. If the contents of the color buffer are in high dynamic range (HDR), then the [`FFX_FSR2_ENABLE_HIGH_DYNAMIC_RANGE`](src/ffx-fsr2-api/ffx_fsr2.h#L87) flag should be set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L103) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L101) structure. | -| Exposure | Current frame | 1x1 | ``R32_FLOAT`` | Texture | A 1x1 texture containing the exposure value computed for the current frame. This resource can be supplied by the application, or computed by the [Compute luminance pyramid](#compute-luminance-pyramid) stage of FSR2 if the [`FFX_FSR2_ENABLE_AUTO_EXPOSURE`](src/ffx-fsr2-api/ffx_fsr2.h#L92) flag is set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L103) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L101) structure. | +| Color buffer | Current frame | Render | `APPLICATION SPECIFIED` | Texture | The render resolution color buffer for the current frame provided by the application. If the contents of the color buffer are in high dynamic range (HDR), then the [`FFX_FSR2_ENABLE_HIGH_DYNAMIC_RANGE`](src/ffx-fsr2-api/ffx_fsr2.h#L88) flag should be set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L104) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L102) structure. | +| Exposure | Current frame | 1x1 | ``R32_FLOAT`` | Texture | A 1x1 texture containing the exposure value computed for the current frame. This resource can be supplied by the application, or computed by the [Compute luminance pyramid](#compute-luminance-pyramid) stage of FSR2 if the [`FFX_FSR2_ENABLE_AUTO_EXPOSURE`](src/ffx-fsr2-api/ffx_fsr2.h#L93) flag is set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L104) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L102) structure. | ### Resource outputs The following table contains all resources produced or modified by the [Adjust input color](#Adjust-input-color) stage. @@ -567,7 +577,7 @@ As the luminance buffer is persistent (it is not available for aliasing, or clea | Green | n-2 | n - 1 | | Blue | n-3 | n - 2 | -The alpha channel of the luminance history buffer contains a measure of the stability of the luminance over the currrent frame, and the three frames that came before it. This is computed in the following way: +The alpha channel of the luminance history buffer contains a measure of the stability of the luminance over the current frame, and the three frames that came before it. This is computed in the following way: ``` HLSL float stabilityValue = 1.0f; @@ -592,8 +602,8 @@ The following table contains all of the resources which are required by the reco | Name | Temporal layer | Resolution | Format | Type | Notes | | ----------------------------|-----------------|------------|------------------------------------|-----------|------------------------------------------------| -| Depth buffer | Current frame | Render | `APPLICATION SPECIFIED (1x FLOAT)` | Texture | The render resolution depth buffer for the current frame provided by the application. The data should be provided as a single floating point value, the precision of which is under the application's control. The configuration of the depth should be communicated to FSR2 via the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L103) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L101) structure when creating the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L164). You should set the [`FFX_FSR2_ENABLE_DEPTH_INVERTED`](src/ffx-fsr2-api/ffx_fsr2.h#L90) flag if your depth buffer is inverted (that is [1..0] range), and you should set the flag if your depth buffer has as infinite far plane. If the application provides the depth buffer in `D32S8` format, then FSR2 will ignore the stencil component of the buffer, and create an `R32_FLOAT` resource to address the depth buffer. On GCN and RDNA hardware, depth buffers are stored separately from stencil buffers. | -| Motion vectors | Current fraame | Render or presentation | `APPLICATION SPECIFIED (2x FLOAT)` | Texture | The 2D motion vectors for the current frame provided by the application in [*(<-width, -height>*..**] range. If your application renders motion vectors with a different range, you may use the [`motionVectorScale`](src/ffx-fsr2-api/ffx_fsr2.h#L125) field of the [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L114) structure to adjust them to match the expected range for FSR2. Internally, FSR2 uses 16bit quantities to represent motion vectors in many cases, which means that while motion vectors with greater precision can be provided, FSR2 will not benefit from the increased precision. The resolution of the motion vector buffer should be equal to the render resolution, unless the [`FFX_FSR2_ENABLE_DISPLAY_RESOLUTION_MOTION_VECTORS`](src/ffx-fsr2-api/ffx_fsr2.h#L88) flag is set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L103) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L101) structure when creating the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L164), in which case it should be equal to the presentation resolution. | +| Depth buffer | Current frame | Render | `APPLICATION SPECIFIED (1x FLOAT)` | Texture | The render resolution depth buffer for the current frame provided by the application. The data should be provided as a single floating point value, the precision of which is under the application's control. The configuration of the depth should be communicated to FSR2 via the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L104) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L102) structure when creating the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L166). You should set the [`FFX_FSR2_ENABLE_DEPTH_INVERTED`](src/ffx-fsr2-api/ffx_fsr2.h#L91) flag if your depth buffer is inverted (that is [1..0] range), and you should set the flag if your depth buffer has as infinite far plane. If the application provides the depth buffer in `D32S8` format, then FSR2 will ignore the stencil component of the buffer, and create an `R32_FLOAT` resource to address the depth buffer. On GCN and RDNA hardware, depth buffers are stored separately from stencil buffers. | +| Motion vectors | Current fraame | Render or presentation | `APPLICATION SPECIFIED (2x FLOAT)` | Texture | The 2D motion vectors for the current frame provided by the application in [*(<-width, -height>*..**] range. If your application renders motion vectors with a different range, you may use the [`motionVectorScale`](src/ffx-fsr2-api/ffx_fsr2.h#L126) field of the [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L115) structure to adjust them to match the expected range for FSR2. Internally, FSR2 uses 16bit quantities to represent motion vectors in many cases, which means that while motion vectors with greater precision can be provided, FSR2 will not benefit from the increased precision. The resolution of the motion vector buffer should be equal to the render resolution, unless the [`FFX_FSR2_ENABLE_DISPLAY_RESOLUTION_MOTION_VECTORS`](src/ffx-fsr2-api/ffx_fsr2.h#L89) flag is set in the [`flags`](src/ffx-fsr2-api/ffx_fsr2.h#L104) field of the [`FfxFsr2ContextDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L102) structure when creating the [`FfxFsr2Context`](src/ffx-fsr2-api/ffx_fsr2.h#L166), in which case it should be equal to the presentation resolution. | ### Resource outputs The following table contains all of the resources which are produced by the reconstruct & dilate stage. @@ -609,7 +619,7 @@ The following table contains all of the resources which are produced by the reco ### Description The first step of the [Reconstruct & dilate](#reconstruct-and-dilate) stage is to compute the dilated depth values and motion vectors from the application's depth values and motion vectors for the current frame. Dilated depth values and motion vectors emphasise the edges of geometry which has been rendered into the depth buffer. This is because the edges of geometry will often introduce discontinuities into a contiguous series of depth values, meaning that as depth values and motion vectors are dilated, they will naturally follow the contours of the geometric edges present in the depth buffer. In order to compute the dilated depth values and motion vectors, FSR2 looks at the depth values for a 3x3 neighbourhood for each pixel and then selects the depth values and motion vectors in that neighbourhood where the depth value is nearest to the camera. In the diagram below, you can see how the central pixel of the 3x3 kernel is updated with the depth value and motion vectors from the pixel with the largest depth value - the pixel on the central, right hand side. -As this stage is the first time that motion vectors are consumed by FSR2, this is where motion vector scaling is applied if using the FSR2 host API. Motion vector scaling factors provided via the [`motionVectorScale`](src/ffx-fsr2-api/ffx_fsr2.h#L125) field of the [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L114) structure and allows you to transform non-screenspace motion vectors into screenspace motion vectors which FSR2 expects. +As this stage is the first time that motion vectors are consumed by FSR2, this is where motion vector scaling is applied if using the FSR2 host API. Motion vector scaling factors provided via the [`motionVectorScale`](src/ffx-fsr2-api/ffx_fsr2.h#L126) field of the [`FfxFsr2DispatchDescription`](src/ffx-fsr2-api/ffx_fsr2.h#L115) structure and allows you to transform non-screenspace motion vectors into screenspace motion vectors which FSR2 expects. ``` CPP // An example of how to manipulate motion vector scaling factors using the FSR2 host API. @@ -804,16 +814,21 @@ To build the FSR2 sample, please follow the following instructions: 3) Open the solutions in the DX12 or Vulkan directory (depending on your preference), compile and run. +# Limitations + +FSR 2 requires a GPU with typed UAV load support. + # Version history | Version | Date | Notes | | ---------------|-------------------|--------------------------------------------------------------| +| **2.1.0** | 2022-09-06 | Release of FidelityFX Super Resolution 2.1. | | **2.0.1** | 2022-06-22 | Initial release of FidelityFX Super Resolution 2.0. | # References [**Akeley-06**] Kurt Akeley and Jonathan Su, **"Minimum Triangle Separation for Correct Z-Buffer Occlusion"**, -[http://www.cs.cmu.edu/afs/cs/academic/class/15869-f11/www/readings/akeley06_triseparation.pdf](http://www.cs.cmu.edu/afs/cs/academic/class/15869-f11/www/readings/akeley06_triseparation.pdf) +[http://www.cs.cmu.edu/afs/cs/academic/class/15869-f11/www/readings/akeley06_triseparation.pdf](https://www.cs.cmu.edu/afs/cs/academic/class/15869-f11/www/readings/akeley06_triseparation.pdf) [**Lanczos**] Lanczos resampling, **"Lanczos resampling"**, [https://en.wikipedia.org/wiki/Lanczos_resampling](https://en.wikipedia.org/wiki/Lanczos_resampling) diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..4bae377 --- /dev/null +++ b/changelog.md @@ -0,0 +1,19 @@ + +2022-09-06 | FidelityFX Super Resolution 2.1 +------- +- Reactivity mask now uses full range of value in the mask (0.0 - 1.0). +- Reactivity and Composition and Transparency mask dialation is now based on input colors to avoid expanding reactiveness into non-relevant upscaled areas. +- Disocclusion logic improved in order to detect disocclusions in areas with very small depth deparation. +- RCAS Pass forced to fp32 mode to reduce chance of issues seen with HDR input values. +- Fix for display-resolution motion vectors interpretation. +- FP16/FP32 computation review, readjusting balance of fp16/fp32 for maximum quality. +- Amended motion vector description within the documentation. +- Various documentation edits for spelling. +- Clarified the frame delta time input value within the readme documentation. +- Fixed issue with bad memset within the shader blob selection logic. + + +2022-06-22 | FidelityFX Super Resolution 2.0.1 +------- +- First release. + diff --git a/libs/cauldron b/libs/cauldron index 08e3881..b92d559 160000 --- a/libs/cauldron +++ b/libs/cauldron @@ -1 +1 @@ -Subproject commit 08e3881a04a0e207d65b4560d023c74c3775732e +Subproject commit b92d559bd083f44df9f8f42a6ad149c1584ae94c diff --git a/media/checkerboard.dds b/media/checkerboard.dds new file mode 100644 index 0000000000000000000000000000000000000000..f1a6e7f6bfabac3015a8e9ad278a27174608fcfd GIT binary patch literal 87508 zcmeH~L2eUK6a>Gr%8nc%M*%4};5ta=<|acZ$r_|F_$^9~DM zwZ6POt)K6@eXghNr?s9x6% zrEdE6Z*j*r{%!+su+&Z8{w?nK#@}rK4wky<+rPyf-}t)?z`;^Cefzh#;~Rgs0XSId zrf>ficYNdTHUI}p-Sq9>;*M|p-3H)bshht2Tio%DzuN#DEOpbje~UZ5HhDxaXERDb0 zfW;kOH+}nugQf9z8?d!xr2aIiG~ZUYu~eBJcz9}bqr-)+F+j<1`({lmf1_`3~Q z-0^kOw|_WT8h^I|i#xt<`t}b8OXKf0U~$LSP2c|EU}^l_1}yIQy6M|L94w8$+knL# zUpIaGhl8c@cN?&{-7#@}tg;*PJI-rqkxSo-Mwm)~u` zI`{ax>HX(V`P~LMhnBkOJO37UeB6%rEdE6Z*j*r{%!+su+&Z8{w?nK#@}rK4wky<+rPyf-}t)?z`;^C zefzh#;~Rgs0XSIdrf>ficYNdTHUI}p-Sq9>;*M|p-3H)bshht2Tio%DzuN#DEOpbj ze~UZ5@pl`5gQafz_HS{=H~ww|aIn-(-~KJ`_{QID01lSA>Dzz(yA4>o!`DsU{@`F~ z{M`mD?)bXt+dmvEjlbJ~#T{Qaefx)lrSW$gu(;#vrf>gnur&T|0~U9D-Sq7r4wlB> zZNTD=ubaO8!@<({yA4>}@paR;e>hkgf42dPJHBrE_74Y3DxaXERDb0fW;kOH+}nugQf9z8?d!xr2aIiG~ZUYu~eBJcz9}bqr-)+F+ zj<1`({lmf1_`3~Q-0^kOw|_WT8h^I|i#xt<`t}cpd)4XrndZ)a)z$Kk{#XlQYoyn>8@$zWw+7o1MPy|Mu7YH#r0CZ`#1@{S$WGpWUCc)7Smk z{<=RWXQ2Jf8klt7{n!0BJAK`M?XUZ9at7Mptbs}Q-GBc57r&mLlQS@Tdi3v?N%uWJ z{pV-rf7U?fzx!u$24<(P`)AU9_fPjv`|GoWn|4q(7`Aw4~`)_vo=J(&V^Ly>@*ZYs}pZYs) zp!4INpJO`LpD(@txPi{Md%o-cxc~Y&?&o;jJs&$i_kDgk-^UGfzVG{d$M2ZmzMuUX zKKt)KTHZa+{r8X7$D`%l^X7Yc_}}kueIGvPt$)s~@56V~?E}61n%7+Z+U3{V@1IVm HzdnBf@>Y`w literal 0 HcmV?d00001 diff --git a/media/composition_text.dds b/media/composition_text.dds new file mode 100644 index 0000000000000000000000000000000000000000..4baf836ead10f96cf7e88b5e87f2d5022ae41bf9 GIT binary patch literal 1398144 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 z00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa z0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV= z5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHaf zKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_ z009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz z00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_< z0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb z2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$## zAOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;| zfB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U< z00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa z0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV= z5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHaf zKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_ z009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz z00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_< z0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb z2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$## zAOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;| zfB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U< z00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa z0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV= z5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHaf zKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_ z009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz z00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_< z0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb z2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$## zAOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;| zfB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U< z00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa z0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV= 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 DhLItp literal 0 HcmV?d00001 diff --git a/media/lion.jpg b/media/lion.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e830649607218b436e955f5cf23b180628219259 GIT binary patch literal 357550 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 z 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≶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, pOutBlob->GetBufferPointer(), pOutBlob->GetBufferSize(), IID_PPV_ARGS(&m_pRootSignature))); + SetName( m_pRootSignature, "AnimatedTexture" ); + + pOutBlob->Release(); + if (pErrorBlob) + pErrorBlob->Release(); + + D3D12_GRAPHICS_PIPELINE_STATE_DESC descPso = {}; + descPso.pRootSignature = m_pRootSignature; + descPso.VS = vs; + descPso.PS = ps; + descPso.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT); + descPso.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_GREATER_EQUAL; + descPso.DSVFormat = DXGI_FORMAT_D32_FLOAT; + descPso.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); + descPso.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; + descPso.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); + descPso.BlendState.IndependentBlendEnable = true; + descPso.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; + descPso.BlendState.RenderTarget[1].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_RED | D3D12_COLOR_WRITE_ENABLE_GREEN; + descPso.BlendState.RenderTarget[2].RenderTargetWriteMask = 0x0; + descPso.BlendState.RenderTarget[3].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_RED; + descPso.SampleMask = UINT_MAX; + descPso.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + descPso.NumRenderTargets = 4; + descPso.RTVFormats[0] = DXGI_FORMAT_R16G16B16A16_FLOAT; + descPso.RTVFormats[1] = DXGI_FORMAT_R16G16_FLOAT; + descPso.RTVFormats[2] = DXGI_FORMAT_R8_UNORM; + descPso.RTVFormats[3] = DXGI_FORMAT_R8_UNORM; + descPso.SampleDesc.Count = 1; + + ThrowIfFailed(device.GetDevice()->CreateGraphicsPipelineState(&descPso, IID_PPV_ARGS(&m_pPipelines[0]))); + SetName(m_pPipelines[0], "AnimatedTexturePipelineComp"); + + descPso.BlendState.RenderTarget[3].RenderTargetWriteMask = 0; + ThrowIfFailed(device.GetDevice()->CreateGraphicsPipelineState(&descPso, IID_PPV_ARGS(&m_pPipelines[1]))); + SetName(m_pPipelines[1], "AnimatedTexturePipelineNoComp"); + + UINT indices[6] = { 0, 1, 2, 2, 1, 3 }; + bufferPool.AllocIndexBuffer( _countof( indices ), sizeof( UINT ), indices, &m_indexBuffer ); + + resourceViewHeaps.AllocCBV_SRV_UAVDescriptor( _countof( m_textures ), &m_descriptorTable ); + + m_textures[0].InitFromFile( &device, &uploadHeap, "..\\media\\lion.jpg", true ); + m_textures[1].InitFromFile( &device, &uploadHeap, "..\\media\\checkerboard.dds", true ); + m_textures[2].InitFromFile( &device, &uploadHeap, "..\\media\\composition_text.dds", true ); + + for ( int i = 0; i < _countof( m_textures ); i++ ) + { + m_textures[ i ].CreateSRV( i, &m_descriptorTable ); + } +} + + +void AnimatedTextures::OnDestroy() +{ + for ( int i = 0; i < _countof( m_textures ); i++ ) + { + m_textures[i].OnDestroy(); + } + + for ( int i = 0; i < _countof( m_pPipelines ); i++ ) + { + m_pPipelines[i]->Release(); + m_pPipelines[i] = nullptr; + } + + m_pRootSignature->Release(); + m_pRootSignature = nullptr; + m_pResourceViewHeaps = nullptr; +} + + +void AnimatedTextures::Render( ID3D12GraphicsCommandList* pCommandList, float frameTime, float speed, bool compositionMask, const Camera& camera ) +{ + struct ConstantBuffer + { + math::Matrix4 currentViewProj; + math::Matrix4 previousViewProj; + float jitterCompensation[ 2 ]; + float scrollFactor; + float rotationFactor; + int mode; + int pads[3]; + }; + + m_scrollFactor += frameTime * 1.0f * speed; + m_rotationFactor += frameTime * 2.0f * speed; + m_flipTimer += frameTime * 1.0f; + + if ( m_scrollFactor > 10.0f ) + m_scrollFactor -= 10.0f; + + const float twoPI = 6.283185307179586476925286766559f; + + if ( m_rotationFactor > twoPI ) + m_rotationFactor -= twoPI; + + int textureIndex = min( (int)floorf( m_flipTimer * 0.33333f ), _countof( m_textures ) - 1 ); + if ( m_flipTimer > 9.0f ) + m_flipTimer = 0.0f; + + D3D12_GPU_VIRTUAL_ADDRESS cb = {}; + ConstantBuffer* constantBuffer = nullptr; + m_constantBufferRing->AllocConstantBuffer( sizeof(*constantBuffer), (void**)&constantBuffer, &cb ); + + constantBuffer->currentViewProj = camera.GetProjection() * camera.GetView(); + constantBuffer->previousViewProj = camera.GetPrevProjection() * camera.GetPrevView(); + + constantBuffer->jitterCompensation[0] = camera.GetPrevProjection().getCol2().getX() - camera.GetProjection().getCol2().getX(); + constantBuffer->jitterCompensation[1] = camera.GetPrevProjection().getCol2().getY() - camera.GetProjection().getCol2().getY(); + constantBuffer->scrollFactor = m_scrollFactor; + constantBuffer->rotationFactor = m_rotationFactor; + constantBuffer->mode = textureIndex; + + ID3D12DescriptorHeap* descriptorHeaps[] = { m_pResourceViewHeaps->GetCBV_SRV_UAVHeap(), m_pResourceViewHeaps->GetSamplerHeap() }; + pCommandList->SetDescriptorHeaps( _countof( descriptorHeaps ), descriptorHeaps ); + pCommandList->SetGraphicsRootSignature( m_pRootSignature ); + pCommandList->SetGraphicsRootDescriptorTable( 0, m_descriptorTable.GetGPU( textureIndex ) ); + pCommandList->SetGraphicsRootConstantBufferView( 1, cb ); + + pCommandList->IASetPrimitiveTopology( D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST ); + pCommandList->IASetIndexBuffer( &m_indexBuffer ); + pCommandList->IASetVertexBuffers( 0, 0, nullptr ); + pCommandList->SetPipelineState( m_pPipelines[compositionMask ? 0 : 1] ); + pCommandList->DrawIndexedInstanced( 6, 2, 0, 0, 0 ); +} + diff --git a/src/DX12/AnimatedTexture.h b/src/DX12/AnimatedTexture.h new file mode 100644 index 0000000..639b1c4 --- /dev/null +++ b/src/DX12/AnimatedTexture.h @@ -0,0 +1,56 @@ +// FidelityFX Super Resolution Sample +// +// Copyright (c) 2022 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +#pragma once + + +#include "stdafx.h" + + +class AnimatedTextures +{ +public: + + AnimatedTextures() {} + virtual ~AnimatedTextures() {} + + void OnCreate( Device& device, UploadHeap& uploadHeap, StaticBufferPool& bufferPool, ResourceViewHeaps& resourceViewHeaps, DynamicBufferRing& constantBufferRing ); + void OnDestroy(); + + void Render( ID3D12GraphicsCommandList* pCommandList, float frameTime, float speed, bool compositionMask, const Camera& camera ); + +private: + + ResourceViewHeaps* m_pResourceViewHeaps = nullptr; + DynamicBufferRing* m_constantBufferRing = nullptr; + + ID3D12RootSignature* m_pRootSignature = nullptr; + ID3D12PipelineState* m_pPipelines[2] = {}; + D3D12_INDEX_BUFFER_VIEW m_indexBuffer = {}; + + Texture m_textures[3] = {}; + CBV_SRV_UAV m_descriptorTable = {}; + + float m_scrollFactor = 0.0f; + float m_rotationFactor = 0.0f; + float m_flipTimer = 0.0f; +}; \ No newline at end of file diff --git a/src/DX12/AnimatedTexture.hlsl b/src/DX12/AnimatedTexture.hlsl new file mode 100644 index 0000000..0f708e2 --- /dev/null +++ b/src/DX12/AnimatedTexture.hlsl @@ -0,0 +1,129 @@ +// Copyright (c) 2022 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +cbuffer cb : register(b0) +{ + matrix g_CurrentViewProjection; + matrix g_PreviousViewProjection; + float2 g_CameraJitterCompensation; + float g_ScrollFactor; + float g_RotationFactor; + int g_Mode; + int pad0; + int pad1; + int pad2; +} + + +Texture2D g_Texture : register(t0); +SamplerState g_Sampler : register(s0); + +struct VERTEX_OUT +{ + float4 CurrentPosition : TEXCOORD0; + float4 PreviousPosition : TEXCOORD1; + float3 TexCoord : TEXCOORD2; + float4 Position : SV_POSITION; +}; + + +VERTEX_OUT VSMain( uint vertexId : SV_VertexID, uint instanceId : SV_InstanceID ) +{ + VERTEX_OUT output = (VERTEX_OUT)0; + + const float2 offsets[ 4 ] = + { + float2( -1, 1 ), + float2( 1, 1 ), + float2( -1, -1 ), + float2( 1, -1 ), + }; + + float2 offset = offsets[ vertexId ]; + float2 uv = (offset+1)*float2( instanceId == 0 ? -0.5 : 0.5, -0.5 ); + + float4 worldPos = float4( offsets[ vertexId ], 0.0, 1.0 ); + + worldPos.xyz += instanceId == 0 ? float3( -13, 1.5, 2 ) : float3( -13, 1.5, -2 ); + + output.CurrentPosition = mul( g_CurrentViewProjection, worldPos ); + output.PreviousPosition = mul( g_PreviousViewProjection, worldPos ); + + output.Position = output.CurrentPosition; + + output.TexCoord.xy = uv; + output.TexCoord.z = instanceId; + + return output; +} + +struct Output +{ + float4 finalColor : SV_TARGET0; + float2 motionVectors : SV_TARGET1; + float upscaleReactive : SV_TARGET2; + float upscaleTransparencyAndComposition : SV_TARGET3; +}; + + +float4 TextureLookup( int billboardIndex, float2 uv0 ) +{ + float4 color = 1; + + if ( billboardIndex == 0 || g_Mode == 2 ) + { + // Scrolling + float2 uv = uv0; + if ( g_Mode == 2 ) + uv += float2( -g_ScrollFactor, 0.0 ); + else + uv += float2( -g_ScrollFactor, 0.5*g_ScrollFactor ); + + color.rgb = g_Texture.SampleLevel( g_Sampler, uv, 0 ).rgb; + } + else if ( billboardIndex == 1 ) + { + // Rotated UVs + float s, c; + sincos( g_RotationFactor, s, c ); + float2x2 rotation = { float2( c, s ), float2( -s, c ) }; + + float2 rotatedUV = mul( rotation, uv0-float2( 0.5, -0.5) ); + color.rgb = g_Texture.SampleLevel( g_Sampler, rotatedUV, 0 ).rgb; + } + + return color; +} + + +Output PSMain( VERTEX_OUT input ) +{ + Output output = (Output)0; + + output.finalColor = TextureLookup( (int)input.TexCoord.z, input.TexCoord.xy ); + + output.motionVectors = (input.PreviousPosition.xy / input.PreviousPosition.w) - (input.CurrentPosition.xy / input.CurrentPosition.w) + g_CameraJitterCompensation; + output.motionVectors *= float2(0.5f, -0.5f); + + output.upscaleReactive = 0; // Nothing to write to the reactice mask. Color writes are off on this target anyway. + output.upscaleTransparencyAndComposition = 1; // Write a value into here to indicate the depth and motion vectors are as expected for a static object, but the surface contents are changing. + + return output; +} \ No newline at end of file diff --git a/src/DX12/CMakeLists.txt b/src/DX12/CMakeLists.txt index 1ad0f01..01330f1 100644 --- a/src/DX12/CMakeLists.txt +++ b/src/DX12/CMakeLists.txt @@ -38,6 +38,14 @@ set(sources stdafx.h UI.cpp UI.h + AnimatedTexture.cpp + AnimatedTexture.h + ../GpuParticles/ParticleHelpers.h + ../GpuParticles/ParticleSystem.h + ../GpuParticles/ParticleSystemInternal.h + ../GpuParticles/dx12/GPUParticleSystem.cpp + ../GpuParticles/dx12/ParallelSort.h + ../GpuParticles/dx12/ParallelSort.cpp dpiawarescaling.manifest) set(fsr1_shaders_src @@ -94,26 +102,40 @@ set(fsr2_shaders_src ${CMAKE_CURRENT_SOURCE_DIR}/../ffx-fsr2-api/shaders/ffx_fsr2_rcas.h ${CMAKE_CURRENT_SOURCE_DIR}/../ffx-fsr2-api/shaders/ffx_fsr2_autogen_reactive_pass.hlsl) -set(sample_shader_src - ${CMAKE_CURRENT_SOURCE_DIR}/UpscaleSpatial.hlsl - ${CMAKE_CURRENT_SOURCE_DIR}/FSRPass.hlsl +set(particle_shaders_src + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/ParticleStructs.h + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/ParticleHelpers.h + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/fp16util.h + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/ParallelSortCS.hlsl + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/ParticleEmit.hlsl + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/ParticleRender.hlsl + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/ParticleSimulation.hlsl + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/ShaderConstants.h + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/SimulationBindings.h + ${CMAKE_CURRENT_SOURCE_DIR}/../ffx-parallelsort/FFX_ParallelSort.h) + +set(sample_shaders_src ${CMAKE_CURRENT_SOURCE_DIR}/GPUFrameRateLimiter.hlsl - ${CMAKE_CURRENT_SOURCE_DIR}/DebugBlit.hlsl) + ${CMAKE_CURRENT_SOURCE_DIR}/AnimatedTexture.hlsl + ${CMAKE_CURRENT_SOURCE_DIR}/DebugBlit.hlsl + ${CMAKE_CURRENT_SOURCE_DIR}/UpscaleSpatial.hlsl + ${CMAKE_CURRENT_SOURCE_DIR}/FSRPass.hlsl) set(APP_ICON_GPUOPEN "${CMAKE_CURRENT_SOURCE_DIR}/../common/GpuOpenIcon.rc") source_group("sources" FILES ${sources}) -source_group("shaders" FILES ${sample_shader_src}) -source_group("spd_shaders" FILES ${spd_shaders_src}) -source_group("fsr1_shaders" FILES ${fsr1_shaders_src}) +source_group("spatial_shaders" FILES ${fsr1_shaders_src}) source_group("fsr2_shaders" FILES ${fsr2_shaders_src}) +source_group("particle_shaders" FILES ${particle_shaders_src}) +source_group("sample_shaders" FILES ${sample_shaders_src}) copyCommand("${spd_shaders_src}" ${CMAKE_HOME_DIRECTORY}/bin/ShaderLibDX) copyCommand("${fsr1_shaders_src}" ${CMAKE_HOME_DIRECTORY}/bin/ShaderLibDX) copyCommand("${fsr2_shaders_src}" ${CMAKE_HOME_DIRECTORY}/bin/ShaderLibDX) -copyCommand("${sample_shader_src}" ${CMAKE_HOME_DIRECTORY}/bin/ShaderLibDX) +copyCommand("${particle_shaders_src}" ${CMAKE_HOME_DIRECTORY}/bin/ShaderLibDX) +copyCommand("${sample_shaders_src}" ${CMAKE_HOME_DIRECTORY}/bin/ShaderLibDX) -add_executable(FSR2_Sample_DX12 WIN32 ${sources} ${fsr1_shaders_src} ${spd_shaders_src} ${fsr2_shaders_src} ${sample_shader_src} ${common} ${APP_ICON_GPUOPEN}) +add_executable(FSR2_Sample_DX12 WIN32 ${sources} ${fsr2_src} ${sample_shaders_src} ${fsr1_shaders_src} ${fsr2_shaders_src} ${particle_shaders_src} ${spd_shaders_src} ${common} ${APP_ICON_GPUOPEN}) target_compile_definitions(FSR2_Sample_DX12 PRIVATE USE_PIX=1 $<$:FSR2_DEBUG_SHADERS=1>) target_link_libraries(FSR2_Sample_DX12 LINK_PUBLIC FSR2_Sample_Common Cauldron_DX12 ImGUI amd_ags d3dcompiler D3D12 ffx_fsr2_api_x64 ffx_fsr2_api_dx12_x64) target_include_directories(FSR2_Sample_DX12 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../ffx-fsr2-api ${CMAKE_CURRENT_SOURCE_DIR}/../../libs) diff --git a/src/DX12/FSR2Sample.cpp b/src/DX12/FSR2Sample.cpp index 8556b5b..8712b78 100644 --- a/src/DX12/FSR2Sample.cpp +++ b/src/DX12/FSR2Sample.cpp @@ -51,7 +51,7 @@ void FSR2Sample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint32_t* // set some default values *pWidth = 1920; *pHeight = 1080; - m_activeScene = 0; //load the first one by default + m_UIState.m_activeScene = 0; //load the first one by default m_VsyncEnabled = false; m_bIsBenchmarking = false; m_fontSize = 13.f; // default value overridden by a json file if available @@ -66,7 +66,7 @@ void FSR2Sample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint32_t* *pWidth = jData.value("width", *pWidth); *pHeight = jData.value("height", *pHeight); m_fullscreenMode = jData.value("presentationMode", m_fullscreenMode); - m_activeScene = jData.value("activeScene", m_activeScene); + m_UIState.m_activeScene = jData.value("activeScene", m_UIState.m_activeScene); m_activeCamera = jData.value("activeCamera", m_activeCamera); m_isCpuValidationLayerEnabled = jData.value("CpuValidationLayerEnabled", m_isCpuValidationLayerEnabled); m_isGpuValidationLayerEnabled = jData.value("GpuValidationLayerEnabled", m_isGpuValidationLayerEnabled); @@ -774,7 +774,7 @@ int WINAPI WinMain(HINSTANCE hInstance, LPSTR lpCmdLine, int nCmdShow) { - LPCSTR Name = "FidelityFX Super Resolution 2.0"; + LPCSTR Name = "FidelityFX Super Resolution 2.1"; // create new DX sample return RunFramework(hInstance, lpCmdLine, nCmdShow, new FSR2Sample(Name)); diff --git a/src/DX12/FSR2Sample.h b/src/DX12/FSR2Sample.h index 188a3ad..f422309 100644 --- a/src/DX12/FSR2Sample.h +++ b/src/DX12/FSR2Sample.h @@ -77,7 +77,6 @@ private: // json config file json m_jsonConfigFile; std::vector m_sceneNames; - int m_activeScene; int m_activeCamera; bool m_bPlay; diff --git a/src/DX12/Renderer.cpp b/src/DX12/Renderer.cpp index d3d79b0..938302a 100644 --- a/src/DX12/Renderer.cpp +++ b/src/DX12/Renderer.cpp @@ -124,6 +124,9 @@ void Renderer::OnCreate(Device* pDevice, SwapChain *pSwapChain, float FontSize, // TAA m_ResourceViewHeaps.AllocCBV_SRV_UAVDescriptor(3, &m_UpscaleSRVs); + m_pGPUParticleSystem = IParticleSystem::CreateGPUSystem("..\\media\\atlas.dds"); + m_pGPUParticleSystem->OnCreateDevice(*pDevice, m_UploadHeap, m_ResourceViewHeaps, m_VidMemBufferPool, m_ConstantBufferRing); + m_GpuFrameRateLimiter.OnCreate(pDevice, &m_ResourceViewHeaps); // needs to be completely reinitialized, as the format potentially changes @@ -131,6 +134,8 @@ void Renderer::OnCreate(Device* pDevice, SwapChain *pSwapChain, float FontSize, DXGI_FORMAT mFormat = (hdr ? m_pGBufferHDRTexture->GetFormat() : DXGI_FORMAT_R8G8B8A8_UNORM); m_MagnifierPS.OnCreate(m_pDevice, &m_ResourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool, mFormat); + m_AnimatedTextures.OnCreate( *pDevice, m_UploadHeap, m_VidMemBufferPool, m_ResourceViewHeaps, m_ConstantBufferRing ); + ResetScene(); } @@ -141,8 +146,13 @@ void Renderer::OnCreate(Device* pDevice, SwapChain *pSwapChain, float FontSize, //-------------------------------------------------------------------------------------- void Renderer::OnDestroy() { + m_AnimatedTextures.OnDestroy(); m_GpuFrameRateLimiter.OnDestroy(); + m_pGPUParticleSystem->OnDestroyDevice(); + delete m_pGPUParticleSystem; + m_pGPUParticleSystem = nullptr; + m_AsyncPool.Flush(); m_ImGUI.OnDestroy(); @@ -244,6 +254,8 @@ void Renderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, UISta m_MagnifierPS.OnCreateWindowSizeDependentResources(&m_displayOutput); + m_pGPUParticleSystem->OnResizedSwapChain(pState->renderWidth, pState->renderHeight, m_GBuffer.m_DepthBuffer); + // Lazy Upscale context generation: if ((m_pUpscaleContext == NULL) || (pState->m_nUpscaleType != m_pUpscaleContext->Type())) { @@ -276,6 +288,8 @@ void Renderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, UISta //-------------------------------------------------------------------------------------- void Renderer::OnDestroyWindowSizeDependentResources() { + m_pGPUParticleSystem->OnReleasingSwapChain(); + m_displayOutput.OnDestroy(); m_renderOutput.OnDestroy(); m_OpaqueTexture.OnDestroy(); @@ -502,7 +516,7 @@ void Renderer::AllocateShadowMaps(GLTFCommon* pGLTFCommon) std::vector::iterator CurrentShadow = m_shadowMapPool.begin(); for( uint32_t i = 0; CurrentShadow < m_shadowMapPool.end(); ++i, ++CurrentShadow) { - CurrentShadow->ShadowMap.InitDepthStencil(m_pDevice, "m_pShadowMap", &CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_D32_FLOAT, CurrentShadow->ShadowResolution, CurrentShadow->ShadowResolution, 1, 1, 1, 0, D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL), 0.0f); + CurrentShadow->ShadowMap.InitDepthStencil(m_pDevice, "m_pShadowMap", &CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_D32_FLOAT, CurrentShadow->ShadowResolution, CurrentShadow->ShadowResolution, 1, 1, 1, 0, D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL), 1.0f); CurrentShadow->ShadowMap.CreateDSV(CurrentShadow->ShadowIndex, &m_ShadowMapPoolDSV); CurrentShadow->ShadowMap.CreateSRV(CurrentShadow->ShadowIndex, &m_ShadowMapPoolSRV); } @@ -545,6 +559,7 @@ void Renderer::OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChai m_pUpscaleContext->PreDraw(pState); static float fLightModHelper = 2.f; + float fLightMod = 1.f; // Sets the perFrame data per_frame *pPerFrame = NULL; @@ -568,6 +583,27 @@ void Renderer::OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChai m_pGLTFTexturesAndBuffers->SetSkinningMatricesForSkeletons(); } + { + m_state.flags = IParticleSystem::PF_Streaks | IParticleSystem::PF_DepthCull | IParticleSystem::PF_Sort; + m_state.flags |= pState->nReactiveMaskMode == REACTIVE_MASK_MODE_ON ? IParticleSystem::PF_Reactive : 0; + + const Camera& camera = pState->camera; + m_state.constantData.m_ViewProjection = camera.GetProjection() * camera.GetView(); + m_state.constantData.m_View = camera.GetView(); + m_state.constantData.m_ViewInv = math::inverse(camera.GetView()); + m_state.constantData.m_Projection = camera.GetProjection(); + m_state.constantData.m_ProjectionInv = math::inverse(camera.GetProjection()); + m_state.constantData.m_SunDirection = math::Vector4(0.7f, 0.7f, 0, 0); + m_state.constantData.m_SunColor = math::Vector4(0.8f, 0.8f, 0.7f, 0); + m_state.constantData.m_AmbientColor = math::Vector4(0.2f, 0.2f, 0.3f, 0); + + m_state.constantData.m_SunColor *= fLightMod; + m_state.constantData.m_AmbientColor *= fLightMod; + + m_state.constantData.m_FrameTime = pState->m_bPlayAnimations ? (0.001f * (float)pState->deltaTime) : 0.0f; + PopulateEmitters(pState->m_bPlayAnimations, pState->m_activeScene, 0.001f * (float)pState->deltaTime); + } + // command buffer calls ID3D12GraphicsCommandList* pCmdLst1 = m_CommandListRing.GetNewCommandList(); @@ -664,6 +700,12 @@ void Renderer::OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChai #else pGltfPbr->DrawBatchList(pCmdLst1, &m_ShadowMapPoolSRV, &opaque, bWireframe); #endif + + if (pState->bRenderAnimatedTextures) + { + m_AnimatedTextures.Render(pCmdLst1, pState->m_bPlayAnimations ? (0.001f * (float)pState->deltaTime) : 0.0f, pState->m_fTextureAnimationSpeed, pState->bCompositionMask, Cam); + } + m_GPUTimer.GetTimeStamp(pCmdLst1, "PBR Opaque"); pRenderPassFullGBuffer->EndPass(); } @@ -711,6 +753,13 @@ void Renderer::OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChai { pRenderPassFullGBuffer->BeginPass(pCmdLst1, false); + if (pState->bRenderParticleSystem) + { + pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_GBuffer.m_DepthBuffer.GetResource(), D3D12_RESOURCE_STATE_DEPTH_WRITE, D3D12_RESOURCE_STATE_DEPTH_READ | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE)); + m_pGPUParticleSystem->Render(pCmdLst1, m_ConstantBufferRing, m_state.flags, m_state.emitters, m_state.numEmitters, m_state.constantData); + pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_GBuffer.m_DepthBuffer.GetResource(), D3D12_RESOURCE_STATE_DEPTH_READ | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_DEPTH_WRITE)); + } + std::sort(transparent.begin(), transparent.end()); pGltfPbr->DrawBatchList(pCmdLst1, &m_ShadowMapPoolSRV, &transparent, bWireframe); m_GPUTimer.GetTimeStamp(pCmdLst1, "PBR Transparent"); @@ -757,6 +806,24 @@ void Renderer::OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChai }; pCmdLst1->ResourceBarrier(1, preResolve); + // if FSR2 and auto reactive mask is enabled: generate reactive mask + if (pState->nReactiveMaskMode == REACTIVE_MASK_MODE_AUTOGEN) + { + UpscaleContext::FfxUpscaleSetup upscaleSetup; + upscaleSetup.cameraSetup.vCameraPos = pState->camera.GetPosition(); + upscaleSetup.cameraSetup.mCameraView = pState->camera.GetView(); + upscaleSetup.cameraSetup.mCameraViewInv = math::inverse(pState->camera.GetView()); + upscaleSetup.cameraSetup.mCameraProj = pState->camera.GetProjection(); + upscaleSetup.opaqueOnlyColorResource = m_OpaqueTexture.GetResource(); + upscaleSetup.unresolvedColorResource = m_GBuffer.m_HDR.GetResource(); + upscaleSetup.motionvectorResource = m_GBuffer.m_MotionVectors.GetResource(); + upscaleSetup.depthbufferResource = m_GBuffer.m_DepthBuffer.GetResource(); + upscaleSetup.reactiveMapResource = m_GBuffer.m_UpscaleReactive.GetResource(); + upscaleSetup.transparencyAndCompositionResource = m_GBuffer.m_UpscaleTransparencyAndComposition.GetResource(); + upscaleSetup.resolvedColorResource = m_displayOutput.GetResource(); + m_pUpscaleContext->GenerateReactiveMask(pCmdLst1, upscaleSetup, pState); + } + // Post proc--------------------------------------------------------------------------- // Bloom, takes HDR as input and applies bloom to it. @@ -979,12 +1046,101 @@ void Renderer::OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChai void Renderer::ResetScene() { + ZeroMemory(m_EmissionRates, sizeof(m_EmissionRates)); + // Reset the particle system when the scene changes so no particles from the previous scene persist + m_pGPUParticleSystem->Reset(); } -void Renderer::PopulateEmitters(float frameTime) +void Renderer::PopulateEmitters(bool playAnimations, int activeScene, float frameTime) { - bool m_Paused = false; + IParticleSystem::EmitterParams sparksEmitter = {}; + IParticleSystem::EmitterParams smokeEmitter = {}; + + sparksEmitter.m_NumToEmit = 0; + sparksEmitter.m_ParticleLifeSpan = 1.0f; + sparksEmitter.m_StartSize = 0.6f * 0.02f; + sparksEmitter.m_EndSize = 0.4f * 0.02f; + sparksEmitter.m_VelocityVariance = 1.5f; + sparksEmitter.m_Mass = 1.0f; + sparksEmitter.m_TextureIndex = 1; + sparksEmitter.m_Streaks = true; + + smokeEmitter.m_NumToEmit = 0; + smokeEmitter.m_ParticleLifeSpan = 50.0f; + smokeEmitter.m_StartSize = 0.4f; + smokeEmitter.m_EndSize = 1.0f; + smokeEmitter.m_VelocityVariance = 1.0f; + smokeEmitter.m_Mass = 0.0003f; + smokeEmitter.m_TextureIndex = 0; + smokeEmitter.m_Streaks = false; + + if ( activeScene == 0 ) // scene 0 = warehouse + { + m_state.numEmitters = 2; + m_state.emitters[0] = sparksEmitter; + m_state.emitters[1] = sparksEmitter; + + m_state.emitters[0].m_Position = math::Vector4(-4.15f, -1.85f, -3.8f, 1.0f); + m_state.emitters[0].m_PositionVariance = math::Vector4(0.1f, 0.0f, 0.0f, 1.0f); + m_state.emitters[0].m_Velocity = math::Vector4(0.0f, 0.08f, 0.8f, 1.0f); + m_EmissionRates[0].m_ParticlesPerSecond = 300.0f; + + m_state.emitters[1].m_Position = math::Vector4(-4.9f, -1.5f, -4.8f, 1.0f); + m_state.emitters[1].m_PositionVariance = math::Vector4(0.0f, 0.0f, 0.0f, 1.0f); + m_state.emitters[1].m_Velocity = math::Vector4(0.0f, 0.8f, -0.8f, 1.0f); + m_EmissionRates[1].m_ParticlesPerSecond = 400.0f; + + m_state.constantData.m_StartColor[0] = math::Vector4(10.0f, 10.0f, 2.0f, 0.9f); + m_state.constantData.m_EndColor[0] = math::Vector4(10.0f, 10.0f, 0.0f, 0.1f); + m_state.constantData.m_StartColor[1] = math::Vector4(10.0f, 10.0f, 2.0f, 0.9f); + m_state.constantData.m_EndColor[1] = math::Vector4(10.0f, 10.0f, 0.0f, 0.1f); + } + else if ( activeScene == 1 ) // Sponza + { + m_state.numEmitters = 2; + m_state.emitters[0] = smokeEmitter; + m_state.emitters[1] = sparksEmitter; + + m_state.emitters[0].m_Position = math::Vector4(-13.0f, 0.0f, 1.4f, 1.0f); + m_state.emitters[0].m_PositionVariance = math::Vector4(0.1f, 0.0f, 0.1f, 1.0f); + m_state.emitters[0].m_Velocity = math::Vector4(0.0f, 0.2f, 0.0f, 1.0f); + m_EmissionRates[0].m_ParticlesPerSecond = 10.0f; + + m_state.emitters[1].m_Position = math::Vector4(-13.0f, 0.0f, -1.4f, 1.0f); + m_state.emitters[1].m_PositionVariance = math::Vector4(0.05f, 0.0f, 0.05f, 1.0f); + m_state.emitters[1].m_Velocity = math::Vector4(0.0f, 4.0f, 0.0f, 1.0f); + m_state.emitters[1].m_VelocityVariance = 0.5f; + m_state.emitters[1].m_StartSize = 0.02f; + m_state.emitters[1].m_EndSize = 0.02f; + m_state.emitters[1].m_Mass = 1.0f; + m_EmissionRates[1].m_ParticlesPerSecond = 500.0f; + + m_state.constantData.m_StartColor[0] = math::Vector4(0.3f, 0.3f, 0.3f, 0.4f); + m_state.constantData.m_EndColor[0] = math::Vector4(0.4f, 0.4f, 0.4f, 0.1f); + m_state.constantData.m_StartColor[1] = math::Vector4(10.0f, 10.0f, 10.0f, 0.9f); + m_state.constantData.m_EndColor[1] = math::Vector4(5.0f, 8.0f, 5.0f, 0.1f); + } + + // Update all our active emitters so we know how many whole numbers of particles to emit from each emitter this frame + for (int i = 0; i < m_state.numEmitters; i++) + { + m_state.constantData.m_EmitterLightingCenter[i] = m_state.emitters[ i ].m_Position; + + if (m_EmissionRates[i].m_ParticlesPerSecond > 0.0f) + { + m_EmissionRates[i].m_Accumulation += m_EmissionRates[i].m_ParticlesPerSecond * (playAnimations ? frameTime : 0.0f); + + if (m_EmissionRates[i].m_Accumulation > 1.0f) + { + float integerPart = 0.0f; + float fraction = modf(m_EmissionRates[i].m_Accumulation, &integerPart); + + m_state.emitters[i].m_NumToEmit = (int)integerPart; + m_EmissionRates[i].m_Accumulation = fraction; + } + } + } } diff --git a/src/DX12/Renderer.h b/src/DX12/Renderer.h index eb95ca3..d48da04 100644 --- a/src/DX12/Renderer.h +++ b/src/DX12/Renderer.h @@ -28,6 +28,10 @@ #include "UpscaleContext.h" #include "GPUFrameRateLimiter.h" +#include "../GpuParticles/ParticleSystem.h" +#include "../GpuParticleShaders/ShaderConstants.h" +#include "AnimatedTexture.h" + struct UIState; // We are queuing (backBufferCount + 0.5) frames, so we need to triple buffer the resources that get modified each frame @@ -60,11 +64,28 @@ public: void OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChain); - void ResetScene(); - void PopulateEmitters(float frameTime); void BuildDevUI(UIState* pState); private: + + struct State + { + float frameTime = 0.0f; + int numEmitters = 0; + IParticleSystem::EmitterParams emitters[10] = {}; + int flags = 0; + IParticleSystem::ConstantData constantData = {}; + }; + + struct EmissionRate + { + float m_ParticlesPerSecond = 0.0f; // Number of particles to emit per second + float m_Accumulation = 0.0f; // Running total of how many particles to emit over elapsed time + }; + + void ResetScene(); + void PopulateEmitters(bool playAnimations, int activeScene, float frameTime); + Device *m_pDevice; uint32_t m_Width; @@ -99,6 +120,13 @@ private: ColorConversionPS m_ColorConversionPS; MagnifierPS m_MagnifierPS; + // GPU Particle System + State m_state = {}; + IParticleSystem* m_pGPUParticleSystem = nullptr; + EmissionRate m_EmissionRates[NUM_EMITTERS] = {}; + + AnimatedTextures m_AnimatedTextures = {}; + // TAA CBV_SRV_UAV m_UpscaleSRVs; UpscaleContext* m_pUpscaleContext; diff --git a/src/DX12/UI.cpp b/src/DX12/UI.cpp index be15f54..0e625c1 100644 --- a/src/DX12/UI.cpp +++ b/src/DX12/UI.cpp @@ -73,7 +73,7 @@ void FSR2Sample::BuildUI() // if we haven't initialized GLTFLoader yet, don't draw UI. if (m_pGltfLoader == nullptr) { - LoadScene(m_activeScene); + LoadScene(m_UIState.m_activeScene); return; } @@ -123,12 +123,13 @@ void FSR2Sample::BuildUI() ImGui::Checkbox("Camera Headbobbing", &m_UIState.m_bHeadBobbing); auto getterLambda = [](void* data, int idx, const char** out_str)->bool { *out_str = ((std::vector *)data)->at(idx).c_str(); return true; }; - if (ImGui::Combo("Model", &m_activeScene, getterLambda, &m_sceneNames, (int)m_sceneNames.size())) + if (ImGui::Combo("Model", &m_UIState.m_activeScene, getterLambda, &m_sceneNames, (int)m_sceneNames.size())) { + m_UIState.bRenderAnimatedTextures = (m_UIState.m_activeScene == 1); // Note: // probably queueing this as an event and handling it at the end/beginning // of frame is a better idea rather than in the middle of drawing UI. - LoadScene(m_activeScene); + LoadScene(m_UIState.m_activeScene); //bail out as we need to reload everything ImGui::End(); @@ -220,14 +221,15 @@ void FSR2Sample::BuildUI() } - if (m_UIState.m_nUpscaleType == UPSCALE_TYPE_FSR_2_0) + if (ImGui::Checkbox("Dynamic resolution", &m_UIState.bDynamicRes)) { - if (ImGui::Checkbox("Dynamic resolution", &m_UIState.bDynamicRes)) { - OnResize(); - } + OnResize(); } - else - m_UIState.bDynamicRes = false; + + const char* reactiveOptions[] = { "Disabled", "Manual Reactive Mask Generation", "Autogen FSR2 Helper Function" }; + ImGui::Combo("Reactive Mask mode", (int*)(&m_UIState.nReactiveMaskMode), reactiveOptions, _countof(reactiveOptions)); + + ImGui::Checkbox("Use Transparency and Composition Mask", &m_UIState.bCompositionMask); } else if (m_UIState.m_nUpscaleType <= UPSCALE_TYPE_FSR_1_0) { @@ -258,6 +260,11 @@ void FSR2Sample::BuildUI() m_UIState.mipBias = mipBias[UPSCALE_QUALITY_MODE_NONE]; } + if (m_UIState.m_nUpscaleType != UPSCALE_TYPE_FSR_2_0) + { + m_UIState.bDynamicRes = false; + } + ImGui::Checkbox("RCAS Sharpening", &m_UIState.bUseRcas); if (m_UIState.m_nUpscaleType == UPSCALE_TYPE_FSR_2_0) { @@ -349,7 +356,7 @@ void FSR2Sample::BuildUI() if (ImGui::CollapsingHeader("Presentation Mode", ImGuiTreeNodeFlags_DefaultOpen)) { - const char* fullscreenModes[] = { "Windowed", "BorderlessFullscreen", "ExclusiveFulscreen" }; + const char* fullscreenModes[] = { "Windowed", "BorderlessFullscreen", "ExclusiveFullscreen" }; if (ImGui::Combo("Fullscreen Mode", (int*)&m_fullscreenMode, fullscreenModes, _countof(fullscreenModes))) { if (m_previousFullscreenMode != m_fullscreenMode) @@ -661,4 +668,4 @@ bool UIState::DevOption(float* pFloatValue, const char* name, float fMin, float void UIState::Text(const char* text) { ImGui::Text(text); -} \ No newline at end of file +} diff --git a/src/DX12/UI.h b/src/DX12/UI.h index 72a0319..37cc582 100644 --- a/src/DX12/UI.h +++ b/src/DX12/UI.h @@ -55,11 +55,25 @@ typedef enum UpscaleQualityMode { UPSCALE_QUALITY_MODE_COUNT } UpscaleQualityMode; +typedef enum ReactiveMaskMode { + REACTIVE_MASK_MODE_OFF = 0, // Nothing written to the reactive mask + REACTIVE_MASK_MODE_ON = 1, // Particles written to the reactive mask + REACTIVE_MASK_MODE_AUTOGEN = 2, // The mask is auto generated using FSR2's helper function + + // add above this. + REACTIVE_MASK_MODE_COUNT +} ReactiveMaskMode; + struct UIState { Camera camera; bool m_bHeadBobbing = false; + bool m_bPlayAnimations = true; + float m_fTextureAnimationSpeed = 1.0f; + int m_activeScene = 0; + bool m_bAnimateSpotlight = false; + // // WINDOW MANAGEMENT // @@ -72,7 +86,12 @@ struct UIState int SelectedTonemapperIndex; float Exposure; float ExposureHdr = 1.f; - bool bReset = false; + + bool bReset = false; + + int nLightModulationMode = 0; + bool bRenderParticleSystem = true; + bool bRenderAnimatedTextures = true; bool bUseMagnifier; bool bLockMagnifierPosition; bool bLockMagnifierPositionHistory; @@ -104,15 +123,19 @@ struct UIState unsigned int closestVelocitySamplePattern = 0; // 5 samples float Feedback = 15.f / 16.f; - // FSR2 auto reactive - bool bUseFsr2AutoReactive = false; + // FSR2 reactive mask + ReactiveMaskMode nReactiveMaskMode = REACTIVE_MASK_MODE_ON; float fFsr2AutoReactiveScale = 1.f; - float fFsr2AutoReactiveThreshold = 0.01f; + float fFsr2AutoReactiveThreshold = 0.2f; + float fFsr2AutoReactiveBinaryValue = 0.9f; bool bFsr2AutoReactiveTonemap = true; bool bFsr2AutoReactiveInverseTonemap = false; bool bFsr2AutoReactiveThreshold = true; bool bFsr2AutoReactiveUseMax = true; + // FSR2 composition mask + bool bCompositionMask = true; + // FSR2 debug out bool bUseDebugOut = false; int nDebugBlitSurface = 6; // FFX_FSR2_RESOURCE_IDENTIFIER_INTERNAL_UPSCALED_COLOR diff --git a/src/DX12/UpscaleContext_FSR2_API.cpp b/src/DX12/UpscaleContext_FSR2_API.cpp index bb62bb3..896a5b5 100644 --- a/src/DX12/UpscaleContext_FSR2_API.cpp +++ b/src/DX12/UpscaleContext_FSR2_API.cpp @@ -104,8 +104,11 @@ void UpscaleContext_FSR2_API::OnCreateWindowSizeDependentResources( initializationParameters.maxRenderSize.height = renderHeight; initializationParameters.displaySize.width = displayWidth; initializationParameters.displaySize.height = displayHeight; - initializationParameters.flags = FFX_FSR2_ENABLE_DEPTH_INVERTED - | FFX_FSR2_ENABLE_AUTO_EXPOSURE; + initializationParameters.flags = FFX_FSR2_ENABLE_AUTO_EXPOSURE; + + if (m_bInvertedDepth) { + initializationParameters.flags |= FFX_FSR2_ENABLE_DEPTH_INVERTED; + } if (hdr) { initializationParameters.flags |= FFX_FSR2_ENABLE_HIGH_DYNAMIC_RANGE; @@ -130,8 +133,13 @@ void UpscaleContext_FSR2_API::OnCreateWindowSizeDependentResources( void UpscaleContext_FSR2_API::OnDestroyWindowSizeDependentResources() { UpscaleContext::OnDestroyWindowSizeDependentResources(); - ffxFsr2ContextDestroy(&context); - free(initializationParameters.callbacks.scratchBuffer); + // only destroy contexts which are live + if (initializationParameters.callbacks.scratchBuffer != nullptr) + { + ffxFsr2ContextDestroy(&context); + free(initializationParameters.callbacks.scratchBuffer); + initializationParameters.callbacks.scratchBuffer = nullptr; + } } void UpscaleContext_FSR2_API::BuildDevUI(UIState* pState) @@ -158,6 +166,7 @@ void UpscaleContext_FSR2_API::GenerateReactiveMask(ID3D12GraphicsCommandList* pC generateReactiveParameters.scale = pState->fFsr2AutoReactiveScale; generateReactiveParameters.cutoffThreshold = pState->fFsr2AutoReactiveThreshold; + generateReactiveParameters.binaryValue = pState->fFsr2AutoReactiveBinaryValue; generateReactiveParameters.flags = (pState->bFsr2AutoReactiveTonemap ? FFX_FSR2_AUTOREACTIVEFLAGS_APPLY_TONEMAP :0) | (pState->bFsr2AutoReactiveInverseTonemap ? FFX_FSR2_AUTOREACTIVEFLAGS_APPLY_INVERSETONEMAP : 0) | (pState->bFsr2AutoReactiveThreshold ? FFX_FSR2_AUTOREACTIVEFLAGS_APPLY_THRESHOLD : 0) | @@ -174,8 +183,26 @@ void UpscaleContext_FSR2_API::Draw(ID3D12GraphicsCommandList* pCommandList, cons dispatchParameters.depth = ffxGetResourceDX12(&context, cameraSetup.depthbufferResource, L"FSR2_InputDepth"); dispatchParameters.motionVectors = ffxGetResourceDX12(&context, cameraSetup.motionvectorResource, L"FSR2_InputMotionVectors"); dispatchParameters.exposure = ffxGetResourceDX12(&context, nullptr, L"FSR2_InputExposure"); - dispatchParameters.reactive = ffxGetResourceDX12(&context, cameraSetup.reactiveMapResource, L"FSR2_InputReactiveMap"); - dispatchParameters.transparencyAndComposition = ffxGetResourceDX12(&context, cameraSetup.transparencyAndCompositionResource, L"FSR2_TransparencyAndCompositionMap"); + + if ((pState->nReactiveMaskMode == ReactiveMaskMode::REACTIVE_MASK_MODE_ON) + || (pState->nReactiveMaskMode == ReactiveMaskMode::REACTIVE_MASK_MODE_AUTOGEN)) + { + dispatchParameters.reactive = ffxGetResourceDX12(&context, cameraSetup.reactiveMapResource, L"FSR2_InputReactiveMap"); + } + else + { + dispatchParameters.reactive = ffxGetResourceDX12(&context, nullptr, L"FSR2_EmptyInputReactiveMap"); + } + + if (pState->bCompositionMask == true) + { + dispatchParameters.transparencyAndComposition = ffxGetResourceDX12(&context, cameraSetup.transparencyAndCompositionResource, L"FSR2_TransparencyAndCompositionMap"); + } + else + { + dispatchParameters.transparencyAndComposition = ffxGetResourceDX12(&context, nullptr, L"FSR2_EmptyTransparencyAndCompositionMap"); + } + dispatchParameters.output = ffxGetResourceDX12(&context, cameraSetup.resolvedColorResource, L"FSR2_OutputUpscaledColor", FFX_RESOURCE_STATE_UNORDERED_ACCESS); dispatchParameters.jitterOffset.x = m_JitterX; dispatchParameters.jitterOffset.y = m_JitterY; diff --git a/src/DX12/UpscaleContext_Spatial.cpp b/src/DX12/UpscaleContext_Spatial.cpp index 6198201..f517f04 100644 --- a/src/DX12/UpscaleContext_Spatial.cpp +++ b/src/DX12/UpscaleContext_Spatial.cpp @@ -72,7 +72,7 @@ void UpscaleContext_Spatial::OnCreate(const FfxUpscaleInitParams& initParams) CD3DX12_STATIC_SAMPLER_DESC sd[4] = {}; sd[0].Init(0, D3D12_FILTER_MIN_MAG_MIP_POINT, D3D12_TEXTURE_ADDRESS_MODE_CLAMP, D3D12_TEXTURE_ADDRESS_MODE_CLAMP); - sd[1].Init(1, D3D12_FILTER_MIN_MAG_MIP_POINT, D3D12_TEXTURE_ADDRESS_MODE_CLAMP, D3D12_TEXTURE_ADDRESS_MODE_CLAMP); + sd[1].Init(1, D3D12_FILTER_MIN_MAG_MIP_LINEAR, D3D12_TEXTURE_ADDRESS_MODE_CLAMP, D3D12_TEXTURE_ADDRESS_MODE_CLAMP); sd[2].Init(2, D3D12_FILTER_MIN_MAG_MIP_LINEAR, D3D12_TEXTURE_ADDRESS_MODE_CLAMP, D3D12_TEXTURE_ADDRESS_MODE_CLAMP); sd[3].Init(3, D3D12_FILTER_MIN_MAG_MIP_POINT, D3D12_TEXTURE_ADDRESS_MODE_CLAMP, D3D12_TEXTURE_ADDRESS_MODE_CLAMP); diff --git a/src/GpuParticleShaders/Globals.h b/src/GpuParticleShaders/Globals.h new file mode 100644 index 0000000..6c9fce6 --- /dev/null +++ b/src/GpuParticleShaders/Globals.h @@ -0,0 +1,92 @@ +// +// Copyright (c) 2019 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#include "ShaderConstants.h" + + +#define FLOAT float +#define FLOAT2 float2 +#define FLOAT3 float3 +#define FLOAT4 float4 +#define FLOAT2X2 float2x2 +#define UINT uint +#define UINT2 uint2 +#define UINT3 uint3 +#define UINT4 uint4 + + +// Per-frame constant buffer +[[vk::binding( 10, 0 )]] cbuffer PerFrameConstantBuffer : register( b0 ) +{ + float4 g_StartColor[ NUM_EMITTERS ]; + float4 g_EndColor[ NUM_EMITTERS ]; + + float4 g_EmitterLightingCenter[ NUM_EMITTERS ]; + + matrix g_mViewProjection; + matrix g_mView; + matrix g_mViewInv; + matrix g_mProjection; + matrix g_mProjectionInv; + + float4 g_EyePosition; + float4 g_SunDirection; + float4 g_SunColor; + float4 g_AmbientColor; + + float4 g_SunDirectionVS; + + uint g_ScreenWidth; + uint g_ScreenHeight; + float g_InvScreenWidth; + float g_InvScreenHeight; + + float g_AlphaThreshold; + float g_ElapsedTime; + float g_CollisionThickness; + int g_CollideParticles; + + int g_ShowSleepingParticles; + int g_EnableSleepState; + float g_FrameTime; + int g_MaxParticles; + + uint g_NumTilesX; + uint g_NumTilesY; + uint g_NumCoarseCullingTilesX; + uint g_NumCoarseCullingTilesY; + + uint g_NumCullingTilesPerCoarseTileX; + uint g_NumCullingTilesPerCoarseTileY; + uint g_AlignedScreenWidth; + uint g_Pad1; +}; + + + + + +// Declare the global samplers +[[vk::binding( 12, 0 )]] SamplerState g_samWrapLinear : register( s0 ); +[[vk::binding( 13, 0 )]] SamplerState g_samClampLinear : register( s1 ); +[[vk::binding( 14, 0 )]] SamplerState g_samWrapPoint : register( s2 ); + diff --git a/src/GpuParticleShaders/ParallelSortCS.hlsl b/src/GpuParticleShaders/ParallelSortCS.hlsl new file mode 100644 index 0000000..9dfb928 --- /dev/null +++ b/src/GpuParticleShaders/ParallelSortCS.hlsl @@ -0,0 +1,123 @@ +// ParallelSortCS.hlsl +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +//-------------------------------------------------------------------------------------- +// ParallelSort Shaders/Includes +//-------------------------------------------------------------------------------------- +#define FFX_HLSL +#include "FFX_ParallelSort.h" + +[[vk::binding(0, 0)]] ConstantBuffer CBuffer : register(b0); // Constant buffer +[[vk::binding(0, 1)]] cbuffer SetupIndirectCB : register(b1) // Setup Indirect Constant buffer +{ + uint MaxThreadGroups; +}; + +struct RootConstantData +{ + uint CShiftBit; +}; + +#ifdef API_VULKAN +[[vk::push_constant]] RootConstantData rootConstData : register(b2); // Store the shift bit directly in the root signature +#else +ConstantBuffer rootConstData : register(b2); // Store the shift bit directly in the root signature +#endif + +[[vk::binding(0, 2)]] RWStructuredBuffer SrcBuffer : register(u0, space0); // The unsorted keys or scan data +[[vk::binding(2, 2)]] RWStructuredBuffer SrcPayload : register(u0, space1); // The payload data + +[[vk::binding(0, 4)]] RWStructuredBuffer SumTable : register(u0, space2); // The sum table we will write sums to +[[vk::binding(1, 4)]] RWStructuredBuffer ReduceTable : register(u0, space3); // The reduced sum table we will write sums to + +[[vk::binding(1, 2)]] RWStructuredBuffer DstBuffer : register(u0, space4); // The sorted keys or prefixed data +[[vk::binding(3, 2)]] RWStructuredBuffer DstPayload : register(u0, space5); // the sorted payload data + +[[vk::binding(0, 3)]] RWStructuredBuffer ScanSrc : register(u0, space6); // Source for Scan Data +[[vk::binding(1, 3)]] RWStructuredBuffer ScanDst : register(u0, space7); // Destination for Scan Data +[[vk::binding(2, 3)]] RWStructuredBuffer ScanScratch : register(u0, space8); // Scratch data for Scan + +[[vk::binding( 0, 5 )]] StructuredBuffer g_ElementCount : register( t0 ); +[[vk::binding(1, 5)]] RWStructuredBuffer CBufferUAV : register(u0, space10); // UAV for constant buffer parameters for indirect execution +[[vk::binding(2, 5)]] RWStructuredBuffer CountScatterArgs : register(u0, space11); // Count and Scatter Args for indirect execution +[[vk::binding(3, 5)]] RWStructuredBuffer ReduceScanArgs : register(u0, space12); // Reduce and Scan Args for indirect execution + + + +// FPS Count +[numthreads(FFX_PARALLELSORT_THREADGROUP_SIZE, 1, 1)] +void FPS_Count(uint localID : SV_GroupThreadID, uint groupID : SV_GroupID) +{ + // Call the uint version of the count part of the algorithm + FFX_ParallelSort_Count_uint(localID, groupID, CBuffer, rootConstData.CShiftBit, SrcBuffer, SumTable); +} + +// FPS Reduce +[numthreads(FFX_PARALLELSORT_THREADGROUP_SIZE, 1, 1)] +void FPS_CountReduce(uint localID : SV_GroupThreadID, uint groupID : SV_GroupID) +{ + // Call the reduce part of the algorithm + FFX_ParallelSort_ReduceCount(localID, groupID, CBuffer, SumTable, ReduceTable); +} + +// FPS Scan +[numthreads(FFX_PARALLELSORT_THREADGROUP_SIZE, 1, 1)] +void FPS_Scan(uint localID : SV_GroupThreadID, uint groupID : SV_GroupID) +{ + uint BaseIndex = FFX_PARALLELSORT_ELEMENTS_PER_THREAD * FFX_PARALLELSORT_THREADGROUP_SIZE * groupID; + FFX_ParallelSort_ScanPrefix(CBuffer.NumScanValues, localID, groupID, 0, BaseIndex, false, + CBuffer, ScanSrc, ScanDst, ScanScratch); +} + +// FPS ScanAdd +[numthreads(FFX_PARALLELSORT_THREADGROUP_SIZE, 1, 1)] +void FPS_ScanAdd(uint localID : SV_GroupThreadID, uint groupID : SV_GroupID) +{ + // When doing adds, we need to access data differently because reduce + // has a more specialized access pattern to match optimized count + // Access needs to be done similarly to reduce + // Figure out what bin data we are reducing + uint BinID = groupID / CBuffer.NumReduceThreadgroupPerBin; + uint BinOffset = BinID * CBuffer.NumThreadGroups; + + // Get the base index for this thread group + //uint BaseIndex = FFX_PARALLELSORT_ELEMENTS_PER_THREAD * FFX_PARALLELSORT_THREADGROUP_SIZE * (groupID / FFX_PARALLELSORT_SORT_BIN_COUNT); + uint BaseIndex = (groupID % CBuffer.NumReduceThreadgroupPerBin) * FFX_PARALLELSORT_ELEMENTS_PER_THREAD * FFX_PARALLELSORT_THREADGROUP_SIZE; + + FFX_ParallelSort_ScanPrefix(CBuffer.NumThreadGroups, localID, groupID, BinOffset, BaseIndex, true, + CBuffer, ScanSrc, ScanDst, ScanScratch); +} + +// FPS Scatter +[numthreads(FFX_PARALLELSORT_THREADGROUP_SIZE, 1, 1)] +void FPS_Scatter(uint localID : SV_GroupThreadID, uint groupID : SV_GroupID) +{ + FFX_ParallelSort_Scatter_uint(localID, groupID, CBuffer, rootConstData.CShiftBit, SrcBuffer, DstBuffer, SumTable +#ifdef kRS_ValueCopy + ,SrcPayload, DstPayload +#endif // kRS_ValueCopy + ); +} + +[numthreads(1, 1, 1)] +void FPS_SetupIndirectParameters(uint localID : SV_GroupThreadID) +{ + FFX_ParallelSort_SetupIndirectParams(g_ElementCount[ 0 ], MaxThreadGroups, CBufferUAV, CountScatterArgs, ReduceScanArgs); +} \ No newline at end of file diff --git a/src/GpuParticleShaders/ParticleEmit.hlsl b/src/GpuParticleShaders/ParticleEmit.hlsl new file mode 100644 index 0000000..1a0123c --- /dev/null +++ b/src/GpuParticleShaders/ParticleEmit.hlsl @@ -0,0 +1,101 @@ +// +// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#include "ParticleStructs.h" +#include "SimulationBindings.h" + + +// Emitter index has 8 bits +// Texture index has 5 bits +uint WriteEmitterProperties( uint emitterIndex, uint textureIndex, bool isStreakEmitter ) +{ + uint properties = 0; + + properties |= (emitterIndex & 0xff) << 16; + + properties |= ( textureIndex & 0x1f ) << 24; + + if ( isStreakEmitter ) + { + properties |= 1 << 30; + } + + return properties; +} + + +groupshared int g_ldsNumParticlesAvailable; + + +// Emit particles, one per thread, in blocks of 1024 at a time +[numthreads(1024,1,1)] +void CS_Emit( uint3 localIdx : SV_GroupThreadID, uint3 globalIdx : SV_DispatchThreadID ) +{ + if ( localIdx.x == 0 ) + { + int maxParticles = min( g_MaxParticlesThisFrame, g_MaxParticles ); + g_ldsNumParticlesAvailable = clamp( g_DeadList[ 0 ], 0, maxParticles ); + } + + GroupMemoryBarrierWithGroupSync(); + + // Check to make sure we don't emit more particles than we specified + if ( globalIdx.x < g_ldsNumParticlesAvailable ) + { + int numDeadParticles = 0; + InterlockedAdd( g_DeadList[ 0 ], -1, numDeadParticles ); + + if ( numDeadParticles > 0 && numDeadParticles <= g_MaxParticles ) + { + // Initialize the particle data to zero to avoid any unexpected results + GPUParticlePartA pa = (GPUParticlePartA)0; + GPUParticlePartB pb = (GPUParticlePartB)0; + + // Generate some random numbers from reading the random texture + float2 uv = float2( globalIdx.x / 1024.0, g_ElapsedTime ); + float3 randomValues0 = g_RandomBuffer.SampleLevel( g_samWrapPoint, uv, 0 ).xyz; + + float2 uv2 = float2( (globalIdx.x + 1) / 1024.0, g_ElapsedTime ); + float3 randomValues1 = g_RandomBuffer.SampleLevel( g_samWrapPoint, uv2, 0 ).xyz; + + float velocityMagnitude = length( g_vEmitterVelocity.xyz ); + + pb.m_Position = g_vEmitterPosition.xyz + ( randomValues0.xyz * g_PositionVariance.xyz ); + + pa.m_StreakLengthAndEmitterProperties = WriteEmitterProperties( g_EmitterIndex, g_TextureIndex, g_EmitterStreaks ? true : false ); + pa.m_CollisionCount = 0; + + pb.m_Mass = g_Mass; + pb.m_Velocity = g_vEmitterVelocity.xyz + ( randomValues1.xyz * velocityMagnitude * g_VelocityVariance ); + pb.m_Lifespan = g_ParticleLifeSpan; + pb.m_Age = pb.m_Lifespan; + pb.m_StartSize = g_StartSize; + pb.m_EndSize = g_EndSize; + + int index = g_DeadList[ numDeadParticles ]; + + // Write the new particle state into the global particle buffer + g_ParticleBufferA[ index ] = pa; + g_ParticleBufferB[ index ] = pb; + } + } +} diff --git a/src/GpuParticleShaders/ParticleHelpers.h b/src/GpuParticleShaders/ParticleHelpers.h new file mode 100644 index 0000000..c1ed0a0 --- /dev/null +++ b/src/GpuParticleShaders/ParticleHelpers.h @@ -0,0 +1,36 @@ +// +// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + + + +float GetTextureOffset( uint emitterProperties ) +{ + uint index = (emitterProperties & 0x001f00000) >> 24; + + return (float)index * 0.5; // Assumes 2 textures in the atlas! +} + +bool IsStreakEmitter( uint emitterProperties ) +{ + return ( emitterProperties >> 30 ) & 0x01 ? true : false; +} + diff --git a/src/GpuParticleShaders/ParticleRender.hlsl b/src/GpuParticleShaders/ParticleRender.hlsl new file mode 100644 index 0000000..12ad49f --- /dev/null +++ b/src/GpuParticleShaders/ParticleRender.hlsl @@ -0,0 +1,263 @@ +// +// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// +// Shader code for rendering particles as simple quads using rasterization +// + +#include "ParticleStructs.h" +#include "ParticleHelpers.h" +#include "fp16util.h" + + +struct PS_INPUT +{ + nointerpolation float4 ViewSpaceCentreAndRadius : TEXCOORD0; + float2 TexCoord : TEXCOORD1; + float3 ViewPos : TEXCOORD2; + nointerpolation float3 VelocityXYEmitterNdotL : TEXCOORD3; + nointerpolation float3 Extrusion : TEXCOORD4; + nointerpolation float2 EllipsoidRadius : TEXCOORD5; + nointerpolation float4 Color : COLOR0; + float4 Position : SV_POSITION; +}; + + +// The particle buffer data. Note this is only one half of the particle data - the data that is relevant to rendering as opposed to simulation +[[vk::binding( 0, 0 )]] StructuredBuffer g_ParticleBufferA : register( t0 ); + +// A buffer containing the pre-computed view space positions of the particles +[[vk::binding( 1, 0 )]] StructuredBuffer g_PackedViewSpacePositions : register( t1 ); + +// The number of sorted particles +[[vk::binding( 2, 0 )]] StructuredBuffer g_NumParticlesBuffer : register( t2 ); + +// The sorted index list of particles +[[vk::binding( 3, 0 )]] StructuredBuffer g_SortedIndexBuffer : register( t3 ); + +// The texture atlas for the particles +[[vk::binding( 4, 0 )]] Texture2D g_ParticleTexture : register( t4 ); + +// The opaque scene depth buffer read as a texture +[[vk::binding( 5, 0 )]] Texture2D g_DepthTexture : register( t5 ); + +[[vk::binding( 6, 0 )]] cbuffer RenderingConstantBuffer : register( b0 ) +{ + matrix g_mProjection; + matrix g_mProjectionInv; + + float4 g_SunColor; + float4 g_AmbientColor; + float4 g_SunDirectionVS; + + uint g_ScreenWidth; + uint g_ScreenHeight; + uint g_pads0; + uint g_pads1; +}; + +[[vk::binding( 7, 0 )]] SamplerState g_samClampLinear : register( s0 ); + + +// Vertex shader only path +PS_INPUT VS_StructuredBuffer( uint VertexId : SV_VertexID ) +{ + PS_INPUT Output = (PS_INPUT)0; + + // Particle index + uint particleIndex = VertexId / 4; + + // Per-particle corner index + uint cornerIndex = VertexId % 4; + + float xOffset = 0; + + const float2 offsets[ 4 ] = + { + float2( -1, 1 ), + float2( 1, 1 ), + float2( -1, -1 ), + float2( 1, -1 ), + }; + + int NumParticles = g_NumParticlesBuffer[ 0 ]; + + int index = g_SortedIndexBuffer[ NumParticles - particleIndex - 1 ]; + + GPUParticlePartA pa = g_ParticleBufferA[ index ]; + + float4 ViewSpaceCentreAndRadius = UnpackFloat16( g_PackedViewSpacePositions[ index ] ); + float4 VelocityXYEmitterNdotLRotation = UnpackFloat16( pa.m_PackedVelocityXYEmitterNDotLAndRotation ); + + uint emitterProperties = pa.m_StreakLengthAndEmitterProperties; + + bool streaks = IsStreakEmitter( emitterProperties ); + + float2 offset = offsets[ cornerIndex ]; + float2 uv = (offset+1)*float2( 0.25, 0.5 ); + uv.x += GetTextureOffset( emitterProperties ); + + float radius = ViewSpaceCentreAndRadius.w; + float3 cameraFacingPos; + +#if defined (STREAKS) + if ( streaks ) + { + float2 viewSpaceVelocity = VelocityXYEmitterNdotLRotation.xy; + + float2 ellipsoidRadius = float2( radius, UnpackFloat16( pa.m_StreakLengthAndEmitterProperties ).x ); + + float2 extrusionVector = viewSpaceVelocity; + float2 tangentVector = float2( extrusionVector.y, -extrusionVector.x ); + float2x2 transform = float2x2( tangentVector, extrusionVector ); + + Output.Extrusion.xy = extrusionVector; + Output.Extrusion.z = 1.0; + Output.EllipsoidRadius = ellipsoidRadius; + + cameraFacingPos = ViewSpaceCentreAndRadius.xyz; + + cameraFacingPos.xy += mul( offset * ellipsoidRadius, transform ); + } + else +#endif + { + float s, c; + sincos( VelocityXYEmitterNdotLRotation.w, s, c ); + float2x2 rotation = { float2( c, -s ), float2( s, c ) }; + + offset = mul( offset, rotation ); + + cameraFacingPos = ViewSpaceCentreAndRadius.xyz; + cameraFacingPos.xy += radius * offset; + } + + Output.Position = mul( g_mProjection, float4( cameraFacingPos, 1 ) ); + + Output.TexCoord = uv; + Output.Color = UnpackFloat16( pa.m_PackedTintAndAlpha ); + Output.ViewSpaceCentreAndRadius = ViewSpaceCentreAndRadius; + Output.VelocityXYEmitterNdotL = VelocityXYEmitterNdotLRotation.xyz; + Output.ViewPos = cameraFacingPos; + + return Output; +} + + +struct PS_OUTPUT +{ + float4 color : SV_TARGET0; +#if defined (REACTIVE) + float reactiveMask : SV_TARGET2; +#endif +}; + + +// Ratserization path's pixel shader +PS_OUTPUT PS_Billboard( PS_INPUT In ) +{ + PS_OUTPUT output = (PS_OUTPUT)0; + + // Retrieve the particle data + float3 particleViewSpacePos = In.ViewSpaceCentreAndRadius.xyz; + float particleRadius = In.ViewSpaceCentreAndRadius.w; + + // Get the depth at this point in screen space + float depth = g_DepthTexture.Load( uint3( In.Position.x, In.Position.y, 0 ) ).x; + + // Get viewspace position by generating a point in screen space at the depth of the depth buffer + float4 viewSpacePos; + viewSpacePos.x = In.Position.x / (float)g_ScreenWidth; + viewSpacePos.y = 1 - ( In.Position.y / (float)g_ScreenHeight ); + viewSpacePos.xy = (2*viewSpacePos.xy) - 1; + viewSpacePos.z = depth; + viewSpacePos.w = 1; + + // ...then transform it into view space using the inverse projection matrix and a divide by W + viewSpacePos = mul( g_mProjectionInv, viewSpacePos ); + viewSpacePos.xyz /= viewSpacePos.w; + + // Calculate the depth fade factor + float depthFade = saturate( ( particleViewSpacePos.z - viewSpacePos.z ) / particleRadius ); + + float4 albedo = 1; + albedo.a = depthFade; + + // Read the texture atlas + albedo *= g_ParticleTexture.SampleLevel( g_samClampLinear, In.TexCoord, 0 ); // 2d + + // Multiply in the particle color + output.color = albedo * In.Color; + + // Calculate the UV based the screen space position + float3 n = 0; + float2 uv; +#if defined (STREAKS) + if ( In.Extrusion.z > 0.0 ) + { + float2 ellipsoidRadius = In.EllipsoidRadius; + + float2 extrusionVector = In.Extrusion.xy; + float2 tangentVector = float2( extrusionVector.y, -extrusionVector.x ); + float2x2 transform = float2x2( tangentVector, extrusionVector ); + + float2 vecToCentre = In.ViewPos.xy - particleViewSpacePos.xy; + vecToCentre = mul( transform, vecToCentre ); + + uv = vecToCentre / ellipsoidRadius; + } + else +#endif + { + uv = (In.ViewPos.xy - particleViewSpacePos.xy ) / particleRadius; + } + + // Scale and bias + uv = (1+uv)*0.5; + + float pi = 3.1415926535897932384626433832795; + + n.x = -cos( pi * uv.x ); + n.y = -cos( pi * uv.y ); + n.z = sin( pi * length( uv ) ); + n = normalize( n ); + + float ndotl = saturate( dot( g_SunDirectionVS.xyz, n ) ); + + // Fetch the emitter's lighting term + float emitterNdotL = In.VelocityXYEmitterNdotL.z; + + // Mix the particle lighting term with the emitter lighting + ndotl = lerp( ndotl, emitterNdotL, 0.5 ); + + // Ambient lighting plus directional lighting + float3 lighting = g_AmbientColor.rgb + ndotl * g_SunColor.rgb; + + // Multiply lighting term in + output.color.rgb *= lighting; + +#if defined (REACTIVE) + output.reactiveMask = max( output.color.r, max( output.color.g, output.color.b ) ) * albedo.a; +#endif + + return output; +} diff --git a/src/GpuParticleShaders/ParticleSimulation.hlsl b/src/GpuParticleShaders/ParticleSimulation.hlsl new file mode 100644 index 0000000..49cb4eb --- /dev/null +++ b/src/GpuParticleShaders/ParticleSimulation.hlsl @@ -0,0 +1,313 @@ +// +// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +#include "ParticleStructs.h" +#include "fp16util.h" +#include "SimulationBindings.h" +#include "ParticleHelpers.h" + + + +uint GetEmitterIndex( uint emitterProperties ) +{ + return (emitterProperties >> 16) & 0xff; +} + + +bool IsSleeping( uint emitterProperties ) +{ + return ( emitterProperties >> 31 ) & 0x01 ? true : false; +} + + +uint SetIsSleepingBit( uint properties ) +{ + return properties | (1 << 31); +} + + +// Function to calculate the streak radius in X and Y given the particles radius and velocity +float2 calcEllipsoidRadius( float radius, float viewSpaceVelocitySpeed ) +{ + float radiusY = radius * max( 1.0, 0.1*viewSpaceVelocitySpeed ); + return float2( radius, radiusY ); +} + + +// Calculate the view space position given a point in screen space and a texel offset +float3 calcViewSpacePositionFromDepth( float2 normalizedScreenPosition, int2 texelOffset ) +{ + float2 uv; + + // Add the texel offset to the normalized screen position + normalizedScreenPosition.x += (float)texelOffset.x / (float)g_ScreenWidth; + normalizedScreenPosition.y += (float)texelOffset.y / (float)g_ScreenHeight; + + // Scale, bias and convert to texel range + uv.x = (0.5 + normalizedScreenPosition.x * 0.5) * (float)g_ScreenWidth; + uv.y = (1-(0.5 + normalizedScreenPosition.y * 0.5)) * (float)g_ScreenHeight; + + // Fetch the depth value at this point + float depth = g_DepthBuffer.Load( uint3( uv.x, uv.y, 0 ) ).x; + + // Generate a point in screen space with this depth + float4 viewSpacePosOfDepthBuffer; + viewSpacePosOfDepthBuffer.xy = normalizedScreenPosition.xy; + viewSpacePosOfDepthBuffer.z = depth; + viewSpacePosOfDepthBuffer.w = 1; + + // Transform into view space using the inverse projection matrix + viewSpacePosOfDepthBuffer = mul( g_mProjectionInv, viewSpacePosOfDepthBuffer ); + viewSpacePosOfDepthBuffer.xyz /= viewSpacePosOfDepthBuffer.w; + + return viewSpacePosOfDepthBuffer.xyz; +} + + +// Simulate 256 particles per thread group, one thread per particle +[numthreads(256,1,1)] +void CS_Simulate( uint3 id : SV_DispatchThreadID ) +{ + // Initialize the draw args and index buffer using the first thread in the Dispatch call + if ( id.x == 0 ) + { + g_DrawArgs[ 0 ].IndexCountPerInstance = 0; // Number of primitives reset to zero + g_DrawArgs[ 0 ].InstanceCount = 1; + g_DrawArgs[ 0 ].StartIndexLocation = 0; + g_DrawArgs[ 0 ].BaseVertexLocation = 0; + g_DrawArgs[ 0 ].StartInstanceLocation = 0; + + g_AliveParticleCount[ 0 ] = 0; + } + + // Wait after draw args are written so no other threads can write to them before they are initialized + GroupMemoryBarrierWithGroupSync(); + + const float3 vGravity = float3( 0.0, -9.81, 0.0 ); + + // Fetch the particle from the global buffer + GPUParticlePartA pa = g_ParticleBufferA[ id.x ]; + GPUParticlePartB pb = g_ParticleBufferB[ id.x ]; + + // If the partile is alive + if ( pb.m_Age > 0.0f ) + { + // Extract the individual emitter properties from the particle + uint emitterProperties = pa.m_StreakLengthAndEmitterProperties; + uint emitterIndex = GetEmitterIndex( emitterProperties ); + bool streaks = IsStreakEmitter( emitterProperties ); + float4 velocityXYEmitterNDotLAndRotation;// = UnpackFloat16( pa.m_PackedVelocityXYEmitterNDotLAndRotation ); + + // Age the particle by counting down from Lifespan to zero + pb.m_Age -= g_FrameTime; + + // Update the rotation + pa.m_Rotation += 0.24 * g_FrameTime; + + float3 vNewPosition = pb.m_Position; + + // Apply force due to gravity + if ( !IsSleeping( emitterProperties ) ) + { + pb.m_Velocity += pb.m_Mass * vGravity * g_FrameTime; + + // Apply a little bit of a wind force + float3 windDir = float3( 1, 1, 0 ); + float windStrength = 0.1; + + pb.m_Velocity += normalize( windDir ) * windStrength * g_FrameTime; + + // Calculate the new position of the particle + vNewPosition += pb.m_Velocity * g_FrameTime; + } + + // Calculate the normalized age + float fScaledLife = 1.0 - saturate( pb.m_Age / pb.m_Lifespan ); + + // Calculate the size of the particle based on age + float radius = lerp( pb.m_StartSize, pb.m_EndSize, fScaledLife ); + + // By default, we are not going to kill the particle + bool killParticle = false; + + if ( g_CollideParticles && g_FrameTime > 0.0 ) + { + // Transform new position into view space + float3 viewSpaceParticlePosition = mul( g_mView, float4( vNewPosition, 1 ) ).xyz; + + // Also obtain screen space position + float4 screenSpaceParticlePosition = mul( g_mViewProjection, float4( vNewPosition, 1 ) ); + screenSpaceParticlePosition.xyz /= screenSpaceParticlePosition.w; + + // Only do depth buffer collisions if the particle is onscreen, otherwise assume no collisions + if ( !IsSleeping( emitterProperties ) && screenSpaceParticlePosition.x > -1 && screenSpaceParticlePosition.x < 1 && screenSpaceParticlePosition.y > -1 && screenSpaceParticlePosition.y < 1 ) + { + // Get the view space position of the depth buffer + float3 viewSpacePosOfDepthBuffer = calcViewSpacePositionFromDepth( screenSpaceParticlePosition.xy, int2( 0, 0 ) ); + + // If the particle view space position is behind the depth buffer, but not by more than the collision thickness, then a collision has occurred + if ( ( viewSpaceParticlePosition.z < viewSpacePosOfDepthBuffer.z ) && ( viewSpaceParticlePosition.z > viewSpacePosOfDepthBuffer.z - g_CollisionThickness ) ) + { + // Generate the surface normal. Ideally, we would use the normals from the G-buffer as this would be more reliable than deriving them + float3 surfaceNormal; + + // Take three points on the depth buffer + float3 p0 = viewSpacePosOfDepthBuffer; + float3 p1 = calcViewSpacePositionFromDepth( screenSpaceParticlePosition.xy, int2( 1, 0 ) ); + float3 p2 = calcViewSpacePositionFromDepth( screenSpaceParticlePosition.xy, int2( 0, 1 ) ); + + // Generate the view space normal from the two vectors + float3 viewSpaceNormal = normalize( cross( p2 - p0, p1 - p0 ) ); + + // Transform into world space using the inverse view matrix + surfaceNormal = normalize( mul( g_mViewInv, -viewSpaceNormal ).xyz ); + + // The velocity is reflected in the collision plane + float3 newVelocity = reflect( pb.m_Velocity, surfaceNormal ); + + // Update the velocity and apply some restitution + pb.m_Velocity = 0.3*newVelocity; + + // Update the new collided position + vNewPosition = pb.m_Position + (pb.m_Velocity * g_FrameTime); + + pa.m_CollisionCount++; + } + } + } + + // Put particle to sleep if the velocity is small + if ( g_EnableSleepState && pa.m_CollisionCount > 10 && length( pb.m_Velocity ) < 0.01 ) + { + pa.m_StreakLengthAndEmitterProperties = SetIsSleepingBit( emitterProperties ); + } + + // If the position is below the floor, let's kill it now rather than wait for it to retire + if ( vNewPosition.y < -10 ) + { + killParticle = true; + } + + // Write the new position + pb.m_Position = vNewPosition; + + // Calculate the the distance to the eye for sorting in the rasterization path + float3 vec = vNewPosition - g_EyePosition.xyz; + pb.m_DistanceToEye = length( vec ); + + // Lerp the color based on the age + float4 color0 = g_StartColor[ emitterIndex ]; + float4 color1 = g_EndColor[ emitterIndex ]; + + float4 tintAndAlpha = 0; + + tintAndAlpha = lerp( color0, color1, saturate(4*fScaledLife) ); + tintAndAlpha.a = pb.m_Age <= 0 ? 0 : tintAndAlpha.a; + + if ( g_ShowSleepingParticles && IsSleeping( emitterProperties ) ) + { + tintAndAlpha.rgb = float3( 1, 0, 1 ); + } + + pa.m_PackedTintAndAlpha = PackFloat16( (min16float4)tintAndAlpha ); + + // The emitter-based lighting models the emitter as a vertical cylinder + float2 emitterNormal = normalize( vNewPosition.xz - g_EmitterLightingCenter[ emitterIndex ].xz ); + + // Generate the lighting term for the emitter + float emitterNdotL = saturate( dot( g_SunDirection.xz, emitterNormal ) + 0.5 ); + + // Transform the velocity into view space + float2 vsVelocity = mul( g_mView, float4( pb.m_Velocity.xyz, 0 ) ).xy; + float viewSpaceSpeed = 10 * length( vsVelocity ); + float streakLength = calcEllipsoidRadius( radius, viewSpaceSpeed ).y; + pa.m_StreakLengthAndEmitterProperties = PackFloat16( min16float2( streakLength, 0 ) ); + pa.m_StreakLengthAndEmitterProperties |= (0xffff0000 & emitterProperties); + + velocityXYEmitterNDotLAndRotation.xy = normalize( vsVelocity ); + velocityXYEmitterNDotLAndRotation.z = emitterNdotL; + velocityXYEmitterNDotLAndRotation.w = pa.m_Rotation; + + pa.m_PackedVelocityXYEmitterNDotLAndRotation = PackFloat16( (min16float4)velocityXYEmitterNDotLAndRotation ); + + // Pack the view spaced position and radius into a float4 buffer + float4 viewSpacePositionAndRadius; + + viewSpacePositionAndRadius.xyz = mul( g_mView, float4( vNewPosition, 1 ) ).xyz; + viewSpacePositionAndRadius.w = radius; + + g_PackedViewSpacePositions[ id.x ] = PackFloat16( (min16float4)viewSpacePositionAndRadius ); + + // For streaked particles (the sparks), calculate the the max radius in XY and store in a buffer + if ( streaks ) + { + float2 r2 = calcEllipsoidRadius( radius, viewSpaceSpeed ); + g_MaxRadiusBuffer[ id.x ] = max( r2.x, r2.y ); + } + else + { + // Not a streaked particle so will have rotation. When rotating, the particle has a max radius of the centre to the corner = sqrt( r^2 + r^2 ) + g_MaxRadiusBuffer[ id.x ] = 1.41 * radius; + } + + // Dead particles are added to the dead list for recycling + if ( pb.m_Age <= 0.0f || killParticle ) + { + pb.m_Age = -1; + + uint dstIdx = 0; + InterlockedAdd( g_DeadList[ 0 ], 1, dstIdx ); + g_DeadList[ dstIdx + 1 ] = id.x; + } + else + { + // Alive particles are added to the alive list + int index = 0; + InterlockedAdd( g_AliveParticleCount[ 0 ], 1, index ); + g_IndexBuffer[ index ] = id.x; + g_DistanceBuffer[ index ] = pb.m_DistanceToEye; + + uint dstIdx = 0; + // 6 indices per particle billboard + InterlockedAdd( g_DrawArgs[ 0 ].IndexCountPerInstance, 6, dstIdx ); + } + + // Write the particle data back to the global particle buffer + g_ParticleBufferA[ id.x ] = pa; + g_ParticleBufferB[ id.x ] = pb; + } +} + + +// Reset 256 particles per thread group, one thread per particle +// Also adds each particle to the dead list UAV +[numthreads(256,1,1)] +void CS_Reset( uint3 id : SV_DispatchThreadID, uint3 globalIdx : SV_DispatchThreadID ) +{ + if ( globalIdx.x == 0 ) + { + g_DeadList[ 0 ] = g_MaxParticles; + } + g_DeadList[ globalIdx.x + 1 ] = globalIdx.x; + + g_ParticleBufferA[ id.x ] = (GPUParticlePartA)0; + g_ParticleBufferB[ id.x ] = (GPUParticlePartB)0; +} diff --git a/src/GpuParticleShaders/ParticleStructs.h b/src/GpuParticleShaders/ParticleStructs.h new file mode 100644 index 0000000..0eb20b4 --- /dev/null +++ b/src/GpuParticleShaders/ParticleStructs.h @@ -0,0 +1,54 @@ +// +// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + + +// Particle structures +// =================== + +struct GPUParticlePartA +{ + uint2 m_PackedTintAndAlpha; // The color and opacity + uint2 m_PackedVelocityXYEmitterNDotLAndRotation; // Normalized view space velocity XY used for streak extrusion. The lighting term for the while emitter in Z. The rotation angle in W. + + uint m_StreakLengthAndEmitterProperties; // 0-15: fp16 streak length + // 16-23: The index of the emitter + // 24-29: Atlas index + // 30: Whether or not the emitter supports velocity-based streaks + // 31: Whether or not the particle is sleeping (ie, don't update position) + float m_Rotation; // Uncompressed rotation - some issues with using fp16 rotation (also, saves unpacking it) + uint m_CollisionCount; // Keep track of how many times the particle has collided + uint m_pad; +}; + +struct GPUParticlePartB +{ + float3 m_Position; // World space position + float m_Mass; // Mass of particle + + float3 m_Velocity; // World space velocity + float m_Lifespan; // Lifespan of the particle. + + float m_DistanceToEye; // The distance from the particle to the eye + float m_Age; // The current age counting down from lifespan to zero + float m_StartSize; // The size at spawn time + float m_EndSize; // The time at maximum age +}; diff --git a/src/GpuParticleShaders/RenderScene.hlsl b/src/GpuParticleShaders/RenderScene.hlsl new file mode 100644 index 0000000..901c663 --- /dev/null +++ b/src/GpuParticleShaders/RenderScene.hlsl @@ -0,0 +1,109 @@ +// +// Copyright (c) 2019 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#include "Globals.h" + + +struct VS_RenderSceneInput +{ + float3 f3Position : POSITION; + float3 f3Normal : NORMAL; + float2 f2TexCoord : TEXCOORD0; + float3 f3Tangent : TANGENT; +}; + +struct PS_RenderSceneInput +{ + float4 f4Position : SV_Position; + float2 f2TexCoord : TEXCOORD0; + float3 f3Normal : NORMAL; + float3 f3Tangent : TANGENT; + float3 f3WorldPos : TEXCOORD2; +}; + +Texture2D g_txDiffuse : register( t2 ); +Texture2D g_txNormal : register( t3 ); + +//================================================================================================================================= +// This shader computes standard transform and lighting +//================================================================================================================================= +PS_RenderSceneInput VS_RenderScene( VS_RenderSceneInput I ) +{ + PS_RenderSceneInput O; + + // Transform the position from object space to homogeneous projection space + O.f4Position = mul( float4( I.f3Position, 1.0f ), g_mViewProjection ); + + O.f3WorldPos = I.f3Position; + O.f3Normal = normalize( I.f3Normal ); + O.f3Tangent = normalize( I.f3Tangent ); + + // Pass through tex coords + O.f2TexCoord = I.f2TexCoord; + + return O; +} + + +//================================================================================================================================= +// This shader outputs the pixel's color by passing through the lit +// diffuse material color +//================================================================================================================================= +float4 PS_RenderScene( PS_RenderSceneInput I ) : SV_Target0 +{ + float4 f4Diffuse = g_txDiffuse.Sample( g_samWrapLinear, I.f2TexCoord ); + float fSpecMask = f4Diffuse.a; + float3 f3Norm = g_txNormal.Sample( g_samWrapLinear, I.f2TexCoord ).xyz; + f3Norm *= 2.0f; + f3Norm -= float3( 1.0f, 1.0f, 1.0f ); + + float3 f3Binorm = normalize( cross( I.f3Normal, I.f3Tangent ) ); + float3x3 f3x3BasisMatrix = float3x3( f3Binorm, I.f3Tangent, I.f3Normal ); + f3Norm = normalize( mul( f3Norm, f3x3BasisMatrix ) ); + + // Diffuse lighting + float4 f4Lighting = saturate( dot( f3Norm, g_SunDirection.xyz ) ) * g_SunColor; + f4Lighting += g_AmbientColor; + + // Calculate specular power + float3 f3ViewDir = normalize( g_EyePosition.xyz - I.f3WorldPos ); + float3 f3HalfAngle = normalize( f3ViewDir + g_SunDirection.xyz ); + float4 f4SpecPower1 = pow( saturate( dot( f3HalfAngle, f3Norm ) ), 32 ) * g_SunColor; + + return f4Lighting * f4Diffuse + ( f4SpecPower1 * fSpecMask ); +} + + + +//-------------------------------------------------------------------------------------- +// PS for the sky +//-------------------------------------------------------------------------------------- +float4 PS_Sky( PS_RenderSceneInput I ) : SV_Target +{ + float4 f4O; + + // Bog standard textured rendering + f4O.xyz = g_txDiffuse.Sample( g_samWrapLinear, I.f2TexCoord ).xyz; + f4O.w = 1.0f; + + return f4O; +} \ No newline at end of file diff --git a/src/GpuParticleShaders/ShaderConstants.h b/src/GpuParticleShaders/ShaderConstants.h new file mode 100644 index 0000000..fa847b0 --- /dev/null +++ b/src/GpuParticleShaders/ShaderConstants.h @@ -0,0 +1,26 @@ +// +// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// This file is shared between the HLSL and C++ code for convenience + +// Maximum number of emitters supported +#define NUM_EMITTERS 4 diff --git a/src/GpuParticleShaders/SimulationBindings.h b/src/GpuParticleShaders/SimulationBindings.h new file mode 100644 index 0000000..d898992 --- /dev/null +++ b/src/GpuParticleShaders/SimulationBindings.h @@ -0,0 +1,121 @@ +// +// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + + +#include "ShaderConstants.h" + + +// The particle buffers to fill with new particles +[[vk::binding( 0, 0 )]] RWStructuredBuffer g_ParticleBufferA : register( u0 ); +[[vk::binding( 1, 0 )]] RWStructuredBuffer g_ParticleBufferB : register( u1 ); + +// The dead list, so any particles that are retired this frame can be added to this list. The first element is the number of dead particles +[[vk::binding( 2, 0 )]] RWStructuredBuffer g_DeadList : register( u2 ); + +// The alive list which gets built in the similution. The distances also get written out +[[vk::binding( 3, 0 )]] RWStructuredBuffer g_IndexBuffer : register( u3 ); +[[vk::binding( 4, 0 )]] RWStructuredBuffer g_DistanceBuffer : register( u4 ); + +// The maximum radius in XY is calculated here and stored +[[vk::binding( 5, 0 )]] RWStructuredBuffer g_MaxRadiusBuffer : register( u5 ); + +// Viewspace particle positions are calculated here and stored +[[vk::binding( 6, 0 )]] RWStructuredBuffer g_PackedViewSpacePositions : register( u6 ); + +// The draw args for the ExecuteIndirect call needs to be filled in before the rasterization path is called, so do it here +struct IndirectCommand +{ +#ifdef API_DX12 + uint2 uav; +#endif + uint IndexCountPerInstance; + uint InstanceCount; + uint StartIndexLocation; + int BaseVertexLocation; + uint StartInstanceLocation; +}; +[[vk::binding( 7, 0 )]] RWStructuredBuffer g_DrawArgs : register( u7 ); + +[[vk::binding( 8, 0 )]] RWStructuredBuffer g_AliveParticleCount : register( u8 ); + +// The opaque scene's depth buffer read as a texture +[[vk::binding( 9, 0 )]] Texture2D g_DepthBuffer : register( t0 ); + +// A texture filled with random values for generating some variance in our particles when we spawn them +[[vk::binding( 10, 0 )]] Texture2D g_RandomBuffer : register( t1 ); + + +// Per-frame constant buffer +[[vk::binding( 11, 0 )]] cbuffer SimulationConstantBuffer : register( b0 ) +{ + float4 g_StartColor[ NUM_EMITTERS ]; + float4 g_EndColor[ NUM_EMITTERS ]; + + float4 g_EmitterLightingCenter[ NUM_EMITTERS ]; + + matrix g_mViewProjection; + matrix g_mView; + matrix g_mViewInv; + matrix g_mProjectionInv; + + float4 g_EyePosition; + float4 g_SunDirection; + + uint g_ScreenWidth; + uint g_ScreenHeight; + float g_ElapsedTime; + float g_CollisionThickness; + + int g_CollideParticles; + int g_ShowSleepingParticles; + int g_EnableSleepState; + float g_FrameTime; + + int g_MaxParticles; + uint g_Pad0; + uint g_Pad1; + uint g_Pad2; +}; + +[[vk::binding( 12, 0 )]] cbuffer EmitterConstantBuffer : register( b1 ) +{ + float4 g_vEmitterPosition; + float4 g_vEmitterVelocity; + float4 g_PositionVariance; + + int g_MaxParticlesThisFrame; + float g_ParticleLifeSpan; + float g_StartSize; + float g_EndSize; + + float g_VelocityVariance; + float g_Mass; + uint g_EmitterIndex; + uint g_EmitterStreaks; + + uint g_TextureIndex; + uint g_pads0; + uint g_pads1; + uint g_pads2; +}; + +[[vk::binding( 13, 0 )]] SamplerState g_samWrapPoint : register( s0 ); diff --git a/src/GpuParticleShaders/fp16util.h b/src/GpuParticleShaders/fp16util.h new file mode 100644 index 0000000..2b4df0f --- /dev/null +++ b/src/GpuParticleShaders/fp16util.h @@ -0,0 +1,169 @@ +// HLSL intrinsics +// cross +min16float3 RTGCross(min16float3 a, min16float3 b) +{ + return min16float3( + a.y * b.z - a.z * b.y, + a.z * b.x - a.x * b.z, + a.x * b.y - a.y * b.x); +} + +// dot +min16float RTGDot2(min16float2 a, min16float2 b) +{ + return a.x * b.x + a.y * b.y; +} + +min16float RTGDot3(min16float3 a, min16float3 b) +{ + return a.x * b.x + a.y * b.y + + a.z * b.z; +} + +min16float RTGDot4(min16float4 a, min16float4 b) +{ + return a.x * b.x + a.y * b.y + + a.z * b.z + a.w * b.w; +} + +// length +min16float RTGLength2(min16float2 a) +{ + return sqrt(RTGDot2(a, a)); +} + +min16float RTGLength3(min16float3 a) +{ + return sqrt(RTGDot3(a, a)); +} + +min16float RTGLength4(min16float4 a) +{ + return sqrt(RTGDot4(a, a)); +} + +// normalize +min16float2 RTGNormalize2(min16float2 a) +{ + min16float l = RTGLength2(a); + return l == 0.0 ? a : a / l; +} + +min16float3 RTGNormalize3(min16float3 a) +{ + min16float l = RTGLength3( a ); + return l == 0.0 ? a : a / l; +} + +min16float4 RTGNormalize4(min16float4 a) +{ + min16float l = RTGLength4( a ); + return l == 0.0 ? a : a / l; +} + + +// distance +min16float RTGDistance2(min16float2 from, min16float2 to) +{ + return RTGLength2(to - from); +} + +min16float RTGDistance3(min16float3 from, min16float3 to) +{ + return RTGLength3(to - from); +} + +min16float RTGDistance4(min16float4 from, min16float4 to) +{ + return RTGLength4(to - from); +} + + +// Packing and Unpacking +// min16{u}int2 +int PackInt16(min16int2 v) +{ + uint x = asuint(int(v.x)); + uint y = asuint(int(v.y)); + return asint(x | y << 16); +} + +uint PackInt16(min16uint2 v) +{ + return uint(v.x | (uint)(v.y) << 16); +} + +min16int2 UnpackInt16(int v) +{ + uint x = asuint(v.x) & 0xFFFF; + uint y = asuint(v.x) >> 16; + return min16uint2(asint(x), + asint(y)); +} + +min16uint2 UnpackInt16(uint v) +{ + return min16uint2(v.x & 0xFFFF, + v.x >> 16); +} + +// min16{u}int4 +int2 PackInt16(min16int4 v) +{ + return int2(PackInt16(v.xy), + PackInt16(v.zw)); +} + +uint2 PackInt16(min16uint4 v) +{ + return uint2(PackInt16(v.xy), + PackInt16(v.zw)); +} + +min16int4 UnpackInt16(int2 v) +{ + return min16int4(UnpackInt16(v.x), + UnpackInt16(v.y)); +} + +min16uint4 UnpackInt16(uint2 v) +{ + return min16uint4(UnpackInt16(v.x), + UnpackInt16(v.y)); +} + +uint PackFloat16( min16float v ) +{ + uint p = f32tof16( v ); + return p.x; +} + +// min16float2 +uint PackFloat16(min16float2 v) +{ + uint2 p = f32tof16(float2(v)); + return p.x | (p.y << 16); +} + +min16float2 UnpackFloat16(uint a) +{ + float2 tmp = f16tof32( + uint2(a & 0xFFFF, a >> 16)); + return min16float2(tmp); +} + + +// min16float4 +uint2 PackFloat16(min16float4 v) +{ + return uint2(PackFloat16(v.xy), + PackFloat16(v.zw)); +} + +min16float4 UnpackFloat16(uint2 v) +{ + return min16float4( + UnpackFloat16(v.x), + UnpackFloat16(v.y) + ); +} \ No newline at end of file diff --git a/src/GpuParticles/ParticleHelpers.h b/src/GpuParticles/ParticleHelpers.h new file mode 100644 index 0000000..3133630 --- /dev/null +++ b/src/GpuParticles/ParticleHelpers.h @@ -0,0 +1,36 @@ +// +// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +#pragma once + +inline float RandomVariance( float median, float variance ) +{ + float fUnitRandomValue = (float)rand() / (float)RAND_MAX; + float fRange = variance * fUnitRandomValue; + return median - variance + (2.0f * fRange); +} + +inline float RandomFromAndTo( float lowest, float highest ) +{ + float fUnitRandomValue = (float)rand() / (float)RAND_MAX; + float fRange = (highest - lowest) * fUnitRandomValue; + return lowest + fRange; +} \ No newline at end of file diff --git a/src/GpuParticles/ParticleSystem.h b/src/GpuParticles/ParticleSystem.h new file mode 100644 index 0000000..2658135 --- /dev/null +++ b/src/GpuParticles/ParticleSystem.h @@ -0,0 +1,93 @@ +// +// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +#pragma once + +#include "stdafx.h" + +// Implementation-agnostic particle system interface +struct IParticleSystem +{ + enum Flags + { + PF_Sort = 1 << 0, // Sort the particles + PF_DepthCull = 1 << 1, // Do per-tile depth buffer culling + PF_Streaks = 1 << 2, // Streak the particles based on velocity + PF_Reactive = 1 << 3 // Particles also write to the reactive mask + }; + + // Per-emitter parameters + struct EmitterParams + { + math::Vector4 m_Position = {}; // World position of the emitter + math::Vector4 m_Velocity = {}; // Velocity of each particle from the emitter + math::Vector4 m_PositionVariance = {}; // Variance in position of each particle + int m_NumToEmit = 0; // Number of particles to emit this frame + float m_ParticleLifeSpan = 0.0f; // How long the particles should live + float m_StartSize = 0.0f; // Size of particles at spawn time + float m_EndSize = 0.0f; // Size of particle when they reach retirement age + float m_Mass = 0.0f; // Mass of particle + float m_VelocityVariance = 0.0f; // Variance in velocity of each particle + int m_TextureIndex = 0; // Index of the texture in the atlas + bool m_Streaks = false; // Streak the particles in the direction of travel + }; + + struct ConstantData + { + math::Matrix4 m_ViewProjection = {}; + math::Matrix4 m_View = {}; + math::Matrix4 m_ViewInv = {}; + math::Matrix4 m_Projection = {}; + math::Matrix4 m_ProjectionInv = {}; + + math::Vector4 m_StartColor[ 10 ] = {}; + math::Vector4 m_EndColor[ 10 ] = {}; + math::Vector4 m_EmitterLightingCenter[ 10 ] = {}; + + math::Vector4 m_SunDirection = {}; + math::Vector4 m_SunColor = {}; + math::Vector4 m_AmbientColor = {}; + + float m_FrameTime = 0.0f; + }; + + // Create a GPU particle system. Add more factory functions to create other types of system eg CPU-updated system + static IParticleSystem* CreateGPUSystem( const char* particleAtlas ); + + virtual ~IParticleSystem() {} + +#ifdef API_DX12 + virtual void Render( ID3D12GraphicsCommandList* pCommandList, DynamicBufferRing& constantBufferRing, int flags, const EmitterParams* pEmitters, int nNumEmitters, const ConstantData& constantData ) = 0; + virtual void OnCreateDevice( Device &device, UploadHeap& uploadHeap, ResourceViewHeaps& heaps, StaticBufferPool& bufferPool, DynamicBufferRing& constantBufferRing ) = 0; + virtual void OnResizedSwapChain( int width, int height, Texture& depthBuffer ) = 0; +#endif +#ifdef API_VULKAN + virtual void Render( VkCommandBuffer commandBuffer, DynamicBufferRing& constantBufferRing, int contextFlags, const EmitterParams* pEmitters, int nNumEmitters, const ConstantData& constantData ) = 0; + virtual void OnCreateDevice( Device &device, UploadHeap& uploadHeap, ResourceViewHeaps& heaps, StaticBufferPool& bufferPool, DynamicBufferRing& constantBufferRing, VkRenderPass renderPass ) = 0; + virtual void OnResizedSwapChain( int width, int height, Texture& depthBuffer, VkFramebuffer frameBuffer ) = 0; +#endif + + virtual void OnReleasingSwapChain() = 0; + virtual void OnDestroyDevice() = 0; + + // Completely resets the state of all particles. Handy for changing scenes etc + virtual void Reset() = 0; +}; diff --git a/src/GpuParticles/ParticleSystemInternal.h b/src/GpuParticles/ParticleSystemInternal.h new file mode 100644 index 0000000..c4c94eb --- /dev/null +++ b/src/GpuParticles/ParticleSystemInternal.h @@ -0,0 +1,154 @@ +// +// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +#pragma once + +#include "stdafx.h" +#include "../GpuParticleShaders/ShaderConstants.h" +#include "ParticleSystem.h" + + +// Helper function to align values +int align( int value, int alignment ) { return ( value + (alignment - 1) ) & ~(alignment - 1); } + + +// GPUParticle structure is split into two sections for better cache efficiency - could even be SOA but would require creating more vertex buffers. +struct GPUParticlePartA +{ + math::Vector4 m_params[ 2 ]; +}; + +struct GPUParticlePartB +{ + math::Vector4 m_params[ 3 ]; +}; + + +struct SimulationConstantBuffer +{ + math::Vector4 m_StartColor[ NUM_EMITTERS ] = {}; + math::Vector4 m_EndColor[ NUM_EMITTERS ] = {}; + + math::Vector4 m_EmitterLightingCenter[ NUM_EMITTERS ] = {}; + + math::Matrix4 m_ViewProjection = {}; + math::Matrix4 m_View = {}; + math::Matrix4 m_ViewInv = {}; + math::Matrix4 m_ProjectionInv = {}; + + math::Vector4 m_EyePosition = {}; + math::Vector4 m_SunDirection = {}; + + UINT m_ScreenWidth = 0; + UINT m_ScreenHeight = 0; + float m_ElapsedTime = 0.0f; + float m_CollisionThickness = 4.0f; + + int m_CollideParticles = 0; + int m_ShowSleepingParticles = 0; + int m_EnableSleepState = 0; + float m_FrameTime = 0.0f; + + int m_MaxParticles = 0; + UINT m_pad01 = 0; + UINT m_pad02 = 0; + UINT m_pad03 = 0; +}; + +struct EmitterConstantBuffer +{ + math::Vector4 m_EmitterPosition = {}; + math::Vector4 m_EmitterVelocity = {}; + math::Vector4 m_PositionVariance = {}; + + int m_MaxParticlesThisFrame = 0; + float m_ParticleLifeSpan = 0.0f; + float m_StartSize = 0.0f; + float m_EndSize = 0.0f; + + float m_VelocityVariance = 0.0f; + float m_Mass = 0.0f; + int m_Index = 0; + int m_Streaks = 0; + + int m_TextureIndex = 0; + int m_pads[ 3 ] = {}; +}; + + +// The rasterization path constant buffer +struct RenderingConstantBuffer +{ + math::Matrix4 m_Projection = {}; + math::Matrix4 m_ProjectionInv = {}; + math::Vector4 m_SunColor = {}; + math::Vector4 m_AmbientColor = {}; + math::Vector4 m_SunDirectionVS = {}; + UINT m_ScreenWidth = 0; + UINT m_ScreenHeight = 0; + UINT m_pads[ 2 ] = {}; +}; + +struct CullingConstantBuffer +{ + math::Matrix4 m_ProjectionInv = {}; + math::Matrix4 m_Projection = {}; + + UINT m_ScreenWidth = 0; + UINT m_ScreenHeight = 0; + UINT m_NumTilesX = 0; + UINT m_NumCoarseCullingTilesX = 0; + + UINT m_NumCullingTilesPerCoarseTileX = 0; + UINT m_NumCullingTilesPerCoarseTileY = 0; + UINT m_pad01 = 0; + UINT m_pad02 = 0; +}; + +struct TiledRenderingConstantBuffer +{ + math::Matrix4 m_ProjectionInv = {}; + math::Vector4 m_SunColor = {}; + math::Vector4 m_AmbientColor = {}; + math::Vector4 m_SunDirectionVS = {}; + + UINT m_ScreenHeight = 0; + float m_InvScreenWidth = 0.0f; + float m_InvScreenHeight = 0.0f; + float m_AlphaThreshold = 0.97f; + + UINT m_NumTilesX = 0; + UINT m_NumCoarseCullingTilesX = 0; + UINT m_NumCullingTilesPerCoarseTileX = 0; + UINT m_NumCullingTilesPerCoarseTileY = 0; + + UINT m_AlignedScreenWidth = 0; + UINT m_pads[ 3 ] = {}; +}; + +struct QuadConstantBuffer +{ + UINT m_AlignedScreenWidth; + UINT m_pads[ 3 ]; +}; + +// The maximum number of supported GPU particles +static const int g_maxParticles = 400*1024; diff --git a/src/GpuParticles/dx12/GPUParticleSystem.cpp b/src/GpuParticles/dx12/GPUParticleSystem.cpp new file mode 100644 index 0000000..da9f0cc --- /dev/null +++ b/src/GpuParticles/dx12/GPUParticleSystem.cpp @@ -0,0 +1,745 @@ +// +// Copyright (c) 2019 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +#include "../DX12/stdafx.h" +#include "../ParticleSystem.h" +#include "../ParticleSystemInternal.h" +#include "../ParticleHelpers.h" +#include "ParallelSort.h" + + +const D3D12_RESOURCE_STATES SHADER_READ_STATE = D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER|D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE|D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + + +#pragma warning( disable : 4100 ) // disable unreference formal parameter warnings for /W4 builds + +struct IndirectCommand +{ + D3D12_GPU_VIRTUAL_ADDRESS uav = {}; + D3D12_DRAW_INDEXED_ARGUMENTS drawArguments = {}; +}; + +// GPU Particle System class. Responsible for updating and rendering the particles +class GPUParticleSystem : public IParticleSystem +{ +public: + + GPUParticleSystem( const char* particleAtlas ); + +private: + + enum DepthCullingMode + { + DepthCullingOn, + DepthCullingOff, + NumDepthCullingModes + }; + + enum StreakMode + { + StreaksOn, + StreaksOff, + NumStreakModes + }; + + enum ReactiveMode + { + ReactiveOn, + ReactiveOff, + NumReactiveModes + }; + + virtual ~GPUParticleSystem(); + + virtual void OnCreateDevice( Device &device, UploadHeap& uploadHeap, ResourceViewHeaps& heaps, StaticBufferPool& bufferPool, DynamicBufferRing& constantBufferRing ); + virtual void OnResizedSwapChain( int width, int height, Texture& depthBuffer ); + virtual void OnReleasingSwapChain(); + virtual void OnDestroyDevice(); + + virtual void Reset(); + + virtual void Render( ID3D12GraphicsCommandList* pCommandList, DynamicBufferRing& constantBufferRing, int flags, const EmitterParams* pEmitters, int nNumEmitters, const ConstantData& constantData ); + + void Emit( ID3D12GraphicsCommandList* pCommandList, DynamicBufferRing& constantBufferRing, int numEmitters, const EmitterParams* emitters ); + void Simulate( ID3D12GraphicsCommandList* pCommandList ); + void Sort( ID3D12GraphicsCommandList* pCommandList ); + + void FillRandomTexture( UploadHeap& uploadHeap ); + + void CreateSimulationAssets(); + void CreateRasterizedRenderingAssets(); + + Device* m_pDevice = nullptr; + ResourceViewHeaps* m_heaps = nullptr; + const char* m_AtlasPath = nullptr; + + Texture m_Atlas = {}; + Texture m_ParticleBufferA = {}; + Texture m_ParticleBufferB = {}; + Texture m_PackedViewSpaceParticlePositions = {}; + Texture m_MaxRadiusBuffer = {}; + Texture m_DeadListBuffer = {}; + Texture m_AliveIndexBuffer = {}; + Texture m_AliveDistanceBuffer = {}; + Texture m_AliveCountBuffer = {}; + Texture m_RenderingBuffer = {}; + Texture m_IndirectArgsBuffer = {}; + Texture m_RandomTexture = {}; + + const int m_SimulationUAVDescriptorTableCount = 9; + CBV_SRV_UAV m_SimulationUAVDescriptorTable = {}; + + const int m_SimulationSRVDescriptorTableCount = 2; + CBV_SRV_UAV m_SimulationSRVDescriptorTable = {}; + + const int m_RasterizationSRVDescriptorTableCount = 6; + CBV_SRV_UAV m_RasterizationSRVDescriptorTable = {}; + + UINT m_ScreenWidth = 0; + UINT m_ScreenHeight = 0; + float m_InvScreenWidth = 0.0f; + float m_InvScreenHeight = 0.0f; + float m_ElapsedTime = 0.0f; + float m_AlphaThreshold = 0.97f; + + D3D12_INDEX_BUFFER_VIEW m_IndexBuffer = {}; + ID3D12RootSignature* m_pSimulationRootSignature = nullptr; + ID3D12RootSignature* m_pRasterizationRootSignature = nullptr; + + ID3D12PipelineState* m_pSimulatePipeline = nullptr; + ID3D12PipelineState* m_pEmitPipeline = nullptr; + ID3D12PipelineState* m_pResetParticlesPipeline = nullptr; + ID3D12PipelineState* m_pRasterizationPipelines[ NumStreakModes ][ NumReactiveModes ] = {}; + + ID3D12CommandSignature* m_commandSignature = nullptr; + + bool m_ResetSystem = true; + FFXParallelSort m_SortLib = {}; + + D3D12_RESOURCE_STATES m_ReadBufferStates; + D3D12_RESOURCE_STATES m_WriteBufferStates; + D3D12_RESOURCE_STATES m_StridedBufferStates; +}; + +IParticleSystem* IParticleSystem::CreateGPUSystem( const char* particleAtlas ) +{ + return new GPUParticleSystem( particleAtlas ); +} + + +GPUParticleSystem::GPUParticleSystem( const char* particleAtlas ) : m_AtlasPath( particleAtlas ) +{ +} + + +GPUParticleSystem::~GPUParticleSystem() +{ +} + + +void GPUParticleSystem::Sort( ID3D12GraphicsCommandList* pCommandList ) +{ + // Causes the debug layer to lock up + m_SortLib.Draw( pCommandList ); +} + + +void GPUParticleSystem::Reset() +{ + m_ResetSystem = true; +} + +void GPUParticleSystem::Render( ID3D12GraphicsCommandList* pCommandList, DynamicBufferRing& constantBufferRing, int flags, const EmitterParams* pEmitters, int nNumEmitters, const ConstantData& constantData ) +{ + std::vector barriersBeforeSimulation; + if(m_WriteBufferStates == D3D12_RESOURCE_STATE_COMMON) + { + barriersBeforeSimulation.push_back(CD3DX12_RESOURCE_BARRIER::Transition(m_ParticleBufferB.GetResource(), m_WriteBufferStates, D3D12_RESOURCE_STATE_UNORDERED_ACCESS)); + barriersBeforeSimulation.push_back(CD3DX12_RESOURCE_BARRIER::Transition(m_DeadListBuffer.GetResource(), m_WriteBufferStates, D3D12_RESOURCE_STATE_UNORDERED_ACCESS)); + barriersBeforeSimulation.push_back(CD3DX12_RESOURCE_BARRIER::Transition(m_AliveDistanceBuffer.GetResource(), m_WriteBufferStates, D3D12_RESOURCE_STATE_UNORDERED_ACCESS)); + barriersBeforeSimulation.push_back(CD3DX12_RESOURCE_BARRIER::Transition(m_IndirectArgsBuffer.GetResource(), m_WriteBufferStates, D3D12_RESOURCE_STATE_UNORDERED_ACCESS)); + m_WriteBufferStates = D3D12_RESOURCE_STATE_UNORDERED_ACCESS; + } + + ID3D12DescriptorHeap* descriptorHeaps[] = { m_heaps->GetCBV_SRV_UAVHeap(), m_heaps->GetSamplerHeap() }; + pCommandList->SetDescriptorHeaps( _countof( descriptorHeaps ), descriptorHeaps ); + + SimulationConstantBuffer simulationConstants = {}; + + memcpy( simulationConstants.m_StartColor, constantData.m_StartColor, sizeof( simulationConstants.m_StartColor ) ); + memcpy( simulationConstants.m_EndColor, constantData.m_EndColor, sizeof( simulationConstants.m_EndColor ) ); + memcpy( simulationConstants.m_EmitterLightingCenter, constantData.m_EmitterLightingCenter, sizeof( simulationConstants.m_EmitterLightingCenter ) ); + + simulationConstants.m_ViewProjection = constantData.m_ViewProjection; + simulationConstants.m_View = constantData.m_View; + simulationConstants.m_ViewInv = constantData.m_ViewInv; + simulationConstants.m_ProjectionInv = constantData.m_ProjectionInv; + + simulationConstants.m_EyePosition = constantData.m_ViewInv.getCol3(); + simulationConstants.m_SunDirection = constantData.m_SunDirection; + + simulationConstants.m_ScreenWidth = m_ScreenWidth; + simulationConstants.m_ScreenHeight = m_ScreenHeight; + simulationConstants.m_MaxParticles = g_maxParticles; + simulationConstants.m_FrameTime = constantData.m_FrameTime; + + math::Vector4 sunDirectionVS = constantData.m_View * constantData.m_SunDirection; + + m_ElapsedTime += constantData.m_FrameTime; + if ( m_ElapsedTime > 10.0f ) + m_ElapsedTime -= 10.0f; + + simulationConstants.m_ElapsedTime = m_ElapsedTime; + + { + UserMarker marker( pCommandList, "simulation" ); + + void* data = nullptr; + D3D12_GPU_VIRTUAL_ADDRESS constantBuffer; + constantBufferRing.AllocConstantBuffer( sizeof( simulationConstants ), &data, &constantBuffer ); + memcpy( data, &simulationConstants, sizeof( simulationConstants ) ); + + + pCommandList->SetComputeRootSignature( m_pSimulationRootSignature ); + pCommandList->SetComputeRootDescriptorTable( 0, m_SimulationUAVDescriptorTable.GetGPU() ); + pCommandList->SetComputeRootDescriptorTable( 1, m_SimulationSRVDescriptorTable.GetGPU() ); + pCommandList->SetComputeRootConstantBufferView( 2, constantBuffer ); + + barriersBeforeSimulation.push_back( CD3DX12_RESOURCE_BARRIER::Transition( m_ParticleBufferA.GetResource(), m_ReadBufferStates, D3D12_RESOURCE_STATE_UNORDERED_ACCESS ) ); + barriersBeforeSimulation.push_back( CD3DX12_RESOURCE_BARRIER::Transition( m_PackedViewSpaceParticlePositions.GetResource(), m_ReadBufferStates, D3D12_RESOURCE_STATE_UNORDERED_ACCESS ) ); + barriersBeforeSimulation.push_back( CD3DX12_RESOURCE_BARRIER::Transition( m_MaxRadiusBuffer.GetResource(), m_ReadBufferStates, D3D12_RESOURCE_STATE_UNORDERED_ACCESS ) ); + barriersBeforeSimulation.push_back( CD3DX12_RESOURCE_BARRIER::Transition( m_AliveIndexBuffer.GetResource(), m_ReadBufferStates, D3D12_RESOURCE_STATE_UNORDERED_ACCESS ) ); + barriersBeforeSimulation.push_back( CD3DX12_RESOURCE_BARRIER::Transition( m_AliveCountBuffer.GetResource(), m_ReadBufferStates, D3D12_RESOURCE_STATE_UNORDERED_ACCESS ) ); + pCommandList->ResourceBarrier( (UINT)barriersBeforeSimulation.size(), &barriersBeforeSimulation[ 0 ] ); + m_ReadBufferStates = D3D12_RESOURCE_STATE_UNORDERED_ACCESS; + + // If we are resetting the particle system, then initialize the dead list + if ( m_ResetSystem ) + { + pCommandList->SetPipelineState( m_pResetParticlesPipeline ); + + pCommandList->Dispatch( align( g_maxParticles, 256 ) / 256, 1, 1 ); + + std::vector barriersPostReset; + barriersPostReset.push_back( CD3DX12_RESOURCE_BARRIER::UAV( m_ParticleBufferA.GetResource() ) ); + barriersPostReset.push_back( CD3DX12_RESOURCE_BARRIER::UAV( m_ParticleBufferB.GetResource() ) ); + barriersPostReset.push_back( CD3DX12_RESOURCE_BARRIER::UAV( m_DeadListBuffer.GetResource() ) ); + pCommandList->ResourceBarrier( (UINT)barriersPostReset.size(), &barriersPostReset[ 0 ] ); + + m_ResetSystem = false; + } + + // Emit particles into the system + Emit( pCommandList, constantBufferRing, nNumEmitters, pEmitters ); + + // Run the simulation for this frame + Simulate( pCommandList ); + + + + std::vector barriersAfterSimulation; + barriersAfterSimulation.push_back( CD3DX12_RESOURCE_BARRIER::Transition( m_ParticleBufferA.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, SHADER_READ_STATE) ); + barriersAfterSimulation.push_back( CD3DX12_RESOURCE_BARRIER::Transition( m_PackedViewSpaceParticlePositions.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, SHADER_READ_STATE) ); + barriersAfterSimulation.push_back( CD3DX12_RESOURCE_BARRIER::Transition( m_MaxRadiusBuffer.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, SHADER_READ_STATE) ); + barriersAfterSimulation.push_back( CD3DX12_RESOURCE_BARRIER::Transition( m_AliveCountBuffer.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, SHADER_READ_STATE) ); + barriersAfterSimulation.push_back( CD3DX12_RESOURCE_BARRIER::UAV( m_DeadListBuffer.GetResource() ) ); + pCommandList->ResourceBarrier( (UINT)barriersAfterSimulation.size(), &barriersAfterSimulation[ 0 ] ); + } + + // Conventional rasterization path + { + UserMarker marker( pCommandList, "rasterization" ); + + // Sort if requested. Not doing so results in the particles rendering out of order and not blending correctly + if ( flags & PF_Sort ) + { + UserMarker marker( pCommandList, "sorting" ); + + const D3D12_RESOURCE_BARRIER barriers[] = + { + CD3DX12_RESOURCE_BARRIER::UAV( m_AliveIndexBuffer.GetResource() ), + CD3DX12_RESOURCE_BARRIER::UAV( m_AliveDistanceBuffer.GetResource() ), + }; + pCommandList->ResourceBarrier( _countof( barriers ), barriers ); + + Sort( pCommandList ); + } + + StreakMode streaks = flags & PF_Streaks ? StreaksOn : StreaksOff; + ReactiveMode reactive = flags & PF_Reactive ? ReactiveOn : ReactiveOff; + + RenderingConstantBuffer* cb = nullptr; + D3D12_GPU_VIRTUAL_ADDRESS renderingConstantBuffer; + constantBufferRing.AllocConstantBuffer( sizeof( RenderingConstantBuffer ), (void**)&cb, &renderingConstantBuffer ); + cb->m_Projection = constantData.m_Projection; + cb->m_ProjectionInv = simulationConstants.m_ProjectionInv; + cb->m_SunColor = constantData.m_SunColor; + cb->m_AmbientColor = constantData.m_AmbientColor; + cb->m_SunDirectionVS = sunDirectionVS; + cb->m_ScreenWidth = m_ScreenWidth; + cb->m_ScreenHeight = m_ScreenHeight; + + pCommandList->SetGraphicsRootSignature( m_pRasterizationRootSignature ); + pCommandList->SetGraphicsRootDescriptorTable( 0, m_RasterizationSRVDescriptorTable.GetGPU() ); + pCommandList->SetGraphicsRootConstantBufferView( 1, renderingConstantBuffer ); + pCommandList->SetGraphicsRootUnorderedAccessView( 2, m_IndirectArgsBuffer.GetResource()->GetGPUVirtualAddress() ); + pCommandList->SetPipelineState( m_pRasterizationPipelines[ streaks ][ reactive ] ); + + pCommandList->IASetIndexBuffer( &m_IndexBuffer ); + pCommandList->IASetVertexBuffers( 0, 0, nullptr ); + pCommandList->IASetPrimitiveTopology( D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST ); + + std::vector barriers; + barriers.push_back( CD3DX12_RESOURCE_BARRIER::Transition( m_AliveIndexBuffer.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, SHADER_READ_STATE) ); + barriers.push_back( CD3DX12_RESOURCE_BARRIER::Transition( m_IndirectArgsBuffer.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT ) ); + pCommandList->ResourceBarrier( (UINT)barriers.size(), &barriers[ 0 ] ); + + pCommandList->ExecuteIndirect( m_commandSignature, 1, m_IndirectArgsBuffer.GetResource(), 0, nullptr, 0 ); + + pCommandList->ResourceBarrier( 1, &CD3DX12_RESOURCE_BARRIER::Transition( m_IndirectArgsBuffer.GetResource(), D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT, D3D12_RESOURCE_STATE_UNORDERED_ACCESS ) ); + } + + m_ReadBufferStates = SHADER_READ_STATE; +} + + +void GPUParticleSystem::OnCreateDevice(Device &device, UploadHeap& uploadHeap, ResourceViewHeaps& heaps, StaticBufferPool& bufferPool, DynamicBufferRing& constantBufferRing ) +{ + m_pDevice = &device; + m_heaps = &heaps; + + m_ReadBufferStates = D3D12_RESOURCE_STATE_COMMON; + m_WriteBufferStates = D3D12_RESOURCE_STATE_COMMON; // D3D12_RESOURCE_STATE_UNORDERED_ACCESS + m_StridedBufferStates = D3D12_RESOURCE_STATE_COMMON; + + // Create the global particle pool. Each particle is split into two parts for better cache coherency. The first half contains the data more + // relevant to rendering while the second half is more related to simulation + CD3DX12_RESOURCE_DESC RDescParticlesA = CD3DX12_RESOURCE_DESC::Buffer( sizeof( GPUParticlePartA ) * g_maxParticles, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS ); + m_ParticleBufferA.InitBuffer(&device, "ParticleBufferA", &RDescParticlesA, sizeof( GPUParticlePartA ), m_ReadBufferStates); + + CD3DX12_RESOURCE_DESC RDescParticlesB = CD3DX12_RESOURCE_DESC::Buffer( sizeof( GPUParticlePartB ) * g_maxParticles, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS ); + m_ParticleBufferB.InitBuffer(&device, "ParticleBufferB", &RDescParticlesB, sizeof( GPUParticlePartB ), m_WriteBufferStates); + + // The packed view space positions of particles are cached during simulation so allocate a buffer for them + CD3DX12_RESOURCE_DESC RDescPackedViewSpaceParticlePositions = CD3DX12_RESOURCE_DESC::Buffer( 8 * g_maxParticles, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS ); + m_PackedViewSpaceParticlePositions.InitBuffer(&device, "PackedViewSpaceParticlePositions", &RDescPackedViewSpaceParticlePositions, 8, m_ReadBufferStates); + + // The maximum radii of each particle is cached during simulation to avoid recomputing multiple times later. This is only required + // for streaked particles as they are not round so we cache the max radius of X and Y + CD3DX12_RESOURCE_DESC RDescMaxRadiusBuffer = CD3DX12_RESOURCE_DESC::Buffer( sizeof( float ) * g_maxParticles, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS ); + m_MaxRadiusBuffer.InitBuffer(&device, "MaxRadiusBuffer", &RDescMaxRadiusBuffer, sizeof( float ), m_ReadBufferStates); + + // The dead particle index list. Created as an append buffer + CD3DX12_RESOURCE_DESC RDescDeadListBuffer = CD3DX12_RESOURCE_DESC::Buffer( sizeof( INT ) * ( g_maxParticles + 1 ), D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS ); + m_DeadListBuffer.InitBuffer(&device, "DeadListBuffer", &RDescDeadListBuffer, sizeof( INT ), m_WriteBufferStates); + + // Create the index buffer of alive particles that is to be sorted (at least in the rasterization path). + // For the tiled rendering path this could be just a UINT index buffer as particles are not globally sorted + CD3DX12_RESOURCE_DESC RDescAliveIndexBuffer = CD3DX12_RESOURCE_DESC::Buffer( sizeof( int ) * g_maxParticles, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS ); + m_AliveIndexBuffer.InitBuffer(&device, "AliveIndexBuffer", &RDescAliveIndexBuffer, sizeof( int ), m_ReadBufferStates); + + CD3DX12_RESOURCE_DESC RDescAliveDistanceBuffer = CD3DX12_RESOURCE_DESC::Buffer( sizeof( float ) * g_maxParticles, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS ); + m_AliveDistanceBuffer.InitBuffer(&device, "AliveDistanceBuffer", &RDescAliveDistanceBuffer, sizeof( float ), m_WriteBufferStates); + + // Create the single element buffer which is used to store the count of alive particles + CD3DX12_RESOURCE_DESC RDescAliveCountBuffer = CD3DX12_RESOURCE_DESC::Buffer( sizeof( UINT ), D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS ); + m_AliveCountBuffer.InitBuffer(&device, "AliveCountBuffer", &RDescAliveCountBuffer, sizeof( UINT ), m_ReadBufferStates); + + + // Create the buffer to store the indirect args for the ExecuteIndirect call + // Create the index buffer of alive particles that is to be sorted (at least in the rasterization path). + CD3DX12_RESOURCE_DESC desc = CD3DX12_RESOURCE_DESC::Buffer( sizeof( IndirectCommand ), D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS ); + m_IndirectArgsBuffer.InitBuffer(&device, "IndirectArgsBuffer", &desc, sizeof( IndirectCommand ), m_WriteBufferStates); + + // Create the particle billboard index buffer required for the rasterization VS-only path + UINT* indices = new UINT[ g_maxParticles * 6 ]; + UINT* ptr = indices; + UINT base = 0; + for ( int i = 0; i < g_maxParticles; i++ ) + { + ptr[ 0 ] = base + 0; + ptr[ 1 ] = base + 1; + ptr[ 2 ] = base + 2; + + ptr[ 3 ] = base + 2; + ptr[ 4 ] = base + 1; + ptr[ 5 ] = base + 3; + + base += 4; + ptr += 6; + } + + bufferPool.AllocIndexBuffer( g_maxParticles * 6, sizeof( UINT ), indices, &m_IndexBuffer ); + delete[] indices; + + // Initialize the random numbers texture + FillRandomTexture( uploadHeap ); + + m_Atlas.InitFromFile( &device, &uploadHeap, m_AtlasPath, true ); + + CreateSimulationAssets(); + CreateRasterizedRenderingAssets(); + + // Create the SortLib resources + m_SortLib.OnCreate( m_pDevice, m_heaps, &constantBufferRing, &uploadHeap, &m_AliveCountBuffer, &m_AliveDistanceBuffer, &m_AliveIndexBuffer ); +} + +void GPUParticleSystem::CreateSimulationAssets() +{ + m_heaps->AllocCBV_SRV_UAVDescriptor( m_SimulationUAVDescriptorTableCount, &m_SimulationUAVDescriptorTable ); + + m_ParticleBufferA.CreateBufferUAV( 0, nullptr, &m_SimulationUAVDescriptorTable ); + m_ParticleBufferB.CreateBufferUAV( 1, nullptr, &m_SimulationUAVDescriptorTable ); + m_DeadListBuffer.CreateBufferUAV( 2, nullptr, &m_SimulationUAVDescriptorTable ); + m_AliveIndexBuffer.CreateBufferUAV( 3, nullptr, &m_SimulationUAVDescriptorTable ); + m_AliveDistanceBuffer.CreateBufferUAV( 4, nullptr, &m_SimulationUAVDescriptorTable ); + m_MaxRadiusBuffer.CreateBufferUAV( 5, nullptr, &m_SimulationUAVDescriptorTable ); + m_PackedViewSpaceParticlePositions.CreateBufferUAV( 6, nullptr, &m_SimulationUAVDescriptorTable ); + m_IndirectArgsBuffer.CreateBufferUAV( 7, nullptr, &m_SimulationUAVDescriptorTable ); + m_AliveCountBuffer.CreateBufferUAV( 8, nullptr, &m_SimulationUAVDescriptorTable ); + + m_heaps->AllocCBV_SRV_UAVDescriptor( m_SimulationSRVDescriptorTableCount, &m_SimulationSRVDescriptorTable ); + // depth buffer // t0 + m_RandomTexture.CreateSRV( 1, &m_SimulationSRVDescriptorTable ); // t1 + + { + CD3DX12_DESCRIPTOR_RANGE DescRange[2] = {}; + DescRange[0].Init( D3D12_DESCRIPTOR_RANGE_TYPE_UAV, m_SimulationUAVDescriptorTableCount, 0 ); // u0 - u8 + DescRange[1].Init( D3D12_DESCRIPTOR_RANGE_TYPE_SRV, m_SimulationSRVDescriptorTableCount, 0 ); // t0 - t1 + + CD3DX12_ROOT_PARAMETER rootParamters[4] = {}; + rootParamters[0].InitAsDescriptorTable( 1, &DescRange[0], D3D12_SHADER_VISIBILITY_ALL ); // uavs + rootParamters[1].InitAsDescriptorTable( 1, &DescRange[1], D3D12_SHADER_VISIBILITY_ALL ); // textures + rootParamters[2].InitAsConstantBufferView( 0 ); // b0 - per frame + rootParamters[3].InitAsConstantBufferView( 1 ); // b1 - per emitter + + CD3DX12_STATIC_SAMPLER_DESC sampler( 0, D3D12_FILTER_MIN_MAG_MIP_POINT, D3D12_TEXTURE_ADDRESS_MODE_WRAP, D3D12_TEXTURE_ADDRESS_MODE_WRAP, D3D12_TEXTURE_ADDRESS_MODE_CLAMP ); + + CD3DX12_ROOT_SIGNATURE_DESC descRootSignature = {}; + descRootSignature.Init( _countof( rootParamters ), rootParamters, 1, &sampler ); + + ID3DBlob *pOutBlob, *pErrorBlob = nullptr; + D3D12SerializeRootSignature( &descRootSignature, D3D_ROOT_SIGNATURE_VERSION_1, &pOutBlob, &pErrorBlob ); + m_pDevice->GetDevice()->CreateRootSignature( 0, pOutBlob->GetBufferPointer(), pOutBlob->GetBufferSize(), IID_PPV_ARGS( &m_pSimulationRootSignature ) ); + m_pSimulationRootSignature->SetName( L"SimulationRootSignature" ); + + pOutBlob->Release(); + if (pErrorBlob) + pErrorBlob->Release(); + } + + D3D12_COMPUTE_PIPELINE_STATE_DESC descPso = {}; + descPso.Flags = D3D12_PIPELINE_STATE_FLAG_NONE; + descPso.pRootSignature = m_pSimulationRootSignature; + descPso.NodeMask = 0; + + DefineList defines; + defines["API_DX12"] = ""; + + { + D3D12_SHADER_BYTECODE computeShader; + CompileShaderFromFile( "ParticleSimulation.hlsl", &defines, "CS_Reset", "-T cs_6_0", &computeShader ); + + descPso.CS = computeShader; + m_pDevice->GetDevice()->CreateComputePipelineState( &descPso, IID_PPV_ARGS( &m_pResetParticlesPipeline ) ); + m_pResetParticlesPipeline->SetName( L"ResetParticles" ); + } + + { + D3D12_SHADER_BYTECODE computeShader; + CompileShaderFromFile( "ParticleSimulation.hlsl", &defines, "CS_Simulate", "-T cs_6_0", &computeShader ); + + descPso.CS = computeShader; + m_pDevice->GetDevice()->CreateComputePipelineState( &descPso, IID_PPV_ARGS( &m_pSimulatePipeline ) ); + m_pSimulatePipeline->SetName( L"Simulation" ); + } + + { + D3D12_SHADER_BYTECODE computeShader; + CompileShaderFromFile( "ParticleEmit.hlsl", &defines, "CS_Emit", "-T cs_6_0", &computeShader ); + + descPso.CS = computeShader; + m_pDevice->GetDevice()->CreateComputePipelineState( &descPso, IID_PPV_ARGS( &m_pEmitPipeline ) ); + m_pEmitPipeline->SetName( L"Emit" ); + } +} + + +void GPUParticleSystem::CreateRasterizedRenderingAssets() +{ + m_heaps->AllocCBV_SRV_UAVDescriptor( m_RasterizationSRVDescriptorTableCount, &m_RasterizationSRVDescriptorTable ); + m_ParticleBufferA.CreateSRV( 0, &m_RasterizationSRVDescriptorTable ); + m_PackedViewSpaceParticlePositions.CreateSRV( 1, &m_RasterizationSRVDescriptorTable ); + m_AliveCountBuffer.CreateSRV( 2, &m_RasterizationSRVDescriptorTable ); + m_AliveIndexBuffer.CreateSRV( 3, &m_RasterizationSRVDescriptorTable ); + m_Atlas.CreateSRV( 4, &m_RasterizationSRVDescriptorTable ); + // depth texture t5 + + { + CD3DX12_DESCRIPTOR_RANGE DescRange[1] = {}; + DescRange[0].Init( D3D12_DESCRIPTOR_RANGE_TYPE_SRV, m_RasterizationSRVDescriptorTableCount, 0 ); // t0-t5 + + CD3DX12_ROOT_PARAMETER rootParamters[3] = {}; + rootParamters[0].InitAsDescriptorTable( 1, &DescRange[0], D3D12_SHADER_VISIBILITY_ALL ); // textures + rootParamters[1].InitAsConstantBufferView( 0 ); // b0 + rootParamters[2].InitAsUnorderedAccessView( 0 ); + + CD3DX12_STATIC_SAMPLER_DESC sampler( 0, D3D12_FILTER_MIN_MAG_LINEAR_MIP_POINT, D3D12_TEXTURE_ADDRESS_MODE_CLAMP, D3D12_TEXTURE_ADDRESS_MODE_CLAMP, D3D12_TEXTURE_ADDRESS_MODE_CLAMP ); + + CD3DX12_ROOT_SIGNATURE_DESC descRootSignature = {}; + descRootSignature.Init( _countof( rootParamters ), rootParamters, 1, &sampler ); + + ID3DBlob *pOutBlob, *pErrorBlob = nullptr; + D3D12SerializeRootSignature( &descRootSignature, D3D_ROOT_SIGNATURE_VERSION_1, &pOutBlob, &pErrorBlob ); + m_pDevice->GetDevice()->CreateRootSignature( 0, pOutBlob->GetBufferPointer(), pOutBlob->GetBufferSize(), IID_PPV_ARGS( &m_pRasterizationRootSignature ) ); + m_pRasterizationRootSignature->SetName( L"RasterizationRootSignature" ); + + pOutBlob->Release(); + if (pErrorBlob) + pErrorBlob->Release(); + } + + D3D12_GRAPHICS_PIPELINE_STATE_DESC descPso = {}; + descPso.InputLayout = { nullptr, 0 }; + descPso.pRootSignature = m_pRasterizationRootSignature; + + descPso.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); + descPso.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; + descPso.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); + descPso.BlendState.IndependentBlendEnable = true; + descPso.BlendState.RenderTarget[0].BlendEnable = true; + descPso.BlendState.RenderTarget[2].BlendEnable = true; + + descPso.BlendState.RenderTarget[0].SrcBlend = D3D12_BLEND_SRC_ALPHA; + descPso.BlendState.RenderTarget[0].DestBlend = D3D12_BLEND_INV_SRC_ALPHA; + descPso.BlendState.RenderTarget[0].BlendOp = D3D12_BLEND_OP_ADD; + descPso.BlendState.RenderTarget[0].SrcBlendAlpha = D3D12_BLEND_INV_SRC_ALPHA; + descPso.BlendState.RenderTarget[0].DestBlendAlpha = D3D12_BLEND_ZERO; + descPso.BlendState.RenderTarget[0].BlendOpAlpha = D3D12_BLEND_OP_ADD; + + descPso.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; + descPso.BlendState.RenderTarget[1].RenderTargetWriteMask = 0; + descPso.BlendState.RenderTarget[2].RenderTargetWriteMask = 0; + descPso.BlendState.RenderTarget[3].RenderTargetWriteMask = 0; + + descPso.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT); + descPso.DepthStencilState.DepthEnable = TRUE; + descPso.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO; + descPso.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_GREATER_EQUAL; + descPso.SampleMask = UINT_MAX; + descPso.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + descPso.NumRenderTargets = 4; + descPso.RTVFormats[0] = DXGI_FORMAT_R16G16B16A16_FLOAT; + descPso.RTVFormats[1] = DXGI_FORMAT_R16G16_FLOAT; + descPso.RTVFormats[2] = DXGI_FORMAT_R8_UNORM; + descPso.RTVFormats[3] = DXGI_FORMAT_R8_UNORM; + descPso.DSVFormat = DXGI_FORMAT_D32_FLOAT; + descPso.SampleDesc.Count = 1; + descPso.NodeMask = 0; + + for ( int i = 0; i < NumStreakModes; i++ ) + { + for ( int j = 0; j < NumReactiveModes; j++ ) + { + descPso.BlendState.RenderTarget[2].RenderTargetWriteMask = 0; + + DefineList defines; + defines["API_DX12"] = ""; + if ( i == StreaksOn ) + defines["STREAKS"] = ""; + + if ( j == ReactiveOn ) + { + defines["REACTIVE"] = ""; + descPso.BlendState.RenderTarget[2].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_RED; + } + + D3D12_SHADER_BYTECODE vertexShader = {}; + CompileShaderFromFile( "ParticleRender.hlsl", &defines, "VS_StructuredBuffer", "-T vs_6_0", &vertexShader ); + + D3D12_SHADER_BYTECODE pixelShader = {}; + CompileShaderFromFile( "ParticleRender.hlsl", &defines, "PS_Billboard", "-T ps_6_0", &pixelShader ); + + descPso.VS = vertexShader; + descPso.PS = pixelShader; + m_pDevice->GetDevice()->CreateGraphicsPipelineState( &descPso, IID_PPV_ARGS( &m_pRasterizationPipelines[ i ][ j ] ) ); + } + } + + D3D12_INDIRECT_ARGUMENT_DESC argumentDescs[2] = {}; + argumentDescs[0].Type = D3D12_INDIRECT_ARGUMENT_TYPE_UNORDERED_ACCESS_VIEW; + argumentDescs[0].UnorderedAccessView.RootParameterIndex = 2; + argumentDescs[1].Type = D3D12_INDIRECT_ARGUMENT_TYPE_DRAW_INDEXED; + + D3D12_COMMAND_SIGNATURE_DESC commandSignatureDesc = {}; + commandSignatureDesc.pArgumentDescs = argumentDescs; + commandSignatureDesc.NumArgumentDescs = _countof( argumentDescs ); + commandSignatureDesc.ByteStride = sizeof( IndirectCommand ); + + m_pDevice->GetDevice()->CreateCommandSignature( &commandSignatureDesc, m_pRasterizationRootSignature, IID_PPV_ARGS( &m_commandSignature ) ); + m_commandSignature->SetName( L"CommandSignature" ); +} + + +void GPUParticleSystem::OnResizedSwapChain( int width, int height, Texture& depthBuffer ) +{ + m_ScreenWidth = width; + m_ScreenHeight = height; + m_InvScreenWidth = 1.0f / m_ScreenWidth; + m_InvScreenHeight = 1.0f / m_ScreenHeight; + + depthBuffer.CreateSRV( 0, &m_SimulationSRVDescriptorTable ); + depthBuffer.CreateSRV( 5, &m_RasterizationSRVDescriptorTable ); +} + + +void GPUParticleSystem::OnReleasingSwapChain() +{ +} + + +void GPUParticleSystem::OnDestroyDevice() +{ + m_pDevice = nullptr; + + m_ParticleBufferA.OnDestroy(); + m_ParticleBufferB.OnDestroy(); + m_PackedViewSpaceParticlePositions.OnDestroy(); + m_MaxRadiusBuffer.OnDestroy(); + m_DeadListBuffer.OnDestroy(); + m_AliveIndexBuffer.OnDestroy(); + m_AliveDistanceBuffer.OnDestroy(); + m_AliveCountBuffer.OnDestroy(); + m_RandomTexture.OnDestroy(); + m_Atlas.OnDestroy(); + m_IndirectArgsBuffer.OnDestroy(); + + m_pSimulatePipeline->Release(); + m_pSimulatePipeline = nullptr; + + m_pResetParticlesPipeline->Release(); + m_pResetParticlesPipeline = nullptr; + + m_pEmitPipeline->Release(); + m_pEmitPipeline = nullptr; + + m_pSimulationRootSignature->Release(); + m_pSimulationRootSignature = nullptr; + + for ( int i = 0; i < NumStreakModes; i++ ) + { + for ( int j = 0; j < NumReactiveModes; j++ ) + { + m_pRasterizationPipelines[ i ][ j ]->Release(); + m_pRasterizationPipelines[ i ][ j ] = nullptr; + } + } + + m_pRasterizationRootSignature->Release(); + m_pRasterizationRootSignature = nullptr; + + m_commandSignature->Release(); + m_commandSignature = nullptr; + + m_SortLib.OnDestroy(); + + m_ResetSystem = true; +} + + +// Per-frame emission of particles into the GPU simulation +void GPUParticleSystem::Emit( ID3D12GraphicsCommandList* pCommandList, DynamicBufferRing& constantBufferRing, int numEmitters, const EmitterParams* emitters ) +{ + pCommandList->SetPipelineState( m_pEmitPipeline ); + + // Run CS for each emitter + for ( int i = 0; i < numEmitters; i++ ) + { + const EmitterParams& emitter = emitters[ i ]; + + if ( emitter.m_NumToEmit > 0 ) + { + EmitterConstantBuffer* constants = nullptr; + D3D12_GPU_VIRTUAL_ADDRESS constantBuffer; + constantBufferRing.AllocConstantBuffer( sizeof(*constants), (void**)&constants, &constantBuffer ); + constants->m_EmitterPosition = emitter.m_Position; + constants->m_EmitterVelocity = emitter.m_Velocity; + constants->m_MaxParticlesThisFrame = emitter.m_NumToEmit; + constants->m_ParticleLifeSpan = emitter.m_ParticleLifeSpan; + constants->m_StartSize = emitter.m_StartSize; + constants->m_EndSize = emitter.m_EndSize; + constants->m_PositionVariance = emitter.m_PositionVariance; + constants->m_VelocityVariance = emitter.m_VelocityVariance; + constants->m_Mass = emitter.m_Mass; + constants->m_Index = i; + constants->m_Streaks = emitter.m_Streaks ? 1 : 0; + constants->m_TextureIndex = emitter.m_TextureIndex; + pCommandList->SetComputeRootConstantBufferView( 3, constantBuffer ); + + // Dispatch enough thread groups to spawn the requested particles + int numThreadGroups = align( emitter.m_NumToEmit, 1024 ) / 1024; + pCommandList->Dispatch( numThreadGroups, 1, 1 ); + + pCommandList->ResourceBarrier( 1, &CD3DX12_RESOURCE_BARRIER::UAV( m_DeadListBuffer.GetResource() ) ); + } + } + + // RaW barriers + pCommandList->ResourceBarrier( 1, &CD3DX12_RESOURCE_BARRIER::UAV( m_ParticleBufferA.GetResource() ) ); + pCommandList->ResourceBarrier( 1, &CD3DX12_RESOURCE_BARRIER::UAV( m_ParticleBufferB.GetResource() ) ); +} + + +// Per-frame simulation step +void GPUParticleSystem::Simulate( ID3D12GraphicsCommandList* pCommandList ) +{ + pCommandList->SetPipelineState( m_pSimulatePipeline ); + pCommandList->Dispatch( align( g_maxParticles, 256 ) / 256, 1, 1 ); +} + + +// Populate a texture with random numbers (used for the emission of particles) +void GPUParticleSystem::FillRandomTexture( UploadHeap& uploadHeap ) +{ + IMG_INFO header = {}; + header.width = 1024; + header.height = 1024; + header.depth = 1; + header.arraySize = 1; + header.mipMapCount = 1; + header.format = DXGI_FORMAT_R32G32B32A32_FLOAT; + header.bitCount = 128; + + float* values = new float[ header.width * header.height * 4 ]; + float* ptr = values; + for ( UINT i = 0; i < header.width * header.height; i++ ) + { + ptr[ 0 ] = RandomVariance( 0.0f, 1.0f ); + ptr[ 1 ] = RandomVariance( 0.0f, 1.0f ); + ptr[ 2 ] = RandomVariance( 0.0f, 1.0f ); + ptr[ 3 ] = RandomVariance( 0.0f, 1.0f ); + ptr += 4; + } + + m_RandomTexture.InitFromData(m_pDevice, "RadomTexture", uploadHeap, header, values ); + + delete[] values; +} diff --git a/src/GpuParticles/dx12/ParallelSort.cpp b/src/GpuParticles/dx12/ParallelSort.cpp new file mode 100644 index 0000000..0f1a108 --- /dev/null +++ b/src/GpuParticles/dx12/ParallelSort.cpp @@ -0,0 +1,524 @@ +// ParallelSort.cpp +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#define FFX_CPP +#include "ParallelSort.h" +#include "../FFX-ParallelSort/FFX_ParallelSort.h" + +static const uint32_t NumKeys = { 400*1024 }; + + +void FFXParallelSort::CompileRadixPipeline(const char* shaderFile, const DefineList* defines, const char* entryPoint, ID3D12PipelineState*& pPipeline) +{ + std::string CompileFlags("-T cs_6_0"); +#ifdef _DEBUG + CompileFlags += " -Zi -Od"; +#endif // _DEBUG + + D3D12_SHADER_BYTECODE shaderByteCode = {}; + CompileShaderFromFile(shaderFile, defines, entryPoint, CompileFlags.c_str(), &shaderByteCode); + + D3D12_COMPUTE_PIPELINE_STATE_DESC descPso = {}; + descPso.CS = shaderByteCode; + descPso.Flags = D3D12_PIPELINE_STATE_FLAG_NONE; + descPso.pRootSignature = m_pFPSRootSignature; + descPso.NodeMask = 0; + + ThrowIfFailed(m_pDevice->GetDevice()->CreateComputePipelineState(&descPso, IID_PPV_ARGS(&pPipeline))); + SetName(pPipeline, entryPoint); +} + +void FFXParallelSort::OnCreate(Device* pDevice, ResourceViewHeaps* pResourceViewHeaps, DynamicBufferRing* pConstantBufferRing, UploadHeap* pUploadHeap, Texture* elementCount, Texture* listA, Texture* listB) +{ + m_pDevice = pDevice; + m_pUploadHeap = pUploadHeap; + m_pResourceViewHeaps = pResourceViewHeaps; + m_pConstantBufferRing = pConstantBufferRing; + m_SrcKeyBuffer = listA; + m_SrcPayloadBuffer = listB; + m_MaxNumThreadgroups = 800; + + // Allocate UAVs to use for data + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_ElementCountSRV); + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_SrcKeyUAV); + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_SrcPayloadUAV); + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(2, &m_DstKeyUAVTable); + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(2, &m_DstPayloadUAVTable); + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_FPSScratchUAV); + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_FPSReducedScratchUAV); + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_IndirectKeyCountsUAV); + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_IndirectConstantBufferUAV); + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_IndirectCountScatterArgsUAV); + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_IndirectReduceScanArgsUAV); + + // The DstKey and DstPayload buffers will be used as src/dst when sorting. A copy of the + // source key/payload will be copied into them before hand so we can keep our original values + CD3DX12_RESOURCE_DESC ResourceDesc = CD3DX12_RESOURCE_DESC::Buffer(sizeof(uint32_t) * NumKeys, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS); + m_DstKeyTempBuffer[0].InitBuffer(m_pDevice, "DstKeyTempBuf0", &ResourceDesc, sizeof(uint32_t), D3D12_RESOURCE_STATE_COMMON); + m_DstKeyTempBuffer[1].InitBuffer(m_pDevice, "DstKeyTempBuf1", &ResourceDesc, sizeof(uint32_t), D3D12_RESOURCE_STATE_COMMON); + m_DstPayloadTempBuffer[0].InitBuffer(m_pDevice, "DstPayloadTempBuf0", &ResourceDesc, sizeof(uint32_t), D3D12_RESOURCE_STATE_COMMON); + m_DstPayloadTempBuffer[1].InitBuffer(m_pDevice, "DstPayloadTempBuf1", &ResourceDesc, sizeof(uint32_t), D3D12_RESOURCE_STATE_COMMON); + { + CD3DX12_RESOURCE_BARRIER Barriers[4] = + { + CD3DX12_RESOURCE_BARRIER::Transition(m_DstKeyTempBuffer[0].GetResource(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_UNORDERED_ACCESS), + CD3DX12_RESOURCE_BARRIER::Transition(m_DstKeyTempBuffer[1].GetResource(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_UNORDERED_ACCESS), + CD3DX12_RESOURCE_BARRIER::Transition(m_DstPayloadTempBuffer[0].GetResource(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_UNORDERED_ACCESS), + CD3DX12_RESOURCE_BARRIER::Transition(m_DstPayloadTempBuffer[1].GetResource(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_UNORDERED_ACCESS) + }; + m_pUploadHeap->GetCommandList()->ResourceBarrier(4, Barriers); + } + + // Create UAVs + listA->CreateBufferUAV(0, nullptr, &m_SrcKeyUAV); + listB->CreateBufferUAV(0, nullptr, &m_SrcPayloadUAV); + m_DstKeyTempBuffer[0].CreateBufferUAV(0, nullptr, &m_DstKeyUAVTable); + m_DstPayloadTempBuffer[0].CreateBufferUAV(0, nullptr, &m_DstPayloadUAVTable); + m_DstKeyTempBuffer[1].CreateBufferUAV(1, nullptr, &m_DstKeyUAVTable); + m_DstPayloadTempBuffer[1].CreateBufferUAV(1, nullptr, &m_DstPayloadUAVTable); + + elementCount->CreateSRV( 0, &m_ElementCountSRV, 0 ); + + // We are just going to fudge the indirect execution parameters for each resolution + ResourceDesc = CD3DX12_RESOURCE_DESC::Buffer(sizeof(uint32_t), D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS); + m_IndirectKeyCounts.InitBuffer(m_pDevice, "IndirectKeyCounts", &ResourceDesc, sizeof(uint32_t), D3D12_RESOURCE_STATE_COMMON); + m_IndirectKeyCounts.CreateBufferUAV(0, nullptr, &m_IndirectKeyCountsUAV); + uint8_t* pNumKeysBuffer = m_pUploadHeap->Suballocate(sizeof(uint32_t), D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT); + memcpy(pNumKeysBuffer, &NumKeys, sizeof(uint32_t) ); + m_pUploadHeap->GetCommandList()->CopyBufferRegion(m_IndirectKeyCounts.GetResource(), 0, m_pUploadHeap->GetResource(), pNumKeysBuffer - m_pUploadHeap->BasePtr(), sizeof(uint32_t)); + CD3DX12_RESOURCE_BARRIER Barrier = CD3DX12_RESOURCE_BARRIER::Transition(m_IndirectKeyCounts.GetResource(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + m_pUploadHeap->GetCommandList()->ResourceBarrier(1, &Barrier); + + // Allocate the scratch buffers needed for radix sort + uint32_t scratchBufferSize; + uint32_t reducedScratchBufferSize; + FFX_ParallelSort_CalculateScratchResourceSize(NumKeys, scratchBufferSize, reducedScratchBufferSize); + + ResourceDesc = CD3DX12_RESOURCE_DESC::Buffer(scratchBufferSize, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS); + m_FPSScratchBuffer.InitBuffer(m_pDevice, "Scratch", &ResourceDesc, sizeof(uint32_t), D3D12_RESOURCE_STATE_COMMON); + m_FPSScratchBuffer.CreateBufferUAV(0, nullptr, &m_FPSScratchUAV); + + ResourceDesc = CD3DX12_RESOURCE_DESC::Buffer(reducedScratchBufferSize, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS); + m_FPSReducedScratchBuffer.InitBuffer(m_pDevice, "ReducedScratch", &ResourceDesc, sizeof(uint32_t), D3D12_RESOURCE_STATE_COMMON); + m_FPSReducedScratchBuffer.CreateBufferUAV(0, nullptr, &m_FPSReducedScratchUAV); + + // Allocate the buffers for indirect execution of the algorithm + ResourceDesc = CD3DX12_RESOURCE_DESC::Buffer(sizeof(FFX_ParallelSortCB), D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS); + m_IndirectConstantBuffer.InitBuffer(m_pDevice, "IndirectConstantBuffer", &ResourceDesc, sizeof(FFX_ParallelSortCB), D3D12_RESOURCE_STATE_COMMON); + m_IndirectConstantBuffer.CreateBufferUAV(0, nullptr, &m_IndirectConstantBufferUAV); + + ResourceDesc = CD3DX12_RESOURCE_DESC::Buffer(sizeof(uint32_t) * 3, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS); + m_IndirectCountScatterArgs.InitBuffer(m_pDevice, "IndirectCount_Scatter_DispatchArgs", &ResourceDesc, sizeof(uint32_t), D3D12_RESOURCE_STATE_COMMON); + m_IndirectCountScatterArgs.CreateBufferUAV(0, nullptr, &m_IndirectCountScatterArgsUAV); + m_IndirectReduceScanArgs.InitBuffer(m_pDevice, "IndirectCount_Scatter_DispatchArgs", &ResourceDesc, sizeof(uint32_t), D3D12_RESOURCE_STATE_COMMON); + m_IndirectReduceScanArgs.CreateBufferUAV(0, nullptr, &m_IndirectReduceScanArgsUAV); + + { + CD3DX12_RESOURCE_BARRIER Barriers[5] = + { + CD3DX12_RESOURCE_BARRIER::Transition(m_FPSScratchBuffer.GetResource(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_UNORDERED_ACCESS), + CD3DX12_RESOURCE_BARRIER::Transition(m_FPSReducedScratchBuffer.GetResource(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_UNORDERED_ACCESS), + CD3DX12_RESOURCE_BARRIER::Transition(m_IndirectConstantBuffer.GetResource(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_UNORDERED_ACCESS), + CD3DX12_RESOURCE_BARRIER::Transition(m_IndirectCountScatterArgs.GetResource(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_UNORDERED_ACCESS), + CD3DX12_RESOURCE_BARRIER::Transition(m_IndirectReduceScanArgs.GetResource(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_UNORDERED_ACCESS) + }; + m_pUploadHeap->GetCommandList()->ResourceBarrier(5, Barriers); + } + // Create root signature for Radix sort passes + { + D3D12_DESCRIPTOR_RANGE descRange[16]; + D3D12_ROOT_PARAMETER rootParams[17]; + + // Constant buffer table (always have 1) + descRange[0] = { D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND }; + rootParams[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV; rootParams[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[0].Descriptor = { descRange[0].BaseShaderRegister, descRange[0].RegisterSpace }; + + // Constant buffer to setup indirect params (indirect) + descRange[1] = { D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 1, 0, D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND }; + rootParams[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV; rootParams[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[1].Descriptor = { descRange[1].BaseShaderRegister, descRange[1].RegisterSpace }; + + rootParams[2].ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS; rootParams[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[2].Constants = { 2, 0, 1 }; + + // SrcBuffer (sort or scan) + descRange[2] = { D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND }; + rootParams[3].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; rootParams[3].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[3].DescriptorTable = { 1, &descRange[2] }; + + // ScrPayload (sort only) + descRange[3] = { D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, 1, D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND }; + rootParams[4].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; rootParams[4].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[4].DescriptorTable = { 1, &descRange[3] }; + + // Scratch (sort only) + descRange[4] = { D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, 2, D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND }; + rootParams[5].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; rootParams[5].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[5].DescriptorTable = { 1, &descRange[4] }; + + // Scratch (reduced) + descRange[5] = { D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, 3, D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND }; + rootParams[6].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; rootParams[6].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[6].DescriptorTable = { 1, &descRange[5] }; + + // DstBuffer (sort or scan) + descRange[6] = { D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, 4, D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND }; + rootParams[7].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; rootParams[7].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[7].DescriptorTable = { 1, &descRange[6] }; + + // DstPayload (sort only) + descRange[7] = { D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, 5, D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND }; + rootParams[8].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; rootParams[8].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[8].DescriptorTable = { 1, &descRange[7] }; + + // ScanSrc + descRange[8] = { D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, 6, D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND }; + rootParams[9].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; rootParams[9].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[9].DescriptorTable = { 1, &descRange[8] }; + + // ScanDst + descRange[9] = { D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, 7, D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND }; + rootParams[10].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; rootParams[10].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[10].DescriptorTable = { 1, &descRange[9] }; + + // ScanScratch + descRange[10] = { D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, 8, D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND }; + rootParams[11].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; rootParams[11].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[11].DescriptorTable = { 1, &descRange[10] }; + + // NumKeys (indirect) + descRange[11] = { D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, 9, D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND }; + rootParams[12].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; rootParams[12].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[12].DescriptorTable = { 1, &descRange[11] }; + + // CBufferUAV (indirect) + descRange[12] = { D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, 10, D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND }; + rootParams[13].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; rootParams[13].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[13].DescriptorTable = { 1, &descRange[12] }; + + // CountScatterArgs (indirect) + descRange[13] = { D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, 11, D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND }; + rootParams[14].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; rootParams[14].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[14].DescriptorTable = { 1, &descRange[13] }; + + // ReduceScanArgs (indirect) + descRange[14] = { D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, 12, D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND }; + rootParams[15].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; rootParams[15].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[15].DescriptorTable = { 1, &descRange[14] }; + + descRange[15] = { D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND }; + rootParams[16].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; rootParams[16].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + rootParams[16].DescriptorTable = { 1, &descRange[15] }; + + D3D12_ROOT_SIGNATURE_DESC rootSigDesc = {}; + rootSigDesc.NumParameters = 17; + rootSigDesc.pParameters = rootParams; + rootSigDesc.NumStaticSamplers = 0; + rootSigDesc.pStaticSamplers = nullptr; + rootSigDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE; + + ID3DBlob* pOutBlob, * pErrorBlob = nullptr; + ThrowIfFailed(D3D12SerializeRootSignature(&rootSigDesc, D3D_ROOT_SIGNATURE_VERSION_1, &pOutBlob, &pErrorBlob)); + ThrowIfFailed(pDevice->GetDevice()->CreateRootSignature(0, pOutBlob->GetBufferPointer(), pOutBlob->GetBufferSize(), IID_PPV_ARGS(&m_pFPSRootSignature))); + SetName(m_pFPSRootSignature, "FPS_Signature"); + + pOutBlob->Release(); + if (pErrorBlob) + pErrorBlob->Release(); + + // Also create the command signature for the indirect version + D3D12_INDIRECT_ARGUMENT_DESC dispatch = {}; + dispatch.Type = D3D12_INDIRECT_ARGUMENT_TYPE_DISPATCH; + D3D12_COMMAND_SIGNATURE_DESC desc = {}; + desc.ByteStride = sizeof(D3D12_DISPATCH_ARGUMENTS); + desc.NodeMask = 0; + desc.NumArgumentDescs = 1; + desc.pArgumentDescs = &dispatch; + + ThrowIfFailed(pDevice->GetDevice()->CreateCommandSignature(&desc, nullptr, IID_PPV_ARGS(&m_pFPSCommandSignature))); + m_pFPSCommandSignature->SetName(L"FPS_CommandSignature"); + } + + ////////////////////////////////////////////////////////////////////////// + // Create pipelines for radix sort + { + // Create all of the necessary pipelines for Sort and Scan + DefineList defines; + + // SetupIndirectParams (indirect only) + CompileRadixPipeline("ParallelSortCS.hlsl", &defines, "FPS_SetupIndirectParameters", m_pFPSIndirectSetupParametersPipeline); + + // Radix count (sum table generation) + CompileRadixPipeline("ParallelSortCS.hlsl", &defines, "FPS_Count", m_pFPSCountPipeline); + // Radix count reduce (sum table reduction for offset prescan) + CompileRadixPipeline("ParallelSortCS.hlsl", &defines, "FPS_CountReduce", m_pFPSCountReducePipeline); + // Radix scan (prefix scan) + CompileRadixPipeline("ParallelSortCS.hlsl", &defines, "FPS_Scan", m_pFPSScanPipeline); + // Radix scan add (prefix scan + reduced prefix scan addition) + CompileRadixPipeline("ParallelSortCS.hlsl", &defines, "FPS_ScanAdd", m_pFPSScanAddPipeline); + // Radix scatter (key redistribution) + CompileRadixPipeline("ParallelSortCS.hlsl", &defines, "FPS_Scatter", m_pFPSScatterPipeline); + // Radix scatter with payload (key and payload redistribution) + defines["kRS_ValueCopy"] = std::to_string(1); + CompileRadixPipeline("ParallelSortCS.hlsl", &defines, "FPS_Scatter", m_pFPSScatterPayloadPipeline); + } +} + +void FFXParallelSort::OnDestroy() +{ + // Release radix sort indirect resources + m_IndirectKeyCounts.OnDestroy(); + m_IndirectConstantBuffer.OnDestroy(); + m_IndirectCountScatterArgs.OnDestroy(); + m_IndirectReduceScanArgs.OnDestroy(); + m_pFPSCommandSignature->Release(); + m_pFPSIndirectSetupParametersPipeline->Release(); + + // Release radix sort algorithm resources + m_FPSScratchBuffer.OnDestroy(); + m_FPSReducedScratchBuffer.OnDestroy(); + m_pFPSRootSignature->Release(); + m_pFPSCountPipeline->Release(); + m_pFPSCountReducePipeline->Release(); + m_pFPSScanPipeline->Release(); + m_pFPSScanAddPipeline->Release(); + m_pFPSScatterPipeline->Release(); + m_pFPSScatterPayloadPipeline->Release(); + + // Release all of our resources + m_DstKeyTempBuffer[0].OnDestroy(); + m_DstKeyTempBuffer[1].OnDestroy(); + m_DstPayloadTempBuffer[0].OnDestroy(); + m_DstPayloadTempBuffer[1].OnDestroy(); +} + + +void FFXParallelSort::Draw(ID3D12GraphicsCommandList* pCommandList) +{ + bool bIndirectDispatch = true; + + std::string markerText = "FFXParallelSort"; + if (bIndirectDispatch) markerText += " Indirect"; + UserMarker marker(pCommandList, markerText.c_str()); + + FFX_ParallelSortCB constantBufferData = { 0 }; + + // Bind the descriptor heaps + ID3D12DescriptorHeap* pDescriptorHeap = m_pResourceViewHeaps->GetCBV_SRV_UAVHeap(); + pCommandList->SetDescriptorHeaps(1, &pDescriptorHeap); + + // Bind the root signature + pCommandList->SetComputeRootSignature(m_pFPSRootSignature); + + // Fill in the constant buffer data structure (this will be done by a shader in the indirect version) + uint32_t NumThreadgroupsToRun; + uint32_t NumReducedThreadgroupsToRun; + if (!bIndirectDispatch) + { + uint32_t NumberOfKeys = NumKeys; + FFX_ParallelSort_SetConstantAndDispatchData(NumberOfKeys, m_MaxNumThreadgroups, constantBufferData, NumThreadgroupsToRun, NumReducedThreadgroupsToRun); + } + else + { + struct SetupIndirectCB + { + uint32_t MaxThreadGroups; + }; + SetupIndirectCB IndirectSetupCB; + IndirectSetupCB.MaxThreadGroups = m_MaxNumThreadgroups; + + // Copy the data into the constant buffer + D3D12_GPU_VIRTUAL_ADDRESS constantBuffer = m_pConstantBufferRing->AllocConstantBuffer(sizeof(SetupIndirectCB), &IndirectSetupCB); + pCommandList->SetComputeRootConstantBufferView(1, constantBuffer); // SetupIndirect Constant buffer + + // Bind other buffer + pCommandList->SetComputeRootDescriptorTable(12, m_IndirectKeyCountsUAV.GetGPU()); // Key counts + pCommandList->SetComputeRootDescriptorTable(13, m_IndirectConstantBufferUAV.GetGPU()); // Indirect Sort Constant Buffer + pCommandList->SetComputeRootDescriptorTable(14, m_IndirectCountScatterArgsUAV.GetGPU()); // Indirect Sort Count/Scatter Args + pCommandList->SetComputeRootDescriptorTable(15, m_IndirectReduceScanArgsUAV.GetGPU()); // Indirect Sort Reduce/Scan Args + pCommandList->SetComputeRootDescriptorTable(16, m_ElementCountSRV.GetGPU()); // Indirect Sort Reduce/Scan Args + + // Dispatch + pCommandList->SetPipelineState(m_pFPSIndirectSetupParametersPipeline); + pCommandList->Dispatch(1, 1, 1); + + // When done, transition the args buffers to INDIRECT_ARGUMENT, and the constant buffer UAV to Constant buffer + CD3DX12_RESOURCE_BARRIER barriers[5]; + barriers[0] = CD3DX12_RESOURCE_BARRIER::UAV(m_IndirectCountScatterArgs.GetResource()); + barriers[1] = CD3DX12_RESOURCE_BARRIER::UAV(m_IndirectReduceScanArgs.GetResource()); + barriers[2] = CD3DX12_RESOURCE_BARRIER::Transition(m_IndirectConstantBuffer.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER); + barriers[3] = CD3DX12_RESOURCE_BARRIER::Transition(m_IndirectCountScatterArgs.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT); + barriers[4] = CD3DX12_RESOURCE_BARRIER::Transition(m_IndirectReduceScanArgs.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT); + pCommandList->ResourceBarrier(5, barriers); + } + + // Setup resource/UAV pairs to use during sort + RdxDX12ResourceInfo KeySrcInfo = { m_SrcKeyBuffer->GetResource(), m_SrcKeyUAV.GetGPU(0) }; + RdxDX12ResourceInfo PayloadSrcInfo = { m_SrcPayloadBuffer->GetResource(), m_SrcPayloadUAV.GetGPU(0) }; + RdxDX12ResourceInfo KeyTmpInfo = { m_DstKeyTempBuffer[1].GetResource(), m_DstKeyUAVTable.GetGPU(1) }; + RdxDX12ResourceInfo PayloadTmpInfo = { m_DstPayloadTempBuffer[1].GetResource(), m_DstPayloadUAVTable.GetGPU(1) }; + RdxDX12ResourceInfo ScratchBufferInfo = { m_FPSScratchBuffer.GetResource(), m_FPSScratchUAV.GetGPU() }; + RdxDX12ResourceInfo ReducedScratchBufferInfo = { m_FPSReducedScratchBuffer.GetResource(), m_FPSReducedScratchUAV.GetGPU() }; + + // Buffers to ping-pong between when writing out sorted values + const RdxDX12ResourceInfo* ReadBufferInfo(&KeySrcInfo), * WriteBufferInfo(&KeyTmpInfo); + const RdxDX12ResourceInfo* ReadPayloadBufferInfo(&PayloadSrcInfo), * WritePayloadBufferInfo(&PayloadTmpInfo); + bool bHasPayload = true; + + // Setup barriers for the run + CD3DX12_RESOURCE_BARRIER barriers[3]; + + // Perform Radix Sort (currently only support 32-bit key/payload sorting + for (uint32_t Shift = 0; Shift < 32u; Shift += FFX_PARALLELSORT_SORT_BITS_PER_PASS) + { + // Update the bit shift + pCommandList->SetComputeRoot32BitConstant(2, Shift, 0); + + // Copy the data into the constant buffer + D3D12_GPU_VIRTUAL_ADDRESS constantBuffer; + if (bIndirectDispatch) + constantBuffer = m_IndirectConstantBuffer.GetResource()->GetGPUVirtualAddress(); + else + constantBuffer = m_pConstantBufferRing->AllocConstantBuffer(sizeof(FFX_ParallelSortCB), &constantBufferData); + + // Bind to root signature + pCommandList->SetComputeRootConstantBufferView(0, constantBuffer); // Constant buffer + pCommandList->SetComputeRootDescriptorTable(3, ReadBufferInfo->resourceGPUHandle); // SrcBuffer + pCommandList->SetComputeRootDescriptorTable(5, ScratchBufferInfo.resourceGPUHandle); // Scratch buffer + + // Sort Count + { + pCommandList->SetPipelineState(m_pFPSCountPipeline); + + if (bIndirectDispatch) + { + pCommandList->ExecuteIndirect(m_pFPSCommandSignature, 1, m_IndirectCountScatterArgs.GetResource(), 0, nullptr, 0); + } + else + { + pCommandList->Dispatch(NumThreadgroupsToRun, 1, 1); + } + } + + // UAV barrier on the sum table + barriers[0] = CD3DX12_RESOURCE_BARRIER::UAV(ScratchBufferInfo.pResource); + pCommandList->ResourceBarrier(1, barriers); + + pCommandList->SetComputeRootDescriptorTable(6, ReducedScratchBufferInfo.resourceGPUHandle); // Scratch reduce buffer + + // Sort Reduce + { + pCommandList->SetPipelineState(m_pFPSCountReducePipeline); + + if (bIndirectDispatch) + { + pCommandList->ExecuteIndirect(m_pFPSCommandSignature, 1, m_IndirectReduceScanArgs.GetResource(), 0, nullptr, 0); + } + else + { + pCommandList->Dispatch(NumReducedThreadgroupsToRun, 1, 1); + } + + // UAV barrier on the reduced sum table + barriers[0] = CD3DX12_RESOURCE_BARRIER::UAV(ReducedScratchBufferInfo.pResource); + pCommandList->ResourceBarrier(1, barriers); + } + + // Sort Scan + { + // First do scan prefix of reduced values + pCommandList->SetComputeRootDescriptorTable(9, ReducedScratchBufferInfo.resourceGPUHandle); + pCommandList->SetComputeRootDescriptorTable(10, ReducedScratchBufferInfo.resourceGPUHandle); + + pCommandList->SetPipelineState(m_pFPSScanPipeline); + if (!bIndirectDispatch) + { + assert(NumReducedThreadgroupsToRun < FFX_PARALLELSORT_ELEMENTS_PER_THREAD * FFX_PARALLELSORT_THREADGROUP_SIZE && "Need to account for bigger reduced histogram scan"); + } + pCommandList->Dispatch(1, 1, 1); + + // UAV barrier on the reduced sum table + barriers[0] = CD3DX12_RESOURCE_BARRIER::UAV(ReducedScratchBufferInfo.pResource); + pCommandList->ResourceBarrier(1, barriers); + + // Next do scan prefix on the histogram with partial sums that we just did + pCommandList->SetComputeRootDescriptorTable(9, ScratchBufferInfo.resourceGPUHandle); + pCommandList->SetComputeRootDescriptorTable(10, ScratchBufferInfo.resourceGPUHandle); + pCommandList->SetComputeRootDescriptorTable(11, ReducedScratchBufferInfo.resourceGPUHandle); + + pCommandList->SetPipelineState(m_pFPSScanAddPipeline); + if (bIndirectDispatch) + { + pCommandList->ExecuteIndirect(m_pFPSCommandSignature, 1, m_IndirectReduceScanArgs.GetResource(), 0, nullptr, 0); + } + else + { + pCommandList->Dispatch(NumReducedThreadgroupsToRun, 1, 1); + } + } + + // UAV barrier on the sum table + barriers[0] = CD3DX12_RESOURCE_BARRIER::UAV(ScratchBufferInfo.pResource); + pCommandList->ResourceBarrier(1, barriers); + + if (bHasPayload) + { + pCommandList->SetComputeRootDescriptorTable(4, ReadPayloadBufferInfo->resourceGPUHandle); // ScrPayload + pCommandList->SetComputeRootDescriptorTable(8, WritePayloadBufferInfo->resourceGPUHandle); // DstPayload + } + + pCommandList->SetComputeRootDescriptorTable(7, WriteBufferInfo->resourceGPUHandle); // DstBuffer + + // Sort Scatter + { + pCommandList->SetPipelineState(bHasPayload ? m_pFPSScatterPayloadPipeline : m_pFPSScatterPipeline); + + if (bIndirectDispatch) + { + pCommandList->ExecuteIndirect(m_pFPSCommandSignature, 1, m_IndirectCountScatterArgs.GetResource(), 0, nullptr, 0); + } + else + { + pCommandList->Dispatch(NumThreadgroupsToRun, 1, 1); + } + } + + // Finish doing everything and barrier for the next pass + int numBarriers = 0; + barriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::UAV(WriteBufferInfo->pResource); + if (bHasPayload) + barriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::UAV(WritePayloadBufferInfo->pResource); + pCommandList->ResourceBarrier(numBarriers, barriers); + + // Swap read/write sources + std::swap(ReadBufferInfo, WriteBufferInfo); + if (bHasPayload) + std::swap(ReadPayloadBufferInfo, WritePayloadBufferInfo); + } + + // When we are all done, transition indirect buffers back to UAV for the next frame (if doing indirect dispatch) + if (bIndirectDispatch) + { + barriers[0] = CD3DX12_RESOURCE_BARRIER::Transition(m_IndirectCountScatterArgs.GetResource(), D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + barriers[1] = CD3DX12_RESOURCE_BARRIER::Transition(m_IndirectReduceScanArgs.GetResource(), D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + barriers[2] = CD3DX12_RESOURCE_BARRIER::Transition(m_IndirectConstantBuffer.GetResource(), D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + pCommandList->ResourceBarrier(3, barriers); + } +} \ No newline at end of file diff --git a/src/GpuParticles/dx12/ParallelSort.h b/src/GpuParticles/dx12/ParallelSort.h new file mode 100644 index 0000000..f1c1547 --- /dev/null +++ b/src/GpuParticles/dx12/ParallelSort.h @@ -0,0 +1,102 @@ +// ParallelSort.h +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#pragma once +#include "../DX12/stdafx.h" + +#define SORT_BITS_PER_PASS 4 +#define SORT_BIN_COUNT (1 << SORT_BITS_PER_PASS) +#define THREADGROUP_SIZE 64 +#define ELEMENTS_PER_THREAD 4 // (256 / THREADGROUP_SIZE) +#define ITEMS_PER_WI 16 +#define INV_ITEMS_PER_WI 1/16 + +struct ParallelSortRenderCB // If you change this, also change struct ParallelSortRenderCB in ParallelSortVerify.hlsl +{ + int32_t Width; + int32_t Height; + int32_t SortWidth; + int32_t SortHeight; +}; + +// Convenience struct for passing resource/UAV pairs around +typedef struct RdxDX12ResourceInfo +{ + ID3D12Resource* pResource; ///< Pointer to the resource -- used for barriers and syncs (must NOT be nullptr) + D3D12_GPU_DESCRIPTOR_HANDLE resourceGPUHandle; ///< The GPU Descriptor Handle to use for binding the resource +} RdxDX12ResourceInfo; + +class FFXParallelSort +{ +public: + void OnCreate(Device* pDevice, ResourceViewHeaps* pResourceViewHeaps, DynamicBufferRing* pConstantBufferRing, UploadHeap* pUploadHeap, Texture* elementCount, Texture* listA, Texture* listB); + void OnDestroy(); + + void Draw(ID3D12GraphicsCommandList* pCommandList); + +private: + + void CompileRadixPipeline(const char* shaderFile, const DefineList* defines, const char* entryPoint, ID3D12PipelineState*& pPipeline); + + Device* m_pDevice = nullptr; + UploadHeap* m_pUploadHeap = nullptr; + ResourceViewHeaps* m_pResourceViewHeaps = nullptr; + DynamicBufferRing* m_pConstantBufferRing = nullptr; + uint32_t m_MaxNumThreadgroups = 320; // Use a generic thread group size when not on AMD hardware (taken from experiments to determine best performance threshold) + + // Sample resources + Texture* m_SrcKeyBuffer = nullptr; + Texture* m_SrcPayloadBuffer = nullptr; + CBV_SRV_UAV m_ElementCountSRV; + CBV_SRV_UAV m_SrcKeyUAV; // 32 bit source key UAVs + CBV_SRV_UAV m_SrcPayloadUAV; // 32 bit source payload UAVs + + Texture m_DstKeyTempBuffer[ 2 ]; + CBV_SRV_UAV m_DstKeyUAVTable; // 32 bit destination key UAVs + + Texture m_DstPayloadTempBuffer[ 2 ]; + CBV_SRV_UAV m_DstPayloadUAVTable; // 32 bit destination payload UAVs + + // Resources for parallel sort algorithm + Texture m_FPSScratchBuffer; // Sort scratch buffer + CBV_SRV_UAV m_FPSScratchUAV; // UAV needed for sort scratch buffer + Texture m_FPSReducedScratchBuffer; // Sort reduced scratch buffer + CBV_SRV_UAV m_FPSReducedScratchUAV; // UAV needed for sort reduced scratch buffer + + ID3D12RootSignature* m_pFPSRootSignature = nullptr; + ID3D12PipelineState* m_pFPSCountPipeline = nullptr; + ID3D12PipelineState* m_pFPSCountReducePipeline = nullptr; + ID3D12PipelineState* m_pFPSScanPipeline = nullptr; + ID3D12PipelineState* m_pFPSScanAddPipeline = nullptr; + ID3D12PipelineState* m_pFPSScatterPipeline = nullptr; + ID3D12PipelineState* m_pFPSScatterPayloadPipeline = nullptr; + + // Resources for indirect execution of algorithm + Texture m_IndirectKeyCounts; // Buffer to hold num keys for indirect dispatch + CBV_SRV_UAV m_IndirectKeyCountsUAV; // UAV needed for num keys buffer + Texture m_IndirectConstantBuffer; // Buffer to hold radix sort constant buffer data for indirect dispatch + CBV_SRV_UAV m_IndirectConstantBufferUAV; // UAV needed for indirect constant buffer + Texture m_IndirectCountScatterArgs; // Buffer to hold dispatch arguments used for Count/Scatter parts of the algorithm + CBV_SRV_UAV m_IndirectCountScatterArgsUAV; // UAV needed for count/scatter args buffer + Texture m_IndirectReduceScanArgs; // Buffer to hold dispatch arguments used for Reduce/Scan parts of the algorithm + CBV_SRV_UAV m_IndirectReduceScanArgsUAV; // UAV needed for reduce/scan args buffer + + ID3D12CommandSignature* m_pFPSCommandSignature; + ID3D12PipelineState* m_pFPSIndirectSetupParametersPipeline = nullptr; +}; \ No newline at end of file diff --git a/src/GpuParticles/vk/BufferHelper.h b/src/GpuParticles/vk/BufferHelper.h new file mode 100644 index 0000000..b81f300 --- /dev/null +++ b/src/GpuParticles/vk/BufferHelper.h @@ -0,0 +1,179 @@ +// +// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +#pragma once + +#include "../vk/stdafx.h" + + +namespace CAULDRON_VK +{ + +// For adding markers in RGP +class UserMarker +{ +public: + UserMarker(VkCommandBuffer commandBuffer, const char* name) : m_commandBuffer( commandBuffer ) { SetPerfMarkerBegin(m_commandBuffer, name); } + ~UserMarker() { SetPerfMarkerEnd(m_commandBuffer); } + +private: + VkCommandBuffer m_commandBuffer; +}; + + +size_t FormatSize(VkFormat format); + + +class Buffer +{ +public: + Buffer() {} + virtual ~Buffer() {} + virtual void OnDestroy() + { + if (m_bufferView) + { + vkDestroyBufferView(m_pDevice->GetDevice(), m_bufferView, nullptr); + m_bufferView = VK_NULL_HANDLE; + } + + if (m_buffer) + { + vmaDestroyBuffer(m_pDevice->GetAllocator(), m_buffer, m_alloc); + m_buffer = VK_NULL_HANDLE; + } + m_pDevice = nullptr; + m_sizeInBytes = 0; + } + + bool Init(Device *pDevice, int numElements, VkFormat format, const char* name) + { + m_pDevice = pDevice; + m_sizeInBytes = numElements * FormatSize( format ); + VmaAllocationCreateInfo bufferAllocCreateInfo = {}; + bufferAllocCreateInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; + bufferAllocCreateInfo.flags = VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT; + bufferAllocCreateInfo.pUserData = (void*)name; + VmaAllocationInfo gpuAllocInfo = {}; + + VkBufferCreateInfo bufferInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; + bufferInfo.size = m_sizeInBytes; + bufferInfo.usage = VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT; + + VkResult res = vmaCreateBuffer(m_pDevice->GetAllocator(), &bufferInfo, &bufferAllocCreateInfo, &m_buffer, &m_alloc, &gpuAllocInfo); + assert(res == VK_SUCCESS); + SetResourceName(pDevice->GetDevice(), VK_OBJECT_TYPE_BUFFER, (uint64_t)m_buffer, name); + + VkBufferViewCreateInfo viewInfo = {}; + viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO; + viewInfo.format = format; + viewInfo.buffer = m_buffer; + viewInfo.range = m_sizeInBytes; + vkCreateBufferView(pDevice->GetDevice(), &viewInfo, nullptr, &m_bufferView); + SetResourceName(m_pDevice->GetDevice(), VK_OBJECT_TYPE_BUFFER_VIEW, (uint64_t)m_bufferView, name); + return true; + } + + bool Init(Device *pDevice, int numElements, size_t structSize, const char* name, bool indirectArgs) + { + m_pDevice = pDevice; + m_sizeInBytes = numElements * structSize; + VmaAllocationCreateInfo bufferAllocCreateInfo = {}; + bufferAllocCreateInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; + bufferAllocCreateInfo.flags = VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT; + bufferAllocCreateInfo.pUserData = (void*)name; + VmaAllocationInfo gpuAllocInfo = {}; + + VkBufferCreateInfo bufferInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; + bufferInfo.size = m_sizeInBytes; + bufferInfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + if ( indirectArgs ) + bufferInfo.usage |= VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT; + + VkResult res = vmaCreateBuffer(m_pDevice->GetAllocator(), &bufferInfo, &bufferAllocCreateInfo, &m_buffer, &m_alloc, &gpuAllocInfo); + assert(res == VK_SUCCESS); + SetResourceName(pDevice->GetDevice(), VK_OBJECT_TYPE_BUFFER, (uint64_t)m_buffer, name); + + return true; + } + + VkBuffer& Resource() { return m_buffer; } + + void SetDescriptorSet(int index, VkDescriptorSet descriptorSet, bool asUAV) const + { + VkDescriptorBufferInfo descriptorBufferInfo = {}; + descriptorBufferInfo.buffer = m_buffer; + descriptorBufferInfo.range = m_sizeInBytes; + + VkWriteDescriptorSet write = {}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = descriptorSet; + write.descriptorCount = 1; + if ( m_bufferView ) + { + write.descriptorType = asUAV ? VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER : VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER; + write.pTexelBufferView = &m_bufferView; + } + else + { + write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + write.pBufferInfo = &descriptorBufferInfo; + } + write.dstBinding = index; + vkUpdateDescriptorSets(m_pDevice->GetDevice(), 1, &write, 0, nullptr); + } + + void PipelineBarrier( VkCommandBuffer commandBuffer, VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask ) + { + VkBufferMemoryBarrier memoryBarrier = {}; + memoryBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; + memoryBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + memoryBarrier.dstAccessMask = dstStageMask == VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT ? VK_ACCESS_INDIRECT_COMMAND_READ_BIT : VK_ACCESS_SHADER_READ_BIT; + memoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + memoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + memoryBarrier.buffer = m_buffer; + memoryBarrier.size = m_sizeInBytes; + vkCmdPipelineBarrier( commandBuffer, srcStageMask, dstStageMask, VK_DEPENDENCY_BY_REGION_BIT, 0, nullptr, 1, &memoryBarrier, 0, nullptr ); + } + + void AddPipelineBarrier( std::vector& barrierList, VkPipelineStageFlags dstStageMask ) + { + VkBufferMemoryBarrier memoryBarrier = {}; + memoryBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; + memoryBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + memoryBarrier.dstAccessMask = dstStageMask == VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT ? VK_ACCESS_INDIRECT_COMMAND_READ_BIT : VK_ACCESS_SHADER_READ_BIT; + memoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + memoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + memoryBarrier.buffer = m_buffer; + memoryBarrier.size = m_sizeInBytes; + barrierList.push_back( memoryBarrier ); + } + +private: + + Device* m_pDevice = nullptr; + VmaAllocation m_alloc = VK_NULL_HANDLE; + VkBuffer m_buffer = VK_NULL_HANDLE; + size_t m_sizeInBytes = 0; + VkBufferView m_bufferView = VK_NULL_HANDLE; +}; + +} \ No newline at end of file diff --git a/src/GpuParticles/vk/GPUParticleSystem.cpp b/src/GpuParticles/vk/GPUParticleSystem.cpp new file mode 100644 index 0000000..0bd26ee --- /dev/null +++ b/src/GpuParticles/vk/GPUParticleSystem.cpp @@ -0,0 +1,944 @@ +// +// Copyright (c) 2019 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +#include "../vk/stdafx.h" +#include "BufferHelper.h" +#include "../ParticleSystem.h" +#include "../ParticleHelpers.h" +#include "../ParticleSystemInternal.h" +#include "ParallelSort.h" +#include "base/ExtDebugUtils.h" + + +size_t CAULDRON_VK::FormatSize(VkFormat format) +{ + switch (format) + { + case VK_FORMAT_R8_SINT: return 1;//(BYTE) + case VK_FORMAT_R8_UINT: return 1;//(UNSIGNED_BYTE)1 + case VK_FORMAT_R16_SINT: return 2;//(SHORT)2 + case VK_FORMAT_R16_UINT: return 2;//(UNSIGNED_SHORT)2 + case VK_FORMAT_R32_SINT: return 4;//(SIGNED_INT)4 + case VK_FORMAT_R32_UINT: return 4;//(UNSIGNED_INT)4 + case VK_FORMAT_R32_SFLOAT: return 4;//(FLOAT) + + case VK_FORMAT_R8G8_SINT: return 2 * 1;//(BYTE) + case VK_FORMAT_R8G8_UINT: return 2 * 1;//(UNSIGNED_BYTE)1 + case VK_FORMAT_R16G16_SINT: return 2 * 2;//(SHORT)2 + case VK_FORMAT_R16G16_UINT: return 2 * 2; // (UNSIGNED_SHORT)2 + case VK_FORMAT_R32G32_SINT: return 2 * 4;//(SIGNED_INT)4 + case VK_FORMAT_R32G32_UINT: return 2 * 4;//(UNSIGNED_INT)4 + case VK_FORMAT_R32G32_SFLOAT: return 2 * 4;//(FLOAT) + + case VK_FORMAT_UNDEFINED: return 0;//(BYTE) (UNSIGNED_BYTE) (SHORT) (UNSIGNED_SHORT) + case VK_FORMAT_R32G32B32_SINT: return 3 * 4;//(SIGNED_INT)4 + case VK_FORMAT_R32G32B32_UINT: return 3 * 4;//(UNSIGNED_INT)4 + case VK_FORMAT_R32G32B32_SFLOAT: return 3 * 4;//(FLOAT) + + case VK_FORMAT_R8G8B8A8_SINT: return 4 * 1;//(BYTE) + case VK_FORMAT_R8G8B8A8_UINT: return 4 * 1;//(UNSIGNED_BYTE)1 + case VK_FORMAT_R16G16B16A16_SINT: return 4 * 2;//(SHORT)2 + case VK_FORMAT_R16G16B16A16_UINT: return 4 * 2;//(UNSIGNED_SHORT)2 + case VK_FORMAT_R32G32B32A32_SINT: return 4 * 4;//(SIGNED_INT)4 + case VK_FORMAT_R32G32B32A32_UINT: return 4 * 4;//(UNSIGNED_INT)4 + case VK_FORMAT_R32G32B32A32_SFLOAT: return 4 * 4;//(FLOAT) + + case VK_FORMAT_R16G16B16A16_SFLOAT: return 4 * 2; + } + + assert(0); + return 0; +} + +#pragma warning( disable : 4100 ) // disable unreference formal parameter warnings for /W4 builds + +struct IndirectCommand +{ + int args[ 5 ]; +}; + +// GPU Particle System class. Responsible for updating and rendering the particles +class GPUParticleSystem : public IParticleSystem +{ +public: + + GPUParticleSystem( const char* particleAtlas ); + +private: + + enum DepthCullingMode + { + DepthCullingOn, + DepthCullingOff, + NumDepthCullingModes + }; + + enum StreakMode + { + StreaksOn, + StreaksOff, + NumStreakModes + }; + + enum ReactiveMode + { + ReactiveOn, + ReactiveOff, + NumReactiveModes + }; + + virtual ~GPUParticleSystem(); + + virtual void OnCreateDevice( Device& device, UploadHeap& uploadHeap, ResourceViewHeaps& heaps, StaticBufferPool& bufferPool, DynamicBufferRing& constantBufferRing, VkRenderPass renderPass ); + virtual void OnResizedSwapChain( int width, int height, Texture& depthBuffer, VkFramebuffer frameBuffer ); + virtual void OnReleasingSwapChain(); + virtual void OnDestroyDevice(); + + virtual void Reset(); + + virtual void Render( VkCommandBuffer commandBuffer, DynamicBufferRing& constantBufferRing, int flags, const EmitterParams* pEmitters, int nNumEmitters, const ConstantData& constantData ); + + void Emit( VkCommandBuffer commandBuffer, DynamicBufferRing& constantBufferRing, uint32_t perFrameConstantOffset, int numEmitters, const EmitterParams* emitters ); + void Simulate( VkCommandBuffer commandBuffer ); + void Sort( VkCommandBuffer commandBuffer ); + + void FillRandomTexture( UploadHeap& uploadHeap ); + void CreateSimulationAssets( DynamicBufferRing& constantBufferRing ); + void CreateRasterizedRenderingAssets( DynamicBufferRing& constantBufferRing ); + + VkPipeline CreatePipeline( const char* filename, const char* entry, VkPipelineLayout layout, const DefineList* defines ); + + Device* m_pDevice = nullptr; + ResourceViewHeaps* m_heaps = nullptr; + const char* m_AtlasPath = nullptr; + VkRenderPass m_renderPass = VK_NULL_HANDLE; + VkFramebuffer m_frameBuffer = VK_NULL_HANDLE; + + Texture m_Atlas = {}; + VkImageView m_AtlasSRV = {}; + Buffer m_ParticleBufferA = {}; + Buffer m_ParticleBufferB = {}; + Buffer m_PackedViewSpaceParticlePositions = {}; + Buffer m_MaxRadiusBuffer = {}; + Buffer m_DeadListBuffer = {}; + Buffer m_AliveCountBuffer = {}; + Buffer m_AliveIndexBuffer = {}; + Buffer m_AliveDistanceBuffer = {}; + Buffer m_DstAliveIndexBuffer = {}; // working memory for the Radix sorter + Buffer m_DstAliveDistanceBuffer = {}; // working memory for the Radix sorter + Buffer m_IndirectArgsBuffer = {}; + + Texture m_RandomTexture = {}; + VkImageView m_RandomTextureSRV = {}; + + VkImage m_DepthBuffer = {}; + VkImageView m_DepthBufferSRV = {}; + + VkDescriptorSetLayout m_SimulationDescriptorSetLayout = VK_NULL_HANDLE; + VkDescriptorSet m_SimulationDescriptorSet = VK_NULL_HANDLE; + + VkDescriptorSetLayout m_RasterizationDescriptorSetLayout = VK_NULL_HANDLE; + VkDescriptorSet m_RasterizationDescriptorSet = VK_NULL_HANDLE; + + VkSampler m_samplers[ 3 ] = {}; + + UINT m_ScreenWidth = 0; + UINT m_ScreenHeight = 0; + float m_InvScreenWidth = 0.0f; + float m_InvScreenHeight = 0.0f; + float m_ElapsedTime = 0.0f; + float m_AlphaThreshold = 0.97f; + + VkDescriptorBufferInfo m_IndexBuffer = {}; + + VkPipelineLayout m_SimulationPipelineLayout = VK_NULL_HANDLE; + VkPipelineLayout m_RasterizationPipelineLayout = VK_NULL_HANDLE; + + VkPipeline m_SimulationPipeline = VK_NULL_HANDLE; + VkPipeline m_EmitPipeline = VK_NULL_HANDLE; + VkPipeline m_ResetParticlesPipeline = VK_NULL_HANDLE; + VkPipeline m_RasterizationPipelines[ NumStreakModes ][ NumReactiveModes ] = {}; + + bool m_ResetSystem = true; + FFXParallelSort m_SortLib = {}; +}; + + +IParticleSystem* IParticleSystem::CreateGPUSystem( const char* particleAtlas ) +{ + return new GPUParticleSystem( particleAtlas ); +} + + +GPUParticleSystem::GPUParticleSystem( const char* particleAtlas ) : m_AtlasPath( particleAtlas ) {} +GPUParticleSystem::~GPUParticleSystem() {} + + +// Use the sort lib to perform a bitonic sort over the particle indices based on their distance from camera +void GPUParticleSystem::Sort( VkCommandBuffer commandBuffer ) +{ + m_SortLib.Draw( commandBuffer ); +} + + +void GPUParticleSystem::Reset() +{ + m_ResetSystem = true; +} + + +void GPUParticleSystem::Render( VkCommandBuffer commandBuffer, DynamicBufferRing& constantBufferRing, int flags, const EmitterParams* pEmitters, int nNumEmitters, const ConstantData& constantData ) +{ + SimulationConstantBuffer simulationConstants = {}; + + memcpy( simulationConstants.m_StartColor, constantData.m_StartColor, sizeof( simulationConstants.m_StartColor ) ); + memcpy( simulationConstants.m_EndColor, constantData.m_EndColor, sizeof( simulationConstants.m_EndColor ) ); + memcpy( simulationConstants.m_EmitterLightingCenter, constantData.m_EmitterLightingCenter, sizeof( simulationConstants.m_EmitterLightingCenter ) ); + + simulationConstants.m_ViewProjection = constantData.m_ViewProjection; + simulationConstants.m_View = constantData.m_View; + simulationConstants.m_ViewInv = constantData.m_ViewInv; + simulationConstants.m_ProjectionInv = constantData.m_ProjectionInv; + + simulationConstants.m_EyePosition = constantData.m_ViewInv.getCol3(); + simulationConstants.m_SunDirection = constantData.m_SunDirection; + + simulationConstants.m_ScreenWidth = m_ScreenWidth; + simulationConstants.m_ScreenHeight = m_ScreenHeight; + simulationConstants.m_MaxParticles = g_maxParticles; + simulationConstants.m_FrameTime = constantData.m_FrameTime; + + math::Vector4 sunDirectionVS = constantData.m_View * constantData.m_SunDirection; + + m_ElapsedTime += constantData.m_FrameTime; + if ( m_ElapsedTime > 10.0f ) + m_ElapsedTime -= 10.0f; + + simulationConstants.m_ElapsedTime = m_ElapsedTime; + + void* data = nullptr; + VkDescriptorBufferInfo constantBuffer = {}; + constantBufferRing.AllocConstantBuffer( sizeof( simulationConstants ), &data, &constantBuffer ); + memcpy( data, &simulationConstants, sizeof( simulationConstants ) ); + + { + uint32_t uniformOffsets[] = { (uint32_t)constantBuffer.offset, 0 }; + vkCmdBindDescriptorSets( commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, m_SimulationPipelineLayout, 0, 1, &m_SimulationDescriptorSet, _countof( uniformOffsets ), uniformOffsets ); + + + UserMarker marker( commandBuffer, "simulation" ); + + // If we are resetting the particle system, then initialize the dead list + if ( m_ResetSystem ) + { + vkCmdBindPipeline( commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, m_ResetParticlesPipeline ); + + // Disaptch a set of 1d thread groups to fill out the dead list, one thread per particle + vkCmdDispatch( commandBuffer, align( g_maxParticles, 256 ) / 256, 1, 1 ); + + std::vector barriers = {}; + m_ParticleBufferA.AddPipelineBarrier( barriers, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT ); + m_ParticleBufferB.AddPipelineBarrier( barriers, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT ); + m_DeadListBuffer.AddPipelineBarrier( barriers, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT ); + vkCmdPipelineBarrier( commandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_DEPENDENCY_BY_REGION_BIT, 0, nullptr, (uint32_t)barriers.size(), &barriers[ 0 ], 0, nullptr ); + + m_ResetSystem = false; + } + + // Emit particles into the system + Emit( commandBuffer, constantBufferRing, (uint32_t)constantBuffer.offset, nNumEmitters, pEmitters ); + + // Run the simulation for this frame + Simulate( commandBuffer ); + + std::vector barriersAfterSimulation = {}; + m_ParticleBufferA.AddPipelineBarrier( barriersAfterSimulation, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT ); + m_PackedViewSpaceParticlePositions.AddPipelineBarrier( barriersAfterSimulation, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT ); + m_MaxRadiusBuffer.AddPipelineBarrier( barriersAfterSimulation, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT ); + m_DeadListBuffer.AddPipelineBarrier( barriersAfterSimulation, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT ); + m_AliveCountBuffer.AddPipelineBarrier( barriersAfterSimulation, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT ); + + VkImageMemoryBarrier barrier = {}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + barrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.image = m_DepthBuffer; + + vkCmdPipelineBarrier( commandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT, VK_DEPENDENCY_BY_REGION_BIT, 0, nullptr, (uint32_t)barriersAfterSimulation.size(), &barriersAfterSimulation[ 0 ], 1, &barrier ); + } + + { + UserMarker marker( commandBuffer, "rasterization" ); + + // Sort if requested. Not doing so results in the particles rendering out of order and not blending correctly + if ( flags & PF_Sort ) + { + UserMarker marker( commandBuffer, "sorting" ); + + std::vector barriers = {}; + m_AliveIndexBuffer.AddPipelineBarrier( barriers, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT ); + m_AliveDistanceBuffer.AddPipelineBarrier( barriers, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT ); + vkCmdPipelineBarrier( commandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_DEPENDENCY_BY_REGION_BIT, 0, nullptr, (uint32_t)barriers.size(), &barriers[ 0 ], 0, nullptr ); + + Sort( commandBuffer ); + } + + std::vector barriers = {}; + m_AliveIndexBuffer.AddPipelineBarrier( barriers, VK_PIPELINE_STAGE_VERTEX_SHADER_BIT ); + m_IndirectArgsBuffer.AddPipelineBarrier( barriers, VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT ); + vkCmdPipelineBarrier( commandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, VK_DEPENDENCY_BY_REGION_BIT, 0, nullptr, (uint32_t)barriers.size(), &barriers[ 0 ], 0, nullptr ); + + RenderingConstantBuffer* cb = nullptr; + VkDescriptorBufferInfo constantBuffer = {}; + constantBufferRing.AllocConstantBuffer( sizeof( RenderingConstantBuffer ), (void**)&cb, &constantBuffer ); + cb->m_Projection = constantData.m_Projection; + cb->m_ProjectionInv = simulationConstants.m_ProjectionInv; + cb->m_SunColor = constantData.m_SunColor; + cb->m_AmbientColor = constantData.m_AmbientColor; + cb->m_SunDirectionVS = sunDirectionVS; + cb->m_ScreenWidth = m_ScreenWidth; + cb->m_ScreenHeight = m_ScreenHeight; + + uint32_t uniformOffsets[1] = { (uint32_t)constantBuffer.offset }; + vkCmdBindDescriptorSets( commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_RasterizationPipelineLayout, 0, 1, &m_RasterizationDescriptorSet, 1, uniformOffsets ); + + VkRenderPassBeginInfo renderPassBegin = {}; + renderPassBegin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + renderPassBegin.renderPass = m_renderPass; + renderPassBegin.framebuffer = m_frameBuffer; + renderPassBegin.renderArea.extent.width = m_ScreenWidth; + renderPassBegin.renderArea.extent.height = m_ScreenHeight; + + vkCmdBeginRenderPass( commandBuffer, &renderPassBegin, VK_SUBPASS_CONTENTS_INLINE ); + + StreakMode streaks = flags & PF_Streaks ? StreaksOn : StreaksOff; + ReactiveMode reactive = flags & PF_Reactive ? ReactiveOn : ReactiveOff; + + vkCmdBindPipeline( commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_RasterizationPipelines[ streaks ][ reactive ] ); + + vkCmdBindIndexBuffer( commandBuffer, m_IndexBuffer.buffer, m_IndexBuffer.offset, VK_INDEX_TYPE_UINT32 ); + + vkCmdDrawIndexedIndirect( commandBuffer, m_IndirectArgsBuffer.Resource(), 0, 1, sizeof( IndirectCommand ) ); + + vkCmdEndRenderPass( commandBuffer ); + } +} + + +void GPUParticleSystem::OnCreateDevice( Device& device, UploadHeap& uploadHeap, ResourceViewHeaps& heaps, StaticBufferPool& bufferPool, DynamicBufferRing& constantBufferRing, VkRenderPass renderPass ) +{ + m_pDevice = &device; + m_heaps = &heaps; + m_renderPass = renderPass; + + VkSamplerCreateInfo sampler = {}; + sampler.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + sampler.minLod = 0.0f; + sampler.maxLod = FLT_MAX; + sampler.mipLodBias = 0.0f; + sampler.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; + sampler.compareEnable = VK_FALSE; + sampler.compareOp = VK_COMPARE_OP_NEVER; + sampler.maxAnisotropy = 1.0f; + sampler.anisotropyEnable = VK_FALSE; + + for ( int i = 0; i < 3; i++ ) + { + if ( i == 1 ) + { + sampler.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sampler.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + } + else + { + sampler.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; + sampler.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; + sampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; + } + + if ( i == 2 ) + { + sampler.magFilter = VK_FILTER_NEAREST; + sampler.minFilter = VK_FILTER_NEAREST; + sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + } + else + { + sampler.magFilter = VK_FILTER_LINEAR; + sampler.minFilter = VK_FILTER_LINEAR; + sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + } + + vkCreateSampler( m_pDevice->GetDevice(), &sampler, nullptr, &m_samplers[ i ] ); + } + + // Create the global particle pool. Each particle is split into two parts for better cache coherency. The first half contains the data more + // relevant to rendering while the second half is more related to simulation + m_ParticleBufferA.Init( m_pDevice, g_maxParticles, sizeof( GPUParticlePartA ), "ParticleBufferA", false ); + m_ParticleBufferB.Init( m_pDevice, g_maxParticles, sizeof( GPUParticlePartB ), "ParticleBufferB", false ); + + // The packed view space positions of particles are cached during simulation so allocate a buffer for them + m_PackedViewSpaceParticlePositions.Init( m_pDevice, g_maxParticles, sizeof( UINT ) * 2, "PackedViewSpaceParticlePositions", false ); + + // The maximum radii of each particle is cached during simulation to avoid recomputing multiple times later. This is only required + // for streaked particles as they are not round so we cache the max radius of X and Y + m_MaxRadiusBuffer.Init( m_pDevice, g_maxParticles, 4, "MaxRadiusBuffer", false ); + + // The dead particle index list. Created as an append buffer + m_DeadListBuffer.Init( m_pDevice, g_maxParticles + 1, 4, "DeadListBuffer", false ); + + // Create the buffer to hold the number of alive particles + m_AliveCountBuffer.Init( m_pDevice, 1, 4, "AliveCountBuffer", false ); + + // Create the index buffer of alive particles that is to be sorted (at least in the rasterization path). + m_AliveIndexBuffer.Init( m_pDevice, g_maxParticles, 4, "AliveIndexBuffer", false ); + m_DstAliveIndexBuffer.Init( m_pDevice, g_maxParticles, 4, "DstAliveIndexBuffer", false ); + + // Create the list of distances of each alive particle - used for sorting in the rasterization path. + m_AliveDistanceBuffer.Init( m_pDevice, g_maxParticles, 4, "AliveDistanceBuffer", false ); + m_DstAliveDistanceBuffer.Init( m_pDevice, g_maxParticles, 4, "DstAliveDistanceBuffer", false ); + + // Create the buffer to store the indirect args for the ExecuteIndirect call + // Create the index buffer of alive particles that is to be sorted (at least in the rasterization path). + m_IndirectArgsBuffer.Init( m_pDevice, 1, sizeof( IndirectCommand ), "IndirectArgsBuffer", true ); + + // Create the particle billboard index buffer required for the rasterization VS-only path + UINT* indices = new UINT[ g_maxParticles * 6 ]; + UINT* ptr = indices; + UINT base = 0; + for ( int i = 0; i < g_maxParticles; i++ ) + { + ptr[ 0 ] = base + 0; + ptr[ 1 ] = base + 1; + ptr[ 2 ] = base + 2; + + ptr[ 3 ] = base + 2; + ptr[ 4 ] = base + 1; + ptr[ 5 ] = base + 3; + + base += 4; + ptr += 6; + } + + bufferPool.AllocBuffer( g_maxParticles * 6, sizeof( UINT ), indices, &m_IndexBuffer ); + delete[] indices; + + // Initialize the random numbers texture + FillRandomTexture( uploadHeap ); + + m_Atlas.InitFromFile( &device, &uploadHeap, m_AtlasPath, true ); + m_Atlas.CreateSRV( &m_AtlasSRV ); + + CreateSimulationAssets( constantBufferRing ); + CreateRasterizedRenderingAssets( constantBufferRing ); + + // Create the SortLib resources + m_SortLib.OnCreate( &device, &heaps, &constantBufferRing, &uploadHeap, &m_AliveCountBuffer, &m_AliveDistanceBuffer, &m_AliveIndexBuffer, &m_DstAliveDistanceBuffer, &m_DstAliveIndexBuffer ); +} + + +VkPipeline GPUParticleSystem::CreatePipeline( const char* filename, const char* entry, VkPipelineLayout layout, const DefineList* defines ) +{ + VkPipelineShaderStageCreateInfo computeShader = {}; + VkResult res = VKCompileFromFile( m_pDevice->GetDevice(), VK_SHADER_STAGE_COMPUTE_BIT, filename, entry, "-T cs_6_0", defines, &computeShader ); + assert(res == VK_SUCCESS); + + VkComputePipelineCreateInfo pipelineInfo = {}; + pipelineInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; + pipelineInfo.layout = layout; + pipelineInfo.stage = computeShader; + + VkPipeline pipeline = {}; + res = vkCreateComputePipelines( m_pDevice->GetDevice(), m_pDevice->GetPipelineCache(), 1, &pipelineInfo, nullptr, &pipeline ); + assert(res == VK_SUCCESS); + SetResourceName( m_pDevice->GetDevice(), VK_OBJECT_TYPE_PIPELINE, (uint64_t)pipeline, entry ); + + return pipeline; +} + + +void GPUParticleSystem::CreateSimulationAssets( DynamicBufferRing& constantBufferRing ) +{ + // 0 - g_ParticleBufferA + // 1 - g_ParticleBufferB + // 2 - g_DeadList + // 3 - g_IndexBuffer + // 4 - g_DistanceBuffer + // 5 - g_MaxRadiusBuffer + // 6 - g_PackedViewSpacePositions + // 7 - g_DrawArgs + // 8 - g_AliveParticleCount + // 9 - g_DepthBuffer + // 10 - g_RandomBuffer + // 11 - PerFrameConstantBuffer + // 12 - EmitterConstantBuffer + // 13 - g_samWrapPoint + + std::vector layout_bindings( 14 ); + int binding = 0; + for ( int i = 0; i < 9; i++ ) + { + layout_bindings[binding].binding = binding; + layout_bindings[binding].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + layout_bindings[binding].descriptorCount = 1; + layout_bindings[binding].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + layout_bindings[binding].pImmutableSamplers = nullptr; + binding++; + } + + for ( int i = 0; i < 2; i++ ) + { + layout_bindings[binding].binding = binding; + layout_bindings[binding].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + layout_bindings[binding].descriptorCount = 1; + layout_bindings[binding].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + layout_bindings[binding].pImmutableSamplers = nullptr; + binding++; + } + for ( int i = 0; i < 2; i++ ) + { + layout_bindings[binding].binding = binding; + layout_bindings[binding].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; + layout_bindings[binding].descriptorCount = 1; + layout_bindings[binding].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + layout_bindings[binding].pImmutableSamplers = nullptr; + binding++; + } + + { + layout_bindings[binding].binding = binding; + layout_bindings[binding].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER; + layout_bindings[binding].descriptorCount = 1; + layout_bindings[binding].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + layout_bindings[binding].pImmutableSamplers = &m_samplers[ 2 ]; + binding++; + } + + assert( binding == layout_bindings.size() ); + + m_heaps->CreateDescriptorSetLayoutAndAllocDescriptorSet( &layout_bindings, &m_SimulationDescriptorSetLayout, &m_SimulationDescriptorSet ); + constantBufferRing.SetDescriptorSet( 11, sizeof( SimulationConstantBuffer ), m_SimulationDescriptorSet ); + constantBufferRing.SetDescriptorSet( 12, sizeof( EmitterConstantBuffer ), m_SimulationDescriptorSet ); + + // Create pipeline layout + // + + VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = {}; + pipelineLayoutCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + pipelineLayoutCreateInfo.setLayoutCount = 1; + pipelineLayoutCreateInfo.pSetLayouts = &m_SimulationDescriptorSetLayout; + + VkResult res = vkCreatePipelineLayout( m_pDevice->GetDevice(), &pipelineLayoutCreateInfo, nullptr, &m_SimulationPipelineLayout ); + assert(res == VK_SUCCESS); + + m_ParticleBufferA.SetDescriptorSet( 0, m_SimulationDescriptorSet, true ); + m_ParticleBufferB.SetDescriptorSet( 1, m_SimulationDescriptorSet, true ); + m_DeadListBuffer.SetDescriptorSet( 2, m_SimulationDescriptorSet, true ); + m_AliveIndexBuffer.SetDescriptorSet( 3, m_SimulationDescriptorSet, true ); + m_AliveDistanceBuffer.SetDescriptorSet( 4, m_SimulationDescriptorSet, true ); + m_MaxRadiusBuffer.SetDescriptorSet( 5, m_SimulationDescriptorSet, true ); + m_PackedViewSpaceParticlePositions.SetDescriptorSet( 6, m_SimulationDescriptorSet, true ); + m_IndirectArgsBuffer.SetDescriptorSet( 7, m_SimulationDescriptorSet, true ); + m_AliveCountBuffer.SetDescriptorSet( 8, m_SimulationDescriptorSet, true ); + // depth buffer + SetDescriptorSet( m_pDevice->GetDevice(), 10, m_RandomTextureSRV, nullptr, m_SimulationDescriptorSet ); + + // Create pipelines + // + + DefineList defines = {}; + defines[ "API_VULKAN" ] = ""; + + m_ResetParticlesPipeline = CreatePipeline( "ParticleSimulation.hlsl", "CS_Reset", m_SimulationPipelineLayout, &defines ); + m_SimulationPipeline = CreatePipeline( "ParticleSimulation.hlsl", "CS_Simulate", m_SimulationPipelineLayout, &defines ); + m_EmitPipeline = CreatePipeline( "ParticleEmit.hlsl", "CS_Emit", m_SimulationPipelineLayout, &defines ); +} + + +void GPUParticleSystem::CreateRasterizedRenderingAssets( DynamicBufferRing& constantBufferRing ) +{ + // 0 - g_ParticleBufferA + // 1 - g_PackedViewSpacePositions + // 2 - g_NumParticlesBuffer + // 3 - g_SortedIndexBuffer + // 4 - g_ParticleTexture + // 5 - g_DepthTexture + // 6 - RenderingConstantBuffer + // 7 - g_samClampLinear + + std::vector layout_bindings( 8 ); + for ( uint32_t i = 0; i < layout_bindings.size(); i++ ) + { + layout_bindings[i].binding = i; + layout_bindings[i].descriptorCount = 1; + layout_bindings[i].pImmutableSamplers = nullptr; + } + + layout_bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + layout_bindings[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + + layout_bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + layout_bindings[1].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + + layout_bindings[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + layout_bindings[2].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + + layout_bindings[3].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + layout_bindings[3].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + + layout_bindings[4].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + layout_bindings[4].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + layout_bindings[5].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + layout_bindings[5].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + layout_bindings[6].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; + layout_bindings[6].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_VERTEX_BIT; + + layout_bindings[7].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER; + layout_bindings[7].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + layout_bindings[7].pImmutableSamplers = &m_samplers[ 1 ]; + + m_heaps->CreateDescriptorSetLayoutAndAllocDescriptorSet( &layout_bindings, &m_RasterizationDescriptorSetLayout, &m_RasterizationDescriptorSet ); + m_ParticleBufferA.SetDescriptorSet( 0, m_RasterizationDescriptorSet, false ); + m_PackedViewSpaceParticlePositions.SetDescriptorSet( 1, m_RasterizationDescriptorSet, false ); + m_AliveCountBuffer.SetDescriptorSet( 2, m_RasterizationDescriptorSet, false ); + m_AliveIndexBuffer.SetDescriptorSet( 3, m_RasterizationDescriptorSet, false ); + SetDescriptorSet( m_pDevice->GetDevice(), 4, m_AtlasSRV, nullptr, m_RasterizationDescriptorSet ); + // depth buffer + constantBufferRing.SetDescriptorSet( 6, sizeof( RenderingConstantBuffer ), m_RasterizationDescriptorSet ); + + // Create pipeline layout + // + + VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = {}; + pipelineLayoutCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + pipelineLayoutCreateInfo.setLayoutCount = 1; + pipelineLayoutCreateInfo.pSetLayouts = &m_RasterizationDescriptorSetLayout; + + VkResult res = vkCreatePipelineLayout( m_pDevice->GetDevice(), &pipelineLayoutCreateInfo, nullptr, &m_RasterizationPipelineLayout ); + assert(res == VK_SUCCESS); + + // input assembly state and layout + VkPipelineVertexInputStateCreateInfo vi = {}; + vi.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + + VkPipelineInputAssemblyStateCreateInfo ia; + ia.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + ia.pNext = NULL; + ia.flags = 0; + ia.primitiveRestartEnable = VK_FALSE; + ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + + // rasterizer state + VkPipelineRasterizationStateCreateInfo rs; + rs.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rs.pNext = NULL; + rs.flags = 0; + rs.polygonMode = VK_POLYGON_MODE_FILL; + rs.cullMode = VK_CULL_MODE_NONE; + rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + rs.depthClampEnable = VK_FALSE; + rs.rasterizerDiscardEnable = VK_FALSE; + rs.depthBiasEnable = VK_FALSE; + rs.depthBiasConstantFactor = 0; + rs.depthBiasClamp = 0; + rs.depthBiasSlopeFactor = 0; + rs.lineWidth = 1.0f; + + VkPipelineColorBlendAttachmentState att_state[4] = {}; + att_state[0].colorWriteMask = 0xf; + att_state[0].blendEnable = VK_TRUE; + att_state[0].alphaBlendOp = VK_BLEND_OP_ADD; + att_state[0].colorBlendOp = VK_BLEND_OP_ADD; + att_state[0].srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + att_state[0].dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + att_state[0].srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + att_state[0].dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + att_state[1].colorWriteMask = 0x0; + att_state[2].colorWriteMask = 0xf; + att_state[3].colorWriteMask = 0x0; + + // Color blend state + VkPipelineColorBlendStateCreateInfo cb = {}; + cb.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + cb.attachmentCount = _countof(att_state); + cb.pAttachments = att_state; + cb.logicOpEnable = VK_FALSE; + cb.logicOp = VK_LOGIC_OP_NO_OP; + cb.blendConstants[0] = 1.0f; + cb.blendConstants[1] = 1.0f; + cb.blendConstants[2] = 1.0f; + cb.blendConstants[3] = 1.0f; + + VkDynamicState dynamicStateEnables[] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; + VkPipelineDynamicStateCreateInfo dynamicState = {}; + dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + dynamicState.pNext = NULL; + dynamicState.pDynamicStates = dynamicStateEnables; + dynamicState.dynamicStateCount = _countof( dynamicStateEnables ); + + // view port state + VkPipelineViewportStateCreateInfo vp = {}; + vp.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + vp.viewportCount = 1; + vp.scissorCount = 1; + + // depth stencil state + VkPipelineDepthStencilStateCreateInfo ds = {}; + ds.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; + ds.depthTestEnable = VK_TRUE; + ds.depthWriteEnable = VK_FALSE; + ds.depthCompareOp = VK_COMPARE_OP_GREATER_OR_EQUAL; + ds.depthBoundsTestEnable = VK_FALSE; + ds.stencilTestEnable = VK_FALSE; + ds.back.failOp = VK_STENCIL_OP_KEEP; + ds.back.passOp = VK_STENCIL_OP_KEEP; + ds.back.compareOp = VK_COMPARE_OP_ALWAYS; + ds.back.compareMask = 0; + ds.back.reference = 0; + ds.back.depthFailOp = VK_STENCIL_OP_KEEP; + ds.back.writeMask = 0; + ds.minDepthBounds = 0; + ds.maxDepthBounds = 0; + ds.stencilTestEnable = VK_FALSE; + ds.front = ds.back; + + // multi sample state + VkPipelineMultisampleStateCreateInfo ms = {}; + ms.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + + for ( int i = 0; i < NumStreakModes; i++ ) + { + for ( int j = 0; j < NumReactiveModes; j++ ) + { + att_state[2].colorWriteMask = 0x0; + + DefineList defines; + if ( i == StreaksOn ) + defines[ "STREAKS" ] = ""; + + if ( j == ReactiveOn ) + { + defines["REACTIVE"] = ""; + att_state[2].colorWriteMask = 0xf; + } + + // Compile shaders + // + VkPipelineShaderStageCreateInfo vertexShader = {}; + res = VKCompileFromFile(m_pDevice->GetDevice(), VK_SHADER_STAGE_VERTEX_BIT, "ParticleRender.hlsl", "VS_StructuredBuffer", "-T vs_6_0", &defines, &vertexShader ); + assert(res == VK_SUCCESS); + + VkPipelineShaderStageCreateInfo fragmentShader; + res = VKCompileFromFile(m_pDevice->GetDevice(), VK_SHADER_STAGE_FRAGMENT_BIT, "ParticleRender.hlsl", "PS_Billboard", "-T ps_6_0", &defines, &fragmentShader ); + assert(res == VK_SUCCESS); + + VkPipelineShaderStageCreateInfo shaderStages[] = { vertexShader, fragmentShader }; + + // Create pipeline + // + VkGraphicsPipelineCreateInfo pipeline = {}; + pipeline.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + pipeline.layout = m_RasterizationPipelineLayout; + pipeline.pVertexInputState = &vi; + pipeline.pInputAssemblyState = &ia; + pipeline.pRasterizationState = &rs; + pipeline.pMultisampleState = &ms; + pipeline.pColorBlendState = &cb; + pipeline.pDynamicState = &dynamicState; + pipeline.pViewportState = &vp; + pipeline.pDepthStencilState = &ds; + pipeline.pStages = shaderStages; + pipeline.stageCount = _countof( shaderStages ); + pipeline.renderPass = m_renderPass; + + res = vkCreateGraphicsPipelines( m_pDevice->GetDevice(), m_pDevice->GetPipelineCache(), 1, &pipeline, nullptr, &m_RasterizationPipelines[ i ][ j ] ); + assert(res == VK_SUCCESS); + } + } +} + + +void GPUParticleSystem::OnResizedSwapChain( int width, int height, Texture& depthBuffer, VkFramebuffer frameBuffer ) +{ + m_frameBuffer = frameBuffer; + m_ScreenWidth = width; + m_ScreenHeight = height; + m_InvScreenWidth = 1.0f / m_ScreenWidth; + m_InvScreenHeight = 1.0f / m_ScreenHeight; + + m_DepthBuffer = depthBuffer.Resource(); + depthBuffer.CreateSRV( &m_DepthBufferSRV ); + + SetDescriptorSetForDepth( m_pDevice->GetDevice(), 9, m_DepthBufferSRV, nullptr, m_SimulationDescriptorSet ); + SetDescriptorSetForDepth( m_pDevice->GetDevice(), 5, m_DepthBufferSRV, nullptr, m_RasterizationDescriptorSet ); +} + + +void GPUParticleSystem::OnReleasingSwapChain() +{ + if (m_DepthBufferSRV != nullptr) + { + vkDestroyImageView(m_pDevice->GetDevice(), m_DepthBufferSRV, nullptr); + m_DepthBufferSRV = {}; + } +} + + +void GPUParticleSystem::OnDestroyDevice() +{ + m_ParticleBufferA.OnDestroy(); + m_ParticleBufferB.OnDestroy(); + m_PackedViewSpaceParticlePositions.OnDestroy(); + m_MaxRadiusBuffer.OnDestroy(); + m_DeadListBuffer.OnDestroy(); + m_AliveDistanceBuffer.OnDestroy(); + m_AliveIndexBuffer.OnDestroy(); + m_DstAliveDistanceBuffer.OnDestroy(); + m_DstAliveIndexBuffer.OnDestroy(); + m_AliveCountBuffer.OnDestroy(); + vkDestroyImageView( m_pDevice->GetDevice(), m_RandomTextureSRV, nullptr ); + m_RandomTexture.OnDestroy(); + vkDestroyImageView( m_pDevice->GetDevice(), m_AtlasSRV, nullptr ); + m_Atlas.OnDestroy(); + m_IndirectArgsBuffer.OnDestroy(); + + vkDestroyDescriptorSetLayout( m_pDevice->GetDevice(), m_SimulationDescriptorSetLayout, nullptr ); + vkDestroyDescriptorSetLayout( m_pDevice->GetDevice(), m_RasterizationDescriptorSetLayout, nullptr ); + + vkDestroyPipeline( m_pDevice->GetDevice(), m_SimulationPipeline, nullptr ); + vkDestroyPipeline( m_pDevice->GetDevice(), m_ResetParticlesPipeline, nullptr ); + vkDestroyPipeline( m_pDevice->GetDevice(), m_EmitPipeline, nullptr ); + + for ( int i = 0; i < NumStreakModes; i++ ) + { + for ( int j = 0; j < NumReactiveModes; j++ ) + { + vkDestroyPipeline( m_pDevice->GetDevice(), m_RasterizationPipelines[ i ][ j ], nullptr ); + } + } + + vkDestroyPipelineLayout( m_pDevice->GetDevice(), m_SimulationPipelineLayout, nullptr ); + vkDestroyPipelineLayout( m_pDevice->GetDevice(), m_RasterizationPipelineLayout, nullptr ); + + m_SortLib.OnDestroy(); + + for ( int i = 0; i < _countof( m_samplers ); i++ ) + { + vkDestroySampler( m_pDevice->GetDevice(), m_samplers[ i ], nullptr ); + } + + m_ResetSystem = true; + m_pDevice = nullptr; +} + + +// Per-frame emission of particles into the GPU simulation +void GPUParticleSystem::Emit( VkCommandBuffer commandBuffer, DynamicBufferRing& constantBufferRing, uint32_t perFrameConstantOffset, int numEmitters, const EmitterParams* emitters ) +{ + vkCmdBindPipeline( commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, m_EmitPipeline ); + + // Run CS for each emitter + for ( int i = 0; i < numEmitters; i++ ) + { + const EmitterParams& emitter = emitters[ i ]; + + if ( emitter.m_NumToEmit > 0 ) + { + EmitterConstantBuffer* constants = nullptr; + VkDescriptorBufferInfo constantBuffer = {}; + constantBufferRing.AllocConstantBuffer( sizeof(*constants), (void**)&constants, &constantBuffer ); + constants->m_EmitterPosition = emitter.m_Position; + constants->m_EmitterVelocity = emitter.m_Velocity; + constants->m_MaxParticlesThisFrame = emitter.m_NumToEmit; + constants->m_ParticleLifeSpan = emitter.m_ParticleLifeSpan; + constants->m_StartSize = emitter.m_StartSize; + constants->m_EndSize = emitter.m_EndSize; + constants->m_PositionVariance = emitter.m_PositionVariance; + constants->m_VelocityVariance = emitter.m_VelocityVariance; + constants->m_Mass = emitter.m_Mass; + constants->m_Index = i; + constants->m_Streaks = emitter.m_Streaks ? 1 : 0; + constants->m_TextureIndex = emitter.m_TextureIndex; + + uint32_t uniformOffsets[] = { perFrameConstantOffset, (uint32_t)constantBuffer.offset }; + vkCmdBindDescriptorSets( commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, m_SimulationPipelineLayout, 0, 1, &m_SimulationDescriptorSet, _countof( uniformOffsets ), uniformOffsets ); + + // Dispatch enough thread groups to spawn the requested particles + int numThreadGroups = align( emitter.m_NumToEmit, 1024 ) / 1024; + vkCmdDispatch( commandBuffer, numThreadGroups, 1, 1 ); + } + } + + // RaW barriers + m_ParticleBufferA.PipelineBarrier( commandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT ); + m_ParticleBufferB.PipelineBarrier( commandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT ); + m_DeadListBuffer.PipelineBarrier( commandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT ); +} + + +// Per-frame simulation step +void GPUParticleSystem::Simulate( VkCommandBuffer commandBuffer ) +{ + vkCmdBindPipeline( commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, m_SimulationPipeline ); + vkCmdDispatch( commandBuffer, align( g_maxParticles, 256 ) / 256, 1, 1 ); +} + +// Populate a texture with random numbers (used for the emission of particles) +void GPUParticleSystem::FillRandomTexture( UploadHeap& uploadHeap ) +{ + IMG_INFO header = {}; + header.width = 1024; + header.height = 1024; + header.depth = 1; + header.arraySize = 1; + header.mipMapCount = 1; + header.format = DXGI_FORMAT_R32G32B32A32_FLOAT; + header.bitCount = 128; + + float* values = new float[ header.width * header.height * 4 ]; + float* ptr = values; + for ( UINT i = 0; i < header.width * header.height; i++ ) + { + ptr[ 0 ] = RandomVariance( 0.0f, 1.0f ); + ptr[ 1 ] = RandomVariance( 0.0f, 1.0f ); + ptr[ 2 ] = RandomVariance( 0.0f, 1.0f ); + ptr[ 3 ] = RandomVariance( 0.0f, 1.0f ); + ptr += 4; + } + + m_RandomTexture.InitFromData( m_pDevice, uploadHeap, header, values, "RandomTexture" ); + m_RandomTexture.CreateSRV( &m_RandomTextureSRV ); + + delete[] values; +} diff --git a/src/GpuParticles/vk/ParallelSort.cpp b/src/GpuParticles/vk/ParallelSort.cpp new file mode 100644 index 0000000..9f6be1f --- /dev/null +++ b/src/GpuParticles/vk/ParallelSort.cpp @@ -0,0 +1,559 @@ +// ParallelSort.cpp +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#define FFX_CPP +#include "ParallelSort.h" +#include "../../FFX-ParallelSort/FFX_ParallelSort.h" + +static const uint32_t NumKeys = { 400*1024 }; + +////////////////////////////////////////////////////////////////////////// +// Helper for Vulkan +VkBufferMemoryBarrier BufferTransition(VkBuffer buffer, VkAccessFlags before, VkAccessFlags after, uint32_t size) +{ + VkBufferMemoryBarrier bufferBarrier = {}; + bufferBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; + bufferBarrier.srcAccessMask = before; + bufferBarrier.dstAccessMask = after; + bufferBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + bufferBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + bufferBarrier.buffer = buffer; + bufferBarrier.size = size; + + return bufferBarrier; +} + + +void FFXParallelSort::BindConstantBuffer(VkDescriptorBufferInfo& GPUCB, VkDescriptorSet& DescriptorSet, uint32_t Binding/*=0*/, uint32_t Count/*=1*/) +{ + VkWriteDescriptorSet write_set = { VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET }; + write_set.pNext = nullptr; + write_set.dstSet = DescriptorSet; + write_set.dstBinding = Binding; + write_set.dstArrayElement = 0; + write_set.descriptorCount = Count; + write_set.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + write_set.pImageInfo = nullptr; + write_set.pBufferInfo = &GPUCB; + write_set.pTexelBufferView = nullptr; + vkUpdateDescriptorSets(m_pDevice->GetDevice(), 1, &write_set, 0, nullptr); +} + +void FFXParallelSort::BindUAVBuffer(VkBuffer* pBuffer, VkDescriptorSet& DescriptorSet, uint32_t Binding/*=0*/, uint32_t Count/*=1*/) +{ + std::vector bufferInfos; + for (uint32_t i = 0; i < Count; i++) + { + VkDescriptorBufferInfo bufferInfo; + bufferInfo.buffer = pBuffer[i]; + bufferInfo.offset = 0; + bufferInfo.range = VK_WHOLE_SIZE; + bufferInfos.push_back(bufferInfo); + } + + VkWriteDescriptorSet write_set = { VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET }; + write_set.pNext = nullptr; + write_set.dstSet = DescriptorSet; + write_set.dstBinding = Binding; + write_set.dstArrayElement = 0; + write_set.descriptorCount = Count; + write_set.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + write_set.pImageInfo = nullptr; + write_set.pBufferInfo = bufferInfos.data(); + write_set.pTexelBufferView = nullptr; + + vkUpdateDescriptorSets(m_pDevice->GetDevice(), 1, &write_set, 0, nullptr); +} + + +void FFXParallelSort::CompileRadixPipeline(const char* shaderFile, const DefineList* defines, const char* entryPoint, VkPipeline& pPipeline) +{ + std::string CompileFlags("-T cs_6_0"); +#ifdef _DEBUG + CompileFlags += " -Zi -Od"; +#endif // _DEBUG + + VkPipelineShaderStageCreateInfo stage_create_info = { VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO }; + + VkResult vkResult = VKCompileFromFile(m_pDevice->GetDevice(), VK_SHADER_STAGE_COMPUTE_BIT, shaderFile, entryPoint, "-T cs_6_0", defines, &stage_create_info); + stage_create_info.flags = 0; + assert(vkResult == VK_SUCCESS); + + VkComputePipelineCreateInfo create_info = { VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO }; + create_info.pNext = nullptr; + create_info.basePipelineHandle = VK_NULL_HANDLE; + create_info.basePipelineIndex = 0; + create_info.flags = 0; + create_info.layout = m_SortPipelineLayout; + create_info.stage = stage_create_info; + vkResult = vkCreateComputePipelines(m_pDevice->GetDevice(), VK_NULL_HANDLE, 1, &create_info, nullptr, &pPipeline); + assert(vkResult == VK_SUCCESS); +} + +void FFXParallelSort::OnCreate(Device* pDevice, ResourceViewHeaps* pResourceViewHeaps, DynamicBufferRing* pConstantBufferRing, UploadHeap* pUploadHeap, Buffer* elementCount, Buffer* listA, Buffer* listB, Buffer* listA2, Buffer* listB2) +{ + m_pDevice = pDevice; + m_pUploadHeap = pUploadHeap; + m_pResourceViewHeaps = pResourceViewHeaps; + m_pConstantBufferRing = pConstantBufferRing; + m_SrcKeyBuffer = listA; + m_SrcPayloadBuffer = listB; + m_DstKeyBuffer = listA2; + m_DstPayloadBuffer = listB2; + + m_MaxNumThreadgroups = 800; + + VkBufferCreateInfo bufferCreateInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; + bufferCreateInfo.pNext = nullptr; + bufferCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + bufferCreateInfo.usage = VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; // | VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + + VmaAllocationCreateInfo allocCreateInfo = {}; + allocCreateInfo.memoryTypeBits = 0; + allocCreateInfo.pool = VK_NULL_HANDLE; + allocCreateInfo.preferredFlags = 0; + allocCreateInfo.requiredFlags = 0; + allocCreateInfo.usage = VMA_MEMORY_USAGE_UNKNOWN; + + // Allocate the scratch buffers needed for radix sort + FFX_ParallelSort_CalculateScratchResourceSize(NumKeys, m_ScratchBufferSize, m_ReducedScratchBufferSize); + + bufferCreateInfo.size = m_ScratchBufferSize; + allocCreateInfo.pUserData = "Scratch"; + if (VK_SUCCESS != vmaCreateBuffer(m_pDevice->GetAllocator(), &bufferCreateInfo, &allocCreateInfo, &m_FPSScratchBuffer, &m_FPSScratchBufferAllocation, nullptr)) + { + Trace("Failed to create buffer for Scratch"); + } + + bufferCreateInfo.size = m_ReducedScratchBufferSize; + allocCreateInfo.pUserData = "ReducedScratch"; + if (VK_SUCCESS != vmaCreateBuffer(m_pDevice->GetAllocator(), &bufferCreateInfo, &allocCreateInfo, &m_FPSReducedScratchBuffer, &m_FPSReducedScratchBufferAllocation, nullptr)) + { + Trace("Failed to create buffer for ReducedScratch"); + } + + // Allocate the buffers for indirect execution of the algorithm + + bufferCreateInfo.size = sizeof(uint32_t) * 3; + bufferCreateInfo.usage = VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + allocCreateInfo.pUserData = "IndirectCount_Scatter_DispatchArgs"; + if (VK_SUCCESS != vmaCreateBuffer(m_pDevice->GetAllocator(), &bufferCreateInfo, &allocCreateInfo, &m_IndirectCountScatterArgs, &m_IndirectCountScatterArgsAllocation, nullptr)) + { + Trace("Failed to create buffer for IndirectCount_Scatter_DispatchArgs"); + } + + allocCreateInfo.pUserData = "IndirectReduceScanArgs"; + if (VK_SUCCESS != vmaCreateBuffer(m_pDevice->GetAllocator(), &bufferCreateInfo, &allocCreateInfo, &m_IndirectReduceScanArgs, &m_IndirectReduceScanArgsAllocation, nullptr)) + { + Trace("Failed to create buffer for IndirectCount_Scatter_DispatchArgs"); + } + + bufferCreateInfo.size = sizeof(FFX_ParallelSortCB); + bufferCreateInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + allocCreateInfo.pUserData = "IndirectConstantBuffer"; + if (VK_SUCCESS != vmaCreateBuffer(m_pDevice->GetAllocator(), &bufferCreateInfo, &allocCreateInfo, &m_IndirectConstantBuffer, &m_IndirectConstantBufferAllocation, nullptr)) + { + Trace("Failed to create buffer for IndirectConstantBuffer"); + } + + // Create Pipeline layout for Sort pass + { + // Create binding for Radix sort passes + VkDescriptorSetLayoutBinding layout_bindings_set_0[] = { + { 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr } // Constant buffer table + }; + + VkDescriptorSetLayoutBinding layout_bindings_set_1[] = { + { 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr } // Constant buffer to setup indirect params (indirect) + }; + + VkDescriptorSetLayoutBinding layout_bindings_set_InputOutputs[] = { + { 0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr }, // SrcBuffer (sort) + { 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr }, // DstBuffer (sort) + { 2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr }, // ScrPayload (sort only) + { 3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr }, // DstPayload (sort only) + }; + + VkDescriptorSetLayoutBinding layout_bindings_set_Scan[] = { + { 0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr }, // ScanSrc + { 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr }, // ScanDst + { 2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr }, // ScanScratch + }; + + VkDescriptorSetLayoutBinding layout_bindings_set_Scratch[] = { + { 0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr }, // Scratch (sort only) + { 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr }, // Scratch (reduced) + }; + + VkDescriptorSetLayoutBinding layout_bindings_set_Indirect[] = { + { 0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr }, // NumKeys (indirect) + { 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr }, // CBufferUAV (indirect) + { 2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr }, // CountScatterArgs (indirect) + { 3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr } // ReduceScanArgs (indirect) + }; + + VkDescriptorSetLayoutCreateInfo descriptor_set_layout_create_info = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; + descriptor_set_layout_create_info.pNext = nullptr; + descriptor_set_layout_create_info.flags = 0; + descriptor_set_layout_create_info.pBindings = layout_bindings_set_0; + descriptor_set_layout_create_info.bindingCount = 1; + VkResult vkResult = vkCreateDescriptorSetLayout(m_pDevice->GetDevice(), &descriptor_set_layout_create_info, nullptr, &m_SortDescriptorSetLayoutConstants); + assert(vkResult == VK_SUCCESS); + bool bDescriptorAlloc = true; + bDescriptorAlloc &= m_pResourceViewHeaps->AllocDescriptor(m_SortDescriptorSetLayoutConstants, &m_SortDescriptorSetConstants[0]); + bDescriptorAlloc &= m_pResourceViewHeaps->AllocDescriptor(m_SortDescriptorSetLayoutConstants, &m_SortDescriptorSetConstants[1]); + bDescriptorAlloc &= m_pResourceViewHeaps->AllocDescriptor(m_SortDescriptorSetLayoutConstants, &m_SortDescriptorSetConstants[2]); + assert(bDescriptorAlloc == true); + + descriptor_set_layout_create_info.pBindings = layout_bindings_set_1; + descriptor_set_layout_create_info.bindingCount = 1; + vkResult = vkCreateDescriptorSetLayout(m_pDevice->GetDevice(), &descriptor_set_layout_create_info, nullptr, &m_SortDescriptorSetLayoutConstantsIndirect); + assert(vkResult == VK_SUCCESS); + bDescriptorAlloc &= m_pResourceViewHeaps->AllocDescriptor(m_SortDescriptorSetLayoutConstantsIndirect, &m_SortDescriptorSetConstantsIndirect[0]); + bDescriptorAlloc &= m_pResourceViewHeaps->AllocDescriptor(m_SortDescriptorSetLayoutConstantsIndirect, &m_SortDescriptorSetConstantsIndirect[1]); + bDescriptorAlloc &= m_pResourceViewHeaps->AllocDescriptor(m_SortDescriptorSetLayoutConstantsIndirect, &m_SortDescriptorSetConstantsIndirect[2]); + assert(bDescriptorAlloc == true); + + descriptor_set_layout_create_info.pBindings = layout_bindings_set_InputOutputs; + descriptor_set_layout_create_info.bindingCount = 4; + vkResult = vkCreateDescriptorSetLayout(m_pDevice->GetDevice(), &descriptor_set_layout_create_info, nullptr, &m_SortDescriptorSetLayoutInputOutputs); + assert(vkResult == VK_SUCCESS); + bDescriptorAlloc = m_pResourceViewHeaps->AllocDescriptor(m_SortDescriptorSetLayoutInputOutputs, &m_SortDescriptorSetInputOutput[0]); + assert(bDescriptorAlloc == true); + bDescriptorAlloc = m_pResourceViewHeaps->AllocDescriptor(m_SortDescriptorSetLayoutInputOutputs, &m_SortDescriptorSetInputOutput[1]); + assert(bDescriptorAlloc == true); + + descriptor_set_layout_create_info.pBindings = layout_bindings_set_Scan; + descriptor_set_layout_create_info.bindingCount = 3; + vkResult = vkCreateDescriptorSetLayout(m_pDevice->GetDevice(), &descriptor_set_layout_create_info, nullptr, &m_SortDescriptorSetLayoutScan); + assert(vkResult == VK_SUCCESS); + bDescriptorAlloc = m_pResourceViewHeaps->AllocDescriptor(m_SortDescriptorSetLayoutScan, &m_SortDescriptorSetScanSets[0]); + assert(bDescriptorAlloc == true); + bDescriptorAlloc = m_pResourceViewHeaps->AllocDescriptor(m_SortDescriptorSetLayoutScan, &m_SortDescriptorSetScanSets[1]); + assert(bDescriptorAlloc == true); + + descriptor_set_layout_create_info.pBindings = layout_bindings_set_Scratch; + descriptor_set_layout_create_info.bindingCount = 2; + vkResult = vkCreateDescriptorSetLayout(m_pDevice->GetDevice(), &descriptor_set_layout_create_info, nullptr, &m_SortDescriptorSetLayoutScratch); + assert(vkResult == VK_SUCCESS); + bDescriptorAlloc = m_pResourceViewHeaps->AllocDescriptor(m_SortDescriptorSetLayoutScratch, &m_SortDescriptorSetScratch); + assert(bDescriptorAlloc == true); + + descriptor_set_layout_create_info.pBindings = layout_bindings_set_Indirect; + descriptor_set_layout_create_info.bindingCount = 4; + vkResult = vkCreateDescriptorSetLayout(m_pDevice->GetDevice(), &descriptor_set_layout_create_info, nullptr, &m_SortDescriptorSetLayoutIndirect); + assert(vkResult == VK_SUCCESS); + bDescriptorAlloc = m_pResourceViewHeaps->AllocDescriptor(m_SortDescriptorSetLayoutIndirect, &m_SortDescriptorSetIndirect); + assert(bDescriptorAlloc == true); + + // Create constant range representing our static constant + VkPushConstantRange constant_range; + constant_range.stageFlags = VK_SHADER_STAGE_ALL; + constant_range.offset = 0; + constant_range.size = 4; + + // Create the pipeline layout (Root signature) + VkPipelineLayoutCreateInfo layout_create_info = { VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; + layout_create_info.pNext = nullptr; + layout_create_info.flags = 0; + layout_create_info.setLayoutCount = 6; + VkDescriptorSetLayout layouts[] = { m_SortDescriptorSetLayoutConstants, m_SortDescriptorSetLayoutConstantsIndirect, m_SortDescriptorSetLayoutInputOutputs, + m_SortDescriptorSetLayoutScan, m_SortDescriptorSetLayoutScratch, m_SortDescriptorSetLayoutIndirect }; + layout_create_info.pSetLayouts = layouts; + layout_create_info.pushConstantRangeCount = 1; + layout_create_info.pPushConstantRanges = &constant_range; + VkResult bCreatePipelineLayout = vkCreatePipelineLayout(m_pDevice->GetDevice(), &layout_create_info, nullptr, &m_SortPipelineLayout); + assert(bCreatePipelineLayout == VK_SUCCESS); + } + + // Create Pipeline layout for Render of RadixBuffer info + { + // Create binding for Radix sort passes + VkDescriptorSetLayoutBinding layout_bindings_set_0[] = { + { 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr } // Constant buffer table + }; + + VkDescriptorSetLayoutBinding layout_bindings_set_1[] = { + { 0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr } // Sort Buffer + }; + + VkDescriptorSetLayoutBinding layout_bindings_set_2[] = { + { 0, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1, VK_SHADER_STAGE_ALL, nullptr } // ValidationTexture + }; + } + + ////////////////////////////////////////////////////////////////////////// + // Create pipelines for radix sort + { + // Create all of the necessary pipelines for Sort and Scan + DefineList defines; + defines[ "API_VULKAN" ] = ""; + + // SetupIndirectParams (indirect only) + CompileRadixPipeline("ParallelSortCS.hlsl", &defines, "FPS_SetupIndirectParameters", m_FPSIndirectSetupParametersPipeline); + + // Radix count (sum table generation) + CompileRadixPipeline("ParallelSortCS.hlsl", &defines, "FPS_Count", m_FPSCountPipeline); + // Radix count reduce (sum table reduction for offset prescan) + CompileRadixPipeline("ParallelSortCS.hlsl", &defines, "FPS_CountReduce", m_FPSCountReducePipeline); + // Radix scan (prefix scan) + CompileRadixPipeline("ParallelSortCS.hlsl", &defines, "FPS_Scan", m_FPSScanPipeline); + // Radix scan add (prefix scan + reduced prefix scan addition) + CompileRadixPipeline("ParallelSortCS.hlsl", &defines, "FPS_ScanAdd", m_FPSScanAddPipeline); + // Radix scatter (key redistribution) + CompileRadixPipeline("ParallelSortCS.hlsl", &defines, "FPS_Scatter", m_FPSScatterPipeline); + // Radix scatter with payload (key and payload redistribution) + defines["kRS_ValueCopy"] = std::to_string(1); + CompileRadixPipeline("ParallelSortCS.hlsl", &defines, "FPS_Scatter", m_FPSScatterPayloadPipeline); + } + + // Do binding setups + { + VkBuffer BufferMaps[4]; + + // Map inputs/outputs + BufferMaps[0] = m_SrcKeyBuffer->Resource(); + BufferMaps[1] = m_DstKeyBuffer->Resource(); + BufferMaps[2] = m_SrcPayloadBuffer->Resource(); + BufferMaps[3] = m_DstPayloadBuffer->Resource(); + BindUAVBuffer(BufferMaps, m_SortDescriptorSetInputOutput[0], 0, 4); + + BufferMaps[0] = m_DstKeyBuffer->Resource(); + BufferMaps[1] = m_SrcKeyBuffer->Resource(); + BufferMaps[2] = m_DstPayloadBuffer->Resource(); + BufferMaps[3] = m_SrcPayloadBuffer->Resource(); + BindUAVBuffer(BufferMaps, m_SortDescriptorSetInputOutput[1], 0, 4); + + // Map scan sets (reduced, scratch) + BufferMaps[0] = BufferMaps[1] = m_FPSReducedScratchBuffer; + BindUAVBuffer(BufferMaps, m_SortDescriptorSetScanSets[0], 0, 2); + + BufferMaps[0] = BufferMaps[1] = m_FPSScratchBuffer; + BufferMaps[2] = m_FPSReducedScratchBuffer; + BindUAVBuffer(BufferMaps, m_SortDescriptorSetScanSets[1], 0, 3); + + // Map Scratch areas (fixed) + BufferMaps[0] = m_FPSScratchBuffer; + BufferMaps[1] = m_FPSReducedScratchBuffer; + BindUAVBuffer(BufferMaps, m_SortDescriptorSetScratch, 0, 2); + + // Map indirect buffers + elementCount->SetDescriptorSet( 0, m_SortDescriptorSetIndirect, false ); + BufferMaps[0] = m_IndirectConstantBuffer; + BufferMaps[1] = m_IndirectCountScatterArgs; + BufferMaps[2] = m_IndirectReduceScanArgs; + BindUAVBuffer(BufferMaps, m_SortDescriptorSetIndirect, 1, 3); + } +} + +void FFXParallelSort::OnDestroy() +{ + // Release radix sort indirect resources + vmaDestroyBuffer(m_pDevice->GetAllocator(), m_IndirectConstantBuffer, m_IndirectConstantBufferAllocation); + vmaDestroyBuffer(m_pDevice->GetAllocator(), m_IndirectCountScatterArgs, m_IndirectCountScatterArgsAllocation); + vmaDestroyBuffer(m_pDevice->GetAllocator(), m_IndirectReduceScanArgs, m_IndirectReduceScanArgsAllocation); + vkDestroyPipeline(m_pDevice->GetDevice(), m_FPSIndirectSetupParametersPipeline, nullptr); + + // Release radix sort algorithm resources + vmaDestroyBuffer(m_pDevice->GetAllocator(), m_FPSScratchBuffer, m_FPSScratchBufferAllocation); + vmaDestroyBuffer(m_pDevice->GetAllocator(), m_FPSReducedScratchBuffer, m_FPSReducedScratchBufferAllocation); + + vkDestroyPipelineLayout(m_pDevice->GetDevice(), m_SortPipelineLayout, nullptr); + vkDestroyDescriptorSetLayout(m_pDevice->GetDevice(), m_SortDescriptorSetLayoutConstants, nullptr); + m_pResourceViewHeaps->FreeDescriptor(m_SortDescriptorSetConstants[0]); + m_pResourceViewHeaps->FreeDescriptor(m_SortDescriptorSetConstants[1]); + m_pResourceViewHeaps->FreeDescriptor(m_SortDescriptorSetConstants[2]); + vkDestroyDescriptorSetLayout(m_pDevice->GetDevice(), m_SortDescriptorSetLayoutConstantsIndirect, nullptr); + m_pResourceViewHeaps->FreeDescriptor(m_SortDescriptorSetConstantsIndirect[0]); + m_pResourceViewHeaps->FreeDescriptor(m_SortDescriptorSetConstantsIndirect[1]); + m_pResourceViewHeaps->FreeDescriptor(m_SortDescriptorSetConstantsIndirect[2]); + vkDestroyDescriptorSetLayout(m_pDevice->GetDevice(), m_SortDescriptorSetLayoutInputOutputs, nullptr); + m_pResourceViewHeaps->FreeDescriptor(m_SortDescriptorSetInputOutput[0]); + m_pResourceViewHeaps->FreeDescriptor(m_SortDescriptorSetInputOutput[1]); + + vkDestroyDescriptorSetLayout(m_pDevice->GetDevice(), m_SortDescriptorSetLayoutScan, nullptr); + m_pResourceViewHeaps->FreeDescriptor(m_SortDescriptorSetScanSets[0]); + m_pResourceViewHeaps->FreeDescriptor(m_SortDescriptorSetScanSets[1]); + + vkDestroyDescriptorSetLayout(m_pDevice->GetDevice(), m_SortDescriptorSetLayoutScratch, nullptr); + m_pResourceViewHeaps->FreeDescriptor(m_SortDescriptorSetScratch); + + vkDestroyDescriptorSetLayout(m_pDevice->GetDevice(), m_SortDescriptorSetLayoutIndirect, nullptr); + m_pResourceViewHeaps->FreeDescriptor(m_SortDescriptorSetIndirect); + + vkDestroyPipeline(m_pDevice->GetDevice(), m_FPSCountPipeline, nullptr); + vkDestroyPipeline(m_pDevice->GetDevice(), m_FPSCountReducePipeline, nullptr); + vkDestroyPipeline(m_pDevice->GetDevice(), m_FPSScanPipeline, nullptr); + vkDestroyPipeline(m_pDevice->GetDevice(), m_FPSScanAddPipeline, nullptr); + vkDestroyPipeline(m_pDevice->GetDevice(), m_FPSScatterPipeline, nullptr); + vkDestroyPipeline(m_pDevice->GetDevice(), m_FPSScatterPayloadPipeline, nullptr); +} + + +void FFXParallelSort::Draw(VkCommandBuffer commandList) +{ + // To control which descriptor set to use for updating data + static uint32_t frameCount = 0; + uint32_t frameConstants = (++frameCount) % 3; + + std::string markerText = "FFXParallelSortIndirect"; + SetPerfMarkerBegin(commandList, markerText.c_str()); + + // Buffers to ping-pong between when writing out sorted values + VkBuffer* ReadBufferInfo = &m_SrcKeyBuffer->Resource(); + VkBuffer* WriteBufferInfo(&m_DstKeyBuffer->Resource()); + VkBuffer* ReadPayloadBufferInfo(&m_SrcPayloadBuffer->Resource()), * WritePayloadBufferInfo(&m_DstPayloadBuffer->Resource()); + bool bHasPayload = true; + + // Setup barriers for the run + VkBufferMemoryBarrier Barriers[3]; + + // Fill in the constant buffer data structure (this will be done by a shader in the indirect version) + { + struct SetupIndirectCB + { + uint32_t MaxThreadGroups; + }; + SetupIndirectCB IndirectSetupCB; + IndirectSetupCB.MaxThreadGroups = m_MaxNumThreadgroups; + + // Copy the data into the constant buffer + VkDescriptorBufferInfo constantBuffer = m_pConstantBufferRing->AllocConstantBuffer(sizeof(SetupIndirectCB), (void*)&IndirectSetupCB); + BindConstantBuffer(constantBuffer, m_SortDescriptorSetConstantsIndirect[frameConstants]); + + // Dispatch + vkCmdBindDescriptorSets(commandList, VK_PIPELINE_BIND_POINT_COMPUTE, m_SortPipelineLayout, 1, 1, &m_SortDescriptorSetConstantsIndirect[frameConstants], 0, nullptr); + vkCmdBindDescriptorSets(commandList, VK_PIPELINE_BIND_POINT_COMPUTE, m_SortPipelineLayout, 5, 1, &m_SortDescriptorSetIndirect, 0, nullptr); + vkCmdBindPipeline(commandList, VK_PIPELINE_BIND_POINT_COMPUTE, m_FPSIndirectSetupParametersPipeline); + vkCmdDispatch(commandList, 1, 1, 1); + + // When done, transition the args buffers to INDIRECT_ARGUMENT, and the constant buffer UAV to Constant buffer + VkBufferMemoryBarrier barriers[5]; + barriers[0] = BufferTransition(m_IndirectCountScatterArgs, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, sizeof(uint32_t) * 3); + barriers[1] = BufferTransition(m_IndirectReduceScanArgs, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, sizeof(uint32_t) * 3); + barriers[2] = BufferTransition(m_IndirectConstantBuffer, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, sizeof(FFX_ParallelSortCB)); + barriers[3] = BufferTransition(m_IndirectCountScatterArgs, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_INDIRECT_COMMAND_READ_BIT, sizeof(uint32_t) * 3); + barriers[4] = BufferTransition(m_IndirectReduceScanArgs, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_INDIRECT_COMMAND_READ_BIT, sizeof(uint32_t) * 3); + vkCmdPipelineBarrier(commandList, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 5, barriers, 0, nullptr); + } + + // Bind the scratch descriptor sets + vkCmdBindDescriptorSets(commandList, VK_PIPELINE_BIND_POINT_COMPUTE, m_SortPipelineLayout, 4, 1, &m_SortDescriptorSetScratch, 0, nullptr); + + // Copy the data into the constant buffer and bind + { + //constantBuffer = m_IndirectConstantBuffer.GetResource()->GetGPUVirtualAddress(); + VkDescriptorBufferInfo constantBuffer; + constantBuffer.buffer = m_IndirectConstantBuffer; + constantBuffer.offset = 0; + constantBuffer.range = VK_WHOLE_SIZE; + BindConstantBuffer(constantBuffer, m_SortDescriptorSetConstants[frameConstants]); + } + + // Bind constants + vkCmdBindDescriptorSets(commandList, VK_PIPELINE_BIND_POINT_COMPUTE, m_SortPipelineLayout, 0, 1, &m_SortDescriptorSetConstants[frameConstants], 0, nullptr); + + // Perform Radix Sort (currently only support 32-bit key/payload sorting + uint32_t inputSet = 0; + for (uint32_t Shift = 0; Shift < 32u; Shift += FFX_PARALLELSORT_SORT_BITS_PER_PASS) + { + // Update the bit shift + vkCmdPushConstants(commandList, m_SortPipelineLayout, VK_SHADER_STAGE_ALL, 0, 4, &Shift); + + // Bind input/output for this pass + vkCmdBindDescriptorSets(commandList, VK_PIPELINE_BIND_POINT_COMPUTE, m_SortPipelineLayout, 2, 1, &m_SortDescriptorSetInputOutput[inputSet], 0, nullptr); + + // Sort Count + { + vkCmdBindPipeline(commandList, VK_PIPELINE_BIND_POINT_COMPUTE, m_FPSCountPipeline); + + vkCmdDispatchIndirect(commandList, m_IndirectCountScatterArgs, 0); + } + + // UAV barrier on the sum table + Barriers[0] = BufferTransition(m_FPSScratchBuffer, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, m_ScratchBufferSize); + vkCmdPipelineBarrier(commandList, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 1, Barriers, 0, nullptr); + + // Sort Reduce + { + vkCmdBindPipeline(commandList, VK_PIPELINE_BIND_POINT_COMPUTE, m_FPSCountReducePipeline); + + vkCmdDispatchIndirect(commandList, m_IndirectReduceScanArgs, 0); + + // UAV barrier on the reduced sum table + Barriers[0] = BufferTransition(m_FPSReducedScratchBuffer, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, m_ReducedScratchBufferSize); + vkCmdPipelineBarrier(commandList, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 1, Barriers, 0, nullptr); + } + + // Sort Scan + { + // First do scan prefix of reduced values + vkCmdBindDescriptorSets(commandList, VK_PIPELINE_BIND_POINT_COMPUTE, m_SortPipelineLayout, 3, 1, &m_SortDescriptorSetScanSets[0], 0, nullptr); + vkCmdBindPipeline(commandList, VK_PIPELINE_BIND_POINT_COMPUTE, m_FPSScanPipeline); + + vkCmdDispatch(commandList, 1, 1, 1); + + // UAV barrier on the reduced sum table + Barriers[0] = BufferTransition(m_FPSReducedScratchBuffer, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, m_ReducedScratchBufferSize); + vkCmdPipelineBarrier(commandList, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 1, Barriers, 0, nullptr); + + // Next do scan prefix on the histogram with partial sums that we just did + vkCmdBindDescriptorSets(commandList, VK_PIPELINE_BIND_POINT_COMPUTE, m_SortPipelineLayout, 3, 1, &m_SortDescriptorSetScanSets[1], 0, nullptr); + + vkCmdBindPipeline(commandList, VK_PIPELINE_BIND_POINT_COMPUTE, m_FPSScanAddPipeline); + vkCmdDispatchIndirect(commandList, m_IndirectReduceScanArgs, 0); + } + + // UAV barrier on the sum table + Barriers[0] = BufferTransition(m_FPSScratchBuffer, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, m_ScratchBufferSize); + vkCmdPipelineBarrier(commandList, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 1, Barriers, 0, nullptr); + + // Sort Scatter + { + vkCmdBindPipeline(commandList, VK_PIPELINE_BIND_POINT_COMPUTE, bHasPayload ? m_FPSScatterPayloadPipeline : m_FPSScatterPipeline); + + vkCmdDispatchIndirect(commandList, m_IndirectCountScatterArgs, 0); + } + + // Finish doing everything and barrier for the next pass + int numBarriers = 0; + Barriers[numBarriers++] = BufferTransition(*WriteBufferInfo, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, sizeof(uint32_t) * NumKeys); + if (bHasPayload) + Barriers[numBarriers++] = BufferTransition(*WritePayloadBufferInfo, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, sizeof(uint32_t) * NumKeys); + vkCmdPipelineBarrier(commandList, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, numBarriers, Barriers, 0, nullptr); + + // Swap read/write sources + std::swap(ReadBufferInfo, WriteBufferInfo); + if (bHasPayload) + std::swap(ReadPayloadBufferInfo, WritePayloadBufferInfo); + inputSet = !inputSet; + } + + // When we are all done, transition indirect buffers back to UAV for the next frame (if doing indirect dispatch) + { + VkBufferMemoryBarrier barriers[3]; + barriers[0] = BufferTransition(m_IndirectConstantBuffer, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, sizeof(FFX_ParallelSortCB)); + barriers[1] = BufferTransition(m_IndirectCountScatterArgs, VK_ACCESS_INDIRECT_COMMAND_READ_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, sizeof(uint32_t) * 3); + barriers[2] = BufferTransition(m_IndirectReduceScanArgs, VK_ACCESS_INDIRECT_COMMAND_READ_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, sizeof(uint32_t) * 3); + vkCmdPipelineBarrier(commandList, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 3, barriers, 0, nullptr); + } + + // Close out the perf capture + SetPerfMarkerEnd(commandList); +} diff --git a/src/GpuParticles/vk/ParallelSort.h b/src/GpuParticles/vk/ParallelSort.h new file mode 100644 index 0000000..37c0cd0 --- /dev/null +++ b/src/GpuParticles/vk/ParallelSort.h @@ -0,0 +1,101 @@ +// ParallelSort.h +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#pragma once +#include "../vk/stdafx.h" +#include "BufferHelper.h" + + +struct ParallelSortRenderCB // If you change this, also change struct ParallelSortRenderCB in ParallelSortVerify.hlsl +{ + int32_t Width; + int32_t Height; + int32_t SortWidth; + int32_t SortHeight; +}; + + +class FFXParallelSort +{ +public: + void OnCreate(Device* pDevice, ResourceViewHeaps* pResourceViewHeaps, DynamicBufferRing* pConstantBufferRing, UploadHeap* pUploadHeap, Buffer* elementCount, Buffer* listA, Buffer* listB, Buffer* listA2, Buffer* listB2); + void OnDestroy(); + + void Draw(VkCommandBuffer commandList); + +private: + void CompileRadixPipeline(const char* shaderFile, const DefineList* defines, const char* entryPoint, VkPipeline& pPipeline); + void BindConstantBuffer(VkDescriptorBufferInfo& GPUCB, VkDescriptorSet& DescriptorSet, uint32_t Binding = 0, uint32_t Count = 1); + void BindUAVBuffer(VkBuffer* pBuffer, VkDescriptorSet& DescriptorSet, uint32_t Binding = 0, uint32_t Count = 1); + + + Device* m_pDevice = nullptr; + UploadHeap* m_pUploadHeap = nullptr; + ResourceViewHeaps* m_pResourceViewHeaps = nullptr; + DynamicBufferRing* m_pConstantBufferRing = nullptr; + uint32_t m_MaxNumThreadgroups = 800; + + uint32_t m_ScratchBufferSize; + uint32_t m_ReducedScratchBufferSize; + + Buffer* m_SrcKeyBuffer = nullptr; + Buffer* m_SrcPayloadBuffer = nullptr; + + Buffer* m_DstKeyBuffer = nullptr; + Buffer* m_DstPayloadBuffer = nullptr; + + VkBuffer m_FPSScratchBuffer; // Sort scratch buffer + VmaAllocation m_FPSScratchBufferAllocation; + + VkBuffer m_FPSReducedScratchBuffer; // Sort reduced scratch buffer + VmaAllocation m_FPSReducedScratchBufferAllocation; + + VkDescriptorSetLayout m_SortDescriptorSetLayoutConstants; + VkDescriptorSet m_SortDescriptorSetConstants[3]; + VkDescriptorSetLayout m_SortDescriptorSetLayoutConstantsIndirect; + VkDescriptorSet m_SortDescriptorSetConstantsIndirect[3]; + + VkDescriptorSetLayout m_SortDescriptorSetLayoutInputOutputs; + VkDescriptorSetLayout m_SortDescriptorSetLayoutScan; + VkDescriptorSetLayout m_SortDescriptorSetLayoutScratch; + VkDescriptorSetLayout m_SortDescriptorSetLayoutIndirect; + + VkDescriptorSet m_SortDescriptorSetInputOutput[2]; + VkDescriptorSet m_SortDescriptorSetScanSets[2]; + VkDescriptorSet m_SortDescriptorSetScratch; + VkDescriptorSet m_SortDescriptorSetIndirect; + VkPipelineLayout m_SortPipelineLayout; + + VkPipeline m_FPSCountPipeline; + VkPipeline m_FPSCountReducePipeline; + VkPipeline m_FPSScanPipeline; + VkPipeline m_FPSScanAddPipeline; + VkPipeline m_FPSScatterPipeline; + VkPipeline m_FPSScatterPayloadPipeline; + + // Resources for indirect execution of algorithm + VkBuffer m_IndirectConstantBuffer; // Buffer to hold radix sort constant buffer data for indirect dispatch + VmaAllocation m_IndirectConstantBufferAllocation; + VkBuffer m_IndirectCountScatterArgs; // Buffer to hold dispatch arguments used for Count/Scatter parts of the algorithm + VmaAllocation m_IndirectCountScatterArgsAllocation; + VkBuffer m_IndirectReduceScanArgs; // Buffer to hold dispatch arguments used for Reduce/Scan parts of the algorithm + VmaAllocation m_IndirectReduceScanArgsAllocation; + + VkPipeline m_FPSIndirectSetupParametersPipeline; +}; \ No newline at end of file diff --git a/src/VK/AnimatedTexture.cpp b/src/VK/AnimatedTexture.cpp new file mode 100644 index 0000000..e34e68c --- /dev/null +++ b/src/VK/AnimatedTexture.cpp @@ -0,0 +1,294 @@ +// FidelityFX Super Resolution Sample +// +// Copyright (c) 2022 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +#include "AnimatedTexture.h" + + +struct ConstantBuffer +{ + math::Matrix4 currentViewProj; + math::Matrix4 previousViewProj; + float jitterCompensation[ 2 ]; + float scrollFactor; + float rotationFactor; + int mode; + int pads[3]; +}; + + +void AnimatedTextures::OnCreate( Device& device, UploadHeap& uploadHeap, StaticBufferPool& bufferPool, VkRenderPass renderPass, ResourceViewHeaps& resourceViewHeaps, DynamicBufferRing& constantBufferRing ) +{ + m_pDevice = &device; + m_constantBufferRing = &constantBufferRing; + + VkSamplerCreateInfo sampler = {}; + sampler.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + sampler.magFilter = VK_FILTER_LINEAR; + sampler.minFilter = VK_FILTER_LINEAR; + sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + sampler.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; + sampler.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; + sampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; + sampler.minLod = -1000; + sampler.maxLod = 1000; + sampler.maxAnisotropy = 16.0f; + VkResult res = vkCreateSampler( device.GetDevice(), &sampler, nullptr, &m_sampler); + assert(res == VK_SUCCESS); + + // Compile shaders + // + DefineList attributeDefines; + + VkPipelineShaderStageCreateInfo vertexShader; + res = VKCompileFromFile(m_pDevice->GetDevice(), VK_SHADER_STAGE_VERTEX_BIT, "AnimatedTexture.hlsl", "VSMain", "-T vs_6_0", &attributeDefines, &vertexShader); + assert(res == VK_SUCCESS); + + VkPipelineShaderStageCreateInfo fragmentShader; + res = VKCompileFromFile(m_pDevice->GetDevice(), VK_SHADER_STAGE_FRAGMENT_BIT, "AnimatedTexture.hlsl", "PSMain", "-T ps_6_0", &attributeDefines, &fragmentShader); + assert(res == VK_SUCCESS); + + std::vector shaderStages; + shaderStages.push_back(vertexShader); + shaderStages.push_back(fragmentShader); + + std::vector layoutBindings(3); + layoutBindings[0].binding = 0; + layoutBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; + layoutBindings[0].descriptorCount = 1; + layoutBindings[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + layoutBindings[0].pImmutableSamplers = nullptr; + + layoutBindings[1].binding = 1; + layoutBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + layoutBindings[1].descriptorCount = 1; + layoutBindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + layoutBindings[1].pImmutableSamplers = nullptr; + + layoutBindings[2].binding = 2; + layoutBindings[2].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER; + layoutBindings[2].descriptorCount = 1; + layoutBindings[2].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + layoutBindings[2].pImmutableSamplers = &m_sampler; + + for (int i = 0; i < _countof(m_descriptorSets);i++) + { + resourceViewHeaps.CreateDescriptorSetLayoutAndAllocDescriptorSet( &layoutBindings, &m_descriptorSetLayout, &m_descriptorSets[i] ); + constantBufferRing.SetDescriptorSet( 0, sizeof( ConstantBuffer ), m_descriptorSets[i] ); + } + + // Create pipeline layout + // + VkPipelineLayoutCreateInfo pPipelineLayoutCreateInfo = {}; + pPipelineLayoutCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + pPipelineLayoutCreateInfo.setLayoutCount = 1; + pPipelineLayoutCreateInfo.pSetLayouts = &m_descriptorSetLayout; + + res = vkCreatePipelineLayout(m_pDevice->GetDevice(), &pPipelineLayoutCreateInfo, NULL, &m_pipelineLayout); + assert(res == VK_SUCCESS); + + VkPipelineVertexInputStateCreateInfo vi = {}; + vi.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + + VkPipelineInputAssemblyStateCreateInfo ia = {}; + ia.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + + // rasterizer state + VkPipelineRasterizationStateCreateInfo rs = {}; + rs.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rs.polygonMode = VK_POLYGON_MODE_FILL; + rs.cullMode = VK_CULL_MODE_NONE; + rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + rs.lineWidth = 1.0f; + + VkPipelineColorBlendAttachmentState att_state[4] = {}; + att_state[0].colorWriteMask = 0xf; + att_state[0].blendEnable = VK_FALSE; + att_state[0].alphaBlendOp = VK_BLEND_OP_ADD; + att_state[0].colorBlendOp = VK_BLEND_OP_ADD; + att_state[0].srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + att_state[0].dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + att_state[0].srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + att_state[0].dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + + att_state[1].colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT; + att_state[2].colorWriteMask = 0x0; + att_state[3].colorWriteMask = VK_COLOR_COMPONENT_R_BIT; + + // Color blend state + + VkPipelineColorBlendStateCreateInfo cb = {}; + cb.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + cb.attachmentCount = _countof(att_state); + cb.pAttachments = att_state; + cb.logicOpEnable = VK_FALSE; + cb.logicOp = VK_LOGIC_OP_NO_OP; + cb.blendConstants[0] = 1.0f; + cb.blendConstants[1] = 1.0f; + cb.blendConstants[2] = 1.0f; + cb.blendConstants[3] = 1.0f; + + std::vector dynamicStateEnables = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR, + VK_DYNAMIC_STATE_BLEND_CONSTANTS + }; + VkPipelineDynamicStateCreateInfo dynamicState = {}; + dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + dynamicState.pNext = NULL; + dynamicState.pDynamicStates = dynamicStateEnables.data(); + dynamicState.dynamicStateCount = (uint32_t)dynamicStateEnables.size(); + + // view port state + + VkPipelineViewportStateCreateInfo vp = {}; + vp.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + vp.viewportCount = 1; + vp.scissorCount = 1; + + // depth stencil state + + VkPipelineDepthStencilStateCreateInfo ds = {}; + ds.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; + ds.depthTestEnable = VK_TRUE; + ds.depthWriteEnable = VK_TRUE; + ds.depthCompareOp = VK_COMPARE_OP_GREATER_OR_EQUAL; + ds.back.failOp = VK_STENCIL_OP_KEEP; + ds.back.passOp = VK_STENCIL_OP_KEEP; + ds.back.compareOp = VK_COMPARE_OP_ALWAYS; + ds.back.depthFailOp = VK_STENCIL_OP_KEEP; + ds.front = ds.back; + + // multi sample state + + VkPipelineMultisampleStateCreateInfo ms = {}; + ms.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + + // create pipeline + + VkGraphicsPipelineCreateInfo pipeline = {}; + pipeline.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + pipeline.layout = m_pipelineLayout; + pipeline.pVertexInputState = &vi; + pipeline.pInputAssemblyState = &ia; + pipeline.pRasterizationState = &rs; + pipeline.pColorBlendState = &cb; + pipeline.pMultisampleState = &ms; + pipeline.pDynamicState = &dynamicState; + pipeline.pViewportState = &vp; + pipeline.pDepthStencilState = &ds; + pipeline.pStages = shaderStages.data(); + pipeline.stageCount = (uint32_t)shaderStages.size(); + pipeline.renderPass = renderPass; + pipeline.subpass = 0; + + res = vkCreateGraphicsPipelines(m_pDevice->GetDevice(), device.GetPipelineCache(), 1, &pipeline, NULL, &m_pipelines[0]); + assert(res == VK_SUCCESS); + SetResourceName(m_pDevice->GetDevice(), VK_OBJECT_TYPE_PIPELINE, (uint64_t)m_pipelines[0], "AT pipeline with comp"); + + att_state[3].colorWriteMask = 0; + res = vkCreateGraphicsPipelines(m_pDevice->GetDevice(), device.GetPipelineCache(), 1, &pipeline, NULL, &m_pipelines[1]); + assert(res == VK_SUCCESS); + SetResourceName(m_pDevice->GetDevice(), VK_OBJECT_TYPE_PIPELINE, (uint64_t)m_pipelines[1], "AT pipeline no comp"); + + UINT indices[6] = { 0, 1, 2, 2, 1, 3 }; + bufferPool.AllocBuffer( _countof( indices ), sizeof( UINT ), indices, &m_indexBuffer ); + + m_textures[0].InitFromFile( &device, &uploadHeap, "..\\media\\lion.jpg", true ); + m_textures[1].InitFromFile( &device, &uploadHeap, "..\\media\\checkerboard.dds", true ); + m_textures[2].InitFromFile( &device, &uploadHeap, "..\\media\\composition_text.dds", true ); + + for ( int i = 0; i < _countof( m_textures ); i++ ) + { + m_textures[ i ].CreateSRV( &m_textureSRVs[i] ); + SetDescriptorSet( m_pDevice->GetDevice(), 1, m_textureSRVs[i], nullptr, m_descriptorSets[i] ); + } +} + + +void AnimatedTextures::OnDestroy() +{ + vkDestroySampler(m_pDevice->GetDevice(), m_sampler, nullptr); + m_sampler = VK_NULL_HANDLE; + + for ( int i = 0; i < _countof( m_textures ); i++ ) + { + vkDestroyImageView(m_pDevice->GetDevice(), m_textureSRVs[i], nullptr); + m_textureSRVs[i] = VK_NULL_HANDLE; + + m_textures[i].OnDestroy(); + } + + for ( int i = 0; i < _countof( m_pipelines ); i++ ) + { + vkDestroyPipeline( m_pDevice->GetDevice(), m_pipelines[i], nullptr ); + m_pipelines[i] = VK_NULL_HANDLE; + } + + vkDestroyPipelineLayout( m_pDevice->GetDevice(), m_pipelineLayout, nullptr ); + m_pipelineLayout = VK_NULL_HANDLE; + + vkDestroyDescriptorSetLayout( m_pDevice->GetDevice(), m_descriptorSetLayout, nullptr ); + m_descriptorSetLayout = VK_NULL_HANDLE; + + m_pDevice = nullptr; +} + + +void AnimatedTextures::Render( VkCommandBuffer commandList, float frameTime, float speed, bool compositionMask, const Camera& camera ) +{ + m_scrollFactor += frameTime * 1.0f * speed; + m_rotationFactor += frameTime * 2.0f * speed; + m_flipTimer += frameTime * 1.0f; + + if ( m_scrollFactor > 10.0f ) + m_scrollFactor -= 10.0f; + + const float twoPI = 6.283185307179586476925286766559f; + + if ( m_rotationFactor > twoPI ) + m_rotationFactor -= twoPI; + + int textureIndex = min( (int)floorf( m_flipTimer * 0.33333f ), _countof( m_textures ) - 1 ); + if ( m_flipTimer > 9.0f ) + m_flipTimer = 0.0f; + + VkDescriptorBufferInfo cb = {}; + ConstantBuffer* constantBuffer = nullptr; + m_constantBufferRing->AllocConstantBuffer( sizeof(*constantBuffer), (void**)&constantBuffer, &cb ); + + constantBuffer->currentViewProj = camera.GetProjection() * camera.GetView(); + constantBuffer->previousViewProj = camera.GetPrevProjection() * camera.GetPrevView(); + + constantBuffer->jitterCompensation[0] = camera.GetPrevProjection().getCol2().getX() - camera.GetProjection().getCol2().getX(); + constantBuffer->jitterCompensation[1] = camera.GetPrevProjection().getCol2().getY() - camera.GetProjection().getCol2().getY(); + constantBuffer->scrollFactor = m_scrollFactor; + constantBuffer->rotationFactor = m_rotationFactor; + constantBuffer->mode = textureIndex; + + uint32_t uniformOffsets[] = { (uint32_t)cb.offset }; + vkCmdBindDescriptorSets( commandList, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipelineLayout, 0, 1, &m_descriptorSets[textureIndex], _countof( uniformOffsets ), uniformOffsets ); + vkCmdBindPipeline( commandList, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipelines[compositionMask ? 0 : 1] ); + vkCmdBindIndexBuffer( commandList, m_indexBuffer.buffer, m_indexBuffer.offset, VK_INDEX_TYPE_UINT32 ); + vkCmdDrawIndexed( commandList, 6, 2, 0, 0, 0 ); +} diff --git a/src/VK/AnimatedTexture.h b/src/VK/AnimatedTexture.h new file mode 100644 index 0000000..a4f5e18 --- /dev/null +++ b/src/VK/AnimatedTexture.h @@ -0,0 +1,57 @@ +// FidelityFX Super Resolution Sample +// +// Copyright (c) 2022 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +#pragma once +#include "stdafx.h" + + +class AnimatedTextures +{ +public: + + AnimatedTextures() {} + virtual ~AnimatedTextures() {} + + void OnCreate( Device& device, UploadHeap& uploadHeap, StaticBufferPool& bufferPool, VkRenderPass renderPass, ResourceViewHeaps& resourceViewHeaps, DynamicBufferRing& constantBufferRing ); + void OnDestroy(); + + void Render( VkCommandBuffer commandList, float frameTime, float speed, bool compositionMask, const Camera& camera ); + +private: + + Device* m_pDevice = nullptr; + DynamicBufferRing* m_constantBufferRing = nullptr; + + VkDescriptorSetLayout m_descriptorSetLayout = VK_NULL_HANDLE; + VkDescriptorSet m_descriptorSets[3] = {}; + VkPipelineLayout m_pipelineLayout = VK_NULL_HANDLE; + VkPipeline m_pipelines[2] = {}; + VkDescriptorBufferInfo m_indexBuffer = {}; + + Texture m_textures[3] = {}; + VkImageView m_textureSRVs[3] = {}; + VkSampler m_sampler = VK_NULL_HANDLE; + + float m_scrollFactor = 0.0f; + float m_rotationFactor = 0.0f; + float m_flipTimer = 0.0f; +}; \ No newline at end of file diff --git a/src/VK/AnimatedTexture.hlsl b/src/VK/AnimatedTexture.hlsl new file mode 100644 index 0000000..2dfada8 --- /dev/null +++ b/src/VK/AnimatedTexture.hlsl @@ -0,0 +1,128 @@ +// Copyright (c) 2022 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +[[vk::binding( 0, 0 )]] cbuffer cb : register(b0) +{ + matrix g_CurrentViewProjection; + matrix g_PreviousViewProjection; + float2 g_CameraJitterCompensation; + float g_ScrollFactor; + float g_RotationFactor; + int g_Mode; + int pad0; + int pad1; + int pad2; +} + + +[[vk::binding( 1, 0 )]] Texture2D g_Texture : register(t0); +[[vk::binding( 2, 0 )]] SamplerState g_Sampler : register(s0); + +struct VERTEX_OUT +{ + float4 CurrentPosition : TEXCOORD0; + float4 PreviousPosition : TEXCOORD1; + float3 TexCoord : TEXCOORD2; + float4 Position : SV_POSITION; +}; + + +VERTEX_OUT VSMain( uint vertexId : SV_VertexID, uint instanceId : SV_InstanceID ) +{ + VERTEX_OUT output = (VERTEX_OUT)0; + + const float2 offsets[ 4 ] = + { + float2( -1, 1 ), + float2( 1, 1 ), + float2( -1, -1 ), + float2( 1, -1 ), + }; + + float2 offset = offsets[ vertexId ]; + float2 uv = (offset+1)*float2( instanceId == 0 ? -0.5 : 0.5, -0.5 ); + + float4 worldPos = float4( offsets[ vertexId ], 0.0, 1.0 ); + + worldPos.xyz += instanceId == 0 ? float3( -13, 1.5, 2 ) : float3( -13, 1.5, -2 ); + + output.CurrentPosition = mul( g_CurrentViewProjection, worldPos ); + output.PreviousPosition = mul( g_PreviousViewProjection, worldPos ); + + output.Position = output.CurrentPosition; + + output.TexCoord.xy = uv; + output.TexCoord.z = instanceId; + + return output; +} + +struct Output +{ + float4 finalColor : SV_TARGET0; + float2 motionVectors : SV_TARGET1; + float upscaleReactive : SV_TARGET2; + float upscaleTransparencyAndComposition : SV_TARGET3; +}; + + +float4 TextureLookup( int billboardIndex, float2 uv0 ) +{ + float4 color = 1; + + if ( billboardIndex == 0 || g_Mode == 2 ) + { + // Scrolling + float2 uv = uv0; + if ( g_Mode == 2 ) + uv += float2( -g_ScrollFactor, 0.0 ); + else + uv += float2( -g_ScrollFactor, 0.5*g_ScrollFactor ); + + color.rgb = g_Texture.SampleLevel( g_Sampler, uv, 0 ).rgb; + } + else if ( billboardIndex == 1 ) + { + // Rotated UVs + float s, c; + sincos( g_RotationFactor, s, c ); + float2x2 rotation = { float2( c, s ), float2( -s, c ) }; + + float2 rotatedUV = mul( rotation, uv0-float2( 0.5, -0.5) ); + color.rgb = g_Texture.SampleLevel( g_Sampler, rotatedUV, 0 ).rgb; + } + + return color; +} + + +Output PSMain( VERTEX_OUT input ) +{ + Output output = (Output)0; + + output.finalColor = TextureLookup( (int)input.TexCoord.z, input.TexCoord.xy ); + + output.motionVectors = (input.PreviousPosition.xy / input.PreviousPosition.w) - (input.CurrentPosition.xy / input.CurrentPosition.w) + g_CameraJitterCompensation; + output.motionVectors *= float2(0.5f, -0.5f); + + output.upscaleReactive = 0; // Nothing to write to the reactice mask. Color writes are off on this target anyway. + output.upscaleTransparencyAndComposition = 1; // Write a value into here to indicate the depth and motion vectors are as expected for a static object, but the surface contents are changing. + + return output; +} \ No newline at end of file diff --git a/src/VK/CMakeLists.txt b/src/VK/CMakeLists.txt index 720e3b8..a8d8b60 100644 --- a/src/VK/CMakeLists.txt +++ b/src/VK/CMakeLists.txt @@ -38,6 +38,15 @@ set(sources stdafx.h UI.cpp UI.h + ../GpuParticles/ParticleHelpers.h + ../GpuParticles/ParticleSystem.h + ../GpuParticles/ParticleSystemInternal.h + ../GpuParticles/vk/BufferHelper.h + ../GpuParticles/vk/GPUParticleSystem.cpp + ../GpuParticles/vk/ParallelSort.h + ../GpuParticles/vk/ParallelSort.cpp + AnimatedTexture.cpp + AnimatedTexture.h dpiawarescaling.manifest) set(fsr1_shaders_src @@ -94,7 +103,20 @@ set(fsr2_shaders_src ${CMAKE_CURRENT_SOURCE_DIR}/../ffx-fsr2-api/shaders/ffx_fsr2_rcas_pass.glsl) set(particle_shaders_src + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/ParticleStructs.h + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/ParticleHelpers.h + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/fp16util.h + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/ParallelSortCS.hlsl + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/ParticleEmit.hlsl + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/ParticleRender.hlsl + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/ParticleSimulation.hlsl + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/ShaderConstants.h + ${CMAKE_CURRENT_SOURCE_DIR}/../GpuParticleShaders/SimulationBindings.h + ${CMAKE_CURRENT_SOURCE_DIR}/../ffx-parallelsort/FFX_ParallelSort.h) + +set(sample_shaders_src ${CMAKE_CURRENT_SOURCE_DIR}/GPUFrameRateLimiter.hlsl + ${CMAKE_CURRENT_SOURCE_DIR}/AnimatedTexture.hlsl ${CMAKE_CURRENT_SOURCE_DIR}/DebugBlit.hlsl ${CMAKE_CURRENT_SOURCE_DIR}/UpscaleSpatial.hlsl ${CMAKE_CURRENT_SOURCE_DIR}/FSRPass.hlsl) @@ -104,12 +126,14 @@ set(APP_ICON_GPUOPEN "${CMAKE_CURRENT_SOURCE_DIR}/../common/GpuOpenIcon.rc") source_group("sources" FILES ${sources}) source_group("spatial_shaders" FILES ${fsr1_shaders_src}) source_group("fsr2_shaders" FILES ${fsr2_shaders_src}) +source_group("particle_shaders" FILES ${particle_shaders_src}) source_group("sample_shaders" FILES ${sample_shaders_src}) copyCommand("${spd_shaders_src}" ${CMAKE_HOME_DIRECTORY}/bin/ShaderLibVK) copyCommand("${fsr1_shaders_src}" ${CMAKE_HOME_DIRECTORY}/bin/ShaderLibVK) copyCommand("${fsr2_shaders_src}" ${CMAKE_HOME_DIRECTORY}/bin/ShaderLibVK) copyCommand("${particle_shaders_src}" ${CMAKE_HOME_DIRECTORY}/bin/ShaderLibVK) +copyCommand("${sample_shaders_src}" ${CMAKE_HOME_DIRECTORY}/bin/ShaderLibVK) add_executable(FSR2_Sample_VK WIN32 ${sources} ${fsr2_src} ${sample_shaders_src} ${fsr1_shaders_src} ${fsr2_shaders_src} ${particle_shaders_src} ${spd_shaders_src} ${common} ${APP_ICON_GPUOPEN}) target_compile_definitions(FSR2_Sample_VK PRIVATE $<$:FSR2_DEBUG_SHADERS=1>) diff --git a/src/VK/FSR2Sample.cpp b/src/VK/FSR2Sample.cpp index ec2dc34..2491710 100644 --- a/src/VK/FSR2Sample.cpp +++ b/src/VK/FSR2Sample.cpp @@ -51,7 +51,6 @@ void FSR2Sample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint32_t* // set some default values *pWidth = 1920; *pHeight = 1080; - m_activeScene = 0; //load the first one by default m_VsyncEnabled = false; m_bIsBenchmarking = false; m_fontSize = 13.f; // default value overridden by a json file if available @@ -66,7 +65,7 @@ void FSR2Sample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint32_t* *pWidth = jData.value("width", *pWidth); *pHeight = jData.value("height", *pHeight); m_fullscreenMode = jData.value("presentationMode", m_fullscreenMode); - m_activeScene = jData.value("activeScene", m_activeScene); + m_UIState.m_activeScene = jData.value("activeScene", m_UIState.m_activeScene); m_activeCamera = jData.value("activeCamera", m_activeCamera); m_isCpuValidationLayerEnabled = jData.value("CpuValidationLayerEnabled", m_isCpuValidationLayerEnabled); m_isGpuValidationLayerEnabled = jData.value("GpuValidationLayerEnabled", m_isGpuValidationLayerEnabled); @@ -791,7 +790,7 @@ int WINAPI WinMain(HINSTANCE hInstance, LPSTR lpCmdLine, int nCmdShow) { - LPCSTR Name = "FidelityFX Super Resolution 2.0"; + LPCSTR Name = "FidelityFX Super Resolution 2.1"; // create new DX sample return RunFramework(hInstance, lpCmdLine, nCmdShow, new FSR2Sample(Name)); diff --git a/src/VK/FSR2Sample.h b/src/VK/FSR2Sample.h index 3f796ee..141126a 100644 --- a/src/VK/FSR2Sample.h +++ b/src/VK/FSR2Sample.h @@ -77,7 +77,6 @@ private: // json config file json m_jsonConfigFile; std::vector m_sceneNames; - int m_activeScene; int m_activeCamera; bool m_bPlay; diff --git a/src/VK/Renderer.cpp b/src/VK/Renderer.cpp index 7e64ba2..8a46472 100644 --- a/src/VK/Renderer.cpp +++ b/src/VK/Renderer.cpp @@ -48,12 +48,12 @@ void Renderer::OnCreate(Device *pDevice, SwapChain *pSwapChain, float FontSize, m_ConstantBufferRing.OnCreate(pDevice, backBufferCount, constantBuffersMemSize, "Uniforms"); // Create a 'static' pool for vertices and indices - const uint32_t staticGeometryMemSize = (1024) * 1024 * 1024; + const uint32_t staticGeometryMemSize = (5 * 128) * 1024 * 1024; m_VidMemBufferPool.OnCreate(pDevice, staticGeometryMemSize, true, "StaticGeom"); // Create a 'static' pool for vertices and indices in system memory - const uint32_t systemGeometryMemSize = 32 * 1024; - m_SysMemBufferPool.OnCreate(pDevice, systemGeometryMemSize, false, "PostProcGeom"); + //const uint32_t systemGeometryMemSize = 16 * 1024; + // m_SysMemBufferPool.OnCreate(pDevice, systemGeometryMemSize, false, "PostProcGeom"); // initialize the GPU time stamps module m_GPUTimer.OnCreate(pDevice, backBufferCount); @@ -82,16 +82,17 @@ void Renderer::OnCreate(Device *pDevice, SwapChain *pSwapChain, float FontSize, if (bInvertedDepth) fullGBuffer |= GBUFFER_INVERTED_DEPTH; bool bClear = true; - m_RenderPassFullGBufferWithClear.OnCreate(&m_GBuffer, fullGBuffer, bClear,"m_RenderPassFullGBufferWithClear"); + m_RenderPassFullGBufferWithClear.OnCreate(&m_GBuffer, fullGBuffer, bClear, "m_RenderPassFullGBufferWithClear"); m_RenderPassFullGBuffer.OnCreate(&m_GBuffer, fullGBuffer, !bClear, "m_RenderPassFullGBuffer"); m_RenderPassJustDepthAndHdr.OnCreate(&m_GBuffer, GBUFFER_DEPTH | GBUFFER_FORWARD, !bClear, "m_RenderPassJustDepthAndHdr"); + m_RenderPassFullGBufferNoDepthWrite.OnCreate(&m_GBuffer, fullGBuffer, !bClear, VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, "m_RenderPassFullGBufferNoDepthWrite"); } // Create render pass shadow, will clear contents { VkAttachmentDescription depthAttachments; AttachClearBeforeUse(VK_FORMAT_D32_SFLOAT, VK_SAMPLE_COUNT_1_BIT, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, &depthAttachments); - m_Render_pass_shadow = CreateRenderPassOptimal(m_pDevice->GetDevice(), 0, NULL, &depthAttachments); + m_Render_pass_shadow = CreateRenderPassOptimal(m_pDevice->GetDevice(), 0, NULL, &depthAttachments, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); } m_SkyDome.OnCreate(pDevice, m_RenderPassJustDepthAndHdr.GetRenderPass(), &m_UploadHeap, VK_FORMAT_R16G16B16A16_SFLOAT, &m_ResourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool, "..\\media\\cauldron-media\\envmaps\\papermill\\diffuse.dds", "..\\media\\cauldron-media\\envmaps\\papermill\\specular.dds", VK_SAMPLE_COUNT_1_BIT, m_bInvertedDepth); @@ -114,7 +115,14 @@ void Renderer::OnCreate(Device *pDevice, SwapChain *pSwapChain, float FontSize, m_VidMemBufferPool.UploadData(m_UploadHeap.GetCommandList()); m_UploadHeap.FlushAndFinish(); + m_pGPUParticleSystem = IParticleSystem::CreateGPUSystem("..\\media\\atlas.dds"); + m_pGPUParticleSystem->OnCreateDevice(*pDevice, m_UploadHeap, m_ResourceViewHeaps, m_VidMemBufferPool, m_ConstantBufferRing, m_RenderPassFullGBufferNoDepthWrite.GetRenderPass()); + m_GpuFrameRateLimiter.OnCreate(pDevice, &m_ConstantBufferRing, &m_ResourceViewHeaps); + + m_AnimatedTextures.OnCreate( *pDevice, m_UploadHeap, m_VidMemBufferPool, m_RenderPassFullGBufferWithClear.GetRenderPass(), m_ResourceViewHeaps, m_ConstantBufferRing ); + + ResetScene(); } //-------------------------------------------------------------------------------------- @@ -124,8 +132,13 @@ void Renderer::OnCreate(Device *pDevice, SwapChain *pSwapChain, float FontSize, //-------------------------------------------------------------------------------------- void Renderer::OnDestroy() { + m_AnimatedTextures.OnDestroy(); m_GpuFrameRateLimiter.OnDestroy(); + m_pGPUParticleSystem->OnDestroyDevice(); + delete m_pGPUParticleSystem; + m_pGPUParticleSystem = nullptr; + m_AsyncPool.Flush(); m_ImGUI.OnDestroy(); @@ -140,14 +153,18 @@ void Renderer::OnDestroy() m_SkyDomeProc.OnDestroy(); m_SkyDome.OnDestroy(); + m_RenderPassFullGBufferNoDepthWrite.OnDestroy(); m_RenderPassFullGBufferWithClear.OnDestroy(); m_RenderPassJustDepthAndHdr.OnDestroy(); m_RenderPassFullGBuffer.OnDestroy(); m_GBuffer.OnDestroy(); - m_pUpscaleContext->OnDestroy(); - delete m_pUpscaleContext; - m_pUpscaleContext = NULL; + if (m_pUpscaleContext) + { + m_pUpscaleContext->OnDestroy(); + delete m_pUpscaleContext; + m_pUpscaleContext = NULL; + } vkDestroyRenderPass(m_pDevice->GetDevice(), m_Render_pass_shadow, nullptr); @@ -179,6 +196,7 @@ void Renderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, UISta m_RenderPassFullGBufferWithClear.OnCreateWindowSizeDependentResources(m_Width, m_Height); m_RenderPassJustDepthAndHdr.OnCreateWindowSizeDependentResources(m_Width, m_Height); m_RenderPassFullGBuffer.OnCreateWindowSizeDependentResources(m_Width, m_Height); + m_RenderPassFullGBufferNoDepthWrite.OnCreateWindowSizeDependentResources(m_Width, m_Height); bool renderNative = (pState->m_nUpscaleType == UPSCALE_TYPE_NATIVE); bool hdr = (pSwapChain->GetDisplayMode() != DISPLAYMODE_SDR); @@ -207,6 +225,8 @@ void Renderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, UISta m_ImGUI.UpdatePipeline((pSwapChain->GetDisplayMode() == DISPLAYMODE_SDR) ? pSwapChain->GetRenderPass() : m_RenderPassDisplayOutput); + m_pGPUParticleSystem->OnResizedSwapChain(pState->renderWidth, pState->renderHeight, m_GBuffer.m_DepthBuffer, m_RenderPassFullGBufferNoDepthWrite.GetFramebuffer()); + // Lazy Upscale context generation: if ((m_pUpscaleContext == NULL) || (pState->m_nUpscaleType != m_pUpscaleContext->Type())) { @@ -223,7 +243,7 @@ void Renderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, UISta UpscaleContext::FfxUpscaleInitParams upscaleParams = { pState->m_nUpscaleType, m_bInvertedDepth, m_pDevice, pSwapChain->GetFormat(), &m_UploadHeap, backBufferCount }; m_pUpscaleContext = UpscaleContext::CreateUpscaleContext(upscaleParams); } - m_pUpscaleContext->OnCreateWindowSizeDependentResources(nullptr, m_displayOutputSRV, pState->renderWidth, pState->renderHeight, pState->displayWidth, pState->displayHeight, hdr); + m_pUpscaleContext->OnCreateWindowSizeDependentResources(nullptr, m_displayOutputSRV, pState->renderWidth, pState->renderHeight, pState->displayWidth, pState->displayHeight, true); } //-------------------------------------------------------------------------------------- @@ -233,6 +253,10 @@ void Renderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, UISta //-------------------------------------------------------------------------------------- void Renderer::OnDestroyWindowSizeDependentResources() { + m_pDevice->GPUFlush(); + + m_pGPUParticleSystem->OnReleasingSwapChain(); + vkDestroyImageView(m_pDevice->GetDevice(), m_OpaqueTextureSRV, 0); vkDestroyFramebuffer(m_pDevice->GetDevice(), m_FramebufferDisplayOutput, 0); vkDestroyImageView(m_pDevice->GetDevice(), m_displayOutputSRV, 0); @@ -253,6 +277,7 @@ void Renderer::OnDestroyWindowSizeDependentResources() m_RenderPassFullGBufferWithClear.OnDestroyWindowSizeDependentResources(); m_RenderPassJustDepthAndHdr.OnDestroyWindowSizeDependentResources(); m_RenderPassFullGBuffer.OnDestroyWindowSizeDependentResources(); + m_RenderPassFullGBufferNoDepthWrite.OnDestroyWindowSizeDependentResources(); m_GBuffer.OnDestroyWindowSizeDependentResources(); // destroy upscale context @@ -556,6 +581,7 @@ void Renderer::OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChai m_pUpscaleContext->PreDraw(pState); + float fLightMod = 1.f; // Sets the perFrame data per_frame *pPerFrame = NULL; if (m_pGLTFTexturesAndBuffers) @@ -578,6 +604,27 @@ void Renderer::OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChai m_pGLTFTexturesAndBuffers->SetSkinningMatricesForSkeletons(); } + { + m_state.flags = IParticleSystem::PF_Streaks | IParticleSystem::PF_DepthCull | IParticleSystem::PF_Sort; + m_state.flags |= pState->nReactiveMaskMode == REACTIVE_MASK_MODE_ON ? IParticleSystem::PF_Reactive : 0; + + const Camera& camera = pState->camera; + m_state.constantData.m_ViewProjection = camera.GetProjection() * camera.GetView(); + m_state.constantData.m_View = camera.GetView(); + m_state.constantData.m_ViewInv = math::inverse(camera.GetView()); + m_state.constantData.m_Projection = camera.GetProjection(); + m_state.constantData.m_ProjectionInv = math::inverse(camera.GetProjection()); + m_state.constantData.m_SunDirection = math::Vector4(0.7f, 0.7f, 0, 0); + m_state.constantData.m_SunColor = math::Vector4(0.8f, 0.8f, 0.7f, 0); + m_state.constantData.m_AmbientColor = math::Vector4(0.2f, 0.2f, 0.3f, 0); + + m_state.constantData.m_SunColor *= fLightMod; + m_state.constantData.m_AmbientColor *= fLightMod; + + m_state.constantData.m_FrameTime = pState->m_bPlayAnimations ? (0.001f * (float)pState->deltaTime) : 0.0f; + PopulateEmitters(pState->m_bPlayAnimations, pState->m_activeScene, 0.001f * (float)pState->deltaTime); + } + // Render all shadow maps if (m_GLTFDepth && pPerFrame != NULL) { @@ -646,6 +693,12 @@ void Renderer::OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChai vkCmdSetViewport(cmdBuf1, 0, 1, &vpr); m_GLTFPBR->DrawBatchList(cmdBuf1, &opaque, bWireframe); + + if (pState->bRenderAnimatedTextures) + { + m_AnimatedTextures.Render(cmdBuf1, pState->m_bPlayAnimations ? (0.001f * (float)pState->deltaTime) : 0.0f, pState->m_fTextureAnimationSpeed, pState->bCompositionMask, Cam); + } + m_GPUTimer.GetTimeStamp(cmdBuf1, "PBR Opaque"); m_RenderPassFullGBufferWithClear.EndPass(cmdBuf1); @@ -683,7 +736,7 @@ void Renderer::OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChai m_RenderPassJustDepthAndHdr.EndPass(cmdBuf1); } - if (pState->bUseFsr2AutoReactive|true) + if (pState->nReactiveMaskMode == REACTIVE_MASK_MODE_AUTOGEN) { // Copy resource before we render transparent stuff { @@ -764,13 +817,34 @@ void Renderer::OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChai barriers[1].newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; barriers[1].image = m_GBuffer.m_HDR.Resource(); - vkCmdPipelineBarrier(cmdBuf1, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, 2, barriers); + vkCmdPipelineBarrier(cmdBuf1, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, 2, barriers); } } // draw transparent geometry { - m_RenderPassFullGBuffer.BeginPass(cmdBuf1, currentRect); + if (pState->bRenderParticleSystem) + { + VkImageMemoryBarrier barrier = {}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.image = m_GBuffer.m_DepthBuffer.Resource(); + vkCmdPipelineBarrier(cmdBuf1, VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + + m_pGPUParticleSystem->Render(cmdBuf1, m_ConstantBufferRing, m_state.flags, m_state.emitters, m_state.numEmitters, m_state.constantData); + } + + m_RenderPassFullGBufferNoDepthWrite.BeginPass(cmdBuf1, currentRect); vkCmdSetScissor(cmdBuf1, 0, 1, &srr); vkCmdSetViewport(cmdBuf1, 0, 1, &vpr); @@ -779,11 +853,28 @@ void Renderer::OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChai m_GLTFPBR->DrawBatchList(cmdBuf1, &transparent, bWireframe); m_GPUTimer.GetTimeStamp(cmdBuf1, "PBR Transparent"); - m_RenderPassFullGBuffer.EndPass(cmdBuf1); + m_RenderPassFullGBufferNoDepthWrite.EndPass(cmdBuf1); } // draw object's bounding boxes { + // Put the depth buffer back from the read only state to the write state + VkImageMemoryBarrier barrier = {}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT; + barrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.image = m_GBuffer.m_DepthBuffer.Resource(); + vkCmdPipelineBarrier(cmdBuf1, VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + m_RenderPassJustDepthAndHdr.BeginPass(cmdBuf1, currentRect); vkCmdSetScissor(cmdBuf1, 0, 1, &srr); @@ -850,7 +941,7 @@ void Renderer::OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChai SetPerfMarkerEnd(cmdBuf1); // if FSR2 and auto reactive mask is enabled: generate reactive mask - if (pState->bUseFsr2AutoReactive) + if (pState->nReactiveMaskMode == REACTIVE_MASK_MODE_AUTOGEN) { VkImageMemoryBarrier barrier = {}; barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; @@ -940,8 +1031,8 @@ void Renderer::OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChai barriers[1].image = m_GBuffer.m_MotionVectors.Resource(); barriers[2] = barrier; - barriers[2].srcAccessMask = pState->bUseFsr2AutoReactive ? VK_ACCESS_SHADER_WRITE_BIT : VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - barriers[2].oldLayout = pState->bUseFsr2AutoReactive ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + barriers[2].srcAccessMask = pState->nReactiveMaskMode == REACTIVE_MASK_MODE_AUTOGEN ? VK_ACCESS_SHADER_WRITE_BIT : VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + barriers[2].oldLayout = pState->nReactiveMaskMode == REACTIVE_MASK_MODE_AUTOGEN ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; barriers[2].image = m_GBuffer.m_UpscaleReactive.Resource(); barriers[3] = barrier; @@ -964,7 +1055,7 @@ void Renderer::OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChai VkPipelineStageFlags srcStageFlags = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; - if (pState->bUseFsr2AutoReactive) + if (pState->nReactiveMaskMode == REACTIVE_MASK_MODE_AUTOGEN) srcStageFlags |= VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; vkCmdPipelineBarrier(cmdBuf1, srcStageFlags, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, 6, barriers); @@ -1303,6 +1394,106 @@ void Renderer::OnRender(UIState* pState, const Camera& Cam, SwapChain* pSwapChai } } + +void Renderer::ResetScene() +{ + ZeroMemory(m_EmissionRates, sizeof(m_EmissionRates)); + + // Reset the particle system when the scene changes so no particles from the previous scene persist + m_pGPUParticleSystem->Reset(); +} + +void Renderer::PopulateEmitters(bool playAnimations, int activeScene, float frameTime) +{ + IParticleSystem::EmitterParams sparksEmitter = {}; + IParticleSystem::EmitterParams smokeEmitter = {}; + + sparksEmitter.m_NumToEmit = 0; + sparksEmitter.m_ParticleLifeSpan = 1.0f; + sparksEmitter.m_StartSize = 0.6f * 0.02f; + sparksEmitter.m_EndSize = 0.4f * 0.02f; + sparksEmitter.m_VelocityVariance = 1.5f; + sparksEmitter.m_Mass = 1.0f; + sparksEmitter.m_TextureIndex = 1; + sparksEmitter.m_Streaks = true; + + smokeEmitter.m_NumToEmit = 0; + smokeEmitter.m_ParticleLifeSpan = 50.0f; + smokeEmitter.m_StartSize = 0.4f; + smokeEmitter.m_EndSize = 1.0f; + smokeEmitter.m_VelocityVariance = 1.0f; + smokeEmitter.m_Mass = 0.0003f; + smokeEmitter.m_TextureIndex = 0; + smokeEmitter.m_Streaks = false; + + if ( activeScene == 0 ) // scene 0 = warehouse + { + m_state.numEmitters = 2; + m_state.emitters[0] = sparksEmitter; + m_state.emitters[1] = sparksEmitter; + + m_state.emitters[0].m_Position = math::Vector4(-4.15f, -1.85f, -3.8f, 1.0f); + m_state.emitters[0].m_PositionVariance = math::Vector4(0.1f, 0.0f, 0.0f, 1.0f); + m_state.emitters[0].m_Velocity = math::Vector4(0.0f, 0.08f, 0.8f, 1.0f); + m_EmissionRates[0].m_ParticlesPerSecond = 300.0f; + + m_state.emitters[1].m_Position = math::Vector4(-4.9f, -1.5f, -4.8f, 1.0f); + m_state.emitters[1].m_PositionVariance = math::Vector4(0.0f, 0.0f, 0.0f, 1.0f); + m_state.emitters[1].m_Velocity = math::Vector4(0.0f, 0.8f, -0.8f, 1.0f); + m_EmissionRates[1].m_ParticlesPerSecond = 400.0f; + + m_state.constantData.m_StartColor[0] = math::Vector4(10.0f, 10.0f, 2.0f, 0.9f); + m_state.constantData.m_EndColor[0] = math::Vector4(10.0f, 10.0f, 0.0f, 0.1f); + m_state.constantData.m_StartColor[1] = math::Vector4(10.0f, 10.0f, 2.0f, 0.9f); + m_state.constantData.m_EndColor[1] = math::Vector4(10.0f, 10.0f, 0.0f, 0.1f); + } + else if (activeScene == 1) // Sponza + { + m_state.numEmitters = 2; + m_state.emitters[0] = smokeEmitter; + m_state.emitters[1] = sparksEmitter; + + m_state.emitters[0].m_Position = math::Vector4(-13.0f, 0.0f, 1.4f, 1.0f); + m_state.emitters[0].m_PositionVariance = math::Vector4(0.1f, 0.0f, 0.1f, 1.0f); + m_state.emitters[0].m_Velocity = math::Vector4(0.0f, 0.2f, 0.0f, 1.0f); + m_EmissionRates[0].m_ParticlesPerSecond = 10.0f; + + m_state.emitters[1].m_Position = math::Vector4(-13.0f, 0.0f, -1.4f, 1.0f); + m_state.emitters[1].m_PositionVariance = math::Vector4(0.05f, 0.0f, 0.05f, 1.0f); + m_state.emitters[1].m_Velocity = math::Vector4(0.0f, 4.0f, 0.0f, 1.0f); + m_state.emitters[1].m_VelocityVariance = 0.5f; + m_state.emitters[1].m_StartSize = 0.02f; + m_state.emitters[1].m_EndSize = 0.02f; + m_state.emitters[1].m_Mass = 1.0f; + m_EmissionRates[1].m_ParticlesPerSecond = 500.0f; + + m_state.constantData.m_StartColor[0] = math::Vector4(0.3f, 0.3f, 0.3f, 0.4f); + m_state.constantData.m_EndColor[0] = math::Vector4(0.4f, 0.4f, 0.4f, 0.1f); + m_state.constantData.m_StartColor[1] = math::Vector4(10.0f, 10.0f, 10.0f, 0.9f); + m_state.constantData.m_EndColor[1] = math::Vector4(5.0f, 8.0f, 5.0f, 0.1f); + } + + // Update all our active emitters so we know how many whole numbers of particles to emit from each emitter this frame + for (int i = 0; i < m_state.numEmitters; i++) + { + m_state.constantData.m_EmitterLightingCenter[i] = m_state.emitters[ i ].m_Position; + + if (m_EmissionRates[i].m_ParticlesPerSecond > 0.0f) + { + m_EmissionRates[i].m_Accumulation += m_EmissionRates[i].m_ParticlesPerSecond * (playAnimations ? frameTime : 0.0f); + + if (m_EmissionRates[i].m_Accumulation > 1.0f) + { + float integerPart = 0.0f; + float fraction = modf(m_EmissionRates[i].m_Accumulation, &integerPart); + + m_state.emitters[i].m_NumToEmit = (int)integerPart; + m_EmissionRates[i].m_Accumulation = fraction; + } + } + } +} + void Renderer::BuildDevUI(UIState* pState) { if (m_pUpscaleContext) diff --git a/src/VK/Renderer.h b/src/VK/Renderer.h index 07faff1..5e6d39e 100644 --- a/src/VK/Renderer.h +++ b/src/VK/Renderer.h @@ -27,6 +27,10 @@ #include "PostProc/MagnifierPS.h" #include "UpscaleContext.h" #include "GPUFrameRateLimiter.h" +#include "AnimatedTexture.h" + +#include "../GpuParticles/ParticleSystem.h" +#include "../GpuParticleShaders/ShaderConstants.h" // We are queuing (backBufferCount + 0.5) frames, so we need to triple buffer the resources that get modified each frame static const int backBufferCount = 3; @@ -62,6 +66,25 @@ public: void BuildDevUI(UIState* pState); private: + + struct State + { + float frameTime = 0.0f; + int numEmitters = 0; + IParticleSystem::EmitterParams emitters[10] = {}; + int flags = 0; + IParticleSystem::ConstantData constantData = {}; + }; + + struct EmissionRate + { + float m_ParticlesPerSecond = 0.0f; // Number of particles to emit per second + float m_Accumulation = 0.0f; // Running total of how many particles to emit over elapsed time + }; + + void ResetScene(); + void PopulateEmitters(bool playAnimations, int activeScene, float frameTime); + Device *m_pDevice; uint32_t m_Width; @@ -97,6 +120,13 @@ private: MagnifierPS m_MagnifierPS; bool m_bMagResourceReInit = false; + // GPU Particle System + State m_state = {}; + IParticleSystem* m_pGPUParticleSystem = nullptr; + EmissionRate m_EmissionRates[NUM_EMITTERS] = {}; + + AnimatedTextures m_AnimatedTextures = {}; + // Upscale UpscaleContext* m_pUpscaleContext = nullptr; VkRenderPass m_RenderPassDisplayOutput; @@ -110,6 +140,7 @@ private: GBufferRenderPass m_RenderPassFullGBufferWithClear; GBufferRenderPass m_RenderPassJustDepthAndHdr; GBufferRenderPass m_RenderPassFullGBuffer; + GBufferRenderPass m_RenderPassFullGBufferNoDepthWrite; Texture m_OpaqueTexture; VkImageView m_OpaqueTextureSRV; diff --git a/src/VK/UI.cpp b/src/VK/UI.cpp index 4b47576..8c8dcfb 100644 --- a/src/VK/UI.cpp +++ b/src/VK/UI.cpp @@ -73,7 +73,7 @@ void FSR2Sample::BuildUI() // if we haven't initialized GLTFLoader yet, don't draw UI. if (m_pGltfLoader == nullptr) { - LoadScene(m_activeScene); + LoadScene(m_UIState.m_activeScene); return; } @@ -133,13 +133,13 @@ void FSR2Sample::BuildUI() ImGui::Checkbox("Camera Headbobbing", &m_UIState.m_bHeadBobbing); auto getterLambda = [](void* data, int idx, const char** out_str)->bool { *out_str = ((std::vector *)data)->at(idx).c_str(); return true; }; - if (ImGui::Combo("Model", &m_activeScene, getterLambda, &m_sceneNames, (int)m_sceneNames.size())) + if (ImGui::Combo("Model", &m_UIState.m_activeScene, getterLambda, &m_sceneNames, (int)m_sceneNames.size())) { - m_UIState.bRenderParticleSystem = (m_activeScene == 11); + m_UIState.bRenderAnimatedTextures = (m_UIState.m_activeScene == 1); // Note: // probably queueing this as an event and handling it at the end/beginning // of frame is a better idea rather than in the middle of drawing UI. - LoadScene(m_activeScene); + LoadScene(m_UIState.m_activeScene); //bail out as we need to reload everything ImGui::End(); @@ -188,7 +188,7 @@ void FSR2Sample::BuildUI() OnResize(true); } - if (m_UIState.m_nUpscaleType <= UPSCALE_TYPE_FSR_2_0) + if (m_UIState.m_nUpscaleType == UPSCALE_TYPE_FSR_2_0) { // adjust to match the combo box options int32_t upscaleQualityMode = m_nUpscaleMode - UPSCALE_QUALITY_MODE_QUALITY; @@ -214,20 +214,28 @@ void FSR2Sample::BuildUI() OnResize(); } - if (m_UIState.m_nUpscaleType == UPSCALE_TYPE_FSR_2_0) + + if (ImGui::Checkbox("Dynamic resolution", &m_UIState.bDynamicRes)) { - if (ImGui::Checkbox("Dynamic resolution", &m_UIState.bDynamicRes)) { - OnResize(); - } + OnResize(); } - else - m_UIState.bDynamicRes = false; + + const char* reactiveOptions[] = { "Disabled", "Manual Reactive Mask Generation", "Autogen FSR2 Helper Function" }; + ImGui::Combo("Reactive Mask mode", (int*)(&m_UIState.nReactiveMaskMode), reactiveOptions, _countof(reactiveOptions)); + + ImGui::Checkbox("Use Transparency and Composition Mask", &m_UIState.bCompositionMask); } else { m_UIState.mipBias = mipBias[UPSCALE_TYPE_NATIVE]; } + + if (m_UIState.m_nUpscaleType != UPSCALE_TYPE_FSR_2_0) + { + m_UIState.bDynamicRes = false; + } + ImGui::Checkbox("RCAS Sharpening", &m_UIState.bUseRcas); if (m_UIState.m_nUpscaleType == UPSCALE_TYPE_FSR_2_0) { @@ -318,7 +326,7 @@ void FSR2Sample::BuildUI() if (ImGui::CollapsingHeader("Presentation Mode", ImGuiTreeNodeFlags_DefaultOpen)) { - const char* fullscreenModes[] = { "Windowed", "BorderlessFullscreen", "ExclusiveFulscreen" }; + const char* fullscreenModes[] = { "Windowed", "BorderlessFullscreen", "ExclusiveFullscreen" }; if (ImGui::Combo("Fullscreen Mode", (int*)&m_fullscreenMode, fullscreenModes, _countof(fullscreenModes))) { if (m_previousFullscreenMode != m_fullscreenMode) @@ -633,4 +641,4 @@ bool UIState::DevOption(float* pFloatValue, const char* name, float fMin, float void UIState::Text(const char* text) { ImGui::Text(text); -} \ No newline at end of file +} diff --git a/src/VK/UI.h b/src/VK/UI.h index 844e18c..52be14f 100644 --- a/src/VK/UI.h +++ b/src/VK/UI.h @@ -55,12 +55,26 @@ typedef enum UpscaleQualityMode { UPSCALE_QUALITY_MODE_COUNT } UpscaleQualityMode; +typedef enum ReactiveMaskMode { + REACTIVE_MASK_MODE_OFF = 0, // Nothing written to the reactive mask + REACTIVE_MASK_MODE_ON = 1, // Particles written to the reactive mask + REACTIVE_MASK_MODE_AUTOGEN = 2, // The mask is auto generated using FSR2's helper function + + // add above this. + REACTIVE_MASK_MODE_COUNT +} ReactiveMaskMode; + struct UIState { Camera camera; bool m_bCameraInertia = false; bool m_bHeadBobbing = false; + bool m_bPlayAnimations = true; + float m_fTextureAnimationSpeed = 1.0f; + int m_activeScene = 0; + bool m_bAnimateSpotlight = false; + // // WINDOW MANAGEMENT // @@ -76,7 +90,9 @@ struct UIState bool bReset = false; - bool bRenderParticleSystem = false; + int nLightModulationMode = 0; + bool bRenderParticleSystem = true; + bool bRenderAnimatedTextures = true; bool bUseMagnifier; bool bLockMagnifierPosition; bool bLockMagnifierPositionHistory; @@ -108,15 +124,31 @@ struct UIState unsigned int closestVelocitySamplePattern = 0; // 5 samples float Feedback = 15.f / 16.f; - // FSR2 auto reactive - bool bUseFsr2AutoReactive = false; + // FSR2 reactive mask + ReactiveMaskMode nReactiveMaskMode = REACTIVE_MASK_MODE_ON; float fFsr2AutoReactiveScale = 1.f; - float fFsr2AutoReactiveThreshold = 0.01f; + float fFsr2AutoReactiveThreshold = 0.2f; + float fFsr2AutoReactiveBinaryValue = 0.9f; bool bFsr2AutoReactiveTonemap = true; bool bFsr2AutoReactiveInverseTonemap = false; bool bFsr2AutoReactiveThreshold = true; bool bFsr2AutoReactiveUseMax = true; + // FSR2 composition mask + bool bCompositionMask = true; + + // FSR2 + bool bUseDebugOut = false; + int nDebugBlitSurface = 6; // FFX_FSR2_RESOURCE_IDENTIFIER_INTERNAL_UPSCALED_COLOR + int nDebugOutMappingR = 0; + int nDebugOutMappingG = 1; + int nDebugOutMappingB = 2; + float v2DebugOutMappingR[2] = { 0.f, 1.f }; + float v2DebugOutMappingG[2] = { 0.f, 1.f }; + float v2DebugOutMappingB[2] = { 0.f, 1.f }; + + float v4DebugSliderValues[8] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; + // // APP/SCENE CONTROLS // diff --git a/src/VK/UpscaleContext_FSR2_API.cpp b/src/VK/UpscaleContext_FSR2_API.cpp index f98b837..4805ee9 100644 --- a/src/VK/UpscaleContext_FSR2_API.cpp +++ b/src/VK/UpscaleContext_FSR2_API.cpp @@ -101,8 +101,11 @@ void UpscaleContext_FSR2_API::OnCreateWindowSizeDependentResources( initializationParameters.maxRenderSize.height = renderHeight; initializationParameters.displaySize.width = displayWidth; initializationParameters.displaySize.height = displayHeight; - initializationParameters.flags = FFX_FSR2_ENABLE_DEPTH_INVERTED - | FFX_FSR2_ENABLE_AUTO_EXPOSURE; + initializationParameters.flags = FFX_FSR2_ENABLE_AUTO_EXPOSURE; + + if (m_bInvertedDepth) { + initializationParameters.flags |= FFX_FSR2_ENABLE_DEPTH_INVERTED; + } if (hdr) { initializationParameters.flags |= FFX_FSR2_ENABLE_HIGH_DYNAMIC_RANGE; @@ -117,7 +120,13 @@ void UpscaleContext_FSR2_API::OnCreateWindowSizeDependentResources( void UpscaleContext_FSR2_API::OnDestroyWindowSizeDependentResources() { UpscaleContext::OnDestroyWindowSizeDependentResources(); - ffxFsr2ContextDestroy(&context); + // only destroy contexts which are live + if (initializationParameters.callbacks.scratchBuffer != nullptr) + { + ffxFsr2ContextDestroy(&context); + free(initializationParameters.callbacks.scratchBuffer); + initializationParameters.callbacks.scratchBuffer = nullptr; + } } void UpscaleContext_FSR2_API::BuildDevUI(UIState* pState) @@ -144,6 +153,7 @@ void UpscaleContext_FSR2_API::GenerateReactiveMask(VkCommandBuffer pCommandList, generateReactiveParameters.scale = pState->fFsr2AutoReactiveScale; generateReactiveParameters.cutoffThreshold = pState->fFsr2AutoReactiveThreshold; + generateReactiveParameters.binaryValue = pState->fFsr2AutoReactiveBinaryValue; generateReactiveParameters.flags = (pState->bFsr2AutoReactiveTonemap ? FFX_FSR2_AUTOREACTIVEFLAGS_APPLY_TONEMAP : 0) | (pState->bFsr2AutoReactiveInverseTonemap ? FFX_FSR2_AUTOREACTIVEFLAGS_APPLY_INVERSETONEMAP : 0) | (pState->bFsr2AutoReactiveThreshold ? FFX_FSR2_AUTOREACTIVEFLAGS_APPLY_THRESHOLD : 0) | @@ -160,8 +170,26 @@ void UpscaleContext_FSR2_API::Draw(VkCommandBuffer commandBuffer, const FfxUpsca dispatchParameters.depth = ffxGetTextureResourceVK(&context, cameraSetup.depthbufferResource->Resource(), cameraSetup.depthbufferResourceView, cameraSetup.depthbufferResource->GetWidth(), cameraSetup.depthbufferResource->GetHeight(), cameraSetup.depthbufferResource->GetFormat(), L"FSR2_InputDepth"); dispatchParameters.motionVectors = ffxGetTextureResourceVK(&context, cameraSetup.motionvectorResource->Resource(), cameraSetup.motionvectorResourceView, cameraSetup.motionvectorResource->GetWidth(), cameraSetup.motionvectorResource->GetHeight(), cameraSetup.motionvectorResource->GetFormat(), L"FSR2_InputMotionVectors"); dispatchParameters.exposure = ffxGetTextureResourceVK(&context, nullptr, nullptr, 1, 1, VK_FORMAT_UNDEFINED, L"FSR2_InputExposure"); - dispatchParameters.reactive = ffxGetTextureResourceVK(&context, cameraSetup.reactiveMapResource->Resource(), cameraSetup.reactiveMapResourceView, cameraSetup.reactiveMapResource->GetWidth(), cameraSetup.reactiveMapResource->GetHeight(), cameraSetup.reactiveMapResource->GetFormat(), L"FSR2_InputReactiveMap"); - dispatchParameters.transparencyAndComposition = ffxGetTextureResourceVK(&context, cameraSetup.transparencyAndCompositionResource->Resource(), cameraSetup.transparencyAndCompositionResourceView, cameraSetup.transparencyAndCompositionResource->GetWidth(), cameraSetup.transparencyAndCompositionResource->GetHeight(), cameraSetup.transparencyAndCompositionResource->GetFormat(), L"FSR2_TransparencyAndCompositionMap"); + + if ((pState->nReactiveMaskMode == ReactiveMaskMode::REACTIVE_MASK_MODE_ON) + || (pState->nReactiveMaskMode == ReactiveMaskMode::REACTIVE_MASK_MODE_AUTOGEN)) + { + dispatchParameters.reactive = ffxGetTextureResourceVK(&context, cameraSetup.reactiveMapResource->Resource(), cameraSetup.reactiveMapResourceView, cameraSetup.reactiveMapResource->GetWidth(), cameraSetup.reactiveMapResource->GetHeight(), cameraSetup.reactiveMapResource->GetFormat(), L"FSR2_InputReactiveMap"); + } + else + { + dispatchParameters.reactive = ffxGetTextureResourceVK(&context, nullptr, nullptr, 1, 1, VK_FORMAT_UNDEFINED, L"FSR2_EmptyInputReactiveMap"); + } + + if (pState->bCompositionMask == true) + { + dispatchParameters.transparencyAndComposition = ffxGetTextureResourceVK(&context, cameraSetup.transparencyAndCompositionResource->Resource(), cameraSetup.transparencyAndCompositionResourceView, cameraSetup.transparencyAndCompositionResource->GetWidth(), cameraSetup.transparencyAndCompositionResource->GetHeight(), cameraSetup.transparencyAndCompositionResource->GetFormat(), L"FSR2_TransparencyAndCompositionMap"); + } + else + { + dispatchParameters.transparencyAndComposition = ffxGetTextureResourceVK(&context, nullptr, nullptr, 1, 1, VK_FORMAT_UNDEFINED, L"FSR2_EmptyTransparencyAndCompositionMap"); + } + dispatchParameters.output = ffxGetTextureResourceVK(&context, cameraSetup.resolvedColorResource->Resource(), cameraSetup.resolvedColorResourceView, cameraSetup.resolvedColorResource->GetWidth(), cameraSetup.resolvedColorResource->GetHeight(), cameraSetup.resolvedColorResource->GetFormat(), L"FSR2_OutputUpscaledColor", FFX_RESOURCE_STATE_UNORDERED_ACCESS); dispatchParameters.jitterOffset.x = m_JitterX; dispatchParameters.jitterOffset.y = m_JitterY; diff --git a/src/VK/stdafx.h b/src/VK/stdafx.h index 3b86a85..a714eb1 100644 --- a/src/VK/stdafx.h +++ b/src/VK/stdafx.h @@ -85,5 +85,5 @@ #include "Widgets/WireframeSphere.h" - +#define API_VULKAN using namespace CAULDRON_VK; diff --git a/src/ffx-fsr2-api/CMakeLists.txt b/src/ffx-fsr2-api/CMakeLists.txt index cb291be..7ef023c 100644 --- a/src/ffx-fsr2-api/CMakeLists.txt +++ b/src/ffx-fsr2-api/CMakeLists.txt @@ -20,7 +20,6 @@ # THE SOFTWARE. cmake_minimum_required(VERSION 3.15) -#set(CMAKE_CONFIGURATION_TYPES Debug Release) set(CMAKE_DEBUG_POSTFIX d) option (FFX_FSR2_API_DX12 "Build FSR 2.0 DX12 backend" ON) @@ -55,6 +54,9 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${CMAKE_HOME_DIRECTORY}/bin/ffx_fsr2_ add_compile_definitions(_UNICODE) add_compile_definitions(UNICODE) +#add_compile_definitions(FSR2_VERSION_MAJOR=0) +#add_compile_definitions(FSR2_VERSION_MINOR=1) +#add_compile_definitions(FSR2_VERSION_PATCH=0) if(FSR2_VS_VERSION STREQUAL 2015) message(NOTICE "Forcing the SDK path for VS 2015") @@ -65,10 +67,19 @@ set(FFX_SC_EXECUTABLE ${CMAKE_CURRENT_SOURCE_DIR}/../../tools/sc/FidelityFX_SC.exe) set(FFX_SC_BASE_ARGS - -reflection -deps=gcc -DFFX_GPU=1 -DOPT_PRECOMPUTE_REACTIVE_MAX=1) + -reflection -deps=gcc -DFFX_GPU=1 + # Only reprojection is to do half for now + -DFFX_FSR2_OPTION_UPSAMPLE_SAMPLERS_USE_DATA_HALF=0 + -DFFX_FSR2_OPTION_ACCUMULATE_SAMPLERS_USE_DATA_HALF=0 + -DFFX_FSR2_OPTION_REPROJECT_SAMPLERS_USE_DATA_HALF=1 + -DFFX_FSR2_OPTION_POSTPROCESSLOCKSTATUS_SAMPLERS_USE_DATA_HALF=0 + # Upsample uses lanczos approximation + -DFFX_FSR2_OPTION_UPSAMPLE_USE_LANCZOS_TYPE=2 + ) set(FFX_SC_PERMUTATION_ARGS - -DFFX_FSR2_OPTION_USE_LANCZOS_LUT={0,1} + # Reproject can use either reference lanczos or LUT + -DFFX_FSR2_OPTION_REPROJECT_USE_LANCZOS_TYPE={0,1} -DFFX_FSR2_OPTION_HDR_COLOR_INPUT={0,1} -DFFX_FSR2_OPTION_LOW_RESOLUTION_MOTION_VECTORS={0,1} -DFFX_FSR2_OPTION_JITTERED_MOTION_VECTORS={0,1} diff --git a/src/ffx-fsr2-api/dx12/CMakeLists.txt b/src/ffx-fsr2-api/dx12/CMakeLists.txt index bc44412..77a30c4 100644 --- a/src/ffx-fsr2-api/dx12/CMakeLists.txt +++ b/src/ffx-fsr2-api/dx12/CMakeLists.txt @@ -24,7 +24,7 @@ if(NOT ${FFX_FSR2_API_DX12}) endif() set(FFX_SC_DX12_BASE_ARGS - -E CS -Wno-for-redefinition -Wno-ambig-lit-shift -Wno-conversion -DFFX_HLSL=1 -DFFX_HLSL_6_2=1) + -E CS -Wno-for-redefinition -Wno-ambig-lit-shift -DFFX_HLSL=1 -DFFX_HLSL_6_2=1) file(GLOB SHADERS "${CMAKE_CURRENT_SOURCE_DIR}/../shaders/*.h" diff --git a/src/ffx-fsr2-api/dx12/ffx_fsr2_dx12.cpp b/src/ffx-fsr2-api/dx12/ffx_fsr2_dx12.cpp index a5b41e1..4b0f507 100644 --- a/src/ffx-fsr2-api/dx12/ffx_fsr2_dx12.cpp +++ b/src/ffx-fsr2-api/dx12/ffx_fsr2_dx12.cpp @@ -19,6 +19,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +#include // convert string to wstring #include #include #include @@ -31,8 +32,8 @@ // DX12 prototypes for functions in the backend interface FfxErrorCode GetDeviceCapabilitiesDX12(FfxFsr2Interface* backendInterface, FfxDeviceCapabilities* deviceCapabilities, FfxDevice device); -FfxErrorCode CreateDeviceDX12(FfxFsr2Interface* backendInterface, FfxDevice device); -FfxErrorCode DestroyDeviceDX12(FfxFsr2Interface* backendInterface, FfxDevice device); +FfxErrorCode CreateBackendContextDX12(FfxFsr2Interface* backendInterface, FfxDevice device); +FfxErrorCode DestroyBackendContextDX12(FfxFsr2Interface* backendInterface); FfxErrorCode CreateResourceDX12(FfxFsr2Interface* backendInterface, const FfxCreateResourceDescription* desc, FfxResourceInternal* outTexture); FfxErrorCode RegisterResourceDX12(FfxFsr2Interface* backendInterface, const FfxResource* inResource, FfxResourceInternal* outResourceInternal); FfxErrorCode UnregisterResourcesDX12(FfxFsr2Interface* backendInterface); @@ -40,14 +41,14 @@ FfxResourceDescription GetResourceDescriptorDX12(FfxFsr2Interface* backendInterf FfxErrorCode DestroyResourceDX12(FfxFsr2Interface* backendInterface, FfxResourceInternal resource); FfxErrorCode CreatePipelineDX12(FfxFsr2Interface* backendInterface, FfxFsr2Pass passId, const FfxPipelineDescription* desc, FfxPipelineState* outPass); FfxErrorCode DestroyPipelineDX12(FfxFsr2Interface* backendInterface, FfxPipelineState* pipeline); -FfxErrorCode ScheduleRenderJobDX12(FfxFsr2Interface* backendInterface, const FfxRenderJobDescription* job); -FfxErrorCode ExecuteRenderJobsDX12(FfxFsr2Interface* backendInterface, FfxCommandList commandList); +FfxErrorCode ScheduleGpuJobDX12(FfxFsr2Interface* backendInterface, const FfxGpuJobDescription* job); +FfxErrorCode ExecuteGpuJobsDX12(FfxFsr2Interface* backendInterface, FfxCommandList commandList); #define FSR2_MAX_QUEUED_FRAMES ( 4) #define FSR2_MAX_RESOURCE_COUNT (64) #define FSR2_DESC_RING_SIZE (FSR2_MAX_QUEUED_FRAMES * FFX_FSR2_PASS_COUNT * FSR2_MAX_RESOURCE_COUNT) #define FSR2_MAX_BARRIERS (16) -#define FSR2_MAX_RENDERJOBS (32) +#define FSR2_MAX_GPU_JOBS (32) #define FSR2_MAX_SAMPLERS ( 2) #define UPLOAD_JOB_COUNT (16) @@ -69,8 +70,8 @@ typedef struct BackendContext_DX12 { ID3D12Device* device = nullptr; - FfxRenderJobDescription renderJobs[FSR2_MAX_RENDERJOBS] = {}; - uint32_t renderJobCount; + FfxGpuJobDescription gpuJobs[FSR2_MAX_GPU_JOBS] = {}; + uint32_t gpuJobCount; uint32_t nextStaticResource; uint32_t nextDynamicResource; @@ -113,8 +114,8 @@ FfxErrorCode ffxFsr2GetInterfaceDX12( FFX_ERROR_INSUFFICIENT_MEMORY); outInterface->fpGetDeviceCapabilities = GetDeviceCapabilitiesDX12; - outInterface->fpCreateDevice = CreateDeviceDX12; - outInterface->fpDestroyDevice = DestroyDeviceDX12; + outInterface->fpCreateBackendContext = CreateBackendContextDX12; + outInterface->fpDestroyBackendContext = DestroyBackendContextDX12; outInterface->fpCreateResource = CreateResourceDX12; outInterface->fpRegisterResource = RegisterResourceDX12; outInterface->fpUnregisterResources = UnregisterResourcesDX12; @@ -122,8 +123,8 @@ FfxErrorCode ffxFsr2GetInterfaceDX12( outInterface->fpDestroyResource = DestroyResourceDX12; outInterface->fpCreatePipeline = CreatePipelineDX12; outInterface->fpDestroyPipeline = DestroyPipelineDX12; - outInterface->fpScheduleRenderJob = ScheduleRenderJobDX12; - outInterface->fpExecuteRenderJobs = ExecuteRenderJobsDX12; + outInterface->fpScheduleGpuJob = ScheduleGpuJobDX12; + outInterface->fpExecuteGpuJobs = ExecuteGpuJobsDX12; outInterface->scratchBuffer = scratchBuffer; outInterface->scratchBufferSize = scratchBufferSize; @@ -268,6 +269,8 @@ DXGI_FORMAT ffxGetDX12FormatFromSurfaceFormat(FfxSurfaceFormat surfaceFormat) return DXGI_FORMAT_R16_SNORM; case(FFX_SURFACE_FORMAT_R8_UNORM): return DXGI_FORMAT_R8_UNORM; + case(FFX_SURFACE_FORMAT_R8G8_UNORM): + return DXGI_FORMAT_R8G8_UNORM; case(FFX_SURFACE_FORMAT_R32_FLOAT): return DXGI_FORMAT_R32_FLOAT; default: @@ -323,7 +326,7 @@ FfxSurfaceFormat ffxGetSurfaceFormatDX12(DXGI_FORMAT format) } // register a DX12 resource to the backend -FfxResource ffxGetResourceDX12(FfxFsr2Context* context, ID3D12Resource* dx12Resource, wchar_t* name, FfxResourceStates state, UINT shaderComponentMapping) +FfxResource ffxGetResourceDX12(FfxFsr2Context* context, ID3D12Resource* dx12Resource, const wchar_t* name, FfxResourceStates state, UINT shaderComponentMapping) { FfxResource resource = {}; resource.resource = reinterpret_cast(dx12Resource); @@ -601,7 +604,7 @@ FfxErrorCode GetDeviceCapabilitiesDX12(FfxFsr2Interface* backendInterface, FfxDe } // initialize the DX12 backend -FfxErrorCode CreateDeviceDX12(FfxFsr2Interface* backendInterface, FfxDevice device) +FfxErrorCode CreateBackendContextDX12(FfxFsr2Interface* backendInterface, FfxDevice device) { HRESULT result = S_OK; ID3D12Device* dx12Device = reinterpret_cast(device); @@ -651,12 +654,9 @@ FfxErrorCode CreateDeviceDX12(FfxFsr2Interface* backendInterface, FfxDevice devi } // deinitialize the DX12 backend -FfxErrorCode DestroyDeviceDX12(FfxFsr2Interface* backendInterface, FfxDevice device) +FfxErrorCode DestroyBackendContextDX12(FfxFsr2Interface* backendInterface) { - ID3D12Device* dx12Device = reinterpret_cast(device); - FFX_ASSERT(NULL != backendInterface); - FFX_ASSERT(NULL != dx12Device); BackendContext_DX12* backendContext = (BackendContext_DX12*)backendInterface->scratchBuffer; backendContext->descHeapSrvCpu->Release(); @@ -675,10 +675,10 @@ FfxErrorCode DestroyDeviceDX12(FfxFsr2Interface* backendInterface, FfxDevice dev backendContext->nextStaticResource = 0; - if (dx12Device != NULL) { + if (backendContext->device != NULL) { - dx12Device->Release(); - dx12Device = NULL; + backendContext->device->Release(); + backendContext->device = NULL; } return FFX_OK; @@ -695,8 +695,8 @@ FfxErrorCode CreateResourceDX12( FFX_ASSERT(NULL != createResourceDescription); FFX_ASSERT(NULL != outTexture); - ID3D12Device* dx12Device = reinterpret_cast(createResourceDescription->device); BackendContext_DX12* backendContext = (BackendContext_DX12*)backendInterface->scratchBuffer; + ID3D12Device* dx12Device = backendContext->device; FFX_ASSERT(NULL != dx12Device); @@ -902,14 +902,14 @@ FfxErrorCode CreateResourceDX12( backendInterface->fpCreateResource(backendInterface, &uploadDescription, ©Src); // setup the upload job - FfxRenderJobDescription copyJob = { + FfxGpuJobDescription copyJob = { - FFX_RENDER_JOB_COPY + FFX_GPU_JOB_COPY }; copyJob.copyJobDescriptor.src = copySrc; copyJob.copyJobDescriptor.dst = *outTexture; - backendInterface->fpScheduleRenderJob(backendInterface, ©Job); + backendInterface->fpScheduleGpuJob(backendInterface, ©Job); } } @@ -988,12 +988,11 @@ FfxErrorCode CreatePipelineDX12( flags |= (pipelineDescription->contextFlags & FFX_FSR2_ENABLE_MOTION_VECTORS_JITTER_CANCELLATION) ? FSR2_SHADER_PERMUTATION_JITTER_MOTION_VECTORS : 0; flags |= (pipelineDescription->contextFlags & FFX_FSR2_ENABLE_DEPTH_INVERTED) ? FSR2_SHADER_PERMUTATION_DEPTH_INVERTED : 0; flags |= (pass == FFX_FSR2_PASS_ACCUMULATE_SHARPEN) ? FSR2_SHADER_PERMUTATION_ENABLE_SHARPENING : 0; - flags |= (useLut) ? FSR2_SHADER_PERMUTATION_LANCZOS_LUT : 0; + flags |= (useLut) ? FSR2_SHADER_PERMUTATION_USE_LANCZOS_TYPE : 0; flags |= (canForceWave64) ? FSR2_SHADER_PERMUTATION_FORCE_WAVE64 : 0; - flags |= (supportedFP16) ? FSR2_SHADER_PERMUTATION_ALLOW_FP16 : 0; + flags |= (supportedFP16 && (pass != FFX_FSR2_PASS_RCAS)) ? FSR2_SHADER_PERMUTATION_ALLOW_FP16 : 0; - Fsr2ShaderBlobDX12 shaderBlob = { }; - fsr2GetPermutationBlobByIndex(pass, flags, &shaderBlob); + const Fsr2ShaderBlobDX12 shaderBlob = fsr2GetPermutationBlobByIndex(pass, flags); FFX_ASSERT(shaderBlob.data && shaderBlob.size); // set up root signature @@ -1168,20 +1167,21 @@ FfxErrorCode CreatePipelineDX12( outPipeline->srvCount = shaderBlob.srvCount; outPipeline->uavCount = shaderBlob.uavCount; outPipeline->constCount = shaderBlob.cbvCount; + std::wstring_convert> converter; for (uint32_t srvIndex = 0; srvIndex < outPipeline->srvCount; ++srvIndex) { outPipeline->srvResourceBindings[srvIndex].slotIndex = shaderBlob.boundSRVResources[srvIndex]; - strcpy_s(outPipeline->srvResourceBindings[srvIndex].name, shaderBlob.boundSRVResourceNames[srvIndex]); + wcscpy_s(outPipeline->srvResourceBindings[srvIndex].name, converter.from_bytes(shaderBlob.boundSRVResourceNames[srvIndex]).c_str()); } for (uint32_t uavIndex = 0; uavIndex < outPipeline->uavCount; ++uavIndex) { outPipeline->uavResourceBindings[uavIndex].slotIndex = shaderBlob.boundUAVResources[uavIndex]; - strcpy_s(outPipeline->uavResourceBindings[uavIndex].name, shaderBlob.boundUAVResourceNames[uavIndex]); + wcscpy_s(outPipeline->uavResourceBindings[uavIndex].name, converter.from_bytes(shaderBlob.boundUAVResourceNames[uavIndex]).c_str()); } for (uint32_t cbIndex = 0; cbIndex < outPipeline->constCount; ++cbIndex) { outPipeline->cbResourceBindings[cbIndex].slotIndex = shaderBlob.boundCBVResources[cbIndex]; - strcpy_s(outPipeline->cbResourceBindings[cbIndex].name, shaderBlob.boundCBVResourceNames[cbIndex]); + wcscpy_s(outPipeline->cbResourceBindings[cbIndex].name, converter.from_bytes(shaderBlob.boundCBVResourceNames[cbIndex]).c_str()); } // create the PSO @@ -1200,9 +1200,9 @@ FfxErrorCode CreatePipelineDX12( return FFX_OK; } -FfxErrorCode ScheduleRenderJobDX12( +FfxErrorCode ScheduleGpuJobDX12( FfxFsr2Interface* backendInterface, - const FfxRenderJobDescription* job + const FfxGpuJobDescription* job ) { FFX_ASSERT(NULL != backendInterface); @@ -1210,14 +1210,14 @@ FfxErrorCode ScheduleRenderJobDX12( BackendContext_DX12* backendContext = (BackendContext_DX12*)backendInterface->scratchBuffer; - FFX_ASSERT(backendContext->renderJobCount < FSR2_MAX_RENDERJOBS); + FFX_ASSERT(backendContext->gpuJobCount < FSR2_MAX_GPU_JOBS); - backendContext->renderJobs[backendContext->renderJobCount] = *job; + backendContext->gpuJobs[backendContext->gpuJobCount] = *job; - if (job->jobType == FFX_RENDER_JOB_COMPUTE) { + if (job->jobType == FFX_GPU_JOB_COMPUTE) { // needs to copy SRVs and UAVs in case they are on the stack only - FfxComputeJobDescription* computeJob = &backendContext->renderJobs[backendContext->renderJobCount].computeJobDescriptor; + FfxComputeJobDescription* computeJob = &backendContext->gpuJobs[backendContext->gpuJobCount].computeJobDescriptor; const uint32_t numConstBuffers = job->computeJobDescriptor.pipeline.constCount; for (uint32_t currentRootConstantIndex = 0; currentRootConstantIndex< numConstBuffers; ++currentRootConstantIndex) { @@ -1226,7 +1226,7 @@ FfxErrorCode ScheduleRenderJobDX12( } } - backendContext->renderJobCount++; + backendContext->gpuJobCount++; return FFX_OK; } @@ -1272,7 +1272,7 @@ void flushBarriers(BackendContext_DX12* backendContext, ID3D12GraphicsCommandLis } } -static FfxErrorCode executeRenderJobCompute(BackendContext_DX12* backendContext, FfxRenderJobDescription* job, ID3D12Device* dx12Device, ID3D12GraphicsCommandList* dx12CommandList) +static FfxErrorCode executeGpuJobCompute(BackendContext_DX12* backendContext, FfxGpuJobDescription* job, ID3D12Device* dx12Device, ID3D12GraphicsCommandList* dx12CommandList) { ID3D12DescriptorHeap* dx12DescriptorHeap = reinterpret_cast(backendContext->descRingBuffer); @@ -1390,7 +1390,7 @@ static FfxErrorCode executeRenderJobCompute(BackendContext_DX12* backendContext, return FFX_OK; } -static FfxErrorCode executeRenderJobCopy(BackendContext_DX12* backendContext, FfxRenderJobDescription* job, ID3D12Device* dx12Device, ID3D12GraphicsCommandList* dx12CommandList) +static FfxErrorCode executeGpuJobCopy(BackendContext_DX12* backendContext, FfxGpuJobDescription* job, ID3D12Device* dx12Device, ID3D12GraphicsCommandList* dx12CommandList) { ID3D12Resource* dx12ResourceSrc = getDX12ResourcePtr(backendContext, job->copyJobDescriptor.src.internalIndex); ID3D12Resource* dx12ResourceDst = getDX12ResourcePtr(backendContext, job->copyJobDescriptor.dst.internalIndex); @@ -1420,7 +1420,7 @@ static FfxErrorCode executeRenderJobCopy(BackendContext_DX12* backendContext, Ff return FFX_OK; } -static FfxErrorCode executeRenderJobClearFloat(BackendContext_DX12* backendContext, FfxRenderJobDescription* job, ID3D12Device* dx12Device, ID3D12GraphicsCommandList* dx12CommandList) +static FfxErrorCode executeGpuJobClearFloat(BackendContext_DX12* backendContext, FfxGpuJobDescription* job, ID3D12Device* dx12Device, ID3D12GraphicsCommandList* dx12CommandList) { uint32_t idx = job->clearJobDescriptor.target.internalIndex; BackendContext_DX12::Resource ffxResource = backendContext->resources[idx]; @@ -1444,7 +1444,7 @@ static FfxErrorCode executeRenderJobClearFloat(BackendContext_DX12* backendConte return FFX_OK; } -FfxErrorCode ExecuteRenderJobsDX12( +FfxErrorCode ExecuteGpuJobsDX12( FfxFsr2Interface* backendInterface, FfxCommandList commandList) { @@ -1454,25 +1454,25 @@ FfxErrorCode ExecuteRenderJobsDX12( FfxErrorCode errorCode = FFX_OK; - // execute all renderjobs - for (uint32_t currentRenderJobIndex = 0; currentRenderJobIndex < backendContext->renderJobCount; ++currentRenderJobIndex) { + // execute all GpuJobs + for (uint32_t currentGpuJobIndex = 0; currentGpuJobIndex < backendContext->gpuJobCount; ++currentGpuJobIndex) { - FfxRenderJobDescription* renderJob = &backendContext->renderJobs[currentRenderJobIndex]; + FfxGpuJobDescription* GpuJob = &backendContext->gpuJobs[currentGpuJobIndex]; ID3D12GraphicsCommandList* dx12CommandList = reinterpret_cast(commandList); ID3D12Device* dx12Device = reinterpret_cast(backendContext->device); - switch (renderJob->jobType) { + switch (GpuJob->jobType) { - case FFX_RENDER_JOB_CLEAR_FLOAT: - errorCode = executeRenderJobClearFloat(backendContext, renderJob, dx12Device, dx12CommandList); + case FFX_GPU_JOB_CLEAR_FLOAT: + errorCode = executeGpuJobClearFloat(backendContext, GpuJob, dx12Device, dx12CommandList); break; - case FFX_RENDER_JOB_COPY: - errorCode = executeRenderJobCopy(backendContext, renderJob, dx12Device, dx12CommandList); + case FFX_GPU_JOB_COPY: + errorCode = executeGpuJobCopy(backendContext, GpuJob, dx12Device, dx12CommandList); break; - case FFX_RENDER_JOB_COMPUTE: - errorCode = executeRenderJobCompute(backendContext, renderJob, dx12Device, dx12CommandList); + case FFX_GPU_JOB_COMPUTE: + errorCode = executeGpuJobCompute(backendContext, GpuJob, dx12Device, dx12CommandList); break; default: @@ -1485,7 +1485,7 @@ FfxErrorCode ExecuteRenderJobsDX12( errorCode == FFX_OK, FFX_ERROR_BACKEND_API_ERROR); - backendContext->renderJobCount = 0; + backendContext->gpuJobCount = 0; return FFX_OK; } diff --git a/src/ffx-fsr2-api/dx12/ffx_fsr2_dx12.h b/src/ffx-fsr2-api/dx12/ffx_fsr2_dx12.h index 4129c0a..d3626fc 100644 --- a/src/ffx-fsr2-api/dx12/ffx_fsr2_dx12.h +++ b/src/ffx-fsr2-api/dx12/ffx_fsr2_dx12.h @@ -93,7 +93,7 @@ FFX_API FfxCommandList ffxGetCommandListDX12(ID3D12CommandList* cmdList); FFX_API FfxResource ffxGetResourceDX12( FfxFsr2Context* context, ID3D12Resource* resDx12, - wchar_t* name = nullptr, + const wchar_t* name = nullptr, FfxResourceStates state = FFX_RESOURCE_STATE_COMPUTE_READ, UINT shaderComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING); diff --git a/src/ffx-fsr2-api/dx12/shaders/ffx_fsr2_shaders_dx12.cpp b/src/ffx-fsr2-api/dx12/shaders/ffx_fsr2_shaders_dx12.cpp index 6e74429..c61555b 100644 --- a/src/ffx-fsr2-api/dx12/shaders/ffx_fsr2_shaders_dx12.cpp +++ b/src/ffx-fsr2-api/dx12/shaders/ffx_fsr2_shaders_dx12.cpp @@ -56,14 +56,12 @@ #include "ffx_fsr2_reconstruct_previous_depth_pass_wave64_16bit_permutations.h" #include "ffx_fsr2_rcas_pass_wave64_16bit_permutations.h" -#include // for memset - #if defined(POPULATE_PERMUTATION_KEY) #undef POPULATE_PERMUTATION_KEY #endif // #if defined(POPULATE_PERMUTATION_KEY) #define POPULATE_PERMUTATION_KEY(options, key) \ key.index = 0; \ -key.FFX_FSR2_OPTION_USE_LANCZOS_LUT = FFX_CONTAINS_FLAG(options, FSR2_SHADER_PERMUTATION_LANCZOS_LUT); \ +key.FFX_FSR2_OPTION_REPROJECT_USE_LANCZOS_TYPE = FFX_CONTAINS_FLAG(options, FSR2_SHADER_PERMUTATION_USE_LANCZOS_TYPE); \ key.FFX_FSR2_OPTION_HDR_COLOR_INPUT = FFX_CONTAINS_FLAG(options, FSR2_SHADER_PERMUTATION_HDR_COLOR_INPUT); \ key.FFX_FSR2_OPTION_LOW_RESOLUTION_MOTION_VECTORS = FFX_CONTAINS_FLAG(options, FSR2_SHADER_PERMUTATION_LOW_RES_MOTION_VECTORS); \ key.FFX_FSR2_OPTION_JITTERED_MOTION_VECTORS = FFX_CONTAINS_FLAG(options, FSR2_SHADER_PERMUTATION_JITTER_MOTION_VECTORS); \ @@ -278,10 +276,7 @@ static Fsr2ShaderBlobDX12 fsr2GetComputeLuminancePyramidPassPermutationBlobByInd } } -static Fsr2ShaderBlobDX12 fsr2GetAutogenReactivePassPermutationBlobByIndex( - uint32_t permutationOptions, - bool isWave64, - bool is16bit) { +static Fsr2ShaderBlobDX12 fsr2GetAutogenReactivePassPermutationBlobByIndex(uint32_t permutationOptions, bool isWave64, bool is16bit) { ffx_fsr2_autogen_reactive_pass_PermutationKey key; @@ -315,10 +310,7 @@ static Fsr2ShaderBlobDX12 fsr2GetAutogenReactivePassPermutationBlobByIndex( } } -FfxErrorCode fsr2GetPermutationBlobByIndex( - FfxFsr2Pass passId, - uint32_t permutationOptions, - Fsr2ShaderBlobDX12* outBlob) { +Fsr2ShaderBlobDX12 fsr2GetPermutationBlobByIndex(FfxFsr2Pass passId, uint32_t permutationOptions) { bool isWave64 = FFX_CONTAINS_FLAG(permutationOptions, FSR2_SHADER_PERMUTATION_FORCE_WAVE64); bool is16bit = FFX_CONTAINS_FLAG(permutationOptions, FSR2_SHADER_PERMUTATION_ALLOW_FP16); @@ -326,68 +318,28 @@ FfxErrorCode fsr2GetPermutationBlobByIndex( switch (passId) { case FFX_FSR2_PASS_PREPARE_INPUT_COLOR: - { - Fsr2ShaderBlobDX12 blob = fsr2GetPrepareInputColorPassPermutationBlobByIndex(permutationOptions, isWave64, is16bit); - memcpy(outBlob, &blob, sizeof(Fsr2ShaderBlobDX12)); - return FFX_OK; - } - + return fsr2GetPrepareInputColorPassPermutationBlobByIndex(permutationOptions, isWave64, is16bit); case FFX_FSR2_PASS_DEPTH_CLIP: - { - Fsr2ShaderBlobDX12 blob = fsr2GetDepthClipPassPermutationBlobByIndex(permutationOptions, isWave64, is16bit); - memcpy(outBlob, &blob, sizeof(Fsr2ShaderBlobDX12)); - return FFX_OK; - } - + return fsr2GetDepthClipPassPermutationBlobByIndex(permutationOptions, isWave64, is16bit); case FFX_FSR2_PASS_RECONSTRUCT_PREVIOUS_DEPTH: - { - Fsr2ShaderBlobDX12 blob = fsr2GetReconstructPreviousDepthPassPermutationBlobByIndex(permutationOptions, isWave64, is16bit); - memcpy(outBlob, &blob, sizeof(Fsr2ShaderBlobDX12)); - return FFX_OK; - } - + return fsr2GetReconstructPreviousDepthPassPermutationBlobByIndex(permutationOptions, isWave64, is16bit); case FFX_FSR2_PASS_LOCK: - { - Fsr2ShaderBlobDX12 blob = fsr2GetLockPassPermutationBlobByIndex(permutationOptions, isWave64, is16bit); - memcpy(outBlob, &blob, sizeof(Fsr2ShaderBlobDX12)); - return FFX_OK; - } - + return fsr2GetLockPassPermutationBlobByIndex(permutationOptions, isWave64, is16bit); case FFX_FSR2_PASS_ACCUMULATE: case FFX_FSR2_PASS_ACCUMULATE_SHARPEN: - { - Fsr2ShaderBlobDX12 blob = fsr2GetAccumulatePassPermutationBlobByIndex(permutationOptions, isWave64, is16bit); - memcpy(outBlob, &blob, sizeof(Fsr2ShaderBlobDX12)); - return FFX_OK; - } - + return fsr2GetAccumulatePassPermutationBlobByIndex(permutationOptions, isWave64, is16bit); case FFX_FSR2_PASS_RCAS: - { - Fsr2ShaderBlobDX12 blob = fsr2GetRCASPassPermutationBlobByIndex(permutationOptions, isWave64, is16bit); - memcpy(outBlob, &blob, sizeof(Fsr2ShaderBlobDX12)); - return FFX_OK; - } - + return fsr2GetRCASPassPermutationBlobByIndex(permutationOptions, isWave64, is16bit); case FFX_FSR2_PASS_COMPUTE_LUMINANCE_PYRAMID: - { - Fsr2ShaderBlobDX12 blob = fsr2GetComputeLuminancePyramidPassPermutationBlobByIndex(permutationOptions, isWave64, is16bit); - memcpy(outBlob, &blob, sizeof(Fsr2ShaderBlobDX12)); - return FFX_OK; - } - + return fsr2GetComputeLuminancePyramidPassPermutationBlobByIndex(permutationOptions, isWave64, is16bit); case FFX_FSR2_PASS_GENERATE_REACTIVE: - { - Fsr2ShaderBlobDX12 blob = fsr2GetAutogenReactivePassPermutationBlobByIndex(permutationOptions, isWave64, is16bit); - memcpy(outBlob, &blob, sizeof(Fsr2ShaderBlobDX12)); - return FFX_OK; - } - + return fsr2GetAutogenReactivePassPermutationBlobByIndex(permutationOptions, isWave64, is16bit); default: FFX_ASSERT_FAIL("Should never reach here."); break; } // return an empty blob - memset(&outBlob, 0, sizeof(Fsr2ShaderBlobDX12)); - return FFX_OK; + Fsr2ShaderBlobDX12 emptyBlob = {}; + return emptyBlob; } diff --git a/src/ffx-fsr2-api/dx12/shaders/ffx_fsr2_shaders_dx12.h b/src/ffx-fsr2-api/dx12/shaders/ffx_fsr2_shaders_dx12.h index bee0c1b..70a4003 100644 --- a/src/ffx-fsr2-api/dx12/shaders/ffx_fsr2_shaders_dx12.h +++ b/src/ffx-fsr2-api/dx12/shaders/ffx_fsr2_shaders_dx12.h @@ -35,7 +35,7 @@ typedef struct Fsr2ShaderBlobDX12 { const uint32_t size; // Size in bytes. const uint32_t uavCount; // Number of UAV. const uint32_t srvCount; // Number of SRV. - const uint32_t cbvCount; // Number of CBs. + const uint32_t cbvCount; // Number of CBs. const char** boundUAVResourceNames; const uint32_t* boundUAVResources; // Pointer to an array of bound UAV resources. const char** boundSRVResourceNames; @@ -47,7 +47,7 @@ typedef struct Fsr2ShaderBlobDX12 { // The different options which contribute to permutations. typedef enum Fs2ShaderPermutationOptionsDX12 { - FSR2_SHADER_PERMUTATION_LANCZOS_LUT = (1<<0), // FFX_FSR2_OPTION_USE_LANCZOS_LUT + FSR2_SHADER_PERMUTATION_USE_LANCZOS_TYPE = (1<<0), // FFX_FSR2_OPTION_REPROJECT_USE_LANCZOS_TYPE. Off means reference, On means LUT FSR2_SHADER_PERMUTATION_HDR_COLOR_INPUT = (1<<1), // FFX_FSR2_OPTION_HDR_COLOR_INPUT FSR2_SHADER_PERMUTATION_LOW_RES_MOTION_VECTORS = (1<<2), // FFX_FSR2_OPTION_LOW_RESOLUTION_MOTION_VECTORS FSR2_SHADER_PERMUTATION_JITTER_MOTION_VECTORS = (1<<3), // FFX_FSR2_OPTION_JITTERED_MOTION_VECTORS @@ -58,10 +58,7 @@ typedef enum Fs2ShaderPermutationOptionsDX12 { } Fs2ShaderPermutationOptionsDX12; // Get a DX12 shader blob for the specified pass and permutation index. -FfxErrorCode fsr2GetPermutationBlobByIndex( - FfxFsr2Pass passId, - uint32_t permutationOptions, - Fsr2ShaderBlobDX12* outBlob); +Fsr2ShaderBlobDX12 fsr2GetPermutationBlobByIndex(FfxFsr2Pass passId, uint32_t permutationOptions); #if defined(__cplusplus) } diff --git a/src/ffx-fsr2-api/ffx_assert.cpp b/src/ffx-fsr2-api/ffx_assert.cpp index 03680fd..7705490 100644 --- a/src/ffx-fsr2-api/ffx_assert.cpp +++ b/src/ffx-fsr2-api/ffx_assert.cpp @@ -23,7 +23,9 @@ #include // for malloc() #ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN +#endif #include // required for OutputDebugString() #include // required for sprintf_s #endif // #ifndef _WIN32 @@ -47,8 +49,8 @@ bool ffxAssertReport(const char* file, int32_t line, const char* condition, cons #ifdef _WIN32 // form the final assertion string and output to the TTY. - const size_t bufferSize = snprintf(NULL, 0, "%s(%d): ASSERTION FAILED. %s\n", file, line, message ? message : condition) + 1; - char* tempBuf = (char*)malloc(bufferSize); + const size_t bufferSize = static_cast(snprintf(nullptr, 0, "%s(%d): ASSERTION FAILED. %s\n", file, line, message ? message : condition)) + 1; + char* tempBuf = static_cast(malloc(bufferSize)); if (!tempBuf) { return true; diff --git a/src/ffx-fsr2-api/ffx_assert.h b/src/ffx-fsr2-api/ffx_assert.h index b6daee2..f96b157 100644 --- a/src/ffx-fsr2-api/ffx_assert.h +++ b/src/ffx-fsr2-api/ffx_assert.h @@ -65,7 +65,7 @@ typedef void (*FfxAssertCallback)(const char* message); /// @param [in] file The name of the file as a string. /// @param [in] line The index of the line in the file. /// @param [in] condition The boolean condition that was tested. -/// @param [in] message The optional message to print. +/// @param [in] msg The optional message to print. /// /// @returns /// Always returns true. @@ -78,7 +78,7 @@ FFX_API bool ffxAssertReport(const char* file, int32_t line, const char* conditi /// FFX_API void ffxAssertSetPrintingCallback(FfxAssertCallback callback); -#if _DEBUG +#ifdef _DEBUG /// Standard assert macro. #define FFX_ASSERT(condition) \ do \ diff --git a/src/ffx-fsr2-api/ffx_fsr2.cpp b/src/ffx-fsr2-api/ffx_fsr2.cpp index 39d8f37..ebd69d5 100644 --- a/src/ffx-fsr2-api/ffx_fsr2.cpp +++ b/src/ffx-fsr2-api/ffx_fsr2.cpp @@ -32,6 +32,10 @@ #include "ffx_fsr2_maximum_bias.h" +#ifdef __clang__ +#pragma clang diagnostic ignored "-Wunused-variable" +#endif + // max queued frames for descriptor management static const uint32_t FSR2_MAX_QUEUED_FRAMES = 16; @@ -41,60 +45,60 @@ static const uint32_t FSR2_MAX_QUEUED_FRAMES = 16; typedef struct ResourceBinding { uint32_t index; - char name[64]; + wchar_t name[64]; }ResourceBinding; static const ResourceBinding srvResourceBindingTable[] = { - {FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_COLOR, "r_input_color_jittered"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_MOTION_VECTORS, "r_motion_vectors"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_DEPTH, "r_depth" }, - {FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_EXPOSURE, "r_exposure"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_REACTIVE_MASK, "r_reactive_mask"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_TRANSPARENCY_AND_COMPOSITION_MASK, "r_transparency_and_composition_mask"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_RECONSTRUCTED_PREVIOUS_NEAREST_DEPTH, "r_ReconstructedPrevNearestDepth"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_MOTION_VECTORS, "r_dilated_motion_vectors"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_DEPTH, "r_dilatedDepth"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_INTERNAL_UPSCALED_COLOR, "r_internal_upscaled_color"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_LOCK_STATUS, "r_lock_status"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_DEPTH_CLIP, "r_depth_clip"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_PREPARED_INPUT_COLOR, "r_prepared_input_color"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_LUMA_HISTORY, "r_luma_history" }, - {FFX_FSR2_RESOURCE_IDENTIFIER_RCAS_INPUT, "r_rcas_input"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_LANCZOS_LUT, "r_lanczos_lut"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE, "r_imgMips"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_SHADING_CHANGE, "r_img_mip_shading_change"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_5, "r_img_mip_5"}, - {FFX_FSR2_RESOURCE_IDENTITIER_UPSAMPLE_MAXIMUM_BIAS_LUT, "r_upsample_maximum_bias_lut"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_REACTIVE_MAX, "r_reactive_max"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_COLOR, L"r_input_color_jittered"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_MOTION_VECTORS, L"r_motion_vectors"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_DEPTH, L"r_depth" }, + {FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_EXPOSURE, L"r_exposure"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_REACTIVE_MASK, L"r_reactive_mask"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_TRANSPARENCY_AND_COMPOSITION_MASK, L"r_transparency_and_composition_mask"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_RECONSTRUCTED_PREVIOUS_NEAREST_DEPTH, L"r_reconstructed_previous_nearest_depth"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_MOTION_VECTORS, L"r_dilated_motion_vectors"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_DEPTH, L"r_dilatedDepth"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_INTERNAL_UPSCALED_COLOR, L"r_internal_upscaled_color"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_LOCK_STATUS, L"r_lock_status"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_DEPTH_CLIP, L"r_depth_clip"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_PREPARED_INPUT_COLOR, L"r_prepared_input_color"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_LUMA_HISTORY, L"r_luma_history" }, + {FFX_FSR2_RESOURCE_IDENTIFIER_RCAS_INPUT, L"r_rcas_input"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_LANCZOS_LUT, L"r_lanczos_lut"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE, L"r_imgMips"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_SHADING_CHANGE, L"r_img_mip_shading_change"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_5, L"r_img_mip_5"}, + {FFX_FSR2_RESOURCE_IDENTITIER_UPSAMPLE_MAXIMUM_BIAS_LUT, L"r_upsample_maximum_bias_lut"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_REACTIVE_MASKS, L"r_dilated_reactive_masks"}, }; static const ResourceBinding uavResourceBindingTable[] = { - {FFX_FSR2_RESOURCE_IDENTIFIER_RECONSTRUCTED_PREVIOUS_NEAREST_DEPTH, "rw_ReconstructedPrevNearestDepth"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_MOTION_VECTORS, "rw_dilated_motion_vectors"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_DEPTH, "rw_dilatedDepth"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_INTERNAL_UPSCALED_COLOR, "rw_internal_upscaled_color"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_LOCK_STATUS, "rw_lock_status"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_DEPTH_CLIP, "rw_depth_clip"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_PREPARED_INPUT_COLOR, "rw_prepared_input_color"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_LUMA_HISTORY, "rw_luma_history"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_UPSCALED_OUTPUT, "rw_upscaled_output"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_SHADING_CHANGE, "rw_img_mip_shading_change"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_5, "rw_img_mip_5"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_REACTIVE_MAX, "rw_reactive_max"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_EXPOSURE, "rw_exposure"}, - {FFX_FSR2_RESOURCE_IDENTIFIER_SPD_ATOMIC_COUNT, "rw_spd_global_atomic"}, -#if defined(FFX_INTERNAL) - {FFX_FSR2_RESOURCE_IDENTIFIER_DEBUG_OUTPUT, "rw_debug_out"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_RECONSTRUCTED_PREVIOUS_NEAREST_DEPTH, L"rw_reconstructed_previous_nearest_depth"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_MOTION_VECTORS, L"rw_dilated_motion_vectors"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_DEPTH, L"rw_dilatedDepth"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_INTERNAL_UPSCALED_COLOR, L"rw_internal_upscaled_color"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_LOCK_STATUS, L"rw_lock_status"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_DEPTH_CLIP, L"rw_depth_clip"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_PREPARED_INPUT_COLOR, L"rw_prepared_input_color"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_LUMA_HISTORY, L"rw_luma_history"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_UPSCALED_OUTPUT, L"rw_upscaled_output"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_SHADING_CHANGE, L"rw_img_mip_shading_change"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_5, L"rw_img_mip_5"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_REACTIVE_MASKS, L"rw_dilated_reactive_masks"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_EXPOSURE, L"rw_exposure"}, + {FFX_FSR2_RESOURCE_IDENTIFIER_SPD_ATOMIC_COUNT, L"rw_spd_global_atomic"}, +#if defined(FFX_INTERNAL) + {FFX_FSR2_RESOURCE_IDENTIFIER_DEBUG_OUTPUT, L"rw_debug_out"}, #endif }; static const ResourceBinding cbResourceBindingTable[] = { - {FFX_FSR2_CONSTANTBUFFER_IDENTIFIER_FSR2, "cbFSR2"}, - {FFX_FSR2_CONSTANTBUFFER_IDENTIFIER_SPD, "cbSPD"}, - {FFX_FSR2_CONSTANTBUFFER_IDENTIFIER_RCAS, "cbRCAS"}, + {FFX_FSR2_CONSTANTBUFFER_IDENTIFIER_FSR2, L"cbFSR2"}, + {FFX_FSR2_CONSTANTBUFFER_IDENTIFIER_SPD, L"cbSPD"}, + {FFX_FSR2_CONSTANTBUFFER_IDENTIFIER_RCAS, L"cbRCAS"}, }; // Broad structure of the root signature. @@ -124,8 +128,8 @@ typedef struct Fsr2GenerateReactiveConstants { float scale; float threshold; + float binaryValue; uint32_t flags; - float _Padding; } Fsr2GenerateReactiveConstants; typedef union Fsr2SecondaryUnion { @@ -182,7 +186,7 @@ static FfxErrorCode patchResourceBindings(FfxPipelineState* inoutPipeline) int32_t mapIndex = 0; for (mapIndex = 0; mapIndex < _countof(srvResourceBindingTable); ++mapIndex) { - if (0 == strcmp(srvResourceBindingTable[mapIndex].name, inoutPipeline->srvResourceBindings[srvIndex].name)) + if (0 == wcscmp(srvResourceBindingTable[mapIndex].name, inoutPipeline->srvResourceBindings[srvIndex].name)) break; } if (mapIndex == _countof(srvResourceBindingTable)) @@ -196,7 +200,7 @@ static FfxErrorCode patchResourceBindings(FfxPipelineState* inoutPipeline) int32_t mapIndex = 0; for (mapIndex = 0; mapIndex < _countof(uavResourceBindingTable); ++mapIndex) { - if (0 == strcmp(uavResourceBindingTable[mapIndex].name, inoutPipeline->uavResourceBindings[uavIndex].name)) + if (0 == wcscmp(uavResourceBindingTable[mapIndex].name, inoutPipeline->uavResourceBindings[uavIndex].name)) break; } if (mapIndex == _countof(uavResourceBindingTable)) @@ -210,7 +214,7 @@ static FfxErrorCode patchResourceBindings(FfxPipelineState* inoutPipeline) int32_t mapIndex = 0; for (mapIndex = 0; mapIndex < _countof(cbResourceBindingTable); ++mapIndex) { - if (0 == strcmp(cbResourceBindingTable[mapIndex].name, inoutPipeline->cbResourceBindings[cbIndex].name)) + if (0 == wcscmp(cbResourceBindingTable[mapIndex].name, inoutPipeline->cbResourceBindings[cbIndex].name)) break; } if (mapIndex == _countof(cbResourceBindingTable)) @@ -284,7 +288,7 @@ static FfxErrorCode fsr2Create(FfxFsr2Context_Private* context, const FfxFsr2Con memcpy(&context->contextDescription, contextDescription, sizeof(FfxFsr2ContextDescription)); // Create the device. - FfxErrorCode errorCode = context->contextDescription.callbacks.fpCreateDevice(&context->contextDescription.callbacks, context->device); + FfxErrorCode errorCode = context->contextDescription.callbacks.fpCreateBackendContext(&context->contextDescription.callbacks, context->device); FFX_RETURN_ON_ERROR(errorCode == FFX_OK, errorCode); // call out for device caps. @@ -362,8 +366,8 @@ static FfxErrorCode fsr2Create(FfxFsr2Context_Private* context, const FfxFsr2Con { FFX_FSR2_RESOURCE_IDENTIFIER_SPD_ATOMIC_COUNT, L"FSR2_SpdAtomicCounter", FFX_RESOURCE_USAGE_UAV, FFX_SURFACE_FORMAT_R32_UINT, 1, 1, 1, FFX_RESOURCE_FLAGS_ALIASABLE, sizeof(atomicInitData), &atomicInitData }, - { FFX_FSR2_RESOURCE_IDENTIFIER_REACTIVE_MAX, L"FSR2_ReactiveMaskMax", FFX_RESOURCE_USAGE_UAV, - FFX_SURFACE_FORMAT_R8_UNORM, contextDescription->maxRenderSize.width, contextDescription->maxRenderSize.height, 1, FFX_RESOURCE_FLAGS_ALIASABLE }, + { FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_REACTIVE_MASKS, L"FSR2_DilatedReactiveMasks", FFX_RESOURCE_USAGE_UAV, + FFX_SURFACE_FORMAT_R8G8_UNORM, contextDescription->maxRenderSize.width, contextDescription->maxRenderSize.height, 1, FFX_RESOURCE_FLAGS_ALIASABLE }, { FFX_FSR2_RESOURCE_IDENTIFIER_LANCZOS_LUT, L"FSR2_LanczosLutData", FFX_RESOURCE_USAGE_READ_ONLY, FFX_SURFACE_FORMAT_R16_SNORM, lanczos2LutWidth, 1, 1, FFX_RESOURCE_FLAGS_NONE, sizeof(lanczos2Weights), lanczos2Weights }, @@ -395,7 +399,7 @@ static FfxErrorCode fsr2Create(FfxFsr2Context_Private* context, const FfxFsr2Con const FfxResourceType resourceType = currentSurfaceDescription->height > 1 ? FFX_RESOURCE_TYPE_TEXTURE2D : texture1dResourceType; const FfxResourceDescription resourceDescription = { resourceType, currentSurfaceDescription->format, currentSurfaceDescription->width, currentSurfaceDescription->height, 1, currentSurfaceDescription->mipCount }; const FfxResourceStates initialState = (currentSurfaceDescription->usage == FFX_RESOURCE_USAGE_READ_ONLY) ? FFX_RESOURCE_STATE_COMPUTE_READ : FFX_RESOURCE_STATE_UNORDERED_ACCESS; - const FfxCreateResourceDescription createResourceDescription = { context->device, FFX_HEAP_TYPE_DEFAULT, resourceDescription, initialState, currentSurfaceDescription->initDataSize, currentSurfaceDescription->initData, currentSurfaceDescription->name, currentSurfaceDescription->usage, currentSurfaceDescription->id }; + const FfxCreateResourceDescription createResourceDescription = { FFX_HEAP_TYPE_DEFAULT, resourceDescription, initialState, currentSurfaceDescription->initDataSize, currentSurfaceDescription->initData, currentSurfaceDescription->name, currentSurfaceDescription->usage, currentSurfaceDescription->id }; FFX_VALIDATE(context->contextDescription.callbacks.fpCreateResource(&context->contextDescription.callbacks, &createResourceDescription, &context->srvResources[currentSurfaceDescription->id])); } @@ -430,7 +434,7 @@ static void fsr2SafeReleaseDevice(FfxFsr2Context_Private* context, FfxDevice* de return; } - context->contextDescription.callbacks.fpDestroyDevice(&context->contextDescription.callbacks, *device); + context->contextDescription.callbacks.fpDestroyBackendContext(&context->contextDescription.callbacks); *device = nullptr; } @@ -480,13 +484,13 @@ static void scheduleDispatch(FfxFsr2Context_Private* context, const FfxFsr2Dispa const uint32_t currentResourceId = pipeline->srvResourceBindings[currentShaderResourceViewIndex].resourceIdentifier; const FfxResourceInternal currentResource = context->srvResources[currentResourceId]; jobDescriptor.srvs[currentShaderResourceViewIndex] = currentResource; - strcpy_s(jobDescriptor.srvNames[currentShaderResourceViewIndex], pipeline->srvResourceBindings[currentShaderResourceViewIndex].name); + wcscpy_s(jobDescriptor.srvNames[currentShaderResourceViewIndex], pipeline->srvResourceBindings[currentShaderResourceViewIndex].name); } for (uint32_t currentUnorderedAccessViewIndex = 0; currentUnorderedAccessViewIndex < pipeline->uavCount; ++currentUnorderedAccessViewIndex) { const uint32_t currentResourceId = pipeline->uavResourceBindings[currentUnorderedAccessViewIndex].resourceIdentifier; - strcpy_s(jobDescriptor.uavNames[currentUnorderedAccessViewIndex], pipeline->uavResourceBindings[currentUnorderedAccessViewIndex].name); + wcscpy_s(jobDescriptor.uavNames[currentUnorderedAccessViewIndex], pipeline->uavResourceBindings[currentUnorderedAccessViewIndex].name); if (currentResourceId >= FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_0 && currentResourceId <= FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_12) { @@ -508,14 +512,14 @@ static void scheduleDispatch(FfxFsr2Context_Private* context, const FfxFsr2Dispa jobDescriptor.pipeline = *pipeline; for (uint32_t currentRootConstantIndex = 0; currentRootConstantIndex < pipeline->constCount; ++currentRootConstantIndex) { - strcpy_s( jobDescriptor.cbNames[currentRootConstantIndex], pipeline->cbResourceBindings[currentRootConstantIndex].name); + wcscpy_s( jobDescriptor.cbNames[currentRootConstantIndex], pipeline->cbResourceBindings[currentRootConstantIndex].name); jobDescriptor.cbs[currentRootConstantIndex] = globalFsr2ConstantBuffers[pipeline->cbResourceBindings[currentRootConstantIndex].resourceIdentifier]; } - FfxRenderJobDescription dispatchJob = { FFX_RENDER_JOB_COMPUTE }; + FfxGpuJobDescription dispatchJob = { FFX_GPU_JOB_COMPUTE }; dispatchJob.computeJobDescriptor = jobDescriptor; - context->contextDescription.callbacks.fpScheduleRenderJob(&context->contextDescription.callbacks, &dispatchJob); + context->contextDescription.callbacks.fpScheduleGpuJob(&context->contextDescription.callbacks, &dispatchJob); } static FfxErrorCode fsr2Dispatch(FfxFsr2Context_Private* context, const FfxFsr2DispatchDescription* params) @@ -532,26 +536,28 @@ static FfxErrorCode fsr2Dispatch(FfxFsr2Context_Private* context, const FfxFsr2D FFX_RETURN_ON_ERROR(errorCode == FFX_OK, errorCode); } + static const float lockInitialLifetime = 1.0f; + if (context->firstExecution) { const float clearValuesToZeroFloat[]{ 0.f, 0.f, 0.f, 0.f }; - FfxRenderJobDescription clearJob = { FFX_RENDER_JOB_CLEAR_FLOAT }; + FfxGpuJobDescription clearJob = { FFX_GPU_JOB_CLEAR_FLOAT }; memcpy(clearJob.clearJobDescriptor.color, clearValuesToZeroFloat, 4 * sizeof(float)); clearJob.clearJobDescriptor.target = context->srvResources[FFX_FSR2_RESOURCE_IDENTIFIER_LOCK_STATUS_1]; - context->contextDescription.callbacks.fpScheduleRenderJob(&context->contextDescription.callbacks, &clearJob); + context->contextDescription.callbacks.fpScheduleGpuJob(&context->contextDescription.callbacks, &clearJob); clearJob.clearJobDescriptor.target = context->srvResources[FFX_FSR2_RESOURCE_IDENTIFIER_LOCK_STATUS_2]; - context->contextDescription.callbacks.fpScheduleRenderJob(&context->contextDescription.callbacks, &clearJob); + context->contextDescription.callbacks.fpScheduleGpuJob(&context->contextDescription.callbacks, &clearJob); clearJob.clearJobDescriptor.target = context->srvResources[FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_MOTION_VECTORS]; - context->contextDescription.callbacks.fpScheduleRenderJob(&context->contextDescription.callbacks, &clearJob); + context->contextDescription.callbacks.fpScheduleGpuJob(&context->contextDescription.callbacks, &clearJob); clearJob.clearJobDescriptor.target = context->srvResources[FFX_FSR2_RESOURCE_IDENTIFIER_PREPARED_INPUT_COLOR]; - context->contextDescription.callbacks.fpScheduleRenderJob(&context->contextDescription.callbacks, &clearJob); + context->contextDescription.callbacks.fpScheduleGpuJob(&context->contextDescription.callbacks, &clearJob); clearJob.clearJobDescriptor.target = context->srvResources[FFX_FSR2_RESOURCE_IDENTIFIER_LUMA_HISTORY]; - context->contextDescription.callbacks.fpScheduleRenderJob(&context->contextDescription.callbacks, &clearJob); + context->contextDescription.callbacks.fpScheduleGpuJob(&context->contextDescription.callbacks, &clearJob); clearJob.clearJobDescriptor.target = context->srvResources[FFX_FSR2_RESOURCE_IDENTIFIER_DEPTH_CLIP]; - context->contextDescription.callbacks.fpScheduleRenderJob(&context->contextDescription.callbacks, &clearJob); - clearJob.clearJobDescriptor.target = context->srvResources[FFX_FSR2_RESOURCE_IDENTIFIER_REACTIVE_MAX]; - context->contextDescription.callbacks.fpScheduleRenderJob(&context->contextDescription.callbacks, &clearJob); + context->contextDescription.callbacks.fpScheduleGpuJob(&context->contextDescription.callbacks, &clearJob); + clearJob.clearJobDescriptor.target = context->srvResources[FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_REACTIVE_MASKS]; + context->contextDescription.callbacks.fpScheduleGpuJob(&context->contextDescription.callbacks, &clearJob); } // Prepare per frame descriptor tables @@ -666,7 +672,7 @@ static FfxErrorCode fsr2Dispatch(FfxFsr2Context_Private* context, const FfxFsr2D // lock data, assuming jitter sequence length computation for now const int32_t jitterPhaseCount = ffxFsr2GetJitterPhaseCount(params->renderSize.width, context->contextDescription.displaySize.width); - context->constants.lockInitialLifetime = 1.0f; + context->constants.lockInitialLifetime = lockInitialLifetime; // init on first frame if (resetAccumulation || context->constants.jitterPhaseCount == 0) { @@ -681,7 +687,7 @@ static FfxErrorCode fsr2Dispatch(FfxFsr2Context_Private* context, const FfxFsr2D } const int32_t maxLockFrames = (int32_t)(context->constants.jitterPhaseCount) + 1; - context->constants.lockTickDelta = context->constants.lockInitialLifetime / maxLockFrames; + context->constants.lockTickDelta = lockInitialLifetime / maxLockFrames; // convert delta time to seconds and clamp to [0, 1]. context->constants.deltaTime = FFX_MAXIMUM(0.0f, FFX_MINIMUM(1.0f, params->frameTimeDelta / 1000.0f)); @@ -711,32 +717,32 @@ static FfxErrorCode fsr2Dispatch(FfxFsr2Context_Private* context, const FfxFsr2D // Clear reconstructed depth for max depth store. if (resetAccumulation) { - FfxRenderJobDescription clearJob = { FFX_RENDER_JOB_CLEAR_FLOAT }; + FfxGpuJobDescription clearJob = { FFX_GPU_JOB_CLEAR_FLOAT }; // LockStatus resource has no sign bit, callback functions are compensating for this. // Clearing the resource must follow the same logic. float clearValuesLockStatus[4]{}; - clearValuesLockStatus[LOCK_LIFETIME_REMAINING] = context->constants.lockInitialLifetime * 2.0f; + clearValuesLockStatus[LOCK_LIFETIME_REMAINING] = lockInitialLifetime * 2.0f; clearValuesLockStatus[LOCK_TEMPORAL_LUMA] = 0.0f; clearValuesLockStatus[LOCK_TRUST] = 1.0f; memcpy(clearJob.clearJobDescriptor.color, clearValuesLockStatus, 4 * sizeof(float)); clearJob.clearJobDescriptor.target = context->srvResources[lockStatusSrvResourceIndex]; - context->contextDescription.callbacks.fpScheduleRenderJob(&context->contextDescription.callbacks, &clearJob); + context->contextDescription.callbacks.fpScheduleGpuJob(&context->contextDescription.callbacks, &clearJob); const float clearValuesToZeroFloat[]{ 0.f, 0.f, 0.f, 0.f }; memcpy(clearJob.clearJobDescriptor.color, clearValuesToZeroFloat, 4 * sizeof(float)); clearJob.clearJobDescriptor.target = context->srvResources[upscaledColorSrvResourceIndex]; - context->contextDescription.callbacks.fpScheduleRenderJob(&context->contextDescription.callbacks, &clearJob); + context->contextDescription.callbacks.fpScheduleGpuJob(&context->contextDescription.callbacks, &clearJob); clearJob.clearJobDescriptor.target = context->srvResources[FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE]; - context->contextDescription.callbacks.fpScheduleRenderJob(&context->contextDescription.callbacks, &clearJob); + context->contextDescription.callbacks.fpScheduleGpuJob(&context->contextDescription.callbacks, &clearJob); if (context->contextDescription.flags & FFX_FSR2_ENABLE_AUTO_EXPOSURE) { const float clearValuesExposure[]{ -1.f, 1e8f, 0.f, 0.f }; memcpy(clearJob.clearJobDescriptor.color, clearValuesExposure, 4 * sizeof(float)); clearJob.clearJobDescriptor.target = context->srvResources[FFX_FSR2_RESOURCE_IDENTIFIER_EXPOSURE]; - context->contextDescription.callbacks.fpScheduleRenderJob(&context->contextDescription.callbacks, &clearJob); + context->contextDescription.callbacks.fpScheduleGpuJob(&context->contextDescription.callbacks, &clearJob); } } @@ -791,7 +797,7 @@ static FfxErrorCode fsr2Dispatch(FfxFsr2Context_Private* context, const FfxFsr2D // Fsr2MaxQueuedFrames must be an even number. FFX_STATIC_ASSERT((FSR2_MAX_QUEUED_FRAMES & 1) == 0); - context->contextDescription.callbacks.fpExecuteRenderJobs(&context->contextDescription.callbacks, commandList); + context->contextDescription.callbacks.fpExecuteGpuJobs(&context->contextDescription.callbacks, commandList); // release dynamic resources context->contextDescription.callbacks.fpUnregisterResources(&context->contextDescription.callbacks); @@ -814,8 +820,8 @@ FfxErrorCode ffxFsr2ContextCreate(FfxFsr2Context* context, const FfxFsr2ContextD // validate that all callbacks are set for the interface FFX_RETURN_ON_ERROR(contextDescription->callbacks.fpGetDeviceCapabilities, FFX_ERROR_INCOMPLETE_INTERFACE); - FFX_RETURN_ON_ERROR(contextDescription->callbacks.fpCreateDevice, FFX_ERROR_INCOMPLETE_INTERFACE); - FFX_RETURN_ON_ERROR(contextDescription->callbacks.fpDestroyDevice, FFX_ERROR_INCOMPLETE_INTERFACE); + FFX_RETURN_ON_ERROR(contextDescription->callbacks.fpCreateBackendContext, FFX_ERROR_INCOMPLETE_INTERFACE); + FFX_RETURN_ON_ERROR(contextDescription->callbacks.fpDestroyBackendContext, FFX_ERROR_INCOMPLETE_INTERFACE); // if a scratch buffer is declared, then we must have a size if (contextDescription->callbacks.scratchBuffer) { @@ -875,6 +881,7 @@ FfxErrorCode ffxFsr2ContextDispatch(FfxFsr2Context* context, const FfxFsr2Dispat float ffxFsr2GetUpscaleRatioFromQualityMode(FfxFsr2QualityMode qualityMode) { switch (qualityMode) { + case FFX_FSR2_QUALITY_MODE_QUALITY: return 1.5f; case FFX_FSR2_QUALITY_MODE_BALANCED: @@ -915,6 +922,17 @@ FfxErrorCode ffxFsr2GetRenderResolutionFromQualityMode( return FFX_OK; } +FfxErrorCode ffxFsr2ContextEnqueueRefreshPipelineRequest(FfxFsr2Context* context) +{ + FFX_RETURN_ON_ERROR( + context, + FFX_ERROR_INVALID_POINTER); + + FfxFsr2Context_Private* contextPrivate = (FfxFsr2Context_Private*)context; + contextPrivate->refreshPipelineStates = true; + + return FFX_OK; +} int32_t ffxFsr2GetJitterPhaseCount(int32_t renderWidth, int32_t displayWidth) { @@ -985,9 +1003,9 @@ FfxErrorCode ffxFsr2ContextGenerateReactiveMask(FfxFsr2Context* context, const F contextPrivate->contextDescription.callbacks.fpRegisterResource(&contextPrivate->contextDescription.callbacks, ¶ms->colorOpaqueOnly, &jobDescriptor.srvs[0]); contextPrivate->contextDescription.callbacks.fpRegisterResource(&contextPrivate->contextDescription.callbacks, ¶ms->colorPreUpscale, &jobDescriptor.srvs[1]); contextPrivate->contextDescription.callbacks.fpRegisterResource(&contextPrivate->contextDescription.callbacks, ¶ms->outReactive, &jobDescriptor.uavs[0]); - strcpy_s(jobDescriptor.srvNames[0], pipeline->srvResourceBindings[0].name); - strcpy_s(jobDescriptor.srvNames[1], pipeline->srvResourceBindings[1].name); - strcpy_s(jobDescriptor.uavNames[0], pipeline->uavResourceBindings[0].name); + wcscpy_s(jobDescriptor.srvNames[0], pipeline->srvResourceBindings[0].name); + wcscpy_s(jobDescriptor.srvNames[1], pipeline->srvResourceBindings[1].name); + wcscpy_s(jobDescriptor.uavNames[0], pipeline->uavResourceBindings[0].name); jobDescriptor.dimensions[0] = dispatchSrcX; jobDescriptor.dimensions[1] = dispatchSrcY; @@ -997,18 +1015,19 @@ FfxErrorCode ffxFsr2ContextGenerateReactiveMask(FfxFsr2Context* context, const F Fsr2GenerateReactiveConstants constants = {}; constants.scale = params->scale; constants.threshold = params->cutoffThreshold; + constants.binaryValue = params->binaryValue; constants.flags = params->flags; jobDescriptor.cbs[0].uint32Size = sizeof(constants); memcpy(&jobDescriptor.cbs[0].data, &constants, sizeof(constants)); - strcpy_s(jobDescriptor.cbNames[0], pipeline->cbResourceBindings[0].name); + wcscpy_s(jobDescriptor.cbNames[0], pipeline->cbResourceBindings[0].name); - FfxRenderJobDescription dispatchJob = { FFX_RENDER_JOB_COMPUTE }; + FfxGpuJobDescription dispatchJob = { FFX_GPU_JOB_COMPUTE }; dispatchJob.computeJobDescriptor = jobDescriptor; - contextPrivate->contextDescription.callbacks.fpScheduleRenderJob(&contextPrivate->contextDescription.callbacks, &dispatchJob); + contextPrivate->contextDescription.callbacks.fpScheduleGpuJob(&contextPrivate->contextDescription.callbacks, &dispatchJob); - contextPrivate->contextDescription.callbacks.fpExecuteRenderJobs(&contextPrivate->contextDescription.callbacks, commandList); + contextPrivate->contextDescription.callbacks.fpExecuteGpuJobs(&contextPrivate->contextDescription.callbacks, commandList); return FFX_OK; } diff --git a/src/ffx-fsr2-api/ffx_fsr2.h b/src/ffx-fsr2-api/ffx_fsr2.h index cf0e2ee..ff96d71 100644 --- a/src/ffx-fsr2-api/ffx_fsr2.h +++ b/src/ffx-fsr2-api/ffx_fsr2.h @@ -19,6 +19,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. + // @defgroup FSR2 #pragma once @@ -34,12 +35,12 @@ /// FidelityFX Super Resolution 2 minor version. /// /// @ingroup FSR2 -#define FFX_FSR2_VERSION_MINOR (0) +#define FFX_FSR2_VERSION_MINOR (1) /// FidelityFX Super Resolution 2 patch version. /// /// @ingroup FSR2 -#define FFX_FSR2_VERSION_PATCH (1) +#define FFX_FSR2_VERSION_PATCH (0) /// The size of the context specified in 32bit values. /// @@ -146,6 +147,7 @@ typedef struct FfxFsr2GenerateReactiveDescription { FfxDimensions2D renderSize; ///< The resolution that was used for rendering the input resources. float scale; ///< A value to scale the output float cutoffThreshold; ///< A threshold value to generate a binary reactive mask + float binaryValue; ///< A value to set for the binary reactive mask uint32_t flags; ///< Flags to determine how to generate the reactive mask } FfxFsr2GenerateReactiveDescription; diff --git a/src/ffx-fsr2-api/ffx_fsr2_interface.h b/src/ffx-fsr2-api/ffx_fsr2_interface.h index 488c7b4..db13fd0 100644 --- a/src/ffx-fsr2-api/ffx_fsr2_interface.h +++ b/src/ffx-fsr2-api/ffx_fsr2_interface.h @@ -40,7 +40,7 @@ FFX_FORWARD_DECLARE(FfxFsr2Interface); /// /// FSR2 is implemented as a composite of several compute passes each /// computing a key part of the final result. Each call to the -/// FfxFsr2ScheduleRenderJobFunc callback function will +/// FfxFsr2ScheduleGpuJobFunc callback function will /// correspond to a single pass included in FfxFsr2Pass. For a /// more comprehensive description of each pass, please refer to the FSR2 /// reference documentation. @@ -68,50 +68,13 @@ typedef enum FfxFsr2Pass { FFX_FSR2_PASS_COUNT ///< The number of passes performed by FSR2. } FfxFsr2Pass; -/// A structure containing the description used to create a -/// FfxPipeline structure. +/// Create and initialize the backend context. /// -/// A pipeline is the name given to a shader and the collection of state that -/// is required to dispatch it. In the context of FSR2 and its architecture -/// this means that a FfxPipelineDescription will map to either a -/// monolithic object in an explicit API (such as a -/// PipelineStateObject in DirectX 12). Or a shader and some -/// ancillary API objects (in something like DirectX 11). -/// -/// The contextFlags field contains a copy of the flags passed -/// to ffxFsr2ContextCreate via the flags field of -/// the FfxFsr2InitializationParams structure. These flags are -/// used to determine which permutation of a pipeline for a specific -/// FfxFsr2Pass should be used to implement the features required -/// by each application, as well as to acheive the best performance on specific -/// target hardware configurations. -/// -/// When using one of the provided backends for FSR2 (such as DirectX 12 or -/// Vulkan) the data required to create a pipeline is compiled offline and -/// included into the backend library that you are using. For cases where the -/// backend interface is overriden by providing custom callback function -/// implementations care should be taken to respect the contents of the -/// contextFlags field in order to correctly support the options -/// provided by FSR2, and acheive best performance. -/// -/// @ingroup FSR2 -typedef struct FfxPipelineDescription { - - uint32_t contextFlags; ///< A collection of FfxFsr2InitializationFlagBits which were passed to the context. - FfxFilterType* samplers; ///< Array of static samplers. - size_t samplerCount; ///< The number of samples contained inside samplers. - const uint32_t* rootConstantBufferSizes; ///< Array containing the sizes of the root constant buffers (count of 32 bit elements). - uint32_t rootConstantBufferCount; ///< The number of root constants contained within rootConstantBufferSizes. -} FfxPipelineDescription; - -/// Create (or reference) a device. -/// -/// The callback function should either create a new device or (more likely) it -/// should return an already existing device and add a reference to it (for -/// those APIs which implement reference counting, such as DirectX 11 and 12). +/// The callback function sets up the backend context for rendering. +/// It will create or reference the device and create required internal data structures. /// /// @param [in] backendInterface A pointer to the backend interface. -/// @param [out] outDevice The device that is either created (or referenced). +/// @param [in] device The FfxDevice obtained by ffxGetDevice(DX12/VK/...). /// /// @retval /// FFX_OK The operation completed successfully. @@ -119,9 +82,9 @@ typedef struct FfxPipelineDescription { /// Anything else The operation failed. /// /// @ingroup FSR2 -typedef FfxErrorCode (*FfxFsr2CreateDeviceFunc)( +typedef FfxErrorCode (*FfxFsr2CreateBackendContextFunc)( FfxFsr2Interface* backendInterface, - FfxDevice outDevice); + FfxDevice device); /// Get a list of capabilities of the device. /// @@ -153,12 +116,11 @@ typedef FfxErrorCode(*FfxFsr2GetDeviceCapabilitiesFunc)( FfxDeviceCapabilities* outDeviceCapabilities, FfxDevice device); -/// Destroy (or dereference) a device. +/// Destroy the backend context and dereference the device. /// /// This function is called when the FfxFsr2Context is destroyed. /// /// @param [in] backendInterface A pointer to the backend interface. -/// @param [in] device The FfxDevice object to be destroyed (or deferenced). /// /// @retval /// FFX_OK The operation completed successfully. @@ -166,9 +128,8 @@ typedef FfxErrorCode(*FfxFsr2GetDeviceCapabilitiesFunc)( /// Anything else The operation failed. /// /// @ingroup FSR2 -typedef FfxErrorCode(*FfxFsr2DestroyDeviceFunc)( - FfxFsr2Interface* backendInterface, - FfxDevice device); +typedef FfxErrorCode(*FfxFsr2DestroyBackendContextFunc)( + FfxFsr2Interface* backendInterface); /// Create a resource. /// @@ -308,13 +269,13 @@ typedef FfxErrorCode (*FfxFsr2DestroyPipelineFunc)( FfxPipelineState* pipeline); /// Schedule a render job to be executed on the next call of -/// FfxFsr2ExecuteRenderJobsFunc. +/// FfxFsr2ExecuteGpuJobsFunc. /// /// Render jobs can perform one of three different tasks: clear, copy or /// compute dispatches. /// /// @param [in] backendInterface A pointer to the backend interface. -/// @param [in] job A pointer to a FfxRenderJobDescription structure. +/// @param [in] job A pointer to a FfxGpuJobDescription structure. /// /// @retval /// FFX_OK The operation completed successfully. @@ -322,15 +283,15 @@ typedef FfxErrorCode (*FfxFsr2DestroyPipelineFunc)( /// Anything else The operation failed. /// /// @ingroup FSR2 -typedef FfxErrorCode (*FfxFsr2ScheduleRenderJobFunc)( +typedef FfxErrorCode (*FfxFsr2ScheduleGpuJobFunc)( FfxFsr2Interface* backendInterface, - const FfxRenderJobDescription* job); + const FfxGpuJobDescription* job); /// Execute scheduled render jobs on the comandList provided. /// /// The recording of the graphics API commands should take place in this /// callback function, the render jobs which were previously enqueued (via -/// callbacks made to FfxFsr2ScheduleRenderJobFunc) should be +/// callbacks made to FfxFsr2ScheduleGpuJobFunc) should be /// processed in the order they were received. Advanced users might choose to /// reorder the rendering jobs, but should do so with care to respect the /// resource dependencies. @@ -348,7 +309,7 @@ typedef FfxErrorCode (*FfxFsr2ScheduleRenderJobFunc)( /// Anything else The operation failed. /// /// @ingroup FSR2 -typedef FfxErrorCode (*FfxFsr2ExecuteRenderJobsFunc)( +typedef FfxErrorCode (*FfxFsr2ExecuteGpuJobsFunc)( FfxFsr2Interface* backendInterface, FfxCommandList commandList); @@ -370,8 +331,8 @@ typedef FfxErrorCode (*FfxFsr2ExecuteRenderJobsFunc)( /// FfxFsr2DestroyResourceFunc /// FfxFsr2CreatePipelineFunc /// FfxFsr2DestroyPipelineFunc -/// FfxFsr2ScheduleRenderJobFunc -/// FfxFsr2ExecuteRenderJobsFunc +/// FfxFsr2ScheduleGpuJobFunc +/// FfxFsr2ExecuteGpuJobsFunc /// /// Depending on the graphics API that is abstracted by the backend, it may be /// required that the backend is to some extent stateful. To ensure that @@ -393,9 +354,9 @@ typedef FfxErrorCode (*FfxFsr2ExecuteRenderJobsFunc)( /// @ingroup FSR2 typedef struct FfxFsr2Interface { - FfxFsr2CreateDeviceFunc fpCreateDevice; ///< A callback function to create (or reference) a device. + FfxFsr2CreateBackendContextFunc fpCreateBackendContext; ///< A callback function to create and initialize the backend context. FfxFsr2GetDeviceCapabilitiesFunc fpGetDeviceCapabilities; ///< A callback function to query device capabilites. - FfxFsr2DestroyDeviceFunc fpDestroyDevice; ///< A callback function to destroy (or dereference) a device. + FfxFsr2DestroyBackendContextFunc fpDestroyBackendContext; ///< A callback function to destroy the backendcontext. This also dereferences the device. FfxFsr2CreateResourceFunc fpCreateResource; ///< A callback function to create a resource. FfxFsr2RegisterResourceFunc fpRegisterResource; ///< A callback function to register an external resource. FfxFsr2UnregisterResourcesFunc fpUnregisterResources; ///< A callback function to unregister external resource. @@ -403,8 +364,8 @@ typedef struct FfxFsr2Interface { FfxFsr2DestroyResourceFunc fpDestroyResource; ///< A callback function to destroy a resource. FfxFsr2CreatePipelineFunc fpCreatePipeline; ///< A callback function to create a render or compute pipeline. FfxFsr2DestroyPipelineFunc fpDestroyPipeline; ///< A callback function to destroy a render or compute pipeline. - FfxFsr2ScheduleRenderJobFunc fpScheduleRenderJob; ///< A callback function to schedule a render job. - FfxFsr2ExecuteRenderJobsFunc fpExecuteRenderJobs; ///< A callback function to execute all queued render jobs. + FfxFsr2ScheduleGpuJobFunc fpScheduleGpuJob; ///< A callback function to schedule a render job. + FfxFsr2ExecuteGpuJobsFunc fpExecuteGpuJobs; ///< A callback function to execute all queued render jobs. void* scratchBuffer; ///< A preallocated buffer for memory utilized internally by the backend. size_t scratchBufferSize; ///< Size of the buffer pointed to by scratchBuffer. diff --git a/src/ffx-fsr2-api/ffx_fsr2_maximum_bias.h b/src/ffx-fsr2-api/ffx_fsr2_maximum_bias.h index 2058fef..ad840f3 100644 --- a/src/ffx-fsr2-api/ffx_fsr2_maximum_bias.h +++ b/src/ffx-fsr2-api/ffx_fsr2_maximum_bias.h @@ -23,8 +23,8 @@ #pragma once -static const int32_t FFX_FSR2_MAXIMUM_BIAS_TEXTURE_WIDTH = 16; -static const int32_t FFX_FSR2_MAXIMUM_BIAS_TEXTURE_HEIGHT = 16; +static const int FFX_FSR2_MAXIMUM_BIAS_TEXTURE_WIDTH = 16; +static const int FFX_FSR2_MAXIMUM_BIAS_TEXTURE_HEIGHT = 16; static const float ffxFsr2MaximumBias[] = { 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 1.876f, 1.809f, 1.772f, 1.753f, 1.748f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 1.869f, 1.801f, 1.764f, 1.745f, 1.739f, diff --git a/src/ffx-fsr2-api/ffx_types.h b/src/ffx-fsr2-api/ffx_types.h index dcaa866..75fb0e8 100644 --- a/src/ffx-fsr2-api/ffx_types.h +++ b/src/ffx-fsr2-api/ffx_types.h @@ -43,6 +43,9 @@ /// Maximum size of bound constant buffers. #define FFX_MAX_CONST_SIZE 64 +/// Off by default warnings +#pragma warning(disable : 4365 4710 4820 5039) + #ifdef __cplusplus extern "C" { #endif // #ifdef __cplusplus @@ -66,6 +69,7 @@ typedef enum FfxSurfaceFormat { FFX_SURFACE_FORMAT_R16_UNORM, ///< 16 bit per channel, 1 channel unsigned normalized format FFX_SURFACE_FORMAT_R16_SNORM, ///< 16 bit per channel, 1 channel signed normalized format FFX_SURFACE_FORMAT_R8_UNORM, ///< 8 bit per channel, 1 channel unsigned normalized format + FFX_SURFACE_FORMAT_R8G8_UNORM, ///< 8 bit per channel, 2 channel unsigned normalized format FFX_SURFACE_FORMAT_R32_FLOAT ///< 32 bit per channel, 1 channel float format } FfxSurfaceFormat; @@ -146,12 +150,12 @@ typedef enum FfxHeapType { } FfxHeapType; /// An enumberation for different render job types -typedef enum FfxRenderJobType { +typedef enum FfxGpuJobType { - FFX_RENDER_JOB_CLEAR_FLOAT = 0, ///< The render job is performing a floating-point clear. - FFX_RENDER_JOB_COPY = 1, ///< The render job is performing a copy. - FFX_RENDER_JOB_COMPUTE = 2, ///< The render job is performing a compute dispatch. -} FfxRenderJobType; + FFX_GPU_JOB_CLEAR_FLOAT = 0, ///< The GPU job is performing a floating-point clear. + FFX_GPU_JOB_COPY = 1, ///< The GPU job is performing a copy. + FFX_GPU_JOB_COMPUTE = 2, ///< The GPU job is performing a compute dispatch. +} FfxGpuJobType; /// A typedef representing the graphics device. typedef void* FfxDevice; @@ -211,9 +215,7 @@ typedef struct FfxResourceDescription { /// An outward facing structure containing a resource typedef struct FfxResource { void* resource; ///< pointer to the resource. -#ifdef _DEBUG wchar_t name[64]; -#endif FfxResourceDescription description; FfxResourceStates state; bool isDepth; @@ -225,12 +227,13 @@ typedef struct FfxResourceInternal { int32_t internalIndex; ///< The index of the resource. } FfxResourceInternal; + /// A structure defining a resource bind point typedef struct FfxResourceBinding { uint32_t slotIndex; uint32_t resourceIdentifier; - char name[64]; + wchar_t name[64]; }FfxResourceBinding; /// A structure encapsulating a single pass of an algorithm. @@ -250,7 +253,6 @@ typedef struct FfxPipelineState { /// A structure containing the data required to create a resource. typedef struct FfxCreateResourceDescription { - FfxDevice device; ///< The FfxDevice. FfxHeapType heapType; ///< The heap type to hold the resource, typically FFX_HEAP_TYPE_DEFAULT. FfxResourceDescription resourceDescription; ///< A resource description. FfxResourceStates initalState; ///< The initial resource state. @@ -261,6 +263,42 @@ typedef struct FfxCreateResourceDescription { uint32_t id; ///< Internal resource ID. } FfxCreateResourceDescription; +/// A structure containing the description used to create a +/// FfxPipeline structure. +/// +/// A pipeline is the name given to a shader and the collection of state that +/// is required to dispatch it. In the context of FSR2 and its architecture +/// this means that a FfxPipelineDescription will map to either a +/// monolithic object in an explicit API (such as a +/// PipelineStateObject in DirectX 12). Or a shader and some +/// ancillary API objects (in something like DirectX 11). +/// +/// The contextFlags field contains a copy of the flags passed +/// to ffxFsr2ContextCreate via the flags field of +/// the FfxFsr2InitializationParams structure. These flags are +/// used to determine which permutation of a pipeline for a specific +/// FfxFsr2Pass should be used to implement the features required +/// by each application, as well as to acheive the best performance on specific +/// target hardware configurations. +/// +/// When using one of the provided backends for FSR2 (such as DirectX 12 or +/// Vulkan) the data required to create a pipeline is compiled offline and +/// included into the backend library that you are using. For cases where the +/// backend interface is overriden by providing custom callback function +/// implementations care should be taken to respect the contents of the +/// contextFlags field in order to correctly support the options +/// provided by FSR2, and acheive best performance. +/// +/// @ingroup FSR2 +typedef struct FfxPipelineDescription { + + uint32_t contextFlags; ///< A collection of FfxFsr2InitializationFlagBits which were passed to the context. + FfxFilterType* samplers; ///< Array of static samplers. + size_t samplerCount; ///< The number of samples contained inside samplers. + const uint32_t* rootConstantBufferSizes; ///< Array containing the sizes of the root constant buffers (count of 32 bit elements). + uint32_t rootConstantBufferCount; ///< The number of root constants contained within rootConstantBufferSizes. +} FfxPipelineDescription; + /// A structure containing a constant buffer. typedef struct FfxConstantBuffer { @@ -281,12 +319,12 @@ typedef struct FfxComputeJobDescription { FfxPipelineState pipeline; ///< Compute pipeline for the render job. uint32_t dimensions[3]; ///< Dispatch dimensions. FfxResourceInternal srvs[FFX_MAX_NUM_SRVS]; ///< SRV resources to be bound in the compute job. - char srvNames[FFX_MAX_NUM_SRVS][64]; + wchar_t srvNames[FFX_MAX_NUM_SRVS][64]; FfxResourceInternal uavs[FFX_MAX_NUM_UAVS]; ///< UAV resources to be bound in the compute job. uint32_t uavMip[FFX_MAX_NUM_UAVS]; ///< Mip level of UAV resources to be bound in the compute job. - char uavNames[FFX_MAX_NUM_UAVS][64]; + wchar_t uavNames[FFX_MAX_NUM_UAVS][64]; FfxConstantBuffer cbs[FFX_MAX_NUM_CONST_BUFFERS]; ///< Constant buffers to be bound in the compute job. - char cbNames[FFX_MAX_NUM_CONST_BUFFERS][64]; + wchar_t cbNames[FFX_MAX_NUM_CONST_BUFFERS][64]; } FfxComputeJobDescription; /// A structure describing a copy render job. @@ -297,16 +335,16 @@ typedef struct FfxCopyJobDescription } FfxCopyJobDescription; /// A structure describing a single render job. -typedef struct FfxRenderJobDescription { +typedef struct FfxGpuJobDescription{ - FfxRenderJobType jobType; ///< Type of the render job. + FfxGpuJobType jobType; ///< Type of the job. union { - FfxClearFloatJobDescription clearJobDescriptor; ///< Render job descriptor. Valid when jobType is FFX_RENDER_JOB_CLEAR_FLOAT. - FfxCopyJobDescription copyJobDescriptor; ///< Render job descriptor. Valid when jobType is FFX_RENDER_JOB_COPY. - FfxComputeJobDescription computeJobDescriptor; ///< Render job descriptor. Valid when jobType is FFX_RENDER_JOB_COMPUTE. + FfxClearFloatJobDescription clearJobDescriptor; ///< Clear job descriptor. Valid when jobType is FFX_RENDER_JOB_CLEAR_FLOAT. + FfxCopyJobDescription copyJobDescriptor; ///< Copy job descriptor. Valid when jobType is FFX_RENDER_JOB_COPY. + FfxComputeJobDescription computeJobDescriptor; ///< Compute job descriptor. Valid when jobType is FFX_RENDER_JOB_COMPUTE. }; -} FfxRenderJobDescription; +} FfxGpuJobDescription; #ifdef __cplusplus } diff --git a/src/ffx-fsr2-api/shaders/ffx_common_types.h b/src/ffx-fsr2-api/shaders/ffx_common_types.h index abcb979..cf6ba99 100644 --- a/src/ffx-fsr2-api/shaders/ffx_common_types.h +++ b/src/ffx-fsr2-api/shaders/ffx_common_types.h @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. - #ifndef FFX_COMMON_TYPES_H #define FFX_COMMON_TYPES_H @@ -246,18 +245,22 @@ typedef min16int4 FfxInt16x4; #if FFX_HALF -#define FFX_MIN16_SCALAR( TypeName, BaseComponentType ) typedef min16##BaseComponentType TypeName; -#define FFX_MIN16_VECTOR( TypeName, BaseComponentType, COL ) typedef vector TypeName; -#define FFX_MIN16_MATRIX( TypeName, BaseComponentType, ROW, COL ) typedef matrix TypeName; - #if FFX_HLSL_6_2 +#define FFX_MIN16_SCALAR( TypeName, BaseComponentType ) typedef BaseComponentType##16_t TypeName; +#define FFX_MIN16_VECTOR( TypeName, BaseComponentType, COL ) typedef vector TypeName; +#define FFX_MIN16_MATRIX( TypeName, BaseComponentType, ROW, COL ) typedef matrix TypeName; + #define FFX_16BIT_SCALAR( TypeName, BaseComponentType ) typedef BaseComponentType##16_t TypeName; #define FFX_16BIT_VECTOR( TypeName, BaseComponentType, COL ) typedef vector TypeName; #define FFX_16BIT_MATRIX( TypeName, BaseComponentType, ROW, COL ) typedef matrix TypeName; #else //FFX_HLSL_6_2 +#define FFX_MIN16_SCALAR( TypeName, BaseComponentType ) typedef min16##BaseComponentType TypeName; +#define FFX_MIN16_VECTOR( TypeName, BaseComponentType, COL ) typedef vector TypeName; +#define FFX_MIN16_MATRIX( TypeName, BaseComponentType, ROW, COL ) typedef matrix TypeName; + #define FFX_16BIT_SCALAR( TypeName, BaseComponentType ) FFX_MIN16_SCALAR( TypeName, BaseComponentType ); #define FFX_16BIT_VECTOR( TypeName, BaseComponentType, COL ) FFX_MIN16_VECTOR( TypeName, BaseComponentType, COL ); #define FFX_16BIT_MATRIX( TypeName, BaseComponentType, ROW, COL ) FFX_MIN16_MATRIX( TypeName, BaseComponentType, ROW, COL ); diff --git a/src/ffx-fsr2-api/shaders/ffx_core_cpu.h b/src/ffx-fsr2-api/shaders/ffx_core_cpu.h index b753459..9bb9915 100644 --- a/src/ffx-fsr2-api/shaders/ffx_core_cpu.h +++ b/src/ffx-fsr2-api/shaders/ffx_core_cpu.h @@ -36,6 +36,10 @@ #define FFX_STATIC static #endif // #if !defined(FFX_STATIC) +#ifdef __clang__ +#pragma clang diagnostic ignored "-Wunused-variable" +#endif + /// Interpret the bit layout of an IEEE-754 floating point value as an unsigned integer. /// /// @param [in] x A 32bit floating value. diff --git a/src/ffx-fsr2-api/shaders/ffx_core_hlsl.h b/src/ffx-fsr2-api/shaders/ffx_core_hlsl.h index e1db5e3..f114687 100644 --- a/src/ffx-fsr2-api/shaders/ffx_core_hlsl.h +++ b/src/ffx-fsr2-api/shaders/ffx_core_hlsl.h @@ -1066,405 +1066,274 @@ FfxUInt32 AShrSU1(FfxUInt32 a, FfxUInt32 b) //============================================================================================================================== // Need to use manual unpack to get optimal execution (don't use packed types in buffers directly). // Unpack requires this pattern: https://gpuopen.com/first-steps-implementing-fp16/ -FfxFloat16x2 ffxUint32ToFloat16x2(FfxUInt32 x) +FFX_MIN16_F2 ffxUint32ToFloat16x2(FfxUInt32 x) { - FfxFloat32x2 t = f16tof32(FfxUInt32x2(x & 0xFFFF, x >> 16)); - return FfxFloat16x2(t); + FfxFloat32x2 t = f16tof32(FfxUInt32x2(x & 0xFFFF, x >> 16)); + return FFX_MIN16_F2(t); } -FfxFloat16x4 ffxUint32x2ToFloat16x4(FfxUInt32x2 x) +FFX_MIN16_F4 ffxUint32x2ToFloat16x4(FfxUInt32x2 x) { - return FfxFloat16x4(ffxUint32ToFloat16x2(x.x), ffxUint32ToFloat16x2(x.y)); + return FFX_MIN16_F4(ffxUint32ToFloat16x2(x.x), ffxUint32ToFloat16x2(x.y)); } -FfxUInt16x2 ffxUint32ToUint16x2(FfxUInt32 x) +FFX_MIN16_U2 ffxUint32ToUint16x2(FfxUInt32 x) { - FfxUInt32x2 t = FfxUInt32x2(x & 0xFFFF, x >> 16); - return FfxUInt16x2(t); + FfxUInt32x2 t = FfxUInt32x2(x & 0xFFFF, x >> 16); + return FFX_MIN16_U2(t); } -FfxUInt16x4 ffxUint32x2ToUint16x4(FfxUInt32x2 x) +FFX_MIN16_U4 ffxUint32x2ToUint16x4(FfxUInt32x2 x) { - return FfxUInt16x4(ffxUint32ToUint16x2(x.x), ffxUint32ToUint16x2(x.y)); + return FFX_MIN16_U4(ffxUint32ToUint16x2(x.x), ffxUint32ToUint16x2(x.y)); } #define FFX_UINT32_TO_FLOAT16X2(x) ffxUint32ToFloat16x2(FfxUInt32(x)) #define FFX_UINT32X2_TO_FLOAT16X4(x) ffxUint32x2ToFloat16x4(FfxUInt32x2(x)) #define FFX_UINT32_TO_UINT16X2(x) ffxUint32ToUint16x2(FfxUInt32(x)) #define FFX_UINT32X2_TO_UINT16X4(x) ffxUint32x2ToUint16x4(FfxUInt32x2(x)) //------------------------------------------------------------------------------------------------------------------------------ -FfxUInt32 ffxFloat16x2ToUint32(FfxFloat16x2 x) +FfxUInt32 FFX_MIN16_F2ToUint32(FFX_MIN16_F2 x) { - return f32tof16(x.x) + (f32tof16(x.y) << 16); + return f32tof16(x.x) + (f32tof16(x.y) << 16); } -FfxUInt32x2 ffxFloat16x4ToUint32x2(FfxFloat16x4 x) +FfxUInt32x2 FFX_MIN16_F4ToUint32x2(FFX_MIN16_F4 x) { - return FfxUInt32x2(ffxFloat16x2ToUint32(x.xy), ffxFloat16x2ToUint32(x.zw)); + return FfxUInt32x2(FFX_MIN16_F2ToUint32(x.xy), FFX_MIN16_F2ToUint32(x.zw)); } -FfxUInt32 ffxUint16x2ToUint32(FfxUInt16x2 x) +FfxUInt32 FFX_MIN16_U2ToUint32(FFX_MIN16_U2 x) { - return FfxUInt32(x.x) + (FfxUInt32(x.y) << 16); + return FfxUInt32(x.x) + (FfxUInt32(x.y) << 16); } -FfxUInt32x2 ffxUint16x4ToUint32x2(FfxUInt16x4 x) +FfxUInt32x2 FFX_MIN16_U4ToUint32x2(FFX_MIN16_U4 x) { - return FfxUInt32x2(ffxUint16x2ToUint32(x.xy), ffxUint16x2ToUint32(x.zw)); + return FfxUInt32x2(FFX_MIN16_U2ToUint32(x.xy), FFX_MIN16_U2ToUint32(x.zw)); } -#define FFX_FLOAT16X2_TO_UINT32(x) ffxFloat16x2ToUint32(FfxFloat16x2(x)) -#define FFX_FLOAT16X4_TO_UINT32X2(x) ffxFloat16x4ToUint32x2(FfxFloat16x4(x)) -#define FFX_UINT16X2_TO_UINT32(x) ffxUint16x2ToUint32(FfxUInt16x2(x)) -#define FFX_UINT16X4_TO_UINT32X2(x) ffxUint16x4ToUint32x2(FfxUInt16x4(x)) +#define FFX_FLOAT16X2_TO_UINT32(x) FFX_MIN16_F2ToUint32(FFX_MIN16_F2(x)) +#define FFX_FLOAT16X4_TO_UINT32X2(x) FFX_MIN16_F4ToUint32x2(FFX_MIN16_F4(x)) +#define FFX_UINT16X2_TO_UINT32(x) FFX_MIN16_U2ToUint32(FFX_MIN16_U2(x)) +#define FFX_UINT16X4_TO_UINT32X2(x) FFX_MIN16_U4ToUint32x2(FFX_MIN16_U4(x)) #if defined(FFX_HLSL_6_2) && !defined(FFX_NO_16_BIT_CAST) - #define FFX_TO_UINT16(x) asuint16(x) - #define FFX_TO_UINT16X2(x) asuint16(x) - #define FFX_TO_UINT16X3(x) asuint16(x) - #define FFX_TO_UINT16X4(x) asuint16(x) +#define FFX_TO_UINT16(x) asuint16(x) +#define FFX_TO_UINT16X2(x) asuint16(x) +#define FFX_TO_UINT16X3(x) asuint16(x) +#define FFX_TO_UINT16X4(x) asuint16(x) #else - #define FFX_TO_UINT16(a) FfxUInt16(f32tof16(FfxFloat32(a))) - #define FFX_TO_UINT16X2(a) FfxUInt16x2(FFX_TO_UINT16((a).x), FFX_TO_UINT16((a).y)) - #define FFX_TO_UINT16X3(a) FfxUInt16x3(FFX_TO_UINT16((a).x), FFX_TO_UINT16((a).y), FFX_TO_UINT16((a).z)) - #define FFX_TO_UINT16X4(a) FfxUInt16x4(FFX_TO_UINT16((a).x), FFX_TO_UINT16((a).y), FFX_TO_UINT16((a).z), FFX_TO_UINT16((a).w)) +#define FFX_TO_UINT16(a) FFX_MIN16_U(f32tof16(FfxFloat32(a))) +#define FFX_TO_UINT16X2(a) FFX_MIN16_U2(FFX_TO_UINT16((a).x), FFX_TO_UINT16((a).y)) +#define FFX_TO_UINT16X3(a) FFX_MIN16_U3(FFX_TO_UINT16((a).x), FFX_TO_UINT16((a).y), FFX_TO_UINT16((a).z)) +#define FFX_TO_UINT16X4(a) FFX_MIN16_U4(FFX_TO_UINT16((a).x), FFX_TO_UINT16((a).y), FFX_TO_UINT16((a).z), FFX_TO_UINT16((a).w)) #endif // #if defined(FFX_HLSL_6_2) && !defined(FFX_NO_16_BIT_CAST) #if defined(FFX_HLSL_6_2) && !defined(FFX_NO_16_BIT_CAST) - #define FFX_TO_FLOAT16(x) asfloat16(x) - #define FFX_TO_FLOAT16X2(x) asfloat16(x) - #define FFX_TO_FLOAT16X3(x) asfloat16(x) - #define FFX_TO_FLOAT16X4(x) asfloat16(x) +#define FFX_TO_FLOAT16(x) asfloat16(x) +#define FFX_TO_FLOAT16X2(x) asfloat16(x) +#define FFX_TO_FLOAT16X3(x) asfloat16(x) +#define FFX_TO_FLOAT16X4(x) asfloat16(x) #else - #define FFX_TO_FLOAT16(a) FfxFloat16(f16tof32(FfxUInt32(a))) - #define FFX_TO_FLOAT16X2(a) FfxFloat16x2(FFX_TO_FLOAT16((a).x), FFX_TO_FLOAT16((a).y)) - #define FFX_TO_FLOAT16X3(a) FfxFloat16x3(FFX_TO_FLOAT16((a).x), FFX_TO_FLOAT16((a).y), FFX_TO_FLOAT16((a).z)) - #define FFX_TO_FLOAT16X4(a) FfxFloat16x4(FFX_TO_FLOAT16((a).x), FFX_TO_FLOAT16((a).y), FFX_TO_FLOAT16((a).z), FFX_TO_FLOAT16((a).w)) +#define FFX_TO_FLOAT16(a) FFX_MIN16_F(f16tof32(FfxUInt32(a))) +#define FFX_TO_FLOAT16X2(a) FFX_MIN16_F2(FFX_TO_FLOAT16((a).x), FFX_TO_FLOAT16((a).y)) +#define FFX_TO_FLOAT16X3(a) FFX_MIN16_F3(FFX_TO_FLOAT16((a).x), FFX_TO_FLOAT16((a).y), FFX_TO_FLOAT16((a).z)) +#define FFX_TO_FLOAT16X4(a) FFX_MIN16_F4(FFX_TO_FLOAT16((a).x), FFX_TO_FLOAT16((a).y), FFX_TO_FLOAT16((a).z), FFX_TO_FLOAT16((a).w)) #endif // #if defined(FFX_HLSL_6_2) && !defined(FFX_NO_16_BIT_CAST) //============================================================================================================================== -#if FFX_HLSL_6_2 -FfxFloat16 ffxBroadcastFloat16(FfxFloat16 a) -{ - return FfxFloat16(a); -} -FfxFloat16x2 ffxBroadcastFloat16x2(FfxFloat16 a) -{ - return FfxFloat16x2(a, a); -} -FfxFloat16x3 ffxBroadcastFloat16x3(FfxFloat16 a) -{ - return FfxFloat16x3(a, a, a); -} -FfxFloat16x4 ffxBroadcastFloat16x4(FfxFloat16 a) -{ - return FfxFloat16x4(a, a, a, a); -} -#define FFX_BROADCAST_FLOAT16(a) FfxFloat16(a) -#define FFX_BROADCAST_FLOAT16X2(a) FfxFloat16(a) -#define FFX_BROADCAST_FLOAT16X3(a) FfxFloat16(a) -#define FFX_BROADCAST_FLOAT16X4(a) FfxFloat16(a) -#else #define FFX_BROADCAST_FLOAT16(a) FFX_MIN16_F(a) #define FFX_BROADCAST_FLOAT16X2(a) FFX_MIN16_F(a) #define FFX_BROADCAST_FLOAT16X3(a) FFX_MIN16_F(a) #define FFX_BROADCAST_FLOAT16X4(a) FFX_MIN16_F(a) -#endif + //------------------------------------------------------------------------------------------------------------------------------ -#if FFX_HLSL_6_2 -FfxInt16 ffxBroadcastInt16(FfxInt16 a) -{ - return FfxInt16(a); -} -FfxInt16x2 ffxBroadcastInt16x2(FfxInt16 a) -{ - return FfxInt16x2(a, a); -} -FfxInt16x3 ffxBroadcastInt16x3(FfxInt16 a) -{ - return FfxInt16x3(a, a, a); -} -FfxInt16x4 ffxBroadcastInt16x4(FfxInt16 a) -{ - return FfxInt16x4(a, a, a, a); -} -#define FFX_BROADCAST_INT16(a) FfxInt16(a) -#define FFX_BROADCAST_INT16X2(a) FfxInt16(a) -#define FFX_BROADCAST_INT16X3(a) FfxInt16(a) -#define FFX_BROADCAST_INT16X4(a) FfxInt16(a) -#else #define FFX_BROADCAST_INT16(a) FFX_MIN16_I(a) #define FFX_BROADCAST_INT16X2(a) FFX_MIN16_I(a) #define FFX_BROADCAST_INT16X3(a) FFX_MIN16_I(a) #define FFX_BROADCAST_INT16X4(a) FFX_MIN16_I(a) -#endif + //------------------------------------------------------------------------------------------------------------------------------ -#if FFX_HLSL_6_2 -FfxUInt16 ffxBroadcastUInt16(FfxUInt16 a) -{ - return FfxUInt16(a); -} -FfxUInt16x2 ffxBroadcastUInt16x2(FfxUInt16 a) -{ - return FfxUInt16x2(a, a); -} -FfxUInt16x3 ffxBroadcastUInt16x3(FfxUInt16 a) -{ - return FfxUInt16x3(a, a, a); -} -FfxUInt16x4 ffxBroadcastUInt16x4(FfxUInt16 a) -{ - return FfxUInt16x4(a, a, a, a); -} -#define FFX_BROADCAST_UINT16(a) FfxUInt16(a) -#define FFX_BROADCAST_UINT16X2(a) FfxUInt16(a) -#define FFX_BROADCAST_UINT16X3(a) FfxUInt16(a) -#define FFX_BROADCAST_UINT16X4(a) FfxUInt16(a) -#else #define FFX_BROADCAST_UINT16(a) FFX_MIN16_U(a) #define FFX_BROADCAST_UINT16X2(a) FFX_MIN16_U(a) #define FFX_BROADCAST_UINT16X3(a) FFX_MIN16_U(a) #define FFX_BROADCAST_UINT16X4(a) FFX_MIN16_U(a) -#endif + //============================================================================================================================== -FfxUInt16 ffxAbsHalf(FfxUInt16 a) +FFX_MIN16_U ffxAbsHalf(FFX_MIN16_U a) { - return FfxUInt16(abs(FfxInt16(a))); + return FFX_MIN16_U(abs(FFX_MIN16_I(a))); } -FfxUInt16x2 ffxAbsHalf(FfxUInt16x2 a) +FFX_MIN16_U2 ffxAbsHalf(FFX_MIN16_U2 a) { - return FfxUInt16x2(abs(FfxInt16x2(a))); + return FFX_MIN16_U2(abs(FFX_MIN16_I2(a))); } -FfxUInt16x3 ffxAbsHalf(FfxUInt16x3 a) +FFX_MIN16_U3 ffxAbsHalf(FFX_MIN16_U3 a) { - return FfxUInt16x3(abs(FfxInt16x3(a))); + return FFX_MIN16_U3(abs(FFX_MIN16_I3(a))); } -FfxUInt16x4 ffxAbsHalf(FfxUInt16x4 a) +FFX_MIN16_U4 ffxAbsHalf(FFX_MIN16_U4 a) { - return FfxUInt16x4(abs(FfxInt16x4(a))); + return FFX_MIN16_U4(abs(FFX_MIN16_I4(a))); } //------------------------------------------------------------------------------------------------------------------------------ -FfxFloat16 ffxClampHalf(FfxFloat16 x, FfxFloat16 n, FfxFloat16 m) +FFX_MIN16_F ffxClampHalf(FFX_MIN16_F x, FFX_MIN16_F n, FFX_MIN16_F m) { - return max(n, min(x, m)); + return max(n, min(x, m)); } -FfxFloat16x2 ffxClampHalf(FfxFloat16x2 x, FfxFloat16x2 n, FfxFloat16x2 m) +FFX_MIN16_F2 ffxClampHalf(FFX_MIN16_F2 x, FFX_MIN16_F2 n, FFX_MIN16_F2 m) { - return max(n, min(x, m)); + return max(n, min(x, m)); } -FfxFloat16x3 ffxClampHalf(FfxFloat16x3 x, FfxFloat16x3 n, FfxFloat16x3 m) +FFX_MIN16_F3 ffxClampHalf(FFX_MIN16_F3 x, FFX_MIN16_F3 n, FFX_MIN16_F3 m) { - return max(n, min(x, m)); + return max(n, min(x, m)); } -FfxFloat16x4 ffxClampHalf(FfxFloat16x4 x, FfxFloat16x4 n, FfxFloat16x4 m) +FFX_MIN16_F4 ffxClampHalf(FFX_MIN16_F4 x, FFX_MIN16_F4 n, FFX_MIN16_F4 m) { - return max(n, min(x, m)); + return max(n, min(x, m)); } //------------------------------------------------------------------------------------------------------------------------------ // V_FRACT_F16 (note DX frac() is different). -FfxFloat16 ffxFract(FfxFloat16 x) +FFX_MIN16_F ffxFract(FFX_MIN16_F x) { - return x - floor(x); + return x - floor(x); } -FfxFloat16x2 ffxFract(FfxFloat16x2 x) +FFX_MIN16_F2 ffxFract(FFX_MIN16_F2 x) { - return x - floor(x); + return x - floor(x); } -FfxFloat16x3 ffxFract(FfxFloat16x3 x) +FFX_MIN16_F3 ffxFract(FFX_MIN16_F3 x) { - return x - floor(x); + return x - floor(x); } -FfxFloat16x4 ffxFract(FfxFloat16x4 x) +FFX_MIN16_F4 ffxFract(FFX_MIN16_F4 x) { - return x - floor(x); + return x - floor(x); } //------------------------------------------------------------------------------------------------------------------------------ -FfxFloat16 ffxLerp(FfxFloat16 x, FfxFloat16 y, FfxFloat16 a) +FFX_MIN16_F ffxLerp(FFX_MIN16_F x, FFX_MIN16_F y, FFX_MIN16_F a) { - return lerp(x, y, a); + return lerp(x, y, a); } -FfxFloat16x2 ffxLerp(FfxFloat16x2 x, FfxFloat16x2 y, FfxFloat16 a) +FFX_MIN16_F2 ffxLerp(FFX_MIN16_F2 x, FFX_MIN16_F2 y, FFX_MIN16_F a) { - return lerp(x, y, a); + return lerp(x, y, a); } -FfxFloat16x2 ffxLerp(FfxFloat16x2 x, FfxFloat16x2 y, FfxFloat16x2 a) +FFX_MIN16_F2 ffxLerp(FFX_MIN16_F2 x, FFX_MIN16_F2 y, FFX_MIN16_F2 a) { - return lerp(x, y, a); + return lerp(x, y, a); } -FfxFloat16x3 ffxLerp(FfxFloat16x3 x, FfxFloat16x3 y, FfxFloat16 a) +FFX_MIN16_F3 ffxLerp(FFX_MIN16_F3 x, FFX_MIN16_F3 y, FFX_MIN16_F a) { - return lerp(x, y, a); + return lerp(x, y, a); } -FfxFloat16x3 ffxLerp(FfxFloat16x3 x, FfxFloat16x3 y, FfxFloat16x3 a) +FFX_MIN16_F3 ffxLerp(FFX_MIN16_F3 x, FFX_MIN16_F3 y, FFX_MIN16_F3 a) { - return lerp(x, y, a); + return lerp(x, y, a); } -FfxFloat16x4 ffxLerp(FfxFloat16x4 x, FfxFloat16x4 y, FfxFloat16 a) +FFX_MIN16_F4 ffxLerp(FFX_MIN16_F4 x, FFX_MIN16_F4 y, FFX_MIN16_F a) { - return lerp(x, y, a); + return lerp(x, y, a); } -FfxFloat16x4 ffxLerp(FfxFloat16x4 x, FfxFloat16x4 y, FfxFloat16x4 a) +FFX_MIN16_F4 ffxLerp(FFX_MIN16_F4 x, FFX_MIN16_F4 y, FFX_MIN16_F4 a) { - return lerp(x, y, a); + return lerp(x, y, a); } //------------------------------------------------------------------------------------------------------------------------------ -#if FFX_HLSL_6_2 -FFX_MIN16_F ffxLerp(FFX_MIN16_F x, FFX_MIN16_F y, FFX_MIN16_F t) +FFX_MIN16_F ffxMax3Half(FFX_MIN16_F x, FFX_MIN16_F y, FFX_MIN16_F z) { - return lerp(x, y, t); + return max(x, max(y, z)); } -FFX_MIN16_F2 ffxLerp(FFX_MIN16_F2 x, FFX_MIN16_F2 y, FFX_MIN16_F t) +FFX_MIN16_F2 ffxMax3Half(FFX_MIN16_F2 x, FFX_MIN16_F2 y, FFX_MIN16_F2 z) { - return lerp(x, y, t); + return max(x, max(y, z)); } -FFX_MIN16_F2 ffxLerp(FFX_MIN16_F2 x, FFX_MIN16_F2 y, FFX_MIN16_F2 t) +FFX_MIN16_F3 ffxMax3Half(FFX_MIN16_F3 x, FFX_MIN16_F3 y, FFX_MIN16_F3 z) { - return lerp(x, y, t); + return max(x, max(y, z)); } -FFX_MIN16_F3 ffxLerp(FFX_MIN16_F3 x, FFX_MIN16_F3 y, FFX_MIN16_F t) +FFX_MIN16_F4 ffxMax3Half(FFX_MIN16_F4 x, FFX_MIN16_F4 y, FFX_MIN16_F4 z) { - return lerp(x, y, t); -} -FFX_MIN16_F3 ffxLerp(FFX_MIN16_F3 x, FFX_MIN16_F3 y, FFX_MIN16_F3 t) -{ - return lerp(x, y, t); -} -FFX_MIN16_F4 ffxLerp(FFX_MIN16_F4 x, FFX_MIN16_F4 y, FFX_MIN16_F t) -{ - return lerp(x, y, t); -} -FFX_MIN16_F4 ffxLerp(FFX_MIN16_F4 x, FFX_MIN16_F4 y, FFX_MIN16_F4 t) -{ - return lerp(x, y, t); + return max(x, max(y, z)); } //------------------------------------------------------------------------------------------------------------------------------ -FFX_MIN16_F ffxMin(FFX_MIN16_F x, FFX_MIN16_F y) +FFX_MIN16_F ffxMin3Half(FFX_MIN16_F x, FFX_MIN16_F y, FFX_MIN16_F z) { - return min(x, y); + return min(x, min(y, z)); } -FFX_MIN16_F2 ffxMin(FFX_MIN16_F2 x, FFX_MIN16_F2 y) +FFX_MIN16_F2 ffxMin3Half(FFX_MIN16_F2 x, FFX_MIN16_F2 y, FFX_MIN16_F2 z) { - return min(x, y); + return min(x, min(y, z)); } -FFX_MIN16_F3 ffxMin(FFX_MIN16_F3 x, FFX_MIN16_F3 y) +FFX_MIN16_F3 ffxMin3Half(FFX_MIN16_F3 x, FFX_MIN16_F3 y, FFX_MIN16_F3 z) { - return min(x, y); + return min(x, min(y, z)); } -FFX_MIN16_F4 ffxMin(FFX_MIN16_F4 x, FFX_MIN16_F4 y) +FFX_MIN16_F4 ffxMin3Half(FFX_MIN16_F4 x, FFX_MIN16_F4 y, FFX_MIN16_F4 z) { - return min(x, y); + return min(x, min(y, z)); } //------------------------------------------------------------------------------------------------------------------------------ -FFX_MIN16_F ffxMax(FFX_MIN16_F x, FFX_MIN16_F y) +FFX_MIN16_F ffxReciprocalHalf(FFX_MIN16_F x) { - return max(x, y); + return rcp(x); } -FFX_MIN16_F2 ffxMax(FFX_MIN16_F2 x, FFX_MIN16_F2 y) +FFX_MIN16_F2 ffxReciprocalHalf(FFX_MIN16_F2 x) { - return max(x, y); + return rcp(x); } -FFX_MIN16_F3 ffxMax(FFX_MIN16_F3 x, FFX_MIN16_F3 y) +FFX_MIN16_F3 ffxReciprocalHalf(FFX_MIN16_F3 x) { - return max(x, y); + return rcp(x); } -FFX_MIN16_F4 ffxMax(FFX_MIN16_F4 x, FFX_MIN16_F4 y) +FFX_MIN16_F4 ffxReciprocalHalf(FFX_MIN16_F4 x) { - return max(x, y); -} -#endif -//------------------------------------------------------------------------------------------------------------------------------ -FfxFloat16 ffxMax3Half(FfxFloat16 x, FfxFloat16 y, FfxFloat16 z) -{ - return max(x, max(y, z)); -} -FfxFloat16x2 ffxMax3Half(FfxFloat16x2 x, FfxFloat16x2 y, FfxFloat16x2 z) -{ - return max(x, max(y, z)); -} -FfxFloat16x3 ffxMax3Half(FfxFloat16x3 x, FfxFloat16x3 y, FfxFloat16x3 z) -{ - return max(x, max(y, z)); -} -FfxFloat16x4 ffxMax3Half(FfxFloat16x4 x, FfxFloat16x4 y, FfxFloat16x4 z) -{ - return max(x, max(y, z)); + return rcp(x); } //------------------------------------------------------------------------------------------------------------------------------ -FfxFloat16 ffxMin3Half(FfxFloat16 x, FfxFloat16 y, FfxFloat16 z) +FFX_MIN16_F ffxReciprocalSquareRootHalf(FFX_MIN16_F x) { - return min(x, min(y, z)); + return rsqrt(x); } -FfxFloat16x2 ffxMin3Half(FfxFloat16x2 x, FfxFloat16x2 y, FfxFloat16x2 z) +FFX_MIN16_F2 ffxReciprocalSquareRootHalf(FFX_MIN16_F2 x) { - return min(x, min(y, z)); + return rsqrt(x); } -FfxFloat16x3 ffxMin3Half(FfxFloat16x3 x, FfxFloat16x3 y, FfxFloat16x3 z) +FFX_MIN16_F3 ffxReciprocalSquareRootHalf(FFX_MIN16_F3 x) { - return min(x, min(y, z)); + return rsqrt(x); } -FfxFloat16x4 ffxMin3Half(FfxFloat16x4 x, FfxFloat16x4 y, FfxFloat16x4 z) +FFX_MIN16_F4 ffxReciprocalSquareRootHalf(FFX_MIN16_F4 x) { - return min(x, min(y, z)); + return rsqrt(x); } //------------------------------------------------------------------------------------------------------------------------------ -FfxFloat16 ffxReciprocalHalf(FfxFloat16 x) +FFX_MIN16_F ffxSaturate(FFX_MIN16_F x) { - return rcp(x); + return saturate(x); } -FfxFloat16x2 ffxReciprocalHalf(FfxFloat16x2 x) +FFX_MIN16_F2 ffxSaturate(FFX_MIN16_F2 x) { - return rcp(x); + return saturate(x); } -FfxFloat16x3 ffxReciprocalHalf(FfxFloat16x3 x) +FFX_MIN16_F3 ffxSaturate(FFX_MIN16_F3 x) { - return rcp(x); + return saturate(x); } -FfxFloat16x4 ffxReciprocalHalf(FfxFloat16x4 x) +FFX_MIN16_F4 ffxSaturate(FFX_MIN16_F4 x) { - return rcp(x); + return saturate(x); } //------------------------------------------------------------------------------------------------------------------------------ -FfxFloat16 ffxReciprocalSquareRootHalf(FfxFloat16 x) +FFX_MIN16_U ffxBitShiftRightHalf(FFX_MIN16_U a, FFX_MIN16_U b) { - return rsqrt(x); + return FFX_MIN16_U(FFX_MIN16_I(a) >> FFX_MIN16_I(b)); } -FfxFloat16x2 ffxReciprocalSquareRootHalf(FfxFloat16x2 x) +FFX_MIN16_U2 ffxBitShiftRightHalf(FFX_MIN16_U2 a, FFX_MIN16_U2 b) { - return rsqrt(x); + return FFX_MIN16_U2(FFX_MIN16_I2(a) >> FFX_MIN16_I2(b)); } -FfxFloat16x3 ffxReciprocalSquareRootHalf(FfxFloat16x3 x) +FFX_MIN16_U3 ffxBitShiftRightHalf(FFX_MIN16_U3 a, FFX_MIN16_U3 b) { - return rsqrt(x); + return FFX_MIN16_U3(FFX_MIN16_I3(a) >> FFX_MIN16_I3(b)); } -FfxFloat16x4 ffxReciprocalSquareRootHalf(FfxFloat16x4 x) +FFX_MIN16_U4 ffxBitShiftRightHalf(FFX_MIN16_U4 a, FFX_MIN16_U4 b) { - return rsqrt(x); -} -//------------------------------------------------------------------------------------------------------------------------------ -FfxFloat16 ffxSaturate(FfxFloat16 x) -{ - return saturate(x); -} -FfxFloat16x2 ffxSaturate(FfxFloat16x2 x) -{ - return saturate(x); -} -FfxFloat16x3 ffxSaturate(FfxFloat16x3 x) -{ - return saturate(x); -} -FfxFloat16x4 ffxSaturate(FfxFloat16x4 x) -{ - return saturate(x); -} -//------------------------------------------------------------------------------------------------------------------------------ -FfxUInt16 ffxBitShiftRightHalf(FfxUInt16 a, FfxUInt16 b) -{ - return FfxUInt16(FfxInt16(a) >> FfxInt16(b)); -} -FfxUInt16x2 ffxBitShiftRightHalf(FfxUInt16x2 a, FfxUInt16x2 b) -{ - return FfxUInt16x2(FfxInt16x2(a) >> FfxInt16x2(b)); -} -FfxUInt16x3 ffxBitShiftRightHalf(FfxUInt16x3 a, FfxUInt16x3 b) -{ - return FfxUInt16x3(FfxInt16x3(a) >> FfxInt16x3(b)); -} -FfxUInt16x4 ffxBitShiftRightHalf(FfxUInt16x4 a, FfxUInt16x4 b) -{ - return FfxUInt16x4(FfxInt16x4(a) >> FfxInt16x4(b)); + return FFX_MIN16_U4(FFX_MIN16_I4(a) >> FFX_MIN16_I4(b)); } #endif // FFX_HALF diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr1.h b/src/ffx-fsr2-api/shaders/ffx_fsr1.h index 0636247..1ac23cf 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr1.h +++ b/src/ffx-fsr2-api/shaders/ffx_fsr1.h @@ -19,6 +19,10 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +#ifdef __clang__ +#pragma clang diagnostic ignored "-Wunused-variable" +#endif + /// Setup required constant values for EASU (works on CPU or GPU). /// /// @param [out] con0 diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_accumulate.h b/src/ffx-fsr2-api/shaders/ffx_fsr2_accumulate.h index 83fd286..14620d5 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_accumulate.h +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_accumulate.h @@ -19,38 +19,24 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -FFX_MIN16_F4 WrapDepthClipMask(FFX_MIN16_I2 iPxSample) -{ - return FFX_MIN16_F4(LoadDepthClip(iPxSample).r, 0, 0, 0); -} +#ifndef FFX_FSR2_ACCUMULATE_H +#define FFX_FSR2_ACCUMULATE_H -DeclareCustomFetchBilinearSamples(FetchDepthClipMaskSamples, WrapDepthClipMask) -DeclareCustomTextureSample(DepthClipMaskSample, Bilinear, FetchDepthClipMaskSamples) - -FFX_MIN16_F4 WrapTransparencyAndCompositionMask(FFX_MIN16_I2 iPxSample) -{ - return FFX_MIN16_F4(LoadTransparencyAndCompositionMask(iPxSample).r, 0, 0, 0); -} - -DeclareCustomFetchBilinearSamples(FetchTransparencyAndCompositionMaskSamples, WrapTransparencyAndCompositionMask) -DeclareCustomTextureSample(TransparencyAndCompositionMaskSample, Bilinear, FetchTransparencyAndCompositionMaskSamples) - -FfxFloat32x4 WrapLumaStabilityFactor(FFX_MIN16_I2 iPxSample) -{ - return FfxFloat32x4(LoadLumaStabilityFactor(iPxSample), 0, 0, 0); -} - -DeclareCustomFetchBilinearSamples(FetchLumaStabilitySamples, WrapLumaStabilityFactor) -DeclareCustomTextureSample(LumaStabilityFactorSample, Bilinear, FetchLumaStabilitySamples) +#define FFX_FSR2_OPTION_GUARANTEE_UPSAMPLE_WEIGHT_ON_NEW_SAMPLES 1 FfxFloat32 GetPxHrVelocity(FfxFloat32x2 fMotionVector) { return length(fMotionVector * DisplaySize()); } - -void Accumulate(FFX_MIN16_I2 iPxHrPos, FFX_PARAMETER_INOUT FfxFloat32x4 fHistory, FFX_PARAMETER_IN FfxFloat32x4 fUpsampled, FFX_PARAMETER_IN FfxFloat32 fDepthClipFactor, FFX_PARAMETER_IN FfxFloat32 fHrVelocity) +#if FFX_HALF +FFX_MIN16_F GetPxHrVelocity(FFX_MIN16_F2 fMotionVector) { + return length(fMotionVector * FFX_MIN16_F2(DisplaySize())); +} +#endif +void Accumulate(FfxInt32x2 iPxHrPos, FFX_PARAMETER_INOUT FfxFloat32x4 fHistory, FFX_PARAMETER_IN FfxFloat32x4 fUpsampled, FFX_PARAMETER_IN FfxFloat32 fDepthClipFactor, FFX_PARAMETER_IN FfxFloat32 fHrVelocity) +{ fHistory.w = fHistory.w + fUpsampled.w; fUpsampled.rgb = YCoCgToRGB(fUpsampled.rgb); @@ -58,56 +44,56 @@ void Accumulate(FFX_MIN16_I2 iPxHrPos, FFX_PARAMETER_INOUT FfxFloat32x4 fHistory const FfxFloat32 fAlpha = fUpsampled.w / fHistory.w; fHistory.rgb = ffxLerp(fHistory.rgb, fUpsampled.rgb, fAlpha); - FfxFloat32 fMaxAverageWeight = ffxLerp(MaxAccumulationWeight(), accumulationMaxOnMotion, ffxSaturate(fHrVelocity * 10.0f)); + FfxFloat32 fMaxAverageWeight = FfxFloat32(ffxLerp(MaxAccumulationWeight(), accumulationMaxOnMotion, ffxSaturate(fHrVelocity * 10.0f))); fHistory.w = ffxMin(fHistory.w, fMaxAverageWeight); } void RectifyHistory( RectificationBoxData clippingBox, inout FfxFloat32x4 fHistory, - FFX_PARAMETER_IN LOCK_STATUS_T fLockStatus, - FFX_PARAMETER_IN UPSAMPLE_F fDepthClipFactor, - FFX_PARAMETER_IN UPSAMPLE_F fLumaStabilityFactor, - FFX_PARAMETER_IN UPSAMPLE_F fLuminanceDiff, - FFX_PARAMETER_IN UPSAMPLE_F fUpsampleWeight, + FFX_PARAMETER_IN FfxFloat32x3 fLockStatus, + FFX_PARAMETER_IN FfxFloat32 fDepthClipFactor, + FFX_PARAMETER_IN FfxFloat32 fLumaStabilityFactor, + FFX_PARAMETER_IN FfxFloat32 fLuminanceDiff, + FFX_PARAMETER_IN FfxFloat32 fUpsampleWeight, FFX_PARAMETER_IN FfxFloat32 fLockContributionThisFrame) { - UPSAMPLE_F fScaleFactorInfluence = UPSAMPLE_F(1.0f / DownscaleFactor().x - 1); - UPSAMPLE_F fBoxScale = UPSAMPLE_F(1.0f) + (UPSAMPLE_F(0.5f) * fScaleFactorInfluence); + FfxFloat32 fScaleFactorInfluence = FfxFloat32(1.0f / DownscaleFactor().x - 1); + FfxFloat32 fBoxScale = FfxFloat32(1.0f) + (FfxFloat32(0.5f) * fScaleFactorInfluence); - FFX_MIN16_F3 fScaledBoxVec = clippingBox.boxVec * fBoxScale; - UPSAMPLE_F3 boxMin = clippingBox.boxCenter - fScaledBoxVec; - UPSAMPLE_F3 boxMax = clippingBox.boxCenter + fScaledBoxVec; - UPSAMPLE_F3 boxCenter = clippingBox.boxCenter; - UPSAMPLE_F boxVecSize = length(clippingBox.boxVec); + FfxFloat32x3 fScaledBoxVec = clippingBox.boxVec * fBoxScale; + FfxFloat32x3 boxMin = clippingBox.boxCenter - fScaledBoxVec; + FfxFloat32x3 boxMax = clippingBox.boxCenter + fScaledBoxVec; + FfxFloat32x3 boxCenter = clippingBox.boxCenter; + FfxFloat32 boxVecSize = length(clippingBox.boxVec); boxMin = ffxMax(clippingBox.aabbMin, boxMin); boxMax = ffxMin(clippingBox.aabbMax, boxMax); - UPSAMPLE_F3 distToClampOutside = UPSAMPLE_F3(ffxMax(ffxMax(UPSAMPLE_F3_BROADCAST(0.0f), boxMin - UPSAMPLE_F3(fHistory.xyz)), ffxMax(UPSAMPLE_F3_BROADCAST(0.0f), UPSAMPLE_F3(fHistory.xyz) - boxMax))); + FfxFloat32x3 distToClampOutside = ffxMax(ffxMax(FfxFloat32x3(0, 0, 0), boxMin - fHistory.xyz), ffxMax(FfxFloat32x3(0, 0, 0), fHistory.xyz - boxMax)); - if (any(FFX_GREATER_THAN(distToClampOutside, UPSAMPLE_F3_BROADCAST(0.0f)))) { + if (any(FFX_GREATER_THAN(distToClampOutside, FfxFloat32x3(0, 0, 0)))) { - const UPSAMPLE_F3 clampedHistorySample = clamp(UPSAMPLE_F3(fHistory.xyz), boxMin, boxMax); + const FfxFloat32x3 clampedHistorySample = clamp(fHistory.xyz, boxMin, boxMax); - UPSAMPLE_F3 clippedHistoryToBoxCenter = abs(clampedHistorySample - boxCenter); - UPSAMPLE_F3 historyToBoxCenter = abs(UPSAMPLE_F3(fHistory.xyz) - boxCenter); - UPSAMPLE_F3 HistoryColorWeight; - HistoryColorWeight.x = historyToBoxCenter.x > UPSAMPLE_F(0) ? clippedHistoryToBoxCenter.x / historyToBoxCenter.x : UPSAMPLE_F(0.0f); - HistoryColorWeight.y = historyToBoxCenter.y > UPSAMPLE_F(0) ? clippedHistoryToBoxCenter.y / historyToBoxCenter.y : UPSAMPLE_F(0.0f); - HistoryColorWeight.z = historyToBoxCenter.z > UPSAMPLE_F(0) ? clippedHistoryToBoxCenter.z / historyToBoxCenter.z : UPSAMPLE_F(0.0f); + FfxFloat32x3 clippedHistoryToBoxCenter = abs(clampedHistorySample - boxCenter); + FfxFloat32x3 historyToBoxCenter = abs(fHistory.xyz - boxCenter); + FfxFloat32x3 HistoryColorWeight; + HistoryColorWeight.x = historyToBoxCenter.x > FfxFloat32(0) ? clippedHistoryToBoxCenter.x / historyToBoxCenter.x : FfxFloat32(0.0f); + HistoryColorWeight.y = historyToBoxCenter.y > FfxFloat32(0) ? clippedHistoryToBoxCenter.y / historyToBoxCenter.y : FfxFloat32(0.0f); + HistoryColorWeight.z = historyToBoxCenter.z > FfxFloat32(0) ? clippedHistoryToBoxCenter.z / historyToBoxCenter.z : FfxFloat32(0.0f); - UPSAMPLE_F3 fHistoryContribution = HistoryColorWeight; + FfxFloat32x3 fHistoryContribution = HistoryColorWeight; // only lock luma - fHistoryContribution += UPSAMPLE_F3_BROADCAST(ffxMax(UPSAMPLE_F(fLockContributionThisFrame), fLumaStabilityFactor)); + fHistoryContribution += ffxMax(fLockContributionThisFrame, fLumaStabilityFactor).xxx; fHistoryContribution *= (fDepthClipFactor * fDepthClipFactor); - fHistory.xyz = FfxFloat32x3(ffxLerp(clampedHistorySample.xyz, fHistory.xyz, ffxSaturate(fHistoryContribution))); + fHistory.xyz = ffxLerp(clampedHistorySample.xyz, fHistory.xyz, ffxSaturate(fHistoryContribution)); } } -void WriteUpscaledOutput(FFX_MIN16_I2 iPxHrPos, FfxFloat32x3 fUpscaledColor) +void WriteUpscaledOutput(FfxInt32x2 iPxHrPos, FfxFloat32x3 fUpscaledColor) { StoreUpscaledOutput(iPxHrPos, fUpscaledColor); } @@ -122,68 +108,62 @@ FfxFloat32 GetLumaStabilityFactor(FfxFloat32x2 fHrUv, FfxFloat32 fHrVelocity) return fLumaStabilityFactor; } -FfxFloat32 GetLockContributionThisFrame(FfxFloat32x2 fUvCoord, FfxFloat32 fAccumulationMask, FfxFloat32 fParticleMask, LOCK_STATUS_T fLockStatus) +FfxFloat32 GetLockContributionThisFrame(FfxFloat32x2 fUvCoord, FfxFloat32 fAccumulationMask, FfxFloat32 fParticleMask, FfxFloat32x3 fLockStatus) { - const UPSAMPLE_F fNormalizedLockLifetime = GetNormalizedRemainingLockLifetime(fLockStatus); + const FfxFloat32 fNormalizedLockLifetime = GetNormalizedRemainingLockLifetime(fLockStatus); // Rectify on lock frame - FfxFloat32 fLockContributionThisFrame = ffxSaturate(fNormalizedLockLifetime * UPSAMPLE_F(4)); - - fLockContributionThisFrame *= (1.0f - fParticleMask); - //Take down contribution in transparent areas - fLockContributionThisFrame *= FfxFloat32(fAccumulationMask.r > 0.1f); + FfxFloat32 fLockContributionThisFrame = ffxSaturate(fNormalizedLockLifetime * FfxFloat32(4)); return fLockContributionThisFrame; } -void FinalizeLockStatus(FFX_MIN16_I2 iPxHrPos, LOCK_STATUS_T fLockStatus, FfxFloat32 fUpsampledWeight) +void FinalizeLockStatus(FfxInt32x2 iPxHrPos, FfxFloat32x3 fLockStatus, FfxFloat32 fUpsampledWeight) { // Increase trust - const UPSAMPLE_F fTrustIncreaseLanczosMax = UPSAMPLE_F(12); // same increase no matter the MaxAccumulationWeight() value. - const UPSAMPLE_F fTrustIncrease = UPSAMPLE_F(fUpsampledWeight / fTrustIncreaseLanczosMax); - fLockStatus[LOCK_TRUST] = ffxMin(LOCK_STATUS_F1(1), fLockStatus[LOCK_TRUST] + fTrustIncrease); + const FfxFloat32 fTrustIncreaseLanczosMax = FfxFloat32(12); // same increase no matter the MaxAccumulationWeight() value. + const FfxFloat32 fTrustIncrease = FfxFloat32(fUpsampledWeight / fTrustIncreaseLanczosMax); + fLockStatus[LOCK_TRUST] = ffxMin(FfxFloat32(1), fLockStatus[LOCK_TRUST] + fTrustIncrease); // Decrease lock lifetime - const UPSAMPLE_F fLifetimeDecreaseLanczosMax = UPSAMPLE_F(JitterSequenceLength()) * UPSAMPLE_F(averageLanczosWeightPerFrame); - const UPSAMPLE_F fLifetimeDecrease = UPSAMPLE_F(fUpsampledWeight / fLifetimeDecreaseLanczosMax); - fLockStatus[LOCK_LIFETIME_REMAINING] = ffxMax(LOCK_STATUS_F1(0), fLockStatus[LOCK_LIFETIME_REMAINING] - fLifetimeDecrease); + const FfxFloat32 fLifetimeDecreaseLanczosMax = FfxFloat32(JitterSequenceLength()) * FfxFloat32(averageLanczosWeightPerFrame); + const FfxFloat32 fLifetimeDecrease = FfxFloat32(fUpsampledWeight / fLifetimeDecreaseLanczosMax); + fLockStatus[LOCK_LIFETIME_REMAINING] = ffxMax(FfxFloat32(0), fLockStatus[LOCK_LIFETIME_REMAINING] - fLifetimeDecrease); StoreLockStatus(iPxHrPos, fLockStatus); } -UPSAMPLE_F ComputeMaxAccumulationWeight(UPSAMPLE_F fHrVelocity, UPSAMPLE_F fReactiveMax, UPSAMPLE_F fDepthClipFactor, UPSAMPLE_F fLuminanceDiff, LockState lockState) { +FfxFloat32 ComputeMaxAccumulationWeight(FfxFloat32 fHrVelocity, FfxFloat32 fReactiveMax, FfxFloat32 fDepthClipFactor, FfxFloat32 fLuminanceDiff, LockState lockState) { - UPSAMPLE_F normalizedMinimum = UPSAMPLE_F(accumulationMaxOnMotion) / UPSAMPLE_F(MaxAccumulationWeight()); + FfxFloat32 normalizedMinimum = FfxFloat32(accumulationMaxOnMotion) / FfxFloat32(MaxAccumulationWeight()); - UPSAMPLE_F fReactiveMaxAccumulationWeight = UPSAMPLE_F(1) - fReactiveMax; - UPSAMPLE_F fMotionMaxAccumulationWeight = ffxLerp(UPSAMPLE_F(1), normalizedMinimum, ffxSaturate(fHrVelocity * UPSAMPLE_F(10))); - UPSAMPLE_F fDepthClipMaxAccumulationWeight = fDepthClipFactor; + FfxFloat32 fReactiveMaxAccumulationWeight = FfxFloat32(1) - fReactiveMax; + FfxFloat32 fMotionMaxAccumulationWeight = ffxLerp(FfxFloat32(1), normalizedMinimum, ffxSaturate(fHrVelocity * FfxFloat32(10))); + FfxFloat32 fDepthClipMaxAccumulationWeight = fDepthClipFactor; - UPSAMPLE_F fLuminanceDiffMaxAccumulationWeight = ffxSaturate(ffxMax(normalizedMinimum, UPSAMPLE_F(1) - fLuminanceDiff)); + FfxFloat32 fLuminanceDiffMaxAccumulationWeight = ffxSaturate(ffxMax(normalizedMinimum, FfxFloat32(1) - fLuminanceDiff)); - UPSAMPLE_F maxAccumulation = UPSAMPLE_F(MaxAccumulationWeight()) * ffxMin( + FfxFloat32 maxAccumulation = FfxFloat32(MaxAccumulationWeight()) * ffxMin( ffxMin(fReactiveMaxAccumulationWeight, fMotionMaxAccumulationWeight), ffxMin(fDepthClipMaxAccumulationWeight, fLuminanceDiffMaxAccumulationWeight) ); - return (lockState.NewLock && !lockState.WasLockedPrevFrame) ? UPSAMPLE_F(accumulationMaxOnMotion) : maxAccumulation; + return (lockState.NewLock && !lockState.WasLockedPrevFrame) ? FfxFloat32(accumulationMaxOnMotion) : maxAccumulation; } -UPSAMPLE_F2 ComputeKernelWeight(in UPSAMPLE_F fHistoryWeight, in UPSAMPLE_F fDepthClipFactor, in UPSAMPLE_F fReactivityFactor) { - UPSAMPLE_F fKernelSizeBias = ffxSaturate(ffxMax(UPSAMPLE_F(0), fHistoryWeight - UPSAMPLE_F(0.5)) / UPSAMPLE_F(3)); +FfxFloat32x2 ComputeKernelWeight(in FfxFloat32 fHistoryWeight, in FfxFloat32 fDepthClipFactor, in FfxFloat32 fReactivityFactor) { + FfxFloat32 fKernelSizeBias = ffxSaturate(ffxMax(FfxFloat32(0), fHistoryWeight - FfxFloat32(0.5)) / FfxFloat32(3)); - //high bias on disocclusions - - UPSAMPLE_F fOneMinusReactiveMax = UPSAMPLE_F(1) - fReactivityFactor; - UPSAMPLE_F2 fKernelWeight = UPSAMPLE_F(1) + (UPSAMPLE_F(1.0f) / UPSAMPLE_F2(DownscaleFactor()) - UPSAMPLE_F(1)) * UPSAMPLE_F(fKernelSizeBias) * fOneMinusReactiveMax; + FfxFloat32 fOneMinusReactiveMax = FfxFloat32(1) - fReactivityFactor; + FfxFloat32x2 fKernelWeight = FfxFloat32(1) + (FfxFloat32(1.0f) / FfxFloat32x2(DownscaleFactor()) - FfxFloat32(1)) * FfxFloat32(fKernelSizeBias) * fOneMinusReactiveMax; //average value on disocclusion, to help decrease high value sample importance wait for accumulation to kick in - fKernelWeight *= FFX_BROADCAST_MIN_FLOAT16X2(UPSAMPLE_F(0.5) + fDepthClipFactor * UPSAMPLE_F(0.5)); + fKernelWeight *= FfxFloat32x2(0.5f, 0.5f) + fDepthClipFactor * FfxFloat32x2(0.5f, 0.5f); - return ffxMin(FFX_BROADCAST_MIN_FLOAT16X2(1.99), fKernelWeight); + return ffxMin(FfxFloat32x2(1.99f, 1.99f), fKernelWeight); } -void Accumulate(FFX_MIN16_I2 iPxHrPos) +void Accumulate(FfxInt32x2 iPxHrPos) { const FfxFloat32x2 fSamplePosHr = iPxHrPos + 0.5f; const FfxFloat32x2 fPxLrPos = fSamplePosHr * DownscaleFactor(); // Source resolution output pixel center position @@ -199,16 +179,13 @@ void Accumulate(FFX_MIN16_I2 iPxHrPos) const FfxFloat32 fHrVelocity = GetPxHrVelocity(fMotionVector); const FfxFloat32 fDepthClipFactor = ffxSaturate(SampleDepthClip(fLrUvJittered)); const FfxFloat32 fLumaStabilityFactor = GetLumaStabilityFactor(fHrUv, fHrVelocity); - const FfxFloat32 fAccumulationMask = 1.0f - TransparencyAndCompositionMaskSample(fLrUvJittered, RenderSize()).r; + const FfxFloat32x2 fDilatedReactiveMasks = SampleDilatedReactiveMasks(fLrUvJittered); + const FfxFloat32 fReactiveMax = fDilatedReactiveMasks.x; + const FfxFloat32 fAccumulationMask = fDilatedReactiveMasks.y; - FfxInt32x2 offsetTL; - offsetTL.x = (fSamplePosUnjitterLr.x > fPxLrPos.x) ? FfxInt32(0) : FfxInt32(1); - offsetTL.y = (fSamplePosUnjitterLr.y > fPxLrPos.y) ? FfxInt32(0) : FfxInt32(1); - - const UPSAMPLE_F fReactiveMax = UPSAMPLE_F(1) - Pow3(UPSAMPLE_F(1) - LoadReactiveMax(FFX_MIN16_I2(iPxLrPos + offsetTL))); - - FfxFloat32x4 fHistoryColorAndWeight = FfxFloat32x4(0.0f, 0.0f, 0.0f, 0.0f); - LOCK_STATUS_T fLockStatus = CreateNewLockSample(); + FfxFloat32x4 fHistoryColorAndWeight = FfxFloat32x4(0, 0, 0, 0); + FfxFloat32x3 fLockStatus; + InitializeNewLockSample(fLockStatus); FfxBoolean bIsExistingSample = FFX_TRUE; FfxFloat32x2 fReprojectedHrUv = FfxFloat32x2(0, 0); @@ -219,18 +196,18 @@ void Accumulate(FFX_MIN16_I2 iPxHrPos) ReprojectHistoryLockStatus(iPxHrPos, fReprojectedHrUv, fLockStatus); } - FFX_MIN16_F fLuminanceDiff = FFX_MIN16_F(0.0f); + FfxFloat32 fLuminanceDiff = FfxFloat32(0.0f); - LockState lockState = PostProcessLockStatus(iPxHrPos, fLrUvJittered, FFX_MIN16_F(fDepthClipFactor), fHrVelocity, fHistoryColorAndWeight.w, fLockStatus, fLuminanceDiff); + LockState lockState = PostProcessLockStatus(iPxHrPos, fLrUvJittered, FfxFloat32(fDepthClipFactor), fAccumulationMask, fHrVelocity, fHistoryColorAndWeight.w, fLockStatus, fLuminanceDiff); fHistoryColorAndWeight.w = ffxMin(fHistoryColorAndWeight.w, ComputeMaxAccumulationWeight( - UPSAMPLE_F(fHrVelocity), fReactiveMax, UPSAMPLE_F(fDepthClipFactor), UPSAMPLE_F(fLuminanceDiff), lockState + FfxFloat32(fHrVelocity), fReactiveMax, FfxFloat32(fDepthClipFactor), FfxFloat32(fLuminanceDiff), lockState )); - const UPSAMPLE_F fNormalizedLockLifetime = GetNormalizedRemainingLockLifetime(fLockStatus); + const FfxFloat32 fNormalizedLockLifetime = GetNormalizedRemainingLockLifetime(fLockStatus); // Kill accumulation based on shading change - fHistoryColorAndWeight.w = ffxMin(fHistoryColorAndWeight.w, FFX_MIN16_F(ffxMax(0.0f, MaxAccumulationWeight() * ffxPow(UPSAMPLE_F(1) - fLuminanceDiff, 2.0f / 1.0f)))); + fHistoryColorAndWeight.w = ffxMin(fHistoryColorAndWeight.w, FfxFloat32(ffxMax(0.0f, MaxAccumulationWeight() * ffxPow(FfxFloat32(1) - fLuminanceDiff, 2.0f / 1.0f)))); // Load upsampled input color RectificationBoxData clippingBox; @@ -240,20 +217,24 @@ void Accumulate(FFX_MIN16_I2 iPxHrPos) FfxFloat32 fReactiveWeighted = 0; // No trust in reactive areas - fLockStatus[LOCK_TRUST] = ffxMin(fLockStatus[LOCK_TRUST], LOCK_STATUS_F1(1.0f) - LOCK_STATUS_F1(pow(fReactiveMax, 1.0f / 3.0f))); - fLockStatus[LOCK_TRUST] = ffxMin(fLockStatus[LOCK_TRUST], LOCK_STATUS_F1(fDepthClipFactor)); + fLockStatus[LOCK_TRUST] = ffxMin(fLockStatus[LOCK_TRUST], FfxFloat32(1.0f) - FfxFloat32(pow(fReactiveMax, 1.0f / 3.0f))); + fLockStatus[LOCK_TRUST] = ffxMin(fLockStatus[LOCK_TRUST], FfxFloat32(fDepthClipFactor)); - UPSAMPLE_F2 fKernelWeight = ComputeKernelWeight(UPSAMPLE_F(fHistoryColorAndWeight.w), UPSAMPLE_F(fDepthClipFactor), ffxMax((UPSAMPLE_F(1) - fLockStatus[LOCK_TRUST]), fReactiveMax)); + FfxFloat32x2 fKernelWeight = ComputeKernelWeight(fHistoryColorAndWeight.w, FfxFloat32(fDepthClipFactor), ffxMax((FfxFloat32(1) - fLockStatus[LOCK_TRUST]), fReactiveMax)); - UPSAMPLE_F4 fUpsampledColorAndWeight = ComputeUpsampledColorAndWeight(iPxHrPos, fKernelWeight, clippingBox); + FfxFloat32x4 fUpsampledColorAndWeight = ComputeUpsampledColorAndWeight(iPxHrPos, fKernelWeight, clippingBox); +#if FFX_FSR2_OPTION_GUARANTEE_UPSAMPLE_WEIGHT_ON_NEW_SAMPLES + // Make sure all samples have same weight on reset/first frame. Upsampled weight should never be 0.0f when history accumulation is 0.0f. + fUpsampledColorAndWeight.w = (fHistoryColorAndWeight.w == 0.0f) ? ffxMax(FSR2_EPSILON, fUpsampledColorAndWeight.w) : fUpsampledColorAndWeight.w; +#endif FfxFloat32 fLockContributionThisFrame = GetLockContributionThisFrame(fHrUv, fAccumulationMask, fReactiveMax, fLockStatus); // Update accumulation and rectify history - if (fHistoryColorAndWeight.w > 0.0f) { + if (fHistoryColorAndWeight.w > FfxFloat32(0)) { - RectifyHistory(clippingBox, fHistoryColorAndWeight, fLockStatus, UPSAMPLE_F(fDepthClipFactor), UPSAMPLE_F(fLumaStabilityFactor), UPSAMPLE_F(fLuminanceDiff), fUpsampledColorAndWeight.w, fLockContributionThisFrame); + RectifyHistory(clippingBox, fHistoryColorAndWeight, fLockStatus, FfxFloat32(fDepthClipFactor), FfxFloat32(fLumaStabilityFactor), FfxFloat32(fLuminanceDiff), fUpsampledColorAndWeight.w, fLockContributionThisFrame); fHistoryColorAndWeight.rgb = YCoCgToRGB(fHistoryColorAndWeight.rgb); } @@ -261,12 +242,12 @@ void Accumulate(FFX_MIN16_I2 iPxHrPos) Accumulate(iPxHrPos, fHistoryColorAndWeight, fUpsampledColorAndWeight, fDepthClipFactor, fHrVelocity); //Subtract accumulation weight in reactive areas - fHistoryColorAndWeight.w -= FfxFloat32(fUpsampledColorAndWeight.w * fReactiveMax); + fHistoryColorAndWeight.w -= fUpsampledColorAndWeight.w * fReactiveMax; #if FFX_FSR2_OPTION_HDR_COLOR_INPUT fHistoryColorAndWeight.rgb = InverseTonemap(fHistoryColorAndWeight.rgb); #endif - fHistoryColorAndWeight.rgb /= Exposure(); + fHistoryColorAndWeight.rgb /= FfxFloat32(Exposure()); FinalizeLockStatus(iPxHrPos, fLockStatus, fUpsampledColorAndWeight.w); @@ -277,3 +258,5 @@ void Accumulate(FFX_MIN16_I2 iPxHrPos) WriteUpscaledOutput(iPxHrPos, fHistoryColorAndWeight.rgb); #endif } + +#endif // FFX_FSR2_ACCUMULATE_H diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_accumulate_pass.glsl b/src/ffx-fsr2-api/shaders/ffx_fsr2_accumulate_pass.glsl index bf85d1d..e1ee116 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_accumulate_pass.glsl +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_accumulate_pass.glsl @@ -44,8 +44,12 @@ #extension GL_EXT_samplerless_texture_functions : require #define FSR2_BIND_SRV_EXPOSURE 0 -#define FSR2_BIND_SRV_TRANSPARENCY_AND_COMPOSITION_MASK 1 +#define FSR2_BIND_SRV_DILATED_REACTIVE_MASKS 1 +#if FFX_FSR2_OPTION_LOW_RESOLUTION_MOTION_VECTORS #define FSR2_BIND_SRV_DILATED_MOTION_VECTORS 2 +#else +#define FSR2_BIND_SRV_MOTION_VECTORS 2 +#endif #define FSR2_BIND_SRV_INTERNAL_UPSCALED 3 #define FSR2_BIND_SRV_LOCK_STATUS 4 #define FSR2_BIND_SRV_DEPTH_CLIP 5 @@ -53,13 +57,12 @@ #define FSR2_BIND_SRV_LUMA_HISTORY 7 #define FSR2_BIND_SRV_LANCZOS_LUT 8 #define FSR2_BIND_SRV_UPSCALE_MAXIMUM_BIAS_LUT 9 -#define FSR2_BIND_SRV_REACTIVE_MAX 10 -#define FSR2_BIND_SRV_EXPOSURE_MIPS 11 -#define FSR2_BIND_UAV_INTERNAL_UPSCALED 12 -#define FSR2_BIND_UAV_LOCK_STATUS 13 -#define FSR2_BIND_UAV_UPSCALED_OUTPUT 14 +#define FSR2_BIND_SRV_EXPOSURE_MIPS 10 +#define FSR2_BIND_UAV_INTERNAL_UPSCALED 11 +#define FSR2_BIND_UAV_LOCK_STATUS 12 +#define FSR2_BIND_UAV_UPSCALED_OUTPUT 13 -#define FSR2_BIND_CB_FSR2 15 +#define FSR2_BIND_CB_FSR2 14 #include "ffx_fsr2_callbacks_glsl.h" #include "ffx_fsr2_common.h" @@ -92,5 +95,5 @@ void main() uvec2 uDispatchThreadId = uGroupId * uvec2(FFX_FSR2_THREAD_GROUP_WIDTH, FFX_FSR2_THREAD_GROUP_HEIGHT) + gl_LocalInvocationID.xy; - Accumulate(FFX_MIN16_I2(uDispatchThreadId)); -} + Accumulate(ivec2(uDispatchThreadId)); +} \ No newline at end of file diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_accumulate_pass.hlsl b/src/ffx-fsr2-api/shaders/ffx_fsr2_accumulate_pass.hlsl index d66b075..4321f99 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_accumulate_pass.hlsl +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_accumulate_pass.hlsl @@ -30,7 +30,7 @@ // SRV 14 : FSR2_LumaHistory : r_luma_history // SRV 16 : FSR2_LanczosLutData : r_lanczos_lut // SRV 26 : FSR2_MaximumUpsampleBias : r_upsample_maximum_bias_lut -// SRV 27 : FSR2_ReactiveMaskMax : r_reactive_max +// SRV 27 : FSR2_DilatedReactiveMasks : r_dilated_reactive_masks // SRV 28 : FSR2_ExposureMips : r_imgMips // UAV 10 : FSR2_InternalUpscaled1 : rw_internal_upscaled_color // UAV 11 : FSR2_LockStatus1 : rw_lock_status @@ -39,8 +39,11 @@ // CB 1 : FSR2DispatchOffsets #define FSR2_BIND_SRV_EXPOSURE 0 -#define FSR2_BIND_SRV_TRANSPARENCY_AND_COMPOSITION_MASK 1 +#if FFX_FSR2_OPTION_LOW_RESOLUTION_MOTION_VECTORS #define FSR2_BIND_SRV_DILATED_MOTION_VECTORS 2 +#else +#define FSR2_BIND_SRV_MOTION_VECTORS 2 +#endif #define FSR2_BIND_SRV_INTERNAL_UPSCALED 3 #define FSR2_BIND_SRV_LOCK_STATUS 4 #define FSR2_BIND_SRV_DEPTH_CLIP 5 @@ -48,7 +51,7 @@ #define FSR2_BIND_SRV_LUMA_HISTORY 7 #define FSR2_BIND_SRV_LANCZOS_LUT 8 #define FSR2_BIND_SRV_UPSCALE_MAXIMUM_BIAS_LUT 9 -#define FSR2_BIND_SRV_REACTIVE_MAX 10 +#define FSR2_BIND_SRV_DILATED_REACTIVE_MASKS 10 #define FSR2_BIND_SRV_EXPOSURE_MIPS 11 #define FSR2_BIND_UAV_INTERNAL_UPSCALED 0 #define FSR2_BIND_UAV_LOCK_STATUS 1 @@ -86,5 +89,5 @@ void CS(uint2 uGroupId : SV_GroupID, uint2 uGroupThreadId : SV_GroupThreadID) uint2 uDispatchThreadId = uGroupId * uint2(FFX_FSR2_THREAD_GROUP_WIDTH, FFX_FSR2_THREAD_GROUP_HEIGHT) + uGroupThreadId; - Accumulate(min16int2(uDispatchThreadId)); + Accumulate(uDispatchThreadId); } diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_autogen_reactive_pass.glsl b/src/ffx-fsr2-api/shaders/ffx_fsr2_autogen_reactive_pass.glsl index bef70f8..b509eb0 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_autogen_reactive_pass.glsl +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_autogen_reactive_pass.glsl @@ -28,14 +28,14 @@ #define FSR2_BIND_SRV_POST_ALPHA_COLOR 1 #define FSR2_BIND_UAV_REACTIVE 2 #define FSR2_BIND_CB_REACTIVE 3 -#define FSR2_BIND_CB_FSR2 4 +#define FSR2_BIND_CB_FSR2 4 #include "ffx_fsr2_callbacks_glsl.h" #include "ffx_fsr2_common.h" -layout (set = 1, binding = FSR2_BIND_SRV_PRE_ALPHA_COLOR) uniform texture2D r_input_color_pre_alpha; -layout (set = 1, binding = FSR2_BIND_SRV_POST_ALPHA_COLOR) uniform texture2D r_input_color_post_alpha; -layout (set = 1, binding = FSR2_BIND_UAV_REACTIVE, r8) uniform image2D rw_output_reactive_mask; +layout (set = 1, binding = FSR2_BIND_SRV_PRE_ALPHA_COLOR) uniform texture2D r_input_color_pre_alpha; +layout (set = 1, binding = FSR2_BIND_SRV_POST_ALPHA_COLOR) uniform texture2D r_input_color_post_alpha; +layout (set = 1, binding = FSR2_BIND_UAV_REACTIVE, r8) uniform image2D rw_output_reactive_mask; #ifndef FFX_FSR2_THREAD_GROUP_WIDTH @@ -55,8 +55,8 @@ layout (set = 1, binding = FSR2_BIND_CB_REACTIVE, std140) uniform cbGenerateReac { float scale; float threshold; + float binaryValue; uint flags; - float _padding_; } cbGenerateReactive; FFX_FSR2_NUM_THREADS @@ -85,7 +85,7 @@ void main() out_reactive_value = ((cbGenerateReactive.flags & FFX_FSR2_AUTOREACTIVEFLAGS_USE_COMPONENTS_MAX)!=0) ? max(delta.x, max(delta.y, delta.z)) : length(delta); out_reactive_value *= cbGenerateReactive.scale; - out_reactive_value = ((cbGenerateReactive.flags & FFX_FSR2_AUTOREACTIVEFLAGS_APPLY_THRESHOLD)!=0) ? ((out_reactive_value < cbGenerateReactive.threshold) ? 0 : 1) : out_reactive_value; + out_reactive_value = ((cbGenerateReactive.flags & FFX_FSR2_AUTOREACTIVEFLAGS_APPLY_THRESHOLD)!=0) ? ((out_reactive_value < cbGenerateReactive.threshold) ? 0 : cbGenerateReactive.binaryValue) : out_reactive_value; imageStore(rw_output_reactive_mask, FfxInt32x2(uDispatchThreadId), vec4(out_reactive_value)); } diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_autogen_reactive_pass.hlsl b/src/ffx-fsr2-api/shaders/ffx_fsr2_autogen_reactive_pass.hlsl index 0528cd0..903ceae 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_autogen_reactive_pass.hlsl +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_autogen_reactive_pass.hlsl @@ -48,8 +48,8 @@ cbuffer cbGenerateReactive : register(b0) { float scale; float threshold; + float binaryValue; uint flags; - float _padding_; }; FFX_FSR2_NUM_THREADS @@ -79,7 +79,7 @@ void CS(uint2 uGroupId : SV_GroupID, uint2 uGroupThreadId : SV_GroupThreadID) out_reactive_value = (flags & FFX_FSR2_AUTOREACTIVEFLAGS_USE_COMPONENTS_MAX) ? max(delta.x, max(delta.y, delta.z)) : length(delta); out_reactive_value *= scale; - out_reactive_value = (flags & FFX_FSR2_AUTOREACTIVEFLAGS_APPLY_THRESHOLD) ? (out_reactive_value < threshold ? 0 : 1) : out_reactive_value; + out_reactive_value = (flags & FFX_FSR2_AUTOREACTIVEFLAGS_APPLY_THRESHOLD) ? (out_reactive_value < threshold ? 0 : binaryValue) : out_reactive_value; rw_output_reactive_mask[uDispatchThreadId] = out_reactive_value; } diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_callbacks_glsl.h b/src/ffx-fsr2-api/shaders/ffx_fsr2_callbacks_glsl.h index e92e680..2cd1d15 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_callbacks_glsl.h +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_callbacks_glsl.h @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. - #include "ffx_fsr2_resources.h" #if defined(FFX_GPU) @@ -33,11 +32,11 @@ #if defined(FSR2_BIND_CB_FSR2) layout (set = 1, binding = FSR2_BIND_CB_FSR2, std140) uniform cbFSR2_t { - FfxInt32x2 iRenderSize; - FfxInt32x2 iDisplaySize; - FfxUInt32x2 uLumaMipDimensions; - FfxUInt32 uLumaMipLevelToUse; - FfxUInt32 uFrameIndex; + FfxInt32x2 iRenderSize; + FfxInt32x2 iDisplaySize; + FfxInt32x2 uLumaMipDimensions; + FfxInt32 uLumaMipLevelToUse; + FfxInt32 uFrameIndex; FfxFloat32x2 fDisplaySizeRcp; FfxFloat32x2 fJitter; FfxFloat32x4 fDeviceToViewDepth; @@ -46,15 +45,15 @@ FfxFloat32x2 reactive_mask_dim_rcp; FfxFloat32x2 MotionVectorScale; FfxFloat32x2 fDownscaleFactor; - FfxFloat32 fPreExposure; - FfxFloat32 fTanHalfFOV; + FfxFloat32 fPreExposure; + FfxFloat32 fTanHalfFOV; FfxFloat32x2 fMotionVectorJitterCancellation; - FfxFloat32 fJitterSequenceLength; - FfxFloat32 fLockInitialLifetime; - FfxFloat32 fLockTickDelta; - FfxFloat32 fDeltaTime; - FfxFloat32 fDynamicResChangeFactor; - FfxFloat32 fLumaMipRcp; + FfxFloat32 fJitterSequenceLength; + FfxFloat32 fLockInitialLifetime; + FfxFloat32 fLockTickDelta; + FfxFloat32 fDeltaTime; + FfxFloat32 fDynamicResChangeFactor; + FfxFloat32 fLumaMipRcp; } cbFSR2; #endif @@ -63,12 +62,12 @@ FfxFloat32 LumaMipRcp() return cbFSR2.fLumaMipRcp; } -FfxUInt32x2 LumaMipDimensions() +FfxInt32x2 LumaMipDimensions() { return cbFSR2.uLumaMipDimensions; } -FfxUInt32 LumaMipLevelToUse() +FfxInt32 LumaMipLevelToUse() { return cbFSR2.uLumaMipLevelToUse; } @@ -135,7 +134,7 @@ FfxFloat32 DynamicResChangeFactor() return cbFSR2.fDynamicResChangeFactor; } -FfxUInt32 FrameIndex() +FfxInt32 FrameIndex() { return cbFSR2.uFrameIndex; } @@ -143,121 +142,110 @@ FfxUInt32 FrameIndex() layout (set = 0, binding = 0) uniform sampler s_PointClamp; layout (set = 0, binding = 1) uniform sampler s_LinearClamp; -#define PREPARED_INPUT_COLOR_T FFX_MIN16_F4 -#define PREPARED_INPUT_COLOR_F3 FFX_MIN16_F3 -#define PREPARED_INPUT_COLOR_F1 FFX_MIN16_F - -#define UPSAMPLED_COLOR_T FfxFloat32x3 - -#define RW_UPSAMPLED_WEIGHT_T FfxFloat32 - -#define LOCK_STATUS_T FFX_MIN16_F3 -#define LOCK_STATUS_F1 FFX_MIN16_F - // SRVs #if defined(FSR2_BIND_SRV_INPUT_COLOR) - layout (set = 1, binding = FSR2_BIND_SRV_INPUT_COLOR) uniform texture2D r_input_color_jittered; + layout (set = 1, binding = FSR2_BIND_SRV_INPUT_COLOR) uniform texture2D r_input_color_jittered; #endif #if defined(FSR2_BIND_SRV_MOTION_VECTORS) - layout (set = 1, binding = FSR2_BIND_SRV_MOTION_VECTORS) uniform texture2D r_motion_vectors; + layout (set = 1, binding = FSR2_BIND_SRV_MOTION_VECTORS) uniform texture2D r_motion_vectors; #endif #if defined(FSR2_BIND_SRV_DEPTH) - layout (set = 1, binding = FSR2_BIND_SRV_DEPTH) uniform texture2D r_depth; + layout (set = 1, binding = FSR2_BIND_SRV_DEPTH) uniform texture2D r_depth; #endif #if defined(FSR2_BIND_SRV_EXPOSURE) - layout (set = 1, binding = FSR2_BIND_SRV_EXPOSURE) uniform texture2D r_exposure; + layout (set = 1, binding = FSR2_BIND_SRV_EXPOSURE) uniform texture2D r_exposure; #endif #if defined(FSR2_BIND_SRV_REACTIVE_MASK) - layout (set = 1, binding = FSR2_BIND_SRV_REACTIVE_MASK) uniform texture2D r_reactive_mask; + layout (set = 1, binding = FSR2_BIND_SRV_REACTIVE_MASK) uniform texture2D r_reactive_mask; #endif #if defined(FSR2_BIND_SRV_TRANSPARENCY_AND_COMPOSITION_MASK) - layout (set = 1, binding = FSR2_BIND_SRV_TRANSPARENCY_AND_COMPOSITION_MASK) uniform texture2D r_transparency_and_composition_mask; + layout (set = 1, binding = FSR2_BIND_SRV_TRANSPARENCY_AND_COMPOSITION_MASK) uniform texture2D r_transparency_and_composition_mask; #endif #if defined(FSR2_BIND_SRV_RECONSTRUCTED_PREV_NEAREST_DEPTH) - layout (set = 1, binding = FSR2_BIND_SRV_RECONSTRUCTED_PREV_NEAREST_DEPTH) uniform utexture2D r_ReconstructedPrevNearestDepth; + layout (set = 1, binding = FSR2_BIND_SRV_RECONSTRUCTED_PREV_NEAREST_DEPTH) uniform utexture2D r_reconstructed_previous_nearest_depth; #endif #if defined(FSR2_BIND_SRV_DILATED_MOTION_VECTORS) - layout (set = 1, binding = FSR2_BIND_SRV_DILATED_MOTION_VECTORS) uniform texture2D r_dilated_motion_vectors; + layout (set = 1, binding = FSR2_BIND_SRV_DILATED_MOTION_VECTORS) uniform texture2D r_dilated_motion_vectors; #endif #if defined(FSR2_BIND_SRV_DILATED_DEPTH) - layout (set = 1, binding = FSR2_BIND_SRV_DILATED_DEPTH) uniform texture2D r_dilatedDepth; + layout (set = 1, binding = FSR2_BIND_SRV_DILATED_DEPTH) uniform texture2D r_dilatedDepth; #endif #if defined(FSR2_BIND_SRV_INTERNAL_UPSCALED) - layout (set = 1, binding = FSR2_BIND_SRV_INTERNAL_UPSCALED) uniform texture2D r_internal_upscaled_color; + layout (set = 1, binding = FSR2_BIND_SRV_INTERNAL_UPSCALED) uniform texture2D r_internal_upscaled_color; #endif #if defined(FSR2_BIND_SRV_LOCK_STATUS) - layout (set = 1, binding = FSR2_BIND_SRV_LOCK_STATUS) uniform texture2D r_lock_status; + layout (set = 1, binding = FSR2_BIND_SRV_LOCK_STATUS) uniform texture2D r_lock_status; #endif #if defined(FSR2_BIND_SRV_DEPTH_CLIP) - layout (set = 1, binding = FSR2_BIND_SRV_DEPTH_CLIP) uniform texture2D r_depth_clip; + layout (set = 1, binding = FSR2_BIND_SRV_DEPTH_CLIP) uniform texture2D r_depth_clip; #endif #if defined(FSR2_BIND_SRV_PREPARED_INPUT_COLOR) - layout (set = 1, binding = FSR2_BIND_SRV_PREPARED_INPUT_COLOR) uniform texture2D r_prepared_input_color; + layout (set = 1, binding = FSR2_BIND_SRV_PREPARED_INPUT_COLOR) uniform texture2D r_prepared_input_color; #endif #if defined(FSR2_BIND_SRV_LUMA_HISTORY) - layout (set = 1, binding = FSR2_BIND_SRV_LUMA_HISTORY) uniform texture2D r_luma_history; + layout (set = 1, binding = FSR2_BIND_SRV_LUMA_HISTORY) uniform texture2D r_luma_history; #endif #if defined(FSR2_BIND_SRV_RCAS_INPUT) - layout (set = 1, binding = FSR2_BIND_SRV_RCAS_INPUT) uniform texture2D r_rcas_input; + layout (set = 1, binding = FSR2_BIND_SRV_RCAS_INPUT) uniform texture2D r_rcas_input; #endif #if defined(FSR2_BIND_SRV_LANCZOS_LUT) - layout (set = 1, binding = FSR2_BIND_SRV_LANCZOS_LUT) uniform texture2D r_lanczos_lut; + layout (set = 1, binding = FSR2_BIND_SRV_LANCZOS_LUT) uniform texture2D r_lanczos_lut; #endif #if defined(FSR2_BIND_SRV_EXPOSURE_MIPS) - layout (set = 1, binding = FSR2_BIND_SRV_EXPOSURE_MIPS) uniform texture2D r_imgMips; + layout (set = 1, binding = FSR2_BIND_SRV_EXPOSURE_MIPS) uniform texture2D r_imgMips; #endif #if defined(FSR2_BIND_SRV_UPSCALE_MAXIMUM_BIAS_LUT) - layout (set = 1, binding = FSR2_BIND_SRV_UPSCALE_MAXIMUM_BIAS_LUT) uniform texture2D r_upsample_maximum_bias_lut; + layout (set = 1, binding = FSR2_BIND_SRV_UPSCALE_MAXIMUM_BIAS_LUT) uniform texture2D r_upsample_maximum_bias_lut; #endif -#if defined(FSR2_BIND_SRV_REACTIVE_MAX) - layout (set = 1, binding = FSR2_BIND_SRV_REACTIVE_MAX) uniform texture2D r_reactive_max; +#if defined(FSR2_BIND_SRV_DILATED_REACTIVE_MASKS) + layout (set = 1, binding = FSR2_BIND_SRV_DILATED_REACTIVE_MASKS) uniform texture2D r_dilated_reactive_masks; #endif // UAV #if defined FSR2_BIND_UAV_RECONSTRUCTED_PREV_NEAREST_DEPTH - layout (set = 1, binding = FSR2_BIND_UAV_RECONSTRUCTED_PREV_NEAREST_DEPTH, r32ui) uniform uimage2D rw_ReconstructedPrevNearestDepth; + layout (set = 1, binding = FSR2_BIND_UAV_RECONSTRUCTED_PREV_NEAREST_DEPTH, r32ui) uniform uimage2D rw_reconstructed_previous_nearest_depth; #endif #if defined FSR2_BIND_UAV_DILATED_MOTION_VECTORS - layout (set = 1, binding = FSR2_BIND_UAV_DILATED_MOTION_VECTORS, rg32f) uniform image2D rw_dilated_motion_vectors; + layout (set = 1, binding = FSR2_BIND_UAV_DILATED_MOTION_VECTORS, rg32f) uniform image2D rw_dilated_motion_vectors; #endif #if defined FSR2_BIND_UAV_DILATED_DEPTH - layout (set = 1, binding = FSR2_BIND_UAV_DILATED_DEPTH, r32f) uniform image2D rw_dilatedDepth; + layout (set = 1, binding = FSR2_BIND_UAV_DILATED_DEPTH, r32f) uniform image2D rw_dilatedDepth; #endif #if defined FSR2_BIND_UAV_INTERNAL_UPSCALED - layout (set = 1, binding = FSR2_BIND_UAV_INTERNAL_UPSCALED, rgba32f) uniform image2D rw_internal_upscaled_color; + layout (set = 1, binding = FSR2_BIND_UAV_INTERNAL_UPSCALED, rgba32f) uniform image2D rw_internal_upscaled_color; #endif #if defined FSR2_BIND_UAV_LOCK_STATUS - layout (set = 1, binding = FSR2_BIND_UAV_LOCK_STATUS, r11f_g11f_b10f) uniform image2D rw_lock_status; + layout (set = 1, binding = FSR2_BIND_UAV_LOCK_STATUS, r11f_g11f_b10f) uniform image2D rw_lock_status; #endif #if defined FSR2_BIND_UAV_DEPTH_CLIP - layout (set = 1, binding = FSR2_BIND_UAV_DEPTH_CLIP, r32f) uniform image2D rw_depth_clip; + layout (set = 1, binding = FSR2_BIND_UAV_DEPTH_CLIP, r32f) uniform image2D rw_depth_clip; #endif #if defined FSR2_BIND_UAV_PREPARED_INPUT_COLOR - layout (set = 1, binding = FSR2_BIND_UAV_PREPARED_INPUT_COLOR, rgba32f) uniform image2D rw_prepared_input_color; + layout (set = 1, binding = FSR2_BIND_UAV_PREPARED_INPUT_COLOR, rgba32f) uniform image2D rw_prepared_input_color; #endif #if defined FSR2_BIND_UAV_LUMA_HISTORY - layout (set = 1, binding = FSR2_BIND_UAV_LUMA_HISTORY, rgba32f) uniform image2D rw_luma_history; + layout (set = 1, binding = FSR2_BIND_UAV_LUMA_HISTORY, rgba32f) uniform image2D rw_luma_history; #endif #if defined FSR2_BIND_UAV_UPSCALED_OUTPUT - layout (set = 1, binding = FSR2_BIND_UAV_UPSCALED_OUTPUT, rgba32f) uniform image2D rw_upscaled_output; + layout (set = 1, binding = FSR2_BIND_UAV_UPSCALED_OUTPUT, rgba32f) uniform image2D rw_upscaled_output; #endif #if defined FSR2_BIND_UAV_EXPOSURE_MIP_LUMA_CHANGE - layout (set = 1, binding = FSR2_BIND_UAV_EXPOSURE_MIP_LUMA_CHANGE, r32f) coherent uniform image2D rw_img_mip_shading_change; + layout (set = 1, binding = FSR2_BIND_UAV_EXPOSURE_MIP_LUMA_CHANGE, r32f) coherent uniform image2D rw_img_mip_shading_change; #endif #if defined FSR2_BIND_UAV_EXPOSURE_MIP_5 - layout (set = 1, binding = FSR2_BIND_UAV_EXPOSURE_MIP_5, r32f) coherent uniform image2D rw_img_mip_5; + layout (set = 1, binding = FSR2_BIND_UAV_EXPOSURE_MIP_5, r32f) coherent uniform image2D rw_img_mip_5; #endif -#if defined FSR2_BIND_UAV_REACTIVE_MASK_MAX - layout (set = 1, binding = FSR2_BIND_UAV_REACTIVE_MASK_MAX, r32f) uniform image2D rw_reactive_max; +#if defined FSR2_BIND_UAV_DILATED_REACTIVE_MASKS + layout (set = 1, binding = FSR2_BIND_UAV_DILATED_REACTIVE_MASKS, rg32f) uniform image2D rw_dilated_reactive_masks; #endif #if defined FSR2_BIND_UAV_EXPOSURE - layout (set = 1, binding = FSR2_BIND_UAV_EXPOSURE, rg32f) uniform image2D rw_exposure; + layout (set = 1, binding = FSR2_BIND_UAV_EXPOSURE, rg32f) uniform image2D rw_exposure; #endif #if defined FSR2_BIND_UAV_SPD_GLOBAL_ATOMIC - layout (set = 1, binding = FSR2_BIND_UAV_SPD_GLOBAL_ATOMIC, r32ui) coherent uniform uimage2D rw_spd_global_atomic; + layout (set = 1, binding = FSR2_BIND_UAV_SPD_GLOBAL_ATOMIC, r32ui) coherent uniform uimage2D rw_spd_global_atomic; #endif -FfxFloat32 LoadMipLuma(FfxInt32x2 iPxPos, FfxUInt32 mipLevel) +FfxFloat32 LoadMipLuma(FfxInt32x2 iPxPos, FfxInt32 mipLevel) { #if defined(FSR2_BIND_SRV_EXPOSURE_MIPS) return texelFetch(r_imgMips, iPxPos, FfxInt32(mipLevel)).r; @@ -265,17 +253,9 @@ FfxFloat32 LoadMipLuma(FfxInt32x2 iPxPos, FfxUInt32 mipLevel) return 0.f; #endif } -#if FFX_HALF -FfxFloat16 LoadMipLuma(FfxInt16x2 iPxPos, FfxUInt16 mipLevel) -{ -#if defined(FSR2_BIND_SRV_EXPOSURE_MIPS) - return FfxFloat16(texelFetch(r_imgMips, iPxPos, FfxInt32(mipLevel)).r); -#else - return FfxFloat16(0.f); -#endif -} -#endif -FfxFloat32 SampleMipLuma(FfxFloat32x2 fUV, FfxUInt32 mipLevel) + + +FfxFloat32 SampleMipLuma(FfxFloat32x2 fUV, FfxInt32 mipLevel) { #if defined(FSR2_BIND_SRV_EXPOSURE_MIPS) fUV *= cbFSR2.depthclip_uv_scale; @@ -284,18 +264,6 @@ FfxFloat32 SampleMipLuma(FfxFloat32x2 fUV, FfxUInt32 mipLevel) return 0.f; #endif } -#if FFX_HALF -FfxFloat16 SampleMipLuma(FfxFloat16x2 fUV, FfxUInt32 mipLevel) -{ -#if defined(FSR2_BIND_SRV_EXPOSURE_MIPS) - fUV *= FfxFloat16x2(cbFSR2.depthclip_uv_scale); - return FfxFloat16(textureLod(sampler2D(r_imgMips, s_LinearClamp), fUV, FfxFloat32(mipLevel)).r); -#else - return FfxFloat16(0.f); -#endif -} -#endif - // // a 0 0 0 x @@ -324,7 +292,7 @@ FfxFloat32 LoadInputDepth(FfxInt32x2 iPxPos) #endif } -FfxFloat32 LoadReactiveMask(FFX_MIN16_I2 iPxPos) +FfxFloat32 LoadReactiveMask(FfxInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_REACTIVE_MASK) return texelFetch(r_reactive_mask, FfxInt32x2(iPxPos), 0).r; @@ -342,12 +310,22 @@ FfxFloat32x4 GatherReactiveMask(FfxInt32x2 iPxPos) #endif } -FFX_MIN16_F LoadTransparencyAndCompositionMask(FFX_MIN16_I2 iPxPos) +FfxFloat32 LoadTransparencyAndCompositionMask(FfxInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_TRANSPARENCY_AND_COMPOSITION_MASK) - return FFX_MIN16_F(texelFetch(r_transparency_and_composition_mask, FfxInt32x2(iPxPos), 0).r); + return texelFetch(r_transparency_and_composition_mask, iPxPos, 0).r; #else - return FFX_MIN16_F(0.f); + return 0.f; +#endif +} + +FfxFloat32 SampleTransparencyAndCompositionMask(FfxFloat32x2 fUV) +{ +#if defined(FSR2_BIND_SRV_TRANSPARENCY_AND_COMPOSITION_MASK) + fUV *= cbFSR2.depthclip_uv_scale; + return textureLod(sampler2D(r_transparency_and_composition_mask, s_LinearClamp), fUV, 0.0f).x; +#else + return 0.f; #endif } @@ -365,48 +343,37 @@ FfxFloat32x3 LoadInputColor(FfxInt32x2 iPxPos) #endif } -FfxFloat32x3 LoadInputColorWithoutPreExposure(FFX_MIN16_I2 iPxPos) +FfxFloat32x3 LoadInputColorWithoutPreExposure(FfxInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_INPUT_COLOR) - return texelFetch(r_input_color_jittered, FfxInt32x2(iPxPos), 0).rgb; + return texelFetch(r_input_color_jittered, iPxPos, 0).rgb; #else return FfxFloat32x3(0.f); #endif } -#if FFX_HALF -FfxFloat16x3 LoadPreparedInputColor(FfxInt16x2 iPxPos) +FfxFloat32x3 LoadPreparedInputColor(FfxInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_PREPARED_INPUT_COLOR) - return FfxFloat16x3(texelFetch(r_prepared_input_color, FfxInt32x2(iPxPos), 0).rgb); + return texelFetch(r_prepared_input_color, iPxPos, 0).rgb; #else - return FfxFloat16x3(0.f); -#endif -} -#endif // #if FFX_HALF - -FFX_MIN16_F3 LoadPreparedInputColor(FfxInt32x2 iPxPos) -{ -#if defined(FSR2_BIND_SRV_PREPARED_INPUT_COLOR) - return FFX_MIN16_F3(texelFetch(r_prepared_input_color, iPxPos, 0).rgb); -#else - return FFX_MIN16_F3(0.f); + return FfxFloat32x3(0.f); #endif } -FFX_MIN16_F LoadPreparedInputColorLuma(FFX_MIN16_I2 iPxPos) +FfxFloat32 LoadPreparedInputColorLuma(FfxInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_PREPARED_INPUT_COLOR) - return FFX_MIN16_F(texelFetch(r_prepared_input_color, iPxPos, 0).a); + return texelFetch(r_prepared_input_color, iPxPos, 0).a; #else - return FFX_MIN16_F(0.f); + return 0.f; #endif } -FfxFloat32x2 LoadInputMotionVector(FFX_MIN16_I2 iPxDilatedMotionVectorPos) +FfxFloat32x2 LoadInputMotionVector(FfxInt32x2 iPxDilatedMotionVectorPos) { #if defined(FSR2_BIND_SRV_MOTION_VECTORS) - FfxFloat32x2 fSrcMotionVector = texelFetch(r_motion_vectors, FfxInt32x2(iPxDilatedMotionVectorPos), 0).xy; + FfxFloat32x2 fSrcMotionVector = texelFetch(r_motion_vectors, iPxDilatedMotionVectorPos, 0).xy; #else FfxFloat32x2 fSrcMotionVector = FfxFloat32x2(0.f); #endif @@ -420,32 +387,32 @@ FfxFloat32x2 LoadInputMotionVector(FFX_MIN16_I2 iPxDilatedMotionVectorPos) return fUvMotionVector; } -FFX_MIN16_F4 LoadHistory(FfxInt32x2 iPxHistory) +FfxFloat32x4 LoadHistory(FfxInt32x2 iPxHistory) { #if defined(FSR2_BIND_SRV_INTERNAL_UPSCALED) - return FFX_MIN16_F4(texelFetch(r_internal_upscaled_color, iPxHistory, 0)); + return texelFetch(r_internal_upscaled_color, iPxHistory, 0); #else - return FFX_MIN16_F4(0.f); + return FfxFloat32x4(0.0f); #endif } -FfxFloat32x4 LoadRwInternalUpscaledColorAndWeight(FFX_MIN16_I2 iPxPos) +FfxFloat32x4 LoadRwInternalUpscaledColorAndWeight(FfxInt32x2 iPxPos) { #if defined(FSR2_BIND_UAV_INTERNAL_UPSCALED) - return imageLoad(rw_internal_upscaled_color, FfxInt32x2(iPxPos)); + return imageLoad(rw_internal_upscaled_color, iPxPos); #else return FfxFloat32x4(0.f); #endif } -void StoreLumaHistory(FFX_MIN16_I2 iPxPos, FfxFloat32x4 fLumaHistory) +void StoreLumaHistory(FfxInt32x2 iPxPos, FfxFloat32x4 fLumaHistory) { #if defined(FSR2_BIND_UAV_LUMA_HISTORY) imageStore(rw_luma_history, FfxInt32x2(iPxPos), fLumaHistory); #endif } -FfxFloat32x4 LoadRwLumaHistory(FFX_MIN16_I2 iPxPos) +FfxFloat32x4 LoadRwLumaHistory(FfxInt32x2 iPxPos) { #if defined(FSR2_BIND_UAV_LUMA_HISTORY) return imageLoad(rw_luma_history, FfxInt32x2(iPxPos)); @@ -454,7 +421,7 @@ FfxFloat32x4 LoadRwLumaHistory(FFX_MIN16_I2 iPxPos) #endif } -FfxFloat32 LoadLumaStabilityFactor(FFX_MIN16_I2 iPxPos) +FfxFloat32 LoadLumaStabilityFactor(FfxInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_LUMA_HISTORY) return texelFetch(r_luma_history, FfxInt32x2(iPxPos), 0).w; @@ -473,63 +440,63 @@ FfxFloat32 SampleLumaStabilityFactor(FfxFloat32x2 fUV) #endif } -void StoreReprojectedHistory(FFX_MIN16_I2 iPxHistory, FFX_MIN16_F4 fHistory) +void StoreReprojectedHistory(FfxInt32x2 iPxHistory, FfxFloat32x4 fHistory) { #if defined(FSR2_BIND_UAV_INTERNAL_UPSCALED) imageStore(rw_internal_upscaled_color, iPxHistory, fHistory); #endif } -void StoreInternalColorAndWeight(FFX_MIN16_I2 iPxPos, FfxFloat32x4 fColorAndWeight) +void StoreInternalColorAndWeight(FfxInt32x2 iPxPos, FfxFloat32x4 fColorAndWeight) { #if defined(FSR2_BIND_UAV_INTERNAL_UPSCALED) imageStore(rw_internal_upscaled_color, FfxInt32x2(iPxPos), fColorAndWeight); #endif } -void StoreUpscaledOutput(FFX_MIN16_I2 iPxPos, FfxFloat32x3 fColor) +void StoreUpscaledOutput(FfxInt32x2 iPxPos, FfxFloat32x3 fColor) { #if defined(FSR2_BIND_UAV_UPSCALED_OUTPUT) imageStore(rw_upscaled_output, FfxInt32x2(iPxPos), FfxFloat32x4(fColor * PreExposure(), 1.f)); #endif } -LOCK_STATUS_T LoadLockStatus(FFX_MIN16_I2 iPxPos) +FfxFloat32x3 LoadLockStatus(FfxInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_LOCK_STATUS) - LOCK_STATUS_T fLockStatus = LOCK_STATUS_T(texelFetch(r_lock_status, iPxPos, 0).rgb); + FfxFloat32x3 fLockStatus = texelFetch(r_lock_status, iPxPos, 0).rgb; - fLockStatus[0] -= LOCK_STATUS_F1(LockInitialLifetime() * 2.0f); + fLockStatus[0] -= LockInitialLifetime() * 2.0f; return fLockStatus; #else - return LOCK_STATUS_T(0.f); + return FfxFloat32x3(0.f); #endif } -LOCK_STATUS_T LoadRwLockStatus(FfxInt32x2 iPxPos) +FfxFloat32x3 LoadRwLockStatus(FfxInt32x2 iPxPos) { #if defined(FSR2_BIND_UAV_LOCK_STATUS) - LOCK_STATUS_T fLockStatus = LOCK_STATUS_T(imageLoad(rw_lock_status, iPxPos).rgb); + FfxFloat32x3 fLockStatus = imageLoad(rw_lock_status, iPxPos).rgb; - fLockStatus[0] -= LOCK_STATUS_F1(LockInitialLifetime() * 2.0f); + fLockStatus[0] -= LockInitialLifetime() * 2.0f; return fLockStatus; #else - return LOCK_STATUS_T(0.f); + return FfxFloat32x3(0.f); #endif } -void StoreLockStatus(FFX_MIN16_I2 iPxPos, LOCK_STATUS_T fLockstatus) +void StoreLockStatus(FfxInt32x2 iPxPos, FfxFloat32x3 fLockstatus) { #if defined(FSR2_BIND_UAV_LOCK_STATUS) - fLockstatus[0] += LOCK_STATUS_F1(LockInitialLifetime() * 2.0f); + fLockstatus[0] += LockInitialLifetime() * 2.0f; imageStore(rw_lock_status, iPxPos, vec4(fLockstatus, 0.0f)); #endif } -void StorePreparedInputColor(FFX_PARAMETER_IN FFX_MIN16_I2 iPxPos, FFX_PARAMETER_IN PREPARED_INPUT_COLOR_T fTonemapped) +void StorePreparedInputColor(FFX_PARAMETER_IN FfxInt32x2 iPxPos, FFX_PARAMETER_IN FfxFloat32x4 fTonemapped) { #if defined(FSR2_BIND_UAV_PREPARED_INPUT_COLOR) imageStore(rw_prepared_input_color, iPxPos, fTonemapped); @@ -541,7 +508,7 @@ FfxBoolean IsResponsivePixel(FfxInt32x2 iPxPos) return FFX_FALSE; //not supported in prototype } -FfxFloat32 LoadDepthClip(FFX_MIN16_I2 iPxPos) +FfxFloat32 LoadDepthClip(FfxInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_DEPTH_CLIP) return texelFetch(r_depth_clip, iPxPos, 0).r; @@ -560,19 +527,19 @@ FfxFloat32 SampleDepthClip(FfxFloat32x2 fUV) #endif } -LOCK_STATUS_T SampleLockStatus(FfxFloat32x2 fUV) +FfxFloat32x3 SampleLockStatus(FfxFloat32x2 fUV) { #if defined(FSR2_BIND_SRV_LOCK_STATUS) fUV *= cbFSR2.postprocessed_lockstatus_uv_scale; - LOCK_STATUS_T fLockStatus = LOCK_STATUS_T(textureLod(sampler2D(r_lock_status, s_LinearClamp), fUV, 0.0f).rgb); - fLockStatus[0] -= LOCK_STATUS_F1(LockInitialLifetime() * 2.0f); + FfxFloat32x3 fLockStatus = textureLod(sampler2D(r_lock_status, s_LinearClamp), fUV, 0.0f).rgb; + fLockStatus[0] -= LockInitialLifetime() * 2.0f; return fLockStatus; #else - return LOCK_STATUS_T(0.f); + return FfxFloat32x3(0.f); #endif } -void StoreDepthClip(FFX_MIN16_I2 iPxPos, FfxFloat32 fClip) +void StoreDepthClip(FfxInt32x2 iPxPos, FfxFloat32 fClip) { #if defined(FSR2_BIND_UAV_DEPTH_CLIP) imageStore(rw_depth_clip, iPxPos, vec4(fClip, 0.0f, 0.0f, 0.0f)); @@ -584,7 +551,7 @@ FfxFloat32 TanHalfFoV() return cbFSR2.fTanHalfFOV; } -FfxFloat32 LoadSceneDepth(FFX_MIN16_I2 iPxInput) +FfxFloat32 LoadSceneDepth(FfxInt32x2 iPxInput) { #if defined(FSR2_BIND_SRV_DEPTH) return texelFetch(r_depth, iPxInput, 0).r; @@ -593,35 +560,35 @@ FfxFloat32 LoadSceneDepth(FFX_MIN16_I2 iPxInput) #endif } -FfxFloat32 LoadReconstructedPrevDepth(FFX_MIN16_I2 iPxPos) +FfxFloat32 LoadReconstructedPrevDepth(FfxInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_RECONSTRUCTED_PREV_NEAREST_DEPTH) - return uintBitsToFloat(texelFetch(r_ReconstructedPrevNearestDepth, iPxPos, 0).r); + return uintBitsToFloat(texelFetch(r_reconstructed_previous_nearest_depth, iPxPos, 0).r); #else return 0.f; #endif } -void StoreReconstructedDepth(FFX_MIN16_I2 iPxSample, FfxFloat32 fDepth) +void StoreReconstructedDepth(FfxInt32x2 iPxSample, FfxFloat32 fDepth) { FfxUInt32 uDepth = floatBitsToUint(fDepth); #if defined(FSR2_BIND_UAV_RECONSTRUCTED_PREV_NEAREST_DEPTH) #if FFX_FSR2_OPTION_INVERTED_DEPTH - imageAtomicMax(rw_ReconstructedPrevNearestDepth, iPxSample, uDepth); + imageAtomicMax(rw_reconstructed_previous_nearest_depth, iPxSample, uDepth); #else - imageAtomicMin(rw_ReconstructedPrevNearestDepth, iPxSample, uDepth); // min for standard, max for inverted depth + imageAtomicMin(rw_reconstructed_previous_nearest_depth, iPxSample, uDepth); // min for standard, max for inverted depth #endif #endif } -void SetReconstructedDepth(FFX_MIN16_I2 iPxSample, FfxUInt32 uValue) +void SetReconstructedDepth(FfxInt32x2 iPxSample, FfxUInt32 uValue) { #if defined(FSR2_BIND_UAV_RECONSTRUCTED_PREV_NEAREST_DEPTH) - imageStore(rw_ReconstructedPrevNearestDepth, iPxSample, uvec4(uValue, 0, 0, 0)); + imageStore(rw_reconstructed_previous_nearest_depth, iPxSample, uvec4(uValue, 0, 0, 0)); #endif } -void StoreDilatedDepth(FFX_PARAMETER_IN FFX_MIN16_I2 iPxPos, FFX_PARAMETER_IN FfxFloat32 fDepth) +void StoreDilatedDepth(FFX_PARAMETER_IN FfxInt32x2 iPxPos, FFX_PARAMETER_IN FfxFloat32 fDepth) { #if defined(FSR2_BIND_UAV_DILATED_DEPTH) //FfxUInt32 uDepth = f32tof16(fDepth); @@ -629,14 +596,14 @@ void StoreDilatedDepth(FFX_PARAMETER_IN FFX_MIN16_I2 iPxPos, FFX_PARAMETER_IN Ff #endif } -void StoreDilatedMotionVector(FFX_PARAMETER_IN FFX_MIN16_I2 iPxPos, FFX_PARAMETER_IN FfxFloat32x2 fMotionVector) +void StoreDilatedMotionVector(FFX_PARAMETER_IN FfxInt32x2 iPxPos, FFX_PARAMETER_IN FfxFloat32x2 fMotionVector) { #if defined(FSR2_BIND_UAV_DILATED_MOTION_VECTORS) imageStore(rw_dilated_motion_vectors, iPxPos, vec4(fMotionVector, 0.0f, 0.0f)); #endif } -FfxFloat32x2 LoadDilatedMotionVector(FFX_MIN16_I2 iPxInput) +FfxFloat32x2 LoadDilatedMotionVector(FfxInt32x2 iPxInput) { #if defined(FSR2_BIND_SRV_DILATED_MOTION_VECTORS) return texelFetch(r_dilated_motion_vectors, iPxInput, 0).rg; @@ -655,7 +622,7 @@ FfxFloat32x2 SampleDilatedMotionVector(FfxFloat32x2 fUV) #endif } -FfxFloat32 LoadDilatedDepth(FFX_MIN16_I2 iPxInput) +FfxFloat32 LoadDilatedDepth(FfxInt32x2 iPxInput) { #if defined(FSR2_BIND_SRV_DILATED_DEPTH) return texelFetch(r_dilatedDepth, iPxInput, 0).r; @@ -688,41 +655,41 @@ FfxFloat32 SampleLanczos2Weight(FfxFloat32 x) #endif } -#if FFX_HALF -FfxFloat16 SampleLanczos2Weight(FfxFloat16 x) -{ -#if defined(FSR2_BIND_SRV_LANCZOS_LUT) - return FfxFloat16(textureLod(sampler2D(r_lanczos_lut, s_LinearClamp), FfxFloat32x2(x / 2.0f, 0.5f), 0.0f).x); -#else - return FfxFloat16(0.f); -#endif -} -#endif - -FFX_MIN16_F SampleUpsampleMaximumBias(FFX_MIN16_F2 uv) +FfxFloat32 SampleUpsampleMaximumBias(FfxFloat32x2 uv) { #if defined(FSR2_BIND_SRV_UPSCALE_MAXIMUM_BIAS_LUT) // Stored as a SNORM, so make sure to multiply by 2 to retrieve the actual expected range. - return FFX_MIN16_F(2.0f) * FFX_MIN16_F(textureLod(sampler2D(r_upsample_maximum_bias_lut, s_LinearClamp), abs(uv) * 2.0f, 0.0f).r); + return FfxFloat32(2.0f) * FfxFloat32(textureLod(sampler2D(r_upsample_maximum_bias_lut, s_LinearClamp), abs(uv) * 2.0f, 0.0f).r); #else - return FFX_MIN16_F(0.f); + return FfxFloat32(0.f); #endif } -FFX_MIN16_F LoadReactiveMax(FFX_PARAMETER_IN FFX_MIN16_I2 iPxPos) +FfxFloat32x2 SampleDilatedReactiveMasks(FfxFloat32x2 fUV) { -#if defined(FSR2_BIND_SRV_REACTIVE_MAX) - return FFX_MIN16_F(texelFetch(r_reactive_max, iPxPos, 0).r); +#if defined(FSR2_BIND_SRV_DILATED_REACTIVE_MASKS) + fUV *= cbFSR2.depthclip_uv_scale; // TODO: assuming these are (RenderSize() / MaxRenderSize()) + return textureLod(sampler2D(r_dilated_reactive_masks, s_LinearClamp), fUV, 0.0f).rg; #else - return FFX_MIN16_F(0.f); + return FfxFloat32x2(0.f); #endif } -void StoreReactiveMax(FFX_PARAMETER_IN FFX_MIN16_I2 iPxPos, FFX_PARAMETER_IN FFX_MIN16_F fReactiveMax) +FfxFloat32x2 LoadDilatedReactiveMasks(FFX_PARAMETER_IN FfxInt32x2 iPxPos) { -#if defined(FSR2_BIND_UAV_REACTIVE_MASK_MAX) - imageStore(rw_reactive_max, iPxPos, vec4(FfxFloat32(fReactiveMax), 0.0f, 0.0f, 0.0f)); +#if defined(FSR2_BIND_SRV_DILATED_REACTIVE_MASKS) + return texelFetch(r_dilated_reactive_masks, iPxPos, 0).rg; +#else + return FfxFloat32x2(0.f); #endif } +void StoreDilatedReactiveMasks(FFX_PARAMETER_IN FfxInt32x2 iPxPos, FFX_PARAMETER_IN FfxFloat32x2 fDilatedReactiveMasks) +{ +#if defined(FSR2_BIND_UAV_DILATED_REACTIVE_MASKS) + imageStore(rw_dilated_reactive_masks, iPxPos, vec4(fDilatedReactiveMasks, 0.0f, 0.0f)); +#endif +} + + #endif // #if defined(FFX_GPU) diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_callbacks_hlsl.h b/src/ffx-fsr2-api/shaders/ffx_fsr2_callbacks_hlsl.h index a04a949..646847e 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_callbacks_hlsl.h +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_callbacks_hlsl.h @@ -51,12 +51,11 @@ #if defined(FSR2_BIND_CB_FSR2) cbuffer cbFSR2 : FFX_FSR2_DECLARE_CB(FSR2_BIND_CB_FSR2) { - - FfxInt32x2 iRenderSize; - FfxInt32x2 iDisplaySize; - FfxUInt32x2 uLumaMipDimensions; - FfxUInt32 uLumaMipLevelToUse; - FfxUInt32 uFrameIndex; + FfxInt32x2 uRenderSize; + FfxInt32x2 uDisplaySize; + FfxInt32x2 uLumaMipDimensions; + FfxInt32 uLumaMipLevelToUse; + FfxUInt32 uFrameIndex; FfxFloat32x2 fDisplaySizeRcp; FfxFloat32x2 fJitter; FfxFloat32x4 fDeviceToViewDepth; @@ -65,23 +64,23 @@ FfxFloat32x2 reactive_mask_dim_rcp; FfxFloat32x2 MotionVectorScale; FfxFloat32x2 fDownscaleFactor; - FfxFloat32 fPreExposure; - FfxFloat32 fTanHalfFOV; + FfxFloat32 fPreExposure; + FfxFloat32 fTanHalfFOV; FfxFloat32x2 fMotionVectorJitterCancellation; - FfxFloat32 fJitterSequenceLength; - FfxFloat32 fLockInitialLifetime; - FfxFloat32 fLockTickDelta; - FfxFloat32 fDeltaTime; - FfxFloat32 fDynamicResChangeFactor; - FfxFloat32 fLumaMipRcp; + FfxFloat32 fJitterSequenceLength; + FfxFloat32 fLockInitialLifetime; + FfxFloat32 fLockTickDelta; + FfxFloat32 fDeltaTime; + FfxFloat32 fDynamicResChangeFactor; + FfxFloat32 fLumaMipRcp; #define FFX_FSR2_CONSTANT_BUFFER_1_SIZE 36 // Number of 32-bit values. This must be kept in sync with the cbFSR2 size. }; #else #define iRenderSize 0 #define iDisplaySize 0 - #define uLumaMipDimensions 0 - #define uLumaMipLevelToUse 0 - #define uFrameIndex 0 + #define iLumaMipDimensions 0 + #define iLumaMipLevelToUse 0 + #define iFrameIndex 0 #define fDisplaySizeRcp 0 #define fJitter 0 #define fDeviceToViewDepth FfxFloat32x4(0,0,0,0) @@ -153,12 +152,12 @@ FfxFloat32 LumaMipRcp() return fLumaMipRcp; } -uint2 LumaMipDimensions() +FfxInt32x2 LumaMipDimensions() { return uLumaMipDimensions; } -FfxUInt32 LumaMipLevelToUse() +FfxInt32 LumaMipLevelToUse() { return uLumaMipLevelToUse; } @@ -178,14 +177,14 @@ FfxFloat32x2 MotionVectorJitterCancellation() return fMotionVectorJitterCancellation; } -int2 RenderSize() +FfxInt32x2 RenderSize() { - return iRenderSize; + return uRenderSize; } -int2 DisplaySize() +FfxInt32x2 DisplaySize() { - return iDisplaySize; + return uDisplaySize; } FfxFloat32x2 DisplaySizeRcp() @@ -233,198 +232,171 @@ FfxUInt32 FrameIndex() SamplerState s_PointClamp : register(s0); SamplerState s_LinearClamp : register(s1); - -typedef FFX_MIN16_F4 PREPARED_INPUT_COLOR_T; -typedef FFX_MIN16_F3 PREPARED_INPUT_COLOR_F3; -typedef FFX_MIN16_F PREPARED_INPUT_COLOR_F1; - -typedef FfxFloat32x3 UPSAMPLED_COLOR_T; - -#define RW_UPSAMPLED_WEIGHT_T FfxFloat32 - -typedef FFX_MIN16_F3 LOCK_STATUS_T; -typedef FFX_MIN16_F LOCK_STATUS_F1; - // SRVs #if defined(FFX_INTERNAL) - Texture2D r_input_color_jittered : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_COLOR); - Texture2D r_motion_vectors : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_MOTION_VECTORS); - Texture2D r_depth : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_DEPTH); - Texture2D r_exposure : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_EXPOSURE); - Texture2D r_reactive_mask : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_REACTIVE_MASK); - Texture2D r_transparency_and_composition_mask : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_TRANSPARENCY_AND_COMPOSITION_MASK); - Texture2D r_ReconstructedPrevNearestDepth : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_RECONSTRUCTED_PREVIOUS_NEAREST_DEPTH); - Texture2D r_dilated_motion_vectors : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_MOTION_VECTORS); - Texture2D r_dilatedDepth : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_DEPTH); - Texture2D r_internal_upscaled_color : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_INTERNAL_UPSCALED_COLOR); - Texture2D r_lock_status : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_LOCK_STATUS); - Texture2D r_depth_clip : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_DEPTH_CLIP); - Texture2D r_prepared_input_color : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_PREPARED_INPUT_COLOR); - Texture2D r_luma_history : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_LUMA_HISTORY); - Texture2D r_rcas_input : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_RCAS_INPUT); - Texture2D r_lanczos_lut : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_LANCZOS_LUT); - Texture2D r_imgMips : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE); - Texture2D r_upsample_maximum_bias_lut : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTITIER_UPSAMPLE_MAXIMUM_BIAS_LUT); - Texture2D r_reactive_max : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_REACTIVE_MAX); + Texture2D r_input_color_jittered : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_COLOR); + Texture2D r_motion_vectors : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_MOTION_VECTORS); + Texture2D r_depth : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_DEPTH); + Texture2D r_exposure : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_EXPOSURE); + Texture2D r_reactive_mask : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_REACTIVE_MASK); + Texture2D r_transparency_and_composition_mask : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_INPUT_TRANSPARENCY_AND_COMPOSITION_MASK); + Texture2D r_reconstructed_previous_nearest_depth : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_RECONSTRUCTED_PREVIOUS_NEAREST_DEPTH); + Texture2D r_dilated_motion_vectors : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_MOTION_VECTORS); + Texture2D r_dilatedDepth : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_DEPTH); + Texture2D r_internal_upscaled_color : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_INTERNAL_UPSCALED_COLOR); + Texture2D r_lock_status : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_LOCK_STATUS); + Texture2D r_depth_clip : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_DEPTH_CLIP); + Texture2D r_prepared_input_color : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_PREPARED_INPUT_COLOR); + Texture2D r_luma_history : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_LUMA_HISTORY); + Texture2D r_rcas_input : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_RCAS_INPUT); + Texture2D r_lanczos_lut : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_LANCZOS_LUT); + Texture2D r_imgMips : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE); + Texture2D r_upsample_maximum_bias_lut : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTITIER_UPSAMPLE_MAXIMUM_BIAS_LUT); + Texture2D r_dilated_reactive_masks : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_REACTIVE_MASKS); + Texture2D r_debug_out : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_DEBUG_OUTPUT); // declarations not current form, no accessor functions - Texture2D r_transparency_mask : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_TRANSPARENCY_MASK); - Texture2D r_bias_current_color_mask : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_BIAS_CURRENT_COLOR_MASK); - Texture2D r_gbuffer_albedo : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_GBUFFER_ALBEDO); - Texture2D r_gbuffer_roughness : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_GBUFFER_ROUGHNESS); - Texture2D r_gbuffer_metallic : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_GBUFFER_METALLIC); - Texture2D r_gbuffer_specular : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_GBUFFER_SPECULAR); - Texture2D r_gbuffer_subsurface : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_GBUFFER_SUBSURFACE); - Texture2D r_gbuffer_normals : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_GBUFFER_NORMALS); - Texture2D r_gbuffer_shading_mode_id : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_GBUFFER_SHADING_MODE_ID); - Texture2D r_gbuffer_material_id : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_GBUFFER_MATERIAL_ID); - Texture2D r_motion_vectors_3d : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_VELOCITY_3D); - Texture2D r_is_particle_mask : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_IS_PARTICLE_MASK); - Texture2D r_animated_texture_mask : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_ANIMATED_TEXTURE_MASK); - Texture2D r_depth_high_res : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_DEPTH_HIGH_RES); - Texture2D r_position_view_space : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_POSITION_VIEW_SPACE); - Texture2D r_ray_tracing_hit_distance : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_RAY_TRACING_HIT_DISTANCE); - Texture2D r_motion_vectors_reflection : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_VELOCITY_REFLECTION); + Texture2D r_transparency_mask : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_TRANSPARENCY_MASK); + Texture2D r_bias_current_color_mask : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_BIAS_CURRENT_COLOR_MASK); + Texture2D r_gbuffer_albedo : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_GBUFFER_ALBEDO); + Texture2D r_gbuffer_roughness : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_GBUFFER_ROUGHNESS); + Texture2D r_gbuffer_metallic : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_GBUFFER_METALLIC); + Texture2D r_gbuffer_specular : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_GBUFFER_SPECULAR); + Texture2D r_gbuffer_subsurface : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_GBUFFER_SUBSURFACE); + Texture2D r_gbuffer_normals : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_GBUFFER_NORMALS); + Texture2D r_gbuffer_shading_mode_id : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_GBUFFER_SHADING_MODE_ID); + Texture2D r_gbuffer_material_id : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_GBUFFER_MATERIAL_ID); + Texture2D r_motion_vectors_3d : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_VELOCITY_3D); + Texture2D r_is_particle_mask : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_IS_PARTICLE_MASK); + Texture2D r_animated_texture_mask : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_ANIMATED_TEXTURE_MASK); + Texture2D r_depth_high_res : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_DEPTH_HIGH_RES); + Texture2D r_position_view_space : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_POSITION_VIEW_SPACE); + Texture2D r_ray_tracing_hit_distance : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_RAY_TRACING_HIT_DISTANCE); + Texture2D r_motion_vectors_reflection : FFX_FSR2_DECLARE_SRV(FFX_FSR2_RESOURCE_IDENTIFIER_VELOCITY_REFLECTION); // UAV declarations - RWTexture2D rw_ReconstructedPrevNearestDepth : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_RECONSTRUCTED_PREVIOUS_NEAREST_DEPTH); - RWTexture2D rw_dilated_motion_vectors : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_MOTION_VECTORS); - RWTexture2D rw_dilatedDepth : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_DEPTH); - RWTexture2D rw_internal_upscaled_color : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_INTERNAL_UPSCALED_COLOR); - RWTexture2D rw_lock_status : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_LOCK_STATUS); - RWTexture2D rw_depth_clip : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_DEPTH_CLIP); - RWTexture2D rw_prepared_input_color : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_PREPARED_INPUT_COLOR); - RWTexture2D rw_luma_history : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_LUMA_HISTORY); - RWTexture2D rw_upscaled_output : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_UPSCALED_OUTPUT); - //globallycoherent RWTexture2D rw_imgMipmap[13] : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE); - globallycoherent RWTexture2D rw_img_mip_shading_change : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_SHADING_CHANGE); - globallycoherent RWTexture2D rw_img_mip_5 : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_5); - RWTexture2D rw_reactive_max : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_REACTIVE_MAX); - RWTexture2D rw_exposure : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_EXPOSURE); - globallycoherent RWTexture2D rw_spd_global_atomic : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_SPD_ATOMIC_COUNT); - RWTexture2D rw_debug_out : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_DEBUG_OUTPUT); + RWTexture2D rw_reconstructed_previous_nearest_depth : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_RECONSTRUCTED_PREVIOUS_NEAREST_DEPTH); + RWTexture2D rw_dilated_motion_vectors : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_MOTION_VECTORS); + RWTexture2D rw_dilatedDepth : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_DEPTH); + RWTexture2D rw_internal_upscaled_color : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_INTERNAL_UPSCALED_COLOR); + RWTexture2D rw_lock_status : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_LOCK_STATUS); + RWTexture2D rw_depth_clip : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_DEPTH_CLIP); + RWTexture2D rw_prepared_input_color : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_PREPARED_INPUT_COLOR); + RWTexture2D rw_luma_history : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_LUMA_HISTORY); + RWTexture2D rw_upscaled_output : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_UPSCALED_OUTPUT); + + globallycoherent RWTexture2D rw_img_mip_shading_change : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_SHADING_CHANGE); + globallycoherent RWTexture2D rw_img_mip_5 : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_5); + RWTexture2D rw_dilated_reactive_masks : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_REACTIVE_MASKS); + RWTexture2D rw_exposure : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_EXPOSURE); + globallycoherent RWTexture2D rw_spd_global_atomic : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_SPD_ATOMIC_COUNT); + RWTexture2D rw_debug_out : FFX_FSR2_DECLARE_UAV(FFX_FSR2_RESOURCE_IDENTIFIER_DEBUG_OUTPUT); #else // #if defined(FFX_INTERNAL) #if defined FSR2_BIND_SRV_INPUT_COLOR - Texture2D r_input_color_jittered : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_INPUT_COLOR); + Texture2D r_input_color_jittered : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_INPUT_COLOR); #endif #if defined FSR2_BIND_SRV_MOTION_VECTORS - Texture2D r_motion_vectors : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_MOTION_VECTORS); + Texture2D r_motion_vectors : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_MOTION_VECTORS); #endif #if defined FSR2_BIND_SRV_DEPTH - Texture2D r_depth : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_DEPTH); + Texture2D r_depth : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_DEPTH); #endif #if defined FSR2_BIND_SRV_EXPOSURE - Texture2D r_exposure : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_EXPOSURE); + Texture2D r_exposure : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_EXPOSURE); #endif #if defined FSR2_BIND_SRV_REACTIVE_MASK - Texture2D r_reactive_mask : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_REACTIVE_MASK); + Texture2D r_reactive_mask : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_REACTIVE_MASK); #endif #if defined FSR2_BIND_SRV_TRANSPARENCY_AND_COMPOSITION_MASK - Texture2D r_transparency_and_composition_mask : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_TRANSPARENCY_AND_COMPOSITION_MASK); + Texture2D r_transparency_and_composition_mask : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_TRANSPARENCY_AND_COMPOSITION_MASK); #endif #if defined FSR2_BIND_SRV_RECONSTRUCTED_PREV_NEAREST_DEPTH - Texture2D r_ReconstructedPrevNearestDepth : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_RECONSTRUCTED_PREV_NEAREST_DEPTH); + Texture2D r_reconstructed_previous_nearest_depth : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_RECONSTRUCTED_PREV_NEAREST_DEPTH); #endif #if defined FSR2_BIND_SRV_DILATED_MOTION_VECTORS - Texture2D r_dilated_motion_vectors : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_DILATED_MOTION_VECTORS); + Texture2D r_dilated_motion_vectors : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_DILATED_MOTION_VECTORS); #endif #if defined FSR2_BIND_SRV_DILATED_DEPTH - Texture2D r_dilatedDepth : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_DILATED_DEPTH); + Texture2D r_dilatedDepth : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_DILATED_DEPTH); #endif #if defined FSR2_BIND_SRV_INTERNAL_UPSCALED - Texture2D r_internal_upscaled_color : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_INTERNAL_UPSCALED); + Texture2D r_internal_upscaled_color : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_INTERNAL_UPSCALED); #endif #if defined FSR2_BIND_SRV_LOCK_STATUS - #if FFX_COMPILE_FOR_SPIRV - Texture2D r_lock_status : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_LOCK_STATUS); - #else - Texture2D r_lock_status : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_LOCK_STATUS); - #endif + Texture2D r_lock_status : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_LOCK_STATUS); #endif #if defined FSR2_BIND_SRV_DEPTH_CLIP - Texture2D r_depth_clip : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_DEPTH_CLIP); + Texture2D r_depth_clip : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_DEPTH_CLIP); #endif #if defined FSR2_BIND_SRV_PREPARED_INPUT_COLOR - #if FFX_COMPILE_FOR_SPIRV - Texture2D r_prepared_input_color : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_PREPARED_INPUT_COLOR); - #else - Texture2D r_prepared_input_color : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_PREPARED_INPUT_COLOR); - #endif + Texture2D r_prepared_input_color : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_PREPARED_INPUT_COLOR); #endif #if defined FSR2_BIND_SRV_LUMA_HISTORY - Texture2D r_luma_history : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_LUMA_HISTORY); + Texture2D r_luma_history : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_LUMA_HISTORY); #endif #if defined FSR2_BIND_SRV_RCAS_INPUT - Texture2D r_rcas_input : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_RCAS_INPUT); + Texture2D r_rcas_input : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_RCAS_INPUT); #endif #if defined FSR2_BIND_SRV_LANCZOS_LUT - Texture2D r_lanczos_lut : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_LANCZOS_LUT); + Texture2D r_lanczos_lut : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_LANCZOS_LUT); #endif #if defined FSR2_BIND_SRV_EXPOSURE_MIPS - Texture2D r_imgMips : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_EXPOSURE_MIPS); + Texture2D r_imgMips : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_EXPOSURE_MIPS); #endif #if defined FSR2_BIND_SRV_UPSCALE_MAXIMUM_BIAS_LUT - Texture2D r_upsample_maximum_bias_lut : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_UPSCALE_MAXIMUM_BIAS_LUT); + Texture2D r_upsample_maximum_bias_lut : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_UPSCALE_MAXIMUM_BIAS_LUT); #endif - #if defined FSR2_BIND_SRV_REACTIVE_MAX - Texture2D r_reactive_max : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_REACTIVE_MAX); + #if defined FSR2_BIND_SRV_DILATED_REACTIVE_MASKS + Texture2D r_dilated_reactive_masks : FFX_FSR2_DECLARE_SRV(FSR2_BIND_SRV_DILATED_REACTIVE_MASKS); #endif // UAV declarations #if defined FSR2_BIND_UAV_RECONSTRUCTED_PREV_NEAREST_DEPTH - RWTexture2D rw_ReconstructedPrevNearestDepth : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_RECONSTRUCTED_PREV_NEAREST_DEPTH); + RWTexture2D rw_reconstructed_previous_nearest_depth : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_RECONSTRUCTED_PREV_NEAREST_DEPTH); #endif #if defined FSR2_BIND_UAV_DILATED_MOTION_VECTORS - RWTexture2D rw_dilated_motion_vectors : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_DILATED_MOTION_VECTORS); + RWTexture2D rw_dilated_motion_vectors : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_DILATED_MOTION_VECTORS); #endif #if defined FSR2_BIND_UAV_DILATED_DEPTH - RWTexture2D rw_dilatedDepth : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_DILATED_DEPTH); + RWTexture2D rw_dilatedDepth : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_DILATED_DEPTH); #endif #if defined FSR2_BIND_UAV_INTERNAL_UPSCALED - RWTexture2D rw_internal_upscaled_color : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_INTERNAL_UPSCALED); + RWTexture2D rw_internal_upscaled_color : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_INTERNAL_UPSCALED); #endif #if defined FSR2_BIND_UAV_LOCK_STATUS - #if FFX_COMPILE_FOR_SPIRV - RWTexture2D rw_lock_status : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_LOCK_STATUS); - #else - RWTexture2D rw_lock_status : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_LOCK_STATUS); - #endif + RWTexture2D rw_lock_status : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_LOCK_STATUS); #endif #if defined FSR2_BIND_UAV_DEPTH_CLIP - RWTexture2D rw_depth_clip : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_DEPTH_CLIP); + RWTexture2D rw_depth_clip : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_DEPTH_CLIP); #endif #if defined FSR2_BIND_UAV_PREPARED_INPUT_COLOR - #if FFX_COMPILE_FOR_SPIRV - RWTexture2D rw_prepared_input_color : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_PREPARED_INPUT_COLOR); - #else - RWTexture2D rw_prepared_input_color : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_PREPARED_INPUT_COLOR); - #endif + RWTexture2D rw_prepared_input_color : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_PREPARED_INPUT_COLOR); #endif #if defined FSR2_BIND_UAV_LUMA_HISTORY - RWTexture2D rw_luma_history : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_LUMA_HISTORY); + RWTexture2D rw_luma_history : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_LUMA_HISTORY); #endif #if defined FSR2_BIND_UAV_UPSCALED_OUTPUT - RWTexture2D rw_upscaled_output : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_UPSCALED_OUTPUT); + RWTexture2D rw_upscaled_output : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_UPSCALED_OUTPUT); #endif #if defined FSR2_BIND_UAV_EXPOSURE_MIP_LUMA_CHANGE - globallycoherent RWTexture2D rw_img_mip_shading_change : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_EXPOSURE_MIP_LUMA_CHANGE); + globallycoherent RWTexture2D rw_img_mip_shading_change : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_EXPOSURE_MIP_LUMA_CHANGE); #endif #if defined FSR2_BIND_UAV_EXPOSURE_MIP_5 - globallycoherent RWTexture2D rw_img_mip_5 : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_EXPOSURE_MIP_5); + globallycoherent RWTexture2D rw_img_mip_5 : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_EXPOSURE_MIP_5); #endif - #if defined FSR2_BIND_UAV_REACTIVE_MASK_MAX - RWTexture2D rw_reactive_max : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_REACTIVE_MASK_MAX); + #if defined FSR2_BIND_UAV_DILATED_REACTIVE_MASKS + RWTexture2D rw_dilated_reactive_masks : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_DILATED_REACTIVE_MASKS); #endif #if defined FSR2_BIND_UAV_EXPOSURE - RWTexture2D rw_exposure : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_EXPOSURE); + RWTexture2D rw_exposure : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_EXPOSURE); #endif #if defined FSR2_BIND_UAV_SPD_GLOBAL_ATOMIC - globallycoherent RWTexture2D rw_spd_global_atomic : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_SPD_GLOBAL_ATOMIC); + globallycoherent RWTexture2D rw_spd_global_atomic : FFX_FSR2_DECLARE_UAV(FSR2_BIND_UAV_SPD_GLOBAL_ATOMIC); #endif #endif // #if defined(FFX_INTERNAL) -FfxFloat32 LoadMipLuma(FFX_MIN16_I2 iPxPos, FfxUInt32 mipLevel) +FfxFloat32 LoadMipLuma(FfxUInt32x2 iPxPos, FfxUInt32 mipLevel) { #if defined(FSR2_BIND_SRV_EXPOSURE_MIPS) || defined(FFX_INTERNAL) return r_imgMips.mips[mipLevel][iPxPos]; @@ -432,16 +404,7 @@ FfxFloat32 LoadMipLuma(FFX_MIN16_I2 iPxPos, FfxUInt32 mipLevel) return 0.f; #endif } -#if FFX_HALF -FfxFloat16 LoadMipLuma(FfxInt16x2 iPxPos, FfxUInt16 mipLevel) -{ -#if defined(FSR2_BIND_SRV_EXPOSURE_MIPS) || defined(FFX_INTERNAL) - return r_imgMips.mips[mipLevel][iPxPos]; -#else - return 0.f; -#endif -} -#endif + FfxFloat32 SampleMipLuma(FfxFloat32x2 fUV, FfxUInt32 mipLevel) { #if defined(FSR2_BIND_SRV_EXPOSURE_MIPS) || defined(FFX_INTERNAL) @@ -452,17 +415,6 @@ FfxFloat32 SampleMipLuma(FfxFloat32x2 fUV, FfxUInt32 mipLevel) #endif } -#if FFX_HALF -FfxFloat16 SampleMipLuma(FfxFloat16x2 fUV, FfxUInt32 mipLevel) -{ -#if defined(FSR2_BIND_SRV_EXPOSURE_MIPS) || defined(FFX_INTERNAL) - fUV *= FfxFloat16x2(depthclip_uv_scale); - return r_imgMips.SampleLevel(s_LinearClamp, fUV, mipLevel); -#else - return 0.f; -#endif -} -#endif // // a 0 0 0 x @@ -482,7 +434,7 @@ FfxFloat32 ConvertFromDeviceDepthToViewSpace(FfxFloat32 fDeviceDepth) return -fDeviceToViewDepth[2] / (fDeviceDepth * fDeviceToViewDepth[1] - fDeviceToViewDepth[0]); } -FfxFloat32 LoadInputDepth(FFX_MIN16_I2 iPxPos) +FfxFloat32 LoadInputDepth(FfxUInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_DEPTH) || defined(FFX_INTERNAL) return r_depth[iPxPos]; @@ -491,7 +443,7 @@ FfxFloat32 LoadInputDepth(FFX_MIN16_I2 iPxPos) #endif } -FFX_MIN16_F LoadReactiveMask(FFX_MIN16_I2 iPxPos) +FfxFloat32 LoadReactiveMask(FfxUInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_REACTIVE_MASK) || defined(FFX_INTERNAL) return r_reactive_mask[iPxPos]; @@ -500,7 +452,7 @@ FFX_MIN16_F LoadReactiveMask(FFX_MIN16_I2 iPxPos) #endif } -FfxFloat32x4 GatherReactiveMask(int2 iPxPos) +FfxFloat32x4 GatherReactiveMask(FfxUInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_REACTIVE_MASK) || defined(FFX_INTERNAL) return r_reactive_mask.GatherRed(s_LinearClamp, FfxFloat32x2(iPxPos) * reactive_mask_dim_rcp); @@ -509,7 +461,7 @@ FfxFloat32x4 GatherReactiveMask(int2 iPxPos) #endif } -FFX_MIN16_F LoadTransparencyAndCompositionMask(FFX_MIN16_I2 iPxPos) +FfxFloat32 LoadTransparencyAndCompositionMask(FfxUInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_TRANSPARENCY_AND_COMPOSITION_MASK) || defined(FFX_INTERNAL) return r_transparency_and_composition_mask[iPxPos]; @@ -518,12 +470,22 @@ FFX_MIN16_F LoadTransparencyAndCompositionMask(FFX_MIN16_I2 iPxPos) #endif } +FfxFloat32 SampleTransparencyAndCompositionMask(FfxFloat32x2 fUV) +{ +#if defined(FSR2_BIND_SRV_TRANSPARENCY_AND_COMPOSITION_MASK) || defined(FFX_INTERNAL) + fUV *= depthclip_uv_scale; + return r_transparency_and_composition_mask.SampleLevel(s_LinearClamp, fUV, 0); +#else + return 0.f; +#endif +} + FfxFloat32 PreExposure() { return fPreExposure; } -FfxFloat32x3 LoadInputColor(FFX_MIN16_I2 iPxPos) +FfxFloat32x3 LoadInputColor(FfxUInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_INPUT_COLOR) || defined(FFX_INTERNAL) return r_input_color_jittered[iPxPos].rgb / PreExposure(); @@ -532,7 +494,7 @@ FfxFloat32x3 LoadInputColor(FFX_MIN16_I2 iPxPos) #endif } -FfxFloat32x3 LoadInputColorWithoutPreExposure(FFX_MIN16_I2 iPxPos) +FfxFloat32x3 LoadInputColorWithoutPreExposure(FfxUInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_INPUT_COLOR) || defined(FFX_INTERNAL) return r_input_color_jittered[iPxPos].rgb; @@ -541,18 +503,7 @@ FfxFloat32x3 LoadInputColorWithoutPreExposure(FFX_MIN16_I2 iPxPos) #endif } -#if FFX_HALF -FfxFloat16x3 LoadPreparedInputColor(FfxInt16x2 iPxPos) -{ -#if defined(FSR2_BIND_SRV_PREPARED_INPUT_COLOR) || defined(FFX_INTERNAL) - return r_prepared_input_color[iPxPos].rgb; -#else - return 0.f; -#endif -} -#endif - -FFX_MIN16_F3 LoadPreparedInputColor(int2 iPxPos) +FfxFloat32x3 LoadPreparedInputColor(FfxUInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_PREPARED_INPUT_COLOR) || defined(FFX_INTERNAL) return r_prepared_input_color[iPxPos].rgb; @@ -561,7 +512,7 @@ FFX_MIN16_F3 LoadPreparedInputColor(int2 iPxPos) #endif } -FFX_MIN16_F LoadPreparedInputColorLuma(FFX_MIN16_I2 iPxPos) +FfxFloat32 LoadPreparedInputColorLuma(FfxUInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_PREPARED_INPUT_COLOR) || defined(FFX_INTERNAL) return r_prepared_input_color[iPxPos].a; @@ -570,7 +521,7 @@ FFX_MIN16_F LoadPreparedInputColorLuma(FFX_MIN16_I2 iPxPos) #endif } -FfxFloat32x2 LoadInputMotionVector(FFX_MIN16_I2 iPxDilatedMotionVectorPos) +FfxFloat32x2 LoadInputMotionVector(FfxUInt32x2 iPxDilatedMotionVectorPos) { #if defined(FSR2_BIND_SRV_MOTION_VECTORS) || defined(FFX_INTERNAL) FfxFloat32x2 fSrcMotionVector = r_motion_vectors[iPxDilatedMotionVectorPos].xy; @@ -587,7 +538,7 @@ FfxFloat32x2 LoadInputMotionVector(FFX_MIN16_I2 iPxDilatedMotionVectorPos) return fUvMotionVector; } -FFX_MIN16_F4 LoadHistory(int2 iPxHistory) +FfxFloat32x4 LoadHistory(FfxUInt32x2 iPxHistory) { #if defined(FSR2_BIND_SRV_INTERNAL_UPSCALED) || defined(FFX_INTERNAL) return r_internal_upscaled_color[iPxHistory]; @@ -596,7 +547,7 @@ FFX_MIN16_F4 LoadHistory(int2 iPxHistory) #endif } -FfxFloat32x4 LoadRwInternalUpscaledColorAndWeight(FFX_MIN16_I2 iPxPos) +FfxFloat32x4 LoadRwInternalUpscaledColorAndWeight(FfxUInt32x2 iPxPos) { #if defined(FSR2_BIND_UAV_INTERNAL_UPSCALED) || defined(FFX_INTERNAL) return rw_internal_upscaled_color[iPxPos]; @@ -605,14 +556,14 @@ FfxFloat32x4 LoadRwInternalUpscaledColorAndWeight(FFX_MIN16_I2 iPxPos) #endif } -void StoreLumaHistory(FFX_MIN16_I2 iPxPos, FfxFloat32x4 fLumaHistory) +void StoreLumaHistory(FfxUInt32x2 iPxPos, FfxFloat32x4 fLumaHistory) { #if defined(FSR2_BIND_UAV_LUMA_HISTORY) || defined(FFX_INTERNAL) rw_luma_history[iPxPos] = fLumaHistory; #endif } -FfxFloat32x4 LoadRwLumaHistory(FFX_MIN16_I2 iPxPos) +FfxFloat32x4 LoadRwLumaHistory(FfxUInt32x2 iPxPos) { #if defined(FSR2_BIND_UAV_LUMA_HISTORY) || defined(FFX_INTERNAL) return rw_luma_history[iPxPos]; @@ -621,7 +572,7 @@ FfxFloat32x4 LoadRwLumaHistory(FFX_MIN16_I2 iPxPos) #endif } -FfxFloat32 LoadLumaStabilityFactor(FFX_MIN16_I2 iPxPos) +FfxFloat32 LoadLumaStabilityFactor(FfxUInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_LUMA_HISTORY) || defined(FFX_INTERNAL) return r_luma_history[iPxPos].w; @@ -640,21 +591,21 @@ FfxFloat32 SampleLumaStabilityFactor(FfxFloat32x2 fUV) #endif } -void StoreReprojectedHistory(FFX_MIN16_I2 iPxHistory, FFX_MIN16_F4 fHistory) +void StoreReprojectedHistory(FfxUInt32x2 iPxHistory, FfxFloat32x4 fHistory) { #if defined(FSR2_BIND_UAV_INTERNAL_UPSCALED) || defined(FFX_INTERNAL) rw_internal_upscaled_color[iPxHistory] = fHistory; #endif } -void StoreInternalColorAndWeight(FFX_MIN16_I2 iPxPos, FfxFloat32x4 fColorAndWeight) +void StoreInternalColorAndWeight(FfxUInt32x2 iPxPos, FfxFloat32x4 fColorAndWeight) { #if defined(FSR2_BIND_UAV_INTERNAL_UPSCALED) || defined(FFX_INTERNAL) rw_internal_upscaled_color[iPxPos] = fColorAndWeight; #endif } -void StoreUpscaledOutput(FFX_MIN16_I2 iPxPos, FfxFloat32x3 fColor) +void StoreUpscaledOutput(FfxUInt32x2 iPxPos, FfxFloat32x3 fColor) { #if defined(FSR2_BIND_UAV_UPSCALED_OUTPUT) || defined(FFX_INTERNAL) rw_upscaled_output[iPxPos] = FfxFloat32x4(fColor * PreExposure(), 1.f); @@ -663,10 +614,10 @@ void StoreUpscaledOutput(FFX_MIN16_I2 iPxPos, FfxFloat32x3 fColor) //LOCK_LIFETIME_REMAINING == 0 //Should make LockInitialLifetime() return a const 1.0f later -LOCK_STATUS_T LoadLockStatus(FFX_MIN16_I2 iPxPos) +FfxFloat32x3 LoadLockStatus(FfxUInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_LOCK_STATUS) || defined(FFX_INTERNAL) - LOCK_STATUS_T fLockStatus = r_lock_status[iPxPos]; + FfxFloat32x3 fLockStatus = r_lock_status[iPxPos]; fLockStatus[0] -= LockInitialLifetime() * 2.0f; return fLockStatus; @@ -677,10 +628,10 @@ LOCK_STATUS_T LoadLockStatus(FFX_MIN16_I2 iPxPos) } -LOCK_STATUS_T LoadRwLockStatus(int2 iPxPos) +FfxFloat32x3 LoadRwLockStatus(FfxUInt32x2 iPxPos) { #if defined(FSR2_BIND_UAV_LOCK_STATUS) || defined(FFX_INTERNAL) - LOCK_STATUS_T fLockStatus = rw_lock_status[iPxPos]; + FfxFloat32x3 fLockStatus = rw_lock_status[iPxPos]; fLockStatus[0] -= LockInitialLifetime() * 2.0f; @@ -690,7 +641,7 @@ LOCK_STATUS_T LoadRwLockStatus(int2 iPxPos) #endif } -void StoreLockStatus(FFX_MIN16_I2 iPxPos, LOCK_STATUS_T fLockstatus) +void StoreLockStatus(FfxUInt32x2 iPxPos, FfxFloat32x3 fLockstatus) { #if defined(FSR2_BIND_UAV_LOCK_STATUS) || defined(FFX_INTERNAL) fLockstatus[0] += LockInitialLifetime() * 2.0f; @@ -699,19 +650,19 @@ void StoreLockStatus(FFX_MIN16_I2 iPxPos, LOCK_STATUS_T fLockstatus) #endif } -void StorePreparedInputColor(FFX_PARAMETER_IN FFX_MIN16_I2 iPxPos, FFX_PARAMETER_IN PREPARED_INPUT_COLOR_T fTonemapped) +void StorePreparedInputColor(FFX_PARAMETER_IN FfxUInt32x2 iPxPos, FFX_PARAMETER_IN FfxFloat32x4 fTonemapped) { #if defined(FSR2_BIND_UAV_PREPARED_INPUT_COLOR) || defined(FFX_INTERNAL) rw_prepared_input_color[iPxPos] = fTonemapped; #endif } -FfxBoolean IsResponsivePixel(FFX_MIN16_I2 iPxPos) +FfxBoolean IsResponsivePixel(FfxUInt32x2 iPxPos) { return FFX_FALSE; //not supported in prototype } -FfxFloat32 LoadDepthClip(FFX_MIN16_I2 iPxPos) +FfxFloat32 LoadDepthClip(FfxUInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_DEPTH_CLIP) || defined(FFX_INTERNAL) return r_depth_clip[iPxPos]; @@ -730,11 +681,11 @@ FfxFloat32 SampleDepthClip(FfxFloat32x2 fUV) #endif } -LOCK_STATUS_T SampleLockStatus(FfxFloat32x2 fUV) +FfxFloat32x3 SampleLockStatus(FfxFloat32x2 fUV) { #if defined(FSR2_BIND_SRV_LOCK_STATUS) || defined(FFX_INTERNAL) fUV *= postprocessed_lockstatus_uv_scale; - LOCK_STATUS_T fLockStatus = r_lock_status.SampleLevel(s_LinearClamp, fUV, 0); + FfxFloat32x3 fLockStatus = r_lock_status.SampleLevel(s_LinearClamp, fUV, 0); fLockStatus[0] -= LockInitialLifetime() * 2.0f; return fLockStatus; #else @@ -742,7 +693,7 @@ LOCK_STATUS_T SampleLockStatus(FfxFloat32x2 fUV) #endif } -void StoreDepthClip(FFX_MIN16_I2 iPxPos, FfxFloat32 fClip) +void StoreDepthClip(FfxUInt32x2 iPxPos, FfxFloat32 fClip) { #if defined(FSR2_BIND_UAV_DEPTH_CLIP) || defined(FFX_INTERNAL) rw_depth_clip[iPxPos] = fClip; @@ -754,7 +705,7 @@ FfxFloat32 TanHalfFoV() return fTanHalfFOV; } -FfxFloat32 LoadSceneDepth(FFX_MIN16_I2 iPxInput) +FfxFloat32 LoadSceneDepth(FfxUInt32x2 iPxInput) { #if defined(FSR2_BIND_SRV_DEPTH) || defined(FFX_INTERNAL) return r_depth[iPxInput]; @@ -763,50 +714,49 @@ FfxFloat32 LoadSceneDepth(FFX_MIN16_I2 iPxInput) #endif } -FfxFloat32 LoadReconstructedPrevDepth(FFX_MIN16_I2 iPxPos) +FfxFloat32 LoadReconstructedPrevDepth(FfxUInt32x2 iPxPos) { #if defined(FSR2_BIND_SRV_RECONSTRUCTED_PREV_NEAREST_DEPTH) || defined(FFX_INTERNAL) - return asfloat(r_ReconstructedPrevNearestDepth[iPxPos]); + return asfloat(r_reconstructed_previous_nearest_depth[iPxPos]); #else return 0; #endif } -void StoreReconstructedDepth(FFX_MIN16_I2 iPxSample, FfxFloat32 fDepth) +void StoreReconstructedDepth(FfxUInt32x2 iPxSample, FfxFloat32 fDepth) { FfxUInt32 uDepth = asuint(fDepth); #if defined(FSR2_BIND_UAV_RECONSTRUCTED_PREV_NEAREST_DEPTH) || defined(FFX_INTERNAL) #if FFX_FSR2_OPTION_INVERTED_DEPTH - InterlockedMax(rw_ReconstructedPrevNearestDepth[iPxSample], uDepth); + InterlockedMax(rw_reconstructed_previous_nearest_depth[iPxSample], uDepth); #else - InterlockedMin(rw_ReconstructedPrevNearestDepth[iPxSample], uDepth); // min for standard, max for inverted depth + InterlockedMin(rw_reconstructed_previous_nearest_depth[iPxSample], uDepth); // min for standard, max for inverted depth #endif #endif } -void SetReconstructedDepth(FFX_MIN16_I2 iPxSample, const FfxUInt32 uValue) +void SetReconstructedDepth(FfxUInt32x2 iPxSample, const FfxUInt32 uValue) { #if defined(FSR2_BIND_UAV_RECONSTRUCTED_PREV_NEAREST_DEPTH) || defined(FFX_INTERNAL) - rw_ReconstructedPrevNearestDepth[iPxSample] = uValue; + rw_reconstructed_previous_nearest_depth[iPxSample] = uValue; #endif } -void StoreDilatedDepth(FFX_PARAMETER_IN FFX_MIN16_I2 iPxPos, FFX_PARAMETER_IN FfxFloat32 fDepth) +void StoreDilatedDepth(FFX_PARAMETER_IN FfxUInt32x2 iPxPos, FFX_PARAMETER_IN FfxFloat32 fDepth) { #if defined(FSR2_BIND_UAV_DILATED_DEPTH) || defined(FFX_INTERNAL) - //FfxUInt32 uDepth = f32tof16(fDepth); rw_dilatedDepth[iPxPos] = fDepth; #endif } -void StoreDilatedMotionVector(FFX_PARAMETER_IN FFX_MIN16_I2 iPxPos, FFX_PARAMETER_IN FfxFloat32x2 fMotionVector) +void StoreDilatedMotionVector(FFX_PARAMETER_IN FfxUInt32x2 iPxPos, FFX_PARAMETER_IN FfxFloat32x2 fMotionVector) { #if defined(FSR2_BIND_UAV_DILATED_MOTION_VECTORS) || defined(FFX_INTERNAL) rw_dilated_motion_vectors[iPxPos] = fMotionVector; #endif } -FfxFloat32x2 LoadDilatedMotionVector(FFX_MIN16_I2 iPxInput) +FfxFloat32x2 LoadDilatedMotionVector(FfxUInt32x2 iPxInput) { #if defined(FSR2_BIND_SRV_DILATED_MOTION_VECTORS) || defined(FFX_INTERNAL) return r_dilated_motion_vectors[iPxInput].xy; @@ -825,7 +775,7 @@ FfxFloat32x2 SampleDilatedMotionVector(FfxFloat32x2 fUV) #endif } -FfxFloat32 LoadDilatedDepth(FFX_MIN16_I2 iPxInput) +FfxFloat32 LoadDilatedDepth(FfxUInt32x2 iPxInput) { #if defined(FSR2_BIND_SRV_DILATED_DEPTH) || defined(FFX_INTERNAL) return r_dilatedDepth[iPxInput]; @@ -838,7 +788,7 @@ FfxFloat32 Exposure() { // return 1.0f; #if defined(FSR2_BIND_SRV_EXPOSURE) || defined(FFX_INTERNAL) - FfxFloat32 exposure = r_exposure[FFX_MIN16_I2(0, 0)].x; + FfxFloat32 exposure = r_exposure[FfxUInt32x2(0, 0)].x; #else FfxFloat32 exposure = 1.f; #endif @@ -859,40 +809,39 @@ FfxFloat32 SampleLanczos2Weight(FfxFloat32 x) #endif } -#if FFX_HALF -FfxFloat16 SampleLanczos2Weight(FfxFloat16 x) -{ -#if defined(FSR2_BIND_SRV_LANCZOS_LUT) || defined(FFX_INTERNAL) - return r_lanczos_lut.SampleLevel(s_LinearClamp, FfxFloat16x2(x / 2, 0.5f), 0); -#else - return 0.f; -#endif -} -#endif - -FFX_MIN16_F SampleUpsampleMaximumBias(FFX_MIN16_F2 uv) +FfxFloat32 SampleUpsampleMaximumBias(FfxFloat32x2 uv) { #if defined(FSR2_BIND_SRV_UPSCALE_MAXIMUM_BIAS_LUT) || defined(FFX_INTERNAL) // Stored as a SNORM, so make sure to multiply by 2 to retrieve the actual expected range. - return FFX_MIN16_F(2.0) * r_upsample_maximum_bias_lut.SampleLevel(s_LinearClamp, abs(uv) * 2.0, 0); + return FfxFloat32(2.0) * r_upsample_maximum_bias_lut.SampleLevel(s_LinearClamp, abs(uv) * 2.0, 0); #else return 0.f; #endif } -FFX_MIN16_F LoadReactiveMax(FFX_PARAMETER_IN FFX_MIN16_I2 iPxPos) +FfxFloat32x2 SampleDilatedReactiveMasks(FfxFloat32x2 fUV) { -#if defined(FSR2_BIND_SRV_REACTIVE_MAX) || defined(FFX_INTERNAL) - return r_reactive_max[iPxPos]; +#if defined(FSR2_BIND_SRV_DILATED_REACTIVE_MASKS) || defined(FFX_INTERNAL) + fUV *= depthclip_uv_scale; + return r_dilated_reactive_masks.SampleLevel(s_LinearClamp, fUV, 0); +#else + return 0.f; +#endif +} + +FfxFloat32x2 LoadDilatedReactiveMasks(FFX_PARAMETER_IN FfxUInt32x2 iPxPos) +{ +#if defined(FSR2_BIND_SRV_DILATED_REACTIVE_MASKS) || defined(FFX_INTERNAL) + return r_dilated_reactive_masks[iPxPos]; #else return 0.f; #endif } -void StoreReactiveMax(FFX_PARAMETER_IN FFX_MIN16_I2 iPxPos, FFX_PARAMETER_IN FFX_MIN16_F fReactiveMax) +void StoreDilatedReactiveMasks(FFX_PARAMETER_IN FfxUInt32x2 iPxPos, FFX_PARAMETER_IN FfxFloat32x2 fDilatedReactiveMasks) { -#if defined(FSR2_BIND_UAV_REACTIVE_MASK_MAX) || defined(FFX_INTERNAL) - rw_reactive_max[iPxPos] = fReactiveMax; +#if defined(FSR2_BIND_UAV_DILATED_REACTIVE_MASKS) || defined(FFX_INTERNAL) + rw_dilated_reactive_masks[iPxPos] = fDilatedReactiveMasks; #endif } diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_common.h b/src/ffx-fsr2-api/shaders/ffx_fsr2_common.h index 7be6631..7f6acf2 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_common.h +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_common.h @@ -58,135 +58,160 @@ struct LockState FfxBoolean WasLockedPrevFrame; //Set to identify if the pixel was already locked (relock) }; -FFX_MIN16_F GetNormalizedRemainingLockLifetime(FFX_MIN16_F3 fLockStatus) +FfxFloat32 GetNormalizedRemainingLockLifetime(FfxFloat32x3 fLockStatus) { const FfxFloat32 fTrust = fLockStatus[LOCK_TRUST]; - return FFX_MIN16_F(((ffxSaturate(fLockStatus[LOCK_LIFETIME_REMAINING] - LockInitialLifetime()) / LockInitialLifetime())) * fTrust); + return ffxSaturate(fLockStatus[LOCK_LIFETIME_REMAINING] - LockInitialLifetime()) / LockInitialLifetime() * fTrust; } -LOCK_STATUS_T CreateNewLockSample() +#if FFX_HALF +FFX_MIN16_F GetNormalizedRemainingLockLifetime(FFX_MIN16_F3 fLockStatus) { - LOCK_STATUS_T fLockStatus = LOCK_STATUS_T(0, 0, 0); + const FFX_MIN16_F fTrust = fLockStatus[LOCK_TRUST]; + const FFX_MIN16_F fInitialLockLifetime = FFX_MIN16_F(LockInitialLifetime()); - fLockStatus[LOCK_TRUST] = LOCK_STATUS_F1(1); + return ffxSaturate(fLockStatus[LOCK_LIFETIME_REMAINING] - fInitialLockLifetime) / fInitialLockLifetime * fTrust; +} +#endif - return fLockStatus; +void InitializeNewLockSample(FFX_PARAMETER_OUT FfxFloat32x3 fLockStatus) +{ + fLockStatus = FfxFloat32x3(0, 0, 1); // LOCK_TRUST to 1 } +#if FFX_HALF +void InitializeNewLockSample(FFX_PARAMETER_OUT FFX_MIN16_F3 fLockStatus) +{ + fLockStatus = FFX_MIN16_F3(0, 0, 1); // LOCK_TRUST to 1 +} +#endif + + +void KillLock(FFX_PARAMETER_INOUT FfxFloat32x3 fLockStatus) +{ + fLockStatus[LOCK_LIFETIME_REMAINING] = 0; +} + +#if FFX_HALF void KillLock(FFX_PARAMETER_INOUT FFX_MIN16_F3 fLockStatus) { fLockStatus[LOCK_LIFETIME_REMAINING] = FFX_MIN16_F(0); } - -#define SPLIT_LEFT 0 -#define SPLIT_RIGHT 1 -#ifndef SPLIT_SHADER -#define SPLIT_SHADER SPLIT_RIGHT #endif -#if FFX_HALF - -#define UPSAMPLE_F FfxFloat16 -#define UPSAMPLE_F2 FfxFloat16x2 -#define UPSAMPLE_F3 FfxFloat16x3 -#define UPSAMPLE_F4 FfxFloat16x4 -#define UPSAMPLE_I FfxInt16 -#define UPSAMPLE_I2 FfxInt16x2 -#define UPSAMPLE_I3 FfxInt16x3 -#define UPSAMPLE_I4 FfxInt16x4 -#define UPSAMPLE_U FfxUInt16 -#define UPSAMPLE_U2 FfxUInt16x2 -#define UPSAMPLE_U3 FfxUInt16x3 -#define UPSAMPLE_U4 FfxUInt16x4 -#define UPSAMPLE_F2_BROADCAST(X) FFX_BROADCAST_MIN_FLOAT16X2(X) -#define UPSAMPLE_F3_BROADCAST(X) FFX_BROADCAST_MIN_FLOAT16X3(X) -#define UPSAMPLE_F4_BROADCAST(X) FFX_BROADCAST_MIN_FLOAT16X4(X) -#define UPSAMPLE_I2_BROADCAST(X) FFX_BROADCAST_MIN_INT16X2(X) -#define UPSAMPLE_I3_BROADCAST(X) FFX_BROADCAST_MIN_INT16X3(X) -#define UPSAMPLE_I4_BROADCAST(X) FFX_BROADCAST_MIN_INT16X4(X) -#define UPSAMPLE_U2_BROADCAST(X) FFX_BROADCAST_MIN_UINT16X2(X) -#define UPSAMPLE_U3_BROADCAST(X) FFX_BROADCAST_MIN_UINT16X3(X) -#define UPSAMPLE_U4_BROADCAST(X) FFX_BROADCAST_MIN_UINT16X4(X) - -#else //FFX_HALF - -#define UPSAMPLE_F FfxFloat32 -#define UPSAMPLE_F2 FfxFloat32x2 -#define UPSAMPLE_F3 FfxFloat32x3 -#define UPSAMPLE_F4 FfxFloat32x4 -#define UPSAMPLE_I FfxInt32 -#define UPSAMPLE_I2 FfxInt32x2 -#define UPSAMPLE_I3 FfxInt32x3 -#define UPSAMPLE_I4 FfxInt32x4 -#define UPSAMPLE_U FfxUInt32 -#define UPSAMPLE_U2 FfxUInt32x2 -#define UPSAMPLE_U3 FfxUInt32x3 -#define UPSAMPLE_U4 FfxUInt32x4 -#define UPSAMPLE_F2_BROADCAST(X) FFX_BROADCAST_FLOAT32X2(X) -#define UPSAMPLE_F3_BROADCAST(X) FFX_BROADCAST_FLOAT32X3(X) -#define UPSAMPLE_F4_BROADCAST(X) FFX_BROADCAST_FLOAT32X4(X) -#define UPSAMPLE_I2_BROADCAST(X) FFX_BROADCAST_INT32X2(X) -#define UPSAMPLE_I3_BROADCAST(X) FFX_BROADCAST_INT32X3(X) -#define UPSAMPLE_I4_BROADCAST(X) FFX_BROADCAST_INT32X4(X) -#define UPSAMPLE_U2_BROADCAST(X) FFX_BROADCAST_UINT32X2(X) -#define UPSAMPLE_U3_BROADCAST(X) FFX_BROADCAST_UINT32X3(X) -#define UPSAMPLE_U4_BROADCAST(X) FFX_BROADCAST_UINT32X4(X) - -#endif //FFX_HALF - struct RectificationBoxData { - UPSAMPLE_F3 boxCenter; - UPSAMPLE_F3 boxVec; - UPSAMPLE_F3 aabbMin; - UPSAMPLE_F3 aabbMax; + FfxFloat32x3 boxCenter; + FfxFloat32x3 boxVec; + FfxFloat32x3 aabbMin; + FfxFloat32x3 aabbMax; }; +#if FFX_HALF +struct RectificationBoxDataMin16 +{ + FFX_MIN16_F3 boxCenter; + FFX_MIN16_F3 boxVec; + FFX_MIN16_F3 aabbMin; + FFX_MIN16_F3 aabbMax; +}; +#endif struct RectificationBox { RectificationBoxData data_; - UPSAMPLE_F fBoxCenterWeight; + FfxFloat32 fBoxCenterWeight; }; - -void RectificationBoxReset(FFX_PARAMETER_INOUT RectificationBox rectificationBox, const UPSAMPLE_F3 initialColorSample) +#if FFX_HALF +struct RectificationBoxMin16 { - rectificationBox.fBoxCenterWeight = UPSAMPLE_F(0.0); + RectificationBoxDataMin16 data_; + FFX_MIN16_F fBoxCenterWeight; +}; +#endif - rectificationBox.data_.boxCenter = UPSAMPLE_F3_BROADCAST(0); - rectificationBox.data_.boxVec = UPSAMPLE_F3_BROADCAST(0); +void RectificationBoxReset(FFX_PARAMETER_INOUT RectificationBox rectificationBox, const FfxFloat32x3 initialColorSample) +{ + rectificationBox.fBoxCenterWeight = FfxFloat32(0); + + rectificationBox.data_.boxCenter = FfxFloat32x3(0, 0, 0); + rectificationBox.data_.boxVec = FfxFloat32x3(0, 0, 0); rectificationBox.data_.aabbMin = initialColorSample; rectificationBox.data_.aabbMax = initialColorSample; } +#if FFX_HALF +void RectificationBoxReset(FFX_PARAMETER_INOUT RectificationBoxMin16 rectificationBox, const FFX_MIN16_F3 initialColorSample) +{ + rectificationBox.fBoxCenterWeight = FFX_MIN16_F(0); -void RectificationBoxAddSample(FFX_PARAMETER_INOUT RectificationBox rectificationBox, const UPSAMPLE_F3 colorSample, const UPSAMPLE_F fSampleWeight) + rectificationBox.data_.boxCenter = FFX_MIN16_F3(0, 0, 0); + rectificationBox.data_.boxVec = FFX_MIN16_F3(0, 0, 0); + rectificationBox.data_.aabbMin = initialColorSample; + rectificationBox.data_.aabbMax = initialColorSample; +} +#endif + +void RectificationBoxAddSample(FFX_PARAMETER_INOUT RectificationBox rectificationBox, const FfxFloat32x3 colorSample, const FfxFloat32 fSampleWeight) { rectificationBox.data_.aabbMin = ffxMin(rectificationBox.data_.aabbMin, colorSample); rectificationBox.data_.aabbMax = ffxMax(rectificationBox.data_.aabbMax, colorSample); - UPSAMPLE_F3 weightedSample = colorSample * fSampleWeight; + FfxFloat32x3 weightedSample = colorSample * fSampleWeight; rectificationBox.data_.boxCenter += weightedSample; rectificationBox.data_.boxVec += colorSample * weightedSample; rectificationBox.fBoxCenterWeight += fSampleWeight; } +#if FFX_HALF +void RectificationBoxAddSample(FFX_PARAMETER_INOUT RectificationBoxMin16 rectificationBox, const FFX_MIN16_F3 colorSample, const FFX_MIN16_F fSampleWeight) +{ + rectificationBox.data_.aabbMin = ffxMin(rectificationBox.data_.aabbMin, colorSample); + rectificationBox.data_.aabbMax = ffxMax(rectificationBox.data_.aabbMax, colorSample); + FFX_MIN16_F3 weightedSample = colorSample * fSampleWeight; + rectificationBox.data_.boxCenter += weightedSample; + rectificationBox.data_.boxVec += colorSample * weightedSample; + rectificationBox.fBoxCenterWeight += fSampleWeight; +} +#endif void RectificationBoxComputeVarianceBoxData(FFX_PARAMETER_INOUT RectificationBox rectificationBox) { - rectificationBox.fBoxCenterWeight = (abs(rectificationBox.fBoxCenterWeight) > UPSAMPLE_F(FSR2_EPSILON) ? rectificationBox.fBoxCenterWeight : UPSAMPLE_F(1.f)); + rectificationBox.fBoxCenterWeight = (abs(rectificationBox.fBoxCenterWeight) > FfxFloat32(FSR2_EPSILON) ? rectificationBox.fBoxCenterWeight : FfxFloat32(1.f)); rectificationBox.data_.boxCenter /= rectificationBox.fBoxCenterWeight; rectificationBox.data_.boxVec /= rectificationBox.fBoxCenterWeight; - UPSAMPLE_F3 stdDev = sqrt(abs(rectificationBox.data_.boxVec - rectificationBox.data_.boxCenter * rectificationBox.data_.boxCenter)); + FfxFloat32x3 stdDev = sqrt(abs(rectificationBox.data_.boxVec - rectificationBox.data_.boxCenter * rectificationBox.data_.boxCenter)); rectificationBox.data_.boxVec = stdDev; } +#if FFX_HALF +void RectificationBoxComputeVarianceBoxData(FFX_PARAMETER_INOUT RectificationBoxMin16 rectificationBox) +{ + rectificationBox.fBoxCenterWeight = (abs(rectificationBox.fBoxCenterWeight) > FFX_MIN16_F(FSR2_EPSILON) ? rectificationBox.fBoxCenterWeight : FFX_MIN16_F(1.f)); + rectificationBox.data_.boxCenter /= rectificationBox.fBoxCenterWeight; + rectificationBox.data_.boxVec /= rectificationBox.fBoxCenterWeight; + FFX_MIN16_F3 stdDev = sqrt(abs(rectificationBox.data_.boxVec - rectificationBox.data_.boxCenter * rectificationBox.data_.boxCenter)); + rectificationBox.data_.boxVec = stdDev; +} +#endif RectificationBoxData RectificationBoxGetData(FFX_PARAMETER_INOUT RectificationBox rectificationBox) { return rectificationBox.data_; } +#if FFX_HALF +RectificationBoxDataMin16 RectificationBoxGetData(FFX_PARAMETER_INOUT RectificationBoxMin16 rectificationBox) +{ + return rectificationBox.data_; +} +#endif FfxFloat32x3 SafeRcp3(FfxFloat32x3 v) { - return (all(FFX_NOT_EQUAL(v, FFX_BROADCAST_FLOAT32X3(0)))) ? (FFX_BROADCAST_FLOAT32X3(1.0f) / v) : FFX_BROADCAST_FLOAT32X3(0.0f); + return (all(FFX_NOT_EQUAL(v, FfxFloat32x3(0, 0, 0)))) ? (FfxFloat32x3(1, 1, 1) / v) : FfxFloat32x3(0, 0, 0); } +#if FFX_HALF +FFX_MIN16_F3 SafeRcp3(FFX_MIN16_F3 v) +{ + return (all(FFX_NOT_EQUAL(v, FFX_MIN16_F3(0, 0, 0)))) ? (FFX_MIN16_F3(1, 1, 1) / v) : FFX_MIN16_F3(0, 0, 0); +} +#endif FfxFloat32 MinDividedByMax(const FfxFloat32 v0, const FfxFloat32 v1) { @@ -202,49 +227,31 @@ FFX_MIN16_F MinDividedByMax(const FFX_MIN16_F v0, const FFX_MIN16_F v1) } #endif -FfxFloat32 MaxDividedByMin(const FfxFloat32 v0, const FfxFloat32 v1) -{ - const FfxFloat32 m = ffxMin(v0, v1); - return m != 0 ? ffxMax(v0, v1) / m : 0; -} - -FFX_MIN16_F3 RGBToYCoCg_16(FFX_MIN16_F3 fRgb) -{ - FFX_MIN16_F3 fYCoCg; - fYCoCg.x = dot(fRgb.rgb, FFX_MIN16_F3(+0.25f, +0.50f, +0.25f)); - fYCoCg.y = dot(fRgb.rgb, FFX_MIN16_F3(+0.50f, +0.00f, -0.50f)); - fYCoCg.z = dot(fRgb.rgb, FFX_MIN16_F3(-0.25f, +0.50f, -0.25f)); - return fYCoCg; -} - -FFX_MIN16_F3 RGBToYCoCg_V2_16(FFX_MIN16_F3 fRgb) -{ - FFX_MIN16_F a = fRgb.g * FFX_MIN16_F(0.5f); - FFX_MIN16_F b = (fRgb.r + fRgb.b) * FFX_MIN16_F(0.25f); - FFX_MIN16_F3 fYCoCg; - fYCoCg.x = a + b; - fYCoCg.y = (fRgb.r - fRgb.b) * FFX_MIN16_F(0.5f); - fYCoCg.z = a - b; - return fYCoCg; -} - FfxFloat32x3 YCoCgToRGB(FfxFloat32x3 fYCoCg) { FfxFloat32x3 fRgb; - FfxFloat32 tmp = fYCoCg.x - fYCoCg.z / 2.0; - fRgb.g = fYCoCg.z + tmp; - fRgb.b = tmp - fYCoCg.y / 2.0; - fRgb.r = fRgb.b + fYCoCg.y; + + fYCoCg.yz -= FfxFloat32x2(0.5f, 0.5f); // [0,1] -> [-0.5,0.5] + + fRgb = FfxFloat32x3( + fYCoCg.x + fYCoCg.y - fYCoCg.z, + fYCoCg.x + fYCoCg.z, + fYCoCg.x - fYCoCg.y - fYCoCg.z); + return fRgb; } #if FFX_HALF FFX_MIN16_F3 YCoCgToRGB(FFX_MIN16_F3 fYCoCg) { FFX_MIN16_F3 fRgb; - FFX_MIN16_F tmp = fYCoCg.x - fYCoCg.z * FFX_MIN16_F(0.5f); - fRgb.g = fYCoCg.z + tmp; - fRgb.b = tmp - fYCoCg.y * FFX_MIN16_F(0.5f); - fRgb.r = fRgb.b + fYCoCg.y; + + fYCoCg.yz -= FFX_MIN16_F2(0.5f, 0.5f); // [0,1] -> [-0.5,0.5] + + fRgb = FFX_MIN16_F3( + fYCoCg.x + fYCoCg.y - fYCoCg.z, + fYCoCg.x + fYCoCg.z, + fYCoCg.x - fYCoCg.y - fYCoCg.z); + return fRgb; } #endif @@ -252,39 +259,42 @@ FFX_MIN16_F3 YCoCgToRGB(FFX_MIN16_F3 fYCoCg) FfxFloat32x3 RGBToYCoCg(FfxFloat32x3 fRgb) { FfxFloat32x3 fYCoCg; - fYCoCg.y = fRgb.r - fRgb.b; - FfxFloat32 tmp = fRgb.b + fYCoCg.y / 2.0; - fYCoCg.z = fRgb.g - tmp; - fYCoCg.x = tmp + fYCoCg.z / 2.0; + + fYCoCg = FfxFloat32x3( + 0.25f * fRgb.r + 0.5f * fRgb.g + 0.25f * fRgb.b, + 0.5f * fRgb.r - 0.5f * fRgb.b, + -0.25f * fRgb.r + 0.5f * fRgb.g - 0.25f * fRgb.b); + + fYCoCg.yz += FfxFloat32x2(0.5f, 0.5f); // [-0.5,0.5] -> [0,1] + return fYCoCg; } #if FFX_HALF FFX_MIN16_F3 RGBToYCoCg(FFX_MIN16_F3 fRgb) { FFX_MIN16_F3 fYCoCg; - fYCoCg.y = fRgb.r - fRgb.b; - FFX_MIN16_F tmp = fRgb.b + fYCoCg.y * FFX_MIN16_F(0.5f); - fYCoCg.z = fRgb.g - tmp; - fYCoCg.x = tmp + fYCoCg.z * FFX_MIN16_F(0.5f); + + fYCoCg = FFX_MIN16_F3( + 0.25 * fRgb.r + 0.5 * fRgb.g + 0.25 * fRgb.b, + 0.5 * fRgb.r - 0.5 * fRgb.b, + -0.25 * fRgb.r + 0.5 * fRgb.g - 0.25 * fRgb.b); + + fYCoCg.yz += FFX_MIN16_F2(0.5, 0.5); // [-0.5,0.5] -> [0,1] + return fYCoCg; } #endif -FfxFloat32x3 RGBToYCoCg_V2(FfxFloat32x3 fRgb) -{ - FfxFloat32 a = fRgb.g * 0.5f; - FfxFloat32 b = (fRgb.r + fRgb.b) * 0.25f; - FfxFloat32x3 fYCoCg; - fYCoCg.x = a + b; - fYCoCg.y = (fRgb.r - fRgb.b) * 0.5f; - fYCoCg.z = a - b; - return fYCoCg; -} - FfxFloat32 RGBToLuma(FfxFloat32x3 fLinearRgb) { return dot(fLinearRgb, FfxFloat32x3(0.2126f, 0.7152f, 0.0722f)); } +#if FFX_HALF +FFX_MIN16_F RGBToLuma(FFX_MIN16_F3 fLinearRgb) +{ + return dot(fLinearRgb, FFX_MIN16_F3(0.2126f, 0.7152f, 0.0722f)); +} +#endif FfxFloat32 RGBToPerceivedLuma(FfxFloat32x3 fLinearRgb) { @@ -299,6 +309,22 @@ FfxFloat32 RGBToPerceivedLuma(FfxFloat32x3 fLinearRgb) return fPercievedLuminance * 0.01f; } +#if FFX_HALF +FFX_MIN16_F RGBToPerceivedLuma(FFX_MIN16_F3 fLinearRgb) +{ + FFX_MIN16_F fLuminance = RGBToLuma(fLinearRgb); + + FFX_MIN16_F fPercievedLuminance = FFX_MIN16_F(0); + if (fLuminance <= FFX_MIN16_F(216.0f / 24389.0f)) { + fPercievedLuminance = fLuminance * FFX_MIN16_F(24389.0f / 27.0f); + } + else { + fPercievedLuminance = ffxPow(fLuminance, FFX_MIN16_F(1.0f / 3.0f)) * FFX_MIN16_F(116.0f) - FFX_MIN16_F(16.0f); + } + + return fPercievedLuminance * FFX_MIN16_F(0.01f); +} +#endif FfxFloat32x3 Tonemap(FfxFloat32x3 fRgb) @@ -321,22 +347,29 @@ FFX_MIN16_F3 InverseTonemap(FFX_MIN16_F3 fRgb) { return fRgb / ffxMax(FFX_MIN16_F(FSR2_TONEMAP_EPSILON), FFX_MIN16_F(1.f) - ffxMax(fRgb.r, ffxMax(fRgb.g, fRgb.b))).xxx; } - -FFX_MIN16_I2 ClampLoad(FFX_MIN16_I2 iPxSample, FFX_MIN16_I2 iPxOffset, FFX_MIN16_I2 iTextureSize) -{ - return clamp(iPxSample + iPxOffset, FFX_MIN16_I2(0, 0), iTextureSize - FFX_MIN16_I2(1, 1)); -} #endif FfxInt32x2 ClampLoad(FfxInt32x2 iPxSample, FfxInt32x2 iPxOffset, FfxInt32x2 iTextureSize) { return clamp(iPxSample + iPxOffset, FfxInt32x2(0, 0), iTextureSize - FfxInt32x2(1, 1)); } +#if FFX_HALF +FFX_MIN16_I2 ClampLoad(FFX_MIN16_I2 iPxSample, FFX_MIN16_I2 iPxOffset, FFX_MIN16_I2 iTextureSize) +{ + return clamp(iPxSample + iPxOffset, FFX_MIN16_I2(0, 0), iTextureSize - FFX_MIN16_I2(1, 1)); +} +#endif +FfxBoolean IsOnScreen(FfxInt32x2 pos, FfxInt32x2 size) +{ + return all(FFX_GREATER_THAN_EQUAL(pos, FfxInt32x2(0, 0))) && all(FFX_LESS_THAN(pos, size)); +} +#if FFX_HALF FfxBoolean IsOnScreen(FFX_MIN16_I2 pos, FFX_MIN16_I2 size) { - return all(FFX_GREATER_THAN_EQUAL(pos, FFX_BROADCAST_MIN_FLOAT16X2(0))) && all(FFX_LESS_THAN(pos, size)); + return all(FFX_GREATER_THAN_EQUAL(pos, FFX_MIN16_I2(0, 0))) && all(FFX_LESS_THAN(pos, size)); } +#endif FfxFloat32 ComputeAutoExposureFromLavg(FfxFloat32 Lavg) { @@ -351,6 +384,39 @@ FfxFloat32 ComputeAutoExposureFromLavg(FfxFloat32 Lavg) return 1 / Lmax; } +#if FFX_HALF +FFX_MIN16_F ComputeAutoExposureFromLavg(FFX_MIN16_F Lavg) +{ + Lavg = exp(Lavg); + + const FFX_MIN16_F S = FFX_MIN16_F(100.0f); //ISO arithmetic speed + const FFX_MIN16_F K = FFX_MIN16_F(12.5f); + const FFX_MIN16_F ExposureISO100 = log2((Lavg * S) / K); + + const FFX_MIN16_F q = FFX_MIN16_F(0.65f); + const FFX_MIN16_F Lmax = (FFX_MIN16_F(78.0f) / (q * S)) * ffxPow(FFX_MIN16_F(2.0f), ExposureISO100); + + return FFX_MIN16_F(1) / Lmax; +} +#endif + +FfxInt32x2 ComputeHrPosFromLrPos(FfxInt32x2 iPxLrPos) +{ + FfxFloat32x2 fSrcJitteredPos = FfxFloat32x2(iPxLrPos) + 0.5f - Jitter(); + FfxFloat32x2 fLrPosInHr = (fSrcJitteredPos / RenderSize()) * DisplaySize(); + FfxFloat32x2 fHrPos = floor(fLrPosInHr) + 0.5f; + return FfxInt32x2(fHrPos); +} +#if FFX_HALF +FFX_MIN16_I2 ComputeHrPosFromLrPos(FFX_MIN16_I2 iPxLrPos) +{ + FFX_MIN16_F2 fSrcJitteredPos = FFX_MIN16_F2(iPxLrPos) + FFX_MIN16_F(0.5f) - FFX_MIN16_F2(Jitter()); + FFX_MIN16_F2 fLrPosInHr = (fSrcJitteredPos / FFX_MIN16_F2(RenderSize())) * FFX_MIN16_F2(DisplaySize()); + FFX_MIN16_F2 fHrPos = floor(fLrPosInHr) + FFX_MIN16_F(0.5); + return FFX_MIN16_I2(fHrPos); +} +#endif + #endif // #if defined(FFX_GPU) -#endif //!defined(FFX_FSR2_COMMON_H) \ No newline at end of file +#endif //!defined(FFX_FSR2_COMMON_H) diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_compute_luminance_pyramid_pass.glsl b/src/ffx-fsr2-api/shaders/ffx_fsr2_compute_luminance_pyramid_pass.glsl index 49fe0a8..9a6a329 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_compute_luminance_pyramid_pass.glsl +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_compute_luminance_pyramid_pass.glsl @@ -168,4 +168,4 @@ FFX_FSR2_NUM_THREADS void main() { ComputeAutoExposure(gl_WorkGroupID.xyz, gl_LocalInvocationIndex); -} +} \ No newline at end of file diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_compute_luminance_pyramid_pass.hlsl b/src/ffx-fsr2-api/shaders/ffx_fsr2_compute_luminance_pyramid_pass.hlsl index 8400af2..07a097a 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_compute_luminance_pyramid_pass.hlsl +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_compute_luminance_pyramid_pass.hlsl @@ -97,7 +97,7 @@ void SPD_SetExposureBuffer(float2 value) #endif } -float4 SPD_LoadMipmap5(min16int2 iPxPos) +float4 SPD_LoadMipmap5(int2 iPxPos) { #if defined(FSR2_BIND_UAV_EXPOSURE_MIP_5) || defined(FFX_INTERNAL) return float4(rw_img_mip_5[iPxPos], 0, 0, 0); @@ -106,7 +106,7 @@ float4 SPD_LoadMipmap5(min16int2 iPxPos) #endif } -void SPD_SetMipmap(min16int2 iPxPos, int slice, float value) +void SPD_SetMipmap(int2 iPxPos, int slice, float value) { switch (slice) { diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_depth_clip.h b/src/ffx-fsr2-api/shaders/ffx_fsr2_depth_clip.h index b44cc59..81db737 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_depth_clip.h +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_depth_clip.h @@ -19,9 +19,12 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +#ifndef FFX_FSR2_DEPTH_CLIP_H +#define FFX_FSR2_DEPTH_CLIP_H + FFX_STATIC const FfxFloat32 DepthClipBaseScale = 4.0f; -FfxFloat32 ComputeSampleDepthClip(FFX_MIN16_I2 iPxSamplePos, FfxFloat32 fPreviousDepth, FfxFloat32 fPreviousDepthBilinearWeight, FfxFloat32 fCurrentDepthViewSpace) +FfxFloat32 ComputeSampleDepthClip(FfxInt32x2 iPxSamplePos, FfxFloat32 fPreviousDepth, FfxFloat32 fPreviousDepthBilinearWeight, FfxFloat32 fCurrentDepthViewSpace) { FfxFloat32 fPrevNearestDepthViewSpace = abs(ConvertFromDeviceDepthToViewSpace(fPreviousDepth)); @@ -42,13 +45,13 @@ FfxFloat32 ComputeSampleDepthClip(FFX_MIN16_I2 iPxSamplePos, FfxFloat32 fPreviou rw_debug_out[iPxSamplePos] = FfxFloat32x4(fCurrentDepthViewSpace, fPrevNearestDepthViewSpace, fDepthDiff, fDepthClipFactor); #endif - return fPreviousDepthBilinearWeight * fDepthClipFactor * DepthClipBaseScale; + return fPreviousDepthBilinearWeight * fDepthClipFactor * ffxLerp(1.0f, DepthClipBaseScale, ffxSaturate(fDepthDiff * fDepthDiff)); } FfxFloat32 ComputeDepthClip(FfxFloat32x2 fUvSample, FfxFloat32 fCurrentDepthViewSpace) { FfxFloat32x2 fPxSample = fUvSample * RenderSize() - 0.5f; - FFX_MIN16_I2 iPxSample = FFX_MIN16_I2(floor(fPxSample)); + FfxInt32x2 iPxSample = FfxInt32x2(floor(fPxSample)); FfxFloat32x2 fPxFrac = ffxFract(fPxSample); const FfxFloat32 fBilinearWeights[2][2] = { @@ -66,8 +69,8 @@ FfxFloat32 ComputeDepthClip(FfxFloat32x2 fUvSample, FfxFloat32 fCurrentDepthView FfxFloat32 fWeightSum = 0.0f; for (FfxInt32 y = 0; y <= 1; ++y) { for (FfxInt32 x = 0; x <= 1; ++x) { - FFX_MIN16_I2 iSamplePos = iPxSample + FFX_MIN16_I2(x, y); - if (IsOnScreen(iSamplePos, FFX_MIN16_I2(RenderSize()))) { + FfxInt32x2 iSamplePos = iPxSample + FfxInt32x2(x, y); + if (IsOnScreen(iSamplePos, RenderSize())) { FfxFloat32 fBilinearWeight = fBilinearWeights[y][x]; if (fBilinearWeight > reconstructedDepthBilinearWeightThreshold) { fDepth += ComputeSampleDepthClip(iSamplePos, LoadReconstructedPrevDepth(iSamplePos), fBilinearWeight, fCurrentDepthViewSpace); @@ -80,9 +83,9 @@ FfxFloat32 ComputeDepthClip(FfxFloat32x2 fUvSample, FfxFloat32 fCurrentDepthView return (fWeightSum > 0) ? fDepth / fWeightSum : DepthClipBaseScale; } -void DepthClip(FFX_MIN16_I2 iPxPos) +void DepthClip(FfxInt32x2 iPxPos) { - FfxFloat32x2 fDepthUv = (FfxFloat32x2(iPxPos) + 0.5f) / RenderSize(); + FfxFloat32x2 fDepthUv = (iPxPos + 0.5f) / RenderSize(); FfxFloat32x2 fMotionVector = LoadDilatedMotionVector(iPxPos); FfxFloat32x2 fDilatedUv = fDepthUv + fMotionVector; FfxFloat32 fCurrentDepthViewSpace = abs(ConvertFromDeviceDepthToViewSpace(LoadDilatedDepth(iPxPos))); @@ -91,3 +94,5 @@ void DepthClip(FFX_MIN16_I2 iPxPos) StoreDepthClip(iPxPos, fDepthClip); } + +#endif //!defined( FFX_FSR2_DEPTH_CLIPH ) \ No newline at end of file diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_depth_clip_pass.glsl b/src/ffx-fsr2-api/shaders/ffx_fsr2_depth_clip_pass.glsl index 96218b8..7233ec6 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_depth_clip_pass.glsl +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_depth_clip_pass.glsl @@ -20,7 +20,7 @@ // THE SOFTWARE. // FSR2 pass 3 -// SRV 7 : FSR2_ReconstructedPrevNearestDepth : r_ReconstructedPrevNearestDepth +// SRV 7 : FSR2_ReconstructedPrevNearestDepth : r_reconstructed_previous_nearest_depth // SRV 8 : FSR2_DilatedVelocity : r_dilated_motion_vectors // SRV 9 : FSR2_DilatedDepth : r_dilatedDepth // UAV 12 : FSR2_DepthClip : rw_depth_clip @@ -58,5 +58,5 @@ FFX_FSR2_NUM_THREADS void main() { - DepthClip(FFX_MIN16_I2(gl_GlobalInvocationID.xy)); + DepthClip(ivec2(gl_GlobalInvocationID.xy)); } diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_depth_clip_pass.hlsl b/src/ffx-fsr2-api/shaders/ffx_fsr2_depth_clip_pass.hlsl index d95d2a7..8433734 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_depth_clip_pass.hlsl +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_depth_clip_pass.hlsl @@ -20,7 +20,7 @@ // THE SOFTWARE. // FSR2 pass 3 -// SRV 7 : FSR2_ReconstructedPrevNearestDepth : r_ReconstructedPrevNearestDepth +// SRV 7 : FSR2_ReconstructedPrevNearestDepth : r_reconstructed_previous_nearest_depth // SRV 8 : FSR2_DilatedVelocity : r_dilated_motion_vectors // SRV 9 : FSR2_DilatedDepth : r_dilatedDepth // UAV 12 : FSR2_DepthClip : rw_depth_clip @@ -54,9 +54,9 @@ FFX_FSR2_PREFER_WAVE64 FFX_FSR2_NUM_THREADS FFX_FSR2_EMBED_ROOTSIG_CONTENT void CS( - min16int2 iGroupId : SV_GroupID, - min16int2 iDispatchThreadId : SV_DispatchThreadID, - min16int2 iGroupThreadId : SV_GroupThreadID, + int2 iGroupId : SV_GroupID, + int2 iDispatchThreadId : SV_DispatchThreadID, + int2 iGroupThreadId : SV_GroupThreadID, int iGroupIndex : SV_GroupIndex) { DepthClip(iDispatchThreadId); diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_lock.h b/src/ffx-fsr2-api/shaders/ffx_fsr2_lock.h index 24323df..b2266b7 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_lock.h +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_lock.h @@ -19,13 +19,16 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -FfxFloat32 GetLuma(FFX_MIN16_I2 pos) +#ifndef FFX_FSR2_LOCK_H +#define FFX_FSR2_LOCK_H + +FfxFloat32 GetLuma(FfxInt32x2 pos) { //add some bias to avoid locking dark areas return FfxFloat32(LoadPreparedInputColorLuma(pos)); } -FfxFloat32 ComputeThinFeatureConfidence(FFX_MIN16_I2 pos) +FfxFloat32 ComputeThinFeatureConfidence(FfxInt32x2 pos) { const FfxInt32 RADIUS = 1; @@ -59,7 +62,7 @@ FfxFloat32 ComputeThinFeatureConfidence(FFX_MIN16_I2 pos) for (FfxInt32 x = -RADIUS; x <= RADIUS; x++, idx++) { if (x == 0 && y == 0) continue; - FFX_MIN16_I2 samplePos = ClampLoad(pos, FFX_MIN16_I2(x, y), FFX_MIN16_I2(RenderSize())); + FfxInt32x2 samplePos = ClampLoad(pos, FfxInt32x2(x, y), FfxInt32x2(RenderSize())); FfxFloat32 sampleLuma = GetLuma(samplePos); FfxFloat32 difference = ffxMax(sampleLuma, fNucleus) / ffxMin(sampleLuma, fNucleus); @@ -93,7 +96,7 @@ FfxFloat32 ComputeThinFeatureConfidence(FFX_MIN16_I2 pos) FFX_STATIC FfxBoolean s_bLockUpdated = FFX_FALSE; -LOCK_STATUS_T ComputeLockStatus(FFX_MIN16_I2 iPxLrPos, LOCK_STATUS_T fLockStatus) +FfxFloat32x3 ComputeLockStatus(FfxInt32x2 iPxLrPos, FfxFloat32x3 fLockStatus) { FfxFloat32 fConfidenceOfThinFeature = ComputeThinFeatureConfidence(iPxLrPos); @@ -101,7 +104,7 @@ LOCK_STATUS_T ComputeLockStatus(FFX_MIN16_I2 iPxLrPos, LOCK_STATUS_T fLockStatus if (fConfidenceOfThinFeature > 0.0f) { //put to negative on new lock - fLockStatus[LOCK_LIFETIME_REMAINING] = (fLockStatus[LOCK_LIFETIME_REMAINING] == LOCK_STATUS_F1(0.0f)) ? LOCK_STATUS_F1(-LockInitialLifetime()) : LOCK_STATUS_F1(-(LockInitialLifetime() * 2)); + fLockStatus[LOCK_LIFETIME_REMAINING] = (fLockStatus[LOCK_LIFETIME_REMAINING] == FfxFloat32(0.0f)) ? FfxFloat32(-LockInitialLifetime()) : FfxFloat32(-(LockInitialLifetime() * 2)); s_bLockUpdated = FFX_TRUE; } @@ -109,63 +112,15 @@ LOCK_STATUS_T ComputeLockStatus(FFX_MIN16_I2 iPxLrPos, LOCK_STATUS_T fLockStatus return fLockStatus; } -void ComputeLock(FFX_MIN16_I2 iPxLrPos) +void ComputeLock(FfxInt32x2 iPxLrPos) { - FfxFloat32x2 fSrcJitteredPos = FfxFloat32x2(iPxLrPos) + 0.5f - Jitter(); - FfxFloat32x2 fLrPosInHr = (fSrcJitteredPos / RenderSize()) * DisplaySize(); - FfxFloat32x2 fHrPos = floor(fLrPosInHr) + 0.5; - FFX_MIN16_I2 iPxHrPos = FFX_MIN16_I2(fHrPos); + FfxInt32x2 iPxHrPos = ComputeHrPosFromLrPos(iPxLrPos); - LOCK_STATUS_T fLockStatus = ComputeLockStatus(iPxLrPos, LoadLockStatus(iPxHrPos)); + FfxFloat32x3 fLockStatus = ComputeLockStatus(iPxLrPos, LoadLockStatus(iPxHrPos)); if ((s_bLockUpdated)) { StoreLockStatus(iPxHrPos, fLockStatus); } } -FFX_GROUPSHARED FfxFloat32 gs_ReactiveMask[(8 + 4) * (8 + 4)]; - -void StoreReactiveMaskToLDS(FfxUInt32x2 coord, FfxFloat32x2 value) -{ - FfxUInt32 baseIdx = coord.y * 12 + coord.x; - gs_ReactiveMask[baseIdx] = value.x; - gs_ReactiveMask[baseIdx + 1] = value.y; -} - -FfxFloat32 LoadReactiveMaskFromLDS(FfxUInt32x2 coord) -{ - return gs_ReactiveMask[coord.y * 12 + coord.x]; -} - -void PreProcessReactiveMask(FFX_MIN16_I2 iPxLrPos, FfxUInt32x2 groupId, FfxUInt32x2 groupThreadId) -{ -#if OPT_PRECOMPUTE_REACTIVE_MAX && !OPT_USE_EVAL_ACCUMULATION_REACTIVENESS - - if (all(FFX_LESS_THAN(groupThreadId, FFX_BROADCAST_UINT32X2(6)))) { - - FfxInt32x2 iPos = FfxInt32x2(groupId << 3) + FfxInt32x2(groupThreadId << 1) - 1; - FfxFloat32x4 fReactiveMask2x2 = GatherReactiveMask(iPos).wzxy; - - StoreReactiveMaskToLDS(groupThreadId << 1, fReactiveMask2x2.xy); - StoreReactiveMaskToLDS((groupThreadId << 1) + FfxInt32x2(0, 1), fReactiveMask2x2.zw); - } - - FFX_GROUP_MEMORY_BARRIER(); - - FfxFloat32 fReactiveMax = 0.0f; - - for (FfxUInt32 row = 0; row < 4; row++) { - for (FfxUInt32 col = 0; col < 4; col++) { - const FfxUInt32x2 localOffset = groupThreadId + FfxUInt32x2(col, row); - const FfxBoolean bOutOfRenderBounds = any(FFX_GREATER_THAN_EQUAL((FfxInt32x2(groupId << 3) + FfxInt32x2(localOffset)), RenderSize())); - fReactiveMax = bOutOfRenderBounds ? fReactiveMax : ffxMax(fReactiveMax, LoadReactiveMaskFromLDS(localOffset)); - } - } - - // Threshold reactive value - fReactiveMax = fReactiveMax > 0.8f ? fReactiveMax : 0.0f; - - StoreReactiveMax(iPxLrPos, FFX_MIN16_F(fReactiveMax)); - -#endif //OPT_PRECOMPUTE_REACTIVE_MAX && !OPT_USE_EVAL_ACCUMULATION_REACTIVENESS -} +#endif // FFX_FSR2_LOCK_H diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_lock_pass.glsl b/src/ffx-fsr2-api/shaders/ffx_fsr2_lock_pass.glsl index 1405f48..9c37774 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_lock_pass.glsl +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_lock_pass.glsl @@ -33,12 +33,10 @@ #extension GL_GOOGLE_include_directive : require #extension GL_EXT_samplerless_texture_functions : require -#define FSR2_BIND_SRV_REACTIVE_MASK 0 -#define FSR2_BIND_SRV_LOCK_STATUS 1 -#define FSR2_BIND_SRV_PREPARED_INPUT_COLOR 2 -#define FSR2_BIND_UAV_LOCK_STATUS 3 -#define FSR2_BIND_UAV_REACTIVE_MASK_MAX 4 -#define FSR2_BIND_CB_FSR2 5 +#define FSR2_BIND_SRV_LOCK_STATUS 0 +#define FSR2_BIND_SRV_PREPARED_INPUT_COLOR 1 +#define FSR2_BIND_UAV_LOCK_STATUS 2 +#define FSR2_BIND_CB_FSR2 3 #include "ffx_fsr2_callbacks_glsl.h" #include "ffx_fsr2_common.h" @@ -61,9 +59,7 @@ FFX_FSR2_NUM_THREADS void main() { - uvec2 uDispatchThreadId = gl_WorkGroupID.xy * uvec2(FFX_FSR2_THREAD_GROUP_WIDTH, FFX_FSR2_THREAD_GROUP_HEIGHT) + gl_LocalInvocationID.xy; + uvec2 uDispatchThreadId = gl_WorkGroupID.xy * uvec2(FFX_FSR2_THREAD_GROUP_WIDTH, FFX_FSR2_THREAD_GROUP_HEIGHT) + gl_LocalInvocationID.xy; - ComputeLock(FFX_MIN16_I2(uDispatchThreadId)); - - PreProcessReactiveMask(FFX_MIN16_I2(uDispatchThreadId), gl_WorkGroupID.xy, gl_LocalInvocationID.xy); + ComputeLock(ivec2(uDispatchThreadId)); } diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_lock_pass.hlsl b/src/ffx-fsr2-api/shaders/ffx_fsr2_lock_pass.hlsl index 2f9c20e..492965c 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_lock_pass.hlsl +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_lock_pass.hlsl @@ -24,15 +24,11 @@ // SRV 11 : FSR2_LockStatus2 : r_lock_status // SRV 13 : FSR2_PreparedInputColor : r_prepared_input_color // UAV 11 : FSR2_LockStatus1 : rw_lock_status -// UAV 27 : FSR2_ReactiveMaskMax : rw_reactive_max // CB 0 : cbFSR2 -// CB 1 : FSR2DispatchOffsets -#define FSR2_BIND_SRV_REACTIVE_MASK 0 #define FSR2_BIND_SRV_LOCK_STATUS 1 #define FSR2_BIND_SRV_PREPARED_INPUT_COLOR 2 #define FSR2_BIND_UAV_LOCK_STATUS 0 -#define FSR2_BIND_UAV_REACTIVE_MASK_MAX 1 #define FSR2_BIND_CB_FSR2 0 #include "ffx_fsr2_callbacks_hlsl.h" @@ -61,6 +57,4 @@ void CS(uint2 uGroupId : SV_GroupID, uint2 uGroupThreadId : SV_GroupThreadID) uint2 uDispatchThreadId = uGroupId * uint2(FFX_FSR2_THREAD_GROUP_WIDTH, FFX_FSR2_THREAD_GROUP_HEIGHT) + uGroupThreadId; ComputeLock(uDispatchThreadId); - - PreProcessReactiveMask(uDispatchThreadId, uGroupId, uGroupThreadId); } diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_postprocess_lock_status.h b/src/ffx-fsr2-api/shaders/ffx_fsr2_postprocess_lock_status.h index 06c5495..959031b 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_postprocess_lock_status.h +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_postprocess_lock_status.h @@ -22,57 +22,70 @@ #ifndef FFX_FSR2_POSTPROCESS_LOCK_STATUS_H #define FFX_FSR2_POSTPROCESS_LOCK_STATUS_H +FfxFloat32x4 WrapShadingChangeLuma(FfxInt32x2 iPxSample) +{ + return FfxFloat32x4(LoadMipLuma(iPxSample, LumaMipLevelToUse()), 0, 0, 0); +} + +#if FFX_HALF FFX_MIN16_F4 WrapShadingChangeLuma(FFX_MIN16_I2 iPxSample) { return FFX_MIN16_F4(LoadMipLuma(iPxSample, LumaMipLevelToUse()), 0, 0, 0); } +#endif +#if FFX_FSR2_OPTION_POSTPROCESSLOCKSTATUS_SAMPLERS_USE_DATA_HALF && FFX_HALF +DeclareCustomFetchBilinearSamplesMin16(FetchShadingChangeLumaSamples, WrapShadingChangeLuma) +#else DeclareCustomFetchBilinearSamples(FetchShadingChangeLumaSamples, WrapShadingChangeLuma) +#endif DeclareCustomTextureSample(ShadingChangeLumaSample, Bilinear, FetchShadingChangeLumaSamples) -FFX_MIN16_F GetShadingChangeLuma(FfxFloat32x2 fUvCoord) +FfxFloat32 GetShadingChangeLuma(FfxFloat32x2 fUvCoord) { // const FfxFloat32 fShadingChangeLuma = exp(ShadingChangeLumaSample(fUvCoord, LumaMipDimensions()) * LumaMipRcp()); - const FFX_MIN16_F fShadingChangeLuma = FFX_MIN16_F(exp(SampleMipLuma(fUvCoord, LumaMipLevelToUse()) * FFX_MIN16_F(LumaMipRcp()))); + const FfxFloat32 fShadingChangeLuma = FfxFloat32(exp(SampleMipLuma(fUvCoord, LumaMipLevelToUse()) * FfxFloat32(LumaMipRcp()))); return fShadingChangeLuma; } -LockState GetLockState(LOCK_STATUS_T fLockStatus) +LockState GetLockState(FfxFloat32x3 fLockStatus) { LockState state = { FFX_FALSE, FFX_FALSE }; //Check if this is a new or refreshed lock - state.NewLock = fLockStatus[LOCK_LIFETIME_REMAINING] < LOCK_STATUS_F1(0.0f); + state.NewLock = fLockStatus[LOCK_LIFETIME_REMAINING] < FfxFloat32(0.0f); //For a non-refreshed lock, the lifetime is set to LockInitialLifetime() - state.WasLockedPrevFrame = fLockStatus[LOCK_TRUST] != LOCK_STATUS_F1(0.0f); + state.WasLockedPrevFrame = fLockStatus[LOCK_TRUST] != FfxFloat32(0.0f); return state; } -LockState PostProcessLockStatus(FFX_MIN16_I2 iPxHrPos, FFX_PARAMETER_IN FfxFloat32x2 fLrUvJittered, FFX_PARAMETER_IN FFX_MIN16_F fDepthClipFactor, FFX_PARAMETER_IN FfxFloat32 fHrVelocity, - FFX_PARAMETER_INOUT FfxFloat32 fAccumulationTotalWeight, FFX_PARAMETER_INOUT LOCK_STATUS_T fLockStatus, FFX_PARAMETER_OUT FFX_MIN16_F fLuminanceDiff) { +LockState PostProcessLockStatus(FfxInt32x2 iPxHrPos, FFX_PARAMETER_IN FfxFloat32x2 fLrUvJittered, FFX_PARAMETER_IN FfxFloat32 fDepthClipFactor, const FfxFloat32 fAccumulationMask, FFX_PARAMETER_IN FfxFloat32 fHrVelocity, + FFX_PARAMETER_INOUT FfxFloat32 fAccumulationTotalWeight, FFX_PARAMETER_INOUT FfxFloat32x3 fLockStatus, FFX_PARAMETER_OUT FfxFloat32 fLuminanceDiff) { const LockState state = GetLockState(fLockStatus); fLockStatus[LOCK_LIFETIME_REMAINING] = abs(fLockStatus[LOCK_LIFETIME_REMAINING]); - FFX_MIN16_F fShadingChangeLuma = GetShadingChangeLuma(fLrUvJittered); + FfxFloat32 fShadingChangeLuma = GetShadingChangeLuma(fLrUvJittered); //init temporal shading change factor, init to -1 or so in reproject to know if "true new"? - fLockStatus[LOCK_TEMPORAL_LUMA] = (fLockStatus[LOCK_TEMPORAL_LUMA] == LOCK_STATUS_F1(0.0f)) ? fShadingChangeLuma : fLockStatus[LOCK_TEMPORAL_LUMA]; + fLockStatus[LOCK_TEMPORAL_LUMA] = (fLockStatus[LOCK_TEMPORAL_LUMA] == FfxFloat32(0.0f)) ? fShadingChangeLuma : fLockStatus[LOCK_TEMPORAL_LUMA]; - FFX_MIN16_F fPreviousShadingChangeLuma = fLockStatus[LOCK_TEMPORAL_LUMA]; - fLockStatus[LOCK_TEMPORAL_LUMA] = ffxLerp(fLockStatus[LOCK_TEMPORAL_LUMA], LOCK_STATUS_F1(fShadingChangeLuma), LOCK_STATUS_F1(0.5f)); - fLuminanceDiff = FFX_MIN16_F(1) - MinDividedByMax(fPreviousShadingChangeLuma, fShadingChangeLuma); + FfxFloat32 fPreviousShadingChangeLuma = fLockStatus[LOCK_TEMPORAL_LUMA]; + fLockStatus[LOCK_TEMPORAL_LUMA] = ffxLerp(fLockStatus[LOCK_TEMPORAL_LUMA], FfxFloat32(fShadingChangeLuma), FfxFloat32(0.5f)); + fLuminanceDiff = FfxFloat32(1) - MinDividedByMax(fPreviousShadingChangeLuma, fShadingChangeLuma); - if (fLuminanceDiff > FFX_MIN16_F(0.2f)) { + if (fLuminanceDiff > FfxFloat32(0.2f)) { KillLock(fLockStatus); } - if (!state.NewLock && fLockStatus[LOCK_LIFETIME_REMAINING] >= LOCK_STATUS_F1(0)) + if (!state.NewLock && fLockStatus[LOCK_LIFETIME_REMAINING] >= FfxFloat32(0)) { - const FFX_MIN16_F depthClipThreshold = FFX_MIN16_F(0.99f); + fLockStatus[LOCK_LIFETIME_REMAINING] *= (1.0f - fAccumulationMask); + + const FfxFloat32 depthClipThreshold = FfxFloat32(0.99f); if (fDepthClipFactor < depthClipThreshold) { KillLock(fLockStatus); diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_prepare_input_color.h b/src/ffx-fsr2-api/shaders/ffx_fsr2_prepare_input_color.h index b9772b6..a773cda 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_prepare_input_color.h +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_prepare_input_color.h @@ -19,10 +19,13 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +#ifndef FFX_FSR2_PREPARE_INPUT_COLOR_H +#define FFX_FSR2_PREPARE_INPUT_COLOR_H + //TODO: Move to common location & share with Accumulate -void ClearResourcesForNextFrame(in FFX_MIN16_I2 iPxHrPos) +void ClearResourcesForNextFrame(in FfxInt32x2 iPxHrPos) { - if (all(FFX_LESS_THAN(iPxHrPos, FFX_MIN16_I2(RenderSize())))) + if (all(FFX_LESS_THAN(iPxHrPos, FfxInt32x2(RenderSize())))) { #if FFX_FSR2_OPTION_INVERTED_DEPTH const FfxUInt32 farZ = 0x0; @@ -33,7 +36,7 @@ void ClearResourcesForNextFrame(in FFX_MIN16_I2 iPxHrPos) } } -void ComputeLumaStabilityFactor(FFX_MIN16_I2 iPxLrPos, FfxFloat32 fCurrentFrameLuma) +void ComputeLumaStabilityFactor(FfxInt32x2 iPxLrPos, FfxFloat32 fCurrentFrameLuma) { FfxFloat32x4 fCurrentFrameLumaHistory = LoadRwLumaHistory(iPxLrPos); @@ -54,12 +57,12 @@ void ComputeLumaStabilityFactor(FFX_MIN16_I2 iPxLrPos, FfxFloat32 fCurrentFrameL StoreLumaHistory(iPxLrPos, fCurrentFrameLumaHistory); } -void PrepareInputColor(FFX_MIN16_I2 iPxLrPos) +void PrepareInputColor(FfxInt32x2 iPxLrPos) { //We assume linear data. if non-linear input (sRGB, ...), //then we should convert to linear first and back to sRGB on output. - FfxFloat32x3 fRgb = ffxMax(FFX_BROADCAST_FLOAT32X3(0), LoadInputColor(iPxLrPos)); + FfxFloat32x3 fRgb = ffxMax(FfxFloat32x3(0, 0, 0), LoadInputColor(iPxLrPos)); fRgb *= Exposure(); @@ -68,16 +71,18 @@ void PrepareInputColor(FFX_MIN16_I2 iPxLrPos) fRgb = Tonemap(fRgb); #endif - PREPARED_INPUT_COLOR_T fYCoCg; + FfxFloat32x4 fYCoCg; - fYCoCg.xyz = PREPARED_INPUT_COLOR_F3(RGBToYCoCg(fRgb)); + fYCoCg.xyz = RGBToYCoCg(fRgb); const FfxFloat32 fPerceivedLuma = RGBToPerceivedLuma(fRgb); ComputeLumaStabilityFactor(iPxLrPos, fPerceivedLuma); //compute luma used to lock pixels, if used elsewhere the ffxPow must be moved! - fYCoCg.w = PREPARED_INPUT_COLOR_F1(ffxPow(fPerceivedLuma, 1.0f / 6.0f)); + fYCoCg.w = ffxPow(fPerceivedLuma, FfxFloat32(1.0 / 6.0)); StorePreparedInputColor(iPxLrPos, fYCoCg); ClearResourcesForNextFrame(iPxLrPos); } + +#endif // FFX_FSR2_PREPARE_INPUT_COLOR_H diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_prepare_input_color_pass.glsl b/src/ffx-fsr2-api/shaders/ffx_fsr2_prepare_input_color_pass.glsl index ed10dc1..d37e0af 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_prepare_input_color_pass.glsl +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_prepare_input_color_pass.glsl @@ -18,11 +18,10 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. - // FSR2 pass 1 // SRV 1 : m_HDR : r_input_color_jittered // SRV 4 : FSR2_Exposure : r_exposure -// UAV 7 : FSR2_ReconstructedPrevNearestDepth : rw_ReconstructedPrevNearestDepth +// UAV 7 : FSR2_ReconstructedPrevNearestDepth : rw_reconstructed_previous_nearest_depth // UAV 13 : FSR2_PreparedInputColor : rw_prepared_input_color // UAV 14 : FSR2_LumaHistory : rw_luma_history // CB 0 : cbFSR2 @@ -59,5 +58,5 @@ FFX_FSR2_NUM_THREADS void main() { - PrepareInputColor(FFX_MIN16_I2(gl_GlobalInvocationID.xy)); + PrepareInputColor(ivec2(gl_GlobalInvocationID.xy)); } diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_prepare_input_color_pass.hlsl b/src/ffx-fsr2-api/shaders/ffx_fsr2_prepare_input_color_pass.hlsl index b8d258a..bed086f 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_prepare_input_color_pass.hlsl +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_prepare_input_color_pass.hlsl @@ -22,7 +22,7 @@ // FSR2 pass 1 // SRV 1 : m_HDR : r_input_color_jittered // SRV 4 : FSR2_Exposure : r_exposure -// UAV 7 : FSR2_ReconstructedPrevNearestDepth : rw_ReconstructedPrevNearestDepth +// UAV 7 : FSR2_ReconstructedPrevNearestDepth : rw_reconstructed_previous_nearest_depth // UAV 13 : FSR2_PreparedInputColor : rw_prepared_input_color // UAV 14 : FSR2_LumaHistory : rw_luma_history // CB 0 : cbFSR2 diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_rcas_pass.glsl b/src/ffx-fsr2-api/shaders/ffx_fsr2_rcas_pass.glsl index d437bb9..1097faf 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_rcas_pass.glsl +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_rcas_pass.glsl @@ -89,4 +89,4 @@ FFX_FSR2_NUM_THREADS void main() { RCAS(gl_LocalInvocationID.xyz, gl_WorkGroupID.xyz, gl_GlobalInvocationID.xyz); -} +} \ No newline at end of file diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_reconstruct_dilated_velocity_and_previous_depth.h b/src/ffx-fsr2-api/shaders/ffx_fsr2_reconstruct_dilated_velocity_and_previous_depth.h index 655fa13..aad1992 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_reconstruct_dilated_velocity_and_previous_depth.h +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_reconstruct_dilated_velocity_and_previous_depth.h @@ -22,11 +22,11 @@ #ifndef FFX_FSR2_RECONSTRUCT_DILATED_VELOCITY_AND_PREVIOUS_DEPTH_H #define FFX_FSR2_RECONSTRUCT_DILATED_VELOCITY_AND_PREVIOUS_DEPTH_H -void ReconstructPrevDepth(FFX_MIN16_I2 iPxPos, FfxFloat32 fDepth, FfxFloat32x2 fMotionVector, FFX_MIN16_I2 iPxDepthSize) +void ReconstructPrevDepth(FfxInt32x2 iPxPos, FfxFloat32 fDepth, FfxFloat32x2 fMotionVector, FfxInt32x2 iPxDepthSize) { - FfxFloat32x2 fDepthUv = (FfxFloat32x2(iPxPos) + 0.5f) / iPxDepthSize; - FfxFloat32x2 fPxPrevPos = (fDepthUv + fMotionVector) * FfxFloat32x2(iPxDepthSize)-0.5f; - FFX_MIN16_I2 iPxPrevPos = FFX_MIN16_I2(floor(fPxPrevPos)); + FfxFloat32x2 fDepthUv = (iPxPos + FfxFloat32(0.5)) / iPxDepthSize; + FfxFloat32x2 fPxPrevPos = (fDepthUv + fMotionVector) * iPxDepthSize - FfxFloat32x2(0.5, 0.5); + FfxInt32x2 iPxPrevPos = FfxInt32x2(floor(fPxPrevPos)); FfxFloat32x2 fPxFrac = ffxFract(fPxPrevPos); const FfxFloat32 bilinearWeights[2][2] = { @@ -45,12 +45,12 @@ void ReconstructPrevDepth(FFX_MIN16_I2 iPxPos, FfxFloat32 fDepth, FfxFloat32x2 f for (FfxInt32 y = 0; y <= 1; ++y) { for (FfxInt32 x = 0; x <= 1; ++x) { - FFX_MIN16_I2 offset = FFX_MIN16_I2(x, y); + FfxInt32x2 offset = FfxInt32x2(x, y); FfxFloat32 w = bilinearWeights[y][x]; if (w > reconstructedDepthBilinearWeightThreshold) { - FFX_MIN16_I2 storePos = iPxPrevPos + offset; + FfxInt32x2 storePos = iPxPrevPos + offset; if (IsOnScreen(storePos, iPxDepthSize)) { StoreReconstructedDepth(storePos, fDepth); } @@ -59,19 +59,19 @@ void ReconstructPrevDepth(FFX_MIN16_I2 iPxPos, FfxFloat32 fDepth, FfxFloat32x2 f } } -void FindNearestDepth(FFX_PARAMETER_IN FFX_MIN16_I2 iPxPos, FFX_PARAMETER_IN FFX_MIN16_I2 iPxSize, FFX_PARAMETER_OUT FfxFloat32 fNearestDepth, FFX_PARAMETER_OUT FFX_MIN16_I2 fNearestDepthCoord) +void FindNearestDepth(FFX_PARAMETER_IN FfxInt32x2 iPxPos, FFX_PARAMETER_IN FfxInt32x2 iPxSize, FFX_PARAMETER_OUT FfxFloat32 fNearestDepth, FFX_PARAMETER_OUT FfxInt32x2 fNearestDepthCoord) { const FfxInt32 iSampleCount = 9; - const FFX_MIN16_I2 iSampleOffsets[iSampleCount] = { - FFX_MIN16_I2(+0, +0), - FFX_MIN16_I2(+1, +0), - FFX_MIN16_I2(+0, +1), - FFX_MIN16_I2(+0, -1), - FFX_MIN16_I2(-1, +0), - FFX_MIN16_I2(-1, +1), - FFX_MIN16_I2(+1, +1), - FFX_MIN16_I2(-1, -1), - FFX_MIN16_I2(+1, -1), + const FfxInt32x2 iSampleOffsets[iSampleCount] = { + FfxInt32x2(+0, +0), + FfxInt32x2(+1, +0), + FfxInt32x2(+0, +1), + FfxInt32x2(+0, -1), + FfxInt32x2(-1, +0), + FfxInt32x2(-1, +1), + FfxInt32x2(+1, +1), + FfxInt32x2(-1, -1), + FfxInt32x2(+1, -1), }; // pull out the depth loads to allow SC to batch them @@ -80,7 +80,7 @@ void FindNearestDepth(FFX_PARAMETER_IN FFX_MIN16_I2 iPxPos, FFX_PARAMETER_IN FFX FFX_UNROLL for (iSampleIndex = 0; iSampleIndex < iSampleCount; ++iSampleIndex) { - FFX_MIN16_I2 iPos = iPxPos + iSampleOffsets[iSampleIndex]; + FfxInt32x2 iPos = iPxPos + iSampleOffsets[iSampleIndex]; depth[iSampleIndex] = LoadInputDepth(iPos); } @@ -90,7 +90,7 @@ void FindNearestDepth(FFX_PARAMETER_IN FFX_MIN16_I2 iPxPos, FFX_PARAMETER_IN FFX FFX_UNROLL for (iSampleIndex = 1; iSampleIndex < iSampleCount; ++iSampleIndex) { - FFX_MIN16_I2 iPos = iPxPos + iSampleOffsets[iSampleIndex]; + FfxInt32x2 iPos = iPxPos + iSampleOffsets[iSampleIndex]; if (IsOnScreen(iPos, iPxSize)) { FfxFloat32 fNdDepth = depth[iSampleIndex]; @@ -106,30 +106,97 @@ void FindNearestDepth(FFX_PARAMETER_IN FFX_MIN16_I2 iPxPos, FFX_PARAMETER_IN FFX } } -void ReconstructPrevDepthAndDilateMotionVectors(FFX_MIN16_I2 iPxLrPos) +FfxFloat32 ComputeMotionDivergence(FfxInt32x2 iPxPos, FfxInt32x2 iPxInputMotionVectorSize) { - FFX_MIN16_I2 iPxLrSize = FFX_MIN16_I2(RenderSize()); - FFX_MIN16_I2 iPxHrSize = FFX_MIN16_I2(DisplaySize()); + FfxFloat32 minconvergence = 1.0f; + FfxFloat32x2 fMotionVectorNucleus = LoadInputMotionVector(iPxPos) * RenderSize(); + FfxFloat32 fNucleusVelocity = length(fMotionVectorNucleus); + + const FfxFloat32 MotionVectorVelocityEpsilon = 1e-02f; + + if (fNucleusVelocity > MotionVectorVelocityEpsilon) { + for (FfxInt32 y = -1; y <= 1; ++y) { + for (FfxInt32 x = -1; x <= 1; ++x) { + + FfxInt32x2 sp = ClampLoad(iPxPos, FfxInt32x2(x, y), iPxInputMotionVectorSize); + + FfxFloat32x2 fMotionVector = LoadInputMotionVector(sp) * RenderSize(); + FfxFloat32 fVelocity = length(fMotionVector); + + fVelocity = ffxMax(fVelocity, fNucleusVelocity); + minconvergence = ffxMin(minconvergence, dot(fMotionVector / fVelocity, fMotionVectorNucleus / fVelocity)); + } + } + } + + return ffxSaturate(1.0f - minconvergence); +} + + +void PreProcessReactiveMasks(FfxInt32x2 iPxLrPos, FfxFloat32 fMotionDivergence) +{ + // Compensate for bilinear sampling in accumulation pass + + FfxFloat32x3 fReferenceColor = LoadPreparedInputColor(iPxLrPos); + FfxFloat32x2 fReactiveFactor = FfxFloat32x2(0.0f, fMotionDivergence); + + for (int y = -1; y < 2; y++) { + for (int x = -1; x < 2; x++) { + + const FfxInt32x2 sampleCoord = ClampLoad(iPxLrPos, FfxInt32x2(x, y), FfxInt32x2(RenderSize())); + + FfxFloat32x3 fColorSample = LoadPreparedInputColor(sampleCoord); + FfxFloat32 fReactiveSample = LoadReactiveMask(sampleCoord); + FfxFloat32 fTransparencyAndCompositionSample = LoadTransparencyAndCompositionMask(sampleCoord); + + const FfxFloat32 fColorSimilarity = dot(normalize(fReferenceColor), normalize(fColorSample)); + const FfxFloat32 fVelocitySimilarity = 1.0f - abs(length(fReferenceColor) - length(fColorSample)); + const FfxFloat32 fSimilarity = fColorSimilarity * fVelocitySimilarity; + + // Increase power for non-similar samples + const FfxFloat32 fPowerBiasMax = 6.0f; + const FfxFloat32 fSimilarityPower = 1.0f + (fPowerBiasMax - fSimilarity * fPowerBiasMax); + const FfxFloat32 fWeightedReactiveSample = ffxPow(fReactiveSample, fSimilarityPower); + const FfxFloat32 fWeightedTransparencyAndCompositionSample = ffxPow(fTransparencyAndCompositionSample, fSimilarityPower); + + fReactiveFactor = ffxMax(fReactiveFactor, FfxFloat32x2(fWeightedReactiveSample, fWeightedTransparencyAndCompositionSample)); + } + } + + StoreDilatedReactiveMasks(iPxLrPos, fReactiveFactor); +} + +void ReconstructAndDilate(FfxInt32x2 iPxLrPos) +{ FfxFloat32 fDilatedDepth; - FFX_MIN16_I2 iNearestDepthCoord; + FfxInt32x2 iNearestDepthCoord; - FindNearestDepth(iPxLrPos, iPxLrSize, fDilatedDepth, iNearestDepthCoord); + FindNearestDepth(iPxLrPos, RenderSize(), fDilatedDepth, iNearestDepthCoord); #if FFX_FSR2_OPTION_LOW_RESOLUTION_MOTION_VECTORS - FfxFloat32x2 fDilatedMotionVector = LoadInputMotionVector(iNearestDepthCoord); + FfxInt32x2 iSamplePos = iPxLrPos; + FfxInt32x2 iMotionVectorPos = iNearestDepthCoord; #else - FfxFloat32x2 fSrcJitteredPos = FfxFloat32x2(iNearestDepthCoord) + 0.5f - Jitter(); - FfxFloat32x2 fLrPosInHr = (fSrcJitteredPos / iPxLrSize) * iPxHrSize; - FfxFloat32x2 fHrPos = floor(fLrPosInHr) + 0.5; - - FfxFloat32x2 fDilatedMotionVector = LoadInputMotionVector(FFX_MIN16_I2(fHrPos)); + FfxInt32x2 iSamplePos = ComputeHrPosFromLrPos(iPxLrPos); + FfxInt32x2 iMotionVectorPos = ComputeHrPosFromLrPos(iNearestDepthCoord); #endif + FfxFloat32x2 fDilatedMotionVector = LoadInputMotionVector(iMotionVectorPos); + StoreDilatedDepth(iPxLrPos, fDilatedDepth); StoreDilatedMotionVector(iPxLrPos, fDilatedMotionVector); - ReconstructPrevDepth(iPxLrPos, fDilatedDepth, fDilatedMotionVector, iPxLrSize); + ReconstructPrevDepth(iPxLrPos, fDilatedDepth, fDilatedMotionVector, RenderSize()); + +#if FFX_FSR2_OPTION_LOW_RESOLUTION_MOTION_VECTORS + FfxFloat32 fMotionDivergence = ComputeMotionDivergence(iSamplePos, RenderSize()); +#else + FfxFloat32 fMotionDivergence = ComputeMotionDivergence(iSamplePos, DisplaySize()); +#endif + + PreProcessReactiveMasks(iPxLrPos, fMotionDivergence); } + #endif //!defined( FFX_FSR2_RECONSTRUCT_DILATED_VELOCITY_AND_PREVIOUS_DEPTH_H ) diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_reconstruct_previous_depth_pass.glsl b/src/ffx-fsr2-api/shaders/ffx_fsr2_reconstruct_previous_depth_pass.glsl index 7579c49..96d1383 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_reconstruct_previous_depth_pass.glsl +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_reconstruct_previous_depth_pass.glsl @@ -22,7 +22,7 @@ // FSR2 pass 2 // SRV 2 : m_MotionVector : r_motion_vectors // SRV 3 : m_depthbuffer : r_depth -// UAV 7 : FSR2_ReconstructedPrevNearestDepth : rw_ReconstructedPrevNearestDepth +// UAV 7 : FSR2_ReconstructedPrevNearestDepth : rw_reconstructed_previous_nearest_depth // UAV 8 : FSR2_DilatedVelocity : rw_dilated_motion_vectors // UAV 9 : FSR2_DilatedDepth : rw_dilatedDepth // CB 0 : cbFSR2 @@ -34,10 +34,14 @@ #define FSR2_BIND_SRV_MOTION_VECTORS 0 #define FSR2_BIND_SRV_DEPTH 1 -#define FSR2_BIND_UAV_RECONSTRUCTED_PREV_NEAREST_DEPTH 2 -#define FSR2_BIND_UAV_DILATED_MOTION_VECTORS 3 -#define FSR2_BIND_UAV_DILATED_DEPTH 4 -#define FSR2_BIND_CB_FSR2 5 +#define FSR2_BIND_SRV_REACTIVE_MASK 2 +#define FSR2_BIND_SRV_TRANSPARENCY_AND_COMPOSITION_MASK 3 +#define FSR2_BIND_SRV_PREPARED_INPUT_COLOR 4 +#define FSR2_BIND_UAV_RECONSTRUCTED_PREV_NEAREST_DEPTH 5 +#define FSR2_BIND_UAV_DILATED_MOTION_VECTORS 6 +#define FSR2_BIND_UAV_DILATED_DEPTH 7 +#define FSR2_BIND_UAV_DILATED_REACTIVE_MASKS 8 +#define FSR2_BIND_CB_FSR2 9 #include "ffx_fsr2_callbacks_glsl.h" #include "ffx_fsr2_common.h" @@ -60,5 +64,5 @@ FFX_FSR2_NUM_THREADS void main() { - ReconstructPrevDepthAndDilateMotionVectors(FFX_MIN16_I2(gl_GlobalInvocationID.xy)); + ReconstructAndDilate(FFX_MIN16_I2(gl_GlobalInvocationID.xy)); } diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_reconstruct_previous_depth_pass.hlsl b/src/ffx-fsr2-api/shaders/ffx_fsr2_reconstruct_previous_depth_pass.hlsl index 21825cb..57f3f49 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_reconstruct_previous_depth_pass.hlsl +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_reconstruct_previous_depth_pass.hlsl @@ -22,16 +22,20 @@ // FSR2 pass 2 // SRV 2 : m_MotionVector : r_motion_vectors // SRV 3 : m_depthbuffer : r_depth -// UAV 7 : FSR2_ReconstructedPrevNearestDepth : rw_ReconstructedPrevNearestDepth +// UAV 7 : FSR2_ReconstructedPrevNearestDepth : rw_reconstructed_previous_nearest_depth // UAV 8 : FSR2_DilatedVelocity : rw_dilated_motion_vectors // UAV 9 : FSR2_DilatedDepth : rw_dilatedDepth // CB 0 : cbFSR2 #define FSR2_BIND_SRV_MOTION_VECTORS 0 #define FSR2_BIND_SRV_DEPTH 1 +#define FSR2_BIND_SRV_REACTIVE_MASK 2 +#define FSR2_BIND_SRV_TRANSPARENCY_AND_COMPOSITION_MASK 3 +#define FSR2_BIND_SRV_PREPARED_INPUT_COLOR 4 #define FSR2_BIND_UAV_RECONSTRUCTED_PREV_NEAREST_DEPTH 0 #define FSR2_BIND_UAV_DILATED_MOTION_VECTORS 1 #define FSR2_BIND_UAV_DILATED_DEPTH 2 +#define FSR2_BIND_UAV_DILATED_REACTIVE_MASKS 3 #define FSR2_BIND_CB_FSR2 0 #include "ffx_fsr2_callbacks_hlsl.h" @@ -56,11 +60,11 @@ FFX_FSR2_PREFER_WAVE64 FFX_FSR2_NUM_THREADS FFX_FSR2_EMBED_ROOTSIG_CONTENT void CS( - min16int2 iGroupId : SV_GroupID, - min16int2 iDispatchThreadId : SV_DispatchThreadID, - min16int2 iGroupThreadId : SV_GroupThreadID, + int2 iGroupId : SV_GroupID, + int2 iDispatchThreadId : SV_DispatchThreadID, + int2 iGroupThreadId : SV_GroupThreadID, int iGroupIndex : SV_GroupIndex ) { - ReconstructPrevDepthAndDilateMotionVectors(iDispatchThreadId); + ReconstructAndDilate(iDispatchThreadId); } diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_reproject.h b/src/ffx-fsr2-api/shaders/ffx_fsr2_reproject.h index 3fceafd..5ae962d 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_reproject.h +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_reproject.h @@ -22,31 +22,62 @@ #ifndef FFX_FSR2_REPROJECT_H #define FFX_FSR2_REPROJECT_H -FFX_MIN16_F4 WrapHistory(FfxInt32x2 iPxSample) +#ifndef FFX_FSR2_OPTION_REPROJECT_USE_LANCZOS_TYPE +#define FFX_FSR2_OPTION_REPROJECT_USE_LANCZOS_TYPE 1 // Approximate +#endif + +FfxFloat32x4 WrapHistory(FfxInt32x2 iPxSample) { return LoadHistory(iPxSample); } -DeclareCustomFetchBicubicSamplesMin16(FetchHistorySamples, WrapHistory) -DeclareCustomTextureSample(HistorySample, Lanczos2, FetchHistorySamples) - - -FFX_MIN16_F4 WrapLockStatus(FfxInt32x2 iPxSample) +#if FFX_HALF +FFX_MIN16_F4 WrapHistory(FFX_MIN16_I2 iPxSample) { - return FFX_MIN16_F4(LoadLockStatus(FFX_MIN16_I2(iPxSample)), 0); + return FFX_MIN16_F4(LoadHistory(iPxSample)); } - -#if 1 -DeclareCustomFetchBilinearSamples(FetchLockStatusSamples, WrapLockStatus) -DeclareCustomTextureSample(LockStatusSample, Bilinear, FetchLockStatusSamples) -#else -DeclareCustomFetchBicubicSamplesMin16(FetchLockStatusSamples, WrapLockStatus) -DeclareCustomTextureSample(LockStatusSample, Lanczos2, FetchLockStatusSamples) #endif +#if FFX_FSR2_OPTION_REPROJECT_SAMPLERS_USE_DATA_HALF && FFX_HALF +DeclareCustomFetchBicubicSamplesMin16(FetchHistorySamples, WrapHistory) +DeclareCustomTextureSampleMin16(HistorySample, FFX_FSR2_GET_LANCZOS_SAMPLER1D(FFX_FSR2_OPTION_REPROJECT_USE_LANCZOS_TYPE), FetchHistorySamples) +#else +DeclareCustomFetchBicubicSamples(FetchHistorySamples, WrapHistory) +DeclareCustomTextureSample(HistorySample, FFX_FSR2_GET_LANCZOS_SAMPLER1D(FFX_FSR2_OPTION_REPROJECT_USE_LANCZOS_TYPE), FetchHistorySamples) +#endif -FfxFloat32x2 GetMotionVector(FFX_MIN16_I2 iPxHrPos, FfxFloat32x2 fHrUv) +FfxFloat32x4 WrapLockStatus(FfxInt32x2 iPxSample) +{ + return FfxFloat32x4(LoadLockStatus(iPxSample), 0.0f); +} + +#if FFX_HALF +FFX_MIN16_F4 WrapLockStatus(FFX_MIN16_I2 iPxSample) +{ + return FFX_MIN16_F4(LoadLockStatus(iPxSample), 0.0f); +} +#endif + +#if 1 +#if FFX_FSR2_OPTION_REPROJECT_SAMPLERS_USE_DATA_HALF && FFX_HALF +DeclareCustomFetchBilinearSamplesMin16(FetchLockStatusSamples, WrapLockStatus) +DeclareCustomTextureSampleMin16(LockStatusSample, Bilinear, FetchLockStatusSamples) +#else +DeclareCustomFetchBilinearSamples(FetchLockStatusSamples, WrapLockStatus) +DeclareCustomTextureSample(LockStatusSample, Bilinear, FetchLockStatusSamples) +#endif +#else +#if FFX_FSR2_OPTION_REPROJECT_SAMPLERS_USE_DATA_HALF && FFX_HALF +DeclareCustomFetchBicubicSamplesMin16(FetchLockStatusSamples, WrapLockStatus) +DeclareCustomTextureSampleMin16(LockStatusSample, FFX_FSR2_GET_LANCZOS_SAMPLER1D(FFX_FSR2_OPTION_REPROJECT_USE_LANCZOS_TYPE), FetchLockStatusSamples) +#else +DeclareCustomFetchBicubicSamples(FetchLockStatusSamples, WrapLockStatus) +DeclareCustomTextureSample(LockStatusSample, FFX_FSR2_GET_LANCZOS_SAMPLER1D(FFX_FSR2_OPTION_REPROJECT_USE_LANCZOS_TYPE), FetchLockStatusSamples) +#endif +#endif + +FfxFloat32x2 GetMotionVector(FfxInt32x2 iPxHrPos, FfxFloat32x2 fHrUv) { #if FFX_FSR2_OPTION_LOW_RESOLUTION_MOTION_VECTORS FfxFloat32x2 fDilatedMotionVector = LoadDilatedMotionVector(FFX_MIN16_I2(fHrUv * RenderSize())); @@ -78,10 +109,10 @@ void ReprojectHistoryColor(FfxInt32x2 iPxHrPos, FfxFloat32x2 fReprojectedHrUv, F fHistoryColorAndWeight.rgb = RGBToYCoCg(fHistoryColorAndWeight.rgb); } -void ReprojectHistoryLockStatus(FfxInt32x2 iPxHrPos, FfxFloat32x2 fReprojectedHrUv, FFX_PARAMETER_OUT LOCK_STATUS_T fReprojectedLockStatus) +void ReprojectHistoryLockStatus(FfxInt32x2 iPxHrPos, FfxFloat32x2 fReprojectedHrUv, FFX_PARAMETER_OUT FfxFloat32x3 fReprojectedLockStatus) { // If function is called from Accumulate pass, we need to treat locks differently - LOCK_STATUS_F1 fInPlaceLockLifetime = LoadRwLockStatus(iPxHrPos)[LOCK_LIFETIME_REMAINING]; + FfxFloat32 fInPlaceLockLifetime = LoadRwLockStatus(iPxHrPos)[LOCK_LIFETIME_REMAINING]; fReprojectedLockStatus = SampleLockStatus(fReprojectedHrUv); @@ -90,4 +121,5 @@ void ReprojectHistoryLockStatus(FfxInt32x2 iPxHrPos, FfxFloat32x2 fReprojectedHr fReprojectedLockStatus[LOCK_LIFETIME_REMAINING] = fInPlaceLockLifetime; } } + #endif //!defined( FFX_FSR2_REPROJECT_H ) diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_resources.h b/src/ffx-fsr2-api/shaders/ffx_fsr2_resources.h index 2309351..89734f6 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_resources.h +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_resources.h @@ -50,7 +50,7 @@ #define FFX_FSR2_RESOURCE_IDENTIFIER_INTERNAL_DEFAULT_REACTIVITY 24 #define FFX_FSR2_RESOURCE_IDENTIFIER_INTERNAL_DEFAULT_TRANSPARENCY_AND_COMPOSITION 25 #define FFX_FSR2_RESOURCE_IDENTITIER_UPSAMPLE_MAXIMUM_BIAS_LUT 26 -#define FFX_FSR2_RESOURCE_IDENTIFIER_REACTIVE_MAX 27 +#define FFX_FSR2_RESOURCE_IDENTIFIER_DILATED_REACTIVE_MASKS 27 #define FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE 28 // same as FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_0 #define FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_0 28 #define FFX_FSR2_RESOURCE_IDENTIFIER_AUTO_EXPOSURE_MIPMAP_1 29 @@ -74,7 +74,6 @@ #define FFX_FSR2_RESOURCE_IDENTIFIER_COUNT 43 - #define FFX_FSR2_CONSTANTBUFFER_IDENTIFIER_FSR2 0 #define FFX_FSR2_CONSTANTBUFFER_IDENTIFIER_SPD 1 #define FFX_FSR2_CONSTANTBUFFER_IDENTIFIER_RCAS 2 diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_sample.h b/src/ffx-fsr2-api/shaders/ffx_fsr2_sample.h index 95fa51d..f697d70 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_sample.h +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_sample.h @@ -60,30 +60,40 @@ struct FetchedBicubicSamples { }; #if FFX_HALF +struct FetchedBilinearSamplesMin16 { + + FFX_MIN16_F4 fColor00; + FFX_MIN16_F4 fColor10; + + FFX_MIN16_F4 fColor01; + FFX_MIN16_F4 fColor11; +}; + struct FetchedBicubicSamplesMin16 { - FfxFloat16x4 fColor00; - FfxFloat16x4 fColor10; - FfxFloat16x4 fColor20; - FfxFloat16x4 fColor30; + FFX_MIN16_F4 fColor00; + FFX_MIN16_F4 fColor10; + FFX_MIN16_F4 fColor20; + FFX_MIN16_F4 fColor30; - FfxFloat16x4 fColor01; - FfxFloat16x4 fColor11; - FfxFloat16x4 fColor21; - FfxFloat16x4 fColor31; + FFX_MIN16_F4 fColor01; + FFX_MIN16_F4 fColor11; + FFX_MIN16_F4 fColor21; + FFX_MIN16_F4 fColor31; - FfxFloat16x4 fColor02; - FfxFloat16x4 fColor12; - FfxFloat16x4 fColor22; - FfxFloat16x4 fColor32; + FFX_MIN16_F4 fColor02; + FFX_MIN16_F4 fColor12; + FFX_MIN16_F4 fColor22; + FFX_MIN16_F4 fColor32; - FfxFloat16x4 fColor03; - FfxFloat16x4 fColor13; - FfxFloat16x4 fColor23; - FfxFloat16x4 fColor33; + FFX_MIN16_F4 fColor03; + FFX_MIN16_F4 fColor13; + FFX_MIN16_F4 fColor23; + FFX_MIN16_F4 fColor33; }; #else //FFX_HALF #define FetchedBicubicSamplesMin16 FetchedBicubicSamples +#define FetchedBilinearSamplesMin16 FetchedBilinearSamples #endif //FFX_HALF FfxFloat32x4 Linear(FfxFloat32x4 A, FfxFloat32x4 B, FfxFloat32 t) @@ -99,39 +109,44 @@ FfxFloat32x4 Bilinear(FetchedBilinearSamples BilinearSamples, FfxFloat32x2 fPxFr return fColorXY; } -// SEE: ../Interpolation/CatmullRom.ipynb, t=0 -> B, t=1 -> C -FfxFloat32x4 CubicCatmullRom(FfxFloat32x4 A, FfxFloat32x4 B, FfxFloat32x4 C, FfxFloat32x4 D, FfxFloat32 t) +#if FFX_HALF +FFX_MIN16_F4 Linear(FFX_MIN16_F4 A, FFX_MIN16_F4 B, FFX_MIN16_F t) { - FfxFloat32 t2 = t * t; - FfxFloat32 t3 = t * t * t; - FfxFloat32x4 a = -A / 2.f + (3.f * B) / 2.f - (3.f * C) / 2.f + D / 2.f; - FfxFloat32x4 b = A - (5.f * B) / 2.f + 2.f * C - D / 2.f; - FfxFloat32x4 c = -A / 2.f + C / 2.f; - FfxFloat32x4 d = B; - return a * t3 + b * t2 + c * t + d; + return A + (B - A) * t; } -FfxFloat32x4 BicubicCatmullRom(FetchedBicubicSamples BicubicSamples, FfxFloat32x2 fPxFrac) +FFX_MIN16_F4 Bilinear(FetchedBilinearSamplesMin16 BilinearSamples, FFX_MIN16_F2 fPxFrac) { - FfxFloat32x4 fColorX0 = CubicCatmullRom(BicubicSamples.fColor00, BicubicSamples.fColor10, BicubicSamples.fColor20, BicubicSamples.fColor30, fPxFrac.x); - FfxFloat32x4 fColorX1 = CubicCatmullRom(BicubicSamples.fColor01, BicubicSamples.fColor11, BicubicSamples.fColor21, BicubicSamples.fColor31, fPxFrac.x); - FfxFloat32x4 fColorX2 = CubicCatmullRom(BicubicSamples.fColor02, BicubicSamples.fColor12, BicubicSamples.fColor22, BicubicSamples.fColor32, fPxFrac.x); - FfxFloat32x4 fColorX3 = CubicCatmullRom(BicubicSamples.fColor03, BicubicSamples.fColor13, BicubicSamples.fColor23, BicubicSamples.fColor33, fPxFrac.x); - FfxFloat32x4 fColorXY = CubicCatmullRom(fColorX0, fColorX1, fColorX2, fColorX3, fPxFrac.y); + FFX_MIN16_F4 fColorX0 = Linear(BilinearSamples.fColor00, BilinearSamples.fColor10, fPxFrac.x); + FFX_MIN16_F4 fColorX1 = Linear(BilinearSamples.fColor01, BilinearSamples.fColor11, fPxFrac.x); + FFX_MIN16_F4 fColorXY = Linear(fColorX0, fColorX1, fPxFrac.y); return fColorXY; } +#endif -FfxFloat32 Lanczos2(FfxFloat32 x) +FfxFloat32 Lanczos2NoClamp(FfxFloat32 x) { const FfxFloat32 PI = 3.141592653589793f; // TODO: share SDK constants return abs(x) < FSR2_EPSILON ? 1.f : (sin(PI * x) / (PI * x)) * (sin(0.5f * PI * x) / (0.5f * PI * x)); } -#if FFX_HALF -FfxFloat16 Lanczos2(FfxFloat16 x) +FfxFloat32 Lanczos2(FfxFloat32 x) { - const FFX_MIN16_F PI = FfxFloat16(3.141592653589793f); // TODO: share SDK constants - return abs(x) < FSR2_EPSILON ? FfxFloat16(1.f) : (sin(PI * x) / (PI * x)) * (sin(FfxFloat16(0.5f) * PI * x) / (FfxFloat16(0.5f) * PI * x)); + x = ffxMin(abs(x), 2.0f); + return Lanczos2NoClamp(x); +} + +#if FFX_HALF +FFX_MIN16_F Lanczos2NoClamp(FFX_MIN16_F x) +{ + const FFX_MIN16_F PI = FFX_MIN16_F(3.141592653589793f); // TODO: share SDK constants + return abs(x) < FFX_MIN16_F(FSR2_EPSILON) ? FFX_MIN16_F(1.f) : (sin(PI * x) / (PI * x)) * (sin(FFX_MIN16_F(0.5f) * PI * x) / (FFX_MIN16_F(0.5f) * PI * x)); +} + +FFX_MIN16_F Lanczos2(FFX_MIN16_F x) +{ + x = ffxMin(abs(x), FFX_MIN16_F(2.0f)); + return Lanczos2NoClamp(x); } #endif //FFX_HALF @@ -144,11 +159,11 @@ FfxFloat32 Lanczos2ApproxSqNoClamp(FfxFloat32 x2) } #if FFX_HALF -FfxFloat16 Lanczos2ApproxSqNoClamp(FfxFloat16 x2) +FFX_MIN16_F Lanczos2ApproxSqNoClamp(FFX_MIN16_F x2) { - FfxFloat16 a = FfxFloat16(2.0f / 5.0f) * x2 - FfxFloat16(1); - FfxFloat16 b = FfxFloat16(1.0f / 4.0f) * x2 - FfxFloat16(1); - return (FfxFloat16(25.0f / 16.0f) * a * a - FfxFloat16(25.0f / 16.0f - 1)) * (b * b); + FFX_MIN16_F a = FFX_MIN16_F(2.0f / 5.0f) * x2 - FFX_MIN16_F(1); + FFX_MIN16_F b = FFX_MIN16_F(1.0f / 4.0f) * x2 - FFX_MIN16_F(1); + return (FFX_MIN16_F(25.0f / 16.0f) * a * a - FFX_MIN16_F(25.0f / 16.0f - 1)) * (b * b); } #endif //FFX_HALF @@ -159,9 +174,9 @@ FfxFloat32 Lanczos2ApproxSq(FfxFloat32 x2) } #if FFX_HALF -FfxFloat16 Lanczos2ApproxSq(FfxFloat16 x2) +FFX_MIN16_F Lanczos2ApproxSq(FFX_MIN16_F x2) { - x2 = ffxMin(x2, FfxFloat16(4.0f)); + x2 = ffxMin(x2, FFX_MIN16_F(4.0f)); return Lanczos2ApproxSqNoClamp(x2); } #endif //FFX_HALF @@ -172,7 +187,7 @@ FfxFloat32 Lanczos2ApproxNoClamp(FfxFloat32 x) } #if FFX_HALF -FfxFloat16 Lanczos2ApproxNoClamp(FfxFloat16 x) +FFX_MIN16_F Lanczos2ApproxNoClamp(FFX_MIN16_F x) { return Lanczos2ApproxSqNoClamp(x * x); } @@ -184,7 +199,7 @@ FfxFloat32 Lanczos2Approx(FfxFloat32 x) } #if FFX_HALF -FfxFloat16 Lanczos2Approx(FfxFloat16 x) +FFX_MIN16_F Lanczos2Approx(FFX_MIN16_F x) { return Lanczos2ApproxSq(x * x); } @@ -196,14 +211,13 @@ FfxFloat32 Lanczos2_UseLUT(FfxFloat32 x) } #if FFX_HALF -FfxFloat16 Lanczos2_UseLUT(FfxFloat16 x) +FFX_MIN16_F Lanczos2_UseLUT(FFX_MIN16_F x) { - return SampleLanczos2Weight(abs(x)); + return FFX_MIN16_F(SampleLanczos2Weight(abs(x))); } #endif //FFX_HALF -#if FFX_FSR2_OPTION_USE_LANCZOS_LUT -FfxFloat32x4 Lanczos2_AllowLUT(FfxFloat32x4 fColor0, FfxFloat32x4 fColor1, FfxFloat32x4 fColor2, FfxFloat32x4 fColor3, FfxFloat32 t) +FfxFloat32x4 Lanczos2_UseLUT(FfxFloat32x4 fColor0, FfxFloat32x4 fColor1, FfxFloat32x4 fColor2, FfxFloat32x4 fColor3, FfxFloat32 t) { FfxFloat32 fWeight0 = Lanczos2_UseLUT(-1.f - t); FfxFloat32 fWeight1 = Lanczos2_UseLUT(-0.f - t); @@ -212,18 +226,15 @@ FfxFloat32x4 Lanczos2_AllowLUT(FfxFloat32x4 fColor0, FfxFloat32x4 fColor1, FfxFl return (fWeight0 * fColor0 + fWeight1 * fColor1 + fWeight2 * fColor2 + fWeight3 * fColor3) / (fWeight0 + fWeight1 + fWeight2 + fWeight3); } #if FFX_HALF -FfxFloat16x4 Lanczos2_AllowLUT(FfxFloat16x4 fColor0, FfxFloat16x4 fColor1, FfxFloat16x4 fColor2, FfxFloat16x4 fColor3, FfxFloat16 t) +FFX_MIN16_F4 Lanczos2_UseLUT(FFX_MIN16_F4 fColor0, FFX_MIN16_F4 fColor1, FFX_MIN16_F4 fColor2, FFX_MIN16_F4 fColor3, FFX_MIN16_F t) { - FfxFloat16 fWeight0 = Lanczos2_UseLUT(FfxFloat16(-1.f) - t); - FfxFloat16 fWeight1 = Lanczos2_UseLUT(FfxFloat16(-0.f) - t); - FfxFloat16 fWeight2 = Lanczos2_UseLUT(FfxFloat16(+1.f) - t); - FfxFloat16 fWeight3 = Lanczos2_UseLUT(FfxFloat16(+2.f) - t); + FFX_MIN16_F fWeight0 = Lanczos2_UseLUT(FFX_MIN16_F(-1.f) - t); + FFX_MIN16_F fWeight1 = Lanczos2_UseLUT(FFX_MIN16_F(-0.f) - t); + FFX_MIN16_F fWeight2 = Lanczos2_UseLUT(FFX_MIN16_F(+1.f) - t); + FFX_MIN16_F fWeight3 = Lanczos2_UseLUT(FFX_MIN16_F(+2.f) - t); return (fWeight0 * fColor0 + fWeight1 * fColor1 + fWeight2 * fColor2 + fWeight3 * fColor3) / (fWeight0 + fWeight1 + fWeight2 + fWeight3); } -#endif //FFX_HALF -#else //FFX_FSR2_OPTION_USE_LANCZOS_LUT -#define Lanczos2_AllowLUT Lanczos2 -#endif //FFX_FSR2_OPTION_USE_LANCZOS_LUT +#endif FfxFloat32x4 Lanczos2(FfxFloat32x4 fColor0, FfxFloat32x4 fColor1, FfxFloat32x4 fColor2, FfxFloat32x4 fColor3, FfxFloat32 t) { @@ -236,11 +247,11 @@ FfxFloat32x4 Lanczos2(FfxFloat32x4 fColor0, FfxFloat32x4 fColor1, FfxFloat32x4 f FfxFloat32x4 Lanczos2(FetchedBicubicSamples Samples, FfxFloat32x2 fPxFrac) { - FfxFloat32x4 fColorX0 = Lanczos2_AllowLUT(Samples.fColor00, Samples.fColor10, Samples.fColor20, Samples.fColor30, fPxFrac.x); - FfxFloat32x4 fColorX1 = Lanczos2_AllowLUT(Samples.fColor01, Samples.fColor11, Samples.fColor21, Samples.fColor31, fPxFrac.x); - FfxFloat32x4 fColorX2 = Lanczos2_AllowLUT(Samples.fColor02, Samples.fColor12, Samples.fColor22, Samples.fColor32, fPxFrac.x); - FfxFloat32x4 fColorX3 = Lanczos2_AllowLUT(Samples.fColor03, Samples.fColor13, Samples.fColor23, Samples.fColor33, fPxFrac.x); - FfxFloat32x4 fColorXY = Lanczos2_AllowLUT(fColorX0, fColorX1, fColorX2, fColorX3, fPxFrac.y); + FfxFloat32x4 fColorX0 = Lanczos2(Samples.fColor00, Samples.fColor10, Samples.fColor20, Samples.fColor30, fPxFrac.x); + FfxFloat32x4 fColorX1 = Lanczos2(Samples.fColor01, Samples.fColor11, Samples.fColor21, Samples.fColor31, fPxFrac.x); + FfxFloat32x4 fColorX2 = Lanczos2(Samples.fColor02, Samples.fColor12, Samples.fColor22, Samples.fColor32, fPxFrac.x); + FfxFloat32x4 fColorX3 = Lanczos2(Samples.fColor03, Samples.fColor13, Samples.fColor23, Samples.fColor33, fPxFrac.x); + FfxFloat32x4 fColorXY = Lanczos2(fColorX0, fColorX1, fColorX2, fColorX3, fPxFrac.y); // Deringing @@ -269,36 +280,36 @@ FfxFloat32x4 Lanczos2(FetchedBicubicSamples Samples, FfxFloat32x2 fPxFrac) } #if FFX_HALF -FfxFloat16x4 Lanczos2(FfxFloat16x4 fColor0, FfxFloat16x4 fColor1, FfxFloat16x4 fColor2, FfxFloat16x4 fColor3, FfxFloat16 t) +FFX_MIN16_F4 Lanczos2(FFX_MIN16_F4 fColor0, FFX_MIN16_F4 fColor1, FFX_MIN16_F4 fColor2, FFX_MIN16_F4 fColor3, FFX_MIN16_F t) { - FfxFloat16 fWeight0 = Lanczos2(FfxFloat16(-1.f) - t); - FfxFloat16 fWeight1 = Lanczos2(FfxFloat16(-0.f) - t); - FfxFloat16 fWeight2 = Lanczos2(FfxFloat16(+1.f) - t); - FfxFloat16 fWeight3 = Lanczos2(FfxFloat16(+2.f) - t); + FFX_MIN16_F fWeight0 = Lanczos2(FFX_MIN16_F(-1.f) - t); + FFX_MIN16_F fWeight1 = Lanczos2(FFX_MIN16_F(-0.f) - t); + FFX_MIN16_F fWeight2 = Lanczos2(FFX_MIN16_F(+1.f) - t); + FFX_MIN16_F fWeight3 = Lanczos2(FFX_MIN16_F(+2.f) - t); return (fWeight0 * fColor0 + fWeight1 * fColor1 + fWeight2 * fColor2 + fWeight3 * fColor3) / (fWeight0 + fWeight1 + fWeight2 + fWeight3); } -FfxFloat16x4 Lanczos2(FetchedBicubicSamplesMin16 Samples, FFX_MIN16_F2 fPxFrac) +FFX_MIN16_F4 Lanczos2(FetchedBicubicSamplesMin16 Samples, FFX_MIN16_F2 fPxFrac) { - FfxFloat16x4 fColorX0 = Lanczos2_AllowLUT(Samples.fColor00, Samples.fColor10, Samples.fColor20, Samples.fColor30, fPxFrac.x); - FfxFloat16x4 fColorX1 = Lanczos2_AllowLUT(Samples.fColor01, Samples.fColor11, Samples.fColor21, Samples.fColor31, fPxFrac.x); - FfxFloat16x4 fColorX2 = Lanczos2_AllowLUT(Samples.fColor02, Samples.fColor12, Samples.fColor22, Samples.fColor32, fPxFrac.x); - FfxFloat16x4 fColorX3 = Lanczos2_AllowLUT(Samples.fColor03, Samples.fColor13, Samples.fColor23, Samples.fColor33, fPxFrac.x); - FfxFloat16x4 fColorXY = Lanczos2_AllowLUT(fColorX0, fColorX1, fColorX2, fColorX3, fPxFrac.y); + FFX_MIN16_F4 fColorX0 = Lanczos2(Samples.fColor00, Samples.fColor10, Samples.fColor20, Samples.fColor30, fPxFrac.x); + FFX_MIN16_F4 fColorX1 = Lanczos2(Samples.fColor01, Samples.fColor11, Samples.fColor21, Samples.fColor31, fPxFrac.x); + FFX_MIN16_F4 fColorX2 = Lanczos2(Samples.fColor02, Samples.fColor12, Samples.fColor22, Samples.fColor32, fPxFrac.x); + FFX_MIN16_F4 fColorX3 = Lanczos2(Samples.fColor03, Samples.fColor13, Samples.fColor23, Samples.fColor33, fPxFrac.x); + FFX_MIN16_F4 fColorXY = Lanczos2(fColorX0, fColorX1, fColorX2, fColorX3, fPxFrac.y); // Deringing // TODO: only use 4 by checking jitter const FfxInt32 iDeringingSampleCount = 4; - const FfxFloat16x4 fDeringingSamples[4] = { + const FFX_MIN16_F4 fDeringingSamples[4] = { Samples.fColor11, Samples.fColor21, Samples.fColor12, Samples.fColor22, }; - FfxFloat16x4 fDeringingMin = fDeringingSamples[0]; - FfxFloat16x4 fDeringingMax = fDeringingSamples[0]; + FFX_MIN16_F4 fDeringingMin = fDeringingSamples[0]; + FFX_MIN16_F4 fDeringingMax = fDeringingSamples[0]; FFX_UNROLL for (FfxInt32 iSampleIndex = 1; iSampleIndex < iDeringingSampleCount; ++iSampleIndex) @@ -313,6 +324,79 @@ FfxFloat16x4 Lanczos2(FetchedBicubicSamplesMin16 Samples, FFX_MIN16_F2 fPxFrac) } #endif //FFX_HALF + +FfxFloat32x4 Lanczos2LUT(FetchedBicubicSamples Samples, FfxFloat32x2 fPxFrac) +{ + FfxFloat32x4 fColorX0 = Lanczos2_UseLUT(Samples.fColor00, Samples.fColor10, Samples.fColor20, Samples.fColor30, fPxFrac.x); + FfxFloat32x4 fColorX1 = Lanczos2_UseLUT(Samples.fColor01, Samples.fColor11, Samples.fColor21, Samples.fColor31, fPxFrac.x); + FfxFloat32x4 fColorX2 = Lanczos2_UseLUT(Samples.fColor02, Samples.fColor12, Samples.fColor22, Samples.fColor32, fPxFrac.x); + FfxFloat32x4 fColorX3 = Lanczos2_UseLUT(Samples.fColor03, Samples.fColor13, Samples.fColor23, Samples.fColor33, fPxFrac.x); + FfxFloat32x4 fColorXY = Lanczos2_UseLUT(fColorX0, fColorX1, fColorX2, fColorX3, fPxFrac.y); + + // Deringing + + // TODO: only use 4 by checking jitter + const FfxInt32 iDeringingSampleCount = 4; + const FfxFloat32x4 fDeringingSamples[4] = { + Samples.fColor11, + Samples.fColor21, + Samples.fColor12, + Samples.fColor22, + }; + + FfxFloat32x4 fDeringingMin = fDeringingSamples[0]; + FfxFloat32x4 fDeringingMax = fDeringingSamples[0]; + + FFX_UNROLL + for (FfxInt32 iSampleIndex = 1; iSampleIndex < iDeringingSampleCount; ++iSampleIndex) { + + fDeringingMin = ffxMin(fDeringingMin, fDeringingSamples[iSampleIndex]); + fDeringingMax = ffxMax(fDeringingMax, fDeringingSamples[iSampleIndex]); + } + + fColorXY = clamp(fColorXY, fDeringingMin, fDeringingMax); + + return fColorXY; +} + +#if FFX_HALF +FFX_MIN16_F4 Lanczos2LUT(FetchedBicubicSamplesMin16 Samples, FFX_MIN16_F2 fPxFrac) +{ + FFX_MIN16_F4 fColorX0 = Lanczos2_UseLUT(Samples.fColor00, Samples.fColor10, Samples.fColor20, Samples.fColor30, fPxFrac.x); + FFX_MIN16_F4 fColorX1 = Lanczos2_UseLUT(Samples.fColor01, Samples.fColor11, Samples.fColor21, Samples.fColor31, fPxFrac.x); + FFX_MIN16_F4 fColorX2 = Lanczos2_UseLUT(Samples.fColor02, Samples.fColor12, Samples.fColor22, Samples.fColor32, fPxFrac.x); + FFX_MIN16_F4 fColorX3 = Lanczos2_UseLUT(Samples.fColor03, Samples.fColor13, Samples.fColor23, Samples.fColor33, fPxFrac.x); + FFX_MIN16_F4 fColorXY = Lanczos2_UseLUT(fColorX0, fColorX1, fColorX2, fColorX3, fPxFrac.y); + + // Deringing + + // TODO: only use 4 by checking jitter + const FfxInt32 iDeringingSampleCount = 4; + const FFX_MIN16_F4 fDeringingSamples[4] = { + Samples.fColor11, + Samples.fColor21, + Samples.fColor12, + Samples.fColor22, + }; + + FFX_MIN16_F4 fDeringingMin = fDeringingSamples[0]; + FFX_MIN16_F4 fDeringingMax = fDeringingSamples[0]; + + FFX_UNROLL + for (FfxInt32 iSampleIndex = 1; iSampleIndex < iDeringingSampleCount; ++iSampleIndex) + { + fDeringingMin = ffxMin(fDeringingMin, fDeringingSamples[iSampleIndex]); + fDeringingMax = ffxMax(fDeringingMax, fDeringingSamples[iSampleIndex]); + } + + fColorXY = clamp(fColorXY, fDeringingMin, fDeringingMax); + + return fColorXY; +} +#endif //FFX_HALF + + + FfxFloat32x4 Lanczos2Approx(FfxFloat32x4 fColor0, FfxFloat32x4 fColor1, FfxFloat32x4 fColor2, FfxFloat32x4 fColor3, FfxFloat32 t) { FfxFloat32 fWeight0 = Lanczos2ApproxNoClamp(-1.f - t); @@ -323,12 +407,12 @@ FfxFloat32x4 Lanczos2Approx(FfxFloat32x4 fColor0, FfxFloat32x4 fColor1, FfxFloat } #if FFX_HALF -FfxFloat16x4 Lanczos2Approx(FfxFloat16x4 fColor0, FfxFloat16x4 fColor1, FfxFloat16x4 fColor2, FfxFloat16x4 fColor3, FfxFloat16 t) +FFX_MIN16_F4 Lanczos2Approx(FFX_MIN16_F4 fColor0, FFX_MIN16_F4 fColor1, FFX_MIN16_F4 fColor2, FFX_MIN16_F4 fColor3, FFX_MIN16_F t) { - FfxFloat16 fWeight0 = Lanczos2ApproxNoClamp(FfxFloat16(-1.f) - t); - FfxFloat16 fWeight1 = Lanczos2ApproxNoClamp(FfxFloat16(-0.f) - t); - FfxFloat16 fWeight2 = Lanczos2ApproxNoClamp(FfxFloat16(+1.f) - t); - FfxFloat16 fWeight3 = Lanczos2ApproxNoClamp(FfxFloat16(+2.f) - t); + FFX_MIN16_F fWeight0 = Lanczos2ApproxNoClamp(FFX_MIN16_F(-1.f) - t); + FFX_MIN16_F fWeight1 = Lanczos2ApproxNoClamp(FFX_MIN16_F(-0.f) - t); + FFX_MIN16_F fWeight2 = Lanczos2ApproxNoClamp(FFX_MIN16_F(+1.f) - t); + FFX_MIN16_F fWeight3 = Lanczos2ApproxNoClamp(FFX_MIN16_F(+2.f) - t); return (fWeight0 * fColor0 + fWeight1 * fColor1 + fWeight2 * fColor2 + fWeight3 * fColor3) / (fWeight0 + fWeight1 + fWeight2 + fWeight3); } #endif //FFX_HALF @@ -368,27 +452,27 @@ FfxFloat32x4 Lanczos2Approx(FetchedBicubicSamples Samples, FfxFloat32x2 fPxFrac) } #if FFX_HALF -FfxFloat16x4 Lanczos2Approx(FetchedBicubicSamplesMin16 Samples, FfxFloat16x2 fPxFrac) +FFX_MIN16_F4 Lanczos2Approx(FetchedBicubicSamplesMin16 Samples, FFX_MIN16_F2 fPxFrac) { - FfxFloat16x4 fColorX0 = Lanczos2Approx(Samples.fColor00, Samples.fColor10, Samples.fColor20, Samples.fColor30, fPxFrac.x); - FfxFloat16x4 fColorX1 = Lanczos2Approx(Samples.fColor01, Samples.fColor11, Samples.fColor21, Samples.fColor31, fPxFrac.x); - FfxFloat16x4 fColorX2 = Lanczos2Approx(Samples.fColor02, Samples.fColor12, Samples.fColor22, Samples.fColor32, fPxFrac.x); - FfxFloat16x4 fColorX3 = Lanczos2Approx(Samples.fColor03, Samples.fColor13, Samples.fColor23, Samples.fColor33, fPxFrac.x); - FfxFloat16x4 fColorXY = Lanczos2Approx(fColorX0, fColorX1, fColorX2, fColorX3, fPxFrac.y); + FFX_MIN16_F4 fColorX0 = Lanczos2Approx(Samples.fColor00, Samples.fColor10, Samples.fColor20, Samples.fColor30, fPxFrac.x); + FFX_MIN16_F4 fColorX1 = Lanczos2Approx(Samples.fColor01, Samples.fColor11, Samples.fColor21, Samples.fColor31, fPxFrac.x); + FFX_MIN16_F4 fColorX2 = Lanczos2Approx(Samples.fColor02, Samples.fColor12, Samples.fColor22, Samples.fColor32, fPxFrac.x); + FFX_MIN16_F4 fColorX3 = Lanczos2Approx(Samples.fColor03, Samples.fColor13, Samples.fColor23, Samples.fColor33, fPxFrac.x); + FFX_MIN16_F4 fColorXY = Lanczos2Approx(fColorX0, fColorX1, fColorX2, fColorX3, fPxFrac.y); // Deringing // TODO: only use 4 by checking jitter const FfxInt32 iDeringingSampleCount = 4; - const FfxFloat16x4 fDeringingSamples[4] = { + const FFX_MIN16_F4 fDeringingSamples[4] = { Samples.fColor11, Samples.fColor21, Samples.fColor12, Samples.fColor22, }; - FfxFloat16x4 fDeringingMin = fDeringingSamples[0]; - FfxFloat16x4 fDeringingMax = fDeringingSamples[0]; + FFX_MIN16_F4 fDeringingMin = fDeringingSamples[0]; + FFX_MIN16_F4 fDeringingMax = fDeringingSamples[0]; FFX_UNROLL for (FfxInt32 iSampleIndex = 1; iSampleIndex < iDeringingSampleCount; ++iSampleIndex) @@ -401,94 +485,110 @@ FfxFloat16x4 Lanczos2Approx(FetchedBicubicSamplesMin16 Samples, FfxFloat16x2 fPx return fColorXY; } +#endif // Clamp by offset direction. Assuming iPxSample is already in range and iPxOffset is compile time constant. -FfxInt16x2 ClampLoadBicubic(FfxInt16x2 iPxSample, FfxInt16x2 iPxOffset, FfxInt16x2 iTextureSize) +FfxInt32x2 ClampCoord(FfxInt32x2 iPxSample, FfxInt32x2 iPxOffset, FfxInt32x2 iTextureSize) { - FfxInt16x2 result = iPxSample + iPxOffset; - result.x = (iPxOffset.x <= 0) ? ffxMax(result.x, FfxInt16(0)) : result.x; - result.x = (iPxOffset.x > 0) ? ffxMin(result.x, iTextureSize.x - FfxInt16(1)) : result.x; - result.y = (iPxOffset.y <= 0) ? ffxMax(result.y, FfxInt16(0)) : result.y; - result.y = (iPxOffset.y > 0) ? ffxMin(result.y, iTextureSize.y - FfxInt16(1)) : result.y; + FfxInt32x2 result = iPxSample + iPxOffset; + result.x = (iPxOffset.x < 0) ? ffxMax(result.x, 0) : result.x; + result.x = (iPxOffset.x > 0) ? ffxMin(result.x, iTextureSize.x - 1) : result.x; + result.y = (iPxOffset.y < 0) ? ffxMax(result.y, 0) : result.y; + result.y = (iPxOffset.y > 0) ? ffxMin(result.y, iTextureSize.y - 1) : result.y; + return result; +} +#if FFX_HALF +FFX_MIN16_I2 ClampCoord(FFX_MIN16_I2 iPxSample, FFX_MIN16_I2 iPxOffset, FFX_MIN16_I2 iTextureSize) +{ + FFX_MIN16_I2 result = iPxSample + iPxOffset; + result.x = (iPxOffset.x < FFX_MIN16_I(0)) ? ffxMax(result.x, FFX_MIN16_I(0)) : result.x; + result.x = (iPxOffset.x > FFX_MIN16_I(0)) ? ffxMin(result.x, iTextureSize.x - FFX_MIN16_I(1)) : result.x; + result.y = (iPxOffset.y < FFX_MIN16_I(0)) ? ffxMax(result.y, FFX_MIN16_I(0)) : result.y; + result.y = (iPxOffset.y > FFX_MIN16_I(0)) ? ffxMin(result.y, iTextureSize.y - FFX_MIN16_I(1)) : result.y; return result; } #endif //FFX_HALF -FfxInt32x2 ClampLoadBicubic(FfxInt32x2 iPxSample, FfxInt32x2 iPxOffset, FfxInt32x2 iTextureSize) -{ - FfxInt32x2 result = iPxSample + iPxOffset; - result.x = (iPxOffset.x <= 0) ? ffxMax(result.x, 0) : result.x; - result.x = (iPxOffset.x > 0) ? ffxMin(result.x, iTextureSize.x - 1) : result.x; - result.y = (iPxOffset.y <= 0) ? ffxMax(result.y, 0) : result.y; - result.y = (iPxOffset.y > 0) ? ffxMin(result.y, iTextureSize.y - 1) : result.y; - return result; -} -#define DeclareCustomFetchBicubicSamplesWithType(SampleType, AddrType, Name, LoadTexture) \ +#define DeclareCustomFetchBicubicSamplesWithType(SampleType, TextureType, AddrType, Name, LoadTexture) \ SampleType Name(AddrType iPxSample, AddrType iTextureSize) \ { \ SampleType Samples; \ \ - Samples.fColor00 = LoadTexture(ClampLoadBicubic(iPxSample, AddrType(-1, -1), iTextureSize)); \ - Samples.fColor10 = LoadTexture(ClampLoadBicubic(iPxSample, AddrType(+0, -1), iTextureSize)); \ - Samples.fColor20 = LoadTexture(ClampLoadBicubic(iPxSample, AddrType(+1, -1), iTextureSize)); \ - Samples.fColor30 = LoadTexture(ClampLoadBicubic(iPxSample, AddrType(+2, -1), iTextureSize)); \ + Samples.fColor00 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(-1, -1), iTextureSize))); \ + Samples.fColor10 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(+0, -1), iTextureSize))); \ + Samples.fColor20 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(+1, -1), iTextureSize))); \ + Samples.fColor30 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(+2, -1), iTextureSize))); \ \ - Samples.fColor01 = LoadTexture(ClampLoadBicubic(iPxSample, AddrType(-1, +0), iTextureSize)); \ - Samples.fColor11 = LoadTexture(ClampLoadBicubic(iPxSample, AddrType(+0, +0), iTextureSize)); \ - Samples.fColor21 = LoadTexture(ClampLoadBicubic(iPxSample, AddrType(+1, +0), iTextureSize)); \ - Samples.fColor31 = LoadTexture(ClampLoadBicubic(iPxSample, AddrType(+2, +0), iTextureSize)); \ + Samples.fColor01 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(-1, +0), iTextureSize))); \ + Samples.fColor11 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(+0, +0), iTextureSize))); \ + Samples.fColor21 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(+1, +0), iTextureSize))); \ + Samples.fColor31 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(+2, +0), iTextureSize))); \ \ - Samples.fColor02 = LoadTexture(ClampLoadBicubic(iPxSample, AddrType(-1, +1), iTextureSize)); \ - Samples.fColor12 = LoadTexture(ClampLoadBicubic(iPxSample, AddrType(+0, +1), iTextureSize)); \ - Samples.fColor22 = LoadTexture(ClampLoadBicubic(iPxSample, AddrType(+1, +1), iTextureSize)); \ - Samples.fColor32 = LoadTexture(ClampLoadBicubic(iPxSample, AddrType(+2, +1), iTextureSize)); \ + Samples.fColor02 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(-1, +1), iTextureSize))); \ + Samples.fColor12 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(+0, +1), iTextureSize))); \ + Samples.fColor22 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(+1, +1), iTextureSize))); \ + Samples.fColor32 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(+2, +1), iTextureSize))); \ \ - Samples.fColor03 = LoadTexture(ClampLoadBicubic(iPxSample, AddrType(-1, +2), iTextureSize)); \ - Samples.fColor13 = LoadTexture(ClampLoadBicubic(iPxSample, AddrType(+0, +2), iTextureSize)); \ - Samples.fColor23 = LoadTexture(ClampLoadBicubic(iPxSample, AddrType(+1, +2), iTextureSize)); \ - Samples.fColor33 = LoadTexture(ClampLoadBicubic(iPxSample, AddrType(+2, +2), iTextureSize)); \ + Samples.fColor03 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(-1, +2), iTextureSize))); \ + Samples.fColor13 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(+0, +2), iTextureSize))); \ + Samples.fColor23 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(+1, +2), iTextureSize))); \ + Samples.fColor33 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(+2, +2), iTextureSize))); \ \ return Samples; \ } #define DeclareCustomFetchBicubicSamples(Name, LoadTexture) \ - DeclareCustomFetchBicubicSamplesWithType(FetchedBicubicSamples, FfxInt32x2, Name, LoadTexture) + DeclareCustomFetchBicubicSamplesWithType(FetchedBicubicSamples, FfxFloat32x4, FfxInt32x2, Name, LoadTexture) #define DeclareCustomFetchBicubicSamplesMin16(Name, LoadTexture) \ - DeclareCustomFetchBicubicSamplesWithType(FetchedBicubicSamplesMin16, FFX_MIN16_I2, Name, LoadTexture) + DeclareCustomFetchBicubicSamplesWithType(FetchedBicubicSamplesMin16, FFX_MIN16_F4, FfxInt32x2, Name, LoadTexture) -#define DeclareCustomFetchBilinearSamples(Name, LoadTexture) \ - FetchedBilinearSamples Name(FFX_MIN16_I2 iPxSample, FFX_MIN16_I2 iTextureSize) \ +#define DeclareCustomFetchBilinearSamplesWithType(SampleType, TextureType,AddrType, Name, LoadTexture) \ + SampleType Name(AddrType iPxSample, AddrType iTextureSize) \ { \ - FetchedBilinearSamples Samples; \ - Samples.fColor00 = LoadTexture(ClampLoad(iPxSample, FFX_MIN16_I2(+0, +0), iTextureSize)); \ - Samples.fColor10 = LoadTexture(ClampLoad(iPxSample, FFX_MIN16_I2(+1, +0), iTextureSize)); \ - Samples.fColor01 = LoadTexture(ClampLoad(iPxSample, FFX_MIN16_I2(+0, +1), iTextureSize)); \ - Samples.fColor11 = LoadTexture(ClampLoad(iPxSample, FFX_MIN16_I2(+1, +1), iTextureSize)); \ + SampleType Samples; \ + Samples.fColor00 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(+0, +0), iTextureSize))); \ + Samples.fColor10 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(+1, +0), iTextureSize))); \ + Samples.fColor01 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(+0, +1), iTextureSize))); \ + Samples.fColor11 = TextureType(LoadTexture(ClampCoord(iPxSample, AddrType(+1, +1), iTextureSize))); \ return Samples; \ } +#define DeclareCustomFetchBilinearSamples(Name, LoadTexture) \ + DeclareCustomFetchBilinearSamplesWithType(FetchedBilinearSamples, FfxFloat32x4, FfxInt32x2, Name, LoadTexture) + +#define DeclareCustomFetchBilinearSamplesMin16(Name, LoadTexture) \ + DeclareCustomFetchBilinearSamplesWithType(FetchedBilinearSamplesMin16, FFX_MIN16_F4, FfxInt32x2, Name, LoadTexture) + // BE CAREFUL: there is some precision issues and (3253, 125) leading to (3252.9989778, 125.001102) // is common, so iPxSample can "jitter" #define DeclareCustomTextureSample(Name, InterpolateSamples, FetchSamples) \ FfxFloat32x4 Name(FfxFloat32x2 fUvSample, FfxInt32x2 iTextureSize) \ { \ - FfxFloat32x2 fPxSample = fUvSample * FfxFloat32x2(iTextureSize) - FFX_BROADCAST_FLOAT32X2(0.5f); \ + FfxFloat32x2 fPxSample = fUvSample * FfxFloat32x2(iTextureSize) - FfxFloat32x2(0.5f, 0.5f); \ FfxInt32x2 iPxSample = FfxInt32x2(floor(fPxSample)); \ FfxFloat32x2 fPxFrac = ffxFract(fPxSample); \ - FfxFloat32x4 fColorXY = FfxFloat32x4(InterpolateSamples(FetchSamples(FFX_MIN16_I2(iPxSample), FFX_MIN16_I2(iTextureSize)), FFX_MIN16_F2(fPxFrac))); \ + FfxFloat32x4 fColorXY = FfxFloat32x4(InterpolateSamples(FetchSamples(iPxSample, iTextureSize), fPxFrac)); \ return fColorXY; \ } #define DeclareCustomTextureSampleMin16(Name, InterpolateSamples, FetchSamples) \ - FfxFloat32x4 Name(FfxFloat32x2 fUvSample, FfxInt32x2 iTextureSize) \ + FFX_MIN16_F4 Name(FfxFloat32x2 fUvSample, FfxInt32x2 iTextureSize) \ { \ - FfxFloat32x2 fPxSample = fUvSample * FfxFloat32x2(iTextureSize) - FFX_BROADCAST_FLOAT32X2(0.5f); \ + FfxFloat32x2 fPxSample = fUvSample * FfxFloat32x2(iTextureSize) - FfxFloat32x2(0.5f, 0.5f); \ FfxInt32x2 iPxSample = FfxInt32x2(floor(fPxSample)); \ - FfxFloat32x2 fPxFrac = ffxFract(fPxSample); \ - FfxFloat32x4 fColorXY = FfxFloat32x4(InterpolateSamples(FetchSamples(FFX_MIN16_I2(iPxSample), FFX_MIN16_I2(iTextureSize)), FFX_MIN16_F2(fPxFrac))); \ + FFX_MIN16_F2 fPxFrac = FFX_MIN16_F2(ffxFract(fPxSample)); \ + FFX_MIN16_F4 fColorXY = FFX_MIN16_F4(InterpolateSamples(FetchSamples(iPxSample, iTextureSize), fPxFrac)); \ return fColorXY; \ } +#define FFX_FSR2_CONCAT_ID(x, y) x ## y +#define FFX_FSR2_CONCAT(x, y) FFX_FSR2_CONCAT_ID(x, y) +#define FFX_FSR2_SAMPLER_1D_0 Lanczos2 +#define FFX_FSR2_SAMPLER_1D_1 Lanczos2LUT +#define FFX_FSR2_SAMPLER_1D_2 Lanczos2Approx + +#define FFX_FSR2_GET_LANCZOS_SAMPLER1D(x) FFX_FSR2_CONCAT(FFX_FSR2_SAMPLER_1D_, x) + #endif //!defined( FFX_FSR2_SAMPLE_H ) diff --git a/src/ffx-fsr2-api/shaders/ffx_fsr2_upsample.h b/src/ffx-fsr2-api/shaders/ffx_fsr2_upsample.h index 0b83d6a..80524d4 100644 --- a/src/ffx-fsr2-api/shaders/ffx_fsr2_upsample.h +++ b/src/ffx-fsr2-api/shaders/ffx_fsr2_upsample.h @@ -22,54 +22,100 @@ #ifndef FFX_FSR2_UPSAMPLE_H #define FFX_FSR2_UPSAMPLE_H -FfxFloat32 SmoothStep(FfxFloat32 x, FfxFloat32 a, FfxFloat32 b) -{ - x = clamp((x - a) / (b - a), 0.f, 1.f); - return x * x * (3.f - 2.f * x); -} +#define FFX_FSR2_OPTION_GUARANTEE_POSITIVE_UPSAMPLE_WEIGHT 0 FFX_STATIC const FfxUInt32 iLanczos2SampleCount = 16; -void DeringingWithMinMax(UPSAMPLE_F3 fDeringingMin, UPSAMPLE_F3 fDeringingMax, FFX_PARAMETER_INOUT UPSAMPLE_F3 fColor, FFX_PARAMETER_OUT FfxFloat32 fRangeSimilarity) -{ - fRangeSimilarity = fDeringingMin.x / fDeringingMax.x; - fColor = clamp(fColor, fDeringingMin, fDeringingMax); -} - -void Deringing(RectificationBoxData clippingBox, FFX_PARAMETER_INOUT UPSAMPLE_F3 fColor) +void Deringing(RectificationBoxData clippingBox, FFX_PARAMETER_INOUT FfxFloat32x3 fColor) { fColor = clamp(fColor, clippingBox.aabbMin, clippingBox.aabbMax); } - -UPSAMPLE_F GetUpsampleLanczosWeight(UPSAMPLE_F2 fSrcSampleOffset, UPSAMPLE_F2 fKernelWeight) +#if FFX_HALF +void Deringing(RectificationBoxDataMin16 clippingBox, FFX_PARAMETER_INOUT FFX_MIN16_F3 fColor) { - UPSAMPLE_F2 fSrcSampleOffsetBiased = UPSAMPLE_F2(fSrcSampleOffset * fKernelWeight); - UPSAMPLE_F fSampleWeight = Lanczos2ApproxSq(dot(fSrcSampleOffsetBiased, fSrcSampleOffsetBiased)); // TODO: check other distances (l0, l1, linf...) + fColor = clamp(fColor, clippingBox.aabbMin, clippingBox.aabbMax); +} +#endif +#ifndef FFX_FSR2_OPTION_UPSAMPLE_USE_LANCZOS_TYPE +#define FFX_FSR2_OPTION_UPSAMPLE_USE_LANCZOS_TYPE 1 // Approximate +#endif + +FfxFloat32 GetUpsampleLanczosWeight(FfxFloat32x2 fSrcSampleOffset, FfxFloat32x2 fKernelWeight) +{ + FfxFloat32x2 fSrcSampleOffsetBiased = fSrcSampleOffset * fKernelWeight; +#if FFX_FSR2_OPTION_UPSAMPLE_USE_LANCZOS_TYPE == 0 // LANCZOS_TYPE_REFERENCE + FfxFloat32 fSampleWeight = Lanczos2(length(fSrcSampleOffsetBiased)); +#elif FFX_FSR2_OPTION_UPSAMPLE_USE_LANCZOS_TYPE == 1 // LANCZOS_TYPE_LUT + FfxFloat32 fSampleWeight = Lanczos2_UseLUT(length(fSrcSampleOffsetBiased)); +#elif FFX_FSR2_OPTION_UPSAMPLE_USE_LANCZOS_TYPE == 2 // LANCZOS_TYPE_APPROXIMATE + FfxFloat32 fSampleWeight = Lanczos2ApproxSq(dot(fSrcSampleOffsetBiased, fSrcSampleOffsetBiased)); +#else +#error "Invalid Lanczos type" +#endif return fSampleWeight; } -UPSAMPLE_F Pow3(UPSAMPLE_F x) +#if FFX_HALF +FFX_MIN16_F GetUpsampleLanczosWeight(FFX_MIN16_F2 fSrcSampleOffset, FFX_MIN16_F2 fKernelWeight) +{ + FFX_MIN16_F2 fSrcSampleOffsetBiased = fSrcSampleOffset * fKernelWeight; +#if FFX_FSR2_OPTION_UPSAMPLE_USE_LANCZOS_TYPE == 0 // LANCZOS_TYPE_REFERENCE + FFX_MIN16_F fSampleWeight = Lanczos2(length(fSrcSampleOffsetBiased)); +#elif FFX_FSR2_OPTION_UPSAMPLE_USE_LANCZOS_TYPE == 1 // LANCZOS_TYPE_APPROXIMATE + FFX_MIN16_F fSampleWeight = Lanczos2ApproxSq(dot(fSrcSampleOffsetBiased, fSrcSampleOffsetBiased)); +#elif FFX_FSR2_OPTION_UPSAMPLE_USE_LANCZOS_TYPE == 2 // LANCZOS_TYPE_LUT + FFX_MIN16_F fSampleWeight = Lanczos2_UseLUT(length(fSrcSampleOffsetBiased)); + // To Test: Save reciproqual sqrt compute + // FfxFloat32 fSampleWeight = Lanczos2Sq_UseLUT(dot(fSrcSampleOffsetBiased, fSrcSampleOffsetBiased)); +#else +#error "Invalid Lanczos type" +#endif + return fSampleWeight; +} +#endif + +FfxFloat32 Pow3(FfxFloat32 x) { return x * x * x; } -UPSAMPLE_F4 ComputeUpsampledColorAndWeight(FFX_MIN16_I2 iPxHrPos, UPSAMPLE_F2 fKernelWeight, FFX_PARAMETER_INOUT RectificationBoxData clippingBox) +#if FX_HALF +FFX_MIN16_F Pow3(FFX_MIN16_F x) { + return x * x * x; +} +#endif + +FfxFloat32x4 ComputeUpsampledColorAndWeight(FfxInt32x2 iPxHrPos, FfxFloat32x2 fKernelWeight, FFX_PARAMETER_INOUT RectificationBoxData clippingBox) +{ +#if FFX_FSR2_OPTION_UPSAMPLE_SAMPLERS_USE_DATA_HALF && FFX_HALF +#include "ffx_fsr2_force16_begin.h" +#endif // We compute a sliced lanczos filter with 2 lobes (other slices are accumulated temporaly) FfxFloat32x2 fDstOutputPos = FfxFloat32x2(iPxHrPos) + FFX_BROADCAST_FLOAT32X2(0.5f); // Destination resolution output pixel center position FfxFloat32x2 fSrcOutputPos = fDstOutputPos * DownscaleFactor(); // Source resolution output pixel center position FfxInt32x2 iSrcInputPos = FfxInt32x2(floor(fSrcOutputPos)); // TODO: what about weird upscale factors... - UPSAMPLE_F3 fSamples[iLanczos2SampleCount]; - - FfxFloat32x2 fSrcUnjitteredPos = (FfxFloat32x2(iSrcInputPos) + FFX_BROADCAST_FLOAT32X2(0.5f)) - Jitter(); // This is the un-jittered position of the sample at offset 0,0 - - UPSAMPLE_I2 offsetTL; - offsetTL.x = (fSrcUnjitteredPos.x > fSrcOutputPos.x) ? UPSAMPLE_I(-2) : UPSAMPLE_I(-1); - offsetTL.y = (fSrcUnjitteredPos.y > fSrcOutputPos.y) ? UPSAMPLE_I(-2) : UPSAMPLE_I(-1); +#if FFX_FSR2_OPTION_UPSAMPLE_SAMPLERS_USE_DATA_HALF && FFX_HALF +#include "ffx_fsr2_force16_end.h" +#endif +#if FFX_FSR2_OPTION_UPSAMPLE_SAMPLERS_USE_DATA_HALF && FFX_HALF +#include "ffx_fsr2_force16_begin.h" + RectificationBoxMin16 fRectificationBox; +#else RectificationBox fRectificationBox; +#endif + + FfxFloat32x3 fSamples[iLanczos2SampleCount]; + + + FfxFloat32x2 fSrcUnjitteredPos = (FfxFloat32x2(iSrcInputPos) + FfxFloat32x2(0.5f, 0.5f)) - Jitter(); // This is the un-jittered position of the sample at offset 0,0 + + FfxInt32x2 offsetTL; + offsetTL.x = (fSrcUnjitteredPos.x > fSrcOutputPos.x) ? FfxInt32(-2) : FfxInt32(-1); + offsetTL.y = (fSrcUnjitteredPos.y > fSrcOutputPos.y) ? FfxInt32(-2) : FfxInt32(-1); //Load samples // If fSrcUnjitteredPos.y > fSrcOutputPos.y, indicates offsetTL.y = -2, sample offset Y will be [-2, 1], clipbox will be rows [1, 3]. @@ -78,7 +124,7 @@ UPSAMPLE_F4 ComputeUpsampledColorAndWeight(FFX_MIN16_I2 iPxHrPos, UPSAMPLE_F2 fK const FfxBoolean bFlipRow = fSrcUnjitteredPos.y > fSrcOutputPos.y; const FfxBoolean bFlipCol = fSrcUnjitteredPos.x > fSrcOutputPos.x; - UPSAMPLE_F2 fOffsetTL = UPSAMPLE_F2(offsetTL); + FfxFloat32x2 fOffsetTL = FfxFloat32x2(offsetTL); FFX_UNROLL for (FfxInt32 row = 0; row < 4; row++) { @@ -92,57 +138,77 @@ UPSAMPLE_F4 ComputeUpsampledColorAndWeight(FFX_MIN16_I2 iPxHrPos, UPSAMPLE_F2 fK const FfxInt32x2 sampleCoord = ClampLoad(iSrcSamplePos, FfxInt32x2(0, 0), FfxInt32x2(RenderSize())); - fSamples[iSampleIndex] = LoadPreparedInputColor(FFX_MIN16_I2(sampleCoord)); + fSamples[iSampleIndex] = LoadPreparedInputColor(FfxInt32x2(sampleCoord)); } } RectificationBoxReset(fRectificationBox, fSamples[0]); - UPSAMPLE_F3 fColor = UPSAMPLE_F3(0.f, 0.f, 0.f); - UPSAMPLE_F fWeight = UPSAMPLE_F(0.f); - UPSAMPLE_F2 fBaseSampleOffset = UPSAMPLE_F2(fSrcUnjitteredPos - fSrcOutputPos); + FfxFloat32x3 fColor = FfxFloat32x3(0.f, 0.f, 0.f); + FfxFloat32 fWeight = FfxFloat32(0.f); + FfxFloat32x2 fBaseSampleOffset = FfxFloat32x2(fSrcUnjitteredPos - fSrcOutputPos); FFX_UNROLL - for (FfxUInt32 iSampleIndex = 0; iSampleIndex < iLanczos2SampleCount; ++iSampleIndex) - { - FfxInt32 row = FfxInt32(iSampleIndex >> 2); - FfxInt32 col = FfxInt32(iSampleIndex & 3); + for (FfxInt32 row = 0; row < 3; row++) { - const FfxInt32x2 sampleColRow = FfxInt32x2(bFlipCol ? (3 - col) : col, bFlipRow ? (3 - row) : row); - const UPSAMPLE_F2 fOffset = fOffsetTL + UPSAMPLE_F2(sampleColRow); - UPSAMPLE_F2 fSrcSampleOffset = fBaseSampleOffset + fOffset; + FFX_UNROLL + for (FfxInt32 col = 0; col < 3; col++) { + FfxInt32 iSampleIndex = col + (row << 2); - FfxInt32x2 iSrcSamplePos = FfxInt32x2(iSrcInputPos) + FfxInt32x2(offsetTL) + sampleColRow; + const FfxInt32x2 sampleColRow = FfxInt32x2(bFlipCol ? (3 - col) : col, bFlipRow ? (3 - row) : row); + const FfxFloat32x2 fOffset = fOffsetTL + FfxFloat32x2(sampleColRow); + FfxFloat32x2 fSrcSampleOffset = fBaseSampleOffset + fOffset; - UPSAMPLE_F fSampleWeight = UPSAMPLE_F(IsOnScreen(FFX_MIN16_I2(iSrcSamplePos), FFX_MIN16_I2(RenderSize()))) * GetUpsampleLanczosWeight(fSrcSampleOffset, fKernelWeight); + FfxInt32x2 iSrcSamplePos = FfxInt32x2(iSrcInputPos) + FfxInt32x2(offsetTL) + sampleColRow; - // Update rectification box - if(all(FFX_LESS_THAN(FfxInt32x2(col, row), FFX_BROADCAST_INT32X2(3)))) - { - //update clipping box in non-locked areas - const UPSAMPLE_F fSrcSampleOffsetSq = dot(fSrcSampleOffset, fSrcSampleOffset); - UPSAMPLE_F fBoxSampleWeight = UPSAMPLE_F(1) - ffxSaturate(fSrcSampleOffsetSq / UPSAMPLE_F(3)); + FfxFloat32 fSampleWeight = FfxFloat32(IsOnScreen(FfxInt32x2(iSrcSamplePos), FfxInt32x2(RenderSize()))) * GetUpsampleLanczosWeight(fSrcSampleOffset, fKernelWeight); + + // Update rectification box + const FfxFloat32 fSrcSampleOffsetSq = dot(fSrcSampleOffset, fSrcSampleOffset); + FfxFloat32 fBoxSampleWeight = FfxFloat32(1) - ffxSaturate(fSrcSampleOffsetSq / FfxFloat32(3)); fBoxSampleWeight *= fBoxSampleWeight; RectificationBoxAddSample(fRectificationBox, fSamples[iSampleIndex], fBoxSampleWeight); + + fWeight += fSampleWeight; + fColor += fSampleWeight * fSamples[iSampleIndex]; } - - fWeight += fSampleWeight; - fColor += fSampleWeight * fSamples[iSampleIndex]; } - // Normalize for deringing (we need to compare colors) - fColor = fColor / (abs(fWeight) > FSR2_EPSILON ? fWeight : UPSAMPLE_F(1.f)); + fColor = fColor / (abs(fWeight) > FSR2_EPSILON ? fWeight : FfxFloat32(1.f)); RectificationBoxComputeVarianceBoxData(fRectificationBox); - clippingBox = RectificationBoxGetData(fRectificationBox); +#if FFX_FSR2_OPTION_UPSAMPLE_SAMPLERS_USE_DATA_HALF && FFX_HALF + RectificationBoxDataMin16 rectificationData = RectificationBoxGetData(fRectificationBox); + clippingBox.aabbMax = rectificationData.aabbMax; + clippingBox.aabbMin = rectificationData.aabbMin; + clippingBox.boxCenter = rectificationData.boxCenter; + clippingBox.boxVec = rectificationData.boxVec; +#else + RectificationBoxData rectificationData = RectificationBoxGetData(fRectificationBox); + clippingBox = rectificationData; +#endif - Deringing(RectificationBoxGetData(fRectificationBox), fColor); + Deringing(rectificationData, fColor); - if (any(FFX_LESS_THAN(fKernelWeight, UPSAMPLE_F2_BROADCAST(1.0f)))) { - fWeight = UPSAMPLE_F(averageLanczosWeightPerFrame); +#if FFX_FSR2_OPTION_UPSAMPLE_SAMPLERS_USE_DATA_HALF && FFX_HALF + clippingBox.aabbMax = rectificationData.aabbMax; + clippingBox.aabbMin = rectificationData.aabbMin; + clippingBox.boxCenter = rectificationData.boxCenter; + clippingBox.boxVec = rectificationData.boxVec; +#endif + + if (any(FFX_LESS_THAN(fKernelWeight, FfxFloat32x2(1, 1)))) { + fWeight = FfxFloat32(averageLanczosWeightPerFrame); } +#if FFX_FSR2_OPTION_UPSAMPLE_SAMPLERS_USE_DATA_HALF && FFX_HALF +#include "ffx_fsr2_force16_end.h" +#endif - return UPSAMPLE_F4(fColor, ffxMax(UPSAMPLE_F(0), fWeight)); +#if FFX_FSR2_OPTION_GUARANTEE_POSITIVE_UPSAMPLE_WEIGHT + return FfxFloat32x4(fColor, ffxMax(FfxFloat32(FSR2_EPSILON), fWeight)); +#else + return FfxFloat32x4(fColor, ffxMax(FfxFloat32(0), fWeight)); +#endif } #endif //!defined( FFX_FSR2_UPSAMPLE_H ) diff --git a/src/ffx-fsr2-api/vk/CMakeLists.txt b/src/ffx-fsr2-api/vk/CMakeLists.txt index 23aec21..933d097 100644 --- a/src/ffx-fsr2-api/vk/CMakeLists.txt +++ b/src/ffx-fsr2-api/vk/CMakeLists.txt @@ -107,4 +107,4 @@ add_custom_target(shader_permutations_vk DEPENDS ${PERMUTATION_OUTPUTS}) add_dependencies(${FFX_SC_DEPENDENT_TARGET} shader_permutations_vk) source_group("source" FILES ${VK}) -source_group("shaders" FILES ${SHADERS}) +source_group("shaders" FILES ${SHADERS}) \ No newline at end of file diff --git a/src/ffx-fsr2-api/vk/ffx_fsr2_vk.cpp b/src/ffx-fsr2-api/vk/ffx_fsr2_vk.cpp index f984d86..867da60 100644 --- a/src/ffx-fsr2-api/vk/ffx_fsr2_vk.cpp +++ b/src/ffx-fsr2-api/vk/ffx_fsr2_vk.cpp @@ -26,11 +26,12 @@ #include #include #include +#include // prototypes for functions in the interface FfxErrorCode GetDeviceCapabilitiesVK(FfxFsr2Interface* backendInterface, FfxDeviceCapabilities* deviceCapabilities, FfxDevice device); -FfxErrorCode CreateDeviceVK(FfxFsr2Interface* backendInterface, FfxDevice device); -FfxErrorCode DestroyDeviceVK(FfxFsr2Interface* backendInterface, FfxDevice device); +FfxErrorCode CreateBackendContextVK(FfxFsr2Interface* backendInterface, FfxDevice device); +FfxErrorCode DestroyBackendContextVK(FfxFsr2Interface* backendInterface); FfxErrorCode CreateResourceVK(FfxFsr2Interface* backendInterface, const FfxCreateResourceDescription* desc, FfxResourceInternal* outResource); FfxErrorCode RegisterResourceVK(FfxFsr2Interface* backendInterface, const FfxResource* inResource, FfxResourceInternal* outResourceInternal); FfxErrorCode UnregisterResourcesVK(FfxFsr2Interface* backendInterface); @@ -38,14 +39,14 @@ FfxResourceDescription GetResourceDescriptorVK(FfxFsr2Interface* backendInterfac FfxErrorCode DestroyResourceVK(FfxFsr2Interface* backendInterface, FfxResourceInternal resource); FfxErrorCode CreatePipelineVK(FfxFsr2Interface* backendInterface, FfxFsr2Pass passId, const FfxPipelineDescription* desc, FfxPipelineState* outPass); FfxErrorCode DestroyPipelineVK(FfxFsr2Interface* backendInterface, FfxPipelineState* pipeline); -FfxErrorCode ScheduleRenderJobVK(FfxFsr2Interface* backendInterface, const FfxRenderJobDescription* job); -FfxErrorCode ExecuteRenderJobsVK(FfxFsr2Interface* backendInterface, FfxCommandList commandList); +FfxErrorCode ScheduleGpuJobVK(FfxFsr2Interface* backendInterface, const FfxGpuJobDescription* job); +FfxErrorCode ExecuteGpuJobsVK(FfxFsr2Interface* backendInterface, FfxCommandList commandList); #define FSR2_MAX_QUEUED_FRAMES ( 4) #define FSR2_MAX_RESOURCE_COUNT (64) #define FSR2_MAX_STAGING_RESOURCE_COUNT ( 8) #define FSR2_MAX_BARRIERS (16) -#define FSR2_MAX_RENDERJOBS (32) +#define FSR2_MAX_GPU_JOBS (32) #define FSR2_MAX_IMAGE_COPY_MIPS (32) #define FSR2_MAX_SAMPLERS ( 2) #define FSR2_MAX_UNIFORM_BUFFERS ( 4) @@ -135,8 +136,8 @@ typedef struct BackendContext_VK { VkDevice device = nullptr; VkFunctionTable vkFunctionTable = {}; - uint32_t renderJobCount = 0; - FfxRenderJobDescription renderJobs[FSR2_MAX_RENDERJOBS] = {}; + uint32_t gpuJobCount = 0; + FfxGpuJobDescription gpuJobs[FSR2_MAX_GPU_JOBS] = {}; uint32_t nextStaticResource = 0; uint32_t nextDynamicResource = 0; @@ -197,8 +198,8 @@ FfxErrorCode ffxFsr2GetInterfaceVK( FFX_ERROR_INSUFFICIENT_MEMORY); outInterface->fpGetDeviceCapabilities = GetDeviceCapabilitiesVK; - outInterface->fpCreateDevice = CreateDeviceVK; - outInterface->fpDestroyDevice = DestroyDeviceVK; + outInterface->fpCreateBackendContext = CreateBackendContextVK; + outInterface->fpDestroyBackendContext = DestroyBackendContextVK; outInterface->fpCreateResource = CreateResourceVK; outInterface->fpRegisterResource = RegisterResourceVK; outInterface->fpUnregisterResources = UnregisterResourcesVK; @@ -206,8 +207,8 @@ FfxErrorCode ffxFsr2GetInterfaceVK( outInterface->fpDestroyResource = DestroyResourceVK; outInterface->fpCreatePipeline = CreatePipelineVK; outInterface->fpDestroyPipeline = DestroyPipelineVK; - outInterface->fpScheduleRenderJob = ScheduleRenderJobVK; - outInterface->fpExecuteRenderJobs = ExecuteRenderJobsVK; + outInterface->fpScheduleGpuJob = ScheduleGpuJobVK; + outInterface->fpExecuteGpuJobs = ExecuteGpuJobsVK; outInterface->scratchBuffer = scratchBuffer; outInterface->scratchBufferSize = scratchBufferSize; @@ -305,6 +306,8 @@ VkFormat getVKFormatFromSurfaceFormat(FfxSurfaceFormat fmt) return VK_FORMAT_R16_SNORM; case(FFX_SURFACE_FORMAT_R8_UNORM): return VK_FORMAT_R8_UNORM; + case(FFX_SURFACE_FORMAT_R8G8_UNORM): + return VK_FORMAT_R8G8_UNORM; case(FFX_SURFACE_FORMAT_R32_FLOAT): return VK_FORMAT_R32_SFLOAT; default: @@ -749,7 +752,7 @@ FfxErrorCode GetDeviceCapabilitiesVK(FfxFsr2Interface* backendInterface, FfxDevi return FFX_OK; } -FfxErrorCode CreateDeviceVK(FfxFsr2Interface* backendInterface, FfxDevice device) +FfxErrorCode CreateBackendContextVK(FfxFsr2Interface* backendInterface, FfxDevice device) { FFX_ASSERT(NULL != backendInterface); @@ -926,7 +929,7 @@ FfxErrorCode CreateDeviceVK(FfxFsr2Interface* backendInterface, FfxDevice device } } - backendContext->renderJobCount = 0; + backendContext->gpuJobCount = 0; backendContext->scheduledImageBarrierCount = 0; backendContext->scheduledBufferBarrierCount = 0; backendContext->stagingResourceCount = 0; @@ -938,7 +941,7 @@ FfxErrorCode CreateDeviceVK(FfxFsr2Interface* backendInterface, FfxDevice device return FFX_OK; } -FfxErrorCode DestroyDeviceVK(FfxFsr2Interface* backendInterface, FfxDevice device) +FfxErrorCode DestroyBackendContextVK(FfxFsr2Interface* backendInterface) { FFX_ASSERT(NULL != backendInterface); @@ -973,8 +976,7 @@ FfxErrorCode DestroyDeviceVK(FfxFsr2Interface* backendInterface, FfxDevice devic backendContext->pointSampler = nullptr; backendContext->linearSampler = nullptr; - VkDevice vkDevice = reinterpret_cast(device); - if (vkDevice != nullptr) { + if (backendContext->device != nullptr) { backendContext->device = nullptr; } @@ -992,8 +994,8 @@ FfxErrorCode CreateResourceVK( FFX_ASSERT(NULL != createResourceDescription); FFX_ASSERT(NULL != outResource); - VkDevice vkDevice = reinterpret_cast(createResourceDescription->device); BackendContext_VK* backendContext = (BackendContext_VK*)backendInterface->scratchBuffer; + VkDevice vkDevice = reinterpret_cast(backendContext->device); FFX_ASSERT(backendContext->nextStaticResource + 1 < backendContext->nextDynamicResource); outResource->internalIndex = backendContext->nextStaticResource++; @@ -1196,14 +1198,14 @@ FfxErrorCode CreateResourceVK( backendInterface->fpCreateResource(backendInterface, &uploadDesc, ©Src); // setup the upload job - FfxRenderJobDescription copyJob = + FfxGpuJobDescription copyJob = { - FFX_RENDER_JOB_COPY + FFX_GPU_JOB_COPY }; copyJob.copyJobDescriptor.src = copySrc; copyJob.copyJobDescriptor.dst = *outResource; - backendInterface->fpScheduleRenderJob(backendInterface, ©Job); + backendInterface->fpScheduleGpuJob(backendInterface, ©Job); // add to the list of staging resources to delete later uint32_t stagingResIdx = backendContext->stagingResourceCount++; @@ -1277,12 +1279,11 @@ FfxErrorCode CreatePipelineVK(FfxFsr2Interface* backendInterface, FfxFsr2Pass pa flags |= (pipelineDescription->contextFlags & FFX_FSR2_ENABLE_MOTION_VECTORS_JITTER_CANCELLATION) ? FSR2_SHADER_PERMUTATION_JITTER_MOTION_VECTORS : 0; flags |= (pipelineDescription->contextFlags & FFX_FSR2_ENABLE_DEPTH_INVERTED) ? FSR2_SHADER_PERMUTATION_DEPTH_INVERTED : 0; flags |= (pass == FFX_FSR2_PASS_ACCUMULATE_SHARPEN) ? FSR2_SHADER_PERMUTATION_ENABLE_SHARPENING : 0; - flags |= (useLut) ? FSR2_SHADER_PERMUTATION_LANCZOS_LUT : 0; + flags |= (useLut) ? FSR2_SHADER_PERMUTATION_REPROJECT_USE_LANCZOS_TYPE : 0; flags |= (canForceWave64) ? FSR2_SHADER_PERMUTATION_FORCE_WAVE64 : 0; - flags |= (supportedFP16) ? FSR2_SHADER_PERMUTATION_ALLOW_FP16 : 0; + flags |= (supportedFP16 && (pass != FFX_FSR2_PASS_RCAS)) ? FSR2_SHADER_PERMUTATION_ALLOW_FP16 : 0; - Fsr2ShaderBlobVK shaderBlob = {}; - fsr2GetPermutationBlobByIndex(pass, flags, &shaderBlob); + const Fsr2ShaderBlobVK shaderBlob = fsr2GetPermutationBlobByIndex(pass, flags); FFX_ASSERT(shaderBlob.data && shaderBlob.size); // populate the pass. @@ -1292,21 +1293,22 @@ FfxErrorCode CreatePipelineVK(FfxFsr2Interface* backendInterface, FfxFsr2Pass pa FFX_ASSERT(shaderBlob.storageImageCount < FFX_MAX_NUM_UAVS); FFX_ASSERT(shaderBlob.sampledImageCount < FFX_MAX_NUM_SRVS); + std::wstring_convert> converter; for (uint32_t srvIndex = 0; srvIndex < outPipeline->srvCount; ++srvIndex) { outPipeline->srvResourceBindings[srvIndex].slotIndex = shaderBlob.boundSampledImageBindings[srvIndex]; - strcpy_s(outPipeline->srvResourceBindings[srvIndex].name, shaderBlob.boundSampledImageNames[srvIndex]); + wcscpy_s(outPipeline->srvResourceBindings[srvIndex].name, converter.from_bytes(shaderBlob.boundSampledImageNames[srvIndex]).c_str()); } for (uint32_t uavIndex = 0; uavIndex < outPipeline->uavCount; ++uavIndex) { outPipeline->uavResourceBindings[uavIndex].slotIndex = shaderBlob.boundStorageImageBindings[uavIndex]; - strcpy_s(outPipeline->uavResourceBindings[uavIndex].name, shaderBlob.boundStorageImageNames[uavIndex]); + wcscpy_s(outPipeline->uavResourceBindings[uavIndex].name, converter.from_bytes(shaderBlob.boundStorageImageNames[uavIndex]).c_str()); } for (uint32_t cbIndex = 0; cbIndex < outPipeline->constCount; ++cbIndex) { outPipeline->cbResourceBindings[cbIndex].slotIndex = shaderBlob.boundUniformBufferBindings[cbIndex]; - strcpy_s(outPipeline->cbResourceBindings[cbIndex].name, shaderBlob.boundUniformBufferNames[cbIndex]); + wcscpy_s(outPipeline->cbResourceBindings[cbIndex].name, converter.from_bytes(shaderBlob.boundUniformBufferNames[cbIndex]).c_str()); } // create descriptor set layout @@ -1429,21 +1431,21 @@ FfxErrorCode CreatePipelineVK(FfxFsr2Interface* backendInterface, FfxFsr2Pass pa return FFX_OK; } -FfxErrorCode ScheduleRenderJobVK(FfxFsr2Interface* backendInterface, const FfxRenderJobDescription* job) +FfxErrorCode ScheduleGpuJobVK(FfxFsr2Interface* backendInterface, const FfxGpuJobDescription* job) { FFX_ASSERT(NULL != backendInterface); FFX_ASSERT(NULL != job); BackendContext_VK* backendContext = (BackendContext_VK*)backendInterface->scratchBuffer; - FFX_ASSERT(backendContext->renderJobCount < FSR2_MAX_RENDERJOBS); + FFX_ASSERT(backendContext->gpuJobCount < FSR2_MAX_GPU_JOBS); - backendContext->renderJobs[backendContext->renderJobCount] = *job; + backendContext->gpuJobs[backendContext->gpuJobCount] = *job; - if (job->jobType == FFX_RENDER_JOB_COMPUTE) { + if (job->jobType == FFX_GPU_JOB_COMPUTE) { // needs to copy SRVs and UAVs in case they are on the stack only - FfxComputeJobDescription* computeJob = &backendContext->renderJobs[backendContext->renderJobCount].computeJobDescriptor; + FfxComputeJobDescription* computeJob = &backendContext->gpuJobs[backendContext->gpuJobCount].computeJobDescriptor; const uint32_t numConstBuffers = job->computeJobDescriptor.pipeline.constCount; for (uint32_t currentRootConstantIndex = 0; currentRootConstantIndex < numConstBuffers; ++currentRootConstantIndex) { @@ -1452,7 +1454,7 @@ FfxErrorCode ScheduleRenderJobVK(FfxFsr2Interface* backendInterface, const FfxRe } } - backendContext->renderJobCount++; + backendContext->gpuJobCount++; return FFX_OK; } @@ -1540,7 +1542,7 @@ void flushBarriers(BackendContext_VK* backendContext, VkCommandBuffer vkCommandB } } -static FfxErrorCode executeRenderJobCompute(BackendContext_VK* backendContext, FfxRenderJobDescription* job, VkCommandBuffer vkCommandBuffer) +static FfxErrorCode executeGpuJobCompute(BackendContext_VK* backendContext, FfxGpuJobDescription* job, VkCommandBuffer vkCommandBuffer) { uint32_t imageInfoIndex = 0; uint32_t bufferInfoIndex = 0; @@ -1646,7 +1648,7 @@ static FfxErrorCode executeRenderJobCompute(BackendContext_VK* backendContext, F return FFX_OK; } -static FfxErrorCode executeRenderJobCopy(BackendContext_VK* backendContext, FfxRenderJobDescription* job, VkCommandBuffer vkCommandBuffer) +static FfxErrorCode executeGpuJobCopy(BackendContext_VK* backendContext, FfxGpuJobDescription* job, VkCommandBuffer vkCommandBuffer) { BackendContext_VK::Resource ffxResourceSrc = backendContext->resources[job->copyJobDescriptor.src.internalIndex]; BackendContext_VK::Resource ffxResourceDst = backendContext->resources[job->copyJobDescriptor.dst.internalIndex]; @@ -1745,7 +1747,7 @@ static FfxErrorCode executeRenderJobCopy(BackendContext_VK* backendContext, FfxR return FFX_OK; } -static FfxErrorCode executeRenderJobClearFloat(BackendContext_VK* backendContext, FfxRenderJobDescription* job, VkCommandBuffer vkCommandBuffer) +static FfxErrorCode executeGpuJobClearFloat(BackendContext_VK* backendContext, FfxGpuJobDescription* job, VkCommandBuffer vkCommandBuffer) { uint32_t idx = job->clearJobDescriptor.target.internalIndex; BackendContext_VK::Resource ffxResource = backendContext->resources[idx]; @@ -1777,7 +1779,7 @@ static FfxErrorCode executeRenderJobClearFloat(BackendContext_VK* backendContext return FFX_OK; } -FfxErrorCode ExecuteRenderJobsVK(FfxFsr2Interface* backendInterface, FfxCommandList commandList) +FfxErrorCode ExecuteGpuJobsVK(FfxFsr2Interface* backendInterface, FfxCommandList commandList) { FFX_ASSERT(NULL != backendInterface); @@ -1786,26 +1788,26 @@ FfxErrorCode ExecuteRenderJobsVK(FfxFsr2Interface* backendInterface, FfxCommandL FfxErrorCode errorCode = FFX_OK; // execute all renderjobs - for (uint32_t i = 0; i < backendContext->renderJobCount; ++i) + for (uint32_t i = 0; i < backendContext->gpuJobCount; ++i) { - FfxRenderJobDescription* renderJob = &backendContext->renderJobs[i]; + FfxGpuJobDescription* gpuJob = &backendContext->gpuJobs[i]; VkCommandBuffer vkCommandBuffer = reinterpret_cast(commandList); - switch (renderJob->jobType) + switch (gpuJob->jobType) { - case FFX_RENDER_JOB_CLEAR_FLOAT: + case FFX_GPU_JOB_CLEAR_FLOAT: { - errorCode = executeRenderJobClearFloat(backendContext, renderJob, vkCommandBuffer); + errorCode = executeGpuJobClearFloat(backendContext, gpuJob, vkCommandBuffer); break; } - case FFX_RENDER_JOB_COPY: + case FFX_GPU_JOB_COPY: { - errorCode = executeRenderJobCopy(backendContext, renderJob, vkCommandBuffer); + errorCode = executeGpuJobCopy(backendContext, gpuJob, vkCommandBuffer); break; } - case FFX_RENDER_JOB_COMPUTE: + case FFX_GPU_JOB_COMPUTE: { - errorCode = executeRenderJobCompute(backendContext, renderJob, vkCommandBuffer); + errorCode = executeGpuJobCompute(backendContext, gpuJob, vkCommandBuffer); break; } default:; @@ -1817,7 +1819,7 @@ FfxErrorCode ExecuteRenderJobsVK(FfxFsr2Interface* backendInterface, FfxCommandL errorCode == FFX_OK, FFX_ERROR_BACKEND_API_ERROR); - backendContext->renderJobCount = 0; + backendContext->gpuJobCount = 0; return FFX_OK; } @@ -1911,4 +1913,4 @@ FfxErrorCode DestroyPipelineVK(FfxFsr2Interface* backendInterface, FfxPipelineSt } return FFX_OK; -} +} \ No newline at end of file diff --git a/src/ffx-fsr2-api/vk/ffx_fsr2_vk.h b/src/ffx-fsr2-api/vk/ffx_fsr2_vk.h index c352c98..e0e226a 100644 --- a/src/ffx-fsr2-api/vk/ffx_fsr2_vk.h +++ b/src/ffx-fsr2-api/vk/ffx_fsr2_vk.h @@ -19,7 +19,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. - // @defgroup VK #pragma once @@ -156,4 +155,4 @@ extern "C" { #if defined(__cplusplus) } -#endif // #if defined(__cplusplus) +#endif // #if defined(__cplusplus) \ No newline at end of file diff --git a/src/ffx-fsr2-api/vk/shaders/ffx_fsr2_shaders_vk.cpp b/src/ffx-fsr2-api/vk/shaders/ffx_fsr2_shaders_vk.cpp index 3ecdea1..230ae9b 100644 --- a/src/ffx-fsr2-api/vk/shaders/ffx_fsr2_shaders_vk.cpp +++ b/src/ffx-fsr2-api/vk/shaders/ffx_fsr2_shaders_vk.cpp @@ -29,14 +29,13 @@ #include "ffx_fsr2_prepare_input_color_pass_permutations.h" #include "ffx_fsr2_reconstruct_previous_depth_pass_permutations.h" #include "ffx_fsr2_rcas_pass_permutations.h" -#include #if defined(POPULATE_PERMUTATION_KEY) #undef POPULATE_PERMUTATION_KEY #endif // #if defined(POPULATE_PERMUTATION_KEY) #define POPULATE_PERMUTATION_KEY(options, key) \ key.index = 0; \ -key.FFX_FSR2_OPTION_USE_LANCZOS_LUT = FFX_CONTAINS_FLAG(options, FSR2_SHADER_PERMUTATION_LANCZOS_LUT); \ +key.FFX_FSR2_OPTION_REPROJECT_USE_LANCZOS_TYPE = FFX_CONTAINS_FLAG(options, FSR2_SHADER_PERMUTATION_REPROJECT_USE_LANCZOS_TYPE); \ key.FFX_FSR2_OPTION_HDR_COLOR_INPUT = FFX_CONTAINS_FLAG(options, FSR2_SHADER_PERMUTATION_HDR_COLOR_INPUT); \ key.FFX_FSR2_OPTION_LOW_RESOLUTION_MOTION_VECTORS = FFX_CONTAINS_FLAG(options, FSR2_SHADER_PERMUTATION_LOW_RES_MOTION_VECTORS); \ key.FFX_FSR2_OPTION_JITTERED_MOTION_VECTORS = FFX_CONTAINS_FLAG(options, FSR2_SHADER_PERMUTATION_JITTER_MOTION_VECTORS); \ @@ -114,7 +113,7 @@ Fsr2ShaderBlobVK fsr2GetComputeLuminancePyramidPassPermutationBlobByIndex(uint32 ffx_fsr2_compute_luminance_pyramid_pass_PermutationKey key; key.index = 0; - key.FFX_FSR2_OPTION_USE_LANCZOS_LUT = FFX_CONTAINS_FLAG(permutationOptions, FSR2_SHADER_PERMUTATION_LANCZOS_LUT); + key.FFX_FSR2_OPTION_REPROJECT_USE_LANCZOS_TYPE = FFX_CONTAINS_FLAG(permutationOptions, FSR2_SHADER_PERMUTATION_REPROJECT_USE_LANCZOS_TYPE); key.FFX_FSR2_OPTION_HDR_COLOR_INPUT = FFX_CONTAINS_FLAG(permutationOptions, FSR2_SHADER_PERMUTATION_HDR_COLOR_INPUT); key.FFX_FSR2_OPTION_LOW_RESOLUTION_MOTION_VECTORS = FFX_CONTAINS_FLAG(permutationOptions, FSR2_SHADER_PERMUTATION_LOW_RES_MOTION_VECTORS); key.FFX_FSR2_OPTION_JITTERED_MOTION_VECTORS = FFX_CONTAINS_FLAG(permutationOptions, FSR2_SHADER_PERMUTATION_JITTER_MOTION_VECTORS); @@ -135,73 +134,33 @@ Fsr2ShaderBlobVK fsr2GetAutogenReactivePassPermutationBlobByIndex(uint32_t permu return POPULATE_SHADER_BLOB(g_ffx_fsr2_autogen_reactive_pass_PermutationInfo, tableIndex); } -FfxErrorCode fsr2GetPermutationBlobByIndex(FfxFsr2Pass passId, uint32_t permutationOptions, Fsr2ShaderBlobVK* outBlob) +Fsr2ShaderBlobVK fsr2GetPermutationBlobByIndex(FfxFsr2Pass passId, uint32_t permutationOptions) { switch (passId) { case FFX_FSR2_PASS_PREPARE_INPUT_COLOR: - { - Fsr2ShaderBlobVK blob = fsr2GetPrepareInputColorPassPermutationBlobByIndex(permutationOptions); - memcpy(outBlob, &blob, sizeof(Fsr2ShaderBlobVK)); - return FFX_OK; - } - + return fsr2GetPrepareInputColorPassPermutationBlobByIndex(permutationOptions); case FFX_FSR2_PASS_DEPTH_CLIP: - { - Fsr2ShaderBlobVK blob = fsr2GetDepthClipPassPermutationBlobByIndex(permutationOptions); - memcpy(outBlob, &blob, sizeof(Fsr2ShaderBlobVK)); - return FFX_OK; - } - + return fsr2GetDepthClipPassPermutationBlobByIndex(permutationOptions); case FFX_FSR2_PASS_RECONSTRUCT_PREVIOUS_DEPTH: - { - Fsr2ShaderBlobVK blob = fsr2GetReconstructPreviousDepthPassPermutationBlobByIndex(permutationOptions); - memcpy(outBlob, &blob, sizeof(Fsr2ShaderBlobVK)); - return FFX_OK; - } - + return fsr2GetReconstructPreviousDepthPassPermutationBlobByIndex(permutationOptions); case FFX_FSR2_PASS_LOCK: - { - Fsr2ShaderBlobVK blob = fsr2GetLockPassPermutationBlobByIndex(permutationOptions); - memcpy(outBlob, &blob, sizeof(Fsr2ShaderBlobVK)); - return FFX_OK; - } - + return fsr2GetLockPassPermutationBlobByIndex(permutationOptions); case FFX_FSR2_PASS_ACCUMULATE: case FFX_FSR2_PASS_ACCUMULATE_SHARPEN: - { - Fsr2ShaderBlobVK blob = fsr2GetAccumulatePassPermutationBlobByIndex(permutationOptions); - memcpy(outBlob, &blob, sizeof(Fsr2ShaderBlobVK)); - return FFX_OK; - } - + return fsr2GetAccumulatePassPermutationBlobByIndex(permutationOptions); case FFX_FSR2_PASS_RCAS: - { - Fsr2ShaderBlobVK blob = fsr2GetRCASPassPermutationBlobByIndex(permutationOptions); - memcpy(outBlob, &blob, sizeof(Fsr2ShaderBlobVK)); - return FFX_OK; - } - + return fsr2GetRCASPassPermutationBlobByIndex(permutationOptions); case FFX_FSR2_PASS_COMPUTE_LUMINANCE_PYRAMID: - { - Fsr2ShaderBlobVK blob = fsr2GetComputeLuminancePyramidPassPermutationBlobByIndex(permutationOptions); - memcpy(outBlob, &blob, sizeof(Fsr2ShaderBlobVK)); - return FFX_OK; - } - + return fsr2GetComputeLuminancePyramidPassPermutationBlobByIndex(permutationOptions); case FFX_FSR2_PASS_GENERATE_REACTIVE: - { - Fsr2ShaderBlobVK blob = fsr2GetAutogenReactivePassPermutationBlobByIndex(permutationOptions); - memcpy(outBlob, &blob, sizeof(Fsr2ShaderBlobVK)); - return FFX_OK; - } - + return fsr2GetAutogenReactivePassPermutationBlobByIndex(permutationOptions); default: FFX_ASSERT_FAIL("Should never reach here."); break; } // return an empty blob - memset(outBlob, 0, sizeof(Fsr2ShaderBlobVK)); - return FFX_OK; -} + Fsr2ShaderBlobVK emptyBlob = {}; + return emptyBlob; +} \ No newline at end of file diff --git a/src/ffx-fsr2-api/vk/shaders/ffx_fsr2_shaders_vk.h b/src/ffx-fsr2-api/vk/shaders/ffx_fsr2_shaders_vk.h index 9cadc31..da581c7 100644 --- a/src/ffx-fsr2-api/vk/shaders/ffx_fsr2_shaders_vk.h +++ b/src/ffx-fsr2-api/vk/shaders/ffx_fsr2_shaders_vk.h @@ -28,37 +28,37 @@ extern "C" { #endif // #if defined(__cplusplus) -// A single shader blob and a description of its resources. -typedef struct Fsr2ShaderBlobVK { + // A single shader blob and a description of its resources. + typedef struct Fsr2ShaderBlobVK { - const uint8_t* data; // A pointer to the blob - const uint32_t size; // Size in bytes. - const uint32_t storageImageCount; // Number of storage images. - const uint32_t sampledImageCount; // Number of sampled images. - const uint32_t uniformBufferCount; // Number of uniform buffers. - const char** boundStorageImageNames; - const uint32_t* boundStorageImageBindings; // Pointer to an array of bound UAV resources. - const char** boundSampledImageNames; - const uint32_t* boundSampledImageBindings; // Pointer to an array of bound SRV resources. - const char** boundUniformBufferNames; - const uint32_t* boundUniformBufferBindings; // Pointer to an array of bound ConstantBuffers. -} Fsr2ShaderBlobVK; + const uint8_t* data; // A pointer to the blob + const uint32_t size; // Size in bytes. + const uint32_t storageImageCount; // Number of storage images. + const uint32_t sampledImageCount; // Number of sampled images. + const uint32_t uniformBufferCount; // Number of uniform buffers. + const char** boundStorageImageNames; + const uint32_t* boundStorageImageBindings; // Pointer to an array of bound UAV resources. + const char** boundSampledImageNames; + const uint32_t* boundSampledImageBindings; // Pointer to an array of bound SRV resources. + const char** boundUniformBufferNames; + const uint32_t* boundUniformBufferBindings; // Pointer to an array of bound ConstantBuffers. + } Fsr2ShaderBlobVK; -// The different options which contribute to permutations. -typedef enum Fs2ShaderPermutationOptionsVK { + // The different options which contribute to permutations. + typedef enum Fs2ShaderPermutationOptionsVK { - FSR2_SHADER_PERMUTATION_LANCZOS_LUT = (1 << 0), // FFX_FSR2_OPTION_USE_LANCZOS_LUT - FSR2_SHADER_PERMUTATION_HDR_COLOR_INPUT = (1 << 1), // FFX_FSR2_OPTION_HDR_COLOR_INPUT - FSR2_SHADER_PERMUTATION_LOW_RES_MOTION_VECTORS = (1 << 2), // FFX_FSR2_OPTION_LOW_RESOLUTION_MOTION_VECTORS - FSR2_SHADER_PERMUTATION_JITTER_MOTION_VECTORS = (1 << 3), // FFX_FSR2_OPTION_JITTERED_MOTION_VECTORS - FSR2_SHADER_PERMUTATION_DEPTH_INVERTED = (1 << 4), // FFX_FSR2_OPTION_INVERTED_DEPTH - FSR2_SHADER_PERMUTATION_ENABLE_SHARPENING = (1 << 5), // FFX_FSR2_OPTION_APPLY_SHARPENING - FSR2_SHADER_PERMUTATION_FORCE_WAVE64 = (1 << 6), // doesn't map to a define, selects different table - FSR2_SHADER_PERMUTATION_ALLOW_FP16 = (1 << 7), // FFX_USE_16BIT -} Fs2ShaderPermutationOptionsVK; + FSR2_SHADER_PERMUTATION_REPROJECT_USE_LANCZOS_TYPE = (1 << 0), // FFX_FSR2_OPTION_REPROJECT_USE_LANCZOS_TYPE + FSR2_SHADER_PERMUTATION_HDR_COLOR_INPUT = (1 << 1), // FFX_FSR2_OPTION_HDR_COLOR_INPUT + FSR2_SHADER_PERMUTATION_LOW_RES_MOTION_VECTORS = (1 << 2), // FFX_FSR2_OPTION_LOW_RESOLUTION_MOTION_VECTORS + FSR2_SHADER_PERMUTATION_JITTER_MOTION_VECTORS = (1 << 3), // FFX_FSR2_OPTION_JITTERED_MOTION_VECTORS + FSR2_SHADER_PERMUTATION_DEPTH_INVERTED = (1 << 4), // FFX_FSR2_OPTION_INVERTED_DEPTH + FSR2_SHADER_PERMUTATION_ENABLE_SHARPENING = (1 << 5), // FFX_FSR2_OPTION_APPLY_SHARPENING + FSR2_SHADER_PERMUTATION_FORCE_WAVE64 = (1 << 6), // doesn't map to a define, selects different table + FSR2_SHADER_PERMUTATION_ALLOW_FP16 = (1 << 7), // FFX_USE_16BIT + } Fs2ShaderPermutationOptionsVK; -// Get a VK shader blob for the specified pass and permutation index. -FfxErrorCode fsr2GetPermutationBlobByIndex(FfxFsr2Pass passId, uint32_t permutationOptions, Fsr2ShaderBlobVK* outBlob); + // Get a VK shader blob for the specified pass and permutation index. + Fsr2ShaderBlobVK fsr2GetPermutationBlobByIndex(FfxFsr2Pass passId, uint32_t permutationOptions); #if defined(__cplusplus) } diff --git a/src/ffx-parallelsort/FFX_ParallelSort.h b/src/ffx-parallelsort/FFX_ParallelSort.h new file mode 100644 index 0000000..fc1ce06 --- /dev/null +++ b/src/ffx-parallelsort/FFX_ParallelSort.h @@ -0,0 +1,514 @@ +// FFX_ParallelSort.h +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#define FFX_PARALLELSORT_SORT_BITS_PER_PASS 4 +#define FFX_PARALLELSORT_SORT_BIN_COUNT (1 << FFX_PARALLELSORT_SORT_BITS_PER_PASS) +#define FFX_PARALLELSORT_ELEMENTS_PER_THREAD 4 +#define FFX_PARALLELSORT_THREADGROUP_SIZE 128 + +////////////////////////////////////////////////////////////////////////// +// ParallelSort constant buffer parameters: +// +// NumKeys The number of keys to sort +// Shift How many bits to shift for this sort pass (we sort 4 bits at a time) +// NumBlocksPerThreadGroup How many blocks of keys each thread group needs to process +// NumThreadGroups How many thread groups are being run concurrently for sort +// NumThreadGroupsWithAdditionalBlocks How many thread groups need to process additional block data +// NumReduceThreadgroupPerBin How many thread groups are summed together for each reduced bin entry +// NumScanValues How many values to perform scan prefix (+ add) on +////////////////////////////////////////////////////////////////////////// + +#ifdef FFX_CPP + struct FFX_ParallelSortCB + { + uint32_t NumKeys; + int32_t NumBlocksPerThreadGroup; + uint32_t NumThreadGroups; + uint32_t NumThreadGroupsWithAdditionalBlocks; + uint32_t NumReduceThreadgroupPerBin; + uint32_t NumScanValues; + }; + + void FFX_ParallelSort_CalculateScratchResourceSize(uint32_t MaxNumKeys, uint32_t& ScratchBufferSize, uint32_t& ReduceScratchBufferSize) + { + uint32_t BlockSize = FFX_PARALLELSORT_ELEMENTS_PER_THREAD * FFX_PARALLELSORT_THREADGROUP_SIZE; + uint32_t NumBlocks = (MaxNumKeys + BlockSize - 1) / BlockSize; + uint32_t NumReducedBlocks = (NumBlocks + BlockSize - 1) / BlockSize; + + ScratchBufferSize = FFX_PARALLELSORT_SORT_BIN_COUNT * NumBlocks * sizeof(uint32_t); + ReduceScratchBufferSize = FFX_PARALLELSORT_SORT_BIN_COUNT * NumReducedBlocks * sizeof(uint32_t); + } + + void FFX_ParallelSort_SetConstantAndDispatchData(uint32_t NumKeys, uint32_t MaxThreadGroups, FFX_ParallelSortCB& ConstantBuffer, uint32_t& NumThreadGroupsToRun, uint32_t& NumReducedThreadGroupsToRun) + { + ConstantBuffer.NumKeys = NumKeys; + + uint32_t BlockSize = FFX_PARALLELSORT_ELEMENTS_PER_THREAD * FFX_PARALLELSORT_THREADGROUP_SIZE; + uint32_t NumBlocks = (NumKeys + BlockSize - 1) / BlockSize; + + // Figure out data distribution + NumThreadGroupsToRun = MaxThreadGroups; + uint32_t BlocksPerThreadGroup = (NumBlocks / NumThreadGroupsToRun); + ConstantBuffer.NumThreadGroupsWithAdditionalBlocks = NumBlocks % NumThreadGroupsToRun; + + if (NumBlocks < NumThreadGroupsToRun) + { + BlocksPerThreadGroup = 1; + NumThreadGroupsToRun = NumBlocks; + ConstantBuffer.NumThreadGroupsWithAdditionalBlocks = 0; + } + + ConstantBuffer.NumThreadGroups = NumThreadGroupsToRun; + ConstantBuffer.NumBlocksPerThreadGroup = BlocksPerThreadGroup; + + // Calculate the number of thread groups to run for reduction (each thread group can process BlockSize number of entries) + NumReducedThreadGroupsToRun = FFX_PARALLELSORT_SORT_BIN_COUNT * ((BlockSize > NumThreadGroupsToRun) ? 1 : (NumThreadGroupsToRun + BlockSize - 1) / BlockSize); + ConstantBuffer.NumReduceThreadgroupPerBin = NumReducedThreadGroupsToRun / FFX_PARALLELSORT_SORT_BIN_COUNT; + ConstantBuffer.NumScanValues = NumReducedThreadGroupsToRun; // The number of reduce thread groups becomes our scan count (as each thread group writes out 1 value that needs scan prefix) + } + + // We are using some optimizations to hide buffer load latency, so make sure anyone changing this define is made aware of that fact. + static_assert(FFX_PARALLELSORT_ELEMENTS_PER_THREAD == 4, "FFX_ParallelSort Shaders currently explicitly rely on FFX_PARALLELSORT_ELEMENTS_PER_THREAD being set to 4 in order to optimize buffer loads. Please adjust the optimization to factor in the new define value."); +#elif defined(FFX_HLSL) + + struct FFX_ParallelSortCB + { + uint NumKeys; + int NumBlocksPerThreadGroup; + uint NumThreadGroups; + uint NumThreadGroupsWithAdditionalBlocks; + uint NumReduceThreadgroupPerBin; + uint NumScanValues; + }; + + groupshared uint gs_Histogram[FFX_PARALLELSORT_THREADGROUP_SIZE * FFX_PARALLELSORT_SORT_BIN_COUNT]; + void FFX_ParallelSort_Count_uint(uint localID, uint groupID, FFX_ParallelSortCB CBuffer, uint ShiftBit, RWStructuredBuffer SrcBuffer, RWStructuredBuffer SumTable) + { + int i; + // Start by clearing our local counts in LDS + for ( i = 0; i < FFX_PARALLELSORT_SORT_BIN_COUNT; i++) + gs_Histogram[(i * FFX_PARALLELSORT_THREADGROUP_SIZE) + localID] = 0; + + // Wait for everyone to catch up + GroupMemoryBarrierWithGroupSync(); + + // Data is processed in blocks, and how many we process can changed based on how much data we are processing + // versus how many thread groups we are processing with + int BlockSize = FFX_PARALLELSORT_ELEMENTS_PER_THREAD * FFX_PARALLELSORT_THREADGROUP_SIZE; + + // Figure out this thread group's index into the block data (taking into account thread groups that need to do extra reads) + uint ThreadgroupBlockStart = (BlockSize * CBuffer.NumBlocksPerThreadGroup * groupID); + uint NumBlocksToProcess = CBuffer.NumBlocksPerThreadGroup; + + if (groupID >= CBuffer.NumThreadGroups - CBuffer.NumThreadGroupsWithAdditionalBlocks) + { + ThreadgroupBlockStart += (groupID - (CBuffer.NumThreadGroups - CBuffer.NumThreadGroupsWithAdditionalBlocks)) * BlockSize; + NumBlocksToProcess++; + } + + // Get the block start index for this thread + uint BlockIndex = ThreadgroupBlockStart + localID; + + // Count value occurrence + for (uint BlockCount = 0; BlockCount < NumBlocksToProcess; BlockCount++, BlockIndex += BlockSize) + { + uint DataIndex = BlockIndex; + + // Pre-load the key values in order to hide some of the read latency + uint srcKeys[FFX_PARALLELSORT_ELEMENTS_PER_THREAD]; + srcKeys[0] = SrcBuffer[DataIndex]; + srcKeys[1] = SrcBuffer[DataIndex + FFX_PARALLELSORT_THREADGROUP_SIZE]; + srcKeys[2] = SrcBuffer[DataIndex + (FFX_PARALLELSORT_THREADGROUP_SIZE * 2)]; + srcKeys[3] = SrcBuffer[DataIndex + (FFX_PARALLELSORT_THREADGROUP_SIZE * 3)]; + + for ( i = 0; i < FFX_PARALLELSORT_ELEMENTS_PER_THREAD; i++) + { + if (DataIndex < CBuffer.NumKeys) + { + uint localKey = (srcKeys[i] >> ShiftBit) & 0xf; + InterlockedAdd(gs_Histogram[(localKey * FFX_PARALLELSORT_THREADGROUP_SIZE) + localID], 1); + DataIndex += FFX_PARALLELSORT_THREADGROUP_SIZE; + } + } + } + + // Even though our LDS layout guarantees no collisions, our thread group size is greater than a wave + // so we need to make sure all thread groups are done counting before we start tallying up the results + GroupMemoryBarrierWithGroupSync(); + + if (localID < FFX_PARALLELSORT_SORT_BIN_COUNT) + { + uint sum = 0; + for (i = 0; i < FFX_PARALLELSORT_THREADGROUP_SIZE; i++) + { + sum += gs_Histogram[localID * FFX_PARALLELSORT_THREADGROUP_SIZE + i]; + } + SumTable[localID * CBuffer.NumThreadGroups + groupID] = sum; + } + } + + groupshared uint gs_LDSSums[FFX_PARALLELSORT_THREADGROUP_SIZE]; + uint FFX_ParallelSort_ThreadgroupReduce(uint localSum, uint localID) + { + // Do wave local reduce + uint waveReduced = WaveActiveSum(localSum); + + // First lane in a wave writes out wave reduction to LDS (this accounts for num waves per group greater than HW wave size) + // Note that some hardware with very small HW wave sizes (i.e. <= 8) may exhibit issues with this algorithm, and have not been tested. + uint waveID = localID / WaveGetLaneCount(); + if (WaveIsFirstLane()) + gs_LDSSums[waveID] = waveReduced; + + // Wait for everyone to catch up + GroupMemoryBarrierWithGroupSync(); + + // First wave worth of threads sum up wave reductions + if (!waveID) + waveReduced = WaveActiveSum( (localID < FFX_PARALLELSORT_THREADGROUP_SIZE / WaveGetLaneCount()) ? gs_LDSSums[localID] : 0); + + // Returned the reduced sum + return waveReduced; + } + + uint FFX_ParallelSort_BlockScanPrefix(uint localSum, uint localID) + { + // Do wave local scan-prefix + uint wavePrefixed = WavePrefixSum(localSum); + + // Since we are dealing with thread group sizes greater than HW wave size, we need to account for what wave we are in. + uint waveID = localID / WaveGetLaneCount(); + uint laneID = WaveGetLaneIndex(); + + // Last element in a wave writes out partial sum to LDS + if (laneID == WaveGetLaneCount() - 1) + gs_LDSSums[waveID] = wavePrefixed + localSum; + + // Wait for everyone to catch up + GroupMemoryBarrierWithGroupSync(); + + // First wave prefixes partial sums + if (!waveID) + gs_LDSSums[localID] = WavePrefixSum(gs_LDSSums[localID]); + + // Wait for everyone to catch up + GroupMemoryBarrierWithGroupSync(); + + // Add the partial sums back to each wave prefix + wavePrefixed += gs_LDSSums[waveID]; + + return wavePrefixed; + } + + void FFX_ParallelSort_ReduceCount(uint localID, uint groupID, FFX_ParallelSortCB CBuffer, RWStructuredBuffer SumTable, RWStructuredBuffer ReduceTable) + { + // Figure out what bin data we are reducing + uint BinID = groupID / CBuffer.NumReduceThreadgroupPerBin; + uint BinOffset = BinID * CBuffer.NumThreadGroups; + + // Get the base index for this thread group + uint BaseIndex = (groupID % CBuffer.NumReduceThreadgroupPerBin) * FFX_PARALLELSORT_ELEMENTS_PER_THREAD * FFX_PARALLELSORT_THREADGROUP_SIZE; + + // Calculate partial sums for entries this thread reads in + uint threadgroupSum = 0; + for (uint i = 0; i < FFX_PARALLELSORT_ELEMENTS_PER_THREAD; ++i) + { + uint DataIndex = BaseIndex + (i * FFX_PARALLELSORT_THREADGROUP_SIZE) + localID; + threadgroupSum += (DataIndex < CBuffer.NumThreadGroups) ? SumTable[BinOffset + DataIndex] : 0; + } + + // Reduce across the entirety of the thread group + threadgroupSum = FFX_ParallelSort_ThreadgroupReduce(threadgroupSum, localID); + + // First thread of the group writes out the reduced sum for the bin + if (!localID) + ReduceTable[groupID] = threadgroupSum; + + // What this will look like in the reduced table is: + // [ [bin0 ... bin0] [bin1 ... bin1] ... ] + } + + // This is to transform uncoalesced loads into coalesced loads and + // then scattered loads from LDS + groupshared int gs_LDS[FFX_PARALLELSORT_ELEMENTS_PER_THREAD][FFX_PARALLELSORT_THREADGROUP_SIZE]; + void FFX_ParallelSort_ScanPrefix(uint numValuesToScan, uint localID, uint groupID, uint BinOffset, uint BaseIndex, bool AddPartialSums, + FFX_ParallelSortCB CBuffer, RWStructuredBuffer ScanSrc, RWStructuredBuffer ScanDst, RWStructuredBuffer ScanScratch) + { + uint i; + // Perform coalesced loads into LDS + for ( i = 0; i < FFX_PARALLELSORT_ELEMENTS_PER_THREAD; i++) + { + uint DataIndex = BaseIndex + (i * FFX_PARALLELSORT_THREADGROUP_SIZE) + localID; + + uint col = ((i * FFX_PARALLELSORT_THREADGROUP_SIZE) + localID) / FFX_PARALLELSORT_ELEMENTS_PER_THREAD; + uint row = ((i * FFX_PARALLELSORT_THREADGROUP_SIZE) + localID) % FFX_PARALLELSORT_ELEMENTS_PER_THREAD; + gs_LDS[row][col] = (DataIndex < numValuesToScan) ? ScanSrc[BinOffset + DataIndex] : 0; + } + + // Wait for everyone to catch up + GroupMemoryBarrierWithGroupSync(); + + uint threadgroupSum = 0; + // Calculate the local scan-prefix for current thread + for ( i = 0; i < FFX_PARALLELSORT_ELEMENTS_PER_THREAD; i++) + { + uint tmp = gs_LDS[i][localID]; + gs_LDS[i][localID] = threadgroupSum; + threadgroupSum += tmp; + } + + // Scan prefix partial sums + threadgroupSum = FFX_ParallelSort_BlockScanPrefix(threadgroupSum, localID); + + // Add reduced partial sums if requested + uint partialSum = 0; + if (AddPartialSums) + { + // Partial sum additions are a little special as they are tailored to the optimal number of + // thread groups we ran in the beginning, so need to take that into account + partialSum = ScanScratch[groupID]; + } + + // Add the block scanned-prefixes back in + for (i = 0; i < FFX_PARALLELSORT_ELEMENTS_PER_THREAD; i++) + gs_LDS[i][localID] += threadgroupSum; + + // Wait for everyone to catch up + GroupMemoryBarrierWithGroupSync(); + + // Perform coalesced writes to scan dst + for ( i = 0; i < FFX_PARALLELSORT_ELEMENTS_PER_THREAD; i++) + { + uint DataIndex = BaseIndex + (i * FFX_PARALLELSORT_THREADGROUP_SIZE) + localID; + + uint col = ((i * FFX_PARALLELSORT_THREADGROUP_SIZE) + localID) / FFX_PARALLELSORT_ELEMENTS_PER_THREAD; + uint row = ((i * FFX_PARALLELSORT_THREADGROUP_SIZE) + localID) % FFX_PARALLELSORT_ELEMENTS_PER_THREAD; + + if (DataIndex < numValuesToScan) + ScanDst[BinOffset + DataIndex] = gs_LDS[row][col] + partialSum; + } + } + + // Offset cache to avoid loading the offsets all the time + groupshared uint gs_BinOffsetCache[FFX_PARALLELSORT_THREADGROUP_SIZE]; + // Local histogram for offset calculations + groupshared uint gs_LocalHistogram[FFX_PARALLELSORT_SORT_BIN_COUNT]; + // Scratch area for algorithm + groupshared uint gs_LDSScratch[FFX_PARALLELSORT_THREADGROUP_SIZE]; + void FFX_ParallelSort_Scatter_uint(uint localID, uint groupID, FFX_ParallelSortCB CBuffer, uint ShiftBit, RWStructuredBuffer SrcBuffer, RWStructuredBuffer DstBuffer, RWStructuredBuffer SumTable +#ifdef kRS_ValueCopy + ,RWStructuredBuffer SrcPayload, RWStructuredBuffer DstPayload +#endif // kRS_ValueCopy + ) + { + // Load the sort bin threadgroup offsets into LDS for faster referencing + if (localID < FFX_PARALLELSORT_SORT_BIN_COUNT) + gs_BinOffsetCache[localID] = SumTable[localID * CBuffer.NumThreadGroups + groupID]; + + // Wait for everyone to catch up + GroupMemoryBarrierWithGroupSync(); + + // Data is processed in blocks, and how many we process can changed based on how much data we are processing + // versus how many thread groups we are processing with + int BlockSize = FFX_PARALLELSORT_ELEMENTS_PER_THREAD * FFX_PARALLELSORT_THREADGROUP_SIZE; + + // Figure out this thread group's index into the block data (taking into account thread groups that need to do extra reads) + uint ThreadgroupBlockStart = (BlockSize * CBuffer.NumBlocksPerThreadGroup * groupID); + uint NumBlocksToProcess = CBuffer.NumBlocksPerThreadGroup; + + if (groupID >= CBuffer.NumThreadGroups - CBuffer.NumThreadGroupsWithAdditionalBlocks) + { + ThreadgroupBlockStart += (groupID - (CBuffer.NumThreadGroups - CBuffer.NumThreadGroupsWithAdditionalBlocks)) * BlockSize; + NumBlocksToProcess++; + } + + // Get the block start index for this thread + uint BlockIndex = ThreadgroupBlockStart + localID; + + // Count value occurences + uint newCount; + for (int BlockCount = 0; BlockCount < NumBlocksToProcess; BlockCount++, BlockIndex += BlockSize) + { + uint DataIndex = BlockIndex; + + // Pre-load the key values in order to hide some of the read latency + uint srcKeys[FFX_PARALLELSORT_ELEMENTS_PER_THREAD]; + srcKeys[0] = SrcBuffer[DataIndex]; + srcKeys[1] = SrcBuffer[DataIndex + FFX_PARALLELSORT_THREADGROUP_SIZE]; + srcKeys[2] = SrcBuffer[DataIndex + (FFX_PARALLELSORT_THREADGROUP_SIZE * 2)]; + srcKeys[3] = SrcBuffer[DataIndex + (FFX_PARALLELSORT_THREADGROUP_SIZE * 3)]; + +#ifdef kRS_ValueCopy + uint srcValues[FFX_PARALLELSORT_ELEMENTS_PER_THREAD]; + srcValues[0] = SrcPayload[DataIndex]; + srcValues[1] = SrcPayload[DataIndex + FFX_PARALLELSORT_THREADGROUP_SIZE]; + srcValues[2] = SrcPayload[DataIndex + (FFX_PARALLELSORT_THREADGROUP_SIZE * 2)]; + srcValues[3] = SrcPayload[DataIndex + (FFX_PARALLELSORT_THREADGROUP_SIZE * 3)]; +#endif // kRS_ValueCopy + + for (int i = 0; i < FFX_PARALLELSORT_ELEMENTS_PER_THREAD; i++) + { + // Clear the local histogram + if (localID < FFX_PARALLELSORT_SORT_BIN_COUNT) + gs_LocalHistogram[localID] = 0; + + uint localKey = (DataIndex < CBuffer.NumKeys ? srcKeys[i] : 0xffffffff); +#ifdef kRS_ValueCopy + uint localValue = (DataIndex < CBuffer.NumKeys ? srcValues[i] : 0); +#endif // kRS_ValueCopy + + // Sort the keys locally in LDS + for (uint bitShift = 0; bitShift < FFX_PARALLELSORT_SORT_BITS_PER_PASS; bitShift += 2) + { + // Figure out the keyIndex + uint keyIndex = (localKey >> ShiftBit) & 0xf; + uint bitKey = (keyIndex >> bitShift) & 0x3; + + // Create a packed histogram + uint packedHistogram = 1 << (bitKey * 8); + + // Sum up all the packed keys (generates counted offsets up to current thread group) + uint localSum = FFX_ParallelSort_BlockScanPrefix(packedHistogram, localID); + + // Last thread stores the updated histogram counts for the thread group + // Scratch = 0xsum3|sum2|sum1|sum0 for thread group + if (localID == (FFX_PARALLELSORT_THREADGROUP_SIZE - 1)) + gs_LDSScratch[0] = localSum + packedHistogram; + + // Wait for everyone to catch up + GroupMemoryBarrierWithGroupSync(); + + // Load the sums value for the thread group + packedHistogram = gs_LDSScratch[0]; + + // Add prefix offsets for all 4 bit "keys" (packedHistogram = 0xsum2_1_0|sum1_0|sum0|0) + packedHistogram = (packedHistogram << 8) + (packedHistogram << 16) + (packedHistogram << 24); + + // Calculate the proper offset for this thread's value + localSum += packedHistogram; + + // Calculate target offset + uint keyOffset = (localSum >> (bitKey * 8)) & 0xff; + + // Re-arrange the keys (store, sync, load) + gs_LDSSums[keyOffset] = localKey; + GroupMemoryBarrierWithGroupSync(); + localKey = gs_LDSSums[localID]; + + // Wait for everyone to catch up + GroupMemoryBarrierWithGroupSync(); + +#ifdef kRS_ValueCopy + // Re-arrange the values if we have them (store, sync, load) + gs_LDSSums[keyOffset] = localValue; + GroupMemoryBarrierWithGroupSync(); + localValue = gs_LDSSums[localID]; + + // Wait for everyone to catch up + GroupMemoryBarrierWithGroupSync(); +#endif // kRS_ValueCopy + } + + // Need to recalculate the keyIndex on this thread now that values have been copied around the thread group + uint keyIndex = (localKey >> ShiftBit) & 0xf; + + // Reconstruct histogram + InterlockedAdd(gs_LocalHistogram[keyIndex], 1); + + // Wait for everyone to catch up + GroupMemoryBarrierWithGroupSync(); + + // Prefix histogram + uint histogramPrefixSum = WavePrefixSum(localID < FFX_PARALLELSORT_SORT_BIN_COUNT ? gs_LocalHistogram[localID] : 0); + + // Broadcast prefix-sum via LDS + if (localID < FFX_PARALLELSORT_SORT_BIN_COUNT) + gs_LDSScratch[localID] = histogramPrefixSum; + + // Get the global offset for this key out of the cache + uint globalOffset = gs_BinOffsetCache[keyIndex]; + + // Wait for everyone to catch up + GroupMemoryBarrierWithGroupSync(); + + // Get the local offset (at this point the keys are all in increasing order from 0 -> num bins in localID 0 -> thread group size) + uint localOffset = localID - gs_LDSScratch[keyIndex]; + + // Write to destination + uint totalOffset = globalOffset + localOffset; + + if (totalOffset < CBuffer.NumKeys) + { + DstBuffer[totalOffset] = localKey; + +#ifdef kRS_ValueCopy + DstPayload[totalOffset] = localValue; +#endif // kRS_ValueCopy + } + + // Wait for everyone to catch up + GroupMemoryBarrierWithGroupSync(); + + // Update the cached histogram for the next set of entries + if (localID < FFX_PARALLELSORT_SORT_BIN_COUNT) + gs_BinOffsetCache[localID] += gs_LocalHistogram[localID]; + + DataIndex += FFX_PARALLELSORT_THREADGROUP_SIZE; // Increase the data offset by thread group size + } + } + } + + void FFX_ParallelSort_SetupIndirectParams(uint NumKeys, uint MaxThreadGroups, RWStructuredBuffer CBuffer, RWStructuredBuffer CountScatterArgs, RWStructuredBuffer ReduceScanArgs) + { + CBuffer[0].NumKeys = NumKeys; + + uint BlockSize = FFX_PARALLELSORT_ELEMENTS_PER_THREAD * FFX_PARALLELSORT_THREADGROUP_SIZE; + uint NumBlocks = (NumKeys + BlockSize - 1) / BlockSize; + + // Figure out data distribution + uint NumThreadGroupsToRun = MaxThreadGroups; + uint BlocksPerThreadGroup = (NumBlocks / NumThreadGroupsToRun); + CBuffer[0].NumThreadGroupsWithAdditionalBlocks = NumBlocks % NumThreadGroupsToRun; + + if (NumBlocks < NumThreadGroupsToRun) + { + BlocksPerThreadGroup = 1; + NumThreadGroupsToRun = NumBlocks; + CBuffer[0].NumThreadGroupsWithAdditionalBlocks = 0; + } + + CBuffer[0].NumThreadGroups = NumThreadGroupsToRun; + CBuffer[0].NumBlocksPerThreadGroup = BlocksPerThreadGroup; + + // Calculate the number of thread groups to run for reduction (each thread group can process BlockSize number of entries) + uint NumReducedThreadGroupsToRun = FFX_PARALLELSORT_SORT_BIN_COUNT * ((BlockSize > NumThreadGroupsToRun) ? 1 : (NumThreadGroupsToRun + BlockSize - 1) / BlockSize); + CBuffer[0].NumReduceThreadgroupPerBin = NumReducedThreadGroupsToRun / FFX_PARALLELSORT_SORT_BIN_COUNT; + CBuffer[0].NumScanValues = NumReducedThreadGroupsToRun; // The number of reduce thread groups becomes our scan count (as each thread group writes out 1 value that needs scan prefix) + + // Setup dispatch arguments + CountScatterArgs[0] = NumThreadGroupsToRun; + CountScatterArgs[1] = 1; + CountScatterArgs[2] = 1; + + ReduceScanArgs[0] = NumReducedThreadGroupsToRun; + ReduceScanArgs[1] = 1; + ReduceScanArgs[2] = 1; + } + +#endif // __cplusplus +