From 0ddca95e6714727a252ead345591ca8f2598f261 Mon Sep 17 00:00:00 2001 From: Rys Sommefeldt Date: Thu, 22 Apr 2021 17:33:17 +0100 Subject: [PATCH] FidelityFX CACAO v1.2 --- docs/FFX-CACAO-GUI.pdf | Bin 853563 -> 934129 bytes docs/FFX-CACAO-Technology.pdf | Bin 1367281 -> 1374340 bytes doxygen.conf | 2 +- ffx-cacao/inc/ffx_cacao.h | 394 +- ffx-cacao/inc/ffx_cacao_impl.h | 312 ++ ffx-cacao/readme.md | 14 +- ffx-cacao/src/build_shaders_dxil.bat | 65 +- ffx-cacao/src/build_shaders_spirv.bat | 130 +- ffx-cacao/src/ffx_cacao.cpp | 4234 +---------------- ffx-cacao/src/ffx_cacao.hlsl | 1680 +++---- ffx-cacao/src/ffx_cacao_bindings.hlsl | 367 ++ ffx-cacao/src/ffx_cacao_defines.h | 80 +- ffx-cacao/src/ffx_cacao_impl.cpp | 3834 +++++++++++++++ license.txt | 2 +- readme.md | 13 +- sample/libs/cauldron | 2 +- .../Common/{FFX_CACAO_Common.h => Common.h} | 107 +- ..._CACAO_Sample.json => SampleSettings.json} | 0 sample/src/DX12/CMakeLists.txt | 10 +- .../DX12/{FFX_CACAO_Sample.cpp => Sample.cpp} | 365 +- .../src/DX12/{FFX_CACAO_Sample.h => Sample.h} | 72 +- sample/src/DX12/SampleRenderer.cpp | 1186 +++-- sample/src/DX12/SampleRenderer.h | 252 +- sample/src/VK/CMakeLists.txt | 10 +- sample/src/VK/FFX_CACAO_Sample.h | 96 - .../VK/{FFX_CACAO_Sample.cpp => Sample.cpp} | 526 +- sample/src/VK/Sample.h | 96 + sample/src/VK/SampleRenderer.cpp | 1850 ++++--- sample/src/VK/SampleRenderer.h | 214 +- 29 files changed, 7960 insertions(+), 7953 deletions(-) create mode 100644 ffx-cacao/inc/ffx_cacao_impl.h create mode 100644 ffx-cacao/src/ffx_cacao_bindings.hlsl create mode 100644 ffx-cacao/src/ffx_cacao_impl.cpp rename sample/src/Common/{FFX_CACAO_Common.h => Common.h} (83%) rename sample/src/Common/{FFX_CACAO_Sample.json => SampleSettings.json} (100%) rename sample/src/DX12/{FFX_CACAO_Sample.cpp => Sample.cpp} (68%) rename sample/src/DX12/{FFX_CACAO_Sample.h => Sample.h} (53%) delete mode 100644 sample/src/VK/FFX_CACAO_Sample.h rename sample/src/VK/{FFX_CACAO_Sample.cpp => Sample.cpp} (57%) create mode 100644 sample/src/VK/Sample.h diff --git a/docs/FFX-CACAO-GUI.pdf b/docs/FFX-CACAO-GUI.pdf index e0f8f3c3d51caa458844146904bf72f6958fff21..41ca780f3efcd4c17c97ce186acf357b1cf66a76 100644 GIT binary patch delta 195898 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 N6p=32h9m#b{U6PYb0+`* delta 119333 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 diff --git a/docs/FFX-CACAO-Technology.pdf b/docs/FFX-CACAO-Technology.pdf index 69c5a2893839d476a21a78871d2495b315b03766..8592e595d25ab9a5df8c33b96027b9aae7c8e615 100644 GIT binary patch delta 56480 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 RVXq-EcdcBv?ulog{1;4oHpl<~ delta 50230 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 diff --git a/doxygen.conf b/doxygen.conf index e70520d..e9932e9 100644 --- a/doxygen.conf +++ b/doxygen.conf @@ -1,7 +1,7 @@ DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = "FidelityFX CACAO" OPTIMIZE_OUTPUT_FOR_C = YES -INPUT = ffx-cacao/inc/ffx_cacao.h +INPUT = ffx-cacao/inc/ffx_cacao.h ffx-cacao/inc/ffx_cacao_impl.h OUTPUT_DIRECTORY = docs ENABLE_PREPROCESSING = YES diff --git a/ffx-cacao/inc/ffx_cacao.h b/ffx-cacao/inc/ffx_cacao.h index e67c49d..f270b46 100644 --- a/ffx-cacao/inc/ffx_cacao.h +++ b/ffx-cacao/inc/ffx_cacao.h @@ -1,4 +1,4 @@ -// Modifications Copyright © 2020. Advanced Micro Devices, Inc. All Rights Reserved. +// Modifications Copyright © 2021. Advanced Micro Devices, Inc. All Rights Reserved. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Copyright (c) 2016, Intel Corporation @@ -22,81 +22,54 @@ #pragma once -// In future it is planned that FidelityFX CACAO will allow SSAO generation at native resolution. -// However, at the current time the performance/image quality trade off is poor, and further optimisation -// work is being carried out. If you wish to experiment with native resolution SSAO generation enable this -// flag. Integrating FidelityFX CACAO into games with native resolution enabled is currently not recommended. -// #define FFX_CACAO_ENABLE_NATIVE_RESOLUTION - -// #define FFX_CACAO_ENABLE_PROFILING -// #define FFX_CACAO_ENABLE_D3D12 -// #define FFX_CACAO_ENABLE_VULKAN - #include <stdint.h> -#ifdef FFX_CACAO_ENABLE_D3D12 -#include <d3d12.h> -#endif -#ifdef FFX_CACAO_ENABLE_VULKAN -#include <vulkan/vulkan.h> -#endif -typedef uint8_t FfxCacaoBool; -static const FfxCacaoBool FFX_CACAO_TRUE = 1; -static const FfxCacaoBool FFX_CACAO_FALSE = 0; - -/** - The return codes for the API functions. -*/ -typedef enum FfxCacaoStatus { - FFX_CACAO_STATUS_OK = 0, - FFX_CACAO_STATUS_INVALID_ARGUMENT = -1, - FFX_CACAO_STATUS_INVALID_POINTER = -2, - FFX_CACAO_STATUS_OUT_OF_MEMORY = -3, - FFX_CACAO_STATUS_FAILED = -4, -} FfxCacaoStatus; +typedef uint8_t FFX_CACAO_Bool; +static const FFX_CACAO_Bool FFX_CACAO_TRUE = 1; +static const FFX_CACAO_Bool FFX_CACAO_FALSE = 0; /** The quality levels that FidelityFX CACAO can generate SSAO at. This affects the number of samples taken for generating SSAO. */ -typedef enum FfxCacaoQuality { +typedef enum FFX_CACAO_Quality { FFX_CACAO_QUALITY_LOWEST = 0, FFX_CACAO_QUALITY_LOW = 1, FFX_CACAO_QUALITY_MEDIUM = 2, FFX_CACAO_QUALITY_HIGH = 3, FFX_CACAO_QUALITY_HIGHEST = 4, -} FfxCacaoQuality; +} FFX_CACAO_Quality; /** A structure representing a 4x4 matrix of floats. The matrix is stored in row major order in memory. */ -typedef struct FfxCacaoMatrix4x4 { +typedef struct FFX_CACAO_Matrix4x4 { float elements[4][4]; -} FfxCacaoMatrix4x4; +} FFX_CACAO_Matrix4x4; /** A structure for the settings used by FidelityFX CACAO. These settings may be updated with each draw call. */ -typedef struct FfxCacaoSettings { +typedef struct FFX_CACAO_Settings { float radius; ///< [0.0, ~ ] World (view) space size of the occlusion sphere. - float shadowMultiplier; ///< [0.0, 5.0] Effect strength linear multiplier - float shadowPower; ///< [0.5, 5.0] Effect strength pow modifier - float shadowClamp; ///< [0.0, 1.0] Effect max limit (applied after multiplier but before blur) - float horizonAngleThreshold; ///< [0.0, 0.2] Limits self-shadowing (makes the sampling area less of a hemisphere, more of a spherical cone, to avoid self-shadowing and various artifacts due to low tessellation and depth buffer imprecision, etc.) - float fadeOutFrom; ///< [0.0, ~ ] Distance to start start fading out the effect. + float shadowMultiplier; ///< [0.0, 5.0] Effect strength linear multiplier. + float shadowPower; ///< [0.5, 5.0] Effect strength pow modifier. + float shadowClamp; ///< [0.0, 1.0] Effect max limit (applied after multiplier but before blur). + float horizonAngleThreshold; ///< [0.0, 0.2] Limits self-shadowing (makes the sampling area less of a hemisphere, more of a spherical cone, to avoid self-shadowing and various artifacts due to low tessellation and depth buffer imprecision, etc.). + float fadeOutFrom; ///< [0.0, ~ ] Distance to start fading out the effect. float fadeOutTo; ///< [0.0, ~ ] Distance at which the effect is faded out. - FfxCacaoQuality qualityLevel; ///< Effect quality, affects number of taps etc - float adaptiveQualityLimit; ///< [0.0, 1.0] (only for Quality Level 3) - uint32_t blurPassCount; ///< [ 0, 8] Number of edge-sensitive smart blur passes to apply - float sharpness; ///< [0.0, 1.0] (How much to bleed over edges; 1: not at all, 0.5: half-half; 0.0: completely ignore edges) + FFX_CACAO_Quality qualityLevel; ///< Effect quality, affects number of taps etc. + float adaptiveQualityLimit; ///< [0.0, 1.0] (only for quality level FFX_CACAO_QUALITY_HIGHEST). + uint32_t blurPassCount; ///< [ 0, 8] Number of edge-sensitive smart blur passes to apply. + float sharpness; ///< [0.0, 1.0] (How much to bleed over edges; 1: not at all, 0.5: half-half; 0.0: completely ignore edges). float temporalSupersamplingAngleOffset; ///< [0.0, PI] Used to rotate sampling kernel; If using temporal AA / supersampling, suggested to rotate by ( (frame%3)/3.0*PI ) or similar. Kernel is already symmetrical, which is why we use PI and not 2*PI. float temporalSupersamplingRadiusOffset; ///< [0.0, 2.0] Used to scale sampling kernel; If using temporal AA / supersampling, suggested to scale by ( 1.0f + (((frame%3)-1.0)/3.0)*0.1 ) or similar. float detailShadowStrength; ///< [0.0, 5.0] Used for high-res detail AO using neighboring depth pixels: adds a lot of detail but also reduces temporal stability (adds aliasing). - FfxCacaoBool generateNormals; ///< This option should be set to FFX_CACAO_TRUE if FidelityFX-CACAO should reconstruct a normal buffer from the depth buffer. It is required to be FFX_CACAO_TRUE if no normal buffer is provided. + FFX_CACAO_Bool generateNormals; ///< This option should be set to FFX_CACAO_TRUE if FidelityFX-CACAO should reconstruct a normal buffer from the depth buffer. It is required to be FFX_CACAO_TRUE if no normal buffer is provided. float bilateralSigmaSquared; ///< [0.0, ~ ] Sigma squared value for use in bilateral upsampler giving Gaussian blur term. Should be greater than 0.0. float bilateralSimilarityDistanceSigma; ///< [0.0, ~ ] Sigma squared value for use in bilateral upsampler giving similarity weighting for neighbouring pixels. Should be greater than 0.0. -} FfxCacaoSettings; +} FFX_CACAO_Settings; -static const FfxCacaoSettings FFX_CACAO_DEFAULT_SETTINGS = { +static const FFX_CACAO_Settings FFX_CACAO_DEFAULT_SETTINGS = { /* radius */ 1.2f, /* shadowMultiplier */ 1.0f, /* shadowPower */ 1.50f, @@ -116,271 +89,162 @@ static const FfxCacaoSettings FFX_CACAO_DEFAULT_SETTINGS = { /* bilateralSimilarityDistanceSigma */ 0.01f, }; - -#ifdef FFX_CACAO_ENABLE_D3D12 /** - A struct containing all of the data used by FidelityFX-CACAO. - A context corresponds to an ID3D12Device. + A C++ structure for the constant buffer used by FidelityFX CACAO. */ -typedef struct FfxCacaoD3D12Context FfxCacaoD3D12Context; +typedef struct FFX_CACAO_Constants { + float DepthUnpackConsts[2]; + float CameraTanHalfFOV[2]; -/** - The parameters for creating a context. -*/ -typedef struct FfxCacaoD3D12ScreenSizeInfo { - uint32_t width; ///< width of the input/output buffers - uint32_t height; ///< height of the input/output buffers - ID3D12Resource *depthBufferResource; ///< pointer to depth buffer ID3D12Resource - D3D12_SHADER_RESOURCE_VIEW_DESC depthBufferSrvDesc; ///< depth buffer D3D12_SHADER_RESOURCE_VIEW_DESC - ID3D12Resource *normalBufferResource; ///< optional pointer to normal buffer ID3D12Resource (leave as NULL if none is provided) - D3D12_SHADER_RESOURCE_VIEW_DESC normalBufferSrvDesc; ///< normal buffer D3D12_SHADER_RESOURCE_VIEW_DESC - ID3D12Resource *outputResource; ///< pointer to output buffer ID3D12Resource - D3D12_UNORDERED_ACCESS_VIEW_DESC outputUavDesc; ///< output buffer D3D12_UNORDERED_ACCESS_VIEW_DESC -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - FfxCacaoBool useDownsampledSsao; ///< Whether SSAO should be generated at native resolution or half resolution. It is recommended to enable this setting for improved performance. -#endif -} FfxCacaoD3D12ScreenSizeInfo; -#endif + float NDCToViewMul[2]; + float NDCToViewAdd[2]; -#ifdef FFX_CACAO_ENABLE_VULKAN -/** - A struct containing all of the data used by FidelityFX-CACAO. - A context corresponds to a VkDevice. -*/ -typedef struct FfxCacaoVkContext FfxCacaoVkContext; + float DepthBufferUVToViewMul[2]; + float DepthBufferUVToViewAdd[2]; -/** - Miscellaneous flags for used for Vulkan context creation by FidelityFX-CACAO - */ -typedef enum FfxCacaoVkCreateFlagsBits { - FFX_CACAO_VK_CREATE_USE_16_BIT = 0x00000001, ///< Flag controlling whether 16-bit optimisations are enabled in shaders. - FFX_CACAO_VK_CREATE_USE_DEBUG_MARKERS = 0x00000002, ///< Flag controlling whether debug markers should be used. - FFX_CACAO_VK_CREATE_NAME_OBJECTS = 0x00000004, ///< Flag controlling whether Vulkan objects should be named. -} FfxCacaoVkCreateFlagsBits; -typedef uint32_t FfxCacaoVkCreateFlags; + float EffectRadius; + float EffectShadowStrength; + float EffectShadowPow; + float EffectShadowClamp; -/** - The parameters for creating a context. -*/ -typedef struct FfxCacaoVkCreateInfo { - VkPhysicalDevice physicalDevice; ///< The VkPhysicalDevice corresponding to the VkDevice in use - VkDevice device; ///< The VkDevice to use FFX CACAO with - FfxCacaoVkCreateFlags flags; ///< Miscellaneous flags for context creation -} FfxCacaoVkCreateInfo; + float EffectFadeOutMul; + float EffectFadeOutAdd; + float EffectHorizonAngleThreshold; + float EffectSamplingRadiusNearLimitRec; -/** - The parameters necessary when changing the screen size of FidelityFX CACAO. -*/ -typedef struct FfxCacaoVkScreenSizeInfo { - uint32_t width; ///< width of the input/output buffers - uint32_t height; ///< height of the input/output buffers - VkImageView depthView; ///< An image view for the depth buffer, should be in layout VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL when used with FFX CACAO - VkImageView normalsView; ///< An optional image view for the normal buffer (may be VK_NULL_HANDLE). Should be in layout VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL when used with FFX CACAO - VkImage output; ///< An image for writing output from FFX CACAO, must have the same dimensions as the input - VkImageView outputView; ///< An image view corresponding to the output image. -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - FfxCacaoBool useDownsampledSsao; ///< Whether SSAO should be generated at native resolution or half resolution. It is recommended to enable this setting for improved performance. -#endif -} FfxCacaoVkScreenSizeInfo; -#endif - -#ifdef FFX_CACAO_ENABLE_PROFILING -/** - A timestamp. The label gives the name of the stage of the effect, and the ticks is the number of GPU ticks spent on that stage. -*/ -typedef struct FfxCacaoTimestamp { - const char *label; ///< name of timestamp stage - uint64_t ticks; ///< number of GPU ticks taken for stage -} FfxCacaoTimestamp; - -/** - An array of timestamps for detailed profiling information. The array timestamps contains numTimestamps entries. - Entry 0 of the timestamps array is guaranteed to be the total time taken by the effect. -*/ -typedef struct FfxCacaoDetailedTiming { - uint32_t numTimestamps; ///< number of timetstamps in the array timestamps - FfxCacaoTimestamp timestamps[32]; ///< array of timestamps for each FFX CACAO stage -} FfxCacaoDetailedTiming; -#endif - -#ifdef __cplusplus -extern "C" -{ -#endif + float DepthPrecisionOffsetMod; + float NegRecEffectRadius; + float LoadCounterAvgDiv; + float AdaptiveSampleCountLimit; -#ifdef FFX_CACAO_ENABLE_D3D12 - /** - Gets the size in bytes required by a context. This is to be used to allocate space for the context. - For example: + float InvSharpness; + int PassIndex; + float BilateralSigmaSquared; + float BilateralSimilarityDistanceSigma; - \code{.cpp} - size_t ffxCacaoD3D12ContextSize = ffxCacaoD3D12GetContextSize(); - FfxCacaoD3D12Context *context = (FfxCacaoD3D12Context*)malloc(ffxCacaoD3D12GetContextSize); + float PatternRotScaleMatrices[5][4]; - // ... + float NormalsUnpackMul; + float NormalsUnpackAdd; + float DetailAOStrength; + float Dummy0; - ffxCacaoD3D12DestroyContext(context); - free(context); - \endcode + float SSAOBufferDimensions[2]; + float SSAOBufferInverseDimensions[2]; - \return The size in bytes of an FfxCacaoD3D12Context. - */ - size_t ffxCacaoD3D12GetContextSize(); + float DepthBufferDimensions[2]; + float DepthBufferInverseDimensions[2]; - /** - Initialises an FfxCacaoD3D12Context. + int DepthBufferOffset[2]; + float PerPassFullResUVOffset[2]; - \param context A pointer to the context to initialise. - \param device A pointer to the D3D12 device. - \return The corresponding error code. - */ - FfxCacaoStatus ffxCacaoD3D12InitContext(FfxCacaoD3D12Context* context, ID3D12Device* device); + float InputOutputBufferDimensions[2]; + float InputOutputBufferInverseDimensions[2]; - /** - Destroys an FfxCacaoD3D12Context. + float ImportanceMapDimensions[2]; + float ImportanceMapInverseDimensions[2]; - \param context A pointer to the context to be destroyed. - \return The corresponding error code. + float DeinterleavedDepthBufferDimensions[2]; + float DeinterleavedDepthBufferInverseDimensions[2]; - \note This function does not destroy screen size dependent resources, and must be called after ffxCacaoD3D12DestroyScreenSizeDependentResources. - */ - FfxCacaoStatus ffxCacaoD3D12DestroyContext(FfxCacaoD3D12Context* context); + float DeinterleavedDepthBufferOffset[2]; + float DeinterleavedDepthBufferNormalisedOffset[2]; - /** - Initialises screen size dependent resources for the FfxCacaoD3D12Context. + FFX_CACAO_Matrix4x4 NormalsWorldToViewspaceMatrix; +} FFX_CACAO_Constants; - \param context A pointer to the FfxCacaoD3D12Context. - \param info A pointer to an FfxCacaoD3D12ScreenSizeInfo struct containing screen size info. - \return The corresponding error code. - */ - FfxCacaoStatus ffxCacaoD3D12InitScreenSizeDependentResources(FfxCacaoD3D12Context* context, const FfxCacaoD3D12ScreenSizeInfo* info); +/** + A structure containing sizes of each of the buffers used by FidelityFX CACAO. + */ +typedef struct FFX_CACAO_BufferSizeInfo { + uint32_t inputOutputBufferWidth; + uint32_t inputOutputBufferHeight; - /** - Destroys screen size dependent resources for the FfxCacaoD3D12Context. + uint32_t ssaoBufferWidth; + uint32_t ssaoBufferHeight; - \param context A pointer to the FfxCacaoD3D12Context. - \return The corresponding error code. - */ - FfxCacaoStatus ffxCacaoD3D12DestroyScreenSizeDependentResources(FfxCacaoD3D12Context* context); + uint32_t depthBufferXOffset; + uint32_t depthBufferYOffset; - /** - Update the settings of the FfxCacaoD3D12Context to those stored in the FfxCacaoSettings struct. + uint32_t depthBufferWidth; + uint32_t depthBufferHeight; - \param context A pointer to the FfxCacaoD3D12Context to update. - \param settings A pointer to the FfxCacaoSettings struct containing the new settings. - \return The corresponding error code. - */ - FfxCacaoStatus ffxCacaoD3D12UpdateSettings(FfxCacaoD3D12Context* context, const FfxCacaoSettings* settings); + uint32_t deinterleavedDepthBufferXOffset; + uint32_t deinterleavedDepthBufferYOffset; - /** - Append commands for drawing FFX CACAO to the provided ID3D12GraphicsCommandList. + uint32_t deinterleavedDepthBufferWidth; + uint32_t deinterleavedDepthBufferHeight; - \param context A pointer to the FfxCacaoD3D12Context. - \param commandList A pointer to the ID3D12GraphicsCommandList to append commands to. - \param proj A pointer to the projection matrix. - \param normalsToView An optional pointer to a matrix for transforming normals to in the normal buffer to viewspace. - \return The corresponding error code. - */ - FfxCacaoStatus ffxCacaoD3D12Draw(FfxCacaoD3D12Context* context, ID3D12GraphicsCommandList* commandList, const FfxCacaoMatrix4x4* proj, const FfxCacaoMatrix4x4* normalsToView); + uint32_t importanceMapWidth; + uint32_t importanceMapHeight; -#if FFX_CACAO_ENABLE_PROFILING - /** - Get detailed performance timings from the previous frame. + uint32_t downsampledSsaoBufferWidth; + uint32_t downsampledSsaoBufferHeight; +} FFX_CACAO_BufferSizeInfo; - \param context A pointer to the FfxCacaoD3D12Context. - \param timings A pointer to an FfxCacaoDetailedTiming struct to fill in with detailed timings. - \result The corresponding error code. - */ - FfxCacaoStatus ffxCacaoD3D12GetDetailedTimings(FfxCacaoD3D12Context* context, FfxCacaoDetailedTiming* timings); -#endif +#ifdef __cplusplus +extern "C" +{ #endif -#ifdef FFX_CACAO_ENABLE_VULKAN /** - Gets the size in bytes required by a Vulkan context. This is to be used to allocate space for the context. - For example: + Update buffer size info for resolution width x height. \code{.cpp} - size_t ffxCacaoVkContextSize = ffxCacaoVkGetContextSize(); - FfxCacaoVkContext *context = (FfxCacaoVkContext*)malloc(ffxCacaoVkGetContextSize); - - // ... - - ffxCacaoVkDestroyContext(context); - free(context); + FFX_CACAO_BufferSizeInfo bufferSizeInfo = {}; + FFX_CACAO_UpdateBufferSizeInfo(width, height, useDownsampledSsao, &bufferSizeInfo); \endcode - \return The size in bytes of an FfxCacaoVkContext. + \param width Screen width. + \param height Screen height. + \param useDownsampledSsao Whether FFX CACAO should use downsampling. */ - size_t ffxCacaoVkGetContextSize(); + void FFX_CACAO_UpdateBufferSizeInfo(uint32_t width, uint32_t height, FFX_CACAO_Bool useDownsampledSsao, FFX_CACAO_BufferSizeInfo* bsi); /** - Initialises an FfxCacaoVkContext. - - \param context A pointer to the context to initialise. - \param info A pointer to an FfxCacaoVkCreateInfo struct with parameters such as the vulkan device. - \return The corresponding error code. - */ - FfxCacaoStatus ffxCacaoVkInitContext(FfxCacaoVkContext* context, const FfxCacaoVkCreateInfo *info); + Update the contents of the FFX CACAO constant buffer (an FFX_CACAO_Constants struct). Note, this function does not update + per pass constants. - /** - Destroys an FfxCacaoVkContext. - - \param context A pointer to the context to be destroyed. - \return The corresponding error code. - - \note This function does not destroy screen size dependent resources, and must be called after ffxCacaoVkDestroyScreenSizeDependentResources. - */ - FfxCacaoStatus ffxCacaoVkDestroyContext(FfxCacaoVkContext* context); - - /** - Initialises screen size dependent resources for the FfxCacaoVkContext. - - \param context A pointer to the FfxCacaoVkContext. - \param info A pointer to an FfxCacaoVkScreenSizeInfo struct containing screen size info. - \return The corresponding error code. - */ - FfxCacaoStatus ffxCacaoVkInitScreenSizeDependentResources(FfxCacaoVkContext* context, const FfxCacaoVkScreenSizeInfo* info); + \code{.cpp} + FFX_CACAO_Matrix4x4 proj = ...; // projection matrix for the frame + FFX_CACAO_Matrix4x4 normalsToView = ...; // normals world space to view space matrix for the frame + FFX_CACAO_Settings settings = ...; // settings + FFX_CACAO_BufferSizeInfo bufferSizeInfo = ...; // buffer size info - /** - Destroys screen size dependent resources for the FfxCacaoVkContext. + FFX_CACAO_Constants constants = {}; + FFX_CACAO_UpdateConstants(&constants, &settings, &bufferSizeInfo, &proj, &normalsToView); + \endcode - \param context A pointer to the FfxCacaoVkContext. - \return The corresponding error code. + \param consts FFX_CACAO_Constants constant buffer. + \param settings FFX_CACAO_Settings settings. + \param bufferSizeInfo FFX_CACAO_BufferSizeInfo buffer size info. + \param proj Projection matrix for the frame. + \param normalsToView Normals world space to view space matrix for the frame. */ - FfxCacaoStatus ffxCacaoVkDestroyScreenSizeDependentResources(FfxCacaoVkContext* context); + void FFX_CACAO_UpdateConstants(FFX_CACAO_Constants* consts, const FFX_CACAO_Settings* settings, const FFX_CACAO_BufferSizeInfo* bufferSizeInfo, const FFX_CACAO_Matrix4x4* proj, const FFX_CACAO_Matrix4x4* normalsToView); /** - Update the settings of the FfxCacaoVkContext to those stored in the FfxCacaoSettings struct. - - \param context A pointer to the FfxCacaoVkContext to update. - \param settings A pointer to the FfxCacaoSettings struct containing the new settings. - \return The corresponding error code. - */ - FfxCacaoStatus ffxCacaoVkUpdateSettings(FfxCacaoVkContext* context, const FfxCacaoSettings* settings); + Update the contents of the FFX CACAO constant buffer (an FFX_CACAO_Constants struct) with per pass constants. + FFX CACAO runs 4 passes which use different constants. It is recommended to have four separate FFX_CACAO_Constants structs + each filled with constants for each of the 4 passes. - /** - Append commands for drawing FFX CACAO to the provided VkCommandBuffer. + \code{.cpp} + FFX_CACAO_Settings settings = ...; // settings + FFX_CACAO_BufferSizeInfo bufferSizeInfo = ...; // buffer size info - \param context A pointer to the FfxCacaoVkContext. - \param commandList The VkCommandBuffer to append commands to. - \param proj A pointer to the projection matrix. - \param normalsToView An optional pointer to a matrix for transforming normals to in the normal buffer to viewspace. - \return The corresponding error code. - */ - FfxCacaoStatus ffxCacaoVkDraw(FfxCacaoVkContext* context, VkCommandBuffer commandList, const FfxCacaoMatrix4x4* proj, const FfxCacaoMatrix4x4* normalsToView); + FFX_CACAO_Constants perPassConstants[4] = {}; -#ifdef FFX_CACAO_ENABLE_PROFILING - /** - Get detailed performance timings from the previous frame. + for (int i = 0; i < 4; ++i) { + FFX_CACAO_UpdatePerPassConstants(&perPassConstants[i], &settings, &bufferSizeInfo, i); + } + \endcode - \param context A pointer to the FfxCacaoVkContext. - \param timings A pointer to an FfxCacaoDetailedTiming struct to fill in with detailed timings. - \result The corresponding error code. + \param consts FFX_CACAO_Constants constants buffer. + \param settings FFX_CACAO_Settings settings. + \param bufferSizeInfo FFX_CACAO_BufferSizeInfo buffer size info. + \param pass pass number. */ - FfxCacaoStatus ffxCacaoVkGetDetailedTimings(FfxCacaoVkContext* context, FfxCacaoDetailedTiming* timings); -#endif -#endif + void FFX_CACAO_UpdatePerPassConstants(FFX_CACAO_Constants* consts, const FFX_CACAO_Settings* settings, const FFX_CACAO_BufferSizeInfo* bufferSizeInfo, int pass); #ifdef __cplusplus } diff --git a/ffx-cacao/inc/ffx_cacao_impl.h b/ffx-cacao/inc/ffx_cacao_impl.h new file mode 100644 index 0000000..f5a723d --- /dev/null +++ b/ffx-cacao/inc/ffx_cacao_impl.h @@ -0,0 +1,312 @@ +// Modifications Copyright © 2021. Advanced Micro Devices, Inc. All Rights Reserved. + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Copyright (c) 2016, Intel Corporation +// 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. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// File changes (yyyy-mm-dd) +// 2016-09-07: filip.strugar@intel.com: first commit +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/*! \file */ + +#pragma once + +#include "ffx_cacao.h" + +// #define FFX_CACAO_ENABLE_PROFILING +// #define FFX_CACAO_ENABLE_D3D12 +// #define FFX_CACAO_ENABLE_VULKAN + +#ifdef FFX_CACAO_ENABLE_D3D12 +#include <d3d12.h> +#endif +#ifdef FFX_CACAO_ENABLE_VULKAN +#include <vulkan/vulkan.h> +#endif + +/** + The return codes for the API functions. +*/ +typedef enum FFX_CACAO_Status { + FFX_CACAO_STATUS_OK = 0, + FFX_CACAO_STATUS_INVALID_ARGUMENT = -1, + FFX_CACAO_STATUS_INVALID_POINTER = -2, + FFX_CACAO_STATUS_OUT_OF_MEMORY = -3, + FFX_CACAO_STATUS_FAILED = -4, +} FFX_CACAO_Status; + +#ifdef FFX_CACAO_ENABLE_D3D12 +/** + A struct containing all of the data used by FidelityFX-CACAO. + A context corresponds to an ID3D12Device. +*/ +typedef struct FFX_CACAO_D3D12Context FFX_CACAO_D3D12Context; + +/** + The parameters for creating a context. +*/ +typedef struct FFX_CACAO_D3D12ScreenSizeInfo { + uint32_t width; ///< width of the input/output buffers + uint32_t height; ///< height of the input/output buffers + ID3D12Resource *depthBufferResource; ///< pointer to depth buffer ID3D12Resource + D3D12_SHADER_RESOURCE_VIEW_DESC depthBufferSrvDesc; ///< depth buffer D3D12_SHADER_RESOURCE_VIEW_DESC + ID3D12Resource *normalBufferResource; ///< optional pointer to normal buffer ID3D12Resource (leave as NULL if none is provided) + D3D12_SHADER_RESOURCE_VIEW_DESC normalBufferSrvDesc; ///< normal buffer D3D12_SHADER_RESOURCE_VIEW_DESC + ID3D12Resource *outputResource; ///< pointer to output buffer ID3D12Resource + D3D12_UNORDERED_ACCESS_VIEW_DESC outputUavDesc; ///< output buffer D3D12_UNORDERED_ACCESS_VIEW_DESC + FFX_CACAO_Bool useDownsampledSsao; ///< Whether SSAO should be generated at native resolution or half resolution. It is recommended to enable this setting for improved performance. +} FFX_CACAO_D3D12ScreenSizeInfo; +#endif + +#ifdef FFX_CACAO_ENABLE_VULKAN +/** + A struct containing all of the data used by FidelityFX-CACAO. + A context corresponds to a VkDevice. +*/ +typedef struct FFX_CACAO_VkContext FFX_CACAO_VkContext; + +/** + Miscellaneous flags for used for Vulkan context creation by FidelityFX-CACAO + */ +typedef enum FFX_CACAO_VkCreateFlagsBits { + FFX_CACAO_VK_CREATE_USE_16_BIT = 0x00000001, ///< Flag controlling whether 16-bit optimisations are enabled in shaders. + FFX_CACAO_VK_CREATE_USE_DEBUG_MARKERS = 0x00000002, ///< Flag controlling whether debug markers should be used. + FFX_CACAO_VK_CREATE_NAME_OBJECTS = 0x00000004, ///< Flag controlling whether Vulkan objects should be named. +} FFX_CACAO_VkCreateFlagsBits; +typedef uint32_t FFX_CACAO_VkCreateFlags; + +/** + The parameters for creating a context. +*/ +typedef struct FFX_CACAO_VkCreateInfo { + VkPhysicalDevice physicalDevice; ///< The VkPhysicalDevice corresponding to the VkDevice in use + VkDevice device; ///< The VkDevice to use FFX CACAO with + FFX_CACAO_VkCreateFlags flags; ///< Miscellaneous flags for context creation +} FFX_CACAO_VkCreateInfo; + +/** + The parameters necessary when changing the screen size of FidelityFX CACAO. +*/ +typedef struct FFX_CACAO_VkScreenSizeInfo { + uint32_t width; ///< width of the input/output buffers + uint32_t height; ///< height of the input/output buffers + VkImageView depthView; ///< An image view for the depth buffer, should be in layout VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL when used with FFX CACAO + VkImageView normalsView; ///< An optional image view for the normal buffer (may be VK_NULL_HANDLE). Should be in layout VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL when used with FFX CACAO + VkImage output; ///< An image for writing output from FFX CACAO, must have the same dimensions as the input + VkImageView outputView; ///< An image view corresponding to the output image. + FFX_CACAO_Bool useDownsampledSsao; ///< Whether SSAO should be generated at native resolution or half resolution. It is recommended to enable this setting for improved performance. +} FFX_CACAO_VkScreenSizeInfo; +#endif + +#ifdef FFX_CACAO_ENABLE_PROFILING +/** + A timestamp. The label gives the name of the stage of the effect, and the ticks is the number of GPU ticks spent on that stage. +*/ +typedef struct FFX_CACAO_Timestamp { + const char *label; ///< name of timestamp stage + uint64_t ticks; ///< number of GPU ticks taken for stage +} FFX_CACAO_Timestamp; + +/** + An array of timestamps for detailed profiling information. The array timestamps contains numTimestamps entries. + Entry 0 of the timestamps array is guaranteed to be the total time taken by the effect. +*/ +typedef struct FFX_CACAO_DetailedTiming { + uint32_t numTimestamps; ///< number of timetstamps in the array timestamps + FFX_CACAO_Timestamp timestamps[32]; ///< array of timestamps for each FFX CACAO stage +} FFX_CACAO_DetailedTiming; +#endif + +#ifdef __cplusplus +extern "C" +{ +#endif + +#ifdef FFX_CACAO_ENABLE_D3D12 + /** + Gets the size in bytes required by a context. This is to be used to allocate space for the context. + For example: + + \code{.cpp} + size_t FFX_CACAO_D3D12ContextSize = ffxCacaoD3D12GetContextSize(); + FFX_CACAO_D3D12Context *context = (FFX_CACAO_D3D12Context*)malloc(FFX_CACAO_D3D12GetContextSize); + + // ... + + FFX_CACAO_D3D12DestroyContext(context); + free(context); + \endcode + + \return The size in bytes of an FFX_CACAO_D3D12Context. + */ + size_t FFX_CACAO_D3D12GetContextSize(); + + /** + Initialises an FFX_CACAO_D3D12Context. + + \param context A pointer to the context to initialise. + \param device A pointer to the D3D12 device. + \return The corresponding error code. + */ + FFX_CACAO_Status FFX_CACAO_D3D12InitContext(FFX_CACAO_D3D12Context* context, ID3D12Device* device); + + /** + Destroys an FFX_CACAO_D3D12Context. + + \param context A pointer to the context to be destroyed. + \return The corresponding error code. + + \note This function does not destroy screen size dependent resources, and must be called after FFX_CACAO_D3D12DestroyScreenSizeDependentResources. + */ + FFX_CACAO_Status FFX_CACAO_D3D12DestroyContext(FFX_CACAO_D3D12Context* context); + + /** + Initialises screen size dependent resources for the FFX_CACAO_D3D12Context. + + \param context A pointer to the FFX_CACAO_D3D12Context. + \param info A pointer to an FFX_CACAO_D3D12ScreenSizeInfo struct containing screen size info. + \return The corresponding error code. + */ + FFX_CACAO_Status FFX_CACAO_D3D12InitScreenSizeDependentResources(FFX_CACAO_D3D12Context* context, const FFX_CACAO_D3D12ScreenSizeInfo* info); + + /** + Destroys screen size dependent resources for the FFX_CACAO_D3D12Context. + + \param context A pointer to the FFX_CACAO_D3D12Context. + \return The corresponding error code. + */ + FFX_CACAO_Status FFX_CACAO_D3D12DestroyScreenSizeDependentResources(FFX_CACAO_D3D12Context* context); + + /** + Update the settings of the FFX_CACAO_D3D12Context to those stored in the FFX_CACAO_Settings struct. + + \param context A pointer to the FFX_CACAO_D3D12Context to update. + \param settings A pointer to the FFX_CACAO_Settings struct containing the new settings. + \return The corresponding error code. + */ + FFX_CACAO_Status FFX_CACAO_D3D12UpdateSettings(FFX_CACAO_D3D12Context* context, const FFX_CACAO_Settings* settings); + + /** + Append commands for drawing FFX CACAO to the provided ID3D12GraphicsCommandList. + + \param context A pointer to the FFX_CACAO_D3D12Context. + \param commandList A pointer to the ID3D12GraphicsCommandList to append commands to. + \param proj A pointer to the projection matrix. + \param normalsToView An optional pointer to a matrix for transforming normals to in the normal buffer to viewspace. + \return The corresponding error code. + */ + FFX_CACAO_Status FFX_CACAO_D3D12Draw(FFX_CACAO_D3D12Context* context, ID3D12GraphicsCommandList* commandList, const FFX_CACAO_Matrix4x4* proj, const FFX_CACAO_Matrix4x4* normalsToView); + +#if FFX_CACAO_ENABLE_PROFILING + /** + Get detailed performance timings from the previous frame. + + \param context A pointer to the FFX_CACAO_D3D12Context. + \param timings A pointer to an FFX_CACAO_DetailedTiming struct to fill in with detailed timings. + \result The corresponding error code. + */ + FFX_CACAO_Status FFX_CACAO_D3D12GetDetailedTimings(FFX_CACAO_D3D12Context* context, FFX_CACAO_DetailedTiming* timings); +#endif +#endif + +#ifdef FFX_CACAO_ENABLE_VULKAN + /** + Gets the size in bytes required by a Vulkan context. This is to be used to allocate space for the context. + For example: + + \code{.cpp} + size_t FFX_CACAO_VkContextSize = ffxCacaoVkGetContextSize(); + FFX_CACAO_VkContext *context = (FFX_CACAO_VkContext*)malloc(FFX_CACAO_VkGetContextSize); + + // ... + + FFX_CACAO_VkDestroyContext(context); + free(context); + \endcode + + \return The size in bytes of an FFX_CACAO_VkContext. + */ + size_t FFX_CACAO_VkGetContextSize(); + + /** + Initialises an FFX_CACAO_VkContext. + + \param context A pointer to the context to initialise. + \param info A pointer to an FFX_CACAO_VkCreateInfo struct with parameters such as the vulkan device. + \return The corresponding error code. + */ + FFX_CACAO_Status FFX_CACAO_VkInitContext(FFX_CACAO_VkContext* context, const FFX_CACAO_VkCreateInfo *info); + + /** + Destroys an FFX_CACAO_VkContext. + + \param context A pointer to the context to be destroyed. + \return The corresponding error code. + + \note This function does not destroy screen size dependent resources, and must be called after FFX_CACAO_VkDestroyScreenSizeDependentResources. + */ + FFX_CACAO_Status FFX_CACAO_VkDestroyContext(FFX_CACAO_VkContext* context); + + /** + Initialises screen size dependent resources for the FFX_CACAO_VkContext. + + \param context A pointer to the FFX_CACAO_VkContext. + \param info A pointer to an FFX_CACAO_VkScreenSizeInfo struct containing screen size info. + \return The corresponding error code. + */ + FFX_CACAO_Status FFX_CACAO_VkInitScreenSizeDependentResources(FFX_CACAO_VkContext* context, const FFX_CACAO_VkScreenSizeInfo* info); + + /** + Destroys screen size dependent resources for the FFX_CACAO_VkContext. + + \param context A pointer to the FFX_CACAO_VkContext. + \return The corresponding error code. + */ + FFX_CACAO_Status FFX_CACAO_VkDestroyScreenSizeDependentResources(FFX_CACAO_VkContext* context); + + /** + Update the settings of the FFX_CACAO_VkContext to those stored in the FFX_CACAO_Settings struct. + + \param context A pointer to the FFX_CACAO_VkContext to update. + \param settings A pointer to the FFX_CACAO_Settings struct containing the new settings. + \return The corresponding error code. + */ + FFX_CACAO_Status FFX_CACAO_VkUpdateSettings(FFX_CACAO_VkContext* context, const FFX_CACAO_Settings* settings); + + /** + Append commands for drawing FFX CACAO to the provided VkCommandBuffer. + + \param context A pointer to the FFX_CACAO_VkContext. + \param commandList The VkCommandBuffer to append commands to. + \param proj A pointer to the projection matrix. + \param normalsToView An optional pointer to a matrix for transforming normals to in the normal buffer to viewspace. + \return The corresponding error code. + */ + FFX_CACAO_Status FFX_CACAO_VkDraw(FFX_CACAO_VkContext* context, VkCommandBuffer commandList, const FFX_CACAO_Matrix4x4* proj, const FFX_CACAO_Matrix4x4* normalsToView); + +#ifdef FFX_CACAO_ENABLE_PROFILING + /** + Get detailed performance timings from the previous frame. + + \param context A pointer to the FFX_CACAO_VkContext. + \param timings A pointer to an FFX_CACAO_DetailedTiming struct to fill in with detailed timings. + \result The corresponding error code. + */ + FFX_CACAO_Status FFX_CACAO_VkGetDetailedTimings(FFX_CACAO_VkContext* context, FFX_CACAO_DetailedTiming* timings); +#endif +#endif + +#ifdef __cplusplus +} +#endif diff --git a/ffx-cacao/readme.md b/ffx-cacao/readme.md index cbc1e91..66a9151 100644 --- a/ffx-cacao/readme.md +++ b/ffx-cacao/readme.md @@ -4,7 +4,15 @@ The **FidelityFX CACAO** library implements screen space ambient occlusion for u # Project Integration -FidelityFX CACAO supports three compile time options. These are: +FidelityFX CACAO comes with two main header files, `ffx-cacao/inc/ffx_cacao.h` and `ffx-cacao/inc/ffx_cacao_impl.h`. The file `ffx-cacao/inc/ffx_cacao.h` contains reusable C++ functions and struct definitions for integration of FidelityFX CACAO into custom engines. The functions declared in this header file are defined in `ffx-cacao/src/ffx_cacao.cpp`. The header file `ffx-cacao/inc/ffx_cacao_impl.h` is for use in quick integration of FidelityFX CACAO into DX12 and Vulkan engines. The functions declared in this file are defined in `ffx-cacao/src/ffx_cacao_impl.cpp`, which serves as a reference implementation of FidelityFX CACAO. + +# Reusable Functions and Structs + +The reusable functions and structs provided in `ffx-cacao/src/ffx_cacao.h` are documented via doxygen comments in the header file itself. The functions and structs are used to initialise the constant buffers used by FidelityFX CACAO from a user friendly settings struct `FFX_CACAO_Settings`. + +# Reference Implementation + +The reference implementation of FidelityFX CACAO supports three compile time options. These are: ```C++ FFX_CACAO_ENABLE_D3D12 @@ -12,11 +20,11 @@ FFX_CACAO_ENABLE_VK FFX_CACAO_ENABLE_PROFILING ``` -For use with D3D12 or Vulkan, the symbols `FFX_CACAO_ENABLE_D3D12` or `FFX_CACAO_ENABLE_VK` must be defined. If you wish to get detailed timings from FFX CACAO the symbol `FFX_CACAO_ENABLE_PROFILING` must be defined. These symbols can either be defined in the header `ffx-cacao/inc/ffx_cacao.h` itself by uncommenting the respective definitions, or they can defined in compiler flags. The provided sample of FFX CACAO defines these symbols using compiler flags. +For use with D3D12 or Vulkan, the symbols `FFX_CACAO_ENABLE_D3D12` or `FFX_CACAO_ENABLE_VK` must be defined. If you wish to get detailed timings from FFX CACAO the symbol `FFX_CACAO_ENABLE_PROFILING` must be defined. These symbols can either be defined in the header `ffx-cacao/inc/ffx_cacao_impl.h` itself by uncommenting the respective definitions, or they can defined in compiler flags. The provided sample of FFX CACAO defines these symbols using compiler flags. # Context Initialisation and Shutdown -First the FFX CACAO header must be included. This can be found at `ffx-cacao/inc/ffx_cacao.h`. Then a context must be created. This is usually done only once per device. To create a context you must first query for the size of a context, allocate space for a context, then inintialise the context. +First the FFX CACAO header must be included. This can be found at `ffx-cacao/inc/ffx_cacao_impl.h`. Then a context must be created. This is usually done only once per device. To create a context you must first query for the size of a context, allocate space for a context, then inintialise the context. For D3D12 the initialisation and shutdown processes are as follows: diff --git a/ffx-cacao/src/build_shaders_dxil.bat b/ffx-cacao/src/build_shaders_dxil.bat index 2e8e59e..48cbb93 100644 --- a/ffx-cacao/src/build_shaders_dxil.bat +++ b/ffx-cacao/src/build_shaders_dxil.bat @@ -6,47 +6,50 @@ set cauldron_dxc=..\..\sample\libs\cauldron\libs\DXC\bin\dxc.exe -T cs_6_2 if not exist "PrecompiledShadersDXIL" mkdir "PrecompiledShadersDXIL" -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareDownsampledDepths.h -Vn CSPrepareDownsampledDepthsDXIL -E CSPrepareDownsampledDepths ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOClearLoadCounter.h -Vn CSClearLoadCounterDXIL -E FFX_CACAO_ClearLoadCounter ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareNativeDepths.h -Vn CSPrepareNativeDepthsDXIL -E CSPrepareNativeDepths ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareDownsampledDepths.h -Vn CSPrepareDownsampledDepthsDXIL -E FFX_CACAO_PrepareDownsampledDepths ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareDownsampledDepthsAndMips.h -Vn CSPrepareDownsampledDepthsAndMipsDXIL -E CSPrepareDownsampledDepthsAndMips ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareNativeDepthsAndMips.h -Vn CSPrepareNativeDepthsAndMipsDXIL -E CSPrepareNativeDepthsAndMips ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareNativeDepths.h -Vn CSPrepareNativeDepthsDXIL -E FFX_CACAO_PrepareNativeDepths ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareDownsampledNormals.h -Vn CSPrepareDownsampledNormalsDXIL -E CSPrepareDownsampledNormals ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareNativeNormals.h -Vn CSPrepareNativeNormalsDXIL -E CSPrepareNativeNormals ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareDownsampledDepthsAndMips.h -Vn CSPrepareDownsampledDepthsAndMipsDXIL -E FFX_CACAO_PrepareDownsampledDepthsAndMips ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareNativeDepthsAndMips.h -Vn CSPrepareNativeDepthsAndMipsDXIL -E FFX_CACAO_PrepareNativeDepthsAndMips ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareDownsampledNormalsFromInputNormals.h -Vn CSPrepareDownsampledNormalsFromInputNormalsDXIL -E CSPrepareDownsampledNormalsFromInputNormals ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareNativeNormalsFromInputNormals.h -Vn CSPrepareNativeNormalsFromInputNormalsDXIL -E CSPrepareNativeNormalsFromInputNormals ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareDownsampledNormals.h -Vn CSPrepareDownsampledNormalsDXIL -E FFX_CACAO_PrepareDownsampledNormals ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareNativeNormals.h -Vn CSPrepareNativeNormalsDXIL -E FFX_CACAO_PrepareNativeNormals ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareDownsampledDepthsHalf.h -Vn CSPrepareDownsampledDepthsHalfDXIL -E CSPrepareDownsampledDepthsHalf ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareNativeDepthsHalf.h -Vn CSPrepareNativeDepthsHalfDXIL -E CSPrepareNativeDepthsHalf ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareDownsampledNormalsFromInputNormals.h -Vn CSPrepareDownsampledNormalsFromInputNormalsDXIL -E FFX_CACAO_PrepareDownsampledNormalsFromInputNormals ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareNativeNormalsFromInputNormals.h -Vn CSPrepareNativeNormalsFromInputNormalsDXIL -E FFX_CACAO_PrepareNativeNormalsFromInputNormals ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareDownsampledDepthsHalf.h -Vn CSPrepareDownsampledDepthsHalfDXIL -E FFX_CACAO_PrepareDownsampledDepthsHalf ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPrepareNativeDepthsHalf.h -Vn CSPrepareNativeDepthsHalfDXIL -E FFX_CACAO_PrepareNativeDepthsHalf ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOGenerateQ0.h -Vn CSGenerateQ0DXIL -E CSGenerateQ0 ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOGenerateQ1.h -Vn CSGenerateQ1DXIL -E CSGenerateQ1 ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOGenerateQ2.h -Vn CSGenerateQ2DXIL -E CSGenerateQ2 ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOGenerateQ3.h -Vn CSGenerateQ3DXIL -E CSGenerateQ3 ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOGenerateQ3Base.h -Vn CSGenerateQ3BaseDXIL -E CSGenerateQ3Base ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOGenerateImportanceMap.h -Vn CSGenerateImportanceMapDXIL -E CSGenerateImportanceMap ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPostprocessImportanceMapA.h -Vn CSPostprocessImportanceMapADXIL -E CSPostprocessImportanceMapA ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPostprocessImportanceMapB.h -Vn CSPostprocessImportanceMapBDXIL -E CSPostprocessImportanceMapB ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOGenerateQ0.h -Vn CSGenerateQ0DXIL -E FFX_CACAO_GenerateQ0 ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOGenerateQ1.h -Vn CSGenerateQ1DXIL -E FFX_CACAO_GenerateQ1 ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOGenerateQ2.h -Vn CSGenerateQ2DXIL -E FFX_CACAO_GenerateQ2 ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOGenerateQ3.h -Vn CSGenerateQ3DXIL -E FFX_CACAO_GenerateQ3 ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOGenerateQ3Base.h -Vn CSGenerateQ3BaseDXIL -E FFX_CACAO_GenerateQ3Base ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur1.h -Vn CSEdgeSensitiveBlur1DXIL -E CSEdgeSensitiveBlur1 ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur2.h -Vn CSEdgeSensitiveBlur2DXIL -E CSEdgeSensitiveBlur2 ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur3.h -Vn CSEdgeSensitiveBlur3DXIL -E CSEdgeSensitiveBlur3 ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur4.h -Vn CSEdgeSensitiveBlur4DXIL -E CSEdgeSensitiveBlur4 ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur5.h -Vn CSEdgeSensitiveBlur5DXIL -E CSEdgeSensitiveBlur5 ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur6.h -Vn CSEdgeSensitiveBlur6DXIL -E CSEdgeSensitiveBlur6 ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur7.h -Vn CSEdgeSensitiveBlur7DXIL -E CSEdgeSensitiveBlur7 ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur8.h -Vn CSEdgeSensitiveBlur8DXIL -E CSEdgeSensitiveBlur8 ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOGenerateImportanceMap.h -Vn CSGenerateImportanceMapDXIL -E FFX_CACAO_GenerateImportanceMap ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPostprocessImportanceMapA.h -Vn CSPostprocessImportanceMapADXIL -E FFX_CACAO_PostprocessImportanceMapA ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOPostprocessImportanceMapB.h -Vn CSPostprocessImportanceMapBDXIL -E FFX_CACAO_PostprocessImportanceMapB ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOApply.h -Vn CSApplyDXIL -E CSApply ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAONonSmartApply.h -Vn CSNonSmartApplyDXIL -E CSNonSmartApply ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAONonSmartHalfApply.h -Vn CSNonSmartHalfApplyDXIL -E CSNonSmartHalfApply ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur1.h -Vn CSEdgeSensitiveBlur1DXIL -E FFX_CACAO_EdgeSensitiveBlur1 ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur2.h -Vn CSEdgeSensitiveBlur2DXIL -E FFX_CACAO_EdgeSensitiveBlur2 ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur3.h -Vn CSEdgeSensitiveBlur3DXIL -E FFX_CACAO_EdgeSensitiveBlur3 ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur4.h -Vn CSEdgeSensitiveBlur4DXIL -E FFX_CACAO_EdgeSensitiveBlur4 ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur5.h -Vn CSEdgeSensitiveBlur5DXIL -E FFX_CACAO_EdgeSensitiveBlur5 ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur6.h -Vn CSEdgeSensitiveBlur6DXIL -E FFX_CACAO_EdgeSensitiveBlur6 ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur7.h -Vn CSEdgeSensitiveBlur7DXIL -E FFX_CACAO_EdgeSensitiveBlur7 ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur8.h -Vn CSEdgeSensitiveBlur8DXIL -E FFX_CACAO_EdgeSensitiveBlur8 ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOUpscaleBilateral5x5.h -Vn CSUpscaleBilateral5x5DXIL -E CSUpscaleBilateral5x5 ffx_cacao.hlsl -%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOUpscaleBilateral5x5Half.h -Vn CSUpscaleBilateral5x5HalfDXIL -E CSUpscaleBilateral5x5Half ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOApply.h -Vn CSApplyDXIL -E FFX_CACAO_Apply ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAONonSmartApply.h -Vn CSNonSmartApplyDXIL -E FFX_CACAO_NonSmartApply ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAONonSmartHalfApply.h -Vn CSNonSmartHalfApplyDXIL -E FFX_CACAO_NonSmartHalfApply ffx_cacao.hlsl + +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOUpscaleBilateral5x5NonSmart.h -Vn CSUpscaleBilateral5x5NonSmartDXIL -E FFX_CACAO_UpscaleBilateral5x5NonSmart ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOUpscaleBilateral5x5Smart.h -Vn CSUpscaleBilateral5x5SmartDXIL -E FFX_CACAO_UpscaleBilateral5x5Smart ffx_cacao.hlsl +%cauldron_dxc% -Fh PrecompiledShadersDXIL/CACAOUpscaleBilateral5x5Half.h -Vn CSUpscaleBilateral5x5HalfDXIL -E FFX_CACAO_UpscaleBilateral5x5Half ffx_cacao.hlsl popd diff --git a/ffx-cacao/src/build_shaders_spirv.bat b/ffx-cacao/src/build_shaders_spirv.bat index 6f821bd..b21690c 100644 --- a/ffx-cacao/src/build_shaders_spirv.bat +++ b/ffx-cacao/src/build_shaders_spirv.bat @@ -7,95 +7,97 @@ set cauldron_dxc_32=..\..\sample\libs\cauldron\libs\DXC\bin\dxc.exe -Wno-convers if not exist "PrecompiledShadersSPIRV" mkdir "PrecompiledShadersSPIRV" -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOClearLoadCounter_16.h -Vn CSClearLoadCounterSPIRV16 -E CSClearLoadCounter ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOClearLoadCounter_16.h -Vn CSClearLoadCounterSPIRV16 -E FFX_CACAO_ClearLoadCounter ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepths_16.h -Vn CSPrepareDownsampledDepthsSPIRV16 -E CSPrepareDownsampledDepths ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepths_16.h -Vn CSPrepareDownsampledDepthsSPIRV16 -E FFX_CACAO_PrepareDownsampledDepths ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeDepths_16.h -Vn CSPrepareNativeDepthsSPIRV16 -E CSPrepareNativeDepths ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeDepths_16.h -Vn CSPrepareNativeDepthsSPIRV16 -E FFX_CACAO_PrepareNativeDepths ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepthsAndMips_16.h -Vn CSPrepareDownsampledDepthsAndMipsSPIRV16 -E CSPrepareDownsampledDepthsAndMips ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeDepthsAndMips_16.h -Vn CSPrepareNativeDepthsAndMipsSPIRV16 -E CSPrepareNativeDepthsAndMips ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepthsAndMips_16.h -Vn CSPrepareDownsampledDepthsAndMipsSPIRV16 -E FFX_CACAO_PrepareDownsampledDepthsAndMips ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeDepthsAndMips_16.h -Vn CSPrepareNativeDepthsAndMipsSPIRV16 -E FFX_CACAO_PrepareNativeDepthsAndMips ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledNormals_16.h -Vn CSPrepareDownsampledNormalsSPIRV16 -E CSPrepareDownsampledNormals ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeNormals_16.h -Vn CSPrepareNativeNormalsSPIRV16 -E CSPrepareNativeNormals ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledNormals_16.h -Vn CSPrepareDownsampledNormalsSPIRV16 -E FFX_CACAO_PrepareDownsampledNormals ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeNormals_16.h -Vn CSPrepareNativeNormalsSPIRV16 -E FFX_CACAO_PrepareNativeNormals ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledNormalsFromInputNormals_16.h -Vn CSPrepareDownsampledNormalsFromInputNormalsSPIRV16 -E CSPrepareDownsampledNormalsFromInputNormals ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeNormalsFromInputNormals_16.h -Vn CSPrepareNativeNormalsFromInputNormalsSPIRV16 -E CSPrepareNativeNormalsFromInputNormals ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledNormalsFromInputNormals_16.h -Vn CSPrepareDownsampledNormalsFromInputNormalsSPIRV16 -E FFX_CACAO_PrepareDownsampledNormalsFromInputNormals ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeNormalsFromInputNormals_16.h -Vn CSPrepareNativeNormalsFromInputNormalsSPIRV16 -E FFX_CACAO_PrepareNativeNormalsFromInputNormals ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepthsHalf_16.h -Vn CSPrepareDownsampledDepthsHalfSPIRV16 -E CSPrepareDownsampledDepthsHalf ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeDepthsHalf_16.h -Vn CSPrepareNativeDepthsHalfSPIRV16 -E CSPrepareNativeDepthsHalf ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepthsHalf_16.h -Vn CSPrepareDownsampledDepthsHalfSPIRV16 -E FFX_CACAO_PrepareDownsampledDepthsHalf ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeDepthsHalf_16.h -Vn CSPrepareNativeDepthsHalfSPIRV16 -E FFX_CACAO_PrepareNativeDepthsHalf ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ0_16.h -Vn CSGenerateQ0SPIRV16 -E CSGenerateQ0 ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ1_16.h -Vn CSGenerateQ1SPIRV16 -E CSGenerateQ1 ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ2_16.h -Vn CSGenerateQ2SPIRV16 -E CSGenerateQ2 ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ3_16.h -Vn CSGenerateQ3SPIRV16 -E CSGenerateQ3 ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ3Base_16.h -Vn CSGenerateQ3BaseSPIRV16 -E CSGenerateQ3Base ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ0_16.h -Vn CSGenerateQ0SPIRV16 -E FFX_CACAO_GenerateQ0 ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ1_16.h -Vn CSGenerateQ1SPIRV16 -E FFX_CACAO_GenerateQ1 ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ2_16.h -Vn CSGenerateQ2SPIRV16 -E FFX_CACAO_GenerateQ2 ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ3_16.h -Vn CSGenerateQ3SPIRV16 -E FFX_CACAO_GenerateQ3 ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ3Base_16.h -Vn CSGenerateQ3BaseSPIRV16 -E FFX_CACAO_GenerateQ3Base ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOGenerateImportanceMap_16.h -Vn CSGenerateImportanceMapSPIRV16 -E CSGenerateImportanceMap ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPostprocessImportanceMapA_16.h -Vn CSPostprocessImportanceMapASPIRV16 -E CSPostprocessImportanceMapA ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPostprocessImportanceMapB_16.h -Vn CSPostprocessImportanceMapBSPIRV16 -E CSPostprocessImportanceMapB ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOGenerateImportanceMap_16.h -Vn CSGenerateImportanceMapSPIRV16 -E FFX_CACAO_GenerateImportanceMap ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPostprocessImportanceMapA_16.h -Vn CSPostprocessImportanceMapASPIRV16 -E FFX_CACAO_PostprocessImportanceMapA ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOPostprocessImportanceMapB_16.h -Vn CSPostprocessImportanceMapBSPIRV16 -E FFX_CACAO_PostprocessImportanceMapB ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur1_16.h -Vn CSEdgeSensitiveBlur1SPIRV16 -E CSEdgeSensitiveBlur1 ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur2_16.h -Vn CSEdgeSensitiveBlur2SPIRV16 -E CSEdgeSensitiveBlur2 ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur3_16.h -Vn CSEdgeSensitiveBlur3SPIRV16 -E CSEdgeSensitiveBlur3 ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur4_16.h -Vn CSEdgeSensitiveBlur4SPIRV16 -E CSEdgeSensitiveBlur4 ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur5_16.h -Vn CSEdgeSensitiveBlur5SPIRV16 -E CSEdgeSensitiveBlur5 ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur6_16.h -Vn CSEdgeSensitiveBlur6SPIRV16 -E CSEdgeSensitiveBlur6 ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur7_16.h -Vn CSEdgeSensitiveBlur7SPIRV16 -E CSEdgeSensitiveBlur7 ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur8_16.h -Vn CSEdgeSensitiveBlur8SPIRV16 -E CSEdgeSensitiveBlur8 ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur1_16.h -Vn CSEdgeSensitiveBlur1SPIRV16 -E FFX_CACAO_EdgeSensitiveBlur1 ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur2_16.h -Vn CSEdgeSensitiveBlur2SPIRV16 -E FFX_CACAO_EdgeSensitiveBlur2 ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur3_16.h -Vn CSEdgeSensitiveBlur3SPIRV16 -E FFX_CACAO_EdgeSensitiveBlur3 ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur4_16.h -Vn CSEdgeSensitiveBlur4SPIRV16 -E FFX_CACAO_EdgeSensitiveBlur4 ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur5_16.h -Vn CSEdgeSensitiveBlur5SPIRV16 -E FFX_CACAO_EdgeSensitiveBlur5 ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur6_16.h -Vn CSEdgeSensitiveBlur6SPIRV16 -E FFX_CACAO_EdgeSensitiveBlur6 ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur7_16.h -Vn CSEdgeSensitiveBlur7SPIRV16 -E FFX_CACAO_EdgeSensitiveBlur7 ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur8_16.h -Vn CSEdgeSensitiveBlur8SPIRV16 -E FFX_CACAO_EdgeSensitiveBlur8 ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOApply_16.h -Vn CSApplySPIRV16 -E CSApply ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAONonSmartApply_16.h -Vn CSNonSmartApplySPIRV16 -E CSNonSmartApply ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAONonSmartHalfApply_16.h -Vn CSNonSmartHalfApplySPIRV16 -E CSNonSmartHalfApply ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOApply_16.h -Vn CSApplySPIRV16 -E FFX_CACAO_Apply ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAONonSmartApply_16.h -Vn CSNonSmartApplySPIRV16 -E FFX_CACAO_NonSmartApply ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAONonSmartHalfApply_16.h -Vn CSNonSmartHalfApplySPIRV16 -E FFX_CACAO_NonSmartHalfApply ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5_16.h -Vn CSUpscaleBilateral5x5SPIRV16 -E CSUpscaleBilateral5x5 ffx_cacao.hlsl -%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5Half_16.h -Vn CSUpscaleBilateral5x5HalfSPIRV16 -E CSUpscaleBilateral5x5Half ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5Smart_16.h -Vn CSUpscaleBilateral5x5SmartSPIRV16 -E FFX_CACAO_UpscaleBilateral5x5Smart ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5NonSmart_16.h -Vn CSUpscaleBilateral5x5NonSmartSPIRV16 -E FFX_CACAO_UpscaleBilateral5x5NonSmart ffx_cacao.hlsl +%cauldron_dxc_16% -Fh PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5Half_16.h -Vn CSUpscaleBilateral5x5HalfSPIRV16 -E FFX_CACAO_UpscaleBilateral5x5Half ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOClearLoadCounter_32.h -Vn CSClearLoadCounterSPIRV32 -E CSClearLoadCounter ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOClearLoadCounter_32.h -Vn CSClearLoadCounterSPIRV32 -E FFX_CACAO_ClearLoadCounter ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepths_32.h -Vn CSPrepareDownsampledDepthsSPIRV32 -E CSPrepareDownsampledDepths ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepths_32.h -Vn CSPrepareDownsampledDepthsSPIRV32 -E FFX_CACAO_PrepareDownsampledDepths ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeDepths_32.h -Vn CSPrepareNativeDepthsSPIRV32 -E CSPrepareNativeDepths ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeDepths_32.h -Vn CSPrepareNativeDepthsSPIRV32 -E FFX_CACAO_PrepareNativeDepths ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepthsAndMips_32.h -Vn CSPrepareDownsampledDepthsAndMipsSPIRV32 -E CSPrepareDownsampledDepthsAndMips ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeDepthsAndMips_32.h -Vn CSPrepareNativeDepthsAndMipsSPIRV32 -E CSPrepareNativeDepthsAndMips ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepthsAndMips_32.h -Vn CSPrepareDownsampledDepthsAndMipsSPIRV32 -E FFX_CACAO_PrepareDownsampledDepthsAndMips ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeDepthsAndMips_32.h -Vn CSPrepareNativeDepthsAndMipsSPIRV32 -E FFX_CACAO_PrepareNativeDepthsAndMips ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledNormals_32.h -Vn CSPrepareDownsampledNormalsSPIRV32 -E CSPrepareDownsampledNormals ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeNormals_32.h -Vn CSPrepareNativeNormalsSPIRV32 -E CSPrepareNativeNormals ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledNormals_32.h -Vn CSPrepareDownsampledNormalsSPIRV32 -E FFX_CACAO_PrepareDownsampledNormals ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeNormals_32.h -Vn CSPrepareNativeNormalsSPIRV32 -E FFX_CACAO_PrepareNativeNormals ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledNormalsFromInputNormals_32.h -Vn CSPrepareDownsampledNormalsFromInputNormalsSPIRV32 -E CSPrepareDownsampledNormalsFromInputNormals ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeNormalsFromInputNormals_32.h -Vn CSPrepareNativeNormalsFromInputNormalsSPIRV32 -E CSPrepareNativeNormalsFromInputNormals ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledNormalsFromInputNormals_32.h -Vn CSPrepareDownsampledNormalsFromInputNormalsSPIRV32 -E FFX_CACAO_PrepareDownsampledNormalsFromInputNormals ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeNormalsFromInputNormals_32.h -Vn CSPrepareNativeNormalsFromInputNormalsSPIRV32 -E FFX_CACAO_PrepareNativeNormalsFromInputNormals ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepthsHalf_32.h -Vn CSPrepareDownsampledDepthsHalfSPIRV32 -E CSPrepareDownsampledDepthsHalf ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeDepthsHalf_32.h -Vn CSPrepareNativeDepthsHalfSPIRV32 -E CSPrepareNativeDepthsHalf ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepthsHalf_32.h -Vn CSPrepareDownsampledDepthsHalfSPIRV32 -E FFX_CACAO_PrepareDownsampledDepthsHalf ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPrepareNativeDepthsHalf_32.h -Vn CSPrepareNativeDepthsHalfSPIRV32 -E FFX_CACAO_PrepareNativeDepthsHalf ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ0_32.h -Vn CSGenerateQ0SPIRV32 -E CSGenerateQ0 ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ1_32.h -Vn CSGenerateQ1SPIRV32 -E CSGenerateQ1 ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ2_32.h -Vn CSGenerateQ2SPIRV32 -E CSGenerateQ2 ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ3_32.h -Vn CSGenerateQ3SPIRV32 -E CSGenerateQ3 ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ3Base_32.h -Vn CSGenerateQ3BaseSPIRV32 -E CSGenerateQ3Base ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ0_32.h -Vn CSGenerateQ0SPIRV32 -E FFX_CACAO_GenerateQ0 ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ1_32.h -Vn CSGenerateQ1SPIRV32 -E FFX_CACAO_GenerateQ1 ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ2_32.h -Vn CSGenerateQ2SPIRV32 -E FFX_CACAO_GenerateQ2 ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ3_32.h -Vn CSGenerateQ3SPIRV32 -E FFX_CACAO_GenerateQ3 ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOGenerateQ3Base_32.h -Vn CSGenerateQ3BaseSPIRV32 -E FFX_CACAO_GenerateQ3Base ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOGenerateImportanceMap_32.h -Vn CSGenerateImportanceMapSPIRV32 -E CSGenerateImportanceMap ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPostprocessImportanceMapA_32.h -Vn CSPostprocessImportanceMapASPIRV32 -E CSPostprocessImportanceMapA ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPostprocessImportanceMapB_32.h -Vn CSPostprocessImportanceMapBSPIRV32 -E CSPostprocessImportanceMapB ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOGenerateImportanceMap_32.h -Vn CSGenerateImportanceMapSPIRV32 -E FFX_CACAO_GenerateImportanceMap ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPostprocessImportanceMapA_32.h -Vn CSPostprocessImportanceMapASPIRV32 -E FFX_CACAO_PostprocessImportanceMapA ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOPostprocessImportanceMapB_32.h -Vn CSPostprocessImportanceMapBSPIRV32 -E FFX_CACAO_PostprocessImportanceMapB ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur1_32.h -Vn CSEdgeSensitiveBlur1SPIRV32 -E CSEdgeSensitiveBlur1 ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur2_32.h -Vn CSEdgeSensitiveBlur2SPIRV32 -E CSEdgeSensitiveBlur2 ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur3_32.h -Vn CSEdgeSensitiveBlur3SPIRV32 -E CSEdgeSensitiveBlur3 ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur4_32.h -Vn CSEdgeSensitiveBlur4SPIRV32 -E CSEdgeSensitiveBlur4 ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur5_32.h -Vn CSEdgeSensitiveBlur5SPIRV32 -E CSEdgeSensitiveBlur5 ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur6_32.h -Vn CSEdgeSensitiveBlur6SPIRV32 -E CSEdgeSensitiveBlur6 ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur7_32.h -Vn CSEdgeSensitiveBlur7SPIRV32 -E CSEdgeSensitiveBlur7 ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur8_32.h -Vn CSEdgeSensitiveBlur8SPIRV32 -E CSEdgeSensitiveBlur8 ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur1_32.h -Vn CSEdgeSensitiveBlur1SPIRV32 -E FFX_CACAO_EdgeSensitiveBlur1 ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur2_32.h -Vn CSEdgeSensitiveBlur2SPIRV32 -E FFX_CACAO_EdgeSensitiveBlur2 ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur3_32.h -Vn CSEdgeSensitiveBlur3SPIRV32 -E FFX_CACAO_EdgeSensitiveBlur3 ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur4_32.h -Vn CSEdgeSensitiveBlur4SPIRV32 -E FFX_CACAO_EdgeSensitiveBlur4 ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur5_32.h -Vn CSEdgeSensitiveBlur5SPIRV32 -E FFX_CACAO_EdgeSensitiveBlur5 ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur6_32.h -Vn CSEdgeSensitiveBlur6SPIRV32 -E FFX_CACAO_EdgeSensitiveBlur6 ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur7_32.h -Vn CSEdgeSensitiveBlur7SPIRV32 -E FFX_CACAO_EdgeSensitiveBlur7 ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur8_32.h -Vn CSEdgeSensitiveBlur8SPIRV32 -E FFX_CACAO_EdgeSensitiveBlur8 ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOApply_32.h -Vn CSApplySPIRV32 -E CSApply ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAONonSmartApply_32.h -Vn CSNonSmartApplySPIRV32 -E CSNonSmartApply ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAONonSmartHalfApply_32.h -Vn CSNonSmartHalfApplySPIRV32 -E CSNonSmartHalfApply ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOApply_32.h -Vn CSApplySPIRV32 -E FFX_CACAO_Apply ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAONonSmartApply_32.h -Vn CSNonSmartApplySPIRV32 -E FFX_CACAO_NonSmartApply ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAONonSmartHalfApply_32.h -Vn CSNonSmartHalfApplySPIRV32 -E FFX_CACAO_NonSmartHalfApply ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5_32.h -Vn CSUpscaleBilateral5x5SPIRV32 -E CSUpscaleBilateral5x5 ffx_cacao.hlsl -%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5Half_32.h -Vn CSUpscaleBilateral5x5HalfSPIRV32 -E CSUpscaleBilateral5x5Half ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5Smart_32.h -Vn CSUpscaleBilateral5x5SmartSPIRV32 -E FFX_CACAO_UpscaleBilateral5x5Smart ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5NonSmart_32.h -Vn CSUpscaleBilateral5x5NonSmartSPIRV32 -E FFX_CACAO_UpscaleBilateral5x5NonSmart ffx_cacao.hlsl +%cauldron_dxc_32% -Fh PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5Half_32.h -Vn CSUpscaleBilateral5x5HalfSPIRV32 -E FFX_CACAO_UpscaleBilateral5x5Half ffx_cacao.hlsl popd diff --git a/ffx-cacao/src/ffx_cacao.cpp b/ffx-cacao/src/ffx_cacao.cpp index 547bbf7..0c20374 100644 --- a/ffx-cacao/src/ffx_cacao.cpp +++ b/ffx-cacao/src/ffx_cacao.cpp @@ -1,4 +1,4 @@ -// Modifications Copyright © 2020. Advanced Micro Devices, Inc. All Rights Reserved. +// Modifications Copyright © 2021. Advanced Micro Devices, Inc. All Rights Reserved. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Copyright (c) 2016, Intel Corporation @@ -19,19 +19,14 @@ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #include "ffx_cacao.h" -#include "ffx_cacao_defines.h" #include <assert.h> #include <math.h> // cos, sin #include <string.h> // memcpy #include <stdio.h> // snprintf -#ifdef FFX_CACAO_ENABLE_D3D12 -#include <d3dx12.h> -#endif - // Define symbol to enable DirectX debug markers created using Cauldron -// #define FFX_CACAO_ENABLE_CAULDRON_DEBUG +#define FFX_CACAO_ENABLE_CAULDRON_DEBUG #define FFX_CACAO_ASSERT(exp) assert(exp) #define FFX_CACAO_ARRAY_SIZE(xs) (sizeof(xs)/sizeof(xs[0])) @@ -42,268 +37,73 @@ #define FFX_CACAO_CLAMP(value, lower, upper) FFX_CACAO_MIN(FFX_CACAO_MAX(value, lower), upper) #define FFX_CACAO_OFFSET_OF(T, member) (size_t)(&(((T*)0)->member)) -#ifdef FFX_CACAO_ENABLE_D3D12 -#include "PrecompiledShadersDXIL/CACAOPrepareDownsampledDepthsHalf.h" -#include "PrecompiledShadersDXIL/CACAOPrepareNativeDepthsHalf.h" - -#include "PrecompiledShadersDXIL/CACAOPrepareDownsampledDepthsAndMips.h" -#include "PrecompiledShadersDXIL/CACAOPrepareNativeDepthsAndMips.h" - -#include "PrecompiledShadersDXIL/CACAOPrepareDownsampledNormals.h" -#include "PrecompiledShadersDXIL/CACAOPrepareNativeNormals.h" - -#include "PrecompiledShadersDXIL/CACAOPrepareDownsampledNormalsFromInputNormals.h" -#include "PrecompiledShadersDXIL/CACAOPrepareNativeNormalsFromInputNormals.h" - -#include "PrecompiledShadersDXIL/CACAOPrepareDownsampledDepths.h" -#include "PrecompiledShadersDXIL/CACAOPrepareNativeDepths.h" - -#include "PrecompiledShadersDXIL/CACAOGenerateQ0.h" -#include "PrecompiledShadersDXIL/CACAOGenerateQ1.h" -#include "PrecompiledShadersDXIL/CACAOGenerateQ2.h" -#include "PrecompiledShadersDXIL/CACAOGenerateQ3.h" -#include "PrecompiledShadersDXIL/CACAOGenerateQ3Base.h" - -#include "PrecompiledShadersDXIL/CACAOGenerateImportanceMap.h" -#include "PrecompiledShadersDXIL/CACAOPostprocessImportanceMapA.h" -#include "PrecompiledShadersDXIL/CACAOPostprocessImportanceMapB.h" - -#include "PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur1.h" -#include "PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur2.h" -#include "PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur3.h" -#include "PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur4.h" -#include "PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur5.h" -#include "PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur6.h" -#include "PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur7.h" -#include "PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur8.h" - -#include "PrecompiledShadersDXIL/CACAOApply.h" -#include "PrecompiledShadersDXIL/CACAONonSmartApply.h" -#include "PrecompiledShadersDXIL/CACAONonSmartHalfApply.h" - -#include "PrecompiledShadersDXIL/CACAOUpscaleBilateral5x5.h" -#include "PrecompiledShadersDXIL/CACAOUpscaleBilateral5x5Half.h" -#endif - -#ifdef FFX_CACAO_ENABLE_VULKAN -// 16 bit versions -#include "PrecompiledShadersSPIRV/CACAOClearLoadCounter_16.h" - -#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepthsHalf_16.h" -#include "PrecompiledShadersSPIRV/CACAOPrepareNativeDepthsHalf_16.h" - -#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepthsAndMips_16.h" -#include "PrecompiledShadersSPIRV/CACAOPrepareNativeDepthsAndMips_16.h" - -#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledNormals_16.h" -#include "PrecompiledShadersSPIRV/CACAOPrepareNativeNormals_16.h" - -#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledNormalsFromInputNormals_16.h" -#include "PrecompiledShadersSPIRV/CACAOPrepareNativeNormalsFromInputNormals_16.h" - -#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepths_16.h" -#include "PrecompiledShadersSPIRV/CACAOPrepareNativeDepths_16.h" - -#include "PrecompiledShadersSPIRV/CACAOGenerateQ0_16.h" -#include "PrecompiledShadersSPIRV/CACAOGenerateQ1_16.h" -#include "PrecompiledShadersSPIRV/CACAOGenerateQ2_16.h" -#include "PrecompiledShadersSPIRV/CACAOGenerateQ3_16.h" -#include "PrecompiledShadersSPIRV/CACAOGenerateQ3Base_16.h" - -#include "PrecompiledShadersSPIRV/CACAOGenerateImportanceMap_16.h" -#include "PrecompiledShadersSPIRV/CACAOPostprocessImportanceMapA_16.h" -#include "PrecompiledShadersSPIRV/CACAOPostprocessImportanceMapB_16.h" - -#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur1_16.h" -#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur2_16.h" -#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur3_16.h" -#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur4_16.h" -#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur5_16.h" -#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur6_16.h" -#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur7_16.h" -#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur8_16.h" - -#include "PrecompiledShadersSPIRV/CACAOApply_16.h" -#include "PrecompiledShadersSPIRV/CACAONonSmartApply_16.h" -#include "PrecompiledShadersSPIRV/CACAONonSmartHalfApply_16.h" - -#include "PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5_16.h" -#include "PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5Half_16.h" - -// 32 bit versions -#include "PrecompiledShadersSPIRV/CACAOClearLoadCounter_32.h" - -#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepthsHalf_32.h" -#include "PrecompiledShadersSPIRV/CACAOPrepareNativeDepthsHalf_32.h" - -#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepthsAndMips_32.h" -#include "PrecompiledShadersSPIRV/CACAOPrepareNativeDepthsAndMips_32.h" - -#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledNormals_32.h" -#include "PrecompiledShadersSPIRV/CACAOPrepareNativeNormals_32.h" - -#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledNormalsFromInputNormals_32.h" -#include "PrecompiledShadersSPIRV/CACAOPrepareNativeNormalsFromInputNormals_32.h" - -#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepths_32.h" -#include "PrecompiledShadersSPIRV/CACAOPrepareNativeDepths_32.h" - -#include "PrecompiledShadersSPIRV/CACAOGenerateQ0_32.h" -#include "PrecompiledShadersSPIRV/CACAOGenerateQ1_32.h" -#include "PrecompiledShadersSPIRV/CACAOGenerateQ2_32.h" -#include "PrecompiledShadersSPIRV/CACAOGenerateQ3_32.h" -#include "PrecompiledShadersSPIRV/CACAOGenerateQ3Base_32.h" - -#include "PrecompiledShadersSPIRV/CACAOGenerateImportanceMap_32.h" -#include "PrecompiledShadersSPIRV/CACAOPostprocessImportanceMapA_32.h" -#include "PrecompiledShadersSPIRV/CACAOPostprocessImportanceMapB_32.h" - -#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur1_32.h" -#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur2_32.h" -#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur3_32.h" -#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur4_32.h" -#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur5_32.h" -#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur6_32.h" -#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur7_32.h" -#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur8_32.h" - -#include "PrecompiledShadersSPIRV/CACAOApply_32.h" -#include "PrecompiledShadersSPIRV/CACAONonSmartApply_32.h" -#include "PrecompiledShadersSPIRV/CACAONonSmartHalfApply_32.h" - -#include "PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5_32.h" -#include "PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5Half_32.h" -#endif - #define MATRIX_ROW_MAJOR_ORDER 1 -#define MAX_BLUR_PASSES 8 - -#ifdef FFX_CACAO_ENABLE_CAULDRON_DEBUG -#include <base/UserMarkers.h> - -#define USER_MARKER(name) CAULDRON_DX12::UserMarker __marker(commandList, name) -#else -#define USER_MARKER(name) -#endif - -typedef struct FfxCacaoConstants { - float DepthUnpackConsts[2]; - float CameraTanHalfFOV[2]; - - float NDCToViewMul[2]; - float NDCToViewAdd[2]; - - float DepthBufferUVToViewMul[2]; - float DepthBufferUVToViewAdd[2]; - - float EffectRadius; // world (viewspace) maximum size of the shadow - float EffectShadowStrength; // global strength of the effect (0 - 5) - float EffectShadowPow; - float EffectShadowClamp; - - float EffectFadeOutMul; // effect fade out from distance (ex. 25) - float EffectFadeOutAdd; // effect fade out to distance (ex. 100) - float EffectHorizonAngleThreshold; // limit errors on slopes and caused by insufficient geometry tessellation (0.05 to 0.5) - float EffectSamplingRadiusNearLimitRec; // if viewspace pixel closer than this, don't enlarge shadow sampling radius anymore (makes no sense to grow beyond some distance, not enough samples to cover everything, so just limit the shadow growth; could be SSAOSettingsFadeOutFrom * 0.1 or less) - - float DepthPrecisionOffsetMod; - float NegRecEffectRadius; // -1.0 / EffectRadius - float LoadCounterAvgDiv; // 1.0 / ( halfDepthMip[SSAO_DEPTH_MIP_LEVELS-1].sizeX * halfDepthMip[SSAO_DEPTH_MIP_LEVELS-1].sizeY ) - float AdaptiveSampleCountLimit; - - float InvSharpness; - int PassIndex; - float BilateralSigmaSquared; - float BilateralSimilarityDistanceSigma; - - float PatternRotScaleMatrices[5][4]; - - float NormalsUnpackMul; - float NormalsUnpackAdd; - float DetailAOStrength; - float Dummy0; - - float SSAOBufferDimensions[2]; - float SSAOBufferInverseDimensions[2]; - - float DepthBufferDimensions[2]; - float DepthBufferInverseDimensions[2]; - - int DepthBufferOffset[2]; - float PerPassFullResUVOffset[2]; - - float InputOutputBufferDimensions[2]; - float InputOutputBufferInverseDimensions[2]; - - float ImportanceMapDimensions[2]; - float ImportanceMapInverseDimensions[2]; - - float DeinterleavedDepthBufferDimensions[2]; - float DeinterleavedDepthBufferInverseDimensions[2]; - - float DeinterleavedDepthBufferOffset[2]; - float DeinterleavedDepthBufferNormalisedOffset[2]; - - FfxCacaoMatrix4x4 NormalsWorldToViewspaceMatrix; -} FfxCacaoConstants; - -typedef struct ScreenSizeInfo { - uint32_t width; - uint32_t height; - uint32_t halfWidth; - uint32_t halfHeight; - uint32_t quarterWidth; - uint32_t quarterHeight; - uint32_t eighthWidth; - uint32_t eighthHeight; - uint32_t depthBufferWidth; - uint32_t depthBufferHeight; - uint32_t depthBufferHalfWidth; - uint32_t depthBufferHalfHeight; - uint32_t depthBufferQuarterWidth; - uint32_t depthBufferQuarterHeight; - uint32_t depthBufferOffsetX; - uint32_t depthBufferOffsetY; - uint32_t depthBufferHalfOffsetX; - uint32_t depthBufferHalfOffsetY; -} ScreenSizeInfo; - -typedef struct BufferSizeInfo { - uint32_t inputOutputBufferWidth; - uint32_t inputOutputBufferHeight; - - uint32_t ssaoBufferWidth; - uint32_t ssaoBufferHeight; - - uint32_t depthBufferXOffset; - uint32_t depthBufferYOffset; - - uint32_t depthBufferWidth; - uint32_t depthBufferHeight; - - uint32_t deinterleavedDepthBufferXOffset; - uint32_t deinterleavedDepthBufferYOffset; - - uint32_t deinterleavedDepthBufferWidth; - uint32_t deinterleavedDepthBufferHeight; - - uint32_t importanceMapWidth; - uint32_t importanceMapHeight; -} BufferSizeInfo; - -static const FfxCacaoMatrix4x4 FFX_CACAO_IDENTITY_MATRIX = { +static const FFX_CACAO_Matrix4x4 FFX_CACAO_IDENTITY_MATRIX = { { { 1.0f, 0.0f, 0.0f, 0.0f }, { 0.0f, 1.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f, 0.0f, 1.0f } } }; -inline static uint32_t dispatchSize(uint32_t tileSize, uint32_t totalSize) +void FFX_CACAO_UpdateBufferSizeInfo(uint32_t width, uint32_t height, FFX_CACAO_Bool useDownsampledSsao, FFX_CACAO_BufferSizeInfo* bsi) { - return (totalSize + tileSize - 1) / tileSize; + uint32_t halfWidth = (width + 1) / 2; + uint32_t halfHeight = (height + 1) / 2; + uint32_t quarterWidth = (halfWidth + 1) / 2; + uint32_t quarterHeight = (halfHeight + 1) / 2; + uint32_t eighthWidth = (quarterWidth + 1) / 2; + uint32_t eighthHeight = (quarterHeight + 1) / 2; + + uint32_t depthBufferWidth = width; + uint32_t depthBufferHeight = height; + uint32_t depthBufferHalfWidth = halfWidth; + uint32_t depthBufferHalfHeight = halfHeight; + uint32_t depthBufferQuarterWidth = quarterWidth; + uint32_t depthBufferQuarterHeight = quarterHeight; + + uint32_t depthBufferXOffset = 0; + uint32_t depthBufferYOffset = 0; + uint32_t depthBufferHalfXOffset = 0; + uint32_t depthBufferHalfYOffset = 0; + uint32_t depthBufferQuarterXOffset = 0; + uint32_t depthBufferQuarterYOffset = 0; + + bsi->inputOutputBufferWidth = width; + bsi->inputOutputBufferHeight = height; + bsi->depthBufferXOffset = depthBufferXOffset; + bsi->depthBufferYOffset = depthBufferYOffset; + bsi->depthBufferWidth = depthBufferWidth; + bsi->depthBufferHeight = depthBufferHeight; + + if (useDownsampledSsao) + { + bsi->ssaoBufferWidth = quarterWidth; + bsi->ssaoBufferHeight = quarterHeight; + bsi->deinterleavedDepthBufferXOffset = depthBufferQuarterXOffset; + bsi->deinterleavedDepthBufferYOffset = depthBufferQuarterYOffset; + bsi->deinterleavedDepthBufferWidth = depthBufferQuarterWidth; + bsi->deinterleavedDepthBufferHeight = depthBufferQuarterHeight; + bsi->importanceMapWidth = eighthWidth; + bsi->importanceMapHeight = eighthHeight; + bsi->downsampledSsaoBufferWidth = halfWidth; + bsi->downsampledSsaoBufferHeight = halfHeight; + } + else + { + bsi->ssaoBufferWidth = halfWidth; + bsi->ssaoBufferHeight = halfHeight; + bsi->deinterleavedDepthBufferXOffset = depthBufferHalfXOffset; + bsi->deinterleavedDepthBufferYOffset = depthBufferHalfYOffset; + bsi->deinterleavedDepthBufferWidth = depthBufferHalfWidth; + bsi->deinterleavedDepthBufferHeight = depthBufferHalfHeight; + bsi->importanceMapWidth = quarterWidth; + bsi->importanceMapHeight = quarterHeight; + bsi->downsampledSsaoBufferWidth = 1; + bsi->downsampledSsaoBufferHeight = 1; + } } -static void updateConstants(FfxCacaoConstants* consts, FfxCacaoSettings* settings, BufferSizeInfo* bufferSizeInfo, const FfxCacaoMatrix4x4* proj, const FfxCacaoMatrix4x4* normalsToView) +void FFX_CACAO_UpdateConstants(FFX_CACAO_Constants* consts, const FFX_CACAO_Settings* settings, const FFX_CACAO_BufferSizeInfo* bufferSizeInfo, const FFX_CACAO_Matrix4x4* proj, const FFX_CACAO_Matrix4x4* normalsToView) { consts->BilateralSigmaSquared = settings->bilateralSigmaSquared; consts->BilateralSimilarityDistanceSigma = settings->bilateralSimilarityDistanceSigma; @@ -361,9 +161,6 @@ static void updateConstants(FfxCacaoConstants* consts, FfxCacaoSettings* setting // if the depth precision is switched to 32bit float, this can be set to something closer to 1 (0.9999 is fine) consts->DepthPrecisionOffsetMod = 0.9992f; - // consts->RadiusDistanceScalingFunctionPow = 1.0f - CLAMP( m_settings.RadiusDistanceScalingFunction, 0.0f, 1.0f ); - - // Special settings for lowest quality level - just nerf the effect a tiny bit if (settings->qualityLevel <= FFX_CACAO_QUALITY_LOW) { @@ -432,7 +229,7 @@ static void updateConstants(FfxCacaoConstants* consts, FfxCacaoSettings* setting } } -static void updatePerPassConstants(FfxCacaoConstants* consts, FfxCacaoSettings* settings, BufferSizeInfo* bufferSizeInfo, int pass) +void FFX_CACAO_UpdatePerPassConstants(FFX_CACAO_Constants* consts, const FFX_CACAO_Settings* settings, const FFX_CACAO_BufferSizeInfo* bufferSizeInfo, int pass) { consts->PerPassFullResUVOffset[0] = ((float)(pass % 2)) / (float)bufferSizeInfo->ssaoBufferWidth; consts->PerPassFullResUVOffset[1] = ((float)(pass / 2)) / (float)bufferSizeInfo->ssaoBufferHeight; @@ -452,13 +249,11 @@ static void updatePerPassConstants(FfxCacaoConstants* consts, FfxCacaoSettings* float ca, sa; float angle0 = ((float)a + (float)b / (float)subPassCount) * (3.1415926535897932384626433832795f) * 0.5f; - // angle0 += additionalAngleOffset; ca = FFX_CACAO_COS(angle0); sa = FFX_CACAO_SIN(angle0); float scale = 1.0f + (a - 1.5f + (b - (subPassCount - 1.0f) * 0.5f) / (float)subPassCount) * 0.07f; - // scale *= additionalRadiusScale; consts->PatternRotScaleMatrices[subPass][0] = scale * ca; consts->PatternRotScaleMatrices[subPass][1] = scale * -sa; @@ -466,3912 +261,3 @@ static void updatePerPassConstants(FfxCacaoConstants* consts, FfxCacaoSettings* consts->PatternRotScaleMatrices[subPass][3] = -scale * ca; } } - -#ifdef FFX_CACAO_ENABLE_PROFILING -// TIMESTAMP(name) -#define TIMESTAMPS \ - TIMESTAMP(BEGIN) \ - TIMESTAMP(PREPARE) \ - TIMESTAMP(BASE_SSAO_PASS) \ - TIMESTAMP(IMPORTANCE_MAP) \ - TIMESTAMP(GENERATE_SSAO) \ - TIMESTAMP(EDGE_SENSITIVE_BLUR) \ - TIMESTAMP(BILATERAL_UPSAMPLE) \ - TIMESTAMP(APPLY) - -typedef enum TimestampID { -#define TIMESTAMP(name) TIMESTAMP_##name, - TIMESTAMPS -#undef TIMESTAMP - NUM_TIMESTAMPS -} TimestampID; - -static const char *TIMESTAMP_NAMES[NUM_TIMESTAMPS] = { -#define TIMESTAMP(name) "FFX_CACAO_" #name, - TIMESTAMPS -#undef TIMESTAMP -}; - -#define NUM_TIMESTAMP_BUFFERS 5 -#endif - -// ================================================================================= -// DirectX 12 -// ================================================================================= - -#ifdef FFX_CACAO_ENABLE_D3D12 - -static inline FfxCacaoStatus hresultToFfxCacaoStatus(HRESULT hr) -{ - switch (hr) - { - case E_FAIL: return FFX_CACAO_STATUS_FAILED; - case E_INVALIDARG: return FFX_CACAO_STATUS_INVALID_ARGUMENT; - case E_OUTOFMEMORY: return FFX_CACAO_STATUS_OUT_OF_MEMORY; - case E_NOTIMPL: return FFX_CACAO_STATUS_INVALID_ARGUMENT; - case S_FALSE: return FFX_CACAO_STATUS_OK; - case S_OK: return FFX_CACAO_STATUS_OK; - default: return FFX_CACAO_STATUS_FAILED; - } -} - -static inline void SetName(ID3D12Object* obj, const char* name) -{ - if (name == NULL) - { - return; - } - - FFX_CACAO_ASSERT(obj != NULL); - wchar_t buffer[1024]; - swprintf(buffer, FFX_CACAO_ARRAY_SIZE(buffer), L"%S", name); - obj->SetName(buffer); -} - -static inline size_t AlignOffset(size_t uOffset, size_t uAlign) -{ - return ((uOffset + (uAlign - 1)) & ~(uAlign - 1)); -} - -static size_t GetPixelByteSize(DXGI_FORMAT fmt) -{ - switch (fmt) - { - case(DXGI_FORMAT_R10G10B10A2_TYPELESS): - case(DXGI_FORMAT_R10G10B10A2_UNORM): - case(DXGI_FORMAT_R10G10B10A2_UINT): - case(DXGI_FORMAT_R11G11B10_FLOAT): - case(DXGI_FORMAT_R8G8B8A8_TYPELESS): - case(DXGI_FORMAT_R8G8B8A8_UNORM): - case(DXGI_FORMAT_R8G8B8A8_UNORM_SRGB): - case(DXGI_FORMAT_R8G8B8A8_UINT): - case(DXGI_FORMAT_R8G8B8A8_SNORM): - case(DXGI_FORMAT_R8G8B8A8_SINT): - case(DXGI_FORMAT_B8G8R8A8_UNORM): - case(DXGI_FORMAT_B8G8R8X8_UNORM): - case(DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM): - case(DXGI_FORMAT_B8G8R8A8_TYPELESS): - case(DXGI_FORMAT_B8G8R8A8_UNORM_SRGB): - case(DXGI_FORMAT_B8G8R8X8_TYPELESS): - case(DXGI_FORMAT_B8G8R8X8_UNORM_SRGB): - case(DXGI_FORMAT_R16G16_TYPELESS): - case(DXGI_FORMAT_R16G16_FLOAT): - case(DXGI_FORMAT_R16G16_UNORM): - case(DXGI_FORMAT_R16G16_UINT): - case(DXGI_FORMAT_R16G16_SNORM): - case(DXGI_FORMAT_R16G16_SINT): - case(DXGI_FORMAT_R32_TYPELESS): - case(DXGI_FORMAT_D32_FLOAT): - case(DXGI_FORMAT_R32_FLOAT): - case(DXGI_FORMAT_R32_UINT): - case(DXGI_FORMAT_R32_SINT): - return 4; - - case(DXGI_FORMAT_BC1_TYPELESS): - case(DXGI_FORMAT_BC1_UNORM): - case(DXGI_FORMAT_BC1_UNORM_SRGB): - case(DXGI_FORMAT_BC4_TYPELESS): - case(DXGI_FORMAT_BC4_UNORM): - case(DXGI_FORMAT_BC4_SNORM): - case(DXGI_FORMAT_R16G16B16A16_FLOAT): - case(DXGI_FORMAT_R16G16B16A16_TYPELESS): - return 8; - - case(DXGI_FORMAT_BC2_TYPELESS): - case(DXGI_FORMAT_BC2_UNORM): - case(DXGI_FORMAT_BC2_UNORM_SRGB): - case(DXGI_FORMAT_BC3_TYPELESS): - case(DXGI_FORMAT_BC3_UNORM): - case(DXGI_FORMAT_BC3_UNORM_SRGB): - case(DXGI_FORMAT_BC5_TYPELESS): - case(DXGI_FORMAT_BC5_UNORM): - case(DXGI_FORMAT_BC5_SNORM): - case(DXGI_FORMAT_BC6H_TYPELESS): - case(DXGI_FORMAT_BC6H_UF16): - case(DXGI_FORMAT_BC6H_SF16): - case(DXGI_FORMAT_BC7_TYPELESS): - case(DXGI_FORMAT_BC7_UNORM): - case(DXGI_FORMAT_BC7_UNORM_SRGB): - case(DXGI_FORMAT_R32G32B32A32_FLOAT): - case(DXGI_FORMAT_R32G32B32A32_TYPELESS): - return 16; - - default: - FFX_CACAO_ASSERT(0); - break; - } - return 0; -} - -// ================================================================================================= -// GpuTimer implementation -// ================================================================================================= - -#ifdef FFX_CACAO_ENABLE_PROFILING -#define GPU_TIMER_MAX_VALUES_PER_FRAME (FFX_CACAO_ARRAY_SIZE(((FfxCacaoDetailedTiming*)0)->timestamps)) - -typedef struct D3D12Timestamp { - TimestampID timestampID; - uint64_t value; -} D3D12Timestamp; - -typedef struct GpuTimer { - ID3D12Resource *buffer; - ID3D12QueryHeap *queryHeap; - uint32_t currentFrame; - uint32_t collectFrame; - struct { - uint32_t len; - D3D12Timestamp timestamps[NUM_TIMESTAMPS]; - } timestampBuffers[NUM_TIMESTAMP_BUFFERS]; - -} GpuTimer; - -static FfxCacaoStatus gpuTimerInit(GpuTimer* gpuTimer, ID3D12Device* device) -{ - memset(gpuTimer, 0, sizeof(*gpuTimer)); - - D3D12_QUERY_HEAP_DESC queryHeapDesc = {}; - queryHeapDesc.Type = D3D12_QUERY_HEAP_TYPE_TIMESTAMP; - queryHeapDesc.Count = GPU_TIMER_MAX_VALUES_PER_FRAME * NUM_TIMESTAMP_BUFFERS; - queryHeapDesc.NodeMask = 0; - HRESULT hr = device->CreateQueryHeap(&queryHeapDesc, IID_PPV_ARGS(&gpuTimer->queryHeap)); - if (FAILED(hr)) - { - return hresultToFfxCacaoStatus(hr); - } - - hr = device->CreateCommittedResource( - &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_READBACK), - D3D12_HEAP_FLAG_NONE, - &CD3DX12_RESOURCE_DESC::Buffer(sizeof(uint64_t) * NUM_TIMESTAMP_BUFFERS * GPU_TIMER_MAX_VALUES_PER_FRAME), - D3D12_RESOURCE_STATE_COPY_DEST, - nullptr, - IID_PPV_ARGS(&gpuTimer->buffer)); - if (FAILED(hr)) - { - FFX_CACAO_ASSERT(gpuTimer->queryHeap); - gpuTimer->queryHeap->Release(); - return hresultToFfxCacaoStatus(hr); - } - - SetName(gpuTimer->buffer, "CACAO::GPUTimer::buffer"); - - return FFX_CACAO_STATUS_OK; -} - -static void gpuTimerDestroy(GpuTimer* gpuTimer) -{ - FFX_CACAO_ASSERT(gpuTimer->buffer); - FFX_CACAO_ASSERT(gpuTimer->queryHeap); - gpuTimer->buffer->Release(); - gpuTimer->queryHeap->Release(); -} - -static void gpuTimerStartFrame(GpuTimer* gpuTimer) -{ - uint32_t frame = gpuTimer->currentFrame = (gpuTimer->currentFrame + 1) % NUM_TIMESTAMP_BUFFERS; - gpuTimer->timestampBuffers[frame].len = 0; - - uint32_t collectFrame = gpuTimer->collectFrame = (frame + 1) % NUM_TIMESTAMP_BUFFERS; - - uint32_t numMeasurements = gpuTimer->timestampBuffers[collectFrame].len; - if (!numMeasurements) - { - return; - } - - uint32_t start = GPU_TIMER_MAX_VALUES_PER_FRAME * collectFrame; - uint32_t end = GPU_TIMER_MAX_VALUES_PER_FRAME * (collectFrame + 1); - - D3D12_RANGE readRange; - readRange.Begin = start * sizeof(uint64_t); - readRange.End = end * sizeof(uint64_t); - uint64_t *timingsInTicks = NULL; - gpuTimer->buffer->Map(0, &readRange, (void**)&timingsInTicks); - - for (uint32_t i = 0; i < numMeasurements; ++i) - { - gpuTimer->timestampBuffers[collectFrame].timestamps[i].value = timingsInTicks[start + i]; - } - - D3D12_RANGE writtenRange = {}; - writtenRange.Begin = 0; - writtenRange.End = 0; - gpuTimer->buffer->Unmap(0, &writtenRange); -} - -static void gpuTimerGetTimestamp(GpuTimer* gpuTimer, ID3D12GraphicsCommandList* commandList, TimestampID timestampID) -{ - uint32_t frame = gpuTimer->currentFrame; - uint32_t curTimestamp = gpuTimer->timestampBuffers[frame].len++; - FFX_CACAO_ASSERT(curTimestamp < GPU_TIMER_MAX_VALUES_PER_FRAME); - gpuTimer->timestampBuffers[frame].timestamps[curTimestamp].timestampID = timestampID; - commandList->EndQuery(gpuTimer->queryHeap, D3D12_QUERY_TYPE_TIMESTAMP, frame * GPU_TIMER_MAX_VALUES_PER_FRAME + curTimestamp); -} - -static void gpuTimerEndFrame(GpuTimer* gpuTimer, ID3D12GraphicsCommandList* commandList) -{ - uint32_t frame = gpuTimer->currentFrame; - uint32_t numTimestamps = gpuTimer->timestampBuffers[frame].len; - commandList->ResolveQueryData( - gpuTimer->queryHeap, - D3D12_QUERY_TYPE_TIMESTAMP, - frame * GPU_TIMER_MAX_VALUES_PER_FRAME, - numTimestamps, - gpuTimer->buffer, - frame * GPU_TIMER_MAX_VALUES_PER_FRAME * sizeof(uint64_t)); -} - -static void gpuTimerCollectTimings(GpuTimer* gpuTimer, FfxCacaoDetailedTiming* timings) -{ - uint32_t frame = gpuTimer->collectFrame; - uint32_t numTimestamps = timings->numTimestamps = gpuTimer->timestampBuffers[frame].len; - - uint64_t prevTimeTicks = gpuTimer->timestampBuffers[frame].timestamps[0].value; - for (uint32_t i = 1; i < numTimestamps; ++i) - { - uint64_t thisTimeTicks = gpuTimer->timestampBuffers[frame].timestamps[i].value; - FfxCacaoTimestamp *t = &timings->timestamps[i]; - t->label = TIMESTAMP_NAMES[gpuTimer->timestampBuffers[frame].timestamps[i].timestampID]; - t->ticks = thisTimeTicks - prevTimeTicks; - prevTimeTicks = thisTimeTicks; - } - - timings->timestamps[0].label = "FFX_CACAO_TOTAL"; - timings->timestamps[0].ticks = prevTimeTicks - gpuTimer->timestampBuffers[frame].timestamps[0].value; -} -#endif - -// ================================================================================================= -// CbvSrvUav implementation -// ================================================================================================= - -typedef struct CbvSrvUav { - uint32_t size; - uint32_t descriptorSize; - D3D12_CPU_DESCRIPTOR_HANDLE cpuDescriptor; - D3D12_GPU_DESCRIPTOR_HANDLE gpuDescriptor; - D3D12_CPU_DESCRIPTOR_HANDLE cpuVisibleCpuDescriptor; -} CbvSrvUav; - -static D3D12_CPU_DESCRIPTOR_HANDLE cbvSrvUavGetCpu(CbvSrvUav* cbvSrvUav, uint32_t i) -{ - D3D12_CPU_DESCRIPTOR_HANDLE cpuDescriptor = cbvSrvUav->cpuDescriptor; - cpuDescriptor.ptr += i * cbvSrvUav->descriptorSize; - return cpuDescriptor; -} - -static D3D12_CPU_DESCRIPTOR_HANDLE cbvSrvUavGetCpuVisibleCpu(CbvSrvUav* cbvSrvUav, uint32_t i) -{ - D3D12_CPU_DESCRIPTOR_HANDLE cpuDescriptor = cbvSrvUav->cpuVisibleCpuDescriptor; - cpuDescriptor.ptr += i * cbvSrvUav->descriptorSize; - return cpuDescriptor; -} - -static D3D12_GPU_DESCRIPTOR_HANDLE cbvSrvUavGetGpu(CbvSrvUav* cbvSrvUav, uint32_t i) -{ - D3D12_GPU_DESCRIPTOR_HANDLE gpuDescriptor = cbvSrvUav->gpuDescriptor; - gpuDescriptor.ptr += i * cbvSrvUav->descriptorSize; - return gpuDescriptor; -} - -// ================================================================================================= -// CbvSrvUavHeap implementation -// ================================================================================================= - -typedef struct CbvSrvUavHeap { - uint32_t index; - uint32_t descriptorCount; - uint32_t descriptorElementSize; - ID3D12DescriptorHeap *heap; - ID3D12DescriptorHeap *cpuVisibleHeap; -} ResourceViewHeap; - -static FfxCacaoStatus cbvSrvUavHeapInit(CbvSrvUavHeap* cbvSrvUavHeap, ID3D12Device* device, uint32_t descriptorCount) -{ - FFX_CACAO_ASSERT(cbvSrvUavHeap); - FFX_CACAO_ASSERT(device); - - cbvSrvUavHeap->descriptorCount = descriptorCount; - cbvSrvUavHeap->index = 0; - - cbvSrvUavHeap->descriptorElementSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); - - D3D12_DESCRIPTOR_HEAP_DESC descHeap; - descHeap.NumDescriptors = descriptorCount; - descHeap.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; - descHeap.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; - descHeap.NodeMask = 0; - - HRESULT hr = device->CreateDescriptorHeap(&descHeap, IID_PPV_ARGS(&cbvSrvUavHeap->heap)); - if (FAILED(hr)) - { - return hresultToFfxCacaoStatus(hr); - } - - SetName(cbvSrvUavHeap->heap, "FfxCacaoCbvSrvUavHeap"); - - D3D12_DESCRIPTOR_HEAP_DESC cpuVisibleDescHeap; - cpuVisibleDescHeap.NumDescriptors = descriptorCount; - cpuVisibleDescHeap.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; - cpuVisibleDescHeap.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; - cpuVisibleDescHeap.NodeMask = 0; - - hr = device->CreateDescriptorHeap(&cpuVisibleDescHeap, IID_PPV_ARGS(&cbvSrvUavHeap->cpuVisibleHeap)); - if (FAILED(hr)) - { - FFX_CACAO_ASSERT(cbvSrvUavHeap->heap); - cbvSrvUavHeap->heap->Release(); - return hresultToFfxCacaoStatus(hr); - } - - SetName(cbvSrvUavHeap->cpuVisibleHeap, "FfxCacaoCbvSrvUavCpuVisibleHeap"); - return FFX_CACAO_STATUS_OK; -} - -static void cbvSrvUavHeapDestroy(CbvSrvUavHeap* cbvSrvUavHeap) -{ - FFX_CACAO_ASSERT(cbvSrvUavHeap); - FFX_CACAO_ASSERT(cbvSrvUavHeap->heap); - FFX_CACAO_ASSERT(cbvSrvUavHeap->cpuVisibleHeap); - cbvSrvUavHeap->heap->Release(); - cbvSrvUavHeap->cpuVisibleHeap->Release(); -} - -static void cbvSrvUavHeapAllocDescriptor(CbvSrvUavHeap* cbvSrvUavHeap, CbvSrvUav* cbvSrvUav, uint32_t size) -{ - FFX_CACAO_ASSERT(cbvSrvUavHeap); - FFX_CACAO_ASSERT(cbvSrvUav); - FFX_CACAO_ASSERT(cbvSrvUavHeap->index + size <= cbvSrvUavHeap->descriptorCount); - - D3D12_CPU_DESCRIPTOR_HANDLE cpuView = cbvSrvUavHeap->heap->GetCPUDescriptorHandleForHeapStart(); - cpuView.ptr += cbvSrvUavHeap->index * cbvSrvUavHeap->descriptorElementSize; - - D3D12_GPU_DESCRIPTOR_HANDLE gpuView = cbvSrvUavHeap->heap->GetGPUDescriptorHandleForHeapStart(); - gpuView.ptr += cbvSrvUavHeap->index * cbvSrvUavHeap->descriptorElementSize; - - D3D12_CPU_DESCRIPTOR_HANDLE cpuVisibleCpuView = cbvSrvUavHeap->cpuVisibleHeap->GetCPUDescriptorHandleForHeapStart(); - cpuVisibleCpuView.ptr += cbvSrvUavHeap->index * cbvSrvUavHeap->descriptorElementSize; - - cbvSrvUavHeap->index += size; - - cbvSrvUav->size = size; - cbvSrvUav->descriptorSize = cbvSrvUavHeap->descriptorElementSize; - cbvSrvUav->cpuDescriptor = cpuView; - cbvSrvUav->gpuDescriptor = gpuView; - cbvSrvUav->cpuVisibleCpuDescriptor = cpuVisibleCpuView; -} - -// ================================================================================================= -// ConstantBufferRing implementation -// ================================================================================================= - -typedef struct ConstantBufferRing { - size_t pageSize; - size_t totalSize; - size_t currentOffset; - uint32_t currentPage; - uint32_t numPages; - char *data; - ID3D12Resource *buffer; -} ConstantBufferRing; - -static FfxCacaoStatus constantBufferRingInit(ConstantBufferRing* constantBufferRing, ID3D12Device* device, uint32_t numPages, size_t pageSize) -{ - FFX_CACAO_ASSERT(constantBufferRing); - FFX_CACAO_ASSERT(device); - - pageSize = AlignOffset(pageSize, 256); - size_t totalSize = numPages * pageSize; - char *data = NULL; - ID3D12Resource *buffer = NULL; - - HRESULT hr = device->CreateCommittedResource( - &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD), - D3D12_HEAP_FLAG_NONE, - &CD3DX12_RESOURCE_DESC::Buffer(totalSize), - D3D12_RESOURCE_STATE_GENERIC_READ, - nullptr, - IID_PPV_ARGS(&buffer)); - if (FAILED(hr)) - { - return hresultToFfxCacaoStatus(hr); - } - - SetName(buffer, "DynamicBufferRing::m_pBuffer"); - - buffer->Map(0, NULL, (void**)&data); - - constantBufferRing->pageSize = pageSize; - constantBufferRing->totalSize = totalSize; - constantBufferRing->currentOffset = 0; - constantBufferRing->currentPage = 0; - constantBufferRing->numPages = numPages; - constantBufferRing->data = data; - constantBufferRing->buffer = buffer; - - return FFX_CACAO_STATUS_OK; -} - -static void constantBufferRingDestroy(ConstantBufferRing* constantBufferRing) -{ - FFX_CACAO_ASSERT(constantBufferRing); - FFX_CACAO_ASSERT(constantBufferRing->buffer); - constantBufferRing->buffer->Release(); -} - -static void constantBufferRingStartFrame(ConstantBufferRing* constantBufferRing) -{ - FFX_CACAO_ASSERT(constantBufferRing); - constantBufferRing->currentPage = (constantBufferRing->currentPage + 1) % constantBufferRing->numPages; - constantBufferRing->currentOffset = 0; -} - -static void constantBufferRingAlloc(ConstantBufferRing* constantBufferRing, size_t size, void **data, D3D12_GPU_VIRTUAL_ADDRESS *bufferViewDesc) -{ - FFX_CACAO_ASSERT(constantBufferRing); - size = AlignOffset(size, 256); - FFX_CACAO_ASSERT(constantBufferRing->currentOffset + size <= constantBufferRing->pageSize); - - size_t memOffset = constantBufferRing->pageSize * constantBufferRing->currentPage + constantBufferRing->currentOffset; - *data = constantBufferRing->data + memOffset; - constantBufferRing->currentOffset += size; - - *bufferViewDesc = constantBufferRing->buffer->GetGPUVirtualAddress() + memOffset; -} - -// ================================================================================================= -// ComputeShader implementation -// ================================================================================================= - -typedef struct ComputeShader { - ID3D12RootSignature *rootSignature; - ID3D12PipelineState *pipelineState; -} ComputeShader; - -static FfxCacaoStatus computeShaderInit(ComputeShader* computeShader, ID3D12Device* device, const char* name, const void* bytecode, size_t bytecodeLength, uint32_t uavTableSize, uint32_t srvTableSize, D3D12_STATIC_SAMPLER_DESC* staticSamplers, uint32_t numStaticSamplers) -{ - FFX_CACAO_ASSERT(computeShader); - FFX_CACAO_ASSERT(device); - FFX_CACAO_ASSERT(name); - FFX_CACAO_ASSERT(bytecode); - FFX_CACAO_ASSERT(staticSamplers); - - D3D12_SHADER_BYTECODE shaderByteCode = {}; - shaderByteCode.pShaderBytecode = bytecode; - shaderByteCode.BytecodeLength = bytecodeLength; - - // Create root signature - { - CD3DX12_DESCRIPTOR_RANGE DescRange[4]; - CD3DX12_ROOT_PARAMETER RTSlot[4]; - - // we'll always have a constant buffer - int parameterCount = 0; - DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0); - RTSlot[parameterCount++].InitAsConstantBufferView(0, 0, D3D12_SHADER_VISIBILITY_ALL); - - // if we have a UAV table - if (uavTableSize > 0) - { - DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, uavTableSize, 0); - RTSlot[parameterCount].InitAsDescriptorTable(1, &DescRange[parameterCount], D3D12_SHADER_VISIBILITY_ALL); - ++parameterCount; - } - - // if we have a SRV table - if (srvTableSize > 0) - { - DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, srvTableSize, 0); - RTSlot[parameterCount].InitAsDescriptorTable(1, &DescRange[parameterCount], D3D12_SHADER_VISIBILITY_ALL); - ++parameterCount; - } - - // the root signature contains 3 slots to be used - CD3DX12_ROOT_SIGNATURE_DESC descRootSignature = CD3DX12_ROOT_SIGNATURE_DESC(); - descRootSignature.NumParameters = parameterCount; - descRootSignature.pParameters = RTSlot; - descRootSignature.NumStaticSamplers = numStaticSamplers; - descRootSignature.pStaticSamplers = staticSamplers; - - // deny uneccessary access to certain pipeline stages - descRootSignature.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE; - - ID3DBlob *outBlob, *errorBlob = NULL; - - HRESULT hr = D3D12SerializeRootSignature(&descRootSignature, D3D_ROOT_SIGNATURE_VERSION_1, &outBlob, &errorBlob); - if (FAILED(hr)) - { - return hresultToFfxCacaoStatus(hr); - } - - if (errorBlob) - { - errorBlob->Release(); - if (outBlob) - { - outBlob->Release(); - } - return FFX_CACAO_STATUS_FAILED; - } - - hr = device->CreateRootSignature(0, outBlob->GetBufferPointer(), outBlob->GetBufferSize(), IID_PPV_ARGS(&computeShader->rootSignature)); - if (FAILED(hr)) - { - outBlob->Release(); - return hresultToFfxCacaoStatus(hr); - } - - char nameBuffer[1024] = "PostProcCS::m_pRootSignature::"; - strncat_s(nameBuffer, name, FFX_CACAO_ARRAY_SIZE(nameBuffer)); - SetName(computeShader->rootSignature, nameBuffer); - - outBlob->Release(); - } - - // Create pipeline state - { - D3D12_COMPUTE_PIPELINE_STATE_DESC descPso = {}; - descPso.CS = shaderByteCode; - descPso.Flags = D3D12_PIPELINE_STATE_FLAG_NONE; - descPso.pRootSignature = computeShader->rootSignature; - descPso.NodeMask = 0; - - HRESULT hr = device->CreateComputePipelineState(&descPso, IID_PPV_ARGS(&computeShader->pipelineState)); - if (FAILED(hr)) - { - computeShader->rootSignature->Release(); - return hresultToFfxCacaoStatus(hr); - } - - char nameBuffer[1024] = "PostProcCS::m_pPipeline::"; - strncat_s(nameBuffer, name, FFX_CACAO_ARRAY_SIZE(nameBuffer)); - SetName(computeShader->rootSignature, nameBuffer); - } - - return FFX_CACAO_STATUS_OK; -} - -static void computeShaderDestroy(ComputeShader* computeShader) -{ - FFX_CACAO_ASSERT(computeShader); - FFX_CACAO_ASSERT(computeShader->rootSignature); - FFX_CACAO_ASSERT(computeShader->pipelineState); - computeShader->rootSignature->Release(); - computeShader->pipelineState->Release(); -} - -static void computeShaderDraw(ComputeShader* computeShader, ID3D12GraphicsCommandList* commandList, D3D12_GPU_VIRTUAL_ADDRESS constantBuffer, CbvSrvUav *uavTable, CbvSrvUav *srvTable, uint32_t width, uint32_t height, uint32_t depth) -{ - FFX_CACAO_ASSERT(computeShader); - FFX_CACAO_ASSERT(commandList); - FFX_CACAO_ASSERT(uavTable); - FFX_CACAO_ASSERT(srvTable); - FFX_CACAO_ASSERT(computeShader->pipelineState); - FFX_CACAO_ASSERT(computeShader->rootSignature); - - commandList->SetComputeRootSignature(computeShader->rootSignature); - - int params = 0; - commandList->SetComputeRootConstantBufferView(params++, constantBuffer); - if (uavTable) - { - commandList->SetComputeRootDescriptorTable(params++, uavTable->gpuDescriptor); - } - if (srvTable) - { - commandList->SetComputeRootDescriptorTable(params++, srvTable->gpuDescriptor); - } - - commandList->SetPipelineState(computeShader->pipelineState); - commandList->Dispatch(width, height, depth); -} - -// ================================================================================================= -// Texture implementation -// ================================================================================================= - -typedef struct Texture { - ID3D12Resource *resource; - DXGI_FORMAT format; - uint32_t width; - uint32_t height; - uint32_t arraySize; - uint32_t mipMapCount; -} Texture; - -static FfxCacaoStatus textureInit(Texture* texture, ID3D12Device* device, const char* name, const CD3DX12_RESOURCE_DESC* desc, D3D12_RESOURCE_STATES initialState, const D3D12_CLEAR_VALUE* clearValue) -{ - FFX_CACAO_ASSERT(texture); - FFX_CACAO_ASSERT(device); - FFX_CACAO_ASSERT(name); - FFX_CACAO_ASSERT(desc); - - HRESULT hr = device->CreateCommittedResource( - &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), - D3D12_HEAP_FLAG_NONE, - desc, - initialState, - clearValue, - IID_PPV_ARGS(&texture->resource)); - if (FAILED(hr)) - { - return hresultToFfxCacaoStatus(hr); - } - - texture->format = desc->Format; - texture->width = (uint32_t)desc->Width; - texture->height = desc->Height; - texture->arraySize = desc->DepthOrArraySize; - texture->mipMapCount = desc->MipLevels; - - SetName(texture->resource, name); - - return FFX_CACAO_STATUS_OK; -} - -static void textureDestroy(Texture* texture) -{ - FFX_CACAO_ASSERT(texture); - FFX_CACAO_ASSERT(texture->resource); - texture->resource->Release(); -} - -static void textureCreateSrvFromDesc(Texture* texture, uint32_t index, CbvSrvUav* srv, const D3D12_SHADER_RESOURCE_VIEW_DESC* srvDesc) -{ - FFX_CACAO_ASSERT(texture); - FFX_CACAO_ASSERT(srv); - FFX_CACAO_ASSERT(srvDesc); - - ID3D12Device* device; - texture->resource->GetDevice(__uuidof(*device), (void**)&device); - - device->CreateShaderResourceView(texture->resource, srvDesc, cbvSrvUavGetCpu(srv, index)); - device->CreateShaderResourceView(texture->resource, srvDesc, cbvSrvUavGetCpuVisibleCpu(srv, index)); - - device->Release(); -} - -static void textureCreateSrv(Texture* texture, uint32_t index, CbvSrvUav* srv, int mipLevel, int arraySize, int firstArraySlice) -{ - FFX_CACAO_ASSERT(texture); - FFX_CACAO_ASSERT(srv); - - D3D12_RESOURCE_DESC resourceDesc = texture->resource->GetDesc(); - - D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; - - srvDesc.Format = resourceDesc.Format == DXGI_FORMAT_D32_FLOAT ? DXGI_FORMAT_R32_FLOAT : resourceDesc.Format; - if (resourceDesc.SampleDesc.Count == 1) - { - if (resourceDesc.DepthOrArraySize == 1) - { - srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; - srvDesc.Texture2D.MostDetailedMip = (mipLevel == -1) ? 0 : mipLevel; - srvDesc.Texture2D.MipLevels = (mipLevel == -1) ? texture->mipMapCount : 1; - FFX_CACAO_ASSERT(arraySize == -1); - FFX_CACAO_ASSERT(firstArraySlice == -1); - } - else - { - srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DARRAY; - srvDesc.Texture2DArray.MostDetailedMip = (mipLevel == -1) ? 0 : mipLevel; - srvDesc.Texture2DArray.MipLevels = (mipLevel == -1) ? texture->mipMapCount : 1; - srvDesc.Texture2DArray.FirstArraySlice = (firstArraySlice == -1) ? 0 : firstArraySlice; - srvDesc.Texture2DArray.ArraySize = (arraySize == -1) ? resourceDesc.DepthOrArraySize : arraySize; - } - } - else - { - if (resourceDesc.DepthOrArraySize == 1) - { - srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DMS; - FFX_CACAO_ASSERT(mipLevel == -1); - FFX_CACAO_ASSERT(arraySize == -1); - FFX_CACAO_ASSERT(firstArraySlice == -1); - } - else - { - srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DMSARRAY; - srvDesc.Texture2DMSArray.FirstArraySlice = (firstArraySlice == -1) ? 0 : firstArraySlice; - srvDesc.Texture2DMSArray.ArraySize = (arraySize == -1) ? resourceDesc.DepthOrArraySize : arraySize; - FFX_CACAO_ASSERT(mipLevel == -1); - } - } - srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; - - textureCreateSrvFromDesc(texture, index, srv, &srvDesc); -} - -static void textureCreateUavFromDesc(Texture* texture, uint32_t index, CbvSrvUav* uav, const D3D12_UNORDERED_ACCESS_VIEW_DESC* uavDesc) -{ - FFX_CACAO_ASSERT(texture); - FFX_CACAO_ASSERT(uav); - FFX_CACAO_ASSERT(uavDesc); - - ID3D12Device* device; - texture->resource->GetDevice(__uuidof(*device), (void**)&device); - - device->CreateUnorderedAccessView(texture->resource, NULL, uavDesc, cbvSrvUavGetCpu(uav, index)); - device->CreateUnorderedAccessView(texture->resource, NULL, uavDesc, cbvSrvUavGetCpuVisibleCpu(uav, index)); - - device->Release(); -} - -static void textureCreateUav(Texture* texture, uint32_t index, CbvSrvUav* uav, int mipLevel, int arraySize, int firstArraySlice) -{ - FFX_CACAO_ASSERT(texture); - FFX_CACAO_ASSERT(uav); - - D3D12_RESOURCE_DESC resourceDesc = texture->resource->GetDesc(); - - D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc = {}; - - uavDesc.Format = resourceDesc.Format; - if (arraySize == -1) - { - FFX_CACAO_ASSERT(firstArraySlice == -1); - uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D; - uavDesc.Texture2D.MipSlice = (mipLevel == -1) ? 0 : mipLevel; - } - else - { - uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2DARRAY; - uavDesc.Texture2DArray.ArraySize = arraySize; - uavDesc.Texture2DArray.FirstArraySlice = firstArraySlice; - uavDesc.Texture2DArray.MipSlice = (mipLevel == -1) ? 0 : mipLevel; - } - - textureCreateUavFromDesc(texture, index, uav, &uavDesc); -} - -// ================================================================================================= -// CACAO implementation -// ================================================================================================= - -struct FfxCacaoD3D12Context { - FfxCacaoSettings settings; - FfxCacaoBool useDownsampledSsao; - - ID3D12Device *device; - CbvSrvUavHeap cbvSrvUavHeap; - -#ifdef FFX_CACAO_ENABLE_PROFILING - GpuTimer gpuTimer; -#endif - - ConstantBufferRing constantBufferRing; - BufferSizeInfo bufferSizeInfo; - ID3D12Resource *outputResource; - - // ========================================== - // Prepare shaders/resources - - ComputeShader prepareDownsampledDepthsAndMips; - ComputeShader prepareNativeDepthsAndMips; - - ComputeShader prepareDownsampledNormals; - ComputeShader prepareNativeNormals; - - ComputeShader prepareDownsampledNormalsFromInputNormals; - ComputeShader prepareNativeNormalsFromInputNormals; - - ComputeShader prepareDownsampledDepths; - ComputeShader prepareNativeDepths; - - ComputeShader prepareDownsampledDepthsHalf; - ComputeShader prepareNativeDepthsHalf; - - CbvSrvUav prepareDepthsNormalsAndMipsInputs; // <-- this is just the depth source - CbvSrvUav prepareDepthsAndMipsOutputs; - CbvSrvUav prepareDepthsOutputs; - CbvSrvUav prepareNormalsOutput; - CbvSrvUav prepareNormalsFromInputNormalsInput; - CbvSrvUav prepareNormalsFromInputNormalsOutput; - - // ========================================== - // Generate SSAO shaders/resources - - ComputeShader generateSSAO[5]; - - CbvSrvUav generateSSAOInputs[4]; - CbvSrvUav generateAdaptiveSSAOInputs[4]; - CbvSrvUav generateSSAOOutputsPing[4]; - CbvSrvUav generateSSAOOutputsPong[4]; - - // ========================================== - // Importance map generate/post process shaders/resources - - ComputeShader generateImportanceMap; - ComputeShader postprocessImportanceMapA; - ComputeShader postprocessImportanceMapB; - - CbvSrvUav generateImportanceMapInputs; - CbvSrvUav generateImportanceMapOutputs; - CbvSrvUav generateImportanceMapAInputs; - CbvSrvUav generateImportanceMapAOutputs; - CbvSrvUav generateImportanceMapBInputs; - CbvSrvUav generateImportanceMapBOutputs; - - // ========================================== - // De-interleave Blur shaders/resources - - ComputeShader edgeSensitiveBlur[8]; - - CbvSrvUav edgeSensitiveBlurInput[4]; - CbvSrvUav edgeSensitiveBlurOutput[4]; - - // ========================================== - // Apply shaders/resources - - ComputeShader smartApply; - ComputeShader nonSmartApply; - ComputeShader nonSmartHalfApply; - - CbvSrvUav createOutputInputsPing; - CbvSrvUav createOutputInputsPong; - CbvSrvUav createOutputOutputs; - - // ========================================== - // upscale shaders/resources - - ComputeShader upscaleBilateral5x5; - ComputeShader upscaleBilateral5x5Half; - - CbvSrvUav bilateralUpscaleInputsPing; - CbvSrvUav bilateralUpscaleInputsPong; - CbvSrvUav bilateralUpscaleOutputs; - - // ========================================== - // Intermediate buffers - - Texture deinterleavedDepths; - Texture deinterleavedNormals; - Texture ssaoBufferPing; - Texture ssaoBufferPong; - Texture importanceMap; - Texture importanceMapPong; - Texture loadCounter; - - CbvSrvUav loadCounterUav; // required for LoadCounter clear -}; - -static inline FfxCacaoD3D12Context* getAlignedD3D12ContextPointer(FfxCacaoD3D12Context* ptr) -{ - uintptr_t tmp = (uintptr_t)ptr; - tmp = (tmp + alignof(FfxCacaoD3D12Context) - 1) & (~(alignof(FfxCacaoD3D12Context) - 1)); - return (FfxCacaoD3D12Context*)tmp; -} -#endif - -#ifdef FFX_CACAO_ENABLE_VULKAN -// ================================================================================================= -// CACAO vulkan implementation -// ================================================================================================= - -// DESCRIPTOR_SET_LAYOUT(name, num_inputs, num_outputs) -#define DESCRIPTOR_SET_LAYOUTS \ - DESCRIPTOR_SET_LAYOUT(CLEAR_LOAD_COUNTER, 0, 1) \ - DESCRIPTOR_SET_LAYOUT(PREPARE_DEPTHS, 1, 1) \ - DESCRIPTOR_SET_LAYOUT(PREPARE_DEPTHS_MIPS, 1, 4) \ - DESCRIPTOR_SET_LAYOUT(PREPARE_NORMALS, 1, 1) \ - DESCRIPTOR_SET_LAYOUT(PREPARE_NORMALS_FROM_INPUT_NORMALS, 1, 1) \ - DESCRIPTOR_SET_LAYOUT(GENERATE, 7, 1) \ - DESCRIPTOR_SET_LAYOUT(GENERATE_ADAPTIVE, 7, 1) \ - DESCRIPTOR_SET_LAYOUT(GENERATE_IMPORTANCE_MAP, 1, 1) \ - DESCRIPTOR_SET_LAYOUT(POSTPROCESS_IMPORTANCE_MAP_A, 1, 1) \ - DESCRIPTOR_SET_LAYOUT(POSTPROCESS_IMPORTANCE_MAP_B, 1, 2) \ - DESCRIPTOR_SET_LAYOUT(EDGE_SENSITIVE_BLUR, 1, 1) \ - DESCRIPTOR_SET_LAYOUT(APPLY, 1, 1) \ - DESCRIPTOR_SET_LAYOUT(BILATERAL_UPSAMPLE, 4, 1) - -typedef enum DescriptorSetLayoutID { -#define DESCRIPTOR_SET_LAYOUT(name, _num_inputs, _num_outputs) DSL_##name, - DESCRIPTOR_SET_LAYOUTS -#undef DESCRIPTOR_SET_LAYOUT - NUM_DESCRIPTOR_SET_LAYOUTS -} DescriptorSetLayoutID; - -typedef struct DescriptorSetLayoutMetaData { - uint32_t numInputs; - uint32_t numOutputs; - const char *name; -} DescriptorSetLayoutMetaData; - -static const DescriptorSetLayoutMetaData DESCRIPTOR_SET_LAYOUT_META_DATA[NUM_DESCRIPTOR_SET_LAYOUTS] = { -#define DESCRIPTOR_SET_LAYOUT(name, num_inputs, num_outputs) { num_inputs, num_outputs, "FFX_CACAO_DSL_" #name }, - DESCRIPTOR_SET_LAYOUTS -#undef DESCRIPTOR_SET_LAYOUT -}; - -#define MAX_DESCRIPTOR_BINDINGS 32 -// define all the data for compute shaders -// COMPUTE_SHADER(enum_name, pascal_case_name, descriptor_set) -#define COMPUTE_SHADERS \ - COMPUTE_SHADER(CLEAR_LOAD_COUNTER, ClearLoadCounter, CLEAR_LOAD_COUNTER) \ - \ - COMPUTE_SHADER(PREPARE_DOWNSAMPLED_DEPTHS, PrepareDownsampledDepths, PREPARE_DEPTHS) \ - COMPUTE_SHADER(PREPARE_NATIVE_DEPTHS, PrepareNativeDepths, PREPARE_DEPTHS) \ - COMPUTE_SHADER(PREPARE_DOWNSAMPLED_DEPTHS_AND_MIPS, PrepareDownsampledDepthsAndMips, PREPARE_DEPTHS_MIPS) \ - COMPUTE_SHADER(PREPARE_NATIVE_DEPTHS_AND_MIPS, PrepareNativeDepthsAndMips, PREPARE_DEPTHS_MIPS) \ - COMPUTE_SHADER(PREPARE_DOWNSAMPLED_NORMALS, PrepareDownsampledNormals, PREPARE_NORMALS) \ - COMPUTE_SHADER(PREPARE_NATIVE_NORMALS, PrepareNativeNormals, PREPARE_NORMALS) \ - COMPUTE_SHADER(PREPARE_DOWNSAMPLED_NORMALS_FROM_INPUT_NORMALS, PrepareDownsampledNormalsFromInputNormals, PREPARE_NORMALS_FROM_INPUT_NORMALS) \ - COMPUTE_SHADER(PREPARE_NATIVE_NORMALS_FROM_INPUT_NORMALS, PrepareNativeNormalsFromInputNormals, PREPARE_NORMALS_FROM_INPUT_NORMALS) \ - COMPUTE_SHADER(PREPARE_DOWNSAMPLED_DEPTHS_HALF, PrepareDownsampledDepthsHalf, PREPARE_DEPTHS) \ - COMPUTE_SHADER(PREPARE_NATIVE_DEPTHS_HALF, PrepareNativeDepthsHalf, PREPARE_DEPTHS) \ - \ - COMPUTE_SHADER(GENERATE_Q0, GenerateQ0, GENERATE) \ - COMPUTE_SHADER(GENERATE_Q1, GenerateQ1, GENERATE) \ - COMPUTE_SHADER(GENERATE_Q2, GenerateQ2, GENERATE) \ - COMPUTE_SHADER(GENERATE_Q3, GenerateQ3, GENERATE_ADAPTIVE) \ - COMPUTE_SHADER(GENERATE_Q3_BASE, GenerateQ3Base, GENERATE) \ - \ - COMPUTE_SHADER(GENERATE_IMPORTANCE_MAP, GenerateImportanceMap, GENERATE_IMPORTANCE_MAP) \ - COMPUTE_SHADER(POSTPROCESS_IMPORTANCE_MAP_A, PostprocessImportanceMapA, POSTPROCESS_IMPORTANCE_MAP_A) \ - COMPUTE_SHADER(POSTPROCESS_IMPORTANCE_MAP_B, PostprocessImportanceMapB, POSTPROCESS_IMPORTANCE_MAP_B) \ - \ - COMPUTE_SHADER(EDGE_SENSITIVE_BLUR_1, EdgeSensitiveBlur1, EDGE_SENSITIVE_BLUR) \ - COMPUTE_SHADER(EDGE_SENSITIVE_BLUR_2, EdgeSensitiveBlur2, EDGE_SENSITIVE_BLUR) \ - COMPUTE_SHADER(EDGE_SENSITIVE_BLUR_3, EdgeSensitiveBlur3, EDGE_SENSITIVE_BLUR) \ - COMPUTE_SHADER(EDGE_SENSITIVE_BLUR_4, EdgeSensitiveBlur4, EDGE_SENSITIVE_BLUR) \ - COMPUTE_SHADER(EDGE_SENSITIVE_BLUR_5, EdgeSensitiveBlur5, EDGE_SENSITIVE_BLUR) \ - COMPUTE_SHADER(EDGE_SENSITIVE_BLUR_6, EdgeSensitiveBlur6, EDGE_SENSITIVE_BLUR) \ - COMPUTE_SHADER(EDGE_SENSITIVE_BLUR_7, EdgeSensitiveBlur7, EDGE_SENSITIVE_BLUR) \ - COMPUTE_SHADER(EDGE_SENSITIVE_BLUR_8, EdgeSensitiveBlur8, EDGE_SENSITIVE_BLUR) \ - \ - COMPUTE_SHADER(APPLY, Apply, APPLY) \ - COMPUTE_SHADER(NON_SMART_APPLY, NonSmartApply, APPLY) \ - COMPUTE_SHADER(NON_SMART_HALF_APPLY, NonSmartHalfApply, APPLY) \ - \ - COMPUTE_SHADER(UPSCALE_BILATERAL_5X5, UpscaleBilateral5x5, BILATERAL_UPSAMPLE) \ - COMPUTE_SHADER(UPSCALE_BILATERAL_5X5_HALF, UpscaleBilateral5x5Half, BILATERAL_UPSAMPLE) - -typedef enum ComputeShaderID { -#define COMPUTE_SHADER(name, _pascal_name, _descriptor_set) CS_##name, - COMPUTE_SHADERS -#undef COMPUTE_SHADER - NUM_COMPUTE_SHADERS -} ComputeShaderID; - -typedef struct ComputeShaderMetaData { - const uint32_t *shaderSpirv16; - size_t spirv16Len; - const uint32_t *shaderSpirv32; - size_t spirv32Len; - const char *name; - DescriptorSetLayoutID descriptorSetLayoutID; - const char *objectName; -} ComputeShaderMetaData; - -static const ComputeShaderMetaData COMPUTE_SHADER_META_DATA[NUM_COMPUTE_SHADERS] = { -#define COMPUTE_SHADER(name, pascal_name, descriptor_set_layout) { (uint32_t*)CS##pascal_name##SPIRV16, FFX_CACAO_ARRAY_SIZE(CS##pascal_name##SPIRV16), (uint32_t*)CS##pascal_name##SPIRV32, FFX_CACAO_ARRAY_SIZE(CS##pascal_name##SPIRV32), "CS"#pascal_name, DSL_##descriptor_set_layout, "FFX_CACAO_CS_"#name }, - COMPUTE_SHADERS -#undef COMPUTE_SHADER -}; - -#define TEXTURES \ - TEXTURE(DEINTERLEAVED_DEPTHS, deinterleavedDepthBufferWidth, deinterleavedDepthBufferHeight, VK_FORMAT_R16_SFLOAT, 4, 4) \ - TEXTURE(DEINTERLEAVED_NORMALS, ssaoBufferWidth, ssaoBufferHeight, VK_FORMAT_R8G8B8A8_SNORM, 4, 1) \ - TEXTURE(SSAO_BUFFER_PING, ssaoBufferWidth, ssaoBufferHeight, VK_FORMAT_R8G8_UNORM, 4, 1) \ - TEXTURE(SSAO_BUFFER_PONG, ssaoBufferWidth, ssaoBufferHeight, VK_FORMAT_R8G8_UNORM, 4, 1) \ - TEXTURE(IMPORTANCE_MAP, importanceMapWidth, importanceMapHeight, VK_FORMAT_R8_UNORM, 1, 1) \ - TEXTURE(IMPORTANCE_MAP_PONG, importanceMapWidth, importanceMapHeight, VK_FORMAT_R8_UNORM, 1, 1) - -typedef enum TextureID { -#define TEXTURE(name, _width, _height, _format, _array_size, _num_mips) TEXTURE_##name, - TEXTURES -#undef TEXTURE - NUM_TEXTURES -} TextureID; - -typedef struct TextureMetaData { - size_t widthOffset; - size_t heightOffset; - VkFormat format; - uint32_t arraySize; - uint32_t numMips; - const char *name; -} TextureMetaData; - -static const TextureMetaData TEXTURE_META_DATA[NUM_TEXTURES] = { -#define TEXTURE(name, width, height, format, array_size, num_mips) { FFX_CACAO_OFFSET_OF(BufferSizeInfo, width), FFX_CACAO_OFFSET_OF(BufferSizeInfo, height), format, array_size, num_mips, "FFX_CACAO_" #name }, - TEXTURES -#undef TEXTURE -}; - -// SHADER_RESOURCE_VIEW(name, texture, view_dimension, most_detailed_mip, mip_levels, first_array_slice, array_size) -#define SHADER_RESOURCE_VIEWS \ - SHADER_RESOURCE_VIEW(DEINTERLEAVED_DEPTHS, DEINTERLEAVED_DEPTHS, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 4, 0, 4) \ - SHADER_RESOURCE_VIEW(DEINTERLEAVED_DEPTHS_0, DEINTERLEAVED_DEPTHS, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 4, 0, 1) \ - SHADER_RESOURCE_VIEW(DEINTERLEAVED_DEPTHS_1, DEINTERLEAVED_DEPTHS, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 4, 1, 1) \ - SHADER_RESOURCE_VIEW(DEINTERLEAVED_DEPTHS_2, DEINTERLEAVED_DEPTHS, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 4, 2, 1) \ - SHADER_RESOURCE_VIEW(DEINTERLEAVED_DEPTHS_3, DEINTERLEAVED_DEPTHS, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 4, 3, 1) \ - SHADER_RESOURCE_VIEW(DEINTERLEAVED_NORMALS, DEINTERLEAVED_NORMALS, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 1, 0, 4) \ - SHADER_RESOURCE_VIEW(IMPORTANCE_MAP, IMPORTANCE_MAP, VK_IMAGE_VIEW_TYPE_2D, 0, 1, 0, 1) \ - SHADER_RESOURCE_VIEW(IMPORTANCE_MAP_PONG, IMPORTANCE_MAP_PONG, VK_IMAGE_VIEW_TYPE_2D, 0, 1, 0, 1) \ - SHADER_RESOURCE_VIEW(SSAO_BUFFER_PING, SSAO_BUFFER_PING, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 1, 0, 4) \ - SHADER_RESOURCE_VIEW(SSAO_BUFFER_PING_0, SSAO_BUFFER_PING, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 1, 0, 1) \ - SHADER_RESOURCE_VIEW(SSAO_BUFFER_PING_1, SSAO_BUFFER_PING, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 1, 1, 1) \ - SHADER_RESOURCE_VIEW(SSAO_BUFFER_PING_2, SSAO_BUFFER_PING, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 1, 2, 1) \ - SHADER_RESOURCE_VIEW(SSAO_BUFFER_PING_3, SSAO_BUFFER_PING, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 1, 3, 1) \ - SHADER_RESOURCE_VIEW(SSAO_BUFFER_PONG, SSAO_BUFFER_PONG, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 1, 0, 4) \ - SHADER_RESOURCE_VIEW(SSAO_BUFFER_PONG_0, SSAO_BUFFER_PONG, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 1, 0, 1) \ - SHADER_RESOURCE_VIEW(SSAO_BUFFER_PONG_1, SSAO_BUFFER_PONG, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 1, 1, 1) \ - SHADER_RESOURCE_VIEW(SSAO_BUFFER_PONG_2, SSAO_BUFFER_PONG, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 1, 2, 1) \ - SHADER_RESOURCE_VIEW(SSAO_BUFFER_PONG_3, SSAO_BUFFER_PONG, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 1, 3, 1) - -typedef enum ShaderResourceViewID { -#define SHADER_RESOURCE_VIEW(name, _texture, _view_dimension, _most_detailed_mip, _mip_levels, _first_array_slice, _array_size) SRV_##name, - SHADER_RESOURCE_VIEWS -#undef SHADER_RESOURCE_VIEW - NUM_SHADER_RESOURCE_VIEWS -} ShaderResourceViewID; - -typedef struct ShaderResourceViewMetaData { - TextureID texture; - VkImageViewType viewType; - uint32_t mostDetailedMip; - uint32_t mipLevels; - uint32_t firstArraySlice; - uint32_t arraySize; -} ShaderResourceViewMetaData; - -static const ShaderResourceViewMetaData SRV_META_DATA[NUM_SHADER_RESOURCE_VIEWS] = { -#define SHADER_RESOURCE_VIEW(_name, texture, view_dimension, most_detailed_mip, mip_levels, first_array_slice, array_size) { TEXTURE_##texture, view_dimension, most_detailed_mip, mip_levels, first_array_slice, array_size }, - SHADER_RESOURCE_VIEWS -#undef SHADER_RESOURCE_VIEW -}; - -// UNORDERED_ACCESS_VIEW(name, texture, view_dimension, mip_slice, first_array_slice, array_size) -#define UNORDERED_ACCESS_VIEWS \ - UNORDERED_ACCESS_VIEW(DEINTERLEAVED_DEPTHS_MIP_0, DEINTERLEAVED_DEPTHS, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 0, 4) \ - UNORDERED_ACCESS_VIEW(DEINTERLEAVED_DEPTHS_MIP_1, DEINTERLEAVED_DEPTHS, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 1, 0, 4) \ - UNORDERED_ACCESS_VIEW(DEINTERLEAVED_DEPTHS_MIP_2, DEINTERLEAVED_DEPTHS, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 2, 0, 4) \ - UNORDERED_ACCESS_VIEW(DEINTERLEAVED_DEPTHS_MIP_3, DEINTERLEAVED_DEPTHS, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 3, 0, 4) \ - UNORDERED_ACCESS_VIEW(DEINTERLEAVED_NORMALS, DEINTERLEAVED_NORMALS, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 0, 4) \ - UNORDERED_ACCESS_VIEW(IMPORTANCE_MAP, IMPORTANCE_MAP, VK_IMAGE_VIEW_TYPE_2D, 0, 0, 1) \ - UNORDERED_ACCESS_VIEW(IMPORTANCE_MAP_PONG, IMPORTANCE_MAP_PONG, VK_IMAGE_VIEW_TYPE_2D, 0, 0, 1) \ - UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PING, SSAO_BUFFER_PING, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 0, 4) \ - UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PING_0, SSAO_BUFFER_PING, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 0, 1) \ - UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PING_1, SSAO_BUFFER_PING, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 1, 1) \ - UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PING_2, SSAO_BUFFER_PING, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 2, 1) \ - UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PING_3, SSAO_BUFFER_PING, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 3, 1) \ - UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PONG, SSAO_BUFFER_PONG, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 0, 4) \ - UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PONG_0, SSAO_BUFFER_PONG, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 0, 1) \ - UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PONG_1, SSAO_BUFFER_PONG, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 1, 1) \ - UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PONG_2, SSAO_BUFFER_PONG, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 2, 1) \ - UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PONG_3, SSAO_BUFFER_PONG, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 0, 3, 1) - -typedef enum UnorderedAccessViewID { -#define UNORDERED_ACCESS_VIEW(name, _texture, _view_dimension, _mip_slice, _first_array_slice, _array_size) UAV_##name, - UNORDERED_ACCESS_VIEWS -#undef UNORDERED_ACCESS_VIEW - NUM_UNORDERED_ACCESS_VIEWS -} UnorderedAccessViewID; - -typedef struct UnorderedAccessViewMetaData { - TextureID textureID; - VkImageViewType viewType; - uint32_t mostDetailedMip; - uint32_t firstArraySlice; - uint32_t arraySize; -} UnorderedAccessViewMetaData; - -static const UnorderedAccessViewMetaData UAV_META_DATA[NUM_UNORDERED_ACCESS_VIEWS] = { -#define UNORDERED_ACCESS_VIEW(_name, texture, view_dimension, mip_slice, first_array_slice, array_size) { TEXTURE_##texture, view_dimension, mip_slice, first_array_slice, array_size }, - UNORDERED_ACCESS_VIEWS -#undef UNORDERED_ACCESS_VIEW -}; - -// DESCRIPTOR_SET(name, layout_name, pass) -#define DESCRIPTOR_SETS \ - DESCRIPTOR_SET(CLEAR_LOAD_COUNTER, CLEAR_LOAD_COUNTER, 0) \ - DESCRIPTOR_SET(PREPARE_DEPTHS, PREPARE_DEPTHS, 0) \ - DESCRIPTOR_SET(PREPARE_DEPTHS_MIPS, PREPARE_DEPTHS_MIPS, 0) \ - DESCRIPTOR_SET(PREPARE_NORMALS, PREPARE_NORMALS, 0) \ - DESCRIPTOR_SET(PREPARE_NORMALS_FROM_INPUT_NORMALS, PREPARE_NORMALS_FROM_INPUT_NORMALS, 0) \ - DESCRIPTOR_SET(GENERATE_ADAPTIVE_BASE_0, GENERATE, 0) \ - DESCRIPTOR_SET(GENERATE_ADAPTIVE_BASE_1, GENERATE, 1) \ - DESCRIPTOR_SET(GENERATE_ADAPTIVE_BASE_2, GENERATE, 2) \ - DESCRIPTOR_SET(GENERATE_ADAPTIVE_BASE_3, GENERATE, 3) \ - DESCRIPTOR_SET(GENERATE_0, GENERATE, 0) \ - DESCRIPTOR_SET(GENERATE_1, GENERATE, 1) \ - DESCRIPTOR_SET(GENERATE_2, GENERATE, 2) \ - DESCRIPTOR_SET(GENERATE_3, GENERATE, 3) \ - DESCRIPTOR_SET(GENERATE_ADAPTIVE_0, GENERATE_ADAPTIVE, 0) \ - DESCRIPTOR_SET(GENERATE_ADAPTIVE_1, GENERATE_ADAPTIVE, 1) \ - DESCRIPTOR_SET(GENERATE_ADAPTIVE_2, GENERATE_ADAPTIVE, 2) \ - DESCRIPTOR_SET(GENERATE_ADAPTIVE_3, GENERATE_ADAPTIVE, 3) \ - DESCRIPTOR_SET(GENERATE_IMPORTANCE_MAP, GENERATE_IMPORTANCE_MAP, 0) \ - DESCRIPTOR_SET(POSTPROCESS_IMPORTANCE_MAP_A, POSTPROCESS_IMPORTANCE_MAP_A, 0) \ - DESCRIPTOR_SET(POSTPROCESS_IMPORTANCE_MAP_B, POSTPROCESS_IMPORTANCE_MAP_B, 0) \ - DESCRIPTOR_SET(EDGE_SENSITIVE_BLUR_0, EDGE_SENSITIVE_BLUR, 0) \ - DESCRIPTOR_SET(EDGE_SENSITIVE_BLUR_1, EDGE_SENSITIVE_BLUR, 1) \ - DESCRIPTOR_SET(EDGE_SENSITIVE_BLUR_2, EDGE_SENSITIVE_BLUR, 2) \ - DESCRIPTOR_SET(EDGE_SENSITIVE_BLUR_3, EDGE_SENSITIVE_BLUR, 3) \ - DESCRIPTOR_SET(APPLY_PING, APPLY, 0) \ - DESCRIPTOR_SET(APPLY_PONG, APPLY, 0) \ - DESCRIPTOR_SET(BILATERAL_UPSAMPLE_PING, BILATERAL_UPSAMPLE, 0) \ - DESCRIPTOR_SET(BILATERAL_UPSAMPLE_PONG, BILATERAL_UPSAMPLE, 0) - -typedef enum DescriptorSetID { -#define DESCRIPTOR_SET(name, _layout_name, _pass) DS_##name, - DESCRIPTOR_SETS -#undef DESCRIPTOR_SET - NUM_DESCRIPTOR_SETS -} DescriptorSetID; - -typedef struct DescriptorSetMetaData { - DescriptorSetLayoutID descriptorSetLayoutID; - uint32_t pass; - const char *name; -} DescriptorSetMetaData; - -static const DescriptorSetMetaData DESCRIPTOR_SET_META_DATA[NUM_DESCRIPTOR_SETS] = { -#define DESCRIPTOR_SET(name, layout_name, pass) { DSL_##layout_name, pass, "FFX_CACAO_DS_" #name }, - DESCRIPTOR_SETS -#undef DESCRIPTOR_SET -}; - -// INPUT_DESCRIPTOR(descriptor_set_name, srv_name, binding_num) -#define INPUT_DESCRIPTOR_BINDINGS \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_0, DEINTERLEAVED_DEPTHS_0, 0) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_0, DEINTERLEAVED_NORMALS, 6) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_1, DEINTERLEAVED_DEPTHS_1, 0) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_1, DEINTERLEAVED_NORMALS, 6) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_2, DEINTERLEAVED_DEPTHS_2, 0) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_2, DEINTERLEAVED_NORMALS, 6) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_3, DEINTERLEAVED_DEPTHS_3, 0) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_3, DEINTERLEAVED_NORMALS, 6) \ - \ - INPUT_DESCRIPTOR_BINDING(GENERATE_0, DEINTERLEAVED_DEPTHS_0, 0) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_0, DEINTERLEAVED_NORMALS, 6) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_1, DEINTERLEAVED_DEPTHS_1, 0) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_1, DEINTERLEAVED_NORMALS, 6) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_2, DEINTERLEAVED_DEPTHS_2, 0) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_2, DEINTERLEAVED_NORMALS, 6) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_3, DEINTERLEAVED_DEPTHS_3, 0) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_3, DEINTERLEAVED_NORMALS, 6) \ - \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_0, DEINTERLEAVED_DEPTHS_0, 0) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_0, IMPORTANCE_MAP, 3) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_0, SSAO_BUFFER_PONG_0, 4) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_0, DEINTERLEAVED_NORMALS, 6) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_1, DEINTERLEAVED_DEPTHS_1, 0) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_1, IMPORTANCE_MAP, 3) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_1, SSAO_BUFFER_PONG_1, 4) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_1, DEINTERLEAVED_NORMALS, 6) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_2, DEINTERLEAVED_DEPTHS_2, 0) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_2, IMPORTANCE_MAP, 3) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_2, SSAO_BUFFER_PONG_2, 4) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_2, DEINTERLEAVED_NORMALS, 6) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_3, DEINTERLEAVED_DEPTHS_3, 0) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_3, IMPORTANCE_MAP, 3) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_3, SSAO_BUFFER_PONG_3, 4) \ - INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_3, DEINTERLEAVED_NORMALS, 6) \ - \ - INPUT_DESCRIPTOR_BINDING(GENERATE_IMPORTANCE_MAP, SSAO_BUFFER_PONG, 0) \ - INPUT_DESCRIPTOR_BINDING(POSTPROCESS_IMPORTANCE_MAP_A, IMPORTANCE_MAP, 0) \ - INPUT_DESCRIPTOR_BINDING(POSTPROCESS_IMPORTANCE_MAP_B, IMPORTANCE_MAP_PONG, 0) \ - \ - INPUT_DESCRIPTOR_BINDING(EDGE_SENSITIVE_BLUR_0, SSAO_BUFFER_PING_0, 0) \ - INPUT_DESCRIPTOR_BINDING(EDGE_SENSITIVE_BLUR_1, SSAO_BUFFER_PING_1, 0) \ - INPUT_DESCRIPTOR_BINDING(EDGE_SENSITIVE_BLUR_2, SSAO_BUFFER_PING_2, 0) \ - INPUT_DESCRIPTOR_BINDING(EDGE_SENSITIVE_BLUR_3, SSAO_BUFFER_PING_3, 0) \ - \ - INPUT_DESCRIPTOR_BINDING(BILATERAL_UPSAMPLE_PING, SSAO_BUFFER_PING, 0) \ - INPUT_DESCRIPTOR_BINDING(BILATERAL_UPSAMPLE_PING, DEINTERLEAVED_DEPTHS, 3) \ - INPUT_DESCRIPTOR_BINDING(BILATERAL_UPSAMPLE_PONG, SSAO_BUFFER_PONG, 0) \ - INPUT_DESCRIPTOR_BINDING(BILATERAL_UPSAMPLE_PONG, DEINTERLEAVED_DEPTHS, 3) \ - \ - INPUT_DESCRIPTOR_BINDING(APPLY_PING, SSAO_BUFFER_PING, 0) \ - INPUT_DESCRIPTOR_BINDING(APPLY_PONG, SSAO_BUFFER_PONG, 0) - -// need this to define NUM_INPUT_DESCRIPTOR_BINDINGS -typedef enum InputDescriptorBindingID { -#define INPUT_DESCRIPTOR_BINDING(descriptor_set_name, srv_name, _binding_num) INPUT_DESCRIPTOR_BINDING_##descriptor_set_name##_##srv_name, - INPUT_DESCRIPTOR_BINDINGS -#undef INPUT_DESCRIPTOR_BINDING - NUM_INPUT_DESCRIPTOR_BINDINGS -} InputDescriptorBindingID; - -typedef struct InputDescriptorBindingMetaData { - DescriptorSetID descriptorID; - ShaderResourceViewID srvID; - uint32_t bindingNumber; -} InputDescriptorBindingMetaData; - -static const InputDescriptorBindingMetaData INPUT_DESCRIPTOR_BINDING_META_DATA[NUM_INPUT_DESCRIPTOR_BINDINGS] = { -#define INPUT_DESCRIPTOR_BINDING(descriptor_set_name, srv_name, binding_num) { DS_##descriptor_set_name, SRV_##srv_name, binding_num }, - INPUT_DESCRIPTOR_BINDINGS -#undef INPUT_DESCRIPTOR_BINDING -}; - -// OUTPUT_DESCRIPTOR(descriptor_set_name, uav_name, binding_num) -#define OUTPUT_DESCRIPTOR_BINDINGS \ - OUTPUT_DESCRIPTOR_BINDING(PREPARE_DEPTHS, DEINTERLEAVED_DEPTHS_MIP_0, 0) \ - OUTPUT_DESCRIPTOR_BINDING(PREPARE_DEPTHS_MIPS, DEINTERLEAVED_DEPTHS_MIP_0, 0) \ - OUTPUT_DESCRIPTOR_BINDING(PREPARE_DEPTHS_MIPS, DEINTERLEAVED_DEPTHS_MIP_1, 1) \ - OUTPUT_DESCRIPTOR_BINDING(PREPARE_DEPTHS_MIPS, DEINTERLEAVED_DEPTHS_MIP_2, 2) \ - OUTPUT_DESCRIPTOR_BINDING(PREPARE_DEPTHS_MIPS, DEINTERLEAVED_DEPTHS_MIP_3, 3) \ - OUTPUT_DESCRIPTOR_BINDING(PREPARE_NORMALS, DEINTERLEAVED_NORMALS, 0) \ - OUTPUT_DESCRIPTOR_BINDING(PREPARE_NORMALS_FROM_INPUT_NORMALS, DEINTERLEAVED_NORMALS, 0) \ - OUTPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_0, SSAO_BUFFER_PONG_0, 0) \ - OUTPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_1, SSAO_BUFFER_PONG_1, 0) \ - OUTPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_2, SSAO_BUFFER_PONG_2, 0) \ - OUTPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_3, SSAO_BUFFER_PONG_3, 0) \ - OUTPUT_DESCRIPTOR_BINDING(GENERATE_0, SSAO_BUFFER_PING_0, 0) \ - OUTPUT_DESCRIPTOR_BINDING(GENERATE_1, SSAO_BUFFER_PING_1, 0) \ - OUTPUT_DESCRIPTOR_BINDING(GENERATE_2, SSAO_BUFFER_PING_2, 0) \ - OUTPUT_DESCRIPTOR_BINDING(GENERATE_3, SSAO_BUFFER_PING_3, 0) \ - OUTPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_0, SSAO_BUFFER_PING_0, 0) \ - OUTPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_1, SSAO_BUFFER_PING_1, 0) \ - OUTPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_2, SSAO_BUFFER_PING_2, 0) \ - OUTPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_3, SSAO_BUFFER_PING_3, 0) \ - OUTPUT_DESCRIPTOR_BINDING(GENERATE_IMPORTANCE_MAP, IMPORTANCE_MAP, 0) \ - OUTPUT_DESCRIPTOR_BINDING(POSTPROCESS_IMPORTANCE_MAP_A, IMPORTANCE_MAP_PONG, 0) \ - OUTPUT_DESCRIPTOR_BINDING(POSTPROCESS_IMPORTANCE_MAP_B, IMPORTANCE_MAP, 0) \ - OUTPUT_DESCRIPTOR_BINDING(EDGE_SENSITIVE_BLUR_0, SSAO_BUFFER_PONG_0, 0) \ - OUTPUT_DESCRIPTOR_BINDING(EDGE_SENSITIVE_BLUR_1, SSAO_BUFFER_PONG_1, 0) \ - OUTPUT_DESCRIPTOR_BINDING(EDGE_SENSITIVE_BLUR_2, SSAO_BUFFER_PONG_2, 0) \ - OUTPUT_DESCRIPTOR_BINDING(EDGE_SENSITIVE_BLUR_3, SSAO_BUFFER_PONG_3, 0) - -typedef enum OutputDescriptorBindingID { -#define OUTPUT_DESCRIPTOR_BINDING(descriptor_set_name, uav_name, _binding_num) OUTPUT_DESCRIPTOR_BINDING_##descriptor_set_name##_##uav_name, - OUTPUT_DESCRIPTOR_BINDINGS -#undef OUTPUT_DESCRIPTOR_BINDING - NUM_OUTPUT_DESCRIPTOR_BINDINGS -} OutputDescriptorBindingID; - -typedef struct OutputDescriptorBindingMetaData { - DescriptorSetID descriptorID; - UnorderedAccessViewID uavID; - uint32_t bindingNumber; -} OutputDescriptorBindingMetaData; - -static const OutputDescriptorBindingMetaData OUTPUT_DESCRIPTOR_BINDING_META_DATA[NUM_OUTPUT_DESCRIPTOR_BINDINGS] = { -#define OUTPUT_DESCRIPTOR_BINDING(descriptor_set_name, uav_name, binding_num) { DS_##descriptor_set_name, UAV_##uav_name, binding_num }, - OUTPUT_DESCRIPTOR_BINDINGS -#undef OUTPUT_DESCRIPTOR_BINDING -}; - -#define NUM_BACK_BUFFERS 3 -#define NUM_SAMPLERS 5 -typedef struct FfxCacaoVkContext { - FfxCacaoSettings settings; - FfxCacaoBool useDownsampledSsao; - BufferSizeInfo bufferSizeInfo; - -#ifdef FFX_CACAO_ENABLE_PROFILING - VkQueryPool timestampQueryPool; - uint32_t collectBuffer; - struct { - TimestampID timestamps[NUM_TIMESTAMPS]; - uint64_t timings[NUM_TIMESTAMPS]; - uint32_t numTimestamps; - } timestampQueries[NUM_BACK_BUFFERS]; -#endif - - VkPhysicalDevice physicalDevice; - VkDevice device; - PFN_vkCmdDebugMarkerBeginEXT vkCmdDebugMarkerBegin; - PFN_vkCmdDebugMarkerEndEXT vkCmdDebugMarkerEnd; - PFN_vkSetDebugUtilsObjectNameEXT vkSetDebugUtilsObjectName; - - - VkDescriptorSetLayout descriptorSetLayouts[NUM_DESCRIPTOR_SET_LAYOUTS]; - VkPipelineLayout pipelineLayouts[NUM_DESCRIPTOR_SET_LAYOUTS]; - - VkShaderModule computeShaders[NUM_COMPUTE_SHADERS]; - VkPipeline computePipelines[NUM_COMPUTE_SHADERS]; - - VkDescriptorSet descriptorSets[NUM_BACK_BUFFERS][NUM_DESCRIPTOR_SETS]; - VkDescriptorPool descriptorPool; - - VkSampler samplers[NUM_SAMPLERS]; - - VkImage textures[NUM_TEXTURES]; - VkDeviceMemory textureMemory[NUM_TEXTURES]; - VkImageView shaderResourceViews[NUM_SHADER_RESOURCE_VIEWS]; - VkImageView unorderedAccessViews[NUM_UNORDERED_ACCESS_VIEWS]; - - VkImage loadCounter; - VkDeviceMemory loadCounterMemory; - VkImageView loadCounterView; - - VkImage output; - - uint32_t currentConstantBuffer; - VkBuffer constantBuffer[NUM_BACK_BUFFERS][4]; - VkDeviceMemory constantBufferMemory[NUM_BACK_BUFFERS][4]; -} FfxCacaoVkContext; - -static inline FfxCacaoVkContext* getAlignedVkContextPointer(FfxCacaoVkContext* ptr) -{ - uintptr_t tmp = (uintptr_t)ptr; - tmp = (tmp + alignof(FfxCacaoVkContext) - 1) & (~(alignof(FfxCacaoVkContext) - 1)); - return (FfxCacaoVkContext*)tmp; -} -#endif - -// ================================================================================= -// Interface -// ================================================================================= - -#ifdef __cplusplus -extern "C" -{ -#endif - -#ifdef FFX_CACAO_ENABLE_D3D12 -size_t ffxCacaoD3D12GetContextSize() -{ - return sizeof(FfxCacaoD3D12Context) + alignof(FfxCacaoD3D12Context) - 1; -} - -FfxCacaoStatus ffxCacaoD3D12InitContext(FfxCacaoD3D12Context* context, ID3D12Device* device) -{ - if (context == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - if (device == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - context = getAlignedD3D12ContextPointer(context); - -#define COMPUTE_SHADER_INIT(name, entryPoint, uavSize, srvSize) \ - errorStatus = computeShaderInit(&context->name, device, #entryPoint, entryPoint ## DXIL, sizeof(entryPoint ## DXIL), uavSize, srvSize, samplers, FFX_CACAO_ARRAY_SIZE(samplers)); \ - if (errorStatus) \ - { \ - goto error_create_ ## entryPoint; \ - } -#define ERROR_COMPUTE_SHADER_DESTROY(name, entryPoint) \ - computeShaderDestroy(&context->name); \ -error_create_ ## entryPoint: - - FfxCacaoStatus errorStatus = FFX_CACAO_STATUS_FAILED; - - context->device = device; - CbvSrvUavHeap *cbvSrvUavHeap = &context->cbvSrvUavHeap; - errorStatus = cbvSrvUavHeapInit(cbvSrvUavHeap, device, 256); - if (errorStatus) - { - goto error_create_cbv_srv_uav_heap; - } - errorStatus = constantBufferRingInit(&context->constantBufferRing, device, 5, 1024 * 5); - if (errorStatus) - { - goto error_create_constant_buffer_ring; - } -#ifdef FFX_CACAO_ENABLE_PROFILING - errorStatus = gpuTimerInit(&context->gpuTimer, device); - if (errorStatus) - { - goto error_create_gpu_timer; - } -#endif - - D3D12_STATIC_SAMPLER_DESC samplers[5] = { }; - - samplers[0].Filter = D3D12_FILTER_MIN_MAG_MIP_POINT; - samplers[0].AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - samplers[0].AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - samplers[0].AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - samplers[0].ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; - samplers[0].BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK; - samplers[0].MinLOD = 0.0f; - samplers[0].MaxLOD = D3D12_FLOAT32_MAX; - samplers[0].MipLODBias = 0; - samplers[0].MaxAnisotropy = 1; - samplers[0].ShaderRegister = 0; - samplers[0].RegisterSpace = 0; - samplers[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; - - samplers[1].Filter = D3D12_FILTER_MIN_MAG_MIP_POINT; - samplers[1].AddressU = D3D12_TEXTURE_ADDRESS_MODE_MIRROR; - samplers[1].AddressV = D3D12_TEXTURE_ADDRESS_MODE_MIRROR; - samplers[1].AddressW = D3D12_TEXTURE_ADDRESS_MODE_MIRROR; - samplers[1].ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; - samplers[1].BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK; - samplers[1].MinLOD = 0.0f; - samplers[1].MaxLOD = D3D12_FLOAT32_MAX; - samplers[1].MipLODBias = 0; - samplers[1].MaxAnisotropy = 1; - samplers[1].ShaderRegister = 1; - samplers[1].RegisterSpace = 0; - samplers[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; - - samplers[2].Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR; - samplers[2].AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - samplers[2].AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - samplers[2].AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - samplers[2].ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; - samplers[2].BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK; - samplers[2].MinLOD = 0.0f; - samplers[2].MaxLOD = D3D12_FLOAT32_MAX; - samplers[2].MipLODBias = 0; - samplers[2].MaxAnisotropy = 1; - samplers[2].ShaderRegister = 2; - samplers[2].RegisterSpace = 0; - samplers[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; - - samplers[3].Filter = D3D12_FILTER_MIN_MAG_MIP_POINT; - samplers[3].AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - samplers[3].AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - samplers[3].AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - samplers[3].ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; - samplers[3].BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK; - samplers[3].MinLOD = 0.0f; - samplers[3].MaxLOD = D3D12_FLOAT32_MAX; - samplers[3].MipLODBias = 0; - samplers[3].MaxAnisotropy = 1; - samplers[3].ShaderRegister = 3; - samplers[3].RegisterSpace = 0; - samplers[3].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; - - samplers[4].Filter = D3D12_FILTER_MIN_MAG_MIP_POINT; - samplers[4].AddressU = D3D12_TEXTURE_ADDRESS_MODE_BORDER; - samplers[4].AddressV = D3D12_TEXTURE_ADDRESS_MODE_BORDER; - samplers[4].AddressW = D3D12_TEXTURE_ADDRESS_MODE_BORDER; - samplers[4].ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; - samplers[4].BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK; - samplers[4].MinLOD = 0.0f; - samplers[4].MaxLOD = D3D12_FLOAT32_MAX; - samplers[4].MipLODBias = 0; - samplers[4].MaxAnisotropy = 1; - samplers[4].ShaderRegister = 4; - samplers[4].RegisterSpace = 0; - samplers[4].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; - - // ===================================== - // Prepare shaders/resources - - COMPUTE_SHADER_INIT(prepareDownsampledDepthsHalf, CSPrepareDownsampledDepthsHalf, 1, 1); - COMPUTE_SHADER_INIT(prepareNativeDepthsHalf, CSPrepareNativeDepthsHalf, 1, 1); - - COMPUTE_SHADER_INIT(prepareDownsampledDepthsAndMips, CSPrepareDownsampledDepthsAndMips, 4, 1); - COMPUTE_SHADER_INIT(prepareNativeDepthsAndMips, CSPrepareNativeDepthsAndMips, 4, 1); - - COMPUTE_SHADER_INIT(prepareDownsampledNormals, CSPrepareDownsampledNormals, 1, 1); - COMPUTE_SHADER_INIT(prepareNativeNormals, CSPrepareNativeNormals, 1, 1); - - COMPUTE_SHADER_INIT(prepareDownsampledNormalsFromInputNormals, CSPrepareDownsampledNormalsFromInputNormals, 1, 1); - COMPUTE_SHADER_INIT(prepareNativeNormalsFromInputNormals, CSPrepareNativeNormalsFromInputNormals, 1, 1); - - COMPUTE_SHADER_INIT(prepareDownsampledDepths, CSPrepareDownsampledDepths, 1, 1); - COMPUTE_SHADER_INIT(prepareNativeDepths, CSPrepareNativeDepths, 1, 1); - - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->prepareDepthsAndMipsOutputs, 4); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->prepareDepthsOutputs, 1); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->prepareDepthsNormalsAndMipsInputs, 1); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->prepareNormalsOutput, 1); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->prepareNormalsFromInputNormalsInput, 1); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->prepareNormalsFromInputNormalsOutput, 1); - - // ===================================== - // Generate SSAO shaders/resources - - COMPUTE_SHADER_INIT(generateSSAO[0], CSGenerateQ0, 1, 7); - COMPUTE_SHADER_INIT(generateSSAO[1], CSGenerateQ1, 1, 7); - COMPUTE_SHADER_INIT(generateSSAO[2], CSGenerateQ2, 1, 7); - COMPUTE_SHADER_INIT(generateSSAO[3], CSGenerateQ3, 1, 7); - COMPUTE_SHADER_INIT(generateSSAO[4], CSGenerateQ3Base, 2, 7); - - for (int i = 0; i < 4; ++i) - { - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->generateSSAOInputs[i], 7); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->generateAdaptiveSSAOInputs[i], 7); - - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->generateSSAOOutputsPing[i], 1); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->generateSSAOOutputsPong[i], 1); - } - - // ===================================== - // Importance map shaders/resources - - COMPUTE_SHADER_INIT(generateImportanceMap, CSGenerateImportanceMap, 1, 1); - COMPUTE_SHADER_INIT(postprocessImportanceMapA, CSPostprocessImportanceMapA, 1, 1); - COMPUTE_SHADER_INIT(postprocessImportanceMapB, CSPostprocessImportanceMapB, 2, 1); - - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->generateImportanceMapInputs, 1); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->generateImportanceMapOutputs, 1); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->generateImportanceMapAInputs, 1); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->generateImportanceMapAOutputs, 1); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->generateImportanceMapBInputs, 1); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->generateImportanceMapBOutputs, 2); - - // ===================================== - // De-interleave Blur shaders/resources - - COMPUTE_SHADER_INIT(edgeSensitiveBlur[0], CSEdgeSensitiveBlur1, 1, 1); - COMPUTE_SHADER_INIT(edgeSensitiveBlur[1], CSEdgeSensitiveBlur2, 1, 1); - COMPUTE_SHADER_INIT(edgeSensitiveBlur[2], CSEdgeSensitiveBlur3, 1, 1); - COMPUTE_SHADER_INIT(edgeSensitiveBlur[3], CSEdgeSensitiveBlur4, 1, 1); - COMPUTE_SHADER_INIT(edgeSensitiveBlur[4], CSEdgeSensitiveBlur5, 1, 1); - COMPUTE_SHADER_INIT(edgeSensitiveBlur[5], CSEdgeSensitiveBlur6, 1, 1); - COMPUTE_SHADER_INIT(edgeSensitiveBlur[6], CSEdgeSensitiveBlur7, 1, 1); - COMPUTE_SHADER_INIT(edgeSensitiveBlur[7], CSEdgeSensitiveBlur8, 1, 1); - - for (int i = 0; i < 4; ++i) - { - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->edgeSensitiveBlurOutput[i], 1); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->edgeSensitiveBlurInput[i], 1); - } - - // ===================================== - // Apply shaders/resources - - COMPUTE_SHADER_INIT(smartApply, CSApply, 1, 1); - COMPUTE_SHADER_INIT(nonSmartApply, CSNonSmartApply, 1, 1); - COMPUTE_SHADER_INIT(nonSmartHalfApply, CSNonSmartHalfApply, 1, 1); - - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->createOutputInputsPing, 1); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->createOutputInputsPong, 1); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->createOutputOutputs, 1); - - // ===================================== - // Upacale shaders/resources - - COMPUTE_SHADER_INIT(upscaleBilateral5x5, CSUpscaleBilateral5x5, 1, 4); - COMPUTE_SHADER_INIT(upscaleBilateral5x5Half, CSUpscaleBilateral5x5Half, 1, 4); - - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->bilateralUpscaleInputsPing, 4); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->bilateralUpscaleInputsPong, 4); - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->bilateralUpscaleOutputs, 1); - - // ===================================== - // Misc - - errorStatus = textureInit(&context->loadCounter, device, "CACAO::m_loadCounter", &CD3DX12_RESOURCE_DESC::Tex1D(DXGI_FORMAT_R32_UINT, 1, 1, 1, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, NULL); - if (errorStatus) - { - goto error_create_load_counter_texture; - } - - // create uav for load counter - { - D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc = {}; - uavDesc.Format = DXGI_FORMAT_R32_UINT; - uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE1D; - uavDesc.Texture1D.MipSlice = 0; - - cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->loadCounterUav, 1); // required for clearing the load counter - textureCreateUavFromDesc(&context->loadCounter, 0, &context->loadCounterUav, &uavDesc); - } - - return FFX_CACAO_STATUS_OK; - -error_create_load_counter_texture: - - ERROR_COMPUTE_SHADER_DESTROY(upscaleBilateral5x5Half, CSUpscaleBilateral5x5Half); - ERROR_COMPUTE_SHADER_DESTROY(upscaleBilateral5x5, CSUpscaleBilateral5x5); - - ERROR_COMPUTE_SHADER_DESTROY(nonSmartHalfApply, CSNonSmartHalfApply); - ERROR_COMPUTE_SHADER_DESTROY(nonSmartApply, CSNonSmartApply); - ERROR_COMPUTE_SHADER_DESTROY(smartApply, CSApply); - - ERROR_COMPUTE_SHADER_DESTROY(edgeSensitiveBlur[7], CSEdgeSensitiveBlur8); - ERROR_COMPUTE_SHADER_DESTROY(edgeSensitiveBlur[6], CSEdgeSensitiveBlur7); - ERROR_COMPUTE_SHADER_DESTROY(edgeSensitiveBlur[5], CSEdgeSensitiveBlur6); - ERROR_COMPUTE_SHADER_DESTROY(edgeSensitiveBlur[4], CSEdgeSensitiveBlur5); - ERROR_COMPUTE_SHADER_DESTROY(edgeSensitiveBlur[3], CSEdgeSensitiveBlur4); - ERROR_COMPUTE_SHADER_DESTROY(edgeSensitiveBlur[2], CSEdgeSensitiveBlur3); - ERROR_COMPUTE_SHADER_DESTROY(edgeSensitiveBlur[1], CSEdgeSensitiveBlur2); - ERROR_COMPUTE_SHADER_DESTROY(edgeSensitiveBlur[0], CSEdgeSensitiveBlur1); - - ERROR_COMPUTE_SHADER_DESTROY(postprocessImportanceMapB, CSPostprocessImportanceMapB); - ERROR_COMPUTE_SHADER_DESTROY(postprocessImportanceMapA, CSPostprocessImportanceMapA); - ERROR_COMPUTE_SHADER_DESTROY(generateImportanceMap, CSGenerateImportanceMap); - - ERROR_COMPUTE_SHADER_DESTROY(generateSSAO[4], CSGenerateQ3Base); - ERROR_COMPUTE_SHADER_DESTROY(generateSSAO[3], CSGenerateQ3); - ERROR_COMPUTE_SHADER_DESTROY(generateSSAO[2], CSGenerateQ2); - ERROR_COMPUTE_SHADER_DESTROY(generateSSAO[1], CSGenerateQ1); - ERROR_COMPUTE_SHADER_DESTROY(generateSSAO[0], CSGenerateQ0); - - ERROR_COMPUTE_SHADER_DESTROY(prepareNativeDepths, CSPrepareNativeDepths); - ERROR_COMPUTE_SHADER_DESTROY(prepareDownsampledDepths, CSPrepareDownsampledDepths); - - ERROR_COMPUTE_SHADER_DESTROY(prepareNativeNormalsFromInputNormals, CSPrepareNativeNormalsFromInputNormals); - ERROR_COMPUTE_SHADER_DESTROY(prepareDownsampledNormalsFromInputNormals, CSPrepareDownsampledNormalsFromInputNormals); - - ERROR_COMPUTE_SHADER_DESTROY(prepareNativeNormals, CSPrepareNativeNormals); - ERROR_COMPUTE_SHADER_DESTROY(prepareDownsampledNormals, CSPrepareDownsampledNormals); - - ERROR_COMPUTE_SHADER_DESTROY(prepareNativeDepthsAndMips, CSPrepareNativeDepthsAndMips); - ERROR_COMPUTE_SHADER_DESTROY(prepareDownsampledDepthsAndMips, CSPrepareDownsampledDepthsAndMips); - - ERROR_COMPUTE_SHADER_DESTROY(prepareNativeDepthsHalf, CSPrepareNativeDepthsHalf); - ERROR_COMPUTE_SHADER_DESTROY(prepareDownsampledDepthsHalf, CSPrepareDownsampledDepthsHalf); - -#ifdef FFX_CACAO_ENABLE_PROFILING - gpuTimerDestroy(&context->gpuTimer); -error_create_gpu_timer: -#endif - constantBufferRingDestroy(&context->constantBufferRing); -error_create_constant_buffer_ring: - cbvSrvUavHeapDestroy(&context->cbvSrvUavHeap); -error_create_cbv_srv_uav_heap: - - return errorStatus; - -#undef COMPUTE_SHADER_INIT -#undef ERROR_COMPUTE_SHADER_DESTROY -} - -FfxCacaoStatus ffxCacaoD3D12DestroyContext(FfxCacaoD3D12Context* context) -{ - if (context == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - context = getAlignedD3D12ContextPointer(context); - - textureDestroy(&context->loadCounter); - - computeShaderDestroy(&context->upscaleBilateral5x5Half); - computeShaderDestroy(&context->upscaleBilateral5x5); - - computeShaderDestroy(&context->nonSmartHalfApply); - computeShaderDestroy(&context->nonSmartApply); - computeShaderDestroy(&context->smartApply); - - computeShaderDestroy(&context->edgeSensitiveBlur[7]); - computeShaderDestroy(&context->edgeSensitiveBlur[6]); - computeShaderDestroy(&context->edgeSensitiveBlur[5]); - computeShaderDestroy(&context->edgeSensitiveBlur[4]); - computeShaderDestroy(&context->edgeSensitiveBlur[3]); - computeShaderDestroy(&context->edgeSensitiveBlur[2]); - computeShaderDestroy(&context->edgeSensitiveBlur[1]); - computeShaderDestroy(&context->edgeSensitiveBlur[0]); - - computeShaderDestroy(&context->postprocessImportanceMapB); - computeShaderDestroy(&context->postprocessImportanceMapA); - computeShaderDestroy(&context->generateImportanceMap); - - computeShaderDestroy(&context->generateSSAO[4]); - computeShaderDestroy(&context->generateSSAO[3]); - computeShaderDestroy(&context->generateSSAO[2]); - computeShaderDestroy(&context->generateSSAO[1]); - computeShaderDestroy(&context->generateSSAO[0]); - - computeShaderDestroy(&context->prepareNativeDepths); - computeShaderDestroy(&context->prepareDownsampledDepths); - - computeShaderDestroy(&context->prepareNativeNormalsFromInputNormals); - computeShaderDestroy(&context->prepareDownsampledNormalsFromInputNormals); - - computeShaderDestroy(&context->prepareNativeNormals); - computeShaderDestroy(&context->prepareDownsampledNormals); - - computeShaderDestroy(&context->prepareNativeDepthsAndMips); - computeShaderDestroy(&context->prepareDownsampledDepthsAndMips); - - computeShaderDestroy(&context->prepareNativeDepthsHalf); - computeShaderDestroy(&context->prepareDownsampledDepthsHalf); - -#ifdef FFX_CACAO_ENABLE_PROFILING - gpuTimerDestroy(&context->gpuTimer); -#endif - constantBufferRingDestroy(&context->constantBufferRing); - cbvSrvUavHeapDestroy(&context->cbvSrvUavHeap); - - return FFX_CACAO_STATUS_OK; -} - -FfxCacaoStatus ffxCacaoD3D12InitScreenSizeDependentResources(FfxCacaoD3D12Context* context, const FfxCacaoD3D12ScreenSizeInfo* info) -{ - if (context == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - if (info == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - context = getAlignedD3D12ContextPointer(context); - -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - FfxCacaoBool useDownsampledSsao = info->useDownsampledSsao; -#else - FfxCacaoBool useDownsampledSsao = FFX_CACAO_TRUE; -#endif - context->useDownsampledSsao = useDownsampledSsao; - FfxCacaoStatus errorStatus; - -#define TEXTURE_INIT(name, label, format, width, height, arraySize, mipLevels) \ - errorStatus = textureInit(&context->name, device, "CACAO::" #name, &CD3DX12_RESOURCE_DESC::Tex2D(format, width, height, arraySize, mipLevels, 1, 0, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, NULL); \ - if (errorStatus) \ - { \ - goto error_create_texture_ ## label;\ - } -#define ERROR_TEXTURE_DESTROY(name, label) \ - textureDestroy(&context->name); \ -error_create_texture_ ## label: - - - ID3D12Device * device = context->device; - - uint32_t width = info->width; - uint32_t height = info->height; - uint32_t halfWidth = (width + 1) / 2; - uint32_t halfHeight = (height + 1) / 2; - uint32_t quarterWidth = (halfWidth + 1) / 2; - uint32_t quarterHeight = (halfHeight + 1) / 2; - uint32_t eighthWidth = (quarterWidth + 1) / 2; - uint32_t eighthHeight = (quarterHeight + 1) / 2; - -#if 1 - uint32_t depthBufferWidth = width; - uint32_t depthBufferHeight = height; - uint32_t depthBufferHalfWidth = halfWidth; - uint32_t depthBufferHalfHeight = halfHeight; - uint32_t depthBufferQuarterWidth = quarterWidth; - uint32_t depthBufferQuarterHeight = quarterHeight; - - uint32_t depthBufferXOffset = 0; - uint32_t depthBufferYOffset = 0; - uint32_t depthBufferHalfXOffset = 0; - uint32_t depthBufferHalfYOffset = 0; - uint32_t depthBufferQuarterXOffset = 0; - uint32_t depthBufferQuarterYOffset = 0; -#else - uint32_t depthBufferWidth = info->depthBufferWidth; - uint32_t depthBufferHeight = info->depthBufferHeight; - uint32_t depthBufferHalfWidth = (depthBufferWidth + 1) / 2; - uint32_t depthBufferHalfHeight = (depthBufferHeight + 1) / 2; - uint32_t depthBufferQuarterWidth = (depthBufferHalfWidth + 1) / 2; - uint32_t depthBufferQuarterHeight = (depthBufferHalfHeight + 1) / 2; - - uint32_t depthBufferXOffset = info->depthBufferXOffset; - uint32_t depthBufferYOffset = info->depthBufferYOffset; - uint32_t depthBufferHalfXOffset = (depthBufferXOffset + 1) / 2; // XXX - is this really right? - uint32_t depthBufferHalfYOffset = (depthBufferYOffset + 1) / 2; // XXX - is this really right? - uint32_t depthBufferQuarterXOffset = (depthBufferHalfXOffset + 1) / 2; // XXX - is this really right? - uint32_t depthBufferQuarterYOffset = (depthBufferHalfYOffset + 1) / 2; // XXX - is this really right? -#endif - - BufferSizeInfo bsi = {}; - bsi.inputOutputBufferWidth = width; - bsi.inputOutputBufferHeight = height; - bsi.depthBufferXOffset = depthBufferXOffset; - bsi.depthBufferYOffset = depthBufferYOffset; - bsi.depthBufferWidth = depthBufferWidth; - bsi.depthBufferHeight = depthBufferHeight; - - if (useDownsampledSsao) - { - bsi.ssaoBufferWidth = quarterWidth; - bsi.ssaoBufferHeight = quarterHeight; - bsi.deinterleavedDepthBufferXOffset = depthBufferQuarterXOffset; - bsi.deinterleavedDepthBufferYOffset = depthBufferQuarterYOffset; - bsi.deinterleavedDepthBufferWidth = depthBufferQuarterWidth; - bsi.deinterleavedDepthBufferHeight = depthBufferQuarterHeight; - bsi.importanceMapWidth = eighthWidth; - bsi.importanceMapHeight = eighthHeight; - } - else - { - bsi.ssaoBufferWidth = halfWidth; - bsi.ssaoBufferHeight = halfHeight; - bsi.deinterleavedDepthBufferXOffset = depthBufferHalfXOffset; - bsi.deinterleavedDepthBufferYOffset = depthBufferHalfYOffset; - bsi.deinterleavedDepthBufferWidth = depthBufferHalfWidth; - bsi.deinterleavedDepthBufferHeight = depthBufferHalfHeight; - bsi.importanceMapWidth = quarterWidth; - bsi.importanceMapHeight = quarterHeight; - } - - context->bufferSizeInfo = bsi; - - // ======================================= - // allocate intermediate textures - - TEXTURE_INIT(deinterleavedDepths, deinterleaved_depths, DXGI_FORMAT_R16_FLOAT, bsi.deinterleavedDepthBufferWidth, bsi.deinterleavedDepthBufferHeight, 4, 4); - TEXTURE_INIT(deinterleavedNormals, deinterleaved_normals, DXGI_FORMAT_R8G8B8A8_SNORM, bsi.ssaoBufferWidth, bsi.ssaoBufferHeight, 4, 1); - - TEXTURE_INIT(ssaoBufferPing, ssao_buffer_ping, DXGI_FORMAT_R8G8_UNORM, bsi.ssaoBufferWidth, bsi.ssaoBufferHeight, 4, 1); - TEXTURE_INIT(ssaoBufferPong, ssao_buffer_pong, DXGI_FORMAT_R8G8_UNORM, bsi.ssaoBufferWidth, bsi.ssaoBufferHeight, 4, 1); - - TEXTURE_INIT(importanceMap, importance_map, DXGI_FORMAT_R8_UNORM, bsi.importanceMapWidth, bsi.importanceMapHeight, 1, 1); - TEXTURE_INIT(importanceMapPong, importance_map_pong, DXGI_FORMAT_R8_UNORM, bsi.importanceMapWidth, bsi.importanceMapHeight, 1, 1); - - // ======================================= - // Init Prepare SRVs/UAVs - - for (int i = 0; i < 4; ++i) - { - textureCreateUav(&context->deinterleavedDepths, i, &context->prepareDepthsAndMipsOutputs, i, 4, 0); - } - textureCreateUav(&context->deinterleavedDepths, 0, &context->prepareDepthsOutputs, 0, 4, 0); - textureCreateUav(&context->deinterleavedNormals, 0, &context->prepareNormalsOutput, 0, 4, 0); - - device->CreateShaderResourceView(info->depthBufferResource, &info->depthBufferSrvDesc, context->prepareDepthsNormalsAndMipsInputs.cpuDescriptor); - - textureCreateUav(&context->deinterleavedNormals, 0, &context->prepareNormalsFromInputNormalsOutput, 0, 4, 0); - device->CreateShaderResourceView(info->normalBufferResource, &info->normalBufferSrvDesc, context->prepareNormalsFromInputNormalsInput.cpuDescriptor); - - // ======================================= - // Init Generate SSAO SRVs/UAVs - - for (int i = 0; i < 4; ++i) - { - D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; - srvDesc.Format = DXGI_FORMAT_R32_UINT; - srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE1D; - srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; - srvDesc.Texture1D.MostDetailedMip = 0; - srvDesc.Texture1D.MipLevels = 1; - - D3D12_SHADER_RESOURCE_VIEW_DESC zeroTextureSRVDesc = {}; - zeroTextureSRVDesc.Format = DXGI_FORMAT_R32_FLOAT; - zeroTextureSRVDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE1D; - zeroTextureSRVDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; - zeroTextureSRVDesc.Texture1D.MostDetailedMip = 0; - zeroTextureSRVDesc.Texture1D.MipLevels = 1; - - D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc = {}; - uavDesc.Format = DXGI_FORMAT_R32_UINT; - uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE1D; - uavDesc.Texture1D.MipSlice = 0; - - textureCreateSrv(&context->deinterleavedDepths, 0, &context->generateSSAOInputs[i], -1, 1, i); - textureCreateSrv(&context->deinterleavedNormals, 6, &context->generateSSAOInputs[i], 0, 4, 0); - - - textureCreateSrv(&context->deinterleavedDepths, 0, &context->generateAdaptiveSSAOInputs[i], -1, 1, i); - textureCreateSrvFromDesc(&context->loadCounter, 2, &context->generateAdaptiveSSAOInputs[i], &srvDesc); - textureCreateSrv(&context->importanceMap, 3, &context->generateAdaptiveSSAOInputs[i], -1, -1, -1); - textureCreateSrv(&context->ssaoBufferPong, 4, &context->generateAdaptiveSSAOInputs[i], -1, -1, -1); - textureCreateSrv(&context->deinterleavedNormals, 6, &context->generateAdaptiveSSAOInputs[i], 0, 4, 0); - - textureCreateUav(&context->ssaoBufferPing, 0, &context->generateSSAOOutputsPing[i], 0, 1, i); - - textureCreateUav(&context->ssaoBufferPong, 0, &context->generateSSAOOutputsPong[i], 0, 1, i); - - } - - // ======================================= - // Init Generate/Postprocess Importance map SRVs/UAVs - - { - D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc = {}; - uavDesc.Format = DXGI_FORMAT_R32_UINT; - uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE1D; - uavDesc.Texture1D.MipSlice = 0; - - textureCreateSrv(&context->ssaoBufferPong, 0, &context->generateImportanceMapInputs, -1, -1, -1); - textureCreateUav(&context->importanceMap, 0, &context->generateImportanceMapOutputs, -1, -1, -1); - - textureCreateSrv(&context->importanceMap, 0, &context->generateImportanceMapAInputs, -1, -1, -1); - textureCreateUav(&context->importanceMapPong, 0, &context->generateImportanceMapAOutputs, -1, -1, -1); - - textureCreateSrv(&context->importanceMapPong, 0, &context->generateImportanceMapBInputs, -1, -1, -1); - textureCreateUav(&context->importanceMap, 0, &context->generateImportanceMapBOutputs, -1, -1, -1); - textureCreateUavFromDesc(&context->loadCounter, 1, &context->generateImportanceMapBOutputs, &uavDesc); - } - - // ======================================= - // Init De-interleave Blur SRVs/UAVs - - for (int i = 0; i < 4; ++i) - { - textureCreateSrv(&context->ssaoBufferPing, 0, &context->edgeSensitiveBlurInput[i], 0, 1, i); - textureCreateUav(&context->ssaoBufferPong, 0, &context->edgeSensitiveBlurOutput[i], 0, 1, i); - } - - // ======================================= - // Init apply SRVs/UAVs - - textureCreateSrv(&context->ssaoBufferPing, 0, &context->createOutputInputsPing, 0, 4, 0); - textureCreateSrv(&context->ssaoBufferPong, 0, &context->createOutputInputsPong, 0, 4, 0); - - context->device->CreateUnorderedAccessView(info->outputResource, NULL, &info->outputUavDesc, context->createOutputOutputs.cpuDescriptor); - context->device->CreateUnorderedAccessView(info->outputResource, NULL, &info->outputUavDesc, context->createOutputOutputs.cpuVisibleCpuDescriptor); - - // ======================================= - // Init upscale SRVs/UAVs - - textureCreateSrv(&context->ssaoBufferPing, 0, &context->bilateralUpscaleInputsPing, -1, -1, -1); - context->device->CreateShaderResourceView(info->depthBufferResource, &info->depthBufferSrvDesc, cbvSrvUavGetCpu(&context->bilateralUpscaleInputsPing, 1)); - textureCreateSrv(&context->deinterleavedDepths, 3, &context->bilateralUpscaleInputsPing, 0, -1, -1); - - textureCreateSrv(&context->ssaoBufferPong, 0, &context->bilateralUpscaleInputsPong, -1, -1, -1); - context->device->CreateShaderResourceView(info->depthBufferResource, &info->depthBufferSrvDesc, cbvSrvUavGetCpu(&context->bilateralUpscaleInputsPong, 1)); - textureCreateSrv(&context->deinterleavedDepths, 3, &context->bilateralUpscaleInputsPong, 0, -1, -1); - - context->device->CreateUnorderedAccessView(info->outputResource, NULL, &info->outputUavDesc, context->bilateralUpscaleOutputs.cpuDescriptor); - - // ======================================= - // Init debug SRVs/UAVs - - context->outputResource = info->outputResource; - - return FFX_CACAO_STATUS_OK; - - ERROR_TEXTURE_DESTROY(importanceMapPong, importance_map_pong); - ERROR_TEXTURE_DESTROY(importanceMap, importance_map); - - ERROR_TEXTURE_DESTROY(ssaoBufferPong, ssao_buffer_pong); - ERROR_TEXTURE_DESTROY(ssaoBufferPing, ssao_buffer_ping); - - ERROR_TEXTURE_DESTROY(deinterleavedNormals, deinterleaved_normals); - ERROR_TEXTURE_DESTROY(deinterleavedDepths, deinterleaved_depths); - - return errorStatus; - -#undef TEXTURE_INIT -#undef ERROR_TEXTURE_DESTROY -} - -FfxCacaoStatus ffxCacaoD3D12DestroyScreenSizeDependentResources(FfxCacaoD3D12Context* context) -{ - if (context == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - context = getAlignedD3D12ContextPointer(context); - - textureDestroy(&context->importanceMapPong); - textureDestroy(&context->importanceMap); - - textureDestroy(&context->ssaoBufferPong); - textureDestroy(&context->ssaoBufferPing); - - textureDestroy(&context->deinterleavedNormals); - textureDestroy(&context->deinterleavedDepths); - - return FFX_CACAO_STATUS_OK; -} - -FfxCacaoStatus ffxCacaoD3D12UpdateSettings(FfxCacaoD3D12Context* context, const FfxCacaoSettings* settings) -{ - if (context == NULL || settings == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - context = getAlignedD3D12ContextPointer(context); - - memcpy(&context->settings, settings, sizeof(*settings)); - - return FFX_CACAO_STATUS_OK; -} - -FfxCacaoStatus ffxCacaoD3D12Draw(FfxCacaoD3D12Context* context, ID3D12GraphicsCommandList* commandList, const FfxCacaoMatrix4x4* proj, const FfxCacaoMatrix4x4* normalsToView) -{ - if (context == NULL || commandList == NULL || proj == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - context = getAlignedD3D12ContextPointer(context); - - -#ifdef FFX_CACAO_ENABLE_PROFILING -#define GET_TIMESTAMP(name) gpuTimerGetTimestamp(&context->gpuTimer, commandList, TIMESTAMP_##name) -#else -#define GET_TIMESTAMP(name) -#endif - BufferSizeInfo *bsi = &context->bufferSizeInfo; - - - USER_MARKER("FidelityFX CACAO"); - - constantBufferRingStartFrame(&context->constantBufferRing); - -#ifdef FFX_CACAO_ENABLE_PROFILING - gpuTimerStartFrame(&context->gpuTimer); -#endif - - GET_TIMESTAMP(BEGIN); - - // set the descriptor heaps - { - ID3D12DescriptorHeap *descriptorHeaps[] = { context->cbvSrvUavHeap.heap }; - commandList->SetDescriptorHeaps(FFX_CACAO_ARRAY_SIZE(descriptorHeaps), descriptorHeaps); - } - - // clear load counter - { - UINT clearValue[] = { 0, 0, 0, 0 }; - commandList->ClearUnorderedAccessViewUint(context->loadCounterUav.gpuDescriptor, context->loadCounterUav.cpuVisibleCpuDescriptor, context->loadCounter.resource, clearValue, 0, NULL); - } - - // move this to initialisation - D3D12_GPU_VIRTUAL_ADDRESS cbCACAOHandle; - FfxCacaoConstants *pCACAOConsts; - D3D12_GPU_VIRTUAL_ADDRESS cbCACAOPerPassHandle[4]; - FfxCacaoConstants *pPerPassConsts[4]; - - // upload constant buffers - { - constantBufferRingAlloc(&context->constantBufferRing, sizeof(*pCACAOConsts), (void**)&pCACAOConsts, &cbCACAOHandle); - updateConstants(pCACAOConsts, &context->settings, bsi, proj, normalsToView); - - for (int i = 0; i < 4; ++i) - { - constantBufferRingAlloc(&context->constantBufferRing, sizeof(*pPerPassConsts[0]), (void**)&pPerPassConsts[i], &cbCACAOPerPassHandle[i]); - updateConstants(pPerPassConsts[i], &context->settings, bsi, proj, normalsToView); - updatePerPassConstants(pPerPassConsts[i], &context->settings, &context->bufferSizeInfo, i); - } - } - - // prepare depths, normals and mips - { - USER_MARKER("Prepare downsampled depths, normals and mips"); - - - switch (context->settings.qualityLevel) - { - case FFX_CACAO_QUALITY_LOWEST: { - uint32_t dispatchWidth = dispatchSize(PREPARE_DEPTHS_HALF_WIDTH, bsi->deinterleavedDepthBufferWidth); - uint32_t dispatchHeight = dispatchSize(PREPARE_DEPTHS_HALF_HEIGHT, bsi->deinterleavedDepthBufferHeight); - ComputeShader *prepareDepthsHalf = context->useDownsampledSsao ? &context->prepareDownsampledDepthsHalf : &context->prepareNativeDepthsHalf; - computeShaderDraw(prepareDepthsHalf, commandList, cbCACAOHandle, &context->prepareDepthsOutputs, &context->prepareDepthsNormalsAndMipsInputs, dispatchWidth, dispatchHeight, 1); - break; - } - case FFX_CACAO_QUALITY_LOW: { - uint32_t dispatchWidth = dispatchSize(PREPARE_DEPTHS_WIDTH, bsi->deinterleavedDepthBufferWidth); - uint32_t dispatchHeight = dispatchSize(PREPARE_DEPTHS_HEIGHT, bsi->deinterleavedDepthBufferHeight); - ComputeShader *prepareDepths = context->useDownsampledSsao ? &context->prepareDownsampledDepths : &context->prepareNativeDepths; - computeShaderDraw(prepareDepths, commandList, cbCACAOHandle, &context->prepareDepthsOutputs, &context->prepareDepthsNormalsAndMipsInputs, dispatchWidth, dispatchHeight, 1); - break; - } - default: { - uint32_t dispatchWidth = dispatchSize(PREPARE_DEPTHS_AND_MIPS_WIDTH, bsi->deinterleavedDepthBufferWidth); - uint32_t dispatchHeight = dispatchSize(PREPARE_DEPTHS_AND_MIPS_HEIGHT, bsi->deinterleavedDepthBufferHeight); - ComputeShader *prepareDepthsAndMips = context->useDownsampledSsao ? &context->prepareDownsampledDepthsAndMips : &context->prepareNativeDepthsAndMips; - computeShaderDraw(prepareDepthsAndMips, commandList, cbCACAOHandle, &context->prepareDepthsAndMipsOutputs, &context->prepareDepthsNormalsAndMipsInputs, dispatchWidth, dispatchHeight, 1); - break; - } - } - - if (context->settings.generateNormals) - { - uint32_t dispatchWidth = dispatchSize(PREPARE_NORMALS_WIDTH, bsi->ssaoBufferWidth); - uint32_t dispatchHeight = dispatchSize(PREPARE_NORMALS_HEIGHT, bsi->ssaoBufferHeight); - ComputeShader *prepareNormals = context->useDownsampledSsao ? &context->prepareDownsampledNormals : &context->prepareNativeNormals; - computeShaderDraw(prepareNormals, commandList, cbCACAOHandle, &context->prepareNormalsOutput, &context->prepareDepthsNormalsAndMipsInputs, dispatchWidth, dispatchHeight, 1); - } - else - { - uint32_t dispatchWidth = dispatchSize(PREPARE_NORMALS_FROM_INPUT_NORMALS_WIDTH, bsi->ssaoBufferWidth); - uint32_t dispatchHeight = dispatchSize(PREPARE_NORMALS_FROM_INPUT_NORMALS_HEIGHT, bsi->ssaoBufferHeight); - ComputeShader *prepareNormalsFromInputNormals = context->useDownsampledSsao ? &context->prepareDownsampledNormalsFromInputNormals : &context->prepareNativeNormalsFromInputNormals; - computeShaderDraw(prepareNormalsFromInputNormals, commandList, cbCACAOHandle, &context->prepareNormalsFromInputNormalsOutput, &context->prepareNormalsFromInputNormalsInput, dispatchWidth, dispatchHeight, 1); - } - - GET_TIMESTAMP(PREPARE); - } - - // deinterleaved depths and normals are now read only resources, also used in the next stage - { - D3D12_RESOURCE_BARRIER resourceBarriers[] = { - CD3DX12_RESOURCE_BARRIER::Transition(context->deinterleavedDepths.resource, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE), - CD3DX12_RESOURCE_BARRIER::Transition(context->deinterleavedNormals.resource, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE), - }; - commandList->ResourceBarrier(FFX_CACAO_ARRAY_SIZE(resourceBarriers), resourceBarriers); - } - - // base pass for highest quality setting - if (context->settings.qualityLevel == FFX_CACAO_QUALITY_HIGHEST) - { - USER_MARKER("Generate High Quality Base Pass"); - - // SSAO - { - USER_MARKER("SSAO"); - - for (int pass = 0; pass < 4; ++pass) - { - CbvSrvUav *inputs = &context->generateSSAOInputs[pass]; - uint32_t dispatchWidth = dispatchSize(GENERATE_WIDTH, bsi->ssaoBufferWidth); - uint32_t dispatchHeight = dispatchSize(GENERATE_WIDTH, bsi->ssaoBufferHeight); - computeShaderDraw(&context->generateSSAO[4], commandList, cbCACAOPerPassHandle[pass], &context->generateSSAOOutputsPong[pass], inputs, dispatchWidth, dispatchHeight, 1); - } - GET_TIMESTAMP(BASE_SSAO_PASS); - } - - // results written by base pass are now a reaad only resource, used in next stage - commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(context->ssaoBufferPong.resource, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE)); - - // generate importance map - { - USER_MARKER("Importance Map"); - - CD3DX12_RESOURCE_BARRIER barriers[2]; - UINT barrierCount; - - uint32_t dispatchWidth = dispatchSize(IMPORTANCE_MAP_WIDTH, bsi->importanceMapWidth); - uint32_t dispatchHeight = dispatchSize(IMPORTANCE_MAP_HEIGHT, bsi->importanceMapHeight); - - computeShaderDraw(&context->generateImportanceMap, commandList, cbCACAOHandle, &context->generateImportanceMapOutputs, &context->generateImportanceMapInputs, dispatchWidth, dispatchHeight, 1); - - barrierCount = 0; - barriers[barrierCount++] = CD3DX12_RESOURCE_BARRIER::Transition(context->importanceMap.resource, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); - commandList->ResourceBarrier(barrierCount, barriers); - - computeShaderDraw(&context->postprocessImportanceMapA, commandList, cbCACAOHandle, &context->generateImportanceMapAOutputs, &context->generateImportanceMapAInputs, dispatchWidth, dispatchHeight, 1); - - barrierCount = 0; - barriers[barrierCount++] = CD3DX12_RESOURCE_BARRIER::Transition(context->importanceMap.resource, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); - barriers[barrierCount++] = CD3DX12_RESOURCE_BARRIER::Transition(context->importanceMapPong.resource, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); - commandList->ResourceBarrier(barrierCount, barriers); - - computeShaderDraw(&context->postprocessImportanceMapB, commandList, cbCACAOHandle, &context->generateImportanceMapBOutputs, &context->generateImportanceMapBInputs, dispatchWidth, dispatchHeight, 1); - - barrierCount = 0; - barriers[barrierCount++] = CD3DX12_RESOURCE_BARRIER::Transition(context->importanceMap.resource, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); - barriers[barrierCount++] = CD3DX12_RESOURCE_BARRIER::Transition(context->loadCounter.resource, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); - commandList->ResourceBarrier(barrierCount, barriers); - - GET_TIMESTAMP(IMPORTANCE_MAP); - } - } - - int blurPassCount = context->settings.blurPassCount; - blurPassCount = FFX_CACAO_CLAMP(blurPassCount, 0, MAX_BLUR_PASSES); - - // main ssao generation - { - USER_MARKER("Generate SSAO"); - - ComputeShader *generate = &context->generateSSAO[FFX_CACAO_MAX(0, context->settings.qualityLevel - 1)]; - for (int pass = 0; pass < 4; ++pass) - { - if (context->settings.qualityLevel == FFX_CACAO_QUALITY_LOWEST && (pass == 1 || pass == 2)) - { - continue; - } - - CbvSrvUav *input = context->settings.qualityLevel == FFX_CACAO_QUALITY_HIGHEST ? &context->generateAdaptiveSSAOInputs[pass] : &context->generateSSAOInputs[pass]; - CbvSrvUav *output = &context->generateSSAOOutputsPing[pass]; // blurPassCount == 0 ? &context->generateSSAOOutputsPing[pass] : &context->generateSSAOOutputsPong[pass]; - - uint32_t dispatchWidth = dispatchSize(GENERATE_WIDTH, bsi->ssaoBufferWidth); - uint32_t dispatchHeight = dispatchSize(GENERATE_WIDTH, bsi->ssaoBufferHeight); - computeShaderDraw(generate, commandList, cbCACAOPerPassHandle[pass], output, input, dispatchWidth, dispatchHeight, 1); - } - - GET_TIMESTAMP(GENERATE_SSAO); - } - - // de-interleaved blur - if (blurPassCount) - { - // only need to transition pong to writable if we didn't already use it in the base pass - CD3DX12_RESOURCE_BARRIER barriers[] = { - CD3DX12_RESOURCE_BARRIER::Transition(context->ssaoBufferPing.resource, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE), - CD3DX12_RESOURCE_BARRIER::Transition(context->ssaoBufferPong.resource, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS), - }; - commandList->ResourceBarrier(context->settings.qualityLevel == FFX_CACAO_QUALITY_HIGHEST ? 2 : 1, barriers); - - USER_MARKER("Deinterleaved blur"); - - for (int pass = 0; pass < 4; ++pass) - { - if (context->settings.qualityLevel == FFX_CACAO_QUALITY_LOWEST && (pass == 1 || pass == 2)) - { - continue; - } - - uint32_t w = 4 * BLUR_WIDTH - 2 * blurPassCount; - uint32_t h = 3 * BLUR_HEIGHT - 2 * blurPassCount; - uint32_t blurPassIndex = blurPassCount - 1; - uint32_t dispatchWidth = dispatchSize(w, bsi->ssaoBufferWidth); - uint32_t dispatchHeight = dispatchSize(h, bsi->ssaoBufferHeight); - computeShaderDraw(&context->edgeSensitiveBlur[blurPassIndex], commandList, cbCACAOPerPassHandle[pass], &context->edgeSensitiveBlurOutput[pass], &context->edgeSensitiveBlurInput[pass], dispatchWidth, dispatchHeight, 1); - } - - GET_TIMESTAMP(EDGE_SENSITIVE_BLUR); - - commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(context->ssaoBufferPong.resource, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE)); - } - else - { - commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(context->ssaoBufferPing.resource, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE)); - } - - - if (context->useDownsampledSsao) - { - USER_MARKER("Upscale"); - - CbvSrvUav *inputs = blurPassCount ? &context->bilateralUpscaleInputsPong : &context->bilateralUpscaleInputsPing; - ComputeShader *upscaler = context->settings.qualityLevel == FFX_CACAO_QUALITY_LOWEST ? &context->upscaleBilateral5x5Half : &context->upscaleBilateral5x5; - uint32_t dispatchWidth = dispatchSize(2 * BILATERAL_UPSCALE_WIDTH, bsi->inputOutputBufferWidth); - uint32_t dispatchHeight = dispatchSize(2 * BILATERAL_UPSCALE_HEIGHT, bsi->inputOutputBufferHeight); - computeShaderDraw(upscaler, commandList, cbCACAOHandle, &context->bilateralUpscaleOutputs, inputs, dispatchWidth, dispatchHeight, 1); - - GET_TIMESTAMP(BILATERAL_UPSAMPLE); - } - else - { - USER_MARKER("Create Output"); - CbvSrvUav *inputs = blurPassCount ? &context->createOutputInputsPong : &context->createOutputInputsPing; - uint32_t dispatchWidth = dispatchSize(APPLY_WIDTH, bsi->inputOutputBufferWidth); - uint32_t dispatchHeight = dispatchSize(APPLY_HEIGHT, bsi->inputOutputBufferHeight); - switch (context->settings.qualityLevel) - { - case FFX_CACAO_QUALITY_LOWEST: - computeShaderDraw(&context->nonSmartHalfApply, commandList, cbCACAOHandle, &context->createOutputOutputs, inputs, dispatchWidth, dispatchHeight, 1); - break; - case FFX_CACAO_QUALITY_LOW: - computeShaderDraw(&context->nonSmartApply, commandList, cbCACAOHandle, &context->createOutputOutputs, inputs, dispatchWidth, dispatchHeight, 1); - break; - default: - computeShaderDraw(&context->smartApply, commandList, cbCACAOHandle, &context->createOutputOutputs, inputs, dispatchWidth, dispatchHeight, 1); - break; - } - GET_TIMESTAMP(APPLY); - } - - // end frame resource barrier - { - uint32_t numBarriers = 0; - D3D12_RESOURCE_BARRIER resourceBarriers[10] = {}; - resourceBarriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::Transition(context->deinterleavedDepths.resource, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); - resourceBarriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::Transition(context->deinterleavedNormals.resource, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); - resourceBarriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::Transition(context->outputResource, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_GENERIC_READ); - resourceBarriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::Transition(context->ssaoBufferPing.resource, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); - if (context->settings.qualityLevel == FFX_CACAO_QUALITY_HIGHEST || blurPassCount) - { - resourceBarriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::Transition(context->ssaoBufferPong.resource, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); - } - if (context->settings.qualityLevel == FFX_CACAO_QUALITY_HIGHEST) - { - resourceBarriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::Transition(context->importanceMap.resource, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); - resourceBarriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::Transition(context->importanceMapPong.resource, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); - resourceBarriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::Transition(context->loadCounter.resource, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); - } - commandList->ResourceBarrier(numBarriers, resourceBarriers); - } - -#ifdef FFX_CACAO_ENABLE_PROFILING - gpuTimerEndFrame(&context->gpuTimer, commandList); -#endif - - return FFX_CACAO_STATUS_OK; - -#undef GET_TIMESTAMP -} - -#ifdef FFX_CACAO_ENABLE_PROFILING -FfxCacaoStatus ffxCacaoD3D12GetDetailedTimings(FfxCacaoD3D12Context* context, FfxCacaoDetailedTiming* timings) -{ - if (context == NULL || timings == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - context = getAlignedD3D12ContextPointer(context); - - gpuTimerCollectTimings(&context->gpuTimer, timings); - - return FFX_CACAO_STATUS_OK; -} -#endif -#endif - -#ifdef FFX_CACAO_ENABLE_VULKAN -inline static void setObjectName(VkDevice device, FfxCacaoVkContext* context, VkObjectType type, uint64_t handle, const char* name) -{ - if (!context->vkSetDebugUtilsObjectName) - { - return; - } - - VkDebugUtilsObjectNameInfoEXT info = {}; - info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT; - info.pNext = NULL; - info.objectType = type; - info.objectHandle = handle; - info.pObjectName = name; - - VkResult result = context->vkSetDebugUtilsObjectName(device, &info); - FFX_CACAO_ASSERT(result == VK_SUCCESS); -} - -inline static uint32_t getBestMemoryHeapIndex(VkPhysicalDevice physicalDevice, VkMemoryRequirements memoryRequirements, VkMemoryPropertyFlags desiredProperties) -{ - VkPhysicalDeviceMemoryProperties memoryProperties; - vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memoryProperties); - - uint32_t chosenMemoryTypeIndex = VK_MAX_MEMORY_TYPES; - for (uint32_t i = 0; i < memoryProperties.memoryTypeCount; ++i) - { - uint32_t typeBit = 1 << i; - // can we allocate to memory of this type - if (memoryRequirements.memoryTypeBits & typeBit) - { - VkMemoryType currentMemoryType = memoryProperties.memoryTypes[i]; - // do we want to allocate to memory of this type - if ((currentMemoryType.propertyFlags & desiredProperties) == desiredProperties) - { - chosenMemoryTypeIndex = i; - break; - } - } - } - return chosenMemoryTypeIndex; -} - -size_t ffxCacaoVkGetContextSize() -{ - return sizeof(FfxCacaoVkContext) + alignof(FfxCacaoVkContext) - 1; -} - -FfxCacaoStatus ffxCacaoVkInitContext(FfxCacaoVkContext* context, const FfxCacaoVkCreateInfo* info) -{ - if (context == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - if (info == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - context = getAlignedVkContextPointer(context); - memset(context, 0, sizeof(*context)); - - VkDevice device = info->device; - VkPhysicalDevice physicalDevice = info->physicalDevice; - VkResult result; - FfxCacaoBool use16Bit = info->flags & FFX_CACAO_VK_CREATE_USE_16_BIT ? FFX_CACAO_TRUE : FFX_CACAO_FALSE; - FfxCacaoStatus errorStatus = FFX_CACAO_STATUS_FAILED; - - context->device = device; - context->physicalDevice = physicalDevice; - - if (info->flags & FFX_CACAO_VK_CREATE_USE_DEBUG_MARKERS) - { - context->vkCmdDebugMarkerBegin = (PFN_vkCmdDebugMarkerBeginEXT)vkGetDeviceProcAddr(device, "vkCmdDebugMarkerBeginEXT"); - context->vkCmdDebugMarkerEnd = (PFN_vkCmdDebugMarkerEndEXT)vkGetDeviceProcAddr(device, "vkCmdDebugMarkerEndEXT"); - } - if (info->flags & FFX_CACAO_VK_CREATE_USE_DEBUG_MARKERS) - { - context->vkSetDebugUtilsObjectName = (PFN_vkSetDebugUtilsObjectNameEXT)vkGetDeviceProcAddr(device, "vkSetDebugUtilsObjectNameEXT"); - } - - uint32_t numSamplersInited = 0; - uint32_t numDescriptorSetLayoutsInited = 0; - uint32_t numPipelineLayoutsInited = 0; - uint32_t numShaderModulesInited = 0; - uint32_t numPipelinesInited = 0; - uint32_t numConstantBackBuffersInited = 0; - - VkSampler samplers[NUM_SAMPLERS]; - { - VkSamplerCreateInfo samplerCreateInfo = {}; - samplerCreateInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; - samplerCreateInfo.pNext = NULL; - samplerCreateInfo.flags = 0; - samplerCreateInfo.magFilter = VK_FILTER_LINEAR; - samplerCreateInfo.minFilter = VK_FILTER_LINEAR; - samplerCreateInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; - samplerCreateInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - samplerCreateInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - samplerCreateInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; - samplerCreateInfo.mipLodBias = 0.0f; - samplerCreateInfo.anisotropyEnable = VK_FALSE; - samplerCreateInfo.compareEnable = VK_FALSE; - samplerCreateInfo.minLod = -1000.0f; - samplerCreateInfo.maxLod = 1000.0f; - samplerCreateInfo.unnormalizedCoordinates = VK_FALSE; - - result = vkCreateSampler(device, &samplerCreateInfo, NULL, &samplers[numSamplersInited]); - if (result != VK_SUCCESS) - { - goto error_init_samplers; - } - setObjectName(device, context, VK_OBJECT_TYPE_SAMPLER, (uint64_t)samplers[numSamplersInited], "FFX_CACAO_POINT_CLAMP_SAMPLER"); - ++numSamplersInited; - - samplerCreateInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT; - samplerCreateInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT; - samplerCreateInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT; - - result = vkCreateSampler(device, &samplerCreateInfo, NULL, &samplers[numSamplersInited]); - if (result != VK_SUCCESS) - { - goto error_init_samplers; - } - setObjectName(device, context, VK_OBJECT_TYPE_SAMPLER, (uint64_t)samplers[numSamplersInited], "FFX_CACAO_POINT_MIRROR_SAMPLER"); - ++numSamplersInited; - - samplerCreateInfo.magFilter = VK_FILTER_LINEAR; - samplerCreateInfo.minFilter = VK_FILTER_LINEAR; - samplerCreateInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; - samplerCreateInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - samplerCreateInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - samplerCreateInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - - result = vkCreateSampler(device, &samplerCreateInfo, NULL, &samplers[numSamplersInited]); - if (result != VK_SUCCESS) - { - goto error_init_samplers; - } - setObjectName(device, context, VK_OBJECT_TYPE_SAMPLER, (uint64_t)samplers[numSamplersInited], "FFX_CACAO_LINEAR_CLAMP_SAMPLER"); - ++numSamplersInited; - - samplerCreateInfo.magFilter = VK_FILTER_NEAREST; - samplerCreateInfo.minFilter = VK_FILTER_NEAREST; - samplerCreateInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; - - result = vkCreateSampler(device, &samplerCreateInfo, NULL, &samplers[numSamplersInited]); - if (result != VK_SUCCESS) - { - goto error_init_samplers; - } - setObjectName(device, context, VK_OBJECT_TYPE_SAMPLER, (uint64_t)samplers[numSamplersInited], "FFX_CACAO_VIEWSPACE_DEPTH_TAP_SAMPLER"); - ++numSamplersInited; - - samplerCreateInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; - samplerCreateInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; - samplerCreateInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; - samplerCreateInfo.borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; - - result = vkCreateSampler(device, &samplerCreateInfo, NULL, &samplers[numSamplersInited]); - if (result != VK_SUCCESS) - { - goto error_init_samplers; - } - setObjectName(device, context, VK_OBJECT_TYPE_SAMPLER, (uint64_t)samplers[numSamplersInited], "FFX_CACAO_ZERO_TEXTURE_SAMPLER"); - ++numSamplersInited; - - for (uint32_t i = 0; i < FFX_CACAO_ARRAY_SIZE(samplers); ++i) - { - context->samplers[i] = samplers[i]; - } - } - - // create descriptor set layouts - for ( ; numDescriptorSetLayoutsInited < NUM_DESCRIPTOR_SET_LAYOUTS; ++numDescriptorSetLayoutsInited) - { - VkDescriptorSetLayout descriptorSetLayout; - DescriptorSetLayoutMetaData dslMetaData = DESCRIPTOR_SET_LAYOUT_META_DATA[numDescriptorSetLayoutsInited]; - - VkDescriptorSetLayoutBinding bindings[MAX_DESCRIPTOR_BINDINGS] = {}; - uint32_t numBindings = 0; - for (uint32_t samplerBinding = 0; samplerBinding < FFX_CACAO_ARRAY_SIZE(samplers); ++samplerBinding) - { - VkDescriptorSetLayoutBinding binding = {}; - binding.binding = samplerBinding; - binding.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER; - binding.descriptorCount = 1; - binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - binding.pImmutableSamplers = &samplers[samplerBinding]; - bindings[numBindings++] = binding; - } - - // constant buffer binding - { - VkDescriptorSetLayoutBinding binding = {}; - binding.binding = 10; - binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - binding.descriptorCount = 1; - binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - binding.pImmutableSamplers = NULL; - bindings[numBindings++] = binding; - } - - for (uint32_t inputBinding = 0; inputBinding < dslMetaData.numInputs; ++inputBinding) - { - VkDescriptorSetLayoutBinding binding = {}; - binding.binding = 20 + inputBinding; - binding.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; - binding.descriptorCount = 1; - binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - binding.pImmutableSamplers = NULL; - bindings[numBindings++] = binding; - } - - for (uint32_t outputBinding = 0; outputBinding < dslMetaData.numOutputs; ++outputBinding) - { - VkDescriptorSetLayoutBinding binding = {}; - binding.binding = 30 + outputBinding; // g_PrepareDepthsOut register(u0) - binding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - binding.descriptorCount = 1; - binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - binding.pImmutableSamplers = NULL; - bindings[numBindings++] = binding; - } - - VkDescriptorSetLayoutCreateInfo info = {}; - info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; - info.pNext = NULL; - info.flags = 0; - info.bindingCount = numBindings; - info.pBindings = bindings; - - result = vkCreateDescriptorSetLayout(device, &info, NULL, &descriptorSetLayout); - if (result != VK_SUCCESS) - { - goto error_init_descriptor_set_layouts; - } - setObjectName(device, context, VK_OBJECT_TYPE_DESCRIPTOR_SET_LAYOUT, (uint64_t)descriptorSetLayout, dslMetaData.name); - - context->descriptorSetLayouts[numDescriptorSetLayoutsInited] = descriptorSetLayout; - } - - // create pipeline layouts - for ( ; numPipelineLayoutsInited < NUM_DESCRIPTOR_SET_LAYOUTS; ++numPipelineLayoutsInited) - { - VkPipelineLayout pipelineLayout; - - DescriptorSetLayoutMetaData dslMetaData = DESCRIPTOR_SET_LAYOUT_META_DATA[numPipelineLayoutsInited]; - - VkPipelineLayoutCreateInfo info = {}; - info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; - info.pNext = NULL; - info.flags = 0; - info.setLayoutCount = 1; - info.pSetLayouts = &context->descriptorSetLayouts[numPipelineLayoutsInited]; - info.pushConstantRangeCount = 0; - info.pPushConstantRanges = NULL; - - result = vkCreatePipelineLayout(device, &info, NULL, &pipelineLayout); - if (result != VK_SUCCESS) - { - goto error_init_pipeline_layouts; - } - setObjectName(device, context, VK_OBJECT_TYPE_PIPELINE_LAYOUT, (uint64_t)pipelineLayout, dslMetaData.name); - - context->pipelineLayouts[numPipelineLayoutsInited] = pipelineLayout; - } - - for ( ; numShaderModulesInited < NUM_COMPUTE_SHADERS; ++numShaderModulesInited) - { - VkShaderModule shaderModule; - ComputeShaderMetaData csMetaData = COMPUTE_SHADER_META_DATA[numShaderModulesInited]; - - VkShaderModuleCreateInfo info = {}; - info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; - info.pNext = 0; - info.flags = 0; - if (use16Bit) - { - info.codeSize = csMetaData.spirv16Len; - info.pCode = csMetaData.shaderSpirv16; - } - else - { - info.codeSize = csMetaData.spirv32Len; - info.pCode = csMetaData.shaderSpirv32; - } - - result = vkCreateShaderModule(device, &info, NULL, &shaderModule); - if (result != VK_SUCCESS) - { - goto error_init_shader_modules; - } - setObjectName(device, context, VK_OBJECT_TYPE_SHADER_MODULE, (uint64_t)shaderModule, csMetaData.objectName); - - context->computeShaders[numShaderModulesInited] = shaderModule; - } - - for ( ; numPipelinesInited < NUM_COMPUTE_SHADERS; ++numPipelinesInited) - { - VkPipeline pipeline; - ComputeShaderMetaData csMetaData = COMPUTE_SHADER_META_DATA[numPipelinesInited]; - - VkPipelineShaderStageCreateInfo stageInfo = {}; - stageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; - stageInfo.pNext = NULL; - stageInfo.flags = 0; - stageInfo.stage = VK_SHADER_STAGE_COMPUTE_BIT; - stageInfo.module = context->computeShaders[numPipelinesInited]; - stageInfo.pName = csMetaData.name; - stageInfo.pSpecializationInfo = NULL; - - VkComputePipelineCreateInfo info = {}; - info.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; - info.pNext = NULL; - info.flags = 0; - info.stage = stageInfo; - info.layout = context->pipelineLayouts[csMetaData.descriptorSetLayoutID]; - info.basePipelineHandle = VK_NULL_HANDLE; - info.basePipelineIndex = 0; - - result = vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &info, NULL, &pipeline); - if (result != VK_SUCCESS) - { - goto error_init_pipelines; - } - setObjectName(device, context, VK_OBJECT_TYPE_PIPELINE, (uint64_t)pipeline, csMetaData.objectName); - - context->computePipelines[numPipelinesInited] = pipeline; - } - - // create descriptor pool - { - VkDescriptorPool descriptorPool; - - VkDescriptorPoolSize poolSizes[4] = {}; - poolSizes[0].type = VK_DESCRIPTOR_TYPE_SAMPLER; - poolSizes[0].descriptorCount = NUM_BACK_BUFFERS * NUM_DESCRIPTOR_SETS * 5; - poolSizes[1].type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; - poolSizes[1].descriptorCount = NUM_BACK_BUFFERS * NUM_DESCRIPTOR_SETS * 7; - poolSizes[2].type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - poolSizes[2].descriptorCount = NUM_BACK_BUFFERS * NUM_DESCRIPTOR_SETS * 4; - poolSizes[3].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - poolSizes[3].descriptorCount = NUM_BACK_BUFFERS * NUM_DESCRIPTOR_SETS * 1; - - VkDescriptorPoolCreateInfo info = {}; - info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; - info.pNext = NULL; - info.flags = 0; - info.maxSets = NUM_BACK_BUFFERS * NUM_DESCRIPTOR_SETS; - info.poolSizeCount = FFX_CACAO_ARRAY_SIZE(poolSizes); - info.pPoolSizes = poolSizes; - - result = vkCreateDescriptorPool(device, &info, NULL, &descriptorPool); - if (result != VK_SUCCESS) - { - goto error_init_descriptor_pool; - } - setObjectName(device, context, VK_OBJECT_TYPE_DESCRIPTOR_POOL, (uint64_t)descriptorPool, "FFX_CACAO_DESCRIPTOR_POOL"); - - context->descriptorPool = descriptorPool; - } - - // allocate descriptor sets - { - VkDescriptorSetLayout descriptorSetLayouts[NUM_DESCRIPTOR_SETS]; - for (uint32_t i = 0; i < NUM_DESCRIPTOR_SETS; ++i) { - descriptorSetLayouts[i] = context->descriptorSetLayouts[DESCRIPTOR_SET_META_DATA[i].descriptorSetLayoutID]; - } - - for (uint32_t i = 0; i < NUM_BACK_BUFFERS; ++i) { - VkDescriptorSetAllocateInfo info = {}; - info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; - info.pNext = NULL; - info.descriptorPool = context->descriptorPool; - info.descriptorSetCount = FFX_CACAO_ARRAY_SIZE(descriptorSetLayouts); // FFX_CACAO_ARRAY_SIZE(context->descriptorSetLayouts); - info.pSetLayouts = descriptorSetLayouts; // context->descriptorSetLayouts; - - result = vkAllocateDescriptorSets(device, &info, context->descriptorSets[i]); - if (result != VK_SUCCESS) - { - goto error_allocate_descriptor_sets; - } - } - - char name[1024]; - for (uint32_t j = 0; j < NUM_BACK_BUFFERS; ++j) { - for (uint32_t i = 0; i < NUM_DESCRIPTOR_SETS; ++i) { - DescriptorSetMetaData dsMetaData = DESCRIPTOR_SET_META_DATA[i]; - snprintf(name, FFX_CACAO_ARRAY_SIZE(name), "%s_%u", dsMetaData.name, j); - setObjectName(device, context, VK_OBJECT_TYPE_DESCRIPTOR_SET, (uint64_t)context->descriptorSets[j][i], name); - } - } - } - - // assign memory to constant buffers - for ( ; numConstantBackBuffersInited < NUM_BACK_BUFFERS; ++numConstantBackBuffersInited) - { - for (uint32_t j = 0; j < 4; ++j) - { - VkBuffer buffer = context->constantBuffer[numConstantBackBuffersInited][j]; - - VkBufferCreateInfo info = {}; - info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; - info.pNext = NULL; - info.flags = 0; - info.size = sizeof(FfxCacaoConstants); - info.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; - info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - info.queueFamilyIndexCount = 0; - info.pQueueFamilyIndices = NULL; - - result = vkCreateBuffer(device, &info, NULL, &buffer); - if (result != VK_SUCCESS) - { - goto error_init_constant_buffers; - } - char name[1024]; - snprintf(name, FFX_CACAO_ARRAY_SIZE(name), "FFX_CACAO_CONSTANT_BUFFER_PASS_%u_BACK_BUFFER_%u", j, numConstantBackBuffersInited); - setObjectName(device, context, VK_OBJECT_TYPE_BUFFER, (uint64_t)buffer, name); - - VkMemoryRequirements memoryRequirements; - vkGetBufferMemoryRequirements(device, buffer, &memoryRequirements); - - uint32_t chosenMemoryTypeIndex = getBestMemoryHeapIndex(physicalDevice, memoryRequirements, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); - if (chosenMemoryTypeIndex == VK_MAX_MEMORY_TYPES) - { - vkDestroyBuffer(device, buffer, NULL); - goto error_init_constant_buffers; - } - - VkMemoryAllocateInfo allocationInfo = {}; - allocationInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; - allocationInfo.pNext = NULL; - allocationInfo.allocationSize = memoryRequirements.size; - allocationInfo.memoryTypeIndex = chosenMemoryTypeIndex; - - VkDeviceMemory memory; - result = vkAllocateMemory(device, &allocationInfo, NULL, &memory); - if (result != VK_SUCCESS) - { - vkDestroyBuffer(device, buffer, NULL); - goto error_init_constant_buffers; - } - - result = vkBindBufferMemory(device, buffer, memory, 0); - if (result != VK_SUCCESS) - { - vkDestroyBuffer(device, buffer, NULL); - goto error_init_constant_buffers; - } - - context->constantBufferMemory[numConstantBackBuffersInited][j] = memory; - context->constantBuffer[numConstantBackBuffersInited][j] = buffer; - } - } - - // create load counter VkImage - { - VkImage image = VK_NULL_HANDLE; - - VkImageCreateInfo info = {}; - info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; - info.pNext = NULL; - info.flags = 0; - info.imageType = VK_IMAGE_TYPE_1D; - info.format = VK_FORMAT_R32_UINT; - info.extent.width = 1; - info.extent.height = 1; - info.extent.depth = 1; - info.mipLevels = 1; - info.arrayLayers = 1; - info.samples = VK_SAMPLE_COUNT_1_BIT; - info.tiling = VK_IMAGE_TILING_OPTIMAL; - info.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; - info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - info.queueFamilyIndexCount = 0; - info.pQueueFamilyIndices = NULL; - info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - - result = vkCreateImage(device, &info, NULL, &image); - if (result != VK_SUCCESS) - { - goto error_init_load_counter_image; - } - - setObjectName(device, context, VK_OBJECT_TYPE_IMAGE, (uint64_t)image, "FFX_CACAO_LOAD_COUNTER"); - - VkMemoryRequirements memoryRequirements; - vkGetImageMemoryRequirements(device, image, &memoryRequirements); - - uint32_t chosenMemoryTypeIndex = getBestMemoryHeapIndex(physicalDevice, memoryRequirements, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - if (chosenMemoryTypeIndex == VK_MAX_MEMORY_TYPES) - { - vkDestroyImage(device, image, NULL); - goto error_init_load_counter_image; - } - - VkMemoryAllocateInfo allocationInfo = {}; - allocationInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; - allocationInfo.pNext = NULL; - allocationInfo.allocationSize = memoryRequirements.size; - allocationInfo.memoryTypeIndex = chosenMemoryTypeIndex; - - VkDeviceMemory memory; - result = vkAllocateMemory(device, &allocationInfo, NULL, &memory); - if (result != VK_SUCCESS) - { - vkDestroyImage(device, image, NULL); - goto error_init_load_counter_image; - } - - result = vkBindImageMemory(device, image, memory, 0); - if (result != VK_SUCCESS) - { - vkDestroyImage(device, image, NULL); - goto error_init_load_counter_image; - } - - context->loadCounter = image; - context->loadCounterMemory = memory; - } - - // create load counter view - { - VkImageView imageView; - - VkImageViewCreateInfo info = {}; - info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; - info.pNext = NULL; - info.flags = 0; - info.image = context->loadCounter; - info.viewType = VK_IMAGE_VIEW_TYPE_1D; - info.format = VK_FORMAT_R32_UINT; - info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; - info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; - info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; - info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; - info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - info.subresourceRange.baseMipLevel = 0; - info.subresourceRange.levelCount = 1; - info.subresourceRange.baseArrayLayer = 0; - info.subresourceRange.layerCount = 1; - - result = vkCreateImageView(device, &info, NULL, &imageView); - if (result != VK_SUCCESS) - { - goto error_init_load_counter_view; - } - - context->loadCounterView = imageView; - } - -#ifdef FFX_CACAO_ENABLE_PROFILING - // create timestamp query pool - { - VkQueryPool queryPool = VK_NULL_HANDLE; - - VkQueryPoolCreateInfo info = {}; - info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; - info.pNext = NULL; - info.flags = 0; - info.queryType = VK_QUERY_TYPE_TIMESTAMP; - info.queryCount = NUM_TIMESTAMPS * NUM_BACK_BUFFERS; - - result = vkCreateQueryPool(device, &info, NULL, &queryPool); - if (result != VK_SUCCESS) - { - goto error_init_query_pool; - } - - context->timestampQueryPool = queryPool; - } -#endif - - return FFX_CACAO_STATUS_OK; - -#ifdef FFX_CACAO_ENABLE_PROFILING - vkDestroyQueryPool(device, context->timestampQueryPool, NULL); -error_init_query_pool: -#endif - - vkDestroyImageView(device, context->loadCounterView, NULL); -error_init_load_counter_view: - vkDestroyImage(device, context->loadCounter, NULL); - vkFreeMemory(device, context->loadCounterMemory, NULL); -error_init_load_counter_image: - -error_init_constant_buffers: - for (uint32_t i = 0; i < numConstantBackBuffersInited; ++i) - { - for (uint32_t j = 0; j < 4; ++j) - { - vkDestroyBuffer(device, context->constantBuffer[i][j], NULL); - vkFreeMemory(device, context->constantBufferMemory[i][j], NULL); - } - } - -error_allocate_descriptor_sets: - vkDestroyDescriptorPool(device, context->descriptorPool, NULL); -error_init_descriptor_pool: - -error_init_pipelines: - for (uint32_t i = 0; i < numPipelinesInited; ++i) - { - vkDestroyPipeline(device, context->computePipelines[i], NULL); - } - -error_init_shader_modules: - for (uint32_t i = 0; i < numShaderModulesInited; ++i) - { - vkDestroyShaderModule(device, context->computeShaders[i], NULL); - } - -error_init_pipeline_layouts: - for (uint32_t i = 0; i < numPipelineLayoutsInited; ++i) - { - vkDestroyPipelineLayout(device, context->pipelineLayouts[i], NULL); - } - -error_init_descriptor_set_layouts: - for (uint32_t i = 0; i < numDescriptorSetLayoutsInited; ++i) - { - vkDestroyDescriptorSetLayout(device, context->descriptorSetLayouts[i], NULL); - } - - -error_init_samplers: - for (uint32_t i = 0; i < numSamplersInited; ++i) - { - vkDestroySampler(device, context->samplers[i], NULL); - } - - return errorStatus; -} - -FfxCacaoStatus ffxCacaoVkDestroyContext(FfxCacaoVkContext* context) -{ - if (context == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - context = getAlignedVkContextPointer(context); - - VkDevice device = context->device; - -#ifdef FFX_CACAO_ENABLE_PROFILING - vkDestroyQueryPool(device, context->timestampQueryPool, NULL); -#endif - - vkDestroyImageView(device, context->loadCounterView, NULL); - vkDestroyImage(device, context->loadCounter, NULL); - vkFreeMemory(device, context->loadCounterMemory, NULL); - - for (uint32_t i = 0; i < NUM_BACK_BUFFERS; ++i) - { - for (uint32_t j = 0; j < 4; ++j) - { - vkDestroyBuffer(device, context->constantBuffer[i][j], NULL); - vkFreeMemory(device, context->constantBufferMemory[i][j], NULL); - } - } - - vkDestroyDescriptorPool(device, context->descriptorPool, NULL); - - for (uint32_t i = 0; i < NUM_COMPUTE_SHADERS; ++i) - { - vkDestroyPipeline(device, context->computePipelines[i], NULL); - } - - for (uint32_t i = 0; i < NUM_COMPUTE_SHADERS; ++i) - { - vkDestroyShaderModule(device, context->computeShaders[i], NULL); - } - - for (uint32_t i = 0; i < NUM_DESCRIPTOR_SET_LAYOUTS; ++i) - { - vkDestroyPipelineLayout(device, context->pipelineLayouts[i], NULL); - } - - for(uint32_t i = 0; i < NUM_DESCRIPTOR_SET_LAYOUTS; ++i) - { - vkDestroyDescriptorSetLayout(device, context->descriptorSetLayouts[i], NULL); - } - - - for (uint32_t i = 0; i < FFX_CACAO_ARRAY_SIZE(context->samplers); ++i) - { - vkDestroySampler(device, context->samplers[i], NULL); - } - - return FFX_CACAO_STATUS_OK; -} - -FfxCacaoStatus ffxCacaoVkInitScreenSizeDependentResources(FfxCacaoVkContext* context, const FfxCacaoVkScreenSizeInfo* info) -{ - if (context == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - if (info == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - context = getAlignedVkContextPointer(context); - -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - FfxCacaoBool useDownsampledSsao = info->useDownsampledSsao; -#else - FfxCacaoBool useDownsampledSsao = FFX_CACAO_TRUE; -#endif - context->useDownsampledSsao = useDownsampledSsao; - context->output = info->output; - - VkDevice device = context->device; - VkPhysicalDevice physicalDevice = context->physicalDevice; - VkPhysicalDeviceMemoryProperties memoryProperties; - vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memoryProperties); - VkResult result; - - uint32_t width = info->width; - uint32_t height = info->height; - uint32_t halfWidth = (width + 1) / 2; - uint32_t halfHeight = (height + 1) / 2; - uint32_t quarterWidth = (halfWidth + 1) / 2; - uint32_t quarterHeight = (halfHeight + 1) / 2; - uint32_t eighthWidth = (quarterWidth + 1) / 2; - uint32_t eighthHeight = (quarterHeight + 1) / 2; - - uint32_t depthBufferWidth = width; - uint32_t depthBufferHeight = height; - uint32_t depthBufferHalfWidth = halfWidth; - uint32_t depthBufferHalfHeight = halfHeight; - uint32_t depthBufferQuarterWidth = quarterWidth; - uint32_t depthBufferQuarterHeight = quarterHeight; - - uint32_t depthBufferXOffset = 0; - uint32_t depthBufferYOffset = 0; - uint32_t depthBufferHalfXOffset = 0; - uint32_t depthBufferHalfYOffset = 0; - uint32_t depthBufferQuarterXOffset = 0; - uint32_t depthBufferQuarterYOffset = 0; - - BufferSizeInfo bsi = {}; - bsi.inputOutputBufferWidth = width; - bsi.inputOutputBufferHeight = height; - bsi.depthBufferXOffset = depthBufferXOffset; - bsi.depthBufferYOffset = depthBufferYOffset; - bsi.depthBufferWidth = depthBufferWidth; - bsi.depthBufferHeight = depthBufferHeight; - - if (useDownsampledSsao) - { - bsi.ssaoBufferWidth = quarterWidth; - bsi.ssaoBufferHeight = quarterHeight; - bsi.deinterleavedDepthBufferXOffset = depthBufferQuarterXOffset; - bsi.deinterleavedDepthBufferYOffset = depthBufferQuarterYOffset; - bsi.deinterleavedDepthBufferWidth = depthBufferQuarterWidth; - bsi.deinterleavedDepthBufferHeight = depthBufferQuarterHeight; - bsi.importanceMapWidth = eighthWidth; - bsi.importanceMapHeight = eighthHeight; - } - else - { - bsi.ssaoBufferWidth = halfWidth; - bsi.ssaoBufferHeight = halfHeight; - bsi.deinterleavedDepthBufferXOffset = depthBufferHalfXOffset; - bsi.deinterleavedDepthBufferYOffset = depthBufferHalfYOffset; - bsi.deinterleavedDepthBufferWidth = depthBufferHalfWidth; - bsi.deinterleavedDepthBufferHeight = depthBufferHalfHeight; - bsi.importanceMapWidth = quarterWidth; - bsi.importanceMapHeight = quarterHeight; - } - - context->bufferSizeInfo = bsi; - - FfxCacaoStatus errorStatus = FFX_CACAO_STATUS_FAILED; - uint32_t numTextureImagesInited = 0; - uint32_t numTextureMemoriesInited = 0; - uint32_t numSrvsInited = 0; - uint32_t numUavsInited = 0; - - // create images for textures - for ( ; numTextureImagesInited < NUM_TEXTURES; ++numTextureImagesInited) - { - TextureMetaData metaData = TEXTURE_META_DATA[numTextureImagesInited]; - VkImage image = VK_NULL_HANDLE; - - VkImageCreateInfo info = {}; - info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; - info.pNext = NULL; - info.flags = 0; - info.imageType = VK_IMAGE_TYPE_2D; - info.format = metaData.format; - info.extent.width = *(uint32_t*)((uint8_t*)&bsi + metaData.widthOffset); - info.extent.height = *(uint32_t*)((uint8_t*)&bsi + metaData.heightOffset); - info.extent.depth = 1; - info.mipLevels = metaData.numMips; - info.arrayLayers = metaData.arraySize; - info.samples = VK_SAMPLE_COUNT_1_BIT; - info.tiling = VK_IMAGE_TILING_OPTIMAL; - info.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; - info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - info.queueFamilyIndexCount = 0; - info.pQueueFamilyIndices = NULL; - info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - - result = vkCreateImage(device, &info, NULL, &image); - if (result != VK_SUCCESS) - { - goto error_init_texture_images; - } - - setObjectName(device, context, VK_OBJECT_TYPE_IMAGE, (uint64_t)image, metaData.name); - - context->textures[numTextureImagesInited] = image; - } - - // allocate memory for textures - for ( ; numTextureMemoriesInited < NUM_TEXTURES; ++numTextureMemoriesInited) - { - VkImage image = context->textures[numTextureMemoriesInited]; - - VkMemoryRequirements memoryRequirements; - vkGetImageMemoryRequirements(device, image, &memoryRequirements); - - uint32_t chosenMemoryTypeIndex = getBestMemoryHeapIndex(physicalDevice, memoryRequirements, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - if (chosenMemoryTypeIndex == VK_MAX_MEMORY_TYPES) - { - goto error_init_texture_memories; - } - - VkMemoryAllocateInfo allocationInfo = {}; - allocationInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; - allocationInfo.pNext = NULL; - allocationInfo.allocationSize = memoryRequirements.size; - allocationInfo.memoryTypeIndex = chosenMemoryTypeIndex; - - VkDeviceMemory memory; - result = vkAllocateMemory(device, &allocationInfo, NULL, &memory); - if (result != VK_SUCCESS) - { - goto error_init_texture_memories; - } - - result = vkBindImageMemory(device, image, memory, 0); - if (result != VK_SUCCESS) - { - vkFreeMemory(device, memory, NULL); - goto error_init_texture_memories; - } - - context->textureMemory[numTextureMemoriesInited] = memory; - } - - // create srv image views - for ( ; numSrvsInited < NUM_SHADER_RESOURCE_VIEWS; ++numSrvsInited) - { - VkImageView imageView; - ShaderResourceViewMetaData srvMetaData = SRV_META_DATA[numSrvsInited]; - - VkImageViewCreateInfo info = {}; - info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; - info.pNext = NULL; - info.flags = 0; - info.image = context->textures[srvMetaData.texture]; - info.viewType = srvMetaData.viewType; - info.format = TEXTURE_META_DATA[srvMetaData.texture].format; - info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; - info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; - info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; - info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; - info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - info.subresourceRange.baseMipLevel = srvMetaData.mostDetailedMip; - info.subresourceRange.levelCount = srvMetaData.mipLevels; - info.subresourceRange.baseArrayLayer = srvMetaData.firstArraySlice; - info.subresourceRange.layerCount = srvMetaData.arraySize; - - result = vkCreateImageView(device, &info, NULL, &imageView); - if (result != VK_SUCCESS) - { - goto error_init_srvs; - } - - context->shaderResourceViews[numSrvsInited] = imageView; - } - - // create uav image views - for ( ; numUavsInited < NUM_UNORDERED_ACCESS_VIEWS; ++numUavsInited) - { - VkImageView imageView; - UnorderedAccessViewMetaData uavMetaData = UAV_META_DATA[numUavsInited]; - - VkImageViewCreateInfo info = {}; - info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; - info.pNext = NULL; - info.flags = 0; - info.image = context->textures[uavMetaData.textureID]; - info.viewType = uavMetaData.viewType; - info.format = TEXTURE_META_DATA[uavMetaData.textureID].format; - info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; - info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; - info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; - info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; - info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - info.subresourceRange.baseMipLevel = uavMetaData.mostDetailedMip; - info.subresourceRange.levelCount = 1; - info.subresourceRange.baseArrayLayer = uavMetaData.firstArraySlice; - info.subresourceRange.layerCount = uavMetaData.arraySize; - - result = vkCreateImageView(device, &info, NULL, &imageView); - if (result != VK_SUCCESS) - { - goto error_init_uavs; - } - - context->unorderedAccessViews[numUavsInited] = imageView; - } - - // update descriptor sets from table - for (uint32_t i = 0; i < NUM_BACK_BUFFERS; ++i) { - VkDescriptorImageInfo imageInfos[NUM_INPUT_DESCRIPTOR_BINDINGS + NUM_OUTPUT_DESCRIPTOR_BINDINGS] = {}; - VkDescriptorImageInfo *curImageInfo = imageInfos; - VkWriteDescriptorSet writes[NUM_INPUT_DESCRIPTOR_BINDINGS + NUM_OUTPUT_DESCRIPTOR_BINDINGS] = {}; - VkWriteDescriptorSet *curWrite = writes; - - // write input descriptor bindings - for (uint32_t j = 0; j < NUM_INPUT_DESCRIPTOR_BINDINGS; ++j) - { - InputDescriptorBindingMetaData bindingMetaData = INPUT_DESCRIPTOR_BINDING_META_DATA[j]; - - curImageInfo->sampler = VK_NULL_HANDLE; - curImageInfo->imageView = context->shaderResourceViews[bindingMetaData.srvID]; - curImageInfo->imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - - curWrite->sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - curWrite->pNext = NULL; - curWrite->dstSet = context->descriptorSets[i][bindingMetaData.descriptorID]; - curWrite->dstBinding = 20 + bindingMetaData.bindingNumber; - curWrite->descriptorCount = 1; - curWrite->descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; - curWrite->pImageInfo = curImageInfo; - - ++curWrite; ++curImageInfo; - } - - // write output descriptor bindings - for (uint32_t j = 0; j < NUM_OUTPUT_DESCRIPTOR_BINDINGS; ++j) - { - OutputDescriptorBindingMetaData bindingMetaData = OUTPUT_DESCRIPTOR_BINDING_META_DATA[j]; - - curImageInfo->sampler = VK_NULL_HANDLE; - curImageInfo->imageView = context->unorderedAccessViews[bindingMetaData.uavID]; - curImageInfo->imageLayout = VK_IMAGE_LAYOUT_GENERAL; - - curWrite->sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - curWrite->pNext = VK_NULL_HANDLE; - curWrite->dstSet = context->descriptorSets[i][bindingMetaData.descriptorID]; - curWrite->dstBinding = 30 + bindingMetaData.bindingNumber; - curWrite->descriptorCount = 1; - curWrite->descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - curWrite->pImageInfo = curImageInfo; - - ++curWrite; ++curImageInfo; - } - - vkUpdateDescriptorSets(device, FFX_CACAO_ARRAY_SIZE(writes), writes, 0, NULL); - } - - // update descriptor sets with inputs - for (uint32_t i = 0; i < NUM_BACK_BUFFERS; ++i) { -#define MAX_NUM_MISC_INPUT_DESCRIPTORS 32 - - VkDescriptorImageInfo imageInfos[MAX_NUM_MISC_INPUT_DESCRIPTORS] = {}; - VkWriteDescriptorSet writes[MAX_NUM_MISC_INPUT_DESCRIPTORS] = {}; - - for (uint32_t i = 0; i < FFX_CACAO_ARRAY_SIZE(writes); ++i) - { - VkDescriptorImageInfo *imageInfo = imageInfos + i; - VkWriteDescriptorSet *write = writes + i; - - imageInfo->sampler = VK_NULL_HANDLE; - - write->sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write->pNext = NULL; - write->descriptorCount = 1; - write->pImageInfo = imageInfo; - } - - uint32_t cur = 0; - - // register(t0) -> 20 - // register(u0) -> 30 - imageInfos[cur].imageView = info->depthView; - imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - writes[cur].dstSet = context->descriptorSets[i][DS_PREPARE_DEPTHS]; - writes[cur].dstBinding = 20; - writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; - ++cur; - - imageInfos[cur].imageView = info->depthView; - imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - writes[cur].dstSet = context->descriptorSets[i][DS_PREPARE_DEPTHS_MIPS]; - writes[cur].dstBinding = 20; - writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; - ++cur; - - imageInfos[cur].imageView = info->depthView; - imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - writes[cur].dstSet = context->descriptorSets[i][DS_PREPARE_NORMALS]; - writes[cur].dstBinding = 20; - writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; - ++cur; - - imageInfos[cur].imageView = info->depthView; - imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - writes[cur].dstSet = context->descriptorSets[i][DS_BILATERAL_UPSAMPLE_PING]; - writes[cur].dstBinding = 21; - writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; - ++cur; - - imageInfos[cur].imageView = info->depthView; - imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - writes[cur].dstSet = context->descriptorSets[i][DS_BILATERAL_UPSAMPLE_PONG]; - writes[cur].dstBinding = 21; - writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; - ++cur; - - imageInfos[cur].imageView = info->outputView; - imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_GENERAL; - writes[cur].dstSet = context->descriptorSets[i][DS_BILATERAL_UPSAMPLE_PING]; - writes[cur].dstBinding = 30; - writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - ++cur; - - imageInfos[cur].imageView = info->outputView; - imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_GENERAL; - writes[cur].dstSet = context->descriptorSets[i][DS_BILATERAL_UPSAMPLE_PONG]; - writes[cur].dstBinding = 30; - writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - ++cur; - - imageInfos[cur].imageView = info->outputView; - imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_GENERAL; - writes[cur].dstSet = context->descriptorSets[i][DS_APPLY_PING]; - writes[cur].dstBinding = 30; - writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - ++cur; - - imageInfos[cur].imageView = info->outputView; - imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_GENERAL; - writes[cur].dstSet = context->descriptorSets[i][DS_APPLY_PONG]; - writes[cur].dstBinding = 30; - writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - ++cur; - - imageInfos[cur].imageView = context->loadCounterView; - imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_GENERAL; - writes[cur].dstSet = context->descriptorSets[i][DS_POSTPROCESS_IMPORTANCE_MAP_B]; - writes[cur].dstBinding = 31; - writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - ++cur; - - imageInfos[cur].imageView = context->loadCounterView; - imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_GENERAL; - writes[cur].dstSet = context->descriptorSets[i][DS_CLEAR_LOAD_COUNTER]; - writes[cur].dstBinding = 30; - writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - ++cur; - - for (uint32_t pass = 0; pass < 4; ++pass) - { - imageInfos[cur].imageView = context->loadCounterView; - imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_GENERAL; - writes[cur].dstSet = context->descriptorSets[i][(DescriptorSetID)(DS_GENERATE_ADAPTIVE_0 + pass)]; - writes[cur].dstBinding = 22; - writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; - ++cur; - } - - if (info->normalsView) { - imageInfos[cur].imageView = info->normalsView; - imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - writes[cur].dstSet = context->descriptorSets[i][DS_PREPARE_NORMALS_FROM_INPUT_NORMALS]; - writes[cur].dstBinding = 20; - writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; - ++cur; - } - - FFX_CACAO_ASSERT(cur <= MAX_NUM_MISC_INPUT_DESCRIPTORS); - vkUpdateDescriptorSets(device, cur, writes, 0, NULL); - } - - // update descriptor sets with constant buffers - for (uint32_t i = 0; i < NUM_BACK_BUFFERS; ++i) { - VkDescriptorBufferInfo bufferInfos[NUM_DESCRIPTOR_SETS] = {}; - VkDescriptorBufferInfo *curBufferInfo = bufferInfos; - VkWriteDescriptorSet writes[NUM_DESCRIPTOR_SETS] = {}; - VkWriteDescriptorSet *curWrite = writes; - - for (uint32_t j = 0; j < NUM_DESCRIPTOR_SETS; ++j) - { - DescriptorSetMetaData dsMetaData = DESCRIPTOR_SET_META_DATA[j]; - - curBufferInfo->buffer = context->constantBuffer[i][dsMetaData.pass]; - curBufferInfo->offset = 0; - curBufferInfo->range = VK_WHOLE_SIZE; - - curWrite->sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - curWrite->pNext = NULL; - curWrite->dstSet = context->descriptorSets[i][j]; - curWrite->dstBinding = 10; - curWrite->dstArrayElement = 0; - curWrite->descriptorCount = 1; - curWrite->descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - curWrite->pBufferInfo = curBufferInfo; - - ++curWrite; - ++curBufferInfo; - } - - vkUpdateDescriptorSets(device, FFX_CACAO_ARRAY_SIZE(writes), writes, 0, NULL); - } - - return FFX_CACAO_STATUS_OK; - -error_init_uavs: - for (uint32_t i = 0; i < numUavsInited; ++i) - { - vkDestroyImageView(device, context->unorderedAccessViews[i], NULL); - } - -error_init_srvs: - for (uint32_t i = 0; i < numSrvsInited; ++i) - { - vkDestroyImageView(device, context->shaderResourceViews[i], NULL); - } - -error_init_texture_memories: - for (uint32_t i = 0; i < numTextureMemoriesInited; ++i) - { - vkFreeMemory(device, context->textureMemory[i], NULL); - } - -error_init_texture_images: - for (uint32_t i = 0; i < numTextureImagesInited; ++i) - { - vkDestroyImage(device, context->textures[i], NULL); - } - - return errorStatus; -} - -FfxCacaoStatus ffxCacaoVkDestroyScreenSizeDependentResources(FfxCacaoVkContext* context) -{ - if (context == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - context = getAlignedVkContextPointer(context); - - VkDevice device = context->device; - - for (uint32_t i = 0; i < NUM_UNORDERED_ACCESS_VIEWS; ++i) - { - vkDestroyImageView(device, context->unorderedAccessViews[i], NULL); - } - - for (uint32_t i = 0; i < NUM_SHADER_RESOURCE_VIEWS; ++i) - { - vkDestroyImageView(device, context->shaderResourceViews[i], NULL); - } - - for (uint32_t i = 0; i < NUM_TEXTURES; ++i) - { - vkFreeMemory(device, context->textureMemory[i], NULL); - } - - for (uint32_t i = 0; i < NUM_TEXTURES; ++i) - { - vkDestroyImage(device, context->textures[i], NULL); - } - - return FFX_CACAO_STATUS_OK; -} - -FfxCacaoStatus ffxCacaoVkUpdateSettings(FfxCacaoVkContext* context, const FfxCacaoSettings* settings) -{ - if (context == NULL || settings == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - context = getAlignedVkContextPointer(context); - - memcpy(&context->settings, settings, sizeof(*settings)); - - return FFX_CACAO_STATUS_OK; -} - -static inline void computeDispatch(FfxCacaoVkContext* context, VkCommandBuffer cb, DescriptorSetID ds, ComputeShaderID cs, uint32_t width, uint32_t height) -{ - DescriptorSetLayoutID dsl = DESCRIPTOR_SET_META_DATA[ds].descriptorSetLayoutID; - vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_COMPUTE, context->pipelineLayouts[dsl], 0, 1, &context->descriptorSets[context->currentConstantBuffer][ds], 0, NULL); - vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, context->computePipelines[cs]); - vkCmdDispatch(cb, width, height, 1); -} - -typedef struct BarrierList -{ - uint32_t len; - VkImageMemoryBarrier barriers[32]; -} BarrierList; - -static inline void pushBarrier(BarrierList* barrierList, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkAccessFlags srcAccessFlags, VkAccessFlags dstAccessFlags) -{ - FFX_CACAO_ASSERT(barrierList->len < FFX_CACAO_ARRAY_SIZE(barrierList->barriers)); - VkImageMemoryBarrier barrier = {}; - barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier.pNext = NULL; - barrier.srcAccessMask = srcAccessFlags; - barrier.dstAccessMask = dstAccessFlags; - barrier.oldLayout = oldLayout; - barrier.newLayout = newLayout; - barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - barrier.subresourceRange.baseMipLevel = 0; - barrier.subresourceRange.levelCount = VK_REMAINING_MIP_LEVELS; - barrier.subresourceRange.baseArrayLayer = 0; - barrier.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS; - barrier.image = image; - barrierList->barriers[barrierList->len++] = barrier; -} - -static inline void beginDebugMarker(FfxCacaoVkContext* context, VkCommandBuffer cb, const char* name) -{ - if (context->vkCmdDebugMarkerBegin) - { - VkDebugMarkerMarkerInfoEXT info = {}; - info.sType = VK_STRUCTURE_TYPE_DEBUG_MARKER_MARKER_INFO_EXT; - info.pNext = NULL; - info.pMarkerName = name; - info.color[0] = 1.0f; - info.color[1] = 0.0f; - info.color[2] = 0.0f; - info.color[3] = 1.0f; - - context->vkCmdDebugMarkerBegin(cb, &info); - } -} - -static inline void endDebugMarker(FfxCacaoVkContext* context, VkCommandBuffer cb) -{ - if (context->vkCmdDebugMarkerEnd) - { - context->vkCmdDebugMarkerEnd(cb); - } -} - -FfxCacaoStatus ffxCacaoVkDraw(FfxCacaoVkContext* context, VkCommandBuffer cb, const FfxCacaoMatrix4x4* proj, const FfxCacaoMatrix4x4* normalsToView) -{ - if (context == NULL || cb == VK_NULL_HANDLE || proj == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - context = getAlignedVkContextPointer(context); - - FfxCacaoSettings *settings = &context->settings; - BufferSizeInfo *bsi = &context->bufferSizeInfo; - VkDevice device = context->device; - VkDescriptorSet *ds = context->descriptorSets[context->currentConstantBuffer]; - VkImage *tex = context->textures; - VkResult result; - BarrierList barrierList; - - uint32_t curBuffer = context->currentConstantBuffer; - curBuffer = (curBuffer + 1) % NUM_BACK_BUFFERS; - context->currentConstantBuffer = curBuffer; -#ifdef FFX_CACAO_ENABLE_PROFILING - { - uint32_t collectBuffer = context->collectBuffer = (curBuffer + 1) % NUM_BACK_BUFFERS; - if (uint32_t numQueries = context->timestampQueries[collectBuffer].numTimestamps) - { - uint32_t offset = collectBuffer * NUM_TIMESTAMPS; - vkGetQueryPoolResults(device, context->timestampQueryPool, offset, numQueries, numQueries * sizeof(uint64_t), context->timestampQueries[collectBuffer].timings, sizeof(uint64_t), VK_QUERY_RESULT_64_BIT); - } - } -#endif - - beginDebugMarker(context, cb, "FidelityFX CACAO"); - - // update constant buffer - - for (uint32_t i = 0; i < 4; ++i) - { - VkDeviceMemory memory = context->constantBufferMemory[curBuffer][i]; - void *data = NULL; - result = vkMapMemory(device, memory, 0, VK_WHOLE_SIZE, 0, &data); - FFX_CACAO_ASSERT(result == VK_SUCCESS); - updateConstants((FfxCacaoConstants*)data, settings, bsi, proj, normalsToView); - updatePerPassConstants((FfxCacaoConstants*)data, settings, bsi, i); - vkUnmapMemory(device, memory); - } - -#ifdef FFX_CACAO_ENABLE_PROFILING - uint32_t queryPoolOffset = curBuffer * NUM_TIMESTAMPS; - uint32_t numTimestamps = 0; - vkCmdResetQueryPool(cb, context->timestampQueryPool, queryPoolOffset, NUM_TIMESTAMPS); -#define GET_TIMESTAMP(name) \ - context->timestampQueries[curBuffer].timestamps[numTimestamps] = TIMESTAMP_##name; \ - vkCmdWriteTimestamp(cb, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, context->timestampQueryPool, queryPoolOffset + numTimestamps++); -#else -#define GET_TIMESTAMP(name) -#endif - - GET_TIMESTAMP(BEGIN) - - barrierList.len = 0; - pushBarrier(&barrierList, tex[TEXTURE_DEINTERLEAVED_DEPTHS], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); - pushBarrier(&barrierList, tex[TEXTURE_DEINTERLEAVED_NORMALS], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); - pushBarrier(&barrierList, tex[TEXTURE_SSAO_BUFFER_PING], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); - pushBarrier(&barrierList, tex[TEXTURE_SSAO_BUFFER_PONG], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); - pushBarrier(&barrierList, tex[TEXTURE_IMPORTANCE_MAP], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); - pushBarrier(&barrierList, tex[TEXTURE_IMPORTANCE_MAP_PONG], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); - pushBarrier(&barrierList, context->loadCounter, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); - vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); - - // prepare depths, normals and mips - { - beginDebugMarker(context, cb, "Prepare downsampled depths, normals and mips"); - - // clear load counter - computeDispatch(context, cb, DS_CLEAR_LOAD_COUNTER, CS_CLEAR_LOAD_COUNTER, 1, 1); - - switch (context->settings.qualityLevel) - { - case FFX_CACAO_QUALITY_LOWEST: { - uint32_t dispatchWidth = dispatchSize(PREPARE_DEPTHS_HALF_WIDTH, bsi->deinterleavedDepthBufferWidth); - uint32_t dispatchHeight = dispatchSize(PREPARE_DEPTHS_HALF_HEIGHT, bsi->deinterleavedDepthBufferHeight); - ComputeShaderID csPrepareDepthsHalf = context->useDownsampledSsao ? CS_PREPARE_DOWNSAMPLED_DEPTHS_HALF : CS_PREPARE_NATIVE_DEPTHS_HALF; - computeDispatch(context, cb, DS_PREPARE_DEPTHS, csPrepareDepthsHalf, dispatchWidth, dispatchHeight); - break; - } - case FFX_CACAO_QUALITY_LOW: { - uint32_t dispatchWidth = dispatchSize(PREPARE_DEPTHS_WIDTH, bsi->deinterleavedDepthBufferWidth); - uint32_t dispatchHeight = dispatchSize(PREPARE_DEPTHS_HEIGHT, bsi->deinterleavedDepthBufferHeight); - ComputeShaderID csPrepareDepths = context->useDownsampledSsao ? CS_PREPARE_DOWNSAMPLED_DEPTHS : CS_PREPARE_NATIVE_DEPTHS; - computeDispatch(context, cb, DS_PREPARE_DEPTHS, csPrepareDepths, dispatchWidth, dispatchHeight); - break; - } - default: { - uint32_t dispatchWidth = dispatchSize(PREPARE_DEPTHS_AND_MIPS_WIDTH, bsi->deinterleavedDepthBufferWidth); - uint32_t dispatchHeight = dispatchSize(PREPARE_DEPTHS_AND_MIPS_HEIGHT, bsi->deinterleavedDepthBufferHeight); - ComputeShaderID csPrepareDepthsAndMips = context->useDownsampledSsao ? CS_PREPARE_DOWNSAMPLED_DEPTHS_AND_MIPS : CS_PREPARE_NATIVE_DEPTHS_AND_MIPS; - computeDispatch(context, cb, DS_PREPARE_DEPTHS_MIPS, csPrepareDepthsAndMips, dispatchWidth, dispatchHeight); - break; - } - } - - if (context->settings.generateNormals) - { - uint32_t dispatchWidth = dispatchSize(PREPARE_NORMALS_WIDTH, bsi->ssaoBufferWidth); - uint32_t dispatchHeight = dispatchSize(PREPARE_NORMALS_HEIGHT, bsi->ssaoBufferHeight); - ComputeShaderID csPrepareNormals = context->useDownsampledSsao ? CS_PREPARE_DOWNSAMPLED_NORMALS : CS_PREPARE_NATIVE_NORMALS; - computeDispatch(context, cb, DS_PREPARE_NORMALS, csPrepareNormals, dispatchWidth, dispatchHeight); - } - else - { - uint32_t dispatchWidth = dispatchSize(PREPARE_NORMALS_FROM_INPUT_NORMALS_WIDTH, bsi->ssaoBufferWidth); - uint32_t dispatchHeight = dispatchSize(PREPARE_NORMALS_FROM_INPUT_NORMALS_HEIGHT, bsi->ssaoBufferHeight); - ComputeShaderID csPrepareNormalsFromInputNormals = context->useDownsampledSsao ? CS_PREPARE_DOWNSAMPLED_NORMALS_FROM_INPUT_NORMALS : CS_PREPARE_NATIVE_NORMALS_FROM_INPUT_NORMALS; - computeDispatch(context, cb, DS_PREPARE_NORMALS_FROM_INPUT_NORMALS, csPrepareNormalsFromInputNormals, dispatchWidth, dispatchHeight); - } - - endDebugMarker(context, cb); - GET_TIMESTAMP(PREPARE) - } - - barrierList.len = 0; - pushBarrier(&barrierList, tex[TEXTURE_DEINTERLEAVED_DEPTHS], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); - pushBarrier(&barrierList, tex[TEXTURE_DEINTERLEAVED_NORMALS], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); - pushBarrier(&barrierList, context->loadCounter, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT); - vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); - - // base pass for highest quality setting - if (context->settings.qualityLevel == FFX_CACAO_QUALITY_HIGHEST) - { - beginDebugMarker(context, cb, "Generate High Quality Base Pass"); - - // SSAO - { - beginDebugMarker(context, cb, "Base SSAO"); - - uint32_t dispatchWidth = dispatchSize(GENERATE_WIDTH, bsi->ssaoBufferWidth); - uint32_t dispatchHeight = dispatchSize(GENERATE_WIDTH, bsi->ssaoBufferHeight); - - for (int pass = 0; pass < 4; ++pass) - { - computeDispatch(context, cb, (DescriptorSetID)(DS_GENERATE_ADAPTIVE_BASE_0 + pass), CS_GENERATE_Q3_BASE, dispatchWidth, dispatchHeight); - } - - endDebugMarker(context, cb); - } - - GET_TIMESTAMP(BASE_SSAO_PASS) - - barrierList.len = 0; - pushBarrier(&barrierList, tex[TEXTURE_SSAO_BUFFER_PONG], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); - vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); - - // generate importance map - { - beginDebugMarker(context, cb, "Importance Map"); - - uint32_t dispatchWidth = dispatchSize(IMPORTANCE_MAP_WIDTH, bsi->importanceMapWidth); - uint32_t dispatchHeight = dispatchSize(IMPORTANCE_MAP_HEIGHT, bsi->importanceMapHeight); - - computeDispatch(context, cb, DS_GENERATE_IMPORTANCE_MAP, CS_GENERATE_IMPORTANCE_MAP, dispatchWidth, dispatchHeight); - - barrierList.len = 0; - pushBarrier(&barrierList, tex[TEXTURE_IMPORTANCE_MAP], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); - vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); - - computeDispatch(context, cb, DS_POSTPROCESS_IMPORTANCE_MAP_A, CS_POSTPROCESS_IMPORTANCE_MAP_A, dispatchWidth, dispatchHeight); - - barrierList.len = 0; - pushBarrier(&barrierList, tex[TEXTURE_IMPORTANCE_MAP], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_SHADER_WRITE_BIT); - pushBarrier(&barrierList, tex[TEXTURE_IMPORTANCE_MAP_PONG], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); - vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); - - computeDispatch(context, cb, DS_POSTPROCESS_IMPORTANCE_MAP_B, CS_POSTPROCESS_IMPORTANCE_MAP_B, dispatchWidth, dispatchHeight); - - endDebugMarker(context, cb); - } - - endDebugMarker(context, cb); - GET_TIMESTAMP(IMPORTANCE_MAP) - - barrierList.len = 0; - pushBarrier(&barrierList, tex[TEXTURE_IMPORTANCE_MAP], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); - pushBarrier(&barrierList, context->loadCounter, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_SHADER_READ_BIT); - vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); - } - - // main ssao generation - { - beginDebugMarker(context, cb, "Generate SSAO"); - - uint32_t dispatchWidth = dispatchSize(GENERATE_WIDTH, bsi->ssaoBufferWidth); - uint32_t dispatchHeight = dispatchSize(GENERATE_WIDTH, bsi->ssaoBufferHeight); - - ComputeShaderID generateCS = (ComputeShaderID)(CS_GENERATE_Q0 + FFX_CACAO_MAX(0, context->settings.qualityLevel - 1)); - for (int pass = 0; pass < 4; ++pass) - { - if (context->settings.qualityLevel == FFX_CACAO_QUALITY_LOWEST && (pass == 1 || pass == 2)) - { - continue; - } - - DescriptorSetID descriptorSetID = context->settings.qualityLevel == FFX_CACAO_QUALITY_HIGHEST ? DS_GENERATE_ADAPTIVE_0 : DS_GENERATE_0; - descriptorSetID = (DescriptorSetID)(descriptorSetID + pass); - - computeDispatch(context, cb, descriptorSetID, generateCS, dispatchWidth, dispatchHeight); - } - - endDebugMarker(context, cb); - GET_TIMESTAMP(GENERATE_SSAO) - } - - uint32_t blurPassCount = context->settings.blurPassCount; - blurPassCount = FFX_CACAO_CLAMP(blurPassCount, 0, MAX_BLUR_PASSES); - - // de-interleaved blur - if (blurPassCount) - { - barrierList.len = 0; - pushBarrier(&barrierList, tex[TEXTURE_SSAO_BUFFER_PING], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); - pushBarrier(&barrierList, tex[TEXTURE_SSAO_BUFFER_PONG], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); - vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); - - beginDebugMarker(context, cb, "Deinterleaved Blur"); - - uint32_t w = 4 * BLUR_WIDTH - 2 * blurPassCount; - uint32_t h = 3 * BLUR_HEIGHT - 2 * blurPassCount; - uint32_t dispatchWidth = dispatchSize(w, bsi->ssaoBufferWidth); - uint32_t dispatchHeight = dispatchSize(h, bsi->ssaoBufferHeight); - - for (int pass = 0; pass < 4; ++pass) - { - if (context->settings.qualityLevel == FFX_CACAO_QUALITY_LOWEST && (pass == 1 || pass == 2)) - { - continue; - } - - ComputeShaderID blurShaderID = (ComputeShaderID)(CS_EDGE_SENSITIVE_BLUR_1 + blurPassCount - 1); - DescriptorSetID descriptorSetID = (DescriptorSetID)(DS_EDGE_SENSITIVE_BLUR_0 + pass); - computeDispatch(context, cb, descriptorSetID, blurShaderID, dispatchWidth, dispatchHeight); - } - - endDebugMarker(context, cb); - GET_TIMESTAMP(EDGE_SENSITIVE_BLUR) - - barrierList.len = 0; - pushBarrier(&barrierList, tex[TEXTURE_SSAO_BUFFER_PONG], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); - pushBarrier(&barrierList, context->output, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); - vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); - } - else - { - barrierList.len = 0; - pushBarrier(&barrierList, tex[TEXTURE_SSAO_BUFFER_PING], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); - pushBarrier(&barrierList, context->output, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); - vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); - } - - - if (context->useDownsampledSsao) - { - beginDebugMarker(context, cb, "Bilateral Upsample"); - - uint32_t dispatchWidth = dispatchSize(2 * BILATERAL_UPSCALE_WIDTH, bsi->inputOutputBufferWidth); - uint32_t dispatchHeight = dispatchSize(2 * BILATERAL_UPSCALE_HEIGHT, bsi->inputOutputBufferHeight); - - DescriptorSetID descriptorSetID = blurPassCount ? DS_BILATERAL_UPSAMPLE_PONG : DS_BILATERAL_UPSAMPLE_PING; - ComputeShaderID upscaler = context->settings.qualityLevel == FFX_CACAO_QUALITY_LOWEST ? CS_UPSCALE_BILATERAL_5X5_HALF : CS_UPSCALE_BILATERAL_5X5; - - computeDispatch(context, cb, descriptorSetID, upscaler, dispatchWidth, dispatchHeight); - - endDebugMarker(context, cb); - GET_TIMESTAMP(BILATERAL_UPSAMPLE) - } - else - { - beginDebugMarker(context, cb, "Reinterleave"); - - uint32_t dispatchWidth = dispatchSize(APPLY_WIDTH, bsi->inputOutputBufferWidth); - uint32_t dispatchHeight = dispatchSize(APPLY_HEIGHT, bsi->inputOutputBufferHeight); - - DescriptorSetID descriptorSetID = blurPassCount ? DS_APPLY_PONG : DS_APPLY_PING; - - switch (context->settings.qualityLevel) - { - case FFX_CACAO_QUALITY_LOWEST: - computeDispatch(context, cb, descriptorSetID, CS_NON_SMART_HALF_APPLY, dispatchWidth, dispatchHeight); - break; - case FFX_CACAO_QUALITY_LOW: - computeDispatch(context, cb, descriptorSetID, CS_NON_SMART_APPLY, dispatchWidth, dispatchHeight); - break; - default: - computeDispatch(context, cb, descriptorSetID, CS_APPLY, dispatchWidth, dispatchHeight); - break; - } - - endDebugMarker(context, cb); - GET_TIMESTAMP(APPLY) - } - - endDebugMarker(context, cb); - - barrierList.len = 0; - pushBarrier(&barrierList, context->output, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); - vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); - -#ifdef FFX_CACAO_ENABLE_PROFILING - context->timestampQueries[curBuffer].numTimestamps = numTimestamps; -#endif - - return FFX_CACAO_STATUS_OK; -} - -#ifdef FFX_CACAO_ENABLE_PROFILING -FfxCacaoStatus ffxCacaoVkGetDetailedTimings(FfxCacaoVkContext* context, FfxCacaoDetailedTiming* timings) -{ - if (context == NULL || timings == NULL) - { - return FFX_CACAO_STATUS_INVALID_POINTER; - } - context = getAlignedVkContextPointer(context); - - uint32_t bufferIndex = context->collectBuffer; - uint32_t numTimestamps = context->timestampQueries[bufferIndex].numTimestamps; - uint64_t prevTime = context->timestampQueries[bufferIndex].timings[0]; - for (uint32_t i = 1; i < numTimestamps; ++i) - { - TimestampID timestampID = context->timestampQueries[bufferIndex].timestamps[i]; - timings->timestamps[i].label = TIMESTAMP_NAMES[timestampID]; - uint64_t time = context->timestampQueries[bufferIndex].timings[i]; - timings->timestamps[i].ticks = time - prevTime; - prevTime = time; - } - timings->timestamps[0].label = "FFX_CACAO_TOTAL"; - timings->timestamps[0].ticks = prevTime - context->timestampQueries[bufferIndex].timings[0]; - timings->numTimestamps = numTimestamps; - - return FFX_CACAO_STATUS_OK; -} -#endif -#endif - -#ifdef __cplusplus -} -#endif diff --git a/ffx-cacao/src/ffx_cacao.hlsl b/ffx-cacao/src/ffx_cacao.hlsl index 027ad68..58dd95f 100644 --- a/ffx-cacao/src/ffx_cacao.hlsl +++ b/ffx-cacao/src/ffx_cacao.hlsl @@ -1,17 +1,17 @@ -// Modifications Copyright 2020. Advanced Micro Devices, Inc. All Rights Reserved. +// Modifications Copyright 2021. Advanced Micro Devices, Inc. All Rights Reserved. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Copyright (c) 2016, Intel Corporation -// 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 +// 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 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 +// 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. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // File changes (yyyy-mm-dd) @@ -20,76 +20,9 @@ #include "ffx_cacao_defines.h" +#include "ffx_cacao_bindings.hlsl" -#define SSAO_ENABLE_NORMAL_WORLD_TO_VIEW_CONVERSION 1 - -#define INTELSSAO_MAIN_DISK_SAMPLE_COUNT (32) - -struct CACAOConstants -{ - float2 DepthUnpackConsts; - float2 CameraTanHalfFOV; - - float2 NDCToViewMul; - float2 NDCToViewAdd; - - float2 DepthBufferUVToViewMul; - float2 DepthBufferUVToViewAdd; - - float EffectRadius; // world (viewspace) maximum size of the shadow - float EffectShadowStrength; // global strength of the effect (0 - 5) - float EffectShadowPow; - float EffectShadowClamp; - - float EffectFadeOutMul; // effect fade out from distance (ex. 25) - float EffectFadeOutAdd; // effect fade out to distance (ex. 100) - float EffectHorizonAngleThreshold; // limit errors on slopes and caused by insufficient geometry tessellation (0.05 to 0.5) - float EffectSamplingRadiusNearLimitRec; // if viewspace pixel closer than this, don't enlarge shadow sampling radius anymore (makes no sense to grow beyond some distance, not enough samples to cover everything, so just limit the shadow growth; could be SSAOSettingsFadeOutFrom * 0.1 or less) - - float DepthPrecisionOffsetMod; - float NegRecEffectRadius; // -1.0 / EffectRadius - float LoadCounterAvgDiv; // 1.0 / ( halfDepthMip[SSAO_DEPTH_MIP_LEVELS-1].sizeX * halfDepthMip[SSAO_DEPTH_MIP_LEVELS-1].sizeY ) - float AdaptiveSampleCountLimit; - - float InvSharpness; - int PassIndex; - float BilateralSigmaSquared; - float BilateralSimilarityDistanceSigma; - - float4 PatternRotScaleMatrices[5]; - - float NormalsUnpackMul; - float NormalsUnpackAdd; - float DetailAOStrength; - float Dummy0; - - float2 SSAOBufferDimensions; - float2 SSAOBufferInverseDimensions; - - float2 DepthBufferDimensions; - float2 DepthBufferInverseDimensions; - - int2 DepthBufferOffset; - float2 PerPassFullResUVOffset; - - float2 OutputBufferDimensions; - float2 OutputBufferInverseDimensions; - - float2 ImportanceMapDimensions; - float2 ImportanceMapInverseDimensions; - - float2 DeinterleavedDepthBufferDimensions; - float2 DeinterleavedDepthBufferInverseDimensions; - - float2 DeinterleavedDepthBufferOffset; - float2 DeinterleavedDepthBufferNormalisedOffset; - -#if SSAO_ENABLE_NORMAL_WORLD_TO_VIEW_CONVERSION - float4x4 NormalsWorldToViewspaceMatrix; -#endif -}; - -static const float4 g_samplePatternMain[INTELSSAO_MAIN_DISK_SAMPLE_COUNT] = +static const float4 g_FFX_CACAO_samplePatternMain[] = { 0.78488064, 0.56661671, 1.500000, -0.126083, 0.26022232, -0.29575172, 1.500000, -1.064030, 0.10459357, 0.08372527, 1.110000, -2.730563, -0.68286800, 0.04963045, 1.090000, -0.498827, -0.13570161, -0.64190155, 1.250000, -0.532765, -0.26193795, -0.08205118, 0.670000, -1.783245, -0.61177456, 0.66664219, 0.710000, -0.044234, 0.43675563, 0.25119025, 0.610000, -1.167283, @@ -101,15 +34,13 @@ static const float4 g_samplePatternMain[INTELSSAO_MAIN_DISK_SAMPLE_COUNT] = -0.15064627, -0.14949332, 0.600000, -1.896062, 0.53180975, -0.35210401, 0.600000, -0.758838, 0.41487166, 0.81442589, 0.600000, -0.505648, -0.24106961, -0.32721516, 0.600000, -1.665244 }; -#define SSAO_MAX_TAPS (32) -#define SSAO_MAX_REF_TAPS (512) -#define SSAO_ADAPTIVE_TAP_BASE_COUNT (5) -#define SSAO_ADAPTIVE_TAP_FLEXIBLE_COUNT (SSAO_MAX_TAPS - SSAO_ADAPTIVE_TAP_BASE_COUNT) -#define SSAO_DEPTH_MIP_LEVELS (4) +#define FFX_CACAO_MAX_TAPS (32) +#define FFX_CACAO_ADAPTIVE_TAP_BASE_COUNT (5) +#define FFX_CACAO_ADAPTIVE_TAP_FLEXIBLE_COUNT (FFX_CACAO_MAX_TAPS - FFX_CACAO_ADAPTIVE_TAP_BASE_COUNT) -// these values can be changed (up to SSAO_MAX_TAPS) with no changes required elsewhere; values for 4th and 5th preset are ignored but array needed to avoid compilation errors +// these values can be changed (up to FFX_CACAO_MAX_TAPS) with no changes required elsewhere; values for 4th and 5th preset are ignored but array needed to avoid compilation errors // the actual number of texture samples is two times this value (each "tap" has two symmetrical depth texture samples) -static const uint g_numTaps[5] = { 3, 5, 12, 0, 0 }; +static const uint g_FFX_CACAO_numTaps[5] = { 3, 5, 12, 0, 0 }; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -118,59 +49,35 @@ static const uint g_numTaps[5] = { 3, 5, 12, 0, 0 }; // Each has its own cost. To disable just set to 5 or above. // // (experimental) tilts the disk (although only half of the samples!) towards surface normal; this helps with effect uniformity between objects but reduces effect distance and has other side-effects -#define SSAO_TILT_SAMPLES_ENABLE_AT_QUALITY_PRESET (99) // to disable simply set to 99 or similar -#define SSAO_TILT_SAMPLES_AMOUNT (0.4) +#define FFX_CACAO_TILT_SAMPLES_ENABLE_AT_QUALITY_PRESET (99) // to disable simply set to 99 or similar +#define FFX_CACAO_TILT_SAMPLES_AMOUNT (0.4) // -#define SSAO_HALOING_REDUCTION_ENABLE_AT_QUALITY_PRESET (1) // to disable simply set to 99 or similar -#define SSAO_HALOING_REDUCTION_AMOUNT (0.6) // values from 0.0 - 1.0, 1.0 means max weighting (will cause artifacts, 0.8 is more reasonable) +#define FFX_CACAO_HALOING_REDUCTION_ENABLE_AT_QUALITY_PRESET (1) // to disable simply set to 99 or similar +#define FFX_CACAO_HALOING_REDUCTION_AMOUNT (0.6) // values from 0.0 - 1.0, 1.0 means max weighting (will cause artifacts, 0.8 is more reasonable) // -#define SSAO_NORMAL_BASED_EDGES_ENABLE_AT_QUALITY_PRESET (2) //2 // to disable simply set to 99 or similar -#define SSAO_NORMAL_BASED_EDGES_DOT_THRESHOLD (0.5) // use 0-0.1 for super-sharp normal-based edges +#define FFX_CACAO_NORMAL_BASED_EDGES_ENABLE_AT_QUALITY_PRESET (2) //2 // to disable simply set to 99 or similar +#define FFX_CACAO_NORMAL_BASED_EDGES_DOT_THRESHOLD (0.5) // use 0-0.1 for super-sharp normal-based edges // -#define SSAO_DETAIL_AO_ENABLE_AT_QUALITY_PRESET (1) //1 // whether to use DetailAOStrength; to disable simply set to 99 or similar +#define FFX_CACAO_DETAIL_AO_ENABLE_AT_QUALITY_PRESET (1) //1 // whether to use DetailAOStrength; to disable simply set to 99 or similar // -#define SSAO_DEPTH_MIPS_ENABLE_AT_QUALITY_PRESET (2) // !!warning!! the MIP generation on the C++ side will be enabled on quality preset 2 regardless of this value, so if changing here, change the C++ side too -#define SSAO_DEPTH_MIPS_GLOBAL_OFFSET (-4.3) // best noise/quality/performance tradeoff, found empirically +#define FFX_CACAO_DEPTH_MIPS_ENABLE_AT_QUALITY_PRESET (2) // !!warning!! the MIP generation on the C++ side will be enabled on quality preset 2 regardless of this value, so if changing here, change the C++ side too +#define FFX_CACAO_DEPTH_MIPS_GLOBAL_OFFSET (-4.3) // best noise/quality/performance tradeoff, found empirically // -// !!warning!! the edge handling is hard-coded to 'disabled' on quality level 0, and enabled above, on the C++ side; while toggling it here will work for +// !!warning!! the edge handling is hard-coded to 'disabled' on quality level 0, and enabled above, on the C++ side; while toggling it here will work for // testing purposes, it will not yield performance gains (or correct results) -#define SSAO_DEPTH_BASED_EDGES_ENABLE_AT_QUALITY_PRESET (1) +#define FFX_CACAO_DEPTH_BASED_EDGES_ENABLE_AT_QUALITY_PRESET (1) // -#define SSAO_REDUCE_RADIUS_NEAR_SCREEN_BORDER_ENABLE_AT_QUALITY_PRESET (99) // 99 means disabled; only helpful if artifacts at the edges caused by lack of out of screen depth data are not acceptable with the depth sampler in either clamp or mirror modes +#define FFX_CACAO_REDUCE_RADIUS_NEAR_SCREEN_BORDER_ENABLE_AT_QUALITY_PRESET (99) // 99 means disabled; only helpful if artifacts at the edges caused by lack of out of screen depth data are not acceptable with the depth sampler in either clamp or mirror modes ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -SamplerState g_PointClampSampler : register(s0); // corresponds to SSAO_SAMPLERS_SLOT0 -SamplerState g_PointMirrorSampler : register(s1); // corresponds to SSAO_SAMPLERS_SLOT2 -SamplerState g_LinearClampSampler : register(s2); // corresponds to SSAO_SAMPLERS_SLOT1 -SamplerState g_ViewspaceDepthTapSampler : register(s3); // corresponds to SSAO_SAMPLERS_SLOT3 -SamplerState g_ZeroTextureSampler : register(s4); - -cbuffer SSAOConstantsBuffer : register(b0) // corresponds to SSAO_CONSTANTS_BUFFERSLOT -{ - CACAOConstants g_CACAOConsts; -} - - -RWTexture1D<uint> g_ClearLoadCounterInput : register(u0); -[numthreads(1, 1, 1)] -void CSClearLoadCounter() -{ - g_ClearLoadCounterInput[0] = 0; -} - // packing/unpacking for edges; 2 bits per edge mean 4 gradient values (0, 0.33, 0.66, 1) for smoother transitions! -float PackEdges(float4 edgesLRTB) +float FFX_CACAO_PackEdges(float4 edgesLRTB) { - // int4 edgesLRTBi = int4( saturate( edgesLRTB ) * 3.0 + 0.5 ); - // return ( (edgesLRTBi.x << 6) + (edgesLRTBi.y << 4) + (edgesLRTBi.z << 2) + (edgesLRTBi.w << 0) ) / 255.0; - - // optimized, should be same as above edgesLRTB = round(saturate(edgesLRTB) * 3.05); return dot(edgesLRTB, float4(64.0 / 255.0, 16.0 / 255.0, 4.0 / 255.0, 1.0 / 255.0)); } -float4 UnpackEdges(float _packedVal) +float4 FFX_CACAO_UnpackEdges(float _packedVal) { uint packedVal = (uint)(_packedVal * 255.5); float4 edgesLRTB; @@ -179,70 +86,54 @@ float4 UnpackEdges(float _packedVal) edgesLRTB.z = float((packedVal >> 2) & 0x03) / 3.0; edgesLRTB.w = float((packedVal >> 0) & 0x03) / 3.0; - return saturate(edgesLRTB + g_CACAOConsts.InvSharpness); + return saturate(edgesLRTB + g_FFX_CACAO_Consts.InvSharpness); } -float ScreenSpaceToViewSpaceDepth(float screenDepth) +float FFX_CACAO_ScreenSpaceToViewSpaceDepth(float screenDepth) { - float depthLinearizeMul = g_CACAOConsts.DepthUnpackConsts.x; - float depthLinearizeAdd = g_CACAOConsts.DepthUnpackConsts.y; - - // Optimised version of "-cameraClipNear / (cameraClipFar - projDepth * (cameraClipFar - cameraClipNear)) * cameraClipFar" - - // Set your depthLinearizeMul and depthLinearizeAdd to: - // depthLinearizeMul = ( cameraClipFar * cameraClipNear) / ( cameraClipFar - cameraClipNear ); - // depthLinearizeAdd = cameraClipFar / ( cameraClipFar - cameraClipNear ); + float depthLinearizeMul = g_FFX_CACAO_Consts.DepthUnpackConsts.x; + float depthLinearizeAdd = g_FFX_CACAO_Consts.DepthUnpackConsts.y; return depthLinearizeMul / (depthLinearizeAdd - screenDepth); } -float4 ScreenSpaceToViewSpaceDepth(float4 screenDepth) +float4 FFX_CACAO_ScreenSpaceToViewSpaceDepth(float4 screenDepth) { - float depthLinearizeMul = g_CACAOConsts.DepthUnpackConsts.x; - float depthLinearizeAdd = g_CACAOConsts.DepthUnpackConsts.y; - - // Optimised version of "-cameraClipNear / (cameraClipFar - projDepth * (cameraClipFar - cameraClipNear)) * cameraClipFar" - - // Set your depthLinearizeMul and depthLinearizeAdd to: - // depthLinearizeMul = ( cameraClipFar * cameraClipNear) / ( cameraClipFar - cameraClipNear ); - // depthLinearizeAdd = cameraClipFar / ( cameraClipFar - cameraClipNear ); + float depthLinearizeMul = g_FFX_CACAO_Consts.DepthUnpackConsts.x; + float depthLinearizeAdd = g_FFX_CACAO_Consts.DepthUnpackConsts.y; return depthLinearizeMul / (depthLinearizeAdd - screenDepth); } -float4 CalculateEdges(const float centerZ, const float leftZ, const float rightZ, const float topZ, const float bottomZ) +float4 FFX_CACAO_CalculateEdges(const float centerZ, const float leftZ, const float rightZ, const float topZ, const float bottomZ) { // slope-sensitive depth-based edge detection float4 edgesLRTB = float4(leftZ, rightZ, topZ, bottomZ) - centerZ; float4 edgesLRTBSlopeAdjusted = edgesLRTB + edgesLRTB.yxwz; edgesLRTB = min(abs(edgesLRTB), abs(edgesLRTBSlopeAdjusted)); return saturate((1.3 - edgesLRTB / (centerZ * 0.040))); - - // cheaper version but has artifacts - // edgesLRTB = abs( float4( leftZ, rightZ, topZ, bottomZ ) - centerZ; ); - // return saturate( ( 1.3 - edgesLRTB / (pixZ * 0.06 + 0.1) ) ); } -float3 NDCToViewspace(float2 pos, float viewspaceDepth) +float3 FFX_CACAO_NDCToViewSpace(float2 pos, float viewspaceDepth) { float3 ret; - ret.xy = (g_CACAOConsts.NDCToViewMul * pos.xy + g_CACAOConsts.NDCToViewAdd) * viewspaceDepth; + ret.xy = (g_FFX_CACAO_Consts.NDCToViewMul * pos.xy + g_FFX_CACAO_Consts.NDCToViewAdd) * viewspaceDepth; ret.z = viewspaceDepth; return ret; } -float3 DepthBufferUVToViewspace(float2 pos, float viewspaceDepth) +float3 FFX_CACAO_DepthBufferUVToViewSpace(float2 pos, float viewspaceDepth) { float3 ret; - ret.xy = (g_CACAOConsts.DepthBufferUVToViewMul * pos.xy + g_CACAOConsts.DepthBufferUVToViewAdd) * viewspaceDepth; + ret.xy = (g_FFX_CACAO_Consts.DepthBufferUVToViewMul * pos.xy + g_FFX_CACAO_Consts.DepthBufferUVToViewAdd) * viewspaceDepth; ret.z = viewspaceDepth; return ret; } -float3 CalculateNormal(const float4 edgesLRTB, float3 pixCenterPos, float3 pixLPos, float3 pixRPos, float3 pixTPos, float3 pixBPos) +float3 FFX_CACAO_CalculateNormal(const float4 edgesLRTB, float3 pixCenterPos, float3 pixLPos, float3 pixRPos, float3 pixTPos, float3 pixBPos) { // Get this pixel's viewspace normal float4 acceptedNormals = float4(edgesLRTB.x*edgesLRTB.z, edgesLRTB.z*edgesLRTB.y, edgesLRTB.y*edgesLRTB.w, edgesLRTB.w*edgesLRTB.x); @@ -262,85 +153,45 @@ float3 CalculateNormal(const float4 edgesLRTB, float3 pixCenterPos, float3 pixLP return pixelNormal; } +// ============================================================================= +// Clear Load Counter -// ================================================================================ -// Blur stuff - -Texture2DArray<float2> g_BlurInput : register(t0); -RWTexture2DArray<float2> g_BlurOutput : register(u0); - -void AddSample(float ssaoValue, float edgeValue, inout float sum, inout float sumWeight) +[numthreads(1, 1, 1)] +void FFX_CACAO_ClearLoadCounter() { - float weight = edgeValue; - - sum += (weight * ssaoValue); - sumWeight += weight; + FFX_CACAO_ClearLoadCounter_SetLoadCounter(0); } -float2 SampleBlurredWide(float2 inPos, float2 coord) -{ - float3 fullCoord = float3(coord, 0.0f); - float2 vC = g_BlurInput.SampleLevel(g_PointMirrorSampler, fullCoord, 0.0, int2(0, 0)).xy; - float2 vL = g_BlurInput.SampleLevel(g_PointMirrorSampler, fullCoord, 0.0, int2(-2, 0)).xy; - float2 vT = g_BlurInput.SampleLevel(g_PointMirrorSampler, fullCoord, 0.0, int2(0, -2)).xy; - float2 vR = g_BlurInput.SampleLevel(g_PointMirrorSampler, fullCoord, 0.0, int2(2, 0)).xy; - float2 vB = g_BlurInput.SampleLevel(g_PointMirrorSampler, fullCoord, 0.0, int2(0, 2)).xy; - - float packedEdges = vC.y; - float4 edgesLRTB = UnpackEdges(packedEdges); - edgesLRTB.x *= UnpackEdges(vL.y).y; - edgesLRTB.z *= UnpackEdges(vT.y).w; - edgesLRTB.y *= UnpackEdges(vR.y).x; - edgesLRTB.w *= UnpackEdges(vB.y).z; - - float ssaoValue = vC.x; - float ssaoValueL = vL.x; - float ssaoValueT = vT.x; - float ssaoValueR = vR.x; - float ssaoValueB = vB.x; - - float sumWeight = 0.8f; - float sum = ssaoValue * sumWeight; - - AddSample(ssaoValueL, edgesLRTB.x, sum, sumWeight); - AddSample(ssaoValueR, edgesLRTB.y, sum, sumWeight); - AddSample(ssaoValueT, edgesLRTB.z, sum, sumWeight); - AddSample(ssaoValueB, edgesLRTB.w, sum, sumWeight); - - float ssaoAvg = sum / sumWeight; - - ssaoValue = ssaoAvg; //min( ssaoValue, ssaoAvg ) * 0.2 + ssaoAvg * 0.8; - - return float2(ssaoValue, packedEdges); -} +// ============================================================================= +// Edge Sensitive Blur -uint PackFloat16(min16float2 v) +uint FFX_CACAO_PackFloat16(min16float2 v) { uint2 p = f32tof16(float2(v)); return p.x | (p.y << 16); } -min16float2 UnpackFloat16(uint a) +min16float2 FFX_CACAO_UnpackFloat16(uint a) { float2 tmp = f16tof32(uint2(a & 0xFFFF, a >> 16)); return min16float2(tmp); } // all in one, SIMD in yo SIMD dawg, shader -#define TILE_WIDTH 4 -#define TILE_HEIGHT 3 -#define HALF_TILE_WIDTH (TILE_WIDTH / 2) -#define QUARTER_TILE_WIDTH (TILE_WIDTH / 4) +#define FFX_CACAO_TILE_WIDTH 4 +#define FFX_CACAO_TILE_HEIGHT 3 +#define FFX_CACAO_HALF_TILE_WIDTH (FFX_CACAO_TILE_WIDTH / 2) +#define FFX_CACAO_QUARTER_TILE_WIDTH (FFX_CACAO_TILE_WIDTH / 4) -#define ARRAY_WIDTH (HALF_TILE_WIDTH * BLUR_WIDTH + 2) -#define ARRAY_HEIGHT (TILE_HEIGHT * BLUR_HEIGHT + 2) +#define FFX_CACAO_ARRAY_WIDTH (FFX_CACAO_HALF_TILE_WIDTH * FFX_CACAO_BLUR_WIDTH + 2) +#define FFX_CACAO_ARRAY_HEIGHT (FFX_CACAO_TILE_HEIGHT * FFX_CACAO_BLUR_HEIGHT + 2) -#define ITERS 4 +#define FFX_CACAO_ITERS 4 -groupshared uint s_BlurF16Front_4[ARRAY_WIDTH][ARRAY_HEIGHT]; -groupshared uint s_BlurF16Back_4[ARRAY_WIDTH][ARRAY_HEIGHT]; +groupshared uint s_FFX_CACAO_BlurF16Front_4[FFX_CACAO_ARRAY_WIDTH][FFX_CACAO_ARRAY_HEIGHT]; +groupshared uint s_FFX_CACAO_BlurF16Back_4[FFX_CACAO_ARRAY_WIDTH][FFX_CACAO_ARRAY_HEIGHT]; -struct Edges_4 +struct FFX_CACAO_Edges_4 { min16float4 left; min16float4 right; @@ -348,23 +199,23 @@ struct Edges_4 min16float4 bottom; }; -Edges_4 UnpackEdgesFloat16_4(min16float4 _packedVal) +FFX_CACAO_Edges_4 FFX_CACAO_UnpackEdgesFloat16_4(min16float4 _packedVal) { uint4 packedVal = (uint4)(_packedVal * 255.5); - Edges_4 result; - result.left = min16float4(saturate(min16float4((packedVal >> 6) & 0x03) / 3.0 + g_CACAOConsts.InvSharpness)); - result.right = min16float4(saturate(min16float4((packedVal >> 4) & 0x03) / 3.0 + g_CACAOConsts.InvSharpness)); - result.top = min16float4(saturate(min16float4((packedVal >> 2) & 0x03) / 3.0 + g_CACAOConsts.InvSharpness)); - result.bottom = min16float4(saturate(min16float4((packedVal >> 0) & 0x03) / 3.0 + g_CACAOConsts.InvSharpness)); + FFX_CACAO_Edges_4 result; + result.left = min16float4(saturate(min16float4((packedVal >> 6) & 0x03) / 3.0 + g_FFX_CACAO_Consts.InvSharpness)); + result.right = min16float4(saturate(min16float4((packedVal >> 4) & 0x03) / 3.0 + g_FFX_CACAO_Consts.InvSharpness)); + result.top = min16float4(saturate(min16float4((packedVal >> 2) & 0x03) / 3.0 + g_FFX_CACAO_Consts.InvSharpness)); + result.bottom = min16float4(saturate(min16float4((packedVal >> 0) & 0x03) / 3.0 + g_FFX_CACAO_Consts.InvSharpness)); return result; } -min16float4 CalcBlurredSampleF16_4(min16float4 packedEdges, min16float4 centre, min16float4 left, min16float4 right, min16float4 top, min16float4 bottom) +min16float4 FFX_CACAO_CalcBlurredSampleF16_4(min16float4 packedEdges, min16float4 centre, min16float4 left, min16float4 right, min16float4 top, min16float4 bottom) { min16float4 sum = centre * min16float(0.5f); min16float4 weight = min16float4(0.5f, 0.5f, 0.5f, 0.5f); - Edges_4 edges = UnpackEdgesFloat16_4(packedEdges); + FFX_CACAO_Edges_4 edges = FFX_CACAO_UnpackEdgesFloat16_4(packedEdges); sum += left * edges.left; weight += edges.left; @@ -378,36 +229,35 @@ min16float4 CalcBlurredSampleF16_4(min16float4 packedEdges, min16float4 centre, return sum / weight; } -void LDSEdgeSensitiveBlur(const uint blurPasses, const uint2 tid, const uint2 gid) +void FFX_CACAO_LDSEdgeSensitiveBlur(const uint blurPasses, const uint2 tid, const uint2 gid) { - int2 imageCoord = gid * (int2(TILE_WIDTH * BLUR_WIDTH, TILE_HEIGHT * BLUR_HEIGHT) - (2*blurPasses)) + int2(TILE_WIDTH, TILE_HEIGHT) * tid - blurPasses; - int2 bufferCoord = int2(HALF_TILE_WIDTH, TILE_HEIGHT) * tid + 1; + int2 imageCoord = gid * (int2(FFX_CACAO_TILE_WIDTH * FFX_CACAO_BLUR_WIDTH, FFX_CACAO_TILE_HEIGHT * FFX_CACAO_BLUR_HEIGHT) - (2*blurPasses)) + int2(FFX_CACAO_TILE_WIDTH, FFX_CACAO_TILE_HEIGHT) * tid - blurPasses; + int2 bufferCoord = int2(FFX_CACAO_HALF_TILE_WIDTH, FFX_CACAO_TILE_HEIGHT) * tid + 1; - // todo -- replace this with gathers. - min16float4 packedEdges[QUARTER_TILE_WIDTH][TILE_HEIGHT]; + min16float4 packedEdges[FFX_CACAO_QUARTER_TILE_WIDTH][FFX_CACAO_TILE_HEIGHT]; { - float2 input[TILE_WIDTH][TILE_HEIGHT]; + float2 input[FFX_CACAO_TILE_WIDTH][FFX_CACAO_TILE_HEIGHT]; int y; [unroll] - for (y = 0; y < TILE_HEIGHT; ++y) + for (y = 0; y < FFX_CACAO_TILE_HEIGHT; ++y) { [unroll] - for (int x = 0; x < TILE_WIDTH; ++x) + for (int x = 0; x < FFX_CACAO_TILE_WIDTH; ++x) { - input[x][y] = g_BlurInput.SampleLevel(g_PointMirrorSampler, float3((imageCoord + int2(x, y) + 0.5f) * g_CACAOConsts.SSAOBufferInverseDimensions, 0.0f), 0).xy; + float2 sampleUV = (float2(imageCoord + int2(x, y)) + 0.5f) * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; + input[x][y] = FFX_CACAO_EdgeSensitiveBlur_SampleInput(sampleUV); } } [unroll] - for (y = 0; y < TILE_HEIGHT; ++y) + for (y = 0; y < FFX_CACAO_TILE_HEIGHT; ++y) { [unroll] - for (int x = 0; x < QUARTER_TILE_WIDTH; ++x) + for (int x = 0; x < FFX_CACAO_QUARTER_TILE_WIDTH; ++x) { min16float2 ssaoVals = min16float2(input[4 * x + 0][y].x, input[4 * x + 1][y].x); - s_BlurF16Front_4[bufferCoord.x + 2*x + 0][bufferCoord.y + y] = PackFloat16(ssaoVals); + s_FFX_CACAO_BlurF16Front_4[bufferCoord.x + 2*x + 0][bufferCoord.y + y] = FFX_CACAO_PackFloat16(ssaoVals); ssaoVals = min16float2(input[4 * x + 2][y].x, input[4 * x + 3][y].x); - s_BlurF16Front_4[bufferCoord.x + 2*x + 1][bufferCoord.y + y] = PackFloat16(ssaoVals); - // min16float2 ssaoVals = min16float2(1, 1); + s_FFX_CACAO_BlurF16Front_4[bufferCoord.x + 2*x + 1][bufferCoord.y + y] = FFX_CACAO_PackFloat16(ssaoVals); packedEdges[x][y] = min16float4(input[4 * x + 0][y].y, input[4 * x + 1][y].y, input[4 * x + 2][y].y, input[4 * x + 3][y].y); } } @@ -419,24 +269,24 @@ void LDSEdgeSensitiveBlur(const uint blurPasses, const uint2 tid, const uint2 gi for (uint i = 0; i < (blurPasses + 1) / 2; ++i) { [unroll] - for (int y = 0; y < TILE_HEIGHT; ++y) + for (int y = 0; y < FFX_CACAO_TILE_HEIGHT; ++y) { [unroll] - for (int x = 0; x < QUARTER_TILE_WIDTH; ++x) + for (int x = 0; x < FFX_CACAO_QUARTER_TILE_WIDTH; ++x) { int2 c = bufferCoord + int2(2*x, y); - min16float4 centre = min16float4(UnpackFloat16(s_BlurF16Front_4[c.x + 0][c.y + 0]), UnpackFloat16(s_BlurF16Front_4[c.x + 1][c.y + 0])); - min16float4 top = min16float4(UnpackFloat16(s_BlurF16Front_4[c.x + 0][c.y - 1]), UnpackFloat16(s_BlurF16Front_4[c.x + 1][c.y - 1])); - min16float4 bottom = min16float4(UnpackFloat16(s_BlurF16Front_4[c.x + 0][c.y + 1]), UnpackFloat16(s_BlurF16Front_4[c.x + 1][c.y + 1])); + min16float4 centre = min16float4(FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Front_4[c.x + 0][c.y + 0]), FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Front_4[c.x + 1][c.y + 0])); + min16float4 top = min16float4(FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Front_4[c.x + 0][c.y - 1]), FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Front_4[c.x + 1][c.y - 1])); + min16float4 bottom = min16float4(FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Front_4[c.x + 0][c.y + 1]), FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Front_4[c.x + 1][c.y + 1])); - min16float2 tmp = UnpackFloat16(s_BlurF16Front_4[c.x - 1][c.y + 0]); + min16float2 tmp = FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Front_4[c.x - 1][c.y + 0]); min16float4 left = min16float4(tmp.y, centre.xyz); - tmp = UnpackFloat16(s_BlurF16Front_4[c.x + 2][c.y + 0]); + tmp = FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Front_4[c.x + 2][c.y + 0]); min16float4 right = min16float4(centre.yzw, tmp.x); - min16float4 tmp_4 = CalcBlurredSampleF16_4(packedEdges[x][y], centre, left, right, top, bottom); - s_BlurF16Back_4[c.x + 0][c.y] = PackFloat16(tmp_4.xy); - s_BlurF16Back_4[c.x + 1][c.y] = PackFloat16(tmp_4.zw); + min16float4 tmp_4 = FFX_CACAO_CalcBlurredSampleF16_4(packedEdges[x][y], centre, left, right, top, bottom); + s_FFX_CACAO_BlurF16Back_4[c.x + 0][c.y] = FFX_CACAO_PackFloat16(tmp_4.xy); + s_FFX_CACAO_BlurF16Back_4[c.x + 1][c.y] = FFX_CACAO_PackFloat16(tmp_4.zw); } } GroupMemoryBarrierWithGroupSync(); @@ -444,24 +294,24 @@ void LDSEdgeSensitiveBlur(const uint blurPasses, const uint2 tid, const uint2 gi if (2 * i + 1 < blurPasses) { [unroll] - for (int y = 0; y < TILE_HEIGHT; ++y) + for (int y = 0; y < FFX_CACAO_TILE_HEIGHT; ++y) { [unroll] - for (int x = 0; x < QUARTER_TILE_WIDTH; ++x) + for (int x = 0; x < FFX_CACAO_QUARTER_TILE_WIDTH; ++x) { int2 c = bufferCoord + int2(2 * x, y); - min16float4 centre = min16float4(UnpackFloat16(s_BlurF16Back_4[c.x + 0][c.y + 0]), UnpackFloat16(s_BlurF16Back_4[c.x + 1][c.y + 0])); - min16float4 top = min16float4(UnpackFloat16(s_BlurF16Back_4[c.x + 0][c.y - 1]), UnpackFloat16(s_BlurF16Back_4[c.x + 1][c.y - 1])); - min16float4 bottom = min16float4(UnpackFloat16(s_BlurF16Back_4[c.x + 0][c.y + 1]), UnpackFloat16(s_BlurF16Back_4[c.x + 1][c.y + 1])); + min16float4 centre = min16float4(FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Back_4[c.x + 0][c.y + 0]), FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Back_4[c.x + 1][c.y + 0])); + min16float4 top = min16float4(FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Back_4[c.x + 0][c.y - 1]), FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Back_4[c.x + 1][c.y - 1])); + min16float4 bottom = min16float4(FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Back_4[c.x + 0][c.y + 1]), FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Back_4[c.x + 1][c.y + 1])); - min16float2 tmp = UnpackFloat16(s_BlurF16Back_4[c.x - 1][c.y + 0]); + min16float2 tmp = FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Back_4[c.x - 1][c.y + 0]); min16float4 left = min16float4(tmp.y, centre.xyz); - tmp = UnpackFloat16(s_BlurF16Back_4[c.x + 2][c.y + 0]); + tmp = FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Back_4[c.x + 2][c.y + 0]); min16float4 right = min16float4(centre.yzw, tmp.x); - min16float4 tmp_4 = CalcBlurredSampleF16_4(packedEdges[x][y], centre, left, right, top, bottom); - s_BlurF16Front_4[c.x + 0][c.y] = PackFloat16(tmp_4.xy); - s_BlurF16Front_4[c.x + 1][c.y] = PackFloat16(tmp_4.zw); + min16float4 tmp_4 = FFX_CACAO_CalcBlurredSampleF16_4(packedEdges[x][y], centre, left, right, top, bottom); + s_FFX_CACAO_BlurF16Front_4[c.x + 0][c.y] = FFX_CACAO_PackFloat16(tmp_4.xy); + s_FFX_CACAO_BlurF16Front_4[c.x + 1][c.y] = FFX_CACAO_PackFloat16(tmp_4.zw); } } GroupMemoryBarrierWithGroupSync(); @@ -469,131 +319,121 @@ void LDSEdgeSensitiveBlur(const uint blurPasses, const uint2 tid, const uint2 gi } [unroll] - for (uint y = 0; y < TILE_HEIGHT; ++y) + for (uint y = 0; y < FFX_CACAO_TILE_HEIGHT; ++y) { - uint outputY = TILE_HEIGHT * tid.y + y; - if (blurPasses <= outputY && outputY < TILE_HEIGHT * BLUR_HEIGHT - blurPasses) + uint outputY = FFX_CACAO_TILE_HEIGHT * tid.y + y; + if (blurPasses <= outputY && outputY < FFX_CACAO_TILE_HEIGHT * FFX_CACAO_BLUR_HEIGHT - blurPasses) { [unroll] - for (int x = 0; x < QUARTER_TILE_WIDTH; ++x) + for (int x = 0; x < FFX_CACAO_QUARTER_TILE_WIDTH; ++x) { - uint outputX = TILE_WIDTH * tid.x + 4 * x; + uint outputX = FFX_CACAO_TILE_WIDTH * tid.x + 4 * x; min16float4 ssaoVal; if (blurPasses % 2 == 0) { - ssaoVal = min16float4(UnpackFloat16(s_BlurF16Front_4[bufferCoord.x + x][bufferCoord.y + y]), UnpackFloat16(s_BlurF16Front_4[bufferCoord.x + x + 1][bufferCoord.y + y])); + ssaoVal = min16float4(FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Front_4[bufferCoord.x + x][bufferCoord.y + y]), FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Front_4[bufferCoord.x + x + 1][bufferCoord.y + y])); } else { - ssaoVal = min16float4(UnpackFloat16(s_BlurF16Back_4[bufferCoord.x + x][bufferCoord.y + y]), UnpackFloat16(s_BlurF16Back_4[bufferCoord.x + x + 1][bufferCoord.y + y])); + ssaoVal = min16float4(FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Back_4[bufferCoord.x + x][bufferCoord.y + y]), FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BlurF16Back_4[bufferCoord.x + x + 1][bufferCoord.y + y])); } - if (blurPasses <= outputX && outputX < TILE_WIDTH * BLUR_WIDTH - blurPasses) + if (blurPasses <= outputX && outputX < FFX_CACAO_TILE_WIDTH * FFX_CACAO_BLUR_WIDTH - blurPasses) { - g_BlurOutput[int3(imageCoord + int2(4 * x, y), 0)] = float2(ssaoVal.x, packedEdges[x][y].x); + FFX_CACAO_EdgeSensitiveBlur_StoreOutput(imageCoord + int2(4 * x, y), float2(ssaoVal.x, packedEdges[x][y].x)); } outputX += 1; - if (blurPasses <= outputX && outputX < TILE_WIDTH * BLUR_WIDTH - blurPasses) + if (blurPasses <= outputX && outputX < FFX_CACAO_TILE_WIDTH * FFX_CACAO_BLUR_WIDTH - blurPasses) { - g_BlurOutput[int3(imageCoord + int2(4 * x + 1, y), 0)] = float2(ssaoVal.y, packedEdges[x][y].y); + FFX_CACAO_EdgeSensitiveBlur_StoreOutput(imageCoord + int2(4 * x + 1, y), float2(ssaoVal.y, packedEdges[x][y].y)); } outputX += 1; - if (blurPasses <= outputX && outputX < TILE_WIDTH * BLUR_WIDTH - blurPasses) + if (blurPasses <= outputX && outputX < FFX_CACAO_TILE_WIDTH * FFX_CACAO_BLUR_WIDTH - blurPasses) { - g_BlurOutput[int3(imageCoord + int2(4 * x + 2, y), 0)] = float2(ssaoVal.z, packedEdges[x][y].z); + FFX_CACAO_EdgeSensitiveBlur_StoreOutput(imageCoord + int2(4 * x + 2, y), float2(ssaoVal.z, packedEdges[x][y].z)); } outputX += 1; - if (blurPasses <= outputX && outputX < TILE_WIDTH * BLUR_WIDTH - blurPasses) + if (blurPasses <= outputX && outputX < FFX_CACAO_TILE_WIDTH * FFX_CACAO_BLUR_WIDTH - blurPasses) { - g_BlurOutput[int3(imageCoord + int2(4 * x + 3, y), 0)] = float2(ssaoVal.w, packedEdges[x][y].w); + FFX_CACAO_EdgeSensitiveBlur_StoreOutput(imageCoord + int2(4 * x + 3, y), float2(ssaoVal.w, packedEdges[x][y].w)); } } } } } -[numthreads(BLUR_WIDTH, BLUR_HEIGHT, 1)] -void CSEdgeSensitiveBlur1(uint2 tid : SV_GroupThreadID, uint2 gid : SV_GroupID) +[numthreads(FFX_CACAO_BLUR_WIDTH, FFX_CACAO_BLUR_HEIGHT, 1)] +void FFX_CACAO_EdgeSensitiveBlur1(uint2 tid : SV_GroupThreadID, uint2 gid : SV_GroupID) { - LDSEdgeSensitiveBlur(1, tid, gid); + FFX_CACAO_LDSEdgeSensitiveBlur(1, tid, gid); } -[numthreads(BLUR_WIDTH, BLUR_HEIGHT, 1)] -void CSEdgeSensitiveBlur2(uint2 tid : SV_GroupThreadID, uint2 gid : SV_GroupID) +[numthreads(FFX_CACAO_BLUR_WIDTH, FFX_CACAO_BLUR_HEIGHT, 1)] +void FFX_CACAO_EdgeSensitiveBlur2(uint2 tid : SV_GroupThreadID, uint2 gid : SV_GroupID) { - LDSEdgeSensitiveBlur(2, tid, gid); + FFX_CACAO_LDSEdgeSensitiveBlur(2, tid, gid); } -[numthreads(BLUR_WIDTH, BLUR_HEIGHT, 1)] -void CSEdgeSensitiveBlur3(uint2 tid : SV_GroupThreadID, uint2 gid : SV_GroupID) +[numthreads(FFX_CACAO_BLUR_WIDTH, FFX_CACAO_BLUR_HEIGHT, 1)] +void FFX_CACAO_EdgeSensitiveBlur3(uint2 tid : SV_GroupThreadID, uint2 gid : SV_GroupID) { - LDSEdgeSensitiveBlur(3, tid, gid); + FFX_CACAO_LDSEdgeSensitiveBlur(3, tid, gid); } -[numthreads(BLUR_WIDTH, BLUR_HEIGHT, 1)] -void CSEdgeSensitiveBlur4(uint2 tid : SV_GroupThreadID, uint2 gid : SV_GroupID) +[numthreads(FFX_CACAO_BLUR_WIDTH, FFX_CACAO_BLUR_HEIGHT, 1)] +void FFX_CACAO_EdgeSensitiveBlur4(uint2 tid : SV_GroupThreadID, uint2 gid : SV_GroupID) { - LDSEdgeSensitiveBlur(4, tid, gid); + FFX_CACAO_LDSEdgeSensitiveBlur(4, tid, gid); } -[numthreads(BLUR_WIDTH, BLUR_HEIGHT, 1)] -void CSEdgeSensitiveBlur5(uint2 tid : SV_GroupThreadID, uint2 gid : SV_GroupID) +[numthreads(FFX_CACAO_BLUR_WIDTH, FFX_CACAO_BLUR_HEIGHT, 1)] +void FFX_CACAO_EdgeSensitiveBlur5(uint2 tid : SV_GroupThreadID, uint2 gid : SV_GroupID) { - LDSEdgeSensitiveBlur(5, tid, gid); + FFX_CACAO_LDSEdgeSensitiveBlur(5, tid, gid); } -[numthreads(BLUR_WIDTH, BLUR_HEIGHT, 1)] -void CSEdgeSensitiveBlur6(uint2 tid : SV_GroupThreadID, uint2 gid : SV_GroupID) +[numthreads(FFX_CACAO_BLUR_WIDTH, FFX_CACAO_BLUR_HEIGHT, 1)] +void FFX_CACAO_EdgeSensitiveBlur6(uint2 tid : SV_GroupThreadID, uint2 gid : SV_GroupID) { - LDSEdgeSensitiveBlur(6, tid, gid); + FFX_CACAO_LDSEdgeSensitiveBlur(6, tid, gid); } -[numthreads(BLUR_WIDTH, BLUR_HEIGHT, 1)] -void CSEdgeSensitiveBlur7(uint2 tid : SV_GroupThreadID, uint2 gid : SV_GroupID) +[numthreads(FFX_CACAO_BLUR_WIDTH, FFX_CACAO_BLUR_HEIGHT, 1)] +void FFX_CACAO_EdgeSensitiveBlur7(uint2 tid : SV_GroupThreadID, uint2 gid : SV_GroupID) { - LDSEdgeSensitiveBlur(7, tid, gid); + FFX_CACAO_LDSEdgeSensitiveBlur(7, tid, gid); } -[numthreads(BLUR_WIDTH, BLUR_HEIGHT, 1)] -void CSEdgeSensitiveBlur8(uint2 tid : SV_GroupThreadID, uint2 gid : SV_GroupID) +[numthreads(FFX_CACAO_BLUR_WIDTH, FFX_CACAO_BLUR_HEIGHT, 1)] +void FFX_CACAO_EdgeSensitiveBlur8(uint2 tid : SV_GroupThreadID, uint2 gid : SV_GroupID) { - LDSEdgeSensitiveBlur(8, tid, gid); + FFX_CACAO_LDSEdgeSensitiveBlur(8, tid, gid); } -#undef TILE_WIDTH -#undef TILE_HEIGHT -#undef ARRAY_WIDTH -#undef ARRAY_HEIGHT -#undef ITERS - - +#undef FFX_CACAO_TILE_WIDTH +#undef FFX_CACAO_TILE_HEIGHT +#undef FFX_CACAO_HALF_TILE_WIDTH +#undef FFX_CACAO_QUARTER_TILE_WIDTH +#undef FFX_CACAO_ARRAY_WIDTH +#undef FFX_CACAO_ARRAY_HEIGHT +#undef FFX_CACAO_ITERS // ======================================================================================================= -// SSAO stuff - -Texture2DArray<float> g_ViewspaceDepthSource : register(t0); -Texture1D<uint> g_LoadCounter : register(t2); -Texture2D<float> g_ImportanceMap : register(t3); -Texture2DArray g_FinalSSAO : register(t4); -Texture1D<float> g_ZeroTexture : register(t5); -Texture2DArray g_deinterlacedNormals : register(t6); - -RWTexture2DArray<float2> g_SSAOOutput : register(u0); - +// SSAO Generation // calculate effect radius and fit our screen sampling pattern inside it -void CalculateRadiusParameters(const float pixCenterLength, const float2 pixelDirRBViewspaceSizeAtCenterZ, out float pixLookupRadiusMod, out float effectRadius, out float falloffCalcMulSq) +void FFX_CACAO_CalculateRadiusParameters(const float pixCenterLength, const float2 pixelDirRBViewspaceSizeAtCenterZ, out float pixLookupRadiusMod, out float effectRadius, out float falloffCalcMulSq) { - effectRadius = g_CACAOConsts.EffectRadius; + effectRadius = g_FFX_CACAO_Consts.EffectRadius; // leaving this out for performance reasons: use something similar if radius needs to scale based on distance - //effectRadius *= pow( pixCenterLength, g_CACAOConsts.RadiusDistanceScalingFunctionPow); + //effectRadius *= pow( pixCenterLength, g_FFX_CACAO_Consts.RadiusDistanceScalingFunctionPow); // when too close, on-screen sampling disk will grow beyond screen size; limit this to avoid closeup temporal artifacts - const float tooCloseLimitMod = saturate(pixCenterLength * g_CACAOConsts.EffectSamplingRadiusNearLimitRec) * 0.8 + 0.2; + const float tooCloseLimitMod = saturate(pixCenterLength * g_FFX_CACAO_Consts.EffectSamplingRadiusNearLimitRec) * 0.8 + 0.2; effectRadius *= tooCloseLimitMod; @@ -604,65 +444,49 @@ void CalculateRadiusParameters(const float pixCenterLength, const float2 pixelDi falloffCalcMulSq = -1.0f / (effectRadius*effectRadius); } - -float3 DecodeNormal(float3 encodedNormal) -{ - float3 normal = encodedNormal * g_CACAOConsts.NormalsUnpackMul.xxx + g_CACAOConsts.NormalsUnpackAdd.xxx; - -#if SSAO_ENABLE_NORMAL_WORLD_TO_VIEW_CONVERSION - normal = mul(normal, (float3x3)g_CACAOConsts.NormalsWorldToViewspaceMatrix).xyz; -#endif - - // normal = normalize( normal ); // normalize adds around 2.5% cost on High settings but makes little (PSNR 66.7) visual difference when normals are as in the sample (stored in R8G8B8A8_UNORM, - // // decoded in the shader), however it will likely be required if using different encoding/decoding or the inputs are not normalized, etc. - - return normal; -} - // all vectors in viewspace -float CalculatePixelObscurance(float3 pixelNormal, float3 hitDelta, float falloffCalcMulSq) +float FFX_CACAO_CalculatePixelObscurance(float3 pixelNormal, float3 hitDelta, float falloffCalcMulSq) { float lengthSq = dot(hitDelta, hitDelta); float NdotD = dot(pixelNormal, hitDelta) / sqrt(lengthSq); float falloffMult = max(0.0, lengthSq * falloffCalcMulSq + 1.0); - return max(0, NdotD - g_CACAOConsts.EffectHorizonAngleThreshold) * falloffMult; + return max(0, NdotD - g_FFX_CACAO_Consts.EffectHorizonAngleThreshold) * falloffMult; } -void SSAOTapInner(const int qualityLevel, inout float obscuranceSum, inout float weightSum, const float2 samplingUV, const float mipLevel, const float3 pixCenterPos, const float3 negViewspaceDir, float3 pixelNormal, const float falloffCalcMulSq, const float weightMod, const int dbgTapIndex) +void FFX_CACAO_SSAOTapInner(const int qualityLevel, inout float obscuranceSum, inout float weightSum, const float2 samplingUV, const float mipLevel, const float3 pixCenterPos, const float3 negViewspaceDir, float3 pixelNormal, const float falloffCalcMulSq, const float weightMod, const int dbgTapIndex) { // get depth at sample - float viewspaceSampleZ = g_ViewspaceDepthSource.SampleLevel(g_ViewspaceDepthTapSampler, float3(samplingUV.xy, 0.0f), mipLevel).x; // * g_CACAOConsts.MaxViewspaceDepth; + float viewspaceSampleZ = FFX_CACAO_SSAOGeneration_SampleViewspaceDepthMip(samplingUV, mipLevel); // convert to viewspace - // float3 hitPos = NDCToViewspace(samplingUV.xy, viewspaceSampleZ).xyz; - float3 hitPos = DepthBufferUVToViewspace(samplingUV.xy, viewspaceSampleZ).xyz; + float3 hitPos = FFX_CACAO_DepthBufferUVToViewSpace(samplingUV.xy, viewspaceSampleZ).xyz; float3 hitDelta = hitPos - pixCenterPos; - float obscurance = CalculatePixelObscurance(pixelNormal, hitDelta, falloffCalcMulSq); + float obscurance = FFX_CACAO_CalculatePixelObscurance(pixelNormal, hitDelta, falloffCalcMulSq); float weight = 1.0; - if (qualityLevel >= SSAO_HALOING_REDUCTION_ENABLE_AT_QUALITY_PRESET) + if (qualityLevel >= FFX_CACAO_HALOING_REDUCTION_ENABLE_AT_QUALITY_PRESET) { //float reduct = max( 0, dot( hitDelta, negViewspaceDir ) ); float reduct = max(0, -hitDelta.z); // cheaper, less correct version - reduct = saturate(reduct * g_CACAOConsts.NegRecEffectRadius + 2.0); // saturate( 2.0 - reduct / g_CACAOConsts.EffectRadius ); - weight = SSAO_HALOING_REDUCTION_AMOUNT * reduct + (1.0 - SSAO_HALOING_REDUCTION_AMOUNT); + reduct = saturate(reduct * g_FFX_CACAO_Consts.NegRecEffectRadius + 2.0); // saturate( 2.0 - reduct / g_FFX_CACAO_Consts.EffectRadius ); + weight = FFX_CACAO_HALOING_REDUCTION_AMOUNT * reduct + (1.0 - FFX_CACAO_HALOING_REDUCTION_AMOUNT); } weight *= weightMod; obscuranceSum += obscurance * weight; weightSum += weight; } -void SSAOTap(const int qualityLevel, inout float obscuranceSum, inout float weightSum, const int tapIndex, const float2x2 rotScale, const float3 pixCenterPos, const float3 negViewspaceDir, float3 pixelNormal, const float2 normalizedScreenPos, const float2 depthBufferUV, const float mipOffset, const float falloffCalcMulSq, float weightMod, float2 normXY, float normXYLength) +void FFX_CACAO_SSAOTap(const int qualityLevel, inout float obscuranceSum, inout float weightSum, const int tapIndex, const float2x2 rotScale, const float3 pixCenterPos, const float3 negViewspaceDir, float3 pixelNormal, const float2 normalizedScreenPos, const float2 depthBufferUV, const float mipOffset, const float falloffCalcMulSq, float weightMod, float2 normXY, float normXYLength) { float2 sampleOffset; float samplePow2Len; // patterns { - float4 newSample = g_samplePatternMain[tapIndex]; + float4 newSample = g_FFX_CACAO_samplePatternMain[tapIndex]; sampleOffset = mul(rotScale, newSample.xy); samplePow2Len = newSample.w; // precalculated, same as: samplePow2Len = log2( length( newSample.xy ) ); weightMod *= newSample.z; @@ -671,20 +495,20 @@ void SSAOTap(const int qualityLevel, inout float obscuranceSum, inout float weig // snap to pixel center (more correct obscurance math, avoids artifacts) sampleOffset = round(sampleOffset); - // calculate MIP based on the sample distance from the centre, similar to as described + // calculate MIP based on the sample distance from the centre, similar to as described // in http://graphics.cs.williams.edu/papers/SAOHPG12/. - float mipLevel = (qualityLevel < SSAO_DEPTH_MIPS_ENABLE_AT_QUALITY_PRESET) ? (0) : (samplePow2Len + mipOffset); + float mipLevel = (qualityLevel < FFX_CACAO_DEPTH_MIPS_ENABLE_AT_QUALITY_PRESET) ? (0) : (samplePow2Len + mipOffset); - float2 samplingUV = sampleOffset * g_CACAOConsts.DeinterleavedDepthBufferInverseDimensions + depthBufferUV; + float2 samplingUV = sampleOffset * g_FFX_CACAO_Consts.DeinterleavedDepthBufferInverseDimensions + depthBufferUV; - SSAOTapInner(qualityLevel, obscuranceSum, weightSum, samplingUV, mipLevel, pixCenterPos, negViewspaceDir, pixelNormal, falloffCalcMulSq, weightMod, tapIndex * 2); + FFX_CACAO_SSAOTapInner(qualityLevel, obscuranceSum, weightSum, samplingUV, mipLevel, pixCenterPos, negViewspaceDir, pixelNormal, falloffCalcMulSq, weightMod, tapIndex * 2); // for the second tap, just use the mirrored offset float2 sampleOffsetMirroredUV = -sampleOffset; // tilt the second set of samples so that the disk is effectively rotated by the normal // effective at removing one set of artifacts, but too expensive for lower quality settings - if (qualityLevel >= SSAO_TILT_SAMPLES_ENABLE_AT_QUALITY_PRESET) + if (qualityLevel >= FFX_CACAO_TILT_SAMPLES_ENABLE_AT_QUALITY_PRESET) { float dotNorm = dot(sampleOffsetMirroredUV, normXY); sampleOffsetMirroredUV -= dotNorm * normXYLength * normXY; @@ -692,62 +516,33 @@ void SSAOTap(const int qualityLevel, inout float obscuranceSum, inout float weig } // snap to pixel center (more correct obscurance math, avoids artifacts) - float2 samplingMirroredUV = sampleOffsetMirroredUV * g_CACAOConsts.DeinterleavedDepthBufferInverseDimensions + depthBufferUV; + float2 samplingMirroredUV = sampleOffsetMirroredUV * g_FFX_CACAO_Consts.DeinterleavedDepthBufferInverseDimensions + depthBufferUV; - SSAOTapInner(qualityLevel, obscuranceSum, weightSum, samplingMirroredUV, mipLevel, pixCenterPos, negViewspaceDir, pixelNormal, falloffCalcMulSq, weightMod, tapIndex * 2 + 1); + FFX_CACAO_SSAOTapInner(qualityLevel, obscuranceSum, weightSum, samplingMirroredUV, mipLevel, pixCenterPos, negViewspaceDir, pixelNormal, falloffCalcMulSq, weightMod, tapIndex * 2 + 1); } -struct SSAOHits +struct FFX_CACAO_SSAOHits { float3 hits[2]; float weightMod; }; -SSAOHits SSAOGetHits(const int qualityLevel, const float2 depthBufferUV, const int tapIndex, const float mipOffset, const float2x2 rotScale, const float4 newSample) -{ - SSAOHits result; - - float2 sampleOffset; - float samplePow2Len; - - // patterns - { - // float4 newSample = g_samplePatternMain[tapIndex]; - sampleOffset = mul(rotScale, newSample.xy); - samplePow2Len = newSample.w; // precalculated, same as: samplePow2Len = log2( length( newSample.xy ) ); - result.weightMod = newSample.z; - } - - // snap to pixel center (more correct obscurance math, avoids artifacts) - sampleOffset = round(sampleOffset) * g_CACAOConsts.DeinterleavedDepthBufferInverseDimensions; - - float mipLevel = (qualityLevel < SSAO_DEPTH_MIPS_ENABLE_AT_QUALITY_PRESET) ? (0) : (samplePow2Len + mipOffset); - - float2 sampleUV = depthBufferUV + sampleOffset; - result.hits[0] = float3(sampleUV, g_ViewspaceDepthSource.SampleLevel(g_ViewspaceDepthTapSampler, float3(sampleUV, 0.0f), mipLevel).x); - - sampleUV = depthBufferUV - sampleOffset; - result.hits[1] = float3(sampleUV, g_ViewspaceDepthSource.SampleLevel(g_ViewspaceDepthTapSampler, float3(sampleUV, 0.0f), mipLevel).x); - - return result; -} - -struct SSAOSampleData +struct FFX_CACAO_SSAOSampleData { float2 uvOffset; float mipLevel; float weightMod; }; -SSAOSampleData SSAOGetSampleData(const int qualityLevel, const float2x2 rotScale, const float4 newSample, const float mipOffset) +FFX_CACAO_SSAOSampleData FFX_CACAO_SSAOGetSampleData(const int qualityLevel, const float2x2 rotScale, const float4 newSample, const float mipOffset) { float2 sampleOffset = mul(rotScale, newSample.xy); - sampleOffset = round(sampleOffset) * g_CACAOConsts.DeinterleavedDepthBufferInverseDimensions; + sampleOffset = round(sampleOffset) * g_FFX_CACAO_Consts.DeinterleavedDepthBufferInverseDimensions; float samplePow2Len = newSample.w; - float mipLevel = (qualityLevel < SSAO_DEPTH_MIPS_ENABLE_AT_QUALITY_PRESET) ? (0) : (samplePow2Len + mipOffset); + float mipLevel = (qualityLevel < FFX_CACAO_DEPTH_MIPS_ENABLE_AT_QUALITY_PRESET) ? (0) : (samplePow2Len + mipOffset); - SSAOSampleData result; + FFX_CACAO_SSAOSampleData result; result.uvOffset = sampleOffset; result.mipLevel = mipLevel; @@ -756,64 +551,55 @@ SSAOSampleData SSAOGetSampleData(const int qualityLevel, const float2x2 rotScale return result; } -SSAOHits SSAOGetHits2(SSAOSampleData data, const float2 depthBufferUV) +FFX_CACAO_SSAOHits FFX_CACAO_SSAOGetHits2(FFX_CACAO_SSAOSampleData data, const float2 depthBufferUV) { - SSAOHits result; + FFX_CACAO_SSAOHits result; result.weightMod = data.weightMod; float2 sampleUV = depthBufferUV + data.uvOffset; - result.hits[0] = float3(sampleUV, g_ViewspaceDepthSource.SampleLevel(g_ViewspaceDepthTapSampler, float3(sampleUV, 0.0f), data.mipLevel).x); + result.hits[0] = float3(sampleUV, FFX_CACAO_SSAOGeneration_SampleViewspaceDepthMip(sampleUV, data.mipLevel)); sampleUV = depthBufferUV - data.uvOffset; - result.hits[1] = float3(sampleUV, g_ViewspaceDepthSource.SampleLevel(g_ViewspaceDepthTapSampler, float3(sampleUV, 0.0f), data.mipLevel).x); + result.hits[1] = float3(sampleUV, FFX_CACAO_SSAOGeneration_SampleViewspaceDepthMip(sampleUV, data.mipLevel)); return result; } -void SSAOAddHits(const int qualityLevel, const float3 pixCenterPos, const float3 pixelNormal, const float falloffCalcMulSq, inout float weightSum, inout float obscuranceSum, SSAOHits hits) +void FFX_CACAO_SSAOAddHits(const int qualityLevel, const float3 pixCenterPos, const float3 pixelNormal, const float falloffCalcMulSq, inout float weightSum, inout float obscuranceSum, FFX_CACAO_SSAOHits hits) { float weight = hits.weightMod; [unroll] for (int hitIndex = 0; hitIndex < 2; ++hitIndex) { float3 hit = hits.hits[hitIndex]; - float3 hitPos = DepthBufferUVToViewspace(hit.xy, hit.z); + float3 hitPos = FFX_CACAO_DepthBufferUVToViewSpace(hit.xy, hit.z); float3 hitDelta = hitPos - pixCenterPos; - float obscurance = CalculatePixelObscurance(pixelNormal, hitDelta, falloffCalcMulSq); + float obscurance = FFX_CACAO_CalculatePixelObscurance(pixelNormal, hitDelta, falloffCalcMulSq); - if (qualityLevel >= SSAO_HALOING_REDUCTION_ENABLE_AT_QUALITY_PRESET) + if (qualityLevel >= FFX_CACAO_HALOING_REDUCTION_ENABLE_AT_QUALITY_PRESET) { //float reduct = max( 0, dot( hitDelta, negViewspaceDir ) ); float reduct = max(0, -hitDelta.z); // cheaper, less correct version - reduct = saturate(reduct * g_CACAOConsts.NegRecEffectRadius + 2.0); // saturate( 2.0 - reduct / g_CACAOConsts.EffectRadius ); - weight = SSAO_HALOING_REDUCTION_AMOUNT * reduct + (1.0 - SSAO_HALOING_REDUCTION_AMOUNT); + reduct = saturate(reduct * g_FFX_CACAO_Consts.NegRecEffectRadius + 2.0); // saturate( 2.0 - reduct / g_FFX_CACAO_Consts.EffectRadius ); + weight = FFX_CACAO_HALOING_REDUCTION_AMOUNT * reduct + (1.0 - FFX_CACAO_HALOING_REDUCTION_AMOUNT); } obscuranceSum += obscurance * weight; weightSum += weight; } } -void SSAOTap2(const int qualityLevel, inout float obscuranceSum, inout float weightSum, const int tapIndex, const float2x2 rotScale, const float3 pixCenterPos, const float3 negViewspaceDir, float3 pixelNormal, const float2 normalizedScreenPos, const float mipOffset, const float falloffCalcMulSq, float weightMod, float2 normXY, float normXYLength) -{ - float4 newSample = g_samplePatternMain[tapIndex]; - SSAOSampleData data = SSAOGetSampleData(qualityLevel, rotScale, newSample, mipOffset); - SSAOHits hits = SSAOGetHits2(data, normalizedScreenPos); - SSAOAddHits(qualityLevel, pixCenterPos, pixelNormal, falloffCalcMulSq, weightSum, obscuranceSum, hits); -} - - -void GenerateSSAOShadowsInternal(out float outShadowTerm, out float4 outEdges, out float outWeight, const float2 SVPos/*, const float2 normalizedScreenPos*/, uniform int qualityLevel, bool adaptiveBase) +void FFX_CACAO_GenerateSSAOShadowsInternal(out float outShadowTerm, out float4 outEdges, out float outWeight, const float2 SVPos/*, const float2 normalizedScreenPos*/, uniform int qualityLevel, bool adaptiveBase) { float2 SVPosRounded = trunc(SVPos); uint2 SVPosui = uint2(SVPosRounded); //same as uint2( SVPos ) - const int numberOfTaps = (adaptiveBase) ? (SSAO_ADAPTIVE_TAP_BASE_COUNT) : (g_numTaps[qualityLevel]); + const int numberOfTaps = (adaptiveBase) ? (FFX_CACAO_ADAPTIVE_TAP_BASE_COUNT) : (g_FFX_CACAO_numTaps[qualityLevel]); float pixZ, pixLZ, pixTZ, pixRZ, pixBZ; - float2 depthBufferUV = (SVPos + 0.5f) * g_CACAOConsts.DeinterleavedDepthBufferInverseDimensions + g_CACAOConsts.DeinterleavedDepthBufferNormalisedOffset; - float4 valuesUL = g_ViewspaceDepthSource.GatherRed(g_PointMirrorSampler, float3(depthBufferUV, 0.0f), int2(-1, -1)); - float4 valuesBR = g_ViewspaceDepthSource.GatherRed(g_PointMirrorSampler, float3(depthBufferUV, 0.0f)); + float2 depthBufferUV = (SVPos + 0.5f) * g_FFX_CACAO_Consts.DeinterleavedDepthBufferInverseDimensions + g_FFX_CACAO_Consts.DeinterleavedDepthBufferNormalisedOffset; + float4 valuesUL = FFX_CACAO_SSAOGeneration_GatherViewspaceDepthOffset(depthBufferUV, int2(-1, -1)); + float4 valuesBR = FFX_CACAO_SSAOGeneration_GatherViewspaceDepthOffset(depthBufferUV, int2(0, 0)); // get this pixel's viewspace depth - pixZ = valuesUL.y; //float pixZ = g_ViewspaceDepthSource.SampleLevel( g_PointMirrorSampler, float3(normalizedScreenPos, 0.0f), 0.0 ).x; // * g_CACAOConsts.MaxViewspaceDepth; + pixZ = valuesUL.y; // get left right top bottom neighbouring pixels for edge detection (gets compiled out on qualityLevel == 0) pixLZ = valuesUL.x; @@ -821,31 +607,30 @@ void GenerateSSAOShadowsInternal(out float outShadowTerm, out float4 outEdges, o pixRZ = valuesBR.z; pixBZ = valuesBR.x; - // float2 normalizedScreenPos = SVPosRounded * g_CACAOConsts.Viewport2xPixelSize + g_CACAOConsts.Viewport2xPixelSize_x_025; - float2 normalizedScreenPos = (SVPosRounded + 0.5f) * g_CACAOConsts.SSAOBufferInverseDimensions; - float3 pixCenterPos = NDCToViewspace(normalizedScreenPos, pixZ); // g + // float2 normalizedScreenPos = SVPosRounded * g_FFX_CACAO_Consts.Viewport2xPixelSize + g_FFX_CACAO_Consts.Viewport2xPixelSize_x_025; + float2 normalizedScreenPos = (SVPosRounded + 0.5f) * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; + float3 pixCenterPos = FFX_CACAO_NDCToViewSpace(normalizedScreenPos, pixZ); // g // Load this pixel's viewspace normal - // uint2 fullResCoord = 2 * (SVPosui * 2 + g_CACAOConsts.PerPassFullResCoordOffset.xy); - int3 normalCoord = int3(SVPosui, g_CACAOConsts.PassIndex); - float3 pixelNormal = g_deinterlacedNormals[normalCoord].xyz; + // uint2 fullResCoord = 2 * (SVPosui * 2 + g_FFX_CACAO_Consts.PerPassFullResCoordOffset.xy); + float3 pixelNormal = FFX_CACAO_SSAOGeneration_GetNormalPass(SVPosui, g_FFX_CACAO_Consts.PassIndex); - // optimized approximation of: float2 pixelDirRBViewspaceSizeAtCenterZ = NDCToViewspace( normalizedScreenPos.xy + g_CACAOConsts._ViewportPixelSize.xy, pixCenterPos.z ).xy - pixCenterPos.xy; - // const float2 pixelDirRBViewspaceSizeAtCenterZ = pixCenterPos.z * g_CACAOConsts.NDCToViewMul * g_CACAOConsts.Viewport2xPixelSize; - const float2 pixelDirRBViewspaceSizeAtCenterZ = pixCenterPos.z * g_CACAOConsts.NDCToViewMul * g_CACAOConsts.SSAOBufferInverseDimensions; + // optimized approximation of: float2 pixelDirRBViewspaceSizeAtCenterZ = FFX_CACAO_NDCToViewSpace( normalizedScreenPos.xy + g_FFX_CACAO_Consts._ViewportPixelSize.xy, pixCenterPos.z ).xy - pixCenterPos.xy; + // const float2 pixelDirRBViewspaceSizeAtCenterZ = pixCenterPos.z * g_FFX_CACAO_Consts.NDCToViewMul * g_FFX_CACAO_Consts.Viewport2xPixelSize; + const float2 pixelDirRBViewspaceSizeAtCenterZ = pixCenterPos.z * g_FFX_CACAO_Consts.NDCToViewMul * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; float pixLookupRadiusMod; float falloffCalcMulSq; // calculate effect radius and fit our screen sampling pattern inside it float effectViewspaceRadius; - CalculateRadiusParameters(length(pixCenterPos), pixelDirRBViewspaceSizeAtCenterZ, pixLookupRadiusMod, effectViewspaceRadius, falloffCalcMulSq); + FFX_CACAO_CalculateRadiusParameters(length(pixCenterPos), pixelDirRBViewspaceSizeAtCenterZ, pixLookupRadiusMod, effectViewspaceRadius, falloffCalcMulSq); // calculate samples rotation/scaling float2x2 rotScale; { // reduce effect radius near the screen edges slightly; ideally, one would render a larger depth buffer (5% on each side) instead - if (!adaptiveBase && (qualityLevel >= SSAO_REDUCE_RADIUS_NEAR_SCREEN_BORDER_ENABLE_AT_QUALITY_PRESET)) + if (!adaptiveBase && (qualityLevel >= FFX_CACAO_REDUCE_RADIUS_NEAR_SCREEN_BORDER_ENABLE_AT_QUALITY_PRESET)) { float nearScreenBorder = min(min(depthBufferUV.x, 1.0 - depthBufferUV.x), min(depthBufferUV.y, 1.0 - depthBufferUV.y)); nearScreenBorder = saturate(10.0 * nearScreenBorder + 0.6); @@ -854,7 +639,7 @@ void GenerateSSAOShadowsInternal(out float outShadowTerm, out float4 outEdges, o // load & update pseudo-random rotation matrix uint pseudoRandomIndex = uint(SVPosRounded.y * 2 + SVPosRounded.x) % 5; - float4 rs = g_CACAOConsts.PatternRotScaleMatrices[pseudoRandomIndex]; + float4 rs = g_FFX_CACAO_Consts.PatternRotScaleMatrices[pseudoRandomIndex]; rotScale = float2x2(rs.x * pixLookupRadiusMod, rs.y * pixLookupRadiusMod, rs.z * pixLookupRadiusMod, rs.w * pixLookupRadiusMod); } @@ -866,15 +651,15 @@ void GenerateSSAOShadowsInternal(out float outShadowTerm, out float4 outEdges, o float4 edgesLRTB = float4(1.0, 1.0, 1.0, 1.0); // Move center pixel slightly towards camera to avoid imprecision artifacts due to using of 16bit depth buffer; a lot smaller offsets needed when using 32bit floats - pixCenterPos *= g_CACAOConsts.DepthPrecisionOffsetMod; + pixCenterPos *= g_FFX_CACAO_Consts.DepthPrecisionOffsetMod; - if (!adaptiveBase && (qualityLevel >= SSAO_DEPTH_BASED_EDGES_ENABLE_AT_QUALITY_PRESET)) + if (!adaptiveBase && (qualityLevel >= FFX_CACAO_DEPTH_BASED_EDGES_ENABLE_AT_QUALITY_PRESET)) { - edgesLRTB = CalculateEdges(pixZ, pixLZ, pixRZ, pixTZ, pixBZ); + edgesLRTB = FFX_CACAO_CalculateEdges(pixZ, pixLZ, pixRZ, pixTZ, pixBZ); } // adds a more high definition sharp effect, which gets blurred out (reuses left/right/top/bottom samples that we used for edge detection) - if (!adaptiveBase && (qualityLevel >= SSAO_DETAIL_AO_ENABLE_AT_QUALITY_PRESET)) + if (!adaptiveBase && (qualityLevel >= FFX_CACAO_DETAIL_AO_ENABLE_AT_QUALITY_PRESET)) { // disable in case of quality level 4 (reference) if (qualityLevel != 4) @@ -882,38 +667,37 @@ void GenerateSSAOShadowsInternal(out float outShadowTerm, out float4 outEdges, o //approximate neighbouring pixels positions (actually just deltas or "positions - pixCenterPos" ) float3 viewspaceDirZNormalized = float3(pixCenterPos.xy / pixCenterPos.zz, 1.0); - // very close approximation of: float3 pixLPos = NDCToViewspace( normalizedScreenPos + float2( -g_CACAOConsts.HalfViewportPixelSize.x, 0.0 ), pixLZ ).xyz - pixCenterPos.xyz; + // very close approximation of: float3 pixLPos = FFX_CACAO_NDCToViewSpace( normalizedScreenPos + float2( -g_FFX_CACAO_Consts.HalfViewportPixelSize.x, 0.0 ), pixLZ ).xyz - pixCenterPos.xyz; float3 pixLDelta = float3(-pixelDirRBViewspaceSizeAtCenterZ.x, 0.0, 0.0) + viewspaceDirZNormalized * (pixLZ - pixCenterPos.z); - // very close approximation of: float3 pixRPos = NDCToViewspace( normalizedScreenPos + float2( +g_CACAOConsts.HalfViewportPixelSize.x, 0.0 ), pixRZ ).xyz - pixCenterPos.xyz; + // very close approximation of: float3 pixRPos = FFX_CACAO_NDCToViewSpace( normalizedScreenPos + float2( +g_FFX_CACAO_Consts.HalfViewportPixelSize.x, 0.0 ), pixRZ ).xyz - pixCenterPos.xyz; float3 pixRDelta = float3(+pixelDirRBViewspaceSizeAtCenterZ.x, 0.0, 0.0) + viewspaceDirZNormalized * (pixRZ - pixCenterPos.z); - // very close approximation of: float3 pixTPos = NDCToViewspace( normalizedScreenPos + float2( 0.0, -g_CACAOConsts.HalfViewportPixelSize.y ), pixTZ ).xyz - pixCenterPos.xyz; + // very close approximation of: float3 pixTPos = FFX_CACAO_NDCToViewSpace( normalizedScreenPos + float2( 0.0, -g_FFX_CACAO_Consts.HalfViewportPixelSize.y ), pixTZ ).xyz - pixCenterPos.xyz; float3 pixTDelta = float3(0.0, -pixelDirRBViewspaceSizeAtCenterZ.y, 0.0) + viewspaceDirZNormalized * (pixTZ - pixCenterPos.z); - // very close approximation of: float3 pixBPos = NDCToViewspace( normalizedScreenPos + float2( 0.0, +g_CACAOConsts.HalfViewportPixelSize.y ), pixBZ ).xyz - pixCenterPos.xyz; + // very close approximation of: float3 pixBPos = FFX_CACAO_NDCToViewSpace( normalizedScreenPos + float2( 0.0, +g_FFX_CACAO_Consts.HalfViewportPixelSize.y ), pixBZ ).xyz - pixCenterPos.xyz; float3 pixBDelta = float3(0.0, +pixelDirRBViewspaceSizeAtCenterZ.y, 0.0) + viewspaceDirZNormalized * (pixBZ - pixCenterPos.z); const float rangeReductionConst = 4.0f; // this is to avoid various artifacts const float modifiedFalloffCalcMulSq = rangeReductionConst * falloffCalcMulSq; float4 additionalObscurance; - additionalObscurance.x = CalculatePixelObscurance(pixelNormal, pixLDelta, modifiedFalloffCalcMulSq); - additionalObscurance.y = CalculatePixelObscurance(pixelNormal, pixRDelta, modifiedFalloffCalcMulSq); - additionalObscurance.z = CalculatePixelObscurance(pixelNormal, pixTDelta, modifiedFalloffCalcMulSq); - additionalObscurance.w = CalculatePixelObscurance(pixelNormal, pixBDelta, modifiedFalloffCalcMulSq); + additionalObscurance.x = FFX_CACAO_CalculatePixelObscurance(pixelNormal, pixLDelta, modifiedFalloffCalcMulSq); + additionalObscurance.y = FFX_CACAO_CalculatePixelObscurance(pixelNormal, pixRDelta, modifiedFalloffCalcMulSq); + additionalObscurance.z = FFX_CACAO_CalculatePixelObscurance(pixelNormal, pixTDelta, modifiedFalloffCalcMulSq); + additionalObscurance.w = FFX_CACAO_CalculatePixelObscurance(pixelNormal, pixBDelta, modifiedFalloffCalcMulSq); - obscuranceSum += g_CACAOConsts.DetailAOStrength * dot(additionalObscurance, edgesLRTB); + obscuranceSum += g_FFX_CACAO_Consts.DetailAOStrength * dot(additionalObscurance, edgesLRTB); } } // Sharp normals also create edges - but this adds to the cost as well - if (!adaptiveBase && (qualityLevel >= SSAO_NORMAL_BASED_EDGES_ENABLE_AT_QUALITY_PRESET)) + if (!adaptiveBase && (qualityLevel >= FFX_CACAO_NORMAL_BASED_EDGES_ENABLE_AT_QUALITY_PRESET)) { - float3 neighbourNormalL = g_deinterlacedNormals[normalCoord + int3(-1, +0, 0)].xyz; - float3 neighbourNormalR = g_deinterlacedNormals[normalCoord + int3(+1, +0, 0)].xyz; - float3 neighbourNormalT = g_deinterlacedNormals[normalCoord + int3(+0, -1, 0)].xyz; - float3 neighbourNormalB = g_deinterlacedNormals[normalCoord + int3(+0, +1, 0)].xyz; - + float3 neighbourNormalL = FFX_CACAO_SSAOGeneration_GetNormalPass(SVPosui + int2(-1, +0), g_FFX_CACAO_Consts.PassIndex); + float3 neighbourNormalR = FFX_CACAO_SSAOGeneration_GetNormalPass(SVPosui + int2(+1, +0), g_FFX_CACAO_Consts.PassIndex); + float3 neighbourNormalT = FFX_CACAO_SSAOGeneration_GetNormalPass(SVPosui + int2(+0, -1), g_FFX_CACAO_Consts.PassIndex); + float3 neighbourNormalB = FFX_CACAO_SSAOGeneration_GetNormalPass(SVPosui + int2(+0, +1), g_FFX_CACAO_Consts.PassIndex); - const float dotThreshold = SSAO_NORMAL_BASED_EDGES_DOT_THRESHOLD; + const float dotThreshold = FFX_CACAO_NORMAL_BASED_EDGES_DOT_THRESHOLD; float4 normalEdgesLRTB; normalEdgesLRTB.x = saturate((dot(pixelNormal, neighbourNormalL) + dotThreshold)); @@ -921,8 +705,8 @@ void GenerateSSAOShadowsInternal(out float outShadowTerm, out float4 outEdges, o normalEdgesLRTB.z = saturate((dot(pixelNormal, neighbourNormalT) + dotThreshold)); normalEdgesLRTB.w = saturate((dot(pixelNormal, neighbourNormalB) + dotThreshold)); - //#define SSAO_SMOOTHEN_NORMALS // fixes some aliasing artifacts but kills a lot of high detail and adds to the cost - not worth it probably but feel free to play with it -#ifdef SSAO_SMOOTHEN_NORMALS + //#define FFX_CACAO_SMOOTHEN_NORMALS // fixes some aliasing artifacts but kills a lot of high detail and adds to the cost - not worth it probably but feel free to play with it +#ifdef FFX_CACAO_SMOOTHEN_NORMALS //neighbourNormalL = LoadNormal( fullResCoord, int2( -1, 0 ) ); //neighbourNormalR = LoadNormal( fullResCoord, int2( 1, 0 ) ); //neighbourNormalT = LoadNormal( fullResCoord, int2( 0, -1 ) ); @@ -936,113 +720,75 @@ void GenerateSSAOShadowsInternal(out float outShadowTerm, out float4 outEdges, o - const float globalMipOffset = SSAO_DEPTH_MIPS_GLOBAL_OFFSET; - float mipOffset = (qualityLevel < SSAO_DEPTH_MIPS_ENABLE_AT_QUALITY_PRESET) ? (0) : (log2(pixLookupRadiusMod) + globalMipOffset); + const float globalMipOffset = FFX_CACAO_DEPTH_MIPS_GLOBAL_OFFSET; + float mipOffset = (qualityLevel < FFX_CACAO_DEPTH_MIPS_ENABLE_AT_QUALITY_PRESET) ? (0) : (log2(pixLookupRadiusMod) + globalMipOffset); // Used to tilt the second set of samples so that the disk is effectively rotated by the normal // effective at removing one set of artifacts, but too expensive for lower quality settings float2 normXY = float2(pixelNormal.x, pixelNormal.y); float normXYLength = length(normXY); normXY /= float2(normXYLength, -normXYLength); - normXYLength *= SSAO_TILT_SAMPLES_AMOUNT; + normXYLength *= FFX_CACAO_TILT_SAMPLES_AMOUNT; const float3 negViewspaceDir = -normalize(pixCenterPos); // standard, non-adaptive approach if ((qualityLevel != 3) || adaptiveBase) { - //SSAOHits prevHits = SSAOGetHits(qualityLevel, normalizedScreenPos, 0, mipOffset); - -#if 0 - float4 newSample = g_samplePatternMain[0]; - // float zero = g_ZeroTexture.SampleLevel(g_PointClampSampler, float2(0.5f, 0.5f), 0); - SSAOSampleData data = SSAOGetSampleData(qualityLevel, rotScale, newSample, mipOffset); - SSAOHits hits = SSAOGetHits2(data, depthBufferUV); - newSample = g_samplePatternMain[1]; - // newSample.x += zero; - data = SSAOGetSampleData(qualityLevel, rotScale, newSample, mipOffset); - - [unroll] - for (int i = 0; i < numberOfTaps - 1; ++i) - { - // zero = g_ZeroTexture.SampleLevel(g_PointClampSampler, float2(0.5f + zero, 0.5f), 0); - SSAOHits nextHits = SSAOGetHits2(data, depthBufferUV); - // hits.hits[0].x += zero; - newSample = g_samplePatternMain[i + 2]; - - SSAOAddHits(qualityLevel, pixCenterPos, pixelNormal, falloffCalcMulSq, weightSum, obscuranceSum, hits); - SSAOSampleData nextData = SSAOGetSampleData(qualityLevel, rotScale, newSample, mipOffset); - hits = nextHits; - data = nextData; - } - - // last loop iteration - { - SSAOAddHits(qualityLevel, pixCenterPos, pixelNormal, falloffCalcMulSq, weightSum, obscuranceSum, hits); - } -#else - [unroll] for (int i = 0; i < numberOfTaps; i++) { - SSAOTap(qualityLevel, obscuranceSum, weightSum, i, rotScale, pixCenterPos, negViewspaceDir, pixelNormal, normalizedScreenPos, depthBufferUV, mipOffset, falloffCalcMulSq, 1.0, normXY, normXYLength); - // SSAOHits hits = SSAOGetHits(qualityLevel, normalizedScreenPos, i, mipOffset, rotScale); - // SSAOAddHits(qualityLevel, pixCenterPos, pixelNormal, 1.0f, falloffCalcMulSq, weightSum, obscuranceSum, hits); + FFX_CACAO_SSAOTap(qualityLevel, obscuranceSum, weightSum, i, rotScale, pixCenterPos, negViewspaceDir, pixelNormal, normalizedScreenPos, depthBufferUV, mipOffset, falloffCalcMulSq, 1.0, normXY, normXYLength); } - -#endif } else // if( qualityLevel == 3 ) adaptive approach { // add new ones if needed - float2 fullResUV = normalizedScreenPos + g_CACAOConsts.PerPassFullResUVOffset.xy; - float importance = g_ImportanceMap.SampleLevel(g_LinearClampSampler, fullResUV, 0.0).x; + float2 fullResUV = normalizedScreenPos + g_FFX_CACAO_Consts.PerPassFullResUVOffset.xy; + float importance = FFX_CACAO_SSAOGeneration_SampleImportance(fullResUV); - // this is to normalize SSAO_DETAIL_AO_AMOUNT across all pixel regardless of importance - obscuranceSum *= (SSAO_ADAPTIVE_TAP_BASE_COUNT / (float)SSAO_MAX_TAPS) + (importance * SSAO_ADAPTIVE_TAP_FLEXIBLE_COUNT / (float)SSAO_MAX_TAPS); + // this is to normalize FFX_CACAO_DETAIL_AO_AMOUNT across all pixel regardless of importance + obscuranceSum *= (FFX_CACAO_ADAPTIVE_TAP_BASE_COUNT / (float)FFX_CACAO_MAX_TAPS) + (importance * FFX_CACAO_ADAPTIVE_TAP_FLEXIBLE_COUNT / (float)FFX_CACAO_MAX_TAPS); // load existing base values - float2 baseValues = g_FinalSSAO.Load(int4(SVPosui, g_CACAOConsts.PassIndex, 0)).xy; - weightSum += baseValues.y * (float)(SSAO_ADAPTIVE_TAP_BASE_COUNT * 4.0); + float2 baseValues = FFX_CACAO_SSAOGeneration_LoadBasePassSSAOPass(SVPosui, g_FFX_CACAO_Consts.PassIndex); + weightSum += baseValues.y * (float)(FFX_CACAO_ADAPTIVE_TAP_BASE_COUNT * 4.0); obscuranceSum += (baseValues.x) * weightSum; // increase importance around edges float edgeCount = dot(1.0 - edgesLRTB, float4(1.0, 1.0, 1.0, 1.0)); - float avgTotalImportance = (float)g_LoadCounter[0] * g_CACAOConsts.LoadCounterAvgDiv; + float avgTotalImportance = (float)FFX_CACAO_SSAOGeneration_GetLoadCounter() * g_FFX_CACAO_Consts.LoadCounterAvgDiv; - float importanceLimiter = saturate(g_CACAOConsts.AdaptiveSampleCountLimit / avgTotalImportance); + float importanceLimiter = saturate(g_FFX_CACAO_Consts.AdaptiveSampleCountLimit / avgTotalImportance); importance *= importanceLimiter; - float additionalSampleCountFlt = SSAO_ADAPTIVE_TAP_FLEXIBLE_COUNT * importance; + float additionalSampleCountFlt = FFX_CACAO_ADAPTIVE_TAP_FLEXIBLE_COUNT * importance; additionalSampleCountFlt += 1.5; uint additionalSamples = uint(additionalSampleCountFlt); - uint additionalSamplesTo = min(SSAO_MAX_TAPS, additionalSamples + SSAO_ADAPTIVE_TAP_BASE_COUNT); + uint additionalSamplesTo = min(FFX_CACAO_MAX_TAPS, additionalSamples + FFX_CACAO_ADAPTIVE_TAP_BASE_COUNT); // sample loop { - float4 newSample = g_samplePatternMain[SSAO_ADAPTIVE_TAP_BASE_COUNT]; - SSAOSampleData data = SSAOGetSampleData(qualityLevel, rotScale, newSample, mipOffset); - SSAOHits hits = SSAOGetHits2(data, depthBufferUV); - newSample = g_samplePatternMain[SSAO_ADAPTIVE_TAP_BASE_COUNT + 1]; + float4 newSample = g_FFX_CACAO_samplePatternMain[FFX_CACAO_ADAPTIVE_TAP_BASE_COUNT]; + FFX_CACAO_SSAOSampleData data = FFX_CACAO_SSAOGetSampleData(qualityLevel, rotScale, newSample, mipOffset); + FFX_CACAO_SSAOHits hits = FFX_CACAO_SSAOGetHits2(data, depthBufferUV); + newSample = g_FFX_CACAO_samplePatternMain[FFX_CACAO_ADAPTIVE_TAP_BASE_COUNT + 1]; - for (uint i = SSAO_ADAPTIVE_TAP_BASE_COUNT; i < additionalSamplesTo - 1; i++) + for (uint i = FFX_CACAO_ADAPTIVE_TAP_BASE_COUNT; i < additionalSamplesTo - 1; i++) { - data = SSAOGetSampleData(qualityLevel, rotScale, newSample, mipOffset); - newSample = g_samplePatternMain[i + 2]; - SSAOHits nextHits = SSAOGetHits2(data, depthBufferUV); + data = FFX_CACAO_SSAOGetSampleData(qualityLevel, rotScale, newSample, mipOffset); + newSample = g_FFX_CACAO_samplePatternMain[i + 2]; + FFX_CACAO_SSAOHits nextHits = FFX_CACAO_SSAOGetHits2(data, depthBufferUV); - // float zero = g_ZeroTexture.SampleLevel(g_ZeroTextureSampler, (float)i, 0.0f); - // hits.weightMod += zero; - - SSAOAddHits(qualityLevel, pixCenterPos, pixelNormal, falloffCalcMulSq, weightSum, obscuranceSum, hits); + FFX_CACAO_SSAOAddHits(qualityLevel, pixCenterPos, pixelNormal, falloffCalcMulSq, weightSum, obscuranceSum, hits); hits = nextHits; } // last loop iteration { - SSAOAddHits(qualityLevel, pixCenterPos, pixelNormal, falloffCalcMulSq, weightSum, obscuranceSum, hits); + FFX_CACAO_SSAOAddHits(qualityLevel, pixCenterPos, pixelNormal, falloffCalcMulSq, weightSum, obscuranceSum, hits); } } } @@ -1062,10 +808,10 @@ void GenerateSSAOShadowsInternal(out float outShadowTerm, out float4 outEdges, o float obscurance = obscuranceSum / weightSum; // calculate fadeout (1 close, gradient, 0 far) - float fadeOut = saturate(pixCenterPos.z * g_CACAOConsts.EffectFadeOutMul + g_CACAOConsts.EffectFadeOutAdd); + float fadeOut = saturate(pixCenterPos.z * g_FFX_CACAO_Consts.EffectFadeOutMul + g_FFX_CACAO_Consts.EffectFadeOutAdd); // Reduce the SSAO shadowing if we're on the edge to remove artifacts on edges (we don't care for the lower quality one) - if (!adaptiveBase && (qualityLevel >= SSAO_DEPTH_BASED_EDGES_ENABLE_AT_QUALITY_PRESET)) + if (!adaptiveBase && (qualityLevel >= FFX_CACAO_DEPTH_BASED_EDGES_ENABLE_AT_QUALITY_PRESET)) { // float edgeCount = dot( 1.0-edgesLRTB, float4( 1.0, 1.0, 1.0, 1.0 ) ); @@ -1082,21 +828,21 @@ void GenerateSSAOShadowsInternal(out float outShadowTerm, out float4 outEdges, o // fadeOut *= saturate( dot( edgesLRTB, float4( 0.9, 0.9, 0.9, 0.9 ) ) - 2.6 ); // strength - obscurance = g_CACAOConsts.EffectShadowStrength * obscurance; + obscurance = g_FFX_CACAO_Consts.EffectShadowStrength * obscurance; // clamp - obscurance = min(obscurance, g_CACAOConsts.EffectShadowClamp); + obscurance = min(obscurance, g_FFX_CACAO_Consts.EffectShadowClamp); // fadeout obscurance *= fadeOut; - // conceptually switch to occlusion with the meaning being visibility (grows with visibility, occlusion == 1 implies full visibility), + // conceptually switch to occlusion with the meaning being visibility (grows with visibility, occlusion == 1 implies full visibility), // to be in line with what is more commonly used. float occlusion = 1.0 - obscurance; // modify the gradient // note: this cannot be moved to a later pass because of loss of precision after storing in the render target - occlusion = pow(saturate(occlusion), g_CACAOConsts.EffectShadowPow); + occlusion = pow(saturate(occlusion), g_FFX_CACAO_Consts.EffectShadowPow); // outputs! outShadowTerm = occlusion; // Our final 'occlusion' term (0 means fully occluded, 1 means fully lit) @@ -1104,89 +850,86 @@ void GenerateSSAOShadowsInternal(out float outShadowTerm, out float4 outEdges, o outWeight = weightSum; } -[numthreads(GENERATE_WIDTH, GENERATE_HEIGHT, 1)] -void CSGenerateQ0(uint2 coord : SV_DispatchThreadID) +[numthreads(FFX_CACAO_GENERATE_SPARSE_WIDTH, FFX_CACAO_GENERATE_SPARSE_HEIGHT, 1)] +void FFX_CACAO_GenerateQ0(uint3 tid : SV_DispatchThreadID) { + uint xOffset = (tid.y * 3 + tid.z) % 5; + uint2 coord = uint2(5 * tid.x + xOffset, tid.y); float2 inPos = (float2)coord; float outShadowTerm; float outWeight; float4 outEdges; - GenerateSSAOShadowsInternal(outShadowTerm, outEdges, outWeight, inPos.xy, 0, false); + FFX_CACAO_GenerateSSAOShadowsInternal(outShadowTerm, outEdges, outWeight, inPos.xy, 0, false); float2 out0; out0.x = outShadowTerm; - out0.y = PackEdges(float4(1, 1, 1, 1)); // no edges in low quality - g_SSAOOutput[uint3(coord, 0)] = out0; + out0.y = FFX_CACAO_PackEdges(float4(1, 1, 1, 1)); // no edges in low quality + FFX_CACAO_SSAOGeneration_StoreOutput(coord, out0); } -[numthreads(GENERATE_WIDTH, GENERATE_HEIGHT, 1)] -void CSGenerateQ1(uint2 coord : SV_DispatchThreadID) +[numthreads(FFX_CACAO_GENERATE_SPARSE_WIDTH, FFX_CACAO_GENERATE_SPARSE_HEIGHT, 1)] +void FFX_CACAO_GenerateQ1(uint3 tid : SV_DispatchThreadID) { + uint xOffset = (tid.y * 3 + tid.z) % 5; + uint2 coord = uint2(5 * tid.x + xOffset, tid.y); float2 inPos = (float2)coord; float outShadowTerm; float outWeight; float4 outEdges; - GenerateSSAOShadowsInternal(outShadowTerm, outEdges, outWeight, inPos.xy, 1, false); + FFX_CACAO_GenerateSSAOShadowsInternal(outShadowTerm, outEdges, outWeight, inPos.xy, 1, false); float2 out0; out0.x = outShadowTerm; - out0.y = PackEdges(outEdges); - g_SSAOOutput[uint3(coord, 0)] = out0; + out0.y = FFX_CACAO_PackEdges(outEdges); + FFX_CACAO_SSAOGeneration_StoreOutput(coord, out0); } -[numthreads(GENERATE_WIDTH, GENERATE_HEIGHT, 1)] -void CSGenerateQ2(uint2 coord : SV_DispatchThreadID) +[numthreads(FFX_CACAO_GENERATE_WIDTH, FFX_CACAO_GENERATE_HEIGHT, 1)] +void FFX_CACAO_GenerateQ2(uint2 coord : SV_DispatchThreadID) { float2 inPos = (float2)coord; float outShadowTerm; float outWeight; float4 outEdges; - GenerateSSAOShadowsInternal(outShadowTerm, outEdges, outWeight, inPos.xy, 2, false); + FFX_CACAO_GenerateSSAOShadowsInternal(outShadowTerm, outEdges, outWeight, inPos.xy, 2, false); float2 out0; out0.x = outShadowTerm; - out0.y = PackEdges(outEdges); - g_SSAOOutput[uint3(coord, 0)] = out0; + out0.y = FFX_CACAO_PackEdges(outEdges); + FFX_CACAO_SSAOGeneration_StoreOutput(coord, out0); } -[numthreads(GENERATE_WIDTH, GENERATE_HEIGHT, 1)] -void CSGenerateQ3Base(uint2 coord : SV_DispatchThreadID) +[numthreads(FFX_CACAO_GENERATE_WIDTH, FFX_CACAO_GENERATE_HEIGHT, 1)] +void FFX_CACAO_GenerateQ3Base(uint2 coord : SV_DispatchThreadID) { float2 inPos = (float2)coord; float outShadowTerm; float outWeight; float4 outEdges; - GenerateSSAOShadowsInternal(outShadowTerm, outEdges, outWeight, inPos.xy, 3, true); + FFX_CACAO_GenerateSSAOShadowsInternal(outShadowTerm, outEdges, outWeight, inPos.xy, 3, true); float2 out0; out0.x = outShadowTerm; - out0.y = outWeight / ((float)SSAO_ADAPTIVE_TAP_BASE_COUNT * 4.0); //0.0; //frac(outWeight / 6.0);// / (float)(SSAO_MAX_TAPS * 4.0); - g_SSAOOutput[uint3(coord, 0)] = out0; + out0.y = outWeight / ((float)FFX_CACAO_ADAPTIVE_TAP_BASE_COUNT * 4.0); //0.0; //frac(outWeight / 6.0);// / (float)(FFX_CACAO_MAX_TAPS * 4.0); + FFX_CACAO_SSAOGeneration_StoreOutput(coord, out0); } -[numthreads(GENERATE_WIDTH, GENERATE_HEIGHT, 1)] -void CSGenerateQ3(uint2 coord : SV_DispatchThreadID) +[numthreads(FFX_CACAO_GENERATE_WIDTH, FFX_CACAO_GENERATE_HEIGHT, 1)] +void FFX_CACAO_GenerateQ3(uint2 coord : SV_DispatchThreadID) { float2 inPos = (float2)coord; float outShadowTerm; float outWeight; float4 outEdges; - GenerateSSAOShadowsInternal(outShadowTerm, outEdges, outWeight, inPos.xy, 3, false); + FFX_CACAO_GenerateSSAOShadowsInternal(outShadowTerm, outEdges, outWeight, inPos.xy, 3, false); float2 out0; out0.x = outShadowTerm; - out0.y = PackEdges(outEdges); - g_SSAOOutput[uint3(coord, 0)] = out0; + out0.y = FFX_CACAO_PackEdges(outEdges); + FFX_CACAO_SSAOGeneration_StoreOutput(coord, out0); } - - - // ======================================================= // Apply -Texture2DArray g_ApplyFinalSSAO : register(t0); -RWTexture2D<float> g_ApplyOutput : register(u0); - - -[numthreads(APPLY_WIDTH, APPLY_HEIGHT, 1)] -void CSApply(uint2 coord : SV_DispatchThreadID) +[numthreads(FFX_CACAO_APPLY_WIDTH, FFX_CACAO_APPLY_HEIGHT, 1)] +void FFX_CACAO_Apply(uint2 coord : SV_DispatchThreadID) { float ao; float2 inPos = coord; @@ -1201,12 +944,12 @@ void CSApply(uint2 coord : SV_DispatchThreadID) int iv = mx + (1 - my) * 2; // neighbouring, vertical int id = (1 - mx) + (1 - my) * 2; // diagonal - float2 centerVal = g_ApplyFinalSSAO.Load(int4(pixPosHalf, ic, 0)).xy; + float2 centerVal = FFX_CACAO_Apply_LoadSSAOPass(pixPosHalf, ic); ao = centerVal.x; #if 1 // change to 0 if you want to disable last pass high-res blur (for debugging purposes, etc.) - float4 edgesLRTB = UnpackEdges(centerVal.y); + float4 edgesLRTB = FFX_CACAO_UnpackEdges(centerVal.y); // return 1.0 - float4( edgesLRTB.x, edgesLRTB.y * 0.5 + edgesLRTB.w * 0.5, edgesLRTB.z, 0.0 ); // debug show edges @@ -1219,12 +962,12 @@ void CSApply(uint2 coord : SV_DispatchThreadID) float fmye = (edgesLRTB.w - edgesLRTB.z); // calculate final sampling offsets and sample using bilinear filter - float2 uvH = (inPos.xy + float2(fmx + fmxe - 0.5, 0.5 - fmy)) * 0.5 * g_CACAOConsts.SSAOBufferInverseDimensions; - float aoH = g_ApplyFinalSSAO.SampleLevel(g_LinearClampSampler, float3(uvH, ih), 0).x; - float2 uvV = (inPos.xy + float2(0.5 - fmx, fmy - 0.5 + fmye)) * 0.5 * g_CACAOConsts.SSAOBufferInverseDimensions; - float aoV = g_ApplyFinalSSAO.SampleLevel(g_LinearClampSampler, float3(uvV, iv), 0).x; - float2 uvD = (inPos.xy + float2(fmx - 0.5 + fmxe, fmy - 0.5 + fmye)) * 0.5 * g_CACAOConsts.SSAOBufferInverseDimensions; - float aoD = g_ApplyFinalSSAO.SampleLevel(g_LinearClampSampler, float3(uvD, id), 0).x; + float2 uvH = (inPos.xy + float2(fmx + fmxe - 0.5, 0.5 - fmy)) * 0.5 * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; + float aoH = FFX_CACAO_Apply_SampleSSAOUVPass(uvH, ih); + float2 uvV = (inPos.xy + float2(0.5 - fmx, fmy - 0.5 + fmye)) * 0.5 * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; + float aoV = FFX_CACAO_Apply_SampleSSAOUVPass(uvV, iv); + float2 uvD = (inPos.xy + float2(fmx - 0.5 + fmxe, fmy - 0.5 + fmye)) * 0.5 * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; + float aoD = FFX_CACAO_Apply_SampleSSAOUVPass(uvD, id); // reduce weight for samples near edge - if the edge is on both sides, weight goes to 0 float4 blendWeights; @@ -1238,109 +981,63 @@ void CSApply(uint2 coord : SV_DispatchThreadID) ao = dot(float4(ao, aoH, aoV, aoD), blendWeights) / blendWeightsSum; #endif - g_ApplyOutput[coord] = ao.x; + FFX_CACAO_Apply_StoreOutput(coord, ao.x); } // edge-ignorant blur & apply (for the lowest quality level 0) -[numthreads(APPLY_WIDTH, APPLY_HEIGHT, 1)] -void CSNonSmartApply(uint2 tid : SV_DispatchThreadID) -{ - float2 inUV = float2(tid) * g_CACAOConsts.OutputBufferInverseDimensions; - float a = g_ApplyFinalSSAO.SampleLevel(g_LinearClampSampler, float3(inUV.xy, 0), 0.0).x; - float b = g_ApplyFinalSSAO.SampleLevel(g_LinearClampSampler, float3(inUV.xy, 1), 0.0).x; - float c = g_ApplyFinalSSAO.SampleLevel(g_LinearClampSampler, float3(inUV.xy, 2), 0.0).x; - float d = g_ApplyFinalSSAO.SampleLevel(g_LinearClampSampler, float3(inUV.xy, 3), 0.0).x; - float avg = (a + b + c + d) * 0.25; - g_ApplyOutput[tid] = avg; -} - -// edge-ignorant blur & apply, skipping half pixels in checkerboard pattern (for the Lowest quality level 0 and Settings::SkipHalfPixelsOnLowQualityLevel == true ) -[numthreads(APPLY_WIDTH, APPLY_HEIGHT, 1)] -void CSNonSmartHalfApply(uint2 tid : SV_DispatchThreadID) -{ - float2 inUV = float2(tid) * g_CACAOConsts.OutputBufferInverseDimensions; - float a = g_ApplyFinalSSAO.SampleLevel(g_LinearClampSampler, float3(inUV.xy, 0), 0.0).x; - float d = g_ApplyFinalSSAO.SampleLevel(g_LinearClampSampler, float3(inUV.xy, 3), 0.0).x; - float avg = (a + d) * 0.5; - g_ApplyOutput[tid] = avg; -} - - -// ============================================================================= -// Prepare - -Texture2D<float> g_DepthIn : register(t0); - -groupshared uint s_PrepareMem[10][18]; - - -min16float ScreenSpaceToViewSpaceDepth(min16float screenDepth) +[numthreads(FFX_CACAO_APPLY_WIDTH, FFX_CACAO_APPLY_HEIGHT, 1)] +void FFX_CACAO_NonSmartApply(uint2 tid : SV_DispatchThreadID) { - min16float depthLinearizeMul = min16float(g_CACAOConsts.DepthUnpackConsts.x); - min16float depthLinearizeAdd = min16float(g_CACAOConsts.DepthUnpackConsts.y); + float2 inUV = float2(tid) * g_FFX_CACAO_Consts.OutputBufferInverseDimensions; + float a = FFX_CACAO_Apply_SampleSSAOUVPass(inUV.xy, 0); + float b = FFX_CACAO_Apply_SampleSSAOUVPass(inUV.xy, 1); + float c = FFX_CACAO_Apply_SampleSSAOUVPass(inUV.xy, 2); + float d = FFX_CACAO_Apply_SampleSSAOUVPass(inUV.xy, 3); + float avg = (a + b + c + d) * 0.25f; - // Optimised version of "-cameraClipNear / (cameraClipFar - projDepth * (cameraClipFar - cameraClipNear)) * cameraClipFar" - - // Set your depthLinearizeMul and depthLinearizeAdd to: - // depthLinearizeMul = ( cameraClipFar * cameraClipNear) / ( cameraClipFar - cameraClipNear ); - // depthLinearizeAdd = cameraClipFar / ( cameraClipFar - cameraClipNear ); - - return depthLinearizeMul / (depthLinearizeAdd - screenDepth); + FFX_CACAO_Apply_StoreOutput(tid, avg); } -min16float4 ScreenSpaceToViewSpaceDepth4x(min16float4 screenDepths) +// edge-ignorant blur & apply, skipping half pixels in checkerboard pattern (for the Lowest quality level 0 and Settings::SkipHalfPixelsOnLowQualityLevel == true ) +[numthreads(FFX_CACAO_APPLY_WIDTH, FFX_CACAO_APPLY_HEIGHT, 1)] +void FFX_CACAO_NonSmartHalfApply(uint2 tid : SV_DispatchThreadID) { - min16float depthLinearizeMul = min16float(g_CACAOConsts.DepthUnpackConsts.x); - min16float depthLinearizeAdd = min16float(g_CACAOConsts.DepthUnpackConsts.y); - - // Optimised version of "-cameraClipNear / (cameraClipFar - projDepth * (cameraClipFar - cameraClipNear)) * cameraClipFar" - - // Set your depthLinearizeMul and depthLinearizeAdd to: - // depthLinearizeMul = ( cameraClipFar * cameraClipNear) / ( cameraClipFar - cameraClipNear ); - // depthLinearizeAdd = cameraClipFar / ( cameraClipFar - cameraClipNear ); + float2 inUV = float2(tid) * g_FFX_CACAO_Consts.OutputBufferInverseDimensions; + float a = FFX_CACAO_Apply_SampleSSAOUVPass(inUV.xy, 0); + float d = FFX_CACAO_Apply_SampleSSAOUVPass(inUV.xy, 3); + float avg = (a + d) * 0.5f; - return depthLinearizeMul / (depthLinearizeAdd - screenDepths); + FFX_CACAO_Apply_StoreOutput(tid, avg); } -RWTexture2DArray<float> g_PrepareDepthsAndMips_OutMip0 : register(u0); -RWTexture2DArray<float> g_PrepareDepthsAndMips_OutMip1 : register(u1); -RWTexture2DArray<float> g_PrepareDepthsAndMips_OutMip2 : register(u2); -RWTexture2DArray<float> g_PrepareDepthsAndMips_OutMip3 : register(u3); +// ============================================================================= +// Prepare -groupshared float s_PrepareDepthsAndMipsBuffer[4][8][8]; +groupshared float s_FFX_CACAO_PrepareDepthsAndMipsBuffer[4][8][8]; -float MipSmartAverage(float4 depths) +float FFX_CACAO_MipSmartAverage(float4 depths) { float closest = min(min(depths.x, depths.y), min(depths.z, depths.w)); - float falloffCalcMulSq = -1.0f / g_CACAOConsts.EffectRadius * g_CACAOConsts.EffectRadius; + float falloffCalcMulSq = -1.0f / g_FFX_CACAO_Consts.EffectRadius * g_FFX_CACAO_Consts.EffectRadius; float4 dists = depths - closest.xxxx; float4 weights = saturate(dists * dists * falloffCalcMulSq + 1.0); return dot(weights, depths) / dot(weights, float4(1.0, 1.0, 1.0, 1.0)); } -min16float MipSmartAverage_16(min16float4 depths) -{ - min16float closest = min(min(depths.x, depths.y), min(depths.z, depths.w)); - min16float falloffCalcMulSq = min16float(-1.0f / g_CACAOConsts.EffectRadius * g_CACAOConsts.EffectRadius); - min16float4 dists = depths - closest.xxxx; - min16float4 weights = saturate(dists * dists * falloffCalcMulSq + 1.0); - return dot(weights, depths) / dot(weights, min16float4(1.0, 1.0, 1.0, 1.0)); -} - -void PrepareDepthsAndMips(float4 samples, uint2 outputCoord, uint2 gtid) +void FFX_CACAO_PrepareDepthsAndMips(float4 samples, uint2 outputCoord, uint2 gtid) { - samples = ScreenSpaceToViewSpaceDepth(samples); + samples = FFX_CACAO_ScreenSpaceToViewSpaceDepth(samples); - s_PrepareDepthsAndMipsBuffer[0][gtid.x][gtid.y] = samples.w; - s_PrepareDepthsAndMipsBuffer[1][gtid.x][gtid.y] = samples.z; - s_PrepareDepthsAndMipsBuffer[2][gtid.x][gtid.y] = samples.x; - s_PrepareDepthsAndMipsBuffer[3][gtid.x][gtid.y] = samples.y; + s_FFX_CACAO_PrepareDepthsAndMipsBuffer[0][gtid.x][gtid.y] = samples.w; + s_FFX_CACAO_PrepareDepthsAndMipsBuffer[1][gtid.x][gtid.y] = samples.z; + s_FFX_CACAO_PrepareDepthsAndMipsBuffer[2][gtid.x][gtid.y] = samples.x; + s_FFX_CACAO_PrepareDepthsAndMipsBuffer[3][gtid.x][gtid.y] = samples.y; - g_PrepareDepthsAndMips_OutMip0[int3(outputCoord.x, outputCoord.y, 0)] = samples.w; - g_PrepareDepthsAndMips_OutMip0[int3(outputCoord.x, outputCoord.y, 1)] = samples.z; - g_PrepareDepthsAndMips_OutMip0[int3(outputCoord.x, outputCoord.y, 2)] = samples.x; - g_PrepareDepthsAndMips_OutMip0[int3(outputCoord.x, outputCoord.y, 3)] = samples.y; + FFX_CACAO_Prepare_StoreDepthMip0(outputCoord, 0, samples.w); + FFX_CACAO_Prepare_StoreDepthMip0(outputCoord, 1, samples.z); + FFX_CACAO_Prepare_StoreDepthMip0(outputCoord, 2, samples.x); + FFX_CACAO_Prepare_StoreDepthMip0(outputCoord, 3, samples.y); uint depthArrayIndex = 2 * (gtid.y % 2) + (gtid.x % 2); uint2 depthArrayOffset = int2(gtid.x % 2, gtid.y % 2); @@ -1351,14 +1048,14 @@ void PrepareDepthsAndMips(float4 samples, uint2 outputCoord, uint2 gtid) // if (stillAlive) <-- all threads alive here { - float sample_00 = s_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 0][bufferCoord.y + 0]; - float sample_01 = s_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 0][bufferCoord.y + 1]; - float sample_10 = s_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 1][bufferCoord.y + 0]; - float sample_11 = s_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 1][bufferCoord.y + 1]; - - float avg = MipSmartAverage(float4(sample_00, sample_01, sample_10, sample_11)); - g_PrepareDepthsAndMips_OutMip1[int3(outputCoord.x, outputCoord.y, depthArrayIndex)] = avg; - s_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x][bufferCoord.y] = avg; + float sample_00 = s_FFX_CACAO_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 0][bufferCoord.y + 0]; + float sample_01 = s_FFX_CACAO_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 0][bufferCoord.y + 1]; + float sample_10 = s_FFX_CACAO_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 1][bufferCoord.y + 0]; + float sample_11 = s_FFX_CACAO_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 1][bufferCoord.y + 1]; + + float avg = FFX_CACAO_MipSmartAverage(float4(sample_00, sample_01, sample_10, sample_11)); + FFX_CACAO_Prepare_StoreDepthMip1(outputCoord, depthArrayIndex, avg); + s_FFX_CACAO_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x][bufferCoord.y] = avg; } bool stillAlive = gtid.x % 4 == depthArrayOffset.x && gtid.y % 4 == depthArrayOffset.y; @@ -1368,132 +1065,121 @@ void PrepareDepthsAndMips(float4 samples, uint2 outputCoord, uint2 gtid) if (stillAlive) { - float sample_00 = s_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 0][bufferCoord.y + 0]; - float sample_01 = s_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 0][bufferCoord.y + 2]; - float sample_10 = s_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 2][bufferCoord.y + 0]; - float sample_11 = s_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 2][bufferCoord.y + 2]; - - float avg = MipSmartAverage(float4(sample_00, sample_01, sample_10, sample_11)); - g_PrepareDepthsAndMips_OutMip2[int3(outputCoord.x, outputCoord.y, depthArrayIndex)] = avg; - s_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x][bufferCoord.y] = avg; + float sample_00 = s_FFX_CACAO_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 0][bufferCoord.y + 0]; + float sample_01 = s_FFX_CACAO_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 0][bufferCoord.y + 2]; + float sample_10 = s_FFX_CACAO_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 2][bufferCoord.y + 0]; + float sample_11 = s_FFX_CACAO_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 2][bufferCoord.y + 2]; + + float avg = FFX_CACAO_MipSmartAverage(float4(sample_00, sample_01, sample_10, sample_11)); + FFX_CACAO_Prepare_StoreDepthMip2(outputCoord, depthArrayIndex, avg); + s_FFX_CACAO_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x][bufferCoord.y] = avg; } - stillAlive = gtid.x % 8 == depthArrayOffset.x && depthArrayOffset.y % 8 == depthArrayOffset.y; + stillAlive = gtid.x % 8 == depthArrayOffset.x && gtid.y % 8 == depthArrayOffset.y; outputCoord /= 2; GroupMemoryBarrierWithGroupSync(); if (stillAlive) { - float sample_00 = s_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 0][bufferCoord.y + 0]; - float sample_01 = s_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 0][bufferCoord.y + 4]; - float sample_10 = s_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 4][bufferCoord.y + 0]; - float sample_11 = s_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 4][bufferCoord.y + 4]; + float sample_00 = s_FFX_CACAO_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 0][bufferCoord.y + 0]; + float sample_01 = s_FFX_CACAO_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 0][bufferCoord.y + 4]; + float sample_10 = s_FFX_CACAO_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 4][bufferCoord.y + 0]; + float sample_11 = s_FFX_CACAO_PrepareDepthsAndMipsBuffer[depthArrayIndex][bufferCoord.x + 4][bufferCoord.y + 4]; - float avg = MipSmartAverage(float4(sample_00, sample_01, sample_10, sample_11)); - g_PrepareDepthsAndMips_OutMip3[int3(outputCoord.x, outputCoord.y, depthArrayIndex)] = avg; + float avg = FFX_CACAO_MipSmartAverage(float4(sample_00, sample_01, sample_10, sample_11)); + FFX_CACAO_Prepare_StoreDepthMip3(outputCoord, depthArrayIndex, avg); } } -[numthreads(PREPARE_DEPTHS_AND_MIPS_WIDTH, PREPARE_DEPTHS_AND_MIPS_HEIGHT, 1)] -void CSPrepareDownsampledDepthsAndMips(uint2 tid : SV_DispatchThreadID, uint2 gtid : SV_GroupThreadID) +[numthreads(FFX_CACAO_PREPARE_DEPTHS_AND_MIPS_WIDTH, FFX_CACAO_PREPARE_DEPTHS_AND_MIPS_HEIGHT, 1)] +void FFX_CACAO_PrepareDownsampledDepthsAndMips(uint2 tid : SV_DispatchThreadID, uint2 gtid : SV_GroupThreadID) { int2 depthBufferCoord = 4 * tid.xy; int2 outputCoord = tid; - float2 uv = (float2(depthBufferCoord)+0.5f) * g_CACAOConsts.DepthBufferInverseDimensions; + float2 uv = (float2(depthBufferCoord)+0.5f) * g_FFX_CACAO_Consts.DepthBufferInverseDimensions; float4 samples; -#if 1 - samples.x = g_DepthIn.SampleLevel(g_PointClampSampler, uv, 0, int2(0, 2)); - samples.y = g_DepthIn.SampleLevel(g_PointClampSampler, uv, 0, int2(2, 2)); - samples.z = g_DepthIn.SampleLevel(g_PointClampSampler, uv, 0, int2(2, 0)); - samples.w = g_DepthIn.SampleLevel(g_PointClampSampler, uv, 0, int2(0, 0)); -#else - samples.x = g_DepthIn[depthBufferCoord + uint2(0, 2)]; - samples.y = g_DepthIn[depthBufferCoord + uint2(2, 2)]; - samples.z = g_DepthIn[depthBufferCoord + uint2(2, 0)]; - samples.w = g_DepthIn[depthBufferCoord + uint2(0, 0)]; -#endif - PrepareDepthsAndMips(samples, outputCoord, gtid); + samples.x = FFX_CACAO_Prepare_SampleDepthOffset(uv, int2(0, 2)); + samples.y = FFX_CACAO_Prepare_SampleDepthOffset(uv, int2(2, 2)); + samples.z = FFX_CACAO_Prepare_SampleDepthOffset(uv, int2(2, 0)); + samples.w = FFX_CACAO_Prepare_SampleDepthOffset(uv, int2(0, 0)); + + FFX_CACAO_PrepareDepthsAndMips(samples, outputCoord, gtid); } -[numthreads(PREPARE_DEPTHS_AND_MIPS_WIDTH, PREPARE_DEPTHS_AND_MIPS_HEIGHT, 1)] -void CSPrepareNativeDepthsAndMips(uint2 tid : SV_DispatchThreadID, uint2 gtid : SV_GroupThreadID) +[numthreads(FFX_CACAO_PREPARE_DEPTHS_AND_MIPS_WIDTH, FFX_CACAO_PREPARE_DEPTHS_AND_MIPS_HEIGHT, 1)] +void FFX_CACAO_PrepareNativeDepthsAndMips(uint2 tid : SV_DispatchThreadID, uint2 gtid : SV_GroupThreadID) { int2 depthBufferCoord = 2 * tid.xy; int2 outputCoord = tid; - float2 uv = (float2(depthBufferCoord)+0.5f) * g_CACAOConsts.DepthBufferInverseDimensions; - float4 samples = g_DepthIn.GatherRed(g_PointClampSampler, uv); + float2 uv = (float2(depthBufferCoord)+0.5f) * g_FFX_CACAO_Consts.DepthBufferInverseDimensions; + float4 samples = FFX_CACAO_Prepare_GatherDepth(uv); - PrepareDepthsAndMips(samples, outputCoord, gtid); + FFX_CACAO_PrepareDepthsAndMips(samples, outputCoord, gtid); } -RWTexture2DArray<float> g_PrepareDepthsOut : register(u0); - -void PrepareDepths(float4 samples, uint2 tid) +void FFX_CACAO_PrepareDepths(float4 samples, uint2 tid) { - samples = ScreenSpaceToViewSpaceDepth(samples); - g_PrepareDepthsOut[int3(tid.x, tid.y, 0)] = samples.w; - g_PrepareDepthsOut[int3(tid.x, tid.y, 1)] = samples.z; - g_PrepareDepthsOut[int3(tid.x, tid.y, 2)] = samples.x; - g_PrepareDepthsOut[int3(tid.x, tid.y, 3)] = samples.y; + samples = FFX_CACAO_ScreenSpaceToViewSpaceDepth(samples); + FFX_CACAO_Prepare_StoreDepth(tid, 0, samples.w); + FFX_CACAO_Prepare_StoreDepth(tid, 1, samples.z); + FFX_CACAO_Prepare_StoreDepth(tid, 2, samples.x); + FFX_CACAO_Prepare_StoreDepth(tid, 3, samples.y); } -[numthreads(PREPARE_DEPTHS_WIDTH, PREPARE_DEPTHS_HEIGHT, 1)] -void CSPrepareDownsampledDepths(uint2 tid : SV_DispatchThreadID) +[numthreads(FFX_CACAO_PREPARE_DEPTHS_WIDTH, FFX_CACAO_PREPARE_DEPTHS_HEIGHT, 1)] +void FFX_CACAO_PrepareDownsampledDepths(uint2 tid : SV_DispatchThreadID) { int2 depthBufferCoord = 4 * tid.xy; - float2 uv = (float2(depthBufferCoord)+0.5f) * g_CACAOConsts.DepthBufferInverseDimensions; + float2 uv = (float2(depthBufferCoord)+0.5f) * g_FFX_CACAO_Consts.DepthBufferInverseDimensions; float4 samples; - samples.x = g_DepthIn.SampleLevel(g_PointClampSampler, uv, 0, int2(0, 2)); - samples.y = g_DepthIn.SampleLevel(g_PointClampSampler, uv, 0, int2(2, 2)); - samples.z = g_DepthIn.SampleLevel(g_PointClampSampler, uv, 0, int2(2, 0)); - samples.w = g_DepthIn.SampleLevel(g_PointClampSampler, uv, 0, int2(0, 0)); - - PrepareDepths(samples, tid); + + samples.x = FFX_CACAO_Prepare_SampleDepthOffset(uv, int2(0, 2)); + samples.y = FFX_CACAO_Prepare_SampleDepthOffset(uv, int2(2, 2)); + samples.z = FFX_CACAO_Prepare_SampleDepthOffset(uv, int2(2, 0)); + samples.w = FFX_CACAO_Prepare_SampleDepthOffset(uv, int2(0, 0)); + + FFX_CACAO_PrepareDepths(samples, tid); } -[numthreads(PREPARE_DEPTHS_WIDTH, PREPARE_DEPTHS_HEIGHT, 1)] -void CSPrepareNativeDepths(uint2 tid : SV_DispatchThreadID) +[numthreads(FFX_CACAO_PREPARE_DEPTHS_WIDTH, FFX_CACAO_PREPARE_DEPTHS_HEIGHT, 1)] +void FFX_CACAO_PrepareNativeDepths(uint2 tid : SV_DispatchThreadID) { int2 depthBufferCoord = 2 * tid.xy; - float2 uv = (float2(depthBufferCoord)+0.5f) * g_CACAOConsts.DepthBufferInverseDimensions; - float4 samples = g_DepthIn.GatherRed(g_PointClampSampler, uv); + float2 uv = (float2(depthBufferCoord)+0.5f) * g_FFX_CACAO_Consts.DepthBufferInverseDimensions; + float4 samples = FFX_CACAO_Prepare_GatherDepth(uv); - PrepareDepths(samples, tid); + FFX_CACAO_PrepareDepths(samples, tid); } -[numthreads(PREPARE_DEPTHS_HALF_WIDTH, PREPARE_DEPTHS_HALF_HEIGHT, 1)] -void CSPrepareDownsampledDepthsHalf(uint2 tid : SV_DispatchThreadID) +[numthreads(FFX_CACAO_PREPARE_DEPTHS_HALF_WIDTH, FFX_CACAO_PREPARE_DEPTHS_HALF_HEIGHT, 1)] +void FFX_CACAO_PrepareDownsampledDepthsHalf(uint2 tid : SV_DispatchThreadID) { - float sample_00 = g_DepthIn.Load(int3(4 * tid.x + 0, 4 * tid.y + 0, 0)); - float sample_11 = g_DepthIn.Load(int3(4 * tid.x + 2, 4 * tid.y + 2, 0)); - sample_00 = ScreenSpaceToViewSpaceDepth(sample_00); - sample_11 = ScreenSpaceToViewSpaceDepth(sample_11); - g_PrepareDepthsOut[int3(tid.x, tid.y, 0)] = sample_00; - g_PrepareDepthsOut[int3(tid.x, tid.y, 3)] = sample_11; + float sample_00 = FFX_CACAO_Prepare_LoadDepth(int2(4 * tid.x + 0, 4 * tid.y + 0)); + float sample_11 = FFX_CACAO_Prepare_LoadDepth(int2(4 * tid.x + 2, 4 * tid.y + 2)); + sample_00 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(sample_00); + sample_11 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(sample_11); + FFX_CACAO_Prepare_StoreDepth(tid, 0, sample_00); + FFX_CACAO_Prepare_StoreDepth(tid, 3, sample_11); } -[numthreads(PREPARE_DEPTHS_HALF_WIDTH, PREPARE_DEPTHS_HALF_HEIGHT, 1)] -void CSPrepareNativeDepthsHalf(uint2 tid : SV_DispatchThreadID) +[numthreads(FFX_CACAO_PREPARE_DEPTHS_HALF_WIDTH, FFX_CACAO_PREPARE_DEPTHS_HALF_HEIGHT, 1)] +void FFX_CACAO_PrepareNativeDepthsHalf(uint2 tid : SV_DispatchThreadID) { - float sample_00 = g_DepthIn.Load(int3(2 * tid.x + 0, 2 * tid.y + 0, 0)); - float sample_11 = g_DepthIn.Load(int3(2 * tid.x + 1, 2 * tid.y + 1, 0)); - sample_00 = ScreenSpaceToViewSpaceDepth(sample_00); - sample_11 = ScreenSpaceToViewSpaceDepth(sample_11); - g_PrepareDepthsOut[int3(tid.x, tid.y, 0)] = sample_00; - g_PrepareDepthsOut[int3(tid.x, tid.y, 3)] = sample_11; + float sample_00 = FFX_CACAO_Prepare_LoadDepth(int2(2 * tid.x + 0, 2 * tid.y + 0)); + float sample_11 = FFX_CACAO_Prepare_LoadDepth(int2(2 * tid.x + 1, 2 * tid.y + 1)); + sample_00 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(sample_00); + sample_11 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(sample_11); + FFX_CACAO_Prepare_StoreDepth(tid, 0, sample_00); + FFX_CACAO_Prepare_StoreDepth(tid, 3, sample_11); } -groupshared float s_PrepareDepthsNormalsAndMipsBuffer[18][18]; - -RWTexture2DArray<float4> g_PrepareNormals_NormalOut : register(u0); - -struct PrepareNormalsInputDepths +struct FFX_CACAO_PrepareNormalsInputDepths { float depth_10; float depth_20; @@ -1512,148 +1198,136 @@ struct PrepareNormalsInputDepths float depth_23; }; -void PrepareNormals(PrepareNormalsInputDepths depths, float2 uv, float2 pixelSize, int2 normalCoord) +void FFX_CACAO_PrepareNormals(FFX_CACAO_PrepareNormalsInputDepths depths, float2 uv, float2 pixelSize, int2 normalCoord) { - float3 p_10 = NDCToViewspace(uv + float2(+0.0f, -1.0f) * pixelSize, depths.depth_10); - float3 p_20 = NDCToViewspace(uv + float2(+1.0f, -1.0f) * pixelSize, depths.depth_20); + float3 p_10 = FFX_CACAO_NDCToViewSpace(uv + float2(+0.0f, -1.0f) * pixelSize, depths.depth_10); + float3 p_20 = FFX_CACAO_NDCToViewSpace(uv + float2(+1.0f, -1.0f) * pixelSize, depths.depth_20); - float3 p_01 = NDCToViewspace(uv + float2(-1.0f, +0.0f) * pixelSize, depths.depth_01); - float3 p_11 = NDCToViewspace(uv + float2(+0.0f, +0.0f) * pixelSize, depths.depth_11); - float3 p_21 = NDCToViewspace(uv + float2(+1.0f, +0.0f) * pixelSize, depths.depth_21); - float3 p_31 = NDCToViewspace(uv + float2(+2.0f, +0.0f) * pixelSize, depths.depth_31); + float3 p_01 = FFX_CACAO_NDCToViewSpace(uv + float2(-1.0f, +0.0f) * pixelSize, depths.depth_01); + float3 p_11 = FFX_CACAO_NDCToViewSpace(uv + float2(+0.0f, +0.0f) * pixelSize, depths.depth_11); + float3 p_21 = FFX_CACAO_NDCToViewSpace(uv + float2(+1.0f, +0.0f) * pixelSize, depths.depth_21); + float3 p_31 = FFX_CACAO_NDCToViewSpace(uv + float2(+2.0f, +0.0f) * pixelSize, depths.depth_31); - float3 p_02 = NDCToViewspace(uv + float2(-1.0f, +1.0f) * pixelSize, depths.depth_02); - float3 p_12 = NDCToViewspace(uv + float2(+0.0f, +1.0f) * pixelSize, depths.depth_12); - float3 p_22 = NDCToViewspace(uv + float2(+1.0f, +1.0f) * pixelSize, depths.depth_22); - float3 p_32 = NDCToViewspace(uv + float2(+2.0f, +1.0f) * pixelSize, depths.depth_32); + float3 p_02 = FFX_CACAO_NDCToViewSpace(uv + float2(-1.0f, +1.0f) * pixelSize, depths.depth_02); + float3 p_12 = FFX_CACAO_NDCToViewSpace(uv + float2(+0.0f, +1.0f) * pixelSize, depths.depth_12); + float3 p_22 = FFX_CACAO_NDCToViewSpace(uv + float2(+1.0f, +1.0f) * pixelSize, depths.depth_22); + float3 p_32 = FFX_CACAO_NDCToViewSpace(uv + float2(+2.0f, +1.0f) * pixelSize, depths.depth_32); - float3 p_13 = NDCToViewspace(uv + float2(+0.0f, +2.0f) * pixelSize, depths.depth_13); - float3 p_23 = NDCToViewspace(uv + float2(+1.0f, +2.0f) * pixelSize, depths.depth_23); + float3 p_13 = FFX_CACAO_NDCToViewSpace(uv + float2(+0.0f, +2.0f) * pixelSize, depths.depth_13); + float3 p_23 = FFX_CACAO_NDCToViewSpace(uv + float2(+1.0f, +2.0f) * pixelSize, depths.depth_23); - float4 edges_11 = CalculateEdges(p_11.z, p_01.z, p_21.z, p_10.z, p_12.z); - float4 edges_21 = CalculateEdges(p_21.z, p_11.z, p_31.z, p_20.z, p_22.z); - float4 edges_12 = CalculateEdges(p_12.z, p_02.z, p_22.z, p_11.z, p_13.z); - float4 edges_22 = CalculateEdges(p_22.z, p_12.z, p_32.z, p_21.z, p_23.z); + float4 edges_11 = FFX_CACAO_CalculateEdges(p_11.z, p_01.z, p_21.z, p_10.z, p_12.z); + float4 edges_21 = FFX_CACAO_CalculateEdges(p_21.z, p_11.z, p_31.z, p_20.z, p_22.z); + float4 edges_12 = FFX_CACAO_CalculateEdges(p_12.z, p_02.z, p_22.z, p_11.z, p_13.z); + float4 edges_22 = FFX_CACAO_CalculateEdges(p_22.z, p_12.z, p_32.z, p_21.z, p_23.z); - float3 norm_11 = CalculateNormal(edges_11, p_11, p_01, p_21, p_10, p_12); - float3 norm_21 = CalculateNormal(edges_21, p_21, p_11, p_31, p_20, p_22); - float3 norm_12 = CalculateNormal(edges_12, p_12, p_02, p_22, p_11, p_13); - float3 norm_22 = CalculateNormal(edges_22, p_22, p_12, p_32, p_21, p_23); + float3 norm_11 = FFX_CACAO_CalculateNormal(edges_11, p_11, p_01, p_21, p_10, p_12); + float3 norm_21 = FFX_CACAO_CalculateNormal(edges_21, p_21, p_11, p_31, p_20, p_22); + float3 norm_12 = FFX_CACAO_CalculateNormal(edges_12, p_12, p_02, p_22, p_11, p_13); + float3 norm_22 = FFX_CACAO_CalculateNormal(edges_22, p_22, p_12, p_32, p_21, p_23); - g_PrepareNormals_NormalOut[int3(normalCoord, 0)] = float4(norm_11, 1.0f); - g_PrepareNormals_NormalOut[int3(normalCoord, 1)] = float4(norm_21, 1.0f); - g_PrepareNormals_NormalOut[int3(normalCoord, 2)] = float4(norm_12, 1.0f); - g_PrepareNormals_NormalOut[int3(normalCoord, 3)] = float4(norm_22, 1.0f); + FFX_CACAO_Prepare_StoreNormal(normalCoord, 0, norm_11); + FFX_CACAO_Prepare_StoreNormal(normalCoord, 1, norm_21); + FFX_CACAO_Prepare_StoreNormal(normalCoord, 2, norm_12); + FFX_CACAO_Prepare_StoreNormal(normalCoord, 3, norm_22); } -[numthreads(PREPARE_NORMALS_WIDTH, PREPARE_NORMALS_HEIGHT, 1)] -void CSPrepareDownsampledNormals(int2 tid : SV_DispatchThreadID) +[numthreads(FFX_CACAO_PREPARE_NORMALS_WIDTH, FFX_CACAO_PREPARE_NORMALS_HEIGHT, 1)] +void FFX_CACAO_PrepareDownsampledNormals(int2 tid : SV_DispatchThreadID) { - int2 depthCoord = 4 * tid + g_CACAOConsts.DepthBufferOffset; + int2 depthCoord = 4 * tid + g_FFX_CACAO_Consts.DepthBufferOffset; - PrepareNormalsInputDepths depths; + FFX_CACAO_PrepareNormalsInputDepths depths; - depths.depth_10 = ScreenSpaceToViewSpaceDepth(g_DepthIn.Load(int3(depthCoord, 0), int2(+0, -2))); - depths.depth_20 = ScreenSpaceToViewSpaceDepth(g_DepthIn.Load(int3(depthCoord, 0), int2(+2, -2))); + depths.depth_10 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_Prepare_LoadDepthOffset(depthCoord, int2(+0, -2))); + depths.depth_20 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_Prepare_LoadDepthOffset(depthCoord, int2(+2, -2))); - depths.depth_01 = ScreenSpaceToViewSpaceDepth(g_DepthIn.Load(int3(depthCoord, 0), int2(-2, +0))); - depths.depth_11 = ScreenSpaceToViewSpaceDepth(g_DepthIn.Load(int3(depthCoord, 0), int2(+0, +0))); - depths.depth_21 = ScreenSpaceToViewSpaceDepth(g_DepthIn.Load(int3(depthCoord, 0), int2(+2, +0))); - depths.depth_31 = ScreenSpaceToViewSpaceDepth(g_DepthIn.Load(int3(depthCoord, 0), int2(+4, +0))); + depths.depth_01 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_Prepare_LoadDepthOffset(depthCoord, int2(-2, +0))); + depths.depth_11 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_Prepare_LoadDepthOffset(depthCoord, int2(+0, +0))); + depths.depth_21 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_Prepare_LoadDepthOffset(depthCoord, int2(+2, +0))); + depths.depth_31 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_Prepare_LoadDepthOffset(depthCoord, int2(+4, +0))); - depths.depth_02 = ScreenSpaceToViewSpaceDepth(g_DepthIn.Load(int3(depthCoord, 0), int2(-2, +2))); - depths.depth_12 = ScreenSpaceToViewSpaceDepth(g_DepthIn.Load(int3(depthCoord, 0), int2(+0, +2))); - depths.depth_22 = ScreenSpaceToViewSpaceDepth(g_DepthIn.Load(int3(depthCoord, 0), int2(+2, +2))); - depths.depth_32 = ScreenSpaceToViewSpaceDepth(g_DepthIn.Load(int3(depthCoord, 0), int2(+4, +2))); + depths.depth_02 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_Prepare_LoadDepthOffset(depthCoord, int2(-2, +2))); + depths.depth_12 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_Prepare_LoadDepthOffset(depthCoord, int2(+0, +2))); + depths.depth_22 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_Prepare_LoadDepthOffset(depthCoord, int2(+2, +2))); + depths.depth_32 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_Prepare_LoadDepthOffset(depthCoord, int2(+4, +2))); - depths.depth_13 = ScreenSpaceToViewSpaceDepth(g_DepthIn.Load(int3(depthCoord, 0), int2(+0, +4))); - depths.depth_23 = ScreenSpaceToViewSpaceDepth(g_DepthIn.Load(int3(depthCoord, 0), int2(+2, +4))); + depths.depth_13 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_Prepare_LoadDepthOffset(depthCoord, int2(+0, +4))); + depths.depth_23 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_Prepare_LoadDepthOffset(depthCoord, int2(+2, +4))); - float2 pixelSize = 2.0f * g_CACAOConsts.OutputBufferInverseDimensions; // 2.0f * g_CACAOConsts.DepthBufferInverseDimensions; - float2 uv = (float2(4 * tid) + 0.5f) * g_CACAOConsts.OutputBufferInverseDimensions; // * g_CACAOConsts.SSAOBufferInverseDimensions; + float2 pixelSize = 2.0f * g_FFX_CACAO_Consts.OutputBufferInverseDimensions; // 2.0f * g_FFX_CACAO_Consts.DepthBufferInverseDimensions; + float2 uv = (float2(4 * tid) + 0.5f) * g_FFX_CACAO_Consts.OutputBufferInverseDimensions; // * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; - PrepareNormals(depths, uv, pixelSize, tid); + FFX_CACAO_PrepareNormals(depths, uv, pixelSize, tid); } -[numthreads(PREPARE_NORMALS_WIDTH, PREPARE_NORMALS_HEIGHT, 1)] -void CSPrepareNativeNormals(int2 tid : SV_DispatchThreadID) +[numthreads(FFX_CACAO_PREPARE_NORMALS_WIDTH, FFX_CACAO_PREPARE_NORMALS_HEIGHT, 1)] +void FFX_CACAO_PrepareNativeNormals(int2 tid : SV_DispatchThreadID) { - int2 depthCoord = 2 * tid + g_CACAOConsts.DepthBufferOffset; - float2 depthBufferUV = (float2(depthCoord)-0.5f) * g_CACAOConsts.DepthBufferInverseDimensions; - float4 samples_00 = g_DepthIn.GatherRed(g_PointClampSampler, depthBufferUV, int2(0, 0)); - float4 samples_10 = g_DepthIn.GatherRed(g_PointClampSampler, depthBufferUV, int2(2, 0)); - float4 samples_01 = g_DepthIn.GatherRed(g_PointClampSampler, depthBufferUV, int2(0, 2)); - float4 samples_11 = g_DepthIn.GatherRed(g_PointClampSampler, depthBufferUV, int2(2, 2)); + int2 depthCoord = 2 * tid + g_FFX_CACAO_Consts.DepthBufferOffset; + float2 depthBufferUV = (float2(depthCoord)-0.5f) * g_FFX_CACAO_Consts.DepthBufferInverseDimensions; + float4 samples_00 = FFX_CACAO_Prepare_GatherDepthOffset(depthBufferUV, int2(0, 0)); + float4 samples_10 = FFX_CACAO_Prepare_GatherDepthOffset(depthBufferUV, int2(2, 0)); + float4 samples_01 = FFX_CACAO_Prepare_GatherDepthOffset(depthBufferUV, int2(0, 2)); + float4 samples_11 = FFX_CACAO_Prepare_GatherDepthOffset(depthBufferUV, int2(2, 2)); - PrepareNormalsInputDepths depths; + FFX_CACAO_PrepareNormalsInputDepths depths; - depths.depth_10 = ScreenSpaceToViewSpaceDepth(samples_00.z); - depths.depth_20 = ScreenSpaceToViewSpaceDepth(samples_10.w); + depths.depth_10 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(samples_00.z); + depths.depth_20 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(samples_10.w); - depths.depth_01 = ScreenSpaceToViewSpaceDepth(samples_00.x); - depths.depth_11 = ScreenSpaceToViewSpaceDepth(samples_00.y); - depths.depth_21 = ScreenSpaceToViewSpaceDepth(samples_10.x); - depths.depth_31 = ScreenSpaceToViewSpaceDepth(samples_10.y); + depths.depth_01 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(samples_00.x); + depths.depth_11 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(samples_00.y); + depths.depth_21 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(samples_10.x); + depths.depth_31 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(samples_10.y); - depths.depth_02 = ScreenSpaceToViewSpaceDepth(samples_01.w); - depths.depth_12 = ScreenSpaceToViewSpaceDepth(samples_01.z); - depths.depth_22 = ScreenSpaceToViewSpaceDepth(samples_11.w); - depths.depth_32 = ScreenSpaceToViewSpaceDepth(samples_11.z); + depths.depth_02 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(samples_01.w); + depths.depth_12 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(samples_01.z); + depths.depth_22 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(samples_11.w); + depths.depth_32 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(samples_11.z); - depths.depth_13 = ScreenSpaceToViewSpaceDepth(samples_01.y); - depths.depth_23 = ScreenSpaceToViewSpaceDepth(samples_11.x); + depths.depth_13 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(samples_01.y); + depths.depth_23 = FFX_CACAO_ScreenSpaceToViewSpaceDepth(samples_11.x); // use unused samples to make sure compiler doesn't overlap memory and put a sync // between loads float epsilon = (samples_00.w + samples_10.z + samples_01.x + samples_11.y) * 1e-20f; - float2 pixelSize = g_CACAOConsts.OutputBufferInverseDimensions; - float2 uv = (float2(2 * tid) + 0.5f + epsilon) * g_CACAOConsts.OutputBufferInverseDimensions; - - PrepareNormals(depths, uv, pixelSize, tid); -} - -Texture2D<float4> g_PrepareNormalsFromNormalsInput : register(t0); -RWTexture2DArray<float4> g_PrepareNormalsFromNormalsOutput : register(u0); + float2 pixelSize = g_FFX_CACAO_Consts.OutputBufferInverseDimensions; + float2 uv = (float2(2 * tid) + 0.5f + epsilon) * g_FFX_CACAO_Consts.OutputBufferInverseDimensions; -float3 PrepareNormalsFromInputNormalsLoadNormal(int2 pos) -{ - float3 encodedNormal = g_PrepareNormalsFromNormalsInput.SampleLevel(g_PointClampSampler, (float2(pos)+0.5f) * g_CACAOConsts.OutputBufferInverseDimensions, 0).xyz; - return DecodeNormal(encodedNormal); + FFX_CACAO_PrepareNormals(depths, uv, pixelSize, tid); } [numthreads(PREPARE_NORMALS_FROM_INPUT_NORMALS_WIDTH, PREPARE_NORMALS_FROM_INPUT_NORMALS_HEIGHT, 1)] -void CSPrepareDownsampledNormalsFromInputNormals(int2 tid : SV_DispatchThreadID) +void FFX_CACAO_PrepareDownsampledNormalsFromInputNormals(int2 tid : SV_DispatchThreadID) { int2 baseCoord = 4 * tid; - g_PrepareNormalsFromNormalsOutput[uint3(tid, 0)] = float4(PrepareNormalsFromInputNormalsLoadNormal(baseCoord + int2(0, 0)), 1.0f); - g_PrepareNormalsFromNormalsOutput[uint3(tid, 1)] = float4(PrepareNormalsFromInputNormalsLoadNormal(baseCoord + int2(2, 0)), 1.0f); - g_PrepareNormalsFromNormalsOutput[uint3(tid, 2)] = float4(PrepareNormalsFromInputNormalsLoadNormal(baseCoord + int2(0, 2)), 1.0f); - g_PrepareNormalsFromNormalsOutput[uint3(tid, 3)] = float4(PrepareNormalsFromInputNormalsLoadNormal(baseCoord + int2(2, 2)), 1.0f); + FFX_CACAO_Prepare_StoreNormal(tid, 0, FFX_CACAO_Prepare_LoadNormal(baseCoord + int2(0, 0))); + FFX_CACAO_Prepare_StoreNormal(tid, 1, FFX_CACAO_Prepare_LoadNormal(baseCoord + int2(2, 0))); + FFX_CACAO_Prepare_StoreNormal(tid, 2, FFX_CACAO_Prepare_LoadNormal(baseCoord + int2(0, 2))); + FFX_CACAO_Prepare_StoreNormal(tid, 3, FFX_CACAO_Prepare_LoadNormal(baseCoord + int2(2, 2))); } [numthreads(PREPARE_NORMALS_FROM_INPUT_NORMALS_WIDTH, PREPARE_NORMALS_FROM_INPUT_NORMALS_HEIGHT, 1)] -void CSPrepareNativeNormalsFromInputNormals(int2 tid : SV_DispatchThreadID) +void FFX_CACAO_PrepareNativeNormalsFromInputNormals(int2 tid : SV_DispatchThreadID) { int2 baseCoord = 2 * tid; - g_PrepareNormalsFromNormalsOutput[uint3(tid, 0)] = float4(PrepareNormalsFromInputNormalsLoadNormal(baseCoord + int2(0, 0)), 1.0f); - g_PrepareNormalsFromNormalsOutput[uint3(tid, 1)] = float4(PrepareNormalsFromInputNormalsLoadNormal(baseCoord + int2(1, 0)), 1.0f); - g_PrepareNormalsFromNormalsOutput[uint3(tid, 2)] = float4(PrepareNormalsFromInputNormalsLoadNormal(baseCoord + int2(0, 1)), 1.0f); - g_PrepareNormalsFromNormalsOutput[uint3(tid, 3)] = float4(PrepareNormalsFromInputNormalsLoadNormal(baseCoord + int2(1, 1)), 1.0f); + FFX_CACAO_Prepare_StoreNormal(tid, 0, FFX_CACAO_Prepare_LoadNormal(baseCoord + int2(0, 0))); + FFX_CACAO_Prepare_StoreNormal(tid, 1, FFX_CACAO_Prepare_LoadNormal(baseCoord + int2(1, 0))); + FFX_CACAO_Prepare_StoreNormal(tid, 2, FFX_CACAO_Prepare_LoadNormal(baseCoord + int2(0, 1))); + FFX_CACAO_Prepare_StoreNormal(tid, 3, FFX_CACAO_Prepare_LoadNormal(baseCoord + int2(1, 1))); } -// ====================================================================================== -// importance map stuff - -Texture2DArray<float2> g_ImportanceFinalSSAO : register(t0); -RWTexture2D<float> g_ImportanceOut : register(u0); +// ============================================================================= +// Importance Map [numthreads(IMPORTANCE_MAP_WIDTH, IMPORTANCE_MAP_HEIGHT, 1)] -void CSGenerateImportanceMap(uint2 tid : SV_DispatchThreadID) +void FFX_CACAO_GenerateImportanceMap(uint2 tid : SV_DispatchThreadID) { uint2 basePos = tid * 2; - float2 baseUV = (float2(basePos)+float2(0.5f, 0.5f)) * g_CACAOConsts.SSAOBufferInverseDimensions; + float2 baseUV = (float2(basePos)+float2(0.5f, 0.5f)) * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; float avg = 0.0; float minV = 1.0; @@ -1661,14 +1335,14 @@ void CSGenerateImportanceMap(uint2 tid : SV_DispatchThreadID) [unroll] for (int i = 0; i < 4; i++) { - float4 vals = g_ImportanceFinalSSAO.GatherRed(g_PointClampSampler, float3(baseUV, i)); + float4 vals = FFX_CACAO_Importance_GatherSSAO(baseUV, i); // apply the same modifications that would have been applied in the main shader - vals = g_CACAOConsts.EffectShadowStrength * vals; + vals = g_FFX_CACAO_Consts.EffectShadowStrength * vals; vals = 1 - vals; - vals = pow(saturate(vals), g_CACAOConsts.EffectShadowPow); + vals = pow(saturate(vals), g_FFX_CACAO_Consts.EffectShadowPow); avg += dot(float4(vals.x, vals.y, vals.z, vals.w), float4(1.0 / 16.0, 1.0 / 16.0, 1.0 / 16.0, 1.0 / 16.0)); @@ -1678,129 +1352,215 @@ void CSGenerateImportanceMap(uint2 tid : SV_DispatchThreadID) float minMaxDiff = maxV - minV; - g_ImportanceOut[tid] = pow(saturate(minMaxDiff * 2.0), 0.8); + FFX_CACAO_Importance_StoreImportance(tid, pow(saturate(minMaxDiff * 2.0), 0.8)); } -Texture2D<float> g_ImportanceAIn : register(t0); -RWTexture2D<float> g_ImportanceAOut : register(u0); - -static const float cSmoothenImportance = 1.0; +static const float c_FFX_CACAO_SmoothenImportance = 1.0f; [numthreads(IMPORTANCE_MAP_A_WIDTH, IMPORTANCE_MAP_A_HEIGHT, 1)] -void CSPostprocessImportanceMapA(uint2 tid : SV_DispatchThreadID) +void FFX_CACAO_PostprocessImportanceMapA(uint2 tid : SV_DispatchThreadID) { - float2 uv = (float2(tid)+0.5f) * g_CACAOConsts.ImportanceMapInverseDimensions; + float2 uv = (float2(tid)+0.5f) * g_FFX_CACAO_Consts.ImportanceMapInverseDimensions; - float centre = g_ImportanceAIn.SampleLevel(g_LinearClampSampler, uv, 0.0).x; + float centre = FFX_CACAO_Importance_SampleImportanceA(uv); //return centre; - float2 halfPixel = 0.5f * g_CACAOConsts.ImportanceMapInverseDimensions; + float2 halfPixel = 0.5f * g_FFX_CACAO_Consts.ImportanceMapInverseDimensions; float4 vals; - vals.x = g_ImportanceAIn.SampleLevel(g_LinearClampSampler, uv + float2(-halfPixel.x * 3, -halfPixel.y), 0.0).x; - vals.y = g_ImportanceAIn.SampleLevel(g_LinearClampSampler, uv + float2(+halfPixel.x, -halfPixel.y * 3), 0.0).x; - vals.z = g_ImportanceAIn.SampleLevel(g_LinearClampSampler, uv + float2(+halfPixel.x * 3, +halfPixel.y), 0.0).x; - vals.w = g_ImportanceAIn.SampleLevel(g_LinearClampSampler, uv + float2(-halfPixel.x, +halfPixel.y * 3), 0.0).x; + vals.x = FFX_CACAO_Importance_SampleImportanceA(uv + float2(-halfPixel.x * 3, -halfPixel.y)); + vals.y = FFX_CACAO_Importance_SampleImportanceA(uv + float2(+halfPixel.x, -halfPixel.y * 3)); + vals.z = FFX_CACAO_Importance_SampleImportanceA(uv + float2(+halfPixel.x * 3, +halfPixel.y)); + vals.w = FFX_CACAO_Importance_SampleImportanceA(uv + float2(-halfPixel.x, +halfPixel.y * 3)); float avgVal = dot(vals, float4(0.25, 0.25, 0.25, 0.25)); vals.xy = max(vals.xy, vals.zw); float maxVal = max(centre, max(vals.x, vals.y)); - g_ImportanceAOut[tid] = lerp(maxVal, avgVal, cSmoothenImportance); + FFX_CACAO_Importance_StoreImportanceA(tid, lerp(maxVal, avgVal, c_FFX_CACAO_SmoothenImportance)); } -Texture2D<float> g_ImportanceBIn : register(t0); -RWTexture2D<float> g_ImportanceBOut : register(u0); -RWTexture1D<uint> g_ImportanceBLoadCounter : register(u1); - [numthreads(IMPORTANCE_MAP_B_WIDTH, IMPORTANCE_MAP_B_HEIGHT, 1)] -void CSPostprocessImportanceMapB(uint2 tid : SV_DispatchThreadID) +void FFX_CACAO_PostprocessImportanceMapB(uint2 tid : SV_DispatchThreadID) { - float2 uv = (float2(tid)+0.5f) * g_CACAOConsts.ImportanceMapInverseDimensions; + float2 uv = (float2(tid)+0.5f) * g_FFX_CACAO_Consts.ImportanceMapInverseDimensions; - float centre = g_ImportanceBIn.SampleLevel(g_LinearClampSampler, uv, 0.0).x; + float centre = FFX_CACAO_Importance_SampleImportanceB(uv); //return centre; - float2 halfPixel = 0.5f * g_CACAOConsts.ImportanceMapInverseDimensions; + float2 halfPixel = 0.5f * g_FFX_CACAO_Consts.ImportanceMapInverseDimensions; float4 vals; - vals.x = g_ImportanceBIn.SampleLevel(g_LinearClampSampler, uv + float2(-halfPixel.x, -halfPixel.y * 3), 0.0).x; - vals.y = g_ImportanceBIn.SampleLevel(g_LinearClampSampler, uv + float2(+halfPixel.x * 3, -halfPixel.y), 0.0).x; - vals.z = g_ImportanceBIn.SampleLevel(g_LinearClampSampler, uv + float2(+halfPixel.x, +halfPixel.y * 3), 0.0).x; - vals.w = g_ImportanceBIn.SampleLevel(g_LinearClampSampler, uv + float2(-halfPixel.x * 3, +halfPixel.y), 0.0).x; + vals.x = FFX_CACAO_Importance_SampleImportanceB(uv + float2(-halfPixel.x, -halfPixel.y * 3)); + vals.y = FFX_CACAO_Importance_SampleImportanceB(uv + float2(+halfPixel.x * 3, -halfPixel.y)); + vals.z = FFX_CACAO_Importance_SampleImportanceB(uv + float2(+halfPixel.x, +halfPixel.y * 3)); + vals.w = FFX_CACAO_Importance_SampleImportanceB(uv + float2(-halfPixel.x * 3, +halfPixel.y)); float avgVal = dot(vals, float4(0.25, 0.25, 0.25, 0.25)); vals.xy = max(vals.xy, vals.zw); float maxVal = max(centre, max(vals.x, vals.y)); - float retVal = lerp(maxVal, avgVal, cSmoothenImportance); - g_ImportanceBOut[tid] = retVal; + float retVal = lerp(maxVal, avgVal, c_FFX_CACAO_SmoothenImportance); + FFX_CACAO_Importance_StoreImportanceB(tid, retVal); - // sum the average; to avoid overflowing we assume max AO resolution is not bigger than 16384x16384; so quarter res (used here) will be 4096x4096, which leaves us with 8 bits per pixel + // sum the average; to avoid overflowing we assume max AO resolution is not bigger than 16384x16384; so quarter res (used here) will be 4096x4096, which leaves us with 8 bits per pixel uint sum = (uint)(saturate(retVal) * 255.0 + 0.5); // save every 9th to avoid InterlockedAdd congestion - since we're blurring, this is good enough; compensated by multiplying LoadCounterAvgDiv by 9 if (((tid.x % 3) + (tid.y % 3)) == 0) { - InterlockedAdd(g_ImportanceBLoadCounter[0], sum); + FFX_CACAO_Importance_LoadCounterInterlockedAdd(sum); } } -// ============================================================================================ -// bilateral upscale - -RWTexture2D<float> g_BilateralUpscaleOutput : register(u0); - -Texture2DArray<float2> g_BilateralUpscaleInput : register(t0); - -Texture2D<float> g_BilateralUpscaleDepth : register(t1); -Texture2DArray<float> g_BilateralUpscaleDownscaledDepth : register(t3); - +// ============================================================================= +// Bilateral Upscale -uint DoublePackFloat16(float v) +uint FFX_CACAO_DoublePackFloat16(float v) { uint2 p = f32tof16(float2(v, v)); return p.x | (p.y << 16); } -#define BILATERAL_UPSCALE_BUFFER_WIDTH (BILATERAL_UPSCALE_WIDTH + 4) -#define BILATERAL_UPSCALE_BUFFER_HEIGHT (BILATERAL_UPSCALE_HEIGHT + 4 + 4) +#define FFX_CACAO_BILATERAL_UPSCALE_BUFFER_WIDTH (FFX_CACAO_BILATERAL_UPSCALE_WIDTH + 4) +#define FFX_CACAO_BILATERAL_UPSCALE_BUFFER_HEIGHT (FFX_CACAO_BILATERAL_UPSCALE_HEIGHT + 4 + 4) -struct BilateralBufferVal +struct FFX_CACAO_BilateralBufferVal { - // float depth; - // float ssaoVal; uint packedDepths; uint packedSsaoVals; }; -groupshared BilateralBufferVal s_BilateralUpscaleBuffer[BILATERAL_UPSCALE_BUFFER_WIDTH][BILATERAL_UPSCALE_BUFFER_HEIGHT]; +groupshared FFX_CACAO_BilateralBufferVal s_FFX_CACAO_BilateralUpscaleBuffer[FFX_CACAO_BILATERAL_UPSCALE_BUFFER_WIDTH][FFX_CACAO_BILATERAL_UPSCALE_BUFFER_HEIGHT]; -void BilateralUpscaleNxN(int2 tid, uint2 gtid, uint2 gid, const int width, const int height) +void FFX_CACAO_BilateralUpscaleNxN(int2 tid, uint2 gtid, uint2 gid, const int width, const int height, const bool useEdges) { // fill in group shared buffer { - uint threadNum = (gtid.y * BILATERAL_UPSCALE_WIDTH + gtid.x) * 3; - uint2 bufferCoord = uint2(threadNum % BILATERAL_UPSCALE_BUFFER_WIDTH, threadNum / BILATERAL_UPSCALE_BUFFER_WIDTH); - uint2 imageCoord = (gid * uint2(BILATERAL_UPSCALE_WIDTH, BILATERAL_UPSCALE_HEIGHT)) + bufferCoord - 2; + uint threadNum = (gtid.y * FFX_CACAO_BILATERAL_UPSCALE_WIDTH + gtid.x) * 3; + uint2 bufferCoord = uint2(threadNum % FFX_CACAO_BILATERAL_UPSCALE_BUFFER_WIDTH, threadNum / FFX_CACAO_BILATERAL_UPSCALE_BUFFER_WIDTH); + uint2 imageCoord = (gid * uint2(FFX_CACAO_BILATERAL_UPSCALE_WIDTH, FFX_CACAO_BILATERAL_UPSCALE_HEIGHT)) + bufferCoord - 2; - for (int i = 0; i < 3; ++i) + if (useEdges) { - // uint2 depthBufferCoord = imageCoord + 2 * g_CACAOConsts.DeinterleavedDepthBufferOffset; - // uint3 depthArrayBufferCoord = uint3(depthBufferCoord / 2, 2 * (depthBufferCoord.y % 2) + depthBufferCoord.x % 2); - uint3 ssaoArrayBufferCoord = uint3(imageCoord / 2, 2 * (imageCoord.y % 2) + imageCoord.x % 2); - uint3 depthArrayBufferCoord = ssaoArrayBufferCoord + uint3(g_CACAOConsts.DeinterleavedDepthBufferOffset, 0); - ++imageCoord.x; + float2 inputs[3]; + for (int j = 0; j < 3; ++j) + { + int2 p = int2(imageCoord.x + j, imageCoord.y); + int2 pos = p / 2; + int index = (p.x % 2) + 2 * (p.y % 2); + inputs[j] = FFX_CACAO_BilateralUpscale_LoadSSAO(pos, index); + } + + for (int i = 0; i < 3; ++i) + { + int mx = (imageCoord.x % 2); + int my = (imageCoord.y % 2); + + int ic = mx + my * 2; // center index + int ih = (1 - mx) + my * 2; // neighbouring, horizontal + int iv = mx + (1 - my) * 2; // neighbouring, vertical + int id = (1 - mx) + (1 - my) * 2; // diagonal + + float2 centerVal = inputs[i]; + + float ao = centerVal.x; + + float4 edgesLRTB = FFX_CACAO_UnpackEdges(centerVal.y); + + // convert index shifts to sampling offsets + float fmx = (float)mx; + float fmy = (float)my; + + // in case of an edge, push sampling offsets away from the edge (towards pixel center) + float fmxe = (edgesLRTB.y - edgesLRTB.x); + float fmye = (edgesLRTB.w - edgesLRTB.z); + + // calculate final sampling offsets and sample using bilinear filter + float2 p = imageCoord; + float2 uvH = (p + float2(fmx + fmxe - 0.5, 0.5 - fmy)) * 0.5 * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; + float aoH = FFX_CACAO_BilateralUpscale_SampleSSAOLinear(uvH, ih); + float2 uvV = (p + float2(0.5 - fmx, fmy - 0.5 + fmye)) * 0.5 * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; + float aoV = FFX_CACAO_BilateralUpscale_SampleSSAOLinear(uvV, iv); + float2 uvD = (p + float2(fmx - 0.5 + fmxe, fmy - 0.5 + fmye)) * 0.5 * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; + float aoD = FFX_CACAO_BilateralUpscale_SampleSSAOLinear(uvD, id); + + // reduce weight for samples near edge - if the edge is on both sides, weight goes to 0 + float4 blendWeights; + blendWeights.x = 1.0; + blendWeights.y = (edgesLRTB.x + edgesLRTB.y) * 0.5; + blendWeights.z = (edgesLRTB.z + edgesLRTB.w) * 0.5; + blendWeights.w = (blendWeights.y + blendWeights.z) * 0.5; + + // calculate weighted average + float blendWeightsSum = dot(blendWeights, float4(1.0, 1.0, 1.0, 1.0)); + ao = dot(float4(ao, aoH, aoV, aoD), blendWeights) / blendWeightsSum; + + ++imageCoord.x; - BilateralBufferVal bufferVal; + FFX_CACAO_BilateralBufferVal bufferVal; - float depth = g_BilateralUpscaleDownscaledDepth[depthArrayBufferCoord]; - float ssaoVal = g_BilateralUpscaleInput.SampleLevel(g_PointClampSampler, float3((float2(ssaoArrayBufferCoord.xy) + 0.5f) * g_CACAOConsts.SSAOBufferInverseDimensions, ssaoArrayBufferCoord.z), 0).x; + uint2 depthArrayBufferCoord = (imageCoord / 2) + g_FFX_CACAO_Consts.DeinterleavedDepthBufferOffset; + uint depthArrayBufferIndex = ic; + float depth = FFX_CACAO_BilateralUpscale_LoadDownscaledDepth(depthArrayBufferCoord, depthArrayBufferIndex); - bufferVal.packedDepths = DoublePackFloat16(depth); - bufferVal.packedSsaoVals = DoublePackFloat16(ssaoVal); + bufferVal.packedDepths = FFX_CACAO_DoublePackFloat16(depth); + bufferVal.packedSsaoVals = FFX_CACAO_DoublePackFloat16(ao); - s_BilateralUpscaleBuffer[bufferCoord.x + i][bufferCoord.y] = bufferVal; + s_FFX_CACAO_BilateralUpscaleBuffer[bufferCoord.x + i][bufferCoord.y] = bufferVal; + } + } + else + { + for (int i = 0; i < 3; ++i) + { + float2 sampleLoc0 = (float2(imageCoord / 2) + 0.5f) * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; + float2 sampleLoc1 = sampleLoc0; + float2 sampleLoc2 = sampleLoc0; + float2 sampleLoc3 = sampleLoc0; + switch ((imageCoord.y % 2) * 2 + (imageCoord.x % 2)) { + case 0: + sampleLoc1.x -= 0.5f * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions.x; + sampleLoc2.y -= 0.5f * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions.y; + sampleLoc3 -= 0.5f * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; + break; + case 1: + sampleLoc0.x += 0.5f * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions.x; + sampleLoc2 += float2(0.5f, -0.5f) * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; + sampleLoc3.y -= 0.5f * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions.y; + break; + case 2: + sampleLoc0.y += 0.5f * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions.y; + sampleLoc1 += float2(-0.5f, 0.5f) * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; + sampleLoc3.x -= 0.5f * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions.x; + break; + case 3: + sampleLoc0 += 0.5f * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; + sampleLoc1.y += 0.5f * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions.y; + sampleLoc2.x += 0.5f * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions.x; + break; + } + + float ssaoVal0 = FFX_CACAO_BilateralUpscale_SampleSSAOPoint(sampleLoc0, 0); + float ssaoVal1 = FFX_CACAO_BilateralUpscale_SampleSSAOPoint(sampleLoc1, 1); + float ssaoVal2 = FFX_CACAO_BilateralUpscale_SampleSSAOPoint(sampleLoc2, 2); + float ssaoVal3 = FFX_CACAO_BilateralUpscale_SampleSSAOPoint(sampleLoc3, 3); + + uint3 ssaoArrayBufferCoord = uint3(imageCoord / 2, 2 * (imageCoord.y % 2) + imageCoord.x % 2); + uint2 depthArrayBufferCoord = ssaoArrayBufferCoord.xy + g_FFX_CACAO_Consts.DeinterleavedDepthBufferOffset; + uint depthArrayBufferIndex = ssaoArrayBufferCoord.z; + ++imageCoord.x; + + FFX_CACAO_BilateralBufferVal bufferVal; + + float depth = FFX_CACAO_BilateralUpscale_LoadDownscaledDepth(depthArrayBufferCoord, depthArrayBufferIndex); + float ssaoVal = (ssaoVal0 + ssaoVal1 + ssaoVal2 + ssaoVal3) * 0.25f; + + bufferVal.packedDepths = FFX_CACAO_DoublePackFloat16(depth); + bufferVal.packedSsaoVals = FFX_CACAO_DoublePackFloat16(ssaoVal); + + s_FFX_CACAO_BilateralUpscaleBuffer[bufferCoord.x + i][bufferCoord.y] = bufferVal; + } } } @@ -1810,27 +1570,27 @@ void BilateralUpscaleNxN(int2 tid, uint2 gtid, uint2 gid, const int width, const // load depths { int2 fullBufferCoord = 2 * tid; - int2 fullDepthBufferCoord = fullBufferCoord + g_CACAOConsts.DepthBufferOffset; + int2 fullDepthBufferCoord = fullBufferCoord + g_FFX_CACAO_Consts.DepthBufferOffset; - depths[0] = ScreenSpaceToViewSpaceDepth(g_BilateralUpscaleDepth[fullDepthBufferCoord + int2(0, 0)]); - depths[1] = ScreenSpaceToViewSpaceDepth(g_BilateralUpscaleDepth[fullDepthBufferCoord + int2(1, 0)]); - depths[2] = ScreenSpaceToViewSpaceDepth(g_BilateralUpscaleDepth[fullDepthBufferCoord + int2(0, 1)]); - depths[3] = ScreenSpaceToViewSpaceDepth(g_BilateralUpscaleDepth[fullDepthBufferCoord + int2(1, 1)]); + depths[0] = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_BilateralUpscale_LoadDepth(fullDepthBufferCoord, int2(0, 0))); + depths[1] = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_BilateralUpscale_LoadDepth(fullDepthBufferCoord, int2(1, 0))); + depths[2] = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_BilateralUpscale_LoadDepth(fullDepthBufferCoord, int2(0, 1))); + depths[3] = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_BilateralUpscale_LoadDepth(fullDepthBufferCoord, int2(1, 1))); } min16float4 packedDepths = min16float4(depths[0], depths[1], depths[2], depths[3]); - float totals[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; - float totalWeights[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; - float2 pps[] = { float2(0.0f, 0.0f), float2(0.5f, 0.0f), float2(0.0f, 0.5f), float2(0.5f, 0.5f) }; - - min16float4 packedTotals = min16float4(0.0f, 0.0f, 0.0f, 0.0f); - min16float4 packedTotalWeights = min16float4(0.0f, 0.0f, 0.0f, 0.0f); - int2 baseBufferCoord = gtid + int2(width, height); - float distanceSigma = g_CACAOConsts.BilateralSimilarityDistanceSigma; + min16float epsilonWeight = 1e-3f; + min16float2 nearestSsaoVals = FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BilateralUpscaleBuffer[baseBufferCoord.x][baseBufferCoord.y].packedSsaoVals); + min16float4 packedTotals = epsilonWeight * min16float4(1.0f, 1.0f, 1.0f, 1.0f); + packedTotals.xy *= nearestSsaoVals; + packedTotals.zw *= nearestSsaoVals; + min16float4 packedTotalWeights = epsilonWeight * min16float4(1.0f, 1.0f, 1.0f, 1.0f); + + float distanceSigma = g_FFX_CACAO_Consts.BilateralSimilarityDistanceSigma; min16float2 packedDistSigma = min16float2(1.0f / distanceSigma, 1.0f / distanceSigma); - float sigma = g_CACAOConsts.BilateralSigmaSquared; + float sigma = g_FFX_CACAO_Consts.BilateralSigmaSquared; min16float2 packedSigma = min16float2(1.0f / sigma, 1.0f / sigma); for (int x = -width; x <= width; ++x) @@ -1839,11 +1599,11 @@ void BilateralUpscaleNxN(int2 tid, uint2 gtid, uint2 gid, const int width, const { int2 bufferCoord = baseBufferCoord + int2(x, y); - BilateralBufferVal bufferVal = s_BilateralUpscaleBuffer[bufferCoord.x][bufferCoord.y]; + FFX_CACAO_BilateralBufferVal bufferVal = s_FFX_CACAO_BilateralUpscaleBuffer[bufferCoord.x][bufferCoord.y]; - min16float2 u = min16float2(x, x) + min16float2(0.0f, 0.5f); - min16float2 v1 = min16float2(y, y) + min16float2(0.0f, 0.0f); - min16float2 v2 = min16float2(y, y) + min16float2(0.5f, 0.5f); + min16float2 u = min16float2(x, x) - min16float2(0.0f, 0.5f); + min16float2 v1 = min16float2(y, y) - min16float2(0.0f, 0.0f); + min16float2 v2 = min16float2(y, y) - min16float2(0.5f, 0.5f); u = u * u; v1 = v1 * v1; v2 = v2 * v2; @@ -1854,7 +1614,7 @@ void BilateralUpscaleNxN(int2 tid, uint2 gtid, uint2 gid, const int width, const min16float2 wx1 = exp(-dist1 * packedSigma); min16float2 wx2 = exp(-dist2 * packedSigma); - min16float2 bufferPackedDepths = UnpackFloat16(bufferVal.packedDepths); + min16float2 bufferPackedDepths = FFX_CACAO_UnpackFloat16(bufferVal.packedDepths); #if 0 min16float2 diff1 = abs(packedDepths.xy - bufferPackedDepths); @@ -1872,7 +1632,7 @@ void BilateralUpscaleNxN(int2 tid, uint2 gtid, uint2 gid, const int width, const min16float2 weight1 = wx1 * wy1; min16float2 weight2 = wx2 * wy2; - min16float2 packedSsaoVals = UnpackFloat16(bufferVal.packedSsaoVals); + min16float2 packedSsaoVals = FFX_CACAO_UnpackFloat16(bufferVal.packedSsaoVals); packedTotals.xy += packedSsaoVals * weight1; packedTotals.zw += packedSsaoVals * weight2; packedTotalWeights.xy += weight1; @@ -1882,54 +1642,78 @@ void BilateralUpscaleNxN(int2 tid, uint2 gtid, uint2 gid, const int width, const uint2 outputCoord = 2 * tid; min16float4 outputValues = packedTotals / packedTotalWeights; - g_BilateralUpscaleOutput[outputCoord + int2(0, 0)] = outputValues.x; // totals[0] / totalWeights[0]; - g_BilateralUpscaleOutput[outputCoord + int2(1, 0)] = outputValues.y; // totals[1] / totalWeights[1]; - g_BilateralUpscaleOutput[outputCoord + int2(0, 1)] = outputValues.z; // totals[2] / totalWeights[2]; - g_BilateralUpscaleOutput[outputCoord + int2(1, 1)] = outputValues.w; // totals[3] / totalWeights[3]; + FFX_CACAO_BilateralUpscale_StoreOutput(outputCoord, int2(0, 0), outputValues.x); // totals[0] / totalWeights[0]; + FFX_CACAO_BilateralUpscale_StoreOutput(outputCoord, int2(1, 0), outputValues.y); // totals[1] / totalWeights[1]; + FFX_CACAO_BilateralUpscale_StoreOutput(outputCoord, int2(0, 1), outputValues.z); // totals[2] / totalWeights[2]; + FFX_CACAO_BilateralUpscale_StoreOutput(outputCoord, int2(1, 1), outputValues.w); // totals[3] / totalWeights[3]; +} + +[numthreads(FFX_CACAO_BILATERAL_UPSCALE_WIDTH, FFX_CACAO_BILATERAL_UPSCALE_HEIGHT, 1)] +void FFX_CACAO_UpscaleBilateral5x5Smart(int2 tid : SV_DispatchThreadID, uint2 gtid : SV_GroupThreadID, uint2 gid : SV_GroupID) +{ + FFX_CACAO_BilateralUpscaleNxN(tid, gtid, gid, 2, 2, true); } -[numthreads(BILATERAL_UPSCALE_WIDTH, BILATERAL_UPSCALE_HEIGHT, 1)] -void CSUpscaleBilateral5x5(int2 tid : SV_DispatchThreadID, uint2 gtid : SV_GroupThreadID, uint2 gid : SV_GroupID) +[numthreads(FFX_CACAO_BILATERAL_UPSCALE_WIDTH, FFX_CACAO_BILATERAL_UPSCALE_HEIGHT, 1)] +void FFX_CACAO_UpscaleBilateral5x5NonSmart(int2 tid : SV_DispatchThreadID, uint2 gtid : SV_GroupThreadID, uint2 gid : SV_GroupID) { - BilateralUpscaleNxN(tid, gtid, gid, 2, 2); + FFX_CACAO_BilateralUpscaleNxN(tid, gtid, gid, 2, 2, false); } -[numthreads(BILATERAL_UPSCALE_WIDTH, BILATERAL_UPSCALE_HEIGHT, 1)] -void CSUpscaleBilateral7x7(int2 tid : SV_DispatchThreadID, uint2 gtid : SV_GroupThreadID, uint2 gid : SV_GroupID) +[numthreads(FFX_CACAO_BILATERAL_UPSCALE_WIDTH, FFX_CACAO_BILATERAL_UPSCALE_HEIGHT, 1)] +void FFX_CACAO_UpscaleBilateral7x7(int2 tid : SV_DispatchThreadID, uint2 gtid : SV_GroupThreadID, uint2 gid : SV_GroupID) { - BilateralUpscaleNxN(tid, gtid, gid, 3, 3); + FFX_CACAO_BilateralUpscaleNxN(tid, gtid, gid, 3, 3, true); } -[numthreads(BILATERAL_UPSCALE_WIDTH, BILATERAL_UPSCALE_HEIGHT, 1)] -void CSUpscaleBilateral5x5Half(int2 tid : SV_DispatchThreadID, uint2 gtid : SV_GroupThreadID, uint2 gid : SV_GroupID) +[numthreads(FFX_CACAO_BILATERAL_UPSCALE_WIDTH, FFX_CACAO_BILATERAL_UPSCALE_HEIGHT, 1)] +void FFX_CACAO_UpscaleBilateral5x5Half(int2 tid : SV_DispatchThreadID, uint2 gtid : SV_GroupThreadID, uint2 gid : SV_GroupID) { const int width = 2, height = 2; // fill in group shared buffer { - uint threadNum = (gtid.y * BILATERAL_UPSCALE_WIDTH + gtid.x) * 3; - uint2 bufferCoord = uint2(threadNum % BILATERAL_UPSCALE_BUFFER_WIDTH, threadNum / BILATERAL_UPSCALE_BUFFER_WIDTH); - uint2 imageCoord = (gid * uint2(BILATERAL_UPSCALE_WIDTH, BILATERAL_UPSCALE_HEIGHT)) + bufferCoord - 2; + uint threadNum = (gtid.y * FFX_CACAO_BILATERAL_UPSCALE_WIDTH + gtid.x) * 3; + uint2 bufferCoord = uint2(threadNum % FFX_CACAO_BILATERAL_UPSCALE_BUFFER_WIDTH, threadNum / FFX_CACAO_BILATERAL_UPSCALE_BUFFER_WIDTH); + uint2 imageCoord = (gid * uint2(FFX_CACAO_BILATERAL_UPSCALE_WIDTH, FFX_CACAO_BILATERAL_UPSCALE_HEIGHT)) + bufferCoord - 2; for (int i = 0; i < 3; ++i) { - // uint2 depthBufferCoord = imageCoord + g_CACAOConsts.DeinterleavedDepthBufferOffset; - // uint3 depthArrayBufferCoord = uint3(depthBufferCoord / 2, 2 * (depthBufferCoord.y % 2) + depthBufferCoord.x % 2); - uint idx = (imageCoord.y % 2) * 3; - uint3 ssaoArrayBufferCoord = uint3(imageCoord / 2, idx); - uint3 depthArrayBufferCoord = ssaoArrayBufferCoord + uint3(g_CACAOConsts.DeinterleavedDepthBufferOffset, 0); + float2 sampleLoc0 = (float2(imageCoord / 2) + 0.5f) * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; + float2 sampleLoc1 = sampleLoc0; + switch ((imageCoord.y % 2) * 2 + (imageCoord.x % 2)) { + case 0: + sampleLoc1 -= 0.5f * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; + break; + case 1: + sampleLoc0.x += 0.5f * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions.x; + sampleLoc1.y -= 0.5f * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions.y; + break; + case 2: + sampleLoc0.y += 0.5f * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions.y; + sampleLoc1.x -= 0.5f * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions.x; + break; + case 3: + sampleLoc0 += 0.5f * g_FFX_CACAO_Consts.SSAOBufferInverseDimensions; + break; + } + + float ssaoVal0 = FFX_CACAO_BilateralUpscale_SampleSSAOPoint(sampleLoc0, 0); + float ssaoVal1 = FFX_CACAO_BilateralUpscale_SampleSSAOPoint(sampleLoc1, 3); + + uint2 depthArrayBufferCoord = (imageCoord / 2) + g_FFX_CACAO_Consts.DeinterleavedDepthBufferOffset; + uint depthArrayBufferIndex = (imageCoord.y % 2) * 3; ++imageCoord.x; - BilateralBufferVal bufferVal; + FFX_CACAO_BilateralBufferVal bufferVal; - float depth = g_BilateralUpscaleDownscaledDepth[depthArrayBufferCoord]; - // float ssaoVal = g_BilateralUpscaleInput.SampleLevel(g_PointClampSampler, float3((float2(ssaoArrayBufferCoord.xy) + 0.5f) * g_CACAOConsts.HalfViewportPixelSize, ssaoArrayBufferCoord.z), 0); - float ssaoVal = g_BilateralUpscaleInput.SampleLevel(g_PointClampSampler, float3((float2(ssaoArrayBufferCoord.xy) + 0.5f) * g_CACAOConsts.SSAOBufferInverseDimensions, ssaoArrayBufferCoord.z), 0).x; + float depth = FFX_CACAO_BilateralUpscale_LoadDownscaledDepth(depthArrayBufferCoord, depthArrayBufferIndex); + float ssaoVal = (ssaoVal0 + ssaoVal1) * 0.5f; - bufferVal.packedDepths = DoublePackFloat16(depth); - bufferVal.packedSsaoVals = DoublePackFloat16(ssaoVal); + bufferVal.packedDepths = FFX_CACAO_DoublePackFloat16(depth); + bufferVal.packedSsaoVals = FFX_CACAO_DoublePackFloat16(ssaoVal); - s_BilateralUpscaleBuffer[bufferCoord.x + i][bufferCoord.y] = bufferVal; + s_FFX_CACAO_BilateralUpscaleBuffer[bufferCoord.x + i][bufferCoord.y] = bufferVal; } } @@ -1939,27 +1723,27 @@ void CSUpscaleBilateral5x5Half(int2 tid : SV_DispatchThreadID, uint2 gtid : SV_G // load depths { int2 fullBufferCoord = 2 * tid; - int2 fullDepthBufferCoord = fullBufferCoord + g_CACAOConsts.DepthBufferOffset; + int2 fullDepthBufferCoord = fullBufferCoord + g_FFX_CACAO_Consts.DepthBufferOffset; - depths[0] = ScreenSpaceToViewSpaceDepth(g_BilateralUpscaleDepth[fullDepthBufferCoord + int2(0, 0)]); - depths[1] = ScreenSpaceToViewSpaceDepth(g_BilateralUpscaleDepth[fullDepthBufferCoord + int2(1, 0)]); - depths[2] = ScreenSpaceToViewSpaceDepth(g_BilateralUpscaleDepth[fullDepthBufferCoord + int2(0, 1)]); - depths[3] = ScreenSpaceToViewSpaceDepth(g_BilateralUpscaleDepth[fullDepthBufferCoord + int2(1, 1)]); + depths[0] = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_BilateralUpscale_LoadDepth(fullDepthBufferCoord, int2(0, 0))); + depths[1] = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_BilateralUpscale_LoadDepth(fullDepthBufferCoord, int2(1, 0))); + depths[2] = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_BilateralUpscale_LoadDepth(fullDepthBufferCoord, int2(0, 1))); + depths[3] = FFX_CACAO_ScreenSpaceToViewSpaceDepth(FFX_CACAO_BilateralUpscale_LoadDepth(fullDepthBufferCoord, int2(1, 1))); } min16float4 packedDepths = min16float4(depths[0], depths[1], depths[2], depths[3]); - float totals[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; - float totalWeights[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; - float2 pps[] = { float2(0.0f, 0.0f), float2(0.5f, 0.0f), float2(0.0f, 0.5f), float2(0.5f, 0.5f) }; - - min16float4 packedTotals = min16float4(0.0f, 0.0f, 0.0f, 0.0f); - min16float4 packedTotalWeights = min16float4(0.0f, 0.0f, 0.0f, 0.0f); - int2 baseBufferCoord = gtid + int2(width, height); - float distanceSigma = g_CACAOConsts.BilateralSimilarityDistanceSigma; + min16float epsilonWeight = 1e-3f; + min16float2 nearestSsaoVals = FFX_CACAO_UnpackFloat16(s_FFX_CACAO_BilateralUpscaleBuffer[baseBufferCoord.x][baseBufferCoord.y].packedSsaoVals); + min16float4 packedTotals = epsilonWeight * min16float4(1.0f, 1.0f, 1.0f, 1.0f); + packedTotals.xy *= nearestSsaoVals; + packedTotals.zw *= nearestSsaoVals; + min16float4 packedTotalWeights = epsilonWeight * min16float4(1.0f, 1.0f, 1.0f, 1.0f); + + float distanceSigma = g_FFX_CACAO_Consts.BilateralSimilarityDistanceSigma; min16float2 packedDistSigma = min16float2(1.0f / distanceSigma, 1.0f / distanceSigma); - float sigma = g_CACAOConsts.BilateralSigmaSquared; + float sigma = g_FFX_CACAO_Consts.BilateralSigmaSquared; min16float2 packedSigma = min16float2(1.0f / sigma, 1.0f / sigma); for (int x = -width; x <= width; ++x) @@ -1968,11 +1752,11 @@ void CSUpscaleBilateral5x5Half(int2 tid : SV_DispatchThreadID, uint2 gtid : SV_G { int2 bufferCoord = baseBufferCoord + int2(x, y); - BilateralBufferVal bufferVal = s_BilateralUpscaleBuffer[bufferCoord.x][bufferCoord.y]; + FFX_CACAO_BilateralBufferVal bufferVal = s_FFX_CACAO_BilateralUpscaleBuffer[bufferCoord.x][bufferCoord.y]; - min16float2 u = min16float2(x, x) + min16float2(0.0f, 0.5f); - min16float2 v1 = min16float2(y, y) + min16float2(0.0f, 0.0f); - min16float2 v2 = min16float2(y, y) + min16float2(0.5f, 0.5f); + min16float2 u = min16float2(x, x) - min16float2(0.0f, 0.5f); + min16float2 v1 = min16float2(y, y) - min16float2(0.0f, 0.0f); + min16float2 v2 = min16float2(y, y) - min16float2(0.5f, 0.5f); u = u * u; v1 = v1 * v1; v2 = v2 * v2; @@ -1983,7 +1767,7 @@ void CSUpscaleBilateral5x5Half(int2 tid : SV_DispatchThreadID, uint2 gtid : SV_G min16float2 wx1 = exp(-dist1 * packedSigma); min16float2 wx2 = exp(-dist2 * packedSigma); - min16float2 bufferPackedDepths = UnpackFloat16(bufferVal.packedDepths); + min16float2 bufferPackedDepths = FFX_CACAO_UnpackFloat16(bufferVal.packedDepths); #if 0 min16float2 diff1 = abs(packedDepths.xy - bufferPackedDepths); @@ -2001,7 +1785,7 @@ void CSUpscaleBilateral5x5Half(int2 tid : SV_DispatchThreadID, uint2 gtid : SV_G min16float2 weight1 = wx1 * wy1; min16float2 weight2 = wx2 * wy2; - min16float2 packedSsaoVals = UnpackFloat16(bufferVal.packedSsaoVals); + min16float2 packedSsaoVals = FFX_CACAO_UnpackFloat16(bufferVal.packedSsaoVals); packedTotals.xy += packedSsaoVals * weight1; packedTotals.zw += packedSsaoVals * weight2; packedTotalWeights.xy += weight1; @@ -2011,12 +1795,12 @@ void CSUpscaleBilateral5x5Half(int2 tid : SV_DispatchThreadID, uint2 gtid : SV_G uint2 outputCoord = 2 * tid; min16float4 outputValues = packedTotals / packedTotalWeights; - g_BilateralUpscaleOutput[outputCoord + int2(0, 0)] = outputValues.x; // totals[0] / totalWeights[0]; - g_BilateralUpscaleOutput[outputCoord + int2(1, 0)] = outputValues.y; // totals[1] / totalWeights[1]; - g_BilateralUpscaleOutput[outputCoord + int2(0, 1)] = outputValues.z; // totals[2] / totalWeights[2]; - g_BilateralUpscaleOutput[outputCoord + int2(1, 1)] = outputValues.w; // totals[3] / totalWeights[3]; + FFX_CACAO_BilateralUpscale_StoreOutput(outputCoord, int2(0, 0), outputValues.x); // totals[0] / totalWeights[0]; + FFX_CACAO_BilateralUpscale_StoreOutput(outputCoord, int2(1, 0), outputValues.y); // totals[1] / totalWeights[1]; + FFX_CACAO_BilateralUpscale_StoreOutput(outputCoord, int2(0, 1), outputValues.z); // totals[2] / totalWeights[2]; + FFX_CACAO_BilateralUpscale_StoreOutput(outputCoord, int2(1, 1), outputValues.w); // totals[3] / totalWeights[3]; } -#undef BILATERAL_UPSCALE_BUFFER_WIDTH -#undef BILATERAL_UPSCALE_BUFFER_HEIGHT +#undef FFX_CACAO_BILATERAL_UPSCALE_BUFFER_WIDTH +#undef FFX_CACAO_BILATERAL_UPSCALE_BUFFER_HEIGHT diff --git a/ffx-cacao/src/ffx_cacao_bindings.hlsl b/ffx-cacao/src/ffx_cacao_bindings.hlsl new file mode 100644 index 0000000..2af3b4c --- /dev/null +++ b/ffx-cacao/src/ffx_cacao_bindings.hlsl @@ -0,0 +1,367 @@ +// Modifications Copyright © 2021. Advanced Micro Devices, Inc. All Rights Reserved. + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Copyright (c) 2016, Intel Corporation +// 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. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// File changes (yyyy-mm-dd) +// 2016-09-07: filip.strugar@intel.com: first commit +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef FFX_CACAO_BINDINGS_HLSL +#define FFX_CACAO_BINDINGS_HLSL + +// ============================================================================= +// Constants + +struct FFX_CACAO_Constants +{ + float2 DepthUnpackConsts; + float2 CameraTanHalfFOV; + + float2 NDCToViewMul; + float2 NDCToViewAdd; + + float2 DepthBufferUVToViewMul; + float2 DepthBufferUVToViewAdd; + + float EffectRadius; // world (viewspace) maximum size of the shadow + float EffectShadowStrength; // global strength of the effect (0 - 5) + float EffectShadowPow; + float EffectShadowClamp; + + float EffectFadeOutMul; // effect fade out from distance (ex. 25) + float EffectFadeOutAdd; // effect fade out to distance (ex. 100) + float EffectHorizonAngleThreshold; // limit errors on slopes and caused by insufficient geometry tessellation (0.05 to 0.5) + float EffectSamplingRadiusNearLimitRec; // if viewspace pixel closer than this, don't enlarge shadow sampling radius anymore (makes no sense to grow beyond some distance, not enough samples to cover everything, so just limit the shadow growth; could be SSAOSettingsFadeOutFrom * 0.1 or less) + + float DepthPrecisionOffsetMod; + float NegRecEffectRadius; // -1.0 / EffectRadius + float LoadCounterAvgDiv; // 1.0 / ( halfDepthMip[SSAO_DEPTH_MIP_LEVELS-1].sizeX * halfDepthMip[SSAO_DEPTH_MIP_LEVELS-1].sizeY ) + float AdaptiveSampleCountLimit; + + float InvSharpness; + int PassIndex; + float BilateralSigmaSquared; + float BilateralSimilarityDistanceSigma; + + float4 PatternRotScaleMatrices[5]; + + float NormalsUnpackMul; + float NormalsUnpackAdd; + float DetailAOStrength; + float Dummy0; + + float2 SSAOBufferDimensions; + float2 SSAOBufferInverseDimensions; + + float2 DepthBufferDimensions; + float2 DepthBufferInverseDimensions; + + int2 DepthBufferOffset; + float2 PerPassFullResUVOffset; + + float2 OutputBufferDimensions; + float2 OutputBufferInverseDimensions; + + float2 ImportanceMapDimensions; + float2 ImportanceMapInverseDimensions; + + float2 DeinterleavedDepthBufferDimensions; + float2 DeinterleavedDepthBufferInverseDimensions; + + float2 DeinterleavedDepthBufferOffset; + float2 DeinterleavedDepthBufferNormalisedOffset; + + float4x4 NormalsWorldToViewspaceMatrix; +}; + +cbuffer SSAOConstantsBuffer : register(b0) +{ + FFX_CACAO_Constants g_FFX_CACAO_Consts; +} + +// ============================================================================= +// Samplers + +SamplerState g_PointClampSampler : register(s0); +SamplerState g_PointMirrorSampler : register(s1); +SamplerState g_LinearClampSampler : register(s2); +SamplerState g_ViewspaceDepthTapSampler : register(s3); +SamplerState g_RealPointClampSampler : register(s4); + +// ============================================================================= +// Clear Load Counter + +RWTexture1D<uint> g_ClearLoadCounter_LoadCounter : register(u0); + +void FFX_CACAO_ClearLoadCounter_SetLoadCounter(uint val) +{ + g_ClearLoadCounter_LoadCounter[0] = val; +} + +// ============================================================================= +// Edge Sensitive Blur + +Texture2DArray<float2> g_EdgeSensitiveBlur_Input : register(t0); +RWTexture2DArray<float2> g_EdgeSensitiveBlur_Output : register(u0); + +float2 FFX_CACAO_EdgeSensitiveBlur_SampleInputOffset(float2 uv, int2 offset) +{ + return g_EdgeSensitiveBlur_Input.SampleLevel(g_PointMirrorSampler, float3(uv, 0.0f), 0.0f, offset); +} + +float2 FFX_CACAO_EdgeSensitiveBlur_SampleInput(float2 uv) +{ + return g_EdgeSensitiveBlur_Input.SampleLevel(g_PointMirrorSampler, float3(uv, 0.0f), 0.0f); +} + +void FFX_CACAO_EdgeSensitiveBlur_StoreOutput(int2 coord, float2 value) +{ + g_EdgeSensitiveBlur_Output[int3(coord, 0)] = value; +} + +// ============================================================================= +// SSAO Generation + +Texture2DArray<float> g_ViewspaceDepthSource : register(t0); +Texture2DArray<float4> g_DeinterleavedNormals : register(t1); +Texture1D<uint> g_LoadCounter : register(t2); +Texture2D<float> g_ImportanceMap : register(t3); +Texture2DArray<float2> g_FinalSSAO : register(t4); + +RWTexture2DArray<float2> g_SSAOOutput : register(u0); + +float FFX_CACAO_SSAOGeneration_SampleViewspaceDepthMip(float2 uv, float mip) +{ + return g_ViewspaceDepthSource.SampleLevel(g_ViewspaceDepthTapSampler, float3(uv, 0.0f), mip); +} + +float4 FFX_CACAO_SSAOGeneration_GatherViewspaceDepthOffset(float2 uv, int2 offset) +{ + return g_ViewspaceDepthSource.GatherRed(g_PointMirrorSampler, float3(uv, 0.0f), offset); +} + +uint FFX_CACAO_SSAOGeneration_GetLoadCounter() +{ + return g_LoadCounter[0]; +} + +float FFX_CACAO_SSAOGeneration_SampleImportance(float2 uv) +{ + return g_ImportanceMap.SampleLevel(g_LinearClampSampler, uv, 0.0f); +} + +float2 FFX_CACAO_SSAOGeneration_LoadBasePassSSAOPass(int2 coord, int pass) +{ + return g_FinalSSAO.Load(int4(coord, pass, 0)).xy; +} + +float3 FFX_CACAO_SSAOGeneration_GetNormalPass(int2 coord, int pass) +{ + return g_DeinterleavedNormals[int3(coord, pass)].xyz; +} + +void FFX_CACAO_SSAOGeneration_StoreOutput(int2 coord, float2 val) +{ + g_SSAOOutput[int3(coord, 0)] = val; +} + +// ============================================================================ +// Apply + +Texture2DArray<float2> g_ApplyFinalSSAO : register(t0); +RWTexture2D<float> g_ApplyOutput : register(u0); + +float FFX_CACAO_Apply_SampleSSAOUVPass(float2 uv, int pass) +{ + return g_ApplyFinalSSAO.SampleLevel(g_LinearClampSampler, float3(uv, pass), 0.0f).x; +} + +float2 FFX_CACAO_Apply_LoadSSAOPass(int2 coord, int pass) +{ + return g_ApplyFinalSSAO.Load(int4(coord, pass, 0)); +} + +void FFX_CACAO_Apply_StoreOutput(int2 coord, float val) +{ + g_ApplyOutput[coord] = val; +} + +// ============================================================================= +// Prepare + +Texture2D<float> g_DepthIn : register(t0); +Texture2D<float4> g_PrepareNormalsFromNormalsInput : register(t0); + +RWTexture2DArray<float> g_PrepareDepthsAndMips_OutMip0 : register(u0); +RWTexture2DArray<float> g_PrepareDepthsAndMips_OutMip1 : register(u1); +RWTexture2DArray<float> g_PrepareDepthsAndMips_OutMip2 : register(u2); +RWTexture2DArray<float> g_PrepareDepthsAndMips_OutMip3 : register(u3); + +RWTexture2DArray<float> g_PrepareDepthsOut : register(u0); + +RWTexture2DArray<float4> g_PrepareNormals_NormalOut : register(u0); + +float FFX_CACAO_Prepare_SampleDepthOffset(float2 uv, int2 offset) +{ + return g_DepthIn.SampleLevel(g_PointClampSampler, uv, 0.0f, offset); +} + +float4 FFX_CACAO_Prepare_GatherDepth(float2 uv) +{ + return g_DepthIn.GatherRed(g_PointClampSampler, uv); +} + +float FFX_CACAO_Prepare_LoadDepth(int2 coord) +{ + return g_DepthIn.Load(int3(coord, 0)); +} + +float FFX_CACAO_Prepare_LoadDepthOffset(int2 coord, int2 offset) +{ + return g_DepthIn.Load(int3(coord, 0), offset); +} + +float4 FFX_CACAO_Prepare_GatherDepthOffset(float2 uv, int2 offset) +{ + return g_DepthIn.GatherRed(g_PointClampSampler, uv, offset); +} + +float3 FFX_CACAO_Prepare_LoadNormal(int2 coord) +{ + float3 normal = g_PrepareNormalsFromNormalsInput.Load(int3(coord, 0)).xyz; + normal = normal * g_FFX_CACAO_Consts.NormalsUnpackMul.xxx + g_FFX_CACAO_Consts.NormalsUnpackAdd.xxx; + normal = mul(normal, (float3x3)g_FFX_CACAO_Consts.NormalsWorldToViewspaceMatrix).xyz; + // normal = normalize(normal); + return normal; +} + +void FFX_CACAO_Prepare_StoreDepthMip0(int2 coord, int index, float val) +{ + g_PrepareDepthsAndMips_OutMip0[int3(coord, index)] = val; +} + +void FFX_CACAO_Prepare_StoreDepthMip1(int2 coord, int index, float val) +{ + g_PrepareDepthsAndMips_OutMip1[int3(coord, index)] = val; +} + +void FFX_CACAO_Prepare_StoreDepthMip2(int2 coord, int index, float val) +{ + g_PrepareDepthsAndMips_OutMip2[int3(coord, index)] = val; +} + +void FFX_CACAO_Prepare_StoreDepthMip3(int2 coord, int index, float val) +{ + g_PrepareDepthsAndMips_OutMip3[int3(coord, index)] = val; +} + +void FFX_CACAO_Prepare_StoreDepth(int2 coord, int index, float val) +{ + g_PrepareDepthsOut[int3(coord, index)] = val; +} + +void FFX_CACAO_Prepare_StoreNormal(int2 coord, int index, float3 normal) +{ + g_PrepareNormals_NormalOut[int3(coord, index)] = float4(normal, 1.0f); +} + +// ============================================================================= +// Importance Map + +Texture2DArray<float2> g_ImportanceFinalSSAO : register(t0); +RWTexture2D<float> g_ImportanceOut : register(u0); + +Texture2D<float> g_ImportanceAIn : register(t0); +RWTexture2D<float> g_ImportanceAOut : register(u0); + +Texture2D<float> g_ImportanceBIn : register(t0); +RWTexture2D<float> g_ImportanceBOut : register(u0); +RWTexture1D<uint> g_ImportanceBLoadCounter : register(u1); + +float4 FFX_CACAO_Importance_GatherSSAO(float2 uv, int index) +{ + return g_ImportanceFinalSSAO.GatherRed(g_PointClampSampler, float3(uv, index)); +} + +void FFX_CACAO_Importance_StoreImportance(int2 coord, float val) +{ + g_ImportanceOut[coord] = val; +} + +float FFX_CACAO_Importance_SampleImportanceA(float2 uv) +{ + return g_ImportanceAIn.SampleLevel(g_LinearClampSampler, uv, 0.0f); +} + +void FFX_CACAO_Importance_StoreImportanceA(int2 coord, float val) +{ + g_ImportanceAOut[coord] = val; +} + +float FFX_CACAO_Importance_SampleImportanceB(float2 uv) +{ + return g_ImportanceBIn.SampleLevel(g_LinearClampSampler, uv, 0.0f); +} + +void FFX_CACAO_Importance_StoreImportanceB(int2 coord, float val) +{ + g_ImportanceBOut[coord] = val; +} + +void FFX_CACAO_Importance_LoadCounterInterlockedAdd(uint val) +{ + InterlockedAdd(g_ImportanceBLoadCounter[0], val); +} + +// ============================================================================= +// Bilateral Upscale + +RWTexture2D<float> g_BilateralUpscaleOutput : register(u0); + +Texture2DArray<float2> g_BilateralUpscaleInput : register(t0); +Texture2D<float> g_BilateralUpscaleDepth : register(t1); +Texture2DArray<float> g_BilateralUpscaleDownscaledDepth : register(t2); + +void FFX_CACAO_BilateralUpscale_StoreOutput(int2 coord, int2 offset, float val) +{ + g_BilateralUpscaleOutput[coord + offset] = val; +} + +float FFX_CACAO_BilateralUpscale_SampleSSAOLinear(float2 uv, int index) +{ + return g_BilateralUpscaleInput.SampleLevel(g_LinearClampSampler, float3(uv, index), 0).x; +} + +float FFX_CACAO_BilateralUpscale_SampleSSAOPoint(float2 uv, int index) +{ + return g_BilateralUpscaleInput.SampleLevel(g_PointClampSampler, float3(uv, index), 0).x; +} + +float2 FFX_CACAO_BilateralUpscale_LoadSSAO(int2 coord, int index) +{ + return g_BilateralUpscaleInput.Load(int4(coord, index, 0)); +} + +float FFX_CACAO_BilateralUpscale_LoadDepth(int2 coord, int2 offset) +{ + return g_BilateralUpscaleDepth.Load(int3(coord, 0), offset); +} + +float FFX_CACAO_BilateralUpscale_LoadDownscaledDepth(int2 coord, int index) +{ + return g_BilateralUpscaleDownscaledDepth.Load(int4(coord, index, 0)); +} + +#endif diff --git a/ffx-cacao/src/ffx_cacao_defines.h b/ffx-cacao/src/ffx_cacao_defines.h index 9b17fcf..ff825e0 100644 --- a/ffx-cacao/src/ffx_cacao_defines.h +++ b/ffx-cacao/src/ffx_cacao_defines.h @@ -1,4 +1,4 @@ -// Modifications Copyright © 2020. Advanced Micro Devices, Inc. All Rights Reserved. +// Modifications Copyright © 2021. Advanced Micro Devices, Inc. All Rights Reserved. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Copyright (c) 2016, Intel Corporation @@ -20,41 +20,38 @@ // Defines for constants common to both CACAO.cpp and CACAO.hlsl -// ==================================================================== -// Prepare shader dimensions +#ifndef FFX_CACAO_DEFINES_H +#define FFX_CACAO_DEFINES_H -#define PREPARE_DEPTHS_AND_MIPS_WIDTH 8 -#define PREPARE_DEPTHS_AND_MIPS_HEIGHT 8 +// ============================================================================ +// Prepare -#define PREPARE_DEPTHS_WIDTH 8 -#define PREPARE_DEPTHS_HEIGHT 8 +#define FFX_CACAO_PREPARE_DEPTHS_AND_MIPS_WIDTH 8 +#define FFX_CACAO_PREPARE_DEPTHS_AND_MIPS_HEIGHT 8 -#define PREPARE_DEPTHS_HALF_WIDTH 8 -#define PREPARE_DEPTHS_HALF_HEIGHT 8 +#define FFX_CACAO_PREPARE_DEPTHS_WIDTH 8 +#define FFX_CACAO_PREPARE_DEPTHS_HEIGHT 8 -#define PREPARE_DEPTHS_NORMALS_AND_MIPS_WIDTH 8 -#define PREPARE_DEPTHS_NORMALS_AND_MIPS_HEIGHT 8 +#define FFX_CACAO_PREPARE_DEPTHS_HALF_WIDTH 8 +#define FFX_CACAO_PREPARE_DEPTHS_HALF_HEIGHT 8 -#define PREPARE_DEPTHS_AND_NORMALS_WIDTH 8 -#define PREPARE_DEPTHS_AND_NORMALS_HEIGHT 8 - -#define PREPARE_DEPTHS_AND_NORMALS_HALF_WIDTH 8 -#define PREPARE_DEPTHS_AND_NORMALS_HALF_HEIGHT 8 - -#define PREPARE_NORMALS_WIDTH 8 -#define PREPARE_NORMALS_HEIGHT 8 +#define FFX_CACAO_PREPARE_NORMALS_WIDTH 8 +#define FFX_CACAO_PREPARE_NORMALS_HEIGHT 8 #define PREPARE_NORMALS_FROM_INPUT_NORMALS_WIDTH 8 #define PREPARE_NORMALS_FROM_INPUT_NORMALS_HEIGHT 8 -// ==================================================================== -// Generate SSAO shader dimensions +// ============================================================================ +// SSAO Generation + +#define FFX_CACAO_GENERATE_SPARSE_WIDTH 4 +#define FFX_CACAO_GENERATE_SPARSE_HEIGHT 16 -#define GENERATE_WIDTH 8 -#define GENERATE_HEIGHT 8 +#define FFX_CACAO_GENERATE_WIDTH 8 +#define FFX_CACAO_GENERATE_HEIGHT 8 -// ==================================================================== -// Importance map shader dimensions +// ============================================================================ +// Importance Map #define IMPORTANCE_MAP_WIDTH 8 #define IMPORTANCE_MAP_HEIGHT 8 @@ -65,29 +62,22 @@ #define IMPORTANCE_MAP_B_WIDTH 8 #define IMPORTANCE_MAP_B_HEIGHT 8 -// ==================================================================== -// Blur shader dimensions - -#define BLUR_WIDTH 16 -#define BLUR_HEIGHT 16 - -// ==================================================================== -// Apply shader dimensions +// ============================================================================ +// Edge Sensitive Blur -#define APPLY_WIDTH 8 -#define APPLY_HEIGHT 8 +#define FFX_CACAO_BLUR_WIDTH 16 +#define FFX_CACAO_BLUR_HEIGHT 16 -// ==================================================================== -// Reinterleave shader dimensions +// ============================================================================ +// Apply -#define REINTERLEAVE_WIDTH 16 -#define REINTERLEAVE_HEIGHT 8 +#define FFX_CACAO_APPLY_WIDTH 8 +#define FFX_CACAO_APPLY_HEIGHT 8 -// ==================================================================== -// Upscale +// ============================================================================ +// Bilateral Upscale -#define UPSCALE_WIDTH 8 -#define UPSCALE_HEIGHT 8 +#define FFX_CACAO_BILATERAL_UPSCALE_WIDTH 8 +#define FFX_CACAO_BILATERAL_UPSCALE_HEIGHT 8 -#define BILATERAL_UPSCALE_WIDTH 8 -#define BILATERAL_UPSCALE_HEIGHT 8 \ No newline at end of file +#endif diff --git a/ffx-cacao/src/ffx_cacao_impl.cpp b/ffx-cacao/src/ffx_cacao_impl.cpp new file mode 100644 index 0000000..18aef4f --- /dev/null +++ b/ffx-cacao/src/ffx_cacao_impl.cpp @@ -0,0 +1,3834 @@ +// Modifications Copyright © 2021. Advanced Micro Devices, Inc. All Rights Reserved. + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Copyright (c) 2016, Intel Corporation +// 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. +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// File changes (yyyy-mm-dd) +// 2016-09-07: filip.strugar@intel.com: first commit +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "ffx_cacao_impl.h" +#include "ffx_cacao_defines.h" + +#include <assert.h> +#include <math.h> // cos, sin +#include <string.h> // memcpy +#include <stdio.h> // snprintf + +#ifdef FFX_CACAO_ENABLE_D3D12 +#include <d3dx12.h> +#endif + +// Define symbol to enable DirectX debug markers created using Cauldron +#define FFX_CACAO_ENABLE_CAULDRON_DEBUG + +#define FFX_CACAO_ASSERT(exp) assert(exp) +#define FFX_CACAO_ARRAY_SIZE(xs) (sizeof(xs)/sizeof(xs[0])) +#define FFX_CACAO_COS(x) cosf(x) +#define FFX_CACAO_SIN(x) sinf(x) +#define FFX_CACAO_MIN(x, y) (((x) < (y)) ? (x) : (y)) +#define FFX_CACAO_MAX(x, y) (((x) > (y)) ? (x) : (y)) +#define FFX_CACAO_CLAMP(value, lower, upper) FFX_CACAO_MIN(FFX_CACAO_MAX(value, lower), upper) +#define FFX_CACAO_OFFSET_OF(T, member) (size_t)(&(((T*)0)->member)) + +#ifdef FFX_CACAO_ENABLE_D3D12 +#include "PrecompiledShadersDXIL/CACAOClearLoadCounter.h" + +#include "PrecompiledShadersDXIL/CACAOPrepareDownsampledDepthsHalf.h" +#include "PrecompiledShadersDXIL/CACAOPrepareNativeDepthsHalf.h" + +#include "PrecompiledShadersDXIL/CACAOPrepareDownsampledDepthsAndMips.h" +#include "PrecompiledShadersDXIL/CACAOPrepareNativeDepthsAndMips.h" + +#include "PrecompiledShadersDXIL/CACAOPrepareDownsampledNormals.h" +#include "PrecompiledShadersDXIL/CACAOPrepareNativeNormals.h" + +#include "PrecompiledShadersDXIL/CACAOPrepareDownsampledNormalsFromInputNormals.h" +#include "PrecompiledShadersDXIL/CACAOPrepareNativeNormalsFromInputNormals.h" + +#include "PrecompiledShadersDXIL/CACAOPrepareDownsampledDepths.h" +#include "PrecompiledShadersDXIL/CACAOPrepareNativeDepths.h" + +#include "PrecompiledShadersDXIL/CACAOGenerateQ0.h" +#include "PrecompiledShadersDXIL/CACAOGenerateQ1.h" +#include "PrecompiledShadersDXIL/CACAOGenerateQ2.h" +#include "PrecompiledShadersDXIL/CACAOGenerateQ3.h" +#include "PrecompiledShadersDXIL/CACAOGenerateQ3Base.h" + +#include "PrecompiledShadersDXIL/CACAOGenerateImportanceMap.h" +#include "PrecompiledShadersDXIL/CACAOPostprocessImportanceMapA.h" +#include "PrecompiledShadersDXIL/CACAOPostprocessImportanceMapB.h" + +#include "PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur1.h" +#include "PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur2.h" +#include "PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur3.h" +#include "PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur4.h" +#include "PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur5.h" +#include "PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur6.h" +#include "PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur7.h" +#include "PrecompiledShadersDXIL/CACAOEdgeSensitiveBlur8.h" + +#include "PrecompiledShadersDXIL/CACAOApply.h" +#include "PrecompiledShadersDXIL/CACAONonSmartApply.h" +#include "PrecompiledShadersDXIL/CACAONonSmartHalfApply.h" + +#include "PrecompiledShadersDXIL/CACAOUpscaleBilateral5x5Smart.h" +#include "PrecompiledShadersDXIL/CACAOUpscaleBilateral5x5NonSmart.h" +#include "PrecompiledShadersDXIL/CACAOUpscaleBilateral5x5Half.h" +#endif + +#ifdef FFX_CACAO_ENABLE_VULKAN +// 16 bit versions +#include "PrecompiledShadersSPIRV/CACAOClearLoadCounter_16.h" + +#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepthsHalf_16.h" +#include "PrecompiledShadersSPIRV/CACAOPrepareNativeDepthsHalf_16.h" + +#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepthsAndMips_16.h" +#include "PrecompiledShadersSPIRV/CACAOPrepareNativeDepthsAndMips_16.h" + +#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledNormals_16.h" +#include "PrecompiledShadersSPIRV/CACAOPrepareNativeNormals_16.h" + +#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledNormalsFromInputNormals_16.h" +#include "PrecompiledShadersSPIRV/CACAOPrepareNativeNormalsFromInputNormals_16.h" + +#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepths_16.h" +#include "PrecompiledShadersSPIRV/CACAOPrepareNativeDepths_16.h" + +#include "PrecompiledShadersSPIRV/CACAOGenerateQ0_16.h" +#include "PrecompiledShadersSPIRV/CACAOGenerateQ1_16.h" +#include "PrecompiledShadersSPIRV/CACAOGenerateQ2_16.h" +#include "PrecompiledShadersSPIRV/CACAOGenerateQ3_16.h" +#include "PrecompiledShadersSPIRV/CACAOGenerateQ3Base_16.h" + +#include "PrecompiledShadersSPIRV/CACAOGenerateImportanceMap_16.h" +#include "PrecompiledShadersSPIRV/CACAOPostprocessImportanceMapA_16.h" +#include "PrecompiledShadersSPIRV/CACAOPostprocessImportanceMapB_16.h" + +#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur1_16.h" +#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur2_16.h" +#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur3_16.h" +#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur4_16.h" +#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur5_16.h" +#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur6_16.h" +#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur7_16.h" +#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur8_16.h" + +#include "PrecompiledShadersSPIRV/CACAOApply_16.h" +#include "PrecompiledShadersSPIRV/CACAONonSmartApply_16.h" +#include "PrecompiledShadersSPIRV/CACAONonSmartHalfApply_16.h" + +#include "PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5Smart_16.h" +#include "PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5NonSmart_16.h" +#include "PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5Half_16.h" + +// 32 bit versions +#include "PrecompiledShadersSPIRV/CACAOClearLoadCounter_32.h" + +#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepthsHalf_32.h" +#include "PrecompiledShadersSPIRV/CACAOPrepareNativeDepthsHalf_32.h" + +#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepthsAndMips_32.h" +#include "PrecompiledShadersSPIRV/CACAOPrepareNativeDepthsAndMips_32.h" + +#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledNormals_32.h" +#include "PrecompiledShadersSPIRV/CACAOPrepareNativeNormals_32.h" + +#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledNormalsFromInputNormals_32.h" +#include "PrecompiledShadersSPIRV/CACAOPrepareNativeNormalsFromInputNormals_32.h" + +#include "PrecompiledShadersSPIRV/CACAOPrepareDownsampledDepths_32.h" +#include "PrecompiledShadersSPIRV/CACAOPrepareNativeDepths_32.h" + +#include "PrecompiledShadersSPIRV/CACAOGenerateQ0_32.h" +#include "PrecompiledShadersSPIRV/CACAOGenerateQ1_32.h" +#include "PrecompiledShadersSPIRV/CACAOGenerateQ2_32.h" +#include "PrecompiledShadersSPIRV/CACAOGenerateQ3_32.h" +#include "PrecompiledShadersSPIRV/CACAOGenerateQ3Base_32.h" + +#include "PrecompiledShadersSPIRV/CACAOGenerateImportanceMap_32.h" +#include "PrecompiledShadersSPIRV/CACAOPostprocessImportanceMapA_32.h" +#include "PrecompiledShadersSPIRV/CACAOPostprocessImportanceMapB_32.h" + +#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur1_32.h" +#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur2_32.h" +#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur3_32.h" +#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur4_32.h" +#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur5_32.h" +#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur6_32.h" +#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur7_32.h" +#include "PrecompiledShadersSPIRV/CACAOEdgeSensitiveBlur8_32.h" + +#include "PrecompiledShadersSPIRV/CACAOApply_32.h" +#include "PrecompiledShadersSPIRV/CACAONonSmartApply_32.h" +#include "PrecompiledShadersSPIRV/CACAONonSmartHalfApply_32.h" + +#include "PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5Smart_32.h" +#include "PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5NonSmart_32.h" +#include "PrecompiledShadersSPIRV/CACAOUpscaleBilateral5x5Half_32.h" +#endif + +#define MAX_BLUR_PASSES 8 + +#if defined(FFX_CACAO_ENABLE_CAULDRON_DEBUG) && defined(FFX_CACAO_ENABLE_D3D12) +#include <base/UserMarkers.h> + +#define USER_MARKER(name) CAULDRON_DX12::UserMarker __marker(commandList, name) +#else +#define USER_MARKER(name) +#endif + +inline static uint32_t dispatchSize(uint32_t tileSize, uint32_t totalSize) +{ + return (totalSize + tileSize - 1) / tileSize; +} + +#ifdef FFX_CACAO_ENABLE_PROFILING +// TIMESTAMP(name) +#define TIMESTAMPS \ + TIMESTAMP(BEGIN) \ + TIMESTAMP(PREPARE) \ + TIMESTAMP(BASE_SSAO_PASS) \ + TIMESTAMP(IMPORTANCE_MAP) \ + TIMESTAMP(GENERATE_SSAO) \ + TIMESTAMP(EDGE_SENSITIVE_BLUR) \ + TIMESTAMP(BILATERAL_UPSAMPLE) \ + TIMESTAMP(APPLY) + +typedef enum TimestampID { +#define TIMESTAMP(name) TIMESTAMP_##name, + TIMESTAMPS +#undef TIMESTAMP + NUM_TIMESTAMPS +} TimestampID; + +static const char *TIMESTAMP_NAMES[NUM_TIMESTAMPS] = { +#define TIMESTAMP(name) "FFX_CACAO_" #name, + TIMESTAMPS +#undef TIMESTAMP +}; + +#define NUM_TIMESTAMP_BUFFERS 5 +#endif + +// TIMESTAMP_FORMAT(name, vulkan_format, d3d12_format) +#define TEXTURE_FORMATS \ + TEXTURE_FORMAT(R16_SFLOAT, VK_FORMAT_R16_SFLOAT, DXGI_FORMAT_R16_FLOAT) \ + TEXTURE_FORMAT(R16G16B16A16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT, DXGI_FORMAT_R16G16B16A16_FLOAT) \ + TEXTURE_FORMAT(R8G8B8A8_SNORM, VK_FORMAT_R8G8B8A8_SNORM, DXGI_FORMAT_R8G8B8A8_SNORM) \ + TEXTURE_FORMAT(R8G8_UNORM, VK_FORMAT_R8G8_UNORM, DXGI_FORMAT_R8G8_UNORM) \ + TEXTURE_FORMAT(R8_UNORM, VK_FORMAT_R8_UNORM, DXGI_FORMAT_R8_UNORM) + +typedef enum TextureFormatID { +#define TEXTURE_FORMAT(name, _vulkan_format, _d3d12_format) TEXTURE_FORMAT_##name, + TEXTURE_FORMATS +#undef TEXTURE_FORMAT +} TextureFormatID; + +#ifdef FFX_CACAO_ENABLE_VULKAN +static const VkFormat TEXTURE_FORMAT_LOOKUP_VK[] = { +#define TEXTURE_FORMAT(_name, vulkan_format, _d3d12_format) vulkan_format, + TEXTURE_FORMATS +#undef TEXTURE_FORMAT +}; +#endif +#ifdef FFX_CACAO_ENABLE_D3D12 +static const DXGI_FORMAT TEXTURE_FORMAT_LOOKUP_D3D12[] = { +#define TEXTURE_FORMAT(_name, _vulkan_format, d3d12_format) d3d12_format, + TEXTURE_FORMATS +#undef TEXTURE_FORMAT +}; +#endif + +// TEXTURE(name, width, height, texture_format, array_size, num_mips) +#define TEXTURES \ + TEXTURE(DEINTERLEAVED_DEPTHS, deinterleavedDepthBufferWidth, deinterleavedDepthBufferHeight, TEXTURE_FORMAT_R16_SFLOAT, 4, 4) \ + TEXTURE(DEINTERLEAVED_NORMALS, ssaoBufferWidth, ssaoBufferHeight, TEXTURE_FORMAT_R8G8B8A8_SNORM, 4, 1) \ + TEXTURE(SSAO_BUFFER_PING, ssaoBufferWidth, ssaoBufferHeight, TEXTURE_FORMAT_R8G8_UNORM, 4, 1) \ + TEXTURE(SSAO_BUFFER_PONG, ssaoBufferWidth, ssaoBufferHeight, TEXTURE_FORMAT_R8G8_UNORM, 4, 1) \ + TEXTURE(IMPORTANCE_MAP, importanceMapWidth, importanceMapHeight, TEXTURE_FORMAT_R8_UNORM, 1, 1) \ + TEXTURE(IMPORTANCE_MAP_PONG, importanceMapWidth, importanceMapHeight, TEXTURE_FORMAT_R8_UNORM, 1, 1) \ + TEXTURE(DOWNSAMPLED_SSAO_BUFFER, downsampledSsaoBufferWidth, downsampledSsaoBufferHeight, TEXTURE_FORMAT_R8_UNORM, 1, 1) + +typedef enum TextureID { +#define TEXTURE(name, _width, _height, _format, _array_size, _num_mips) TEXTURE_##name, + TEXTURES +#undef TEXTURE + NUM_TEXTURES +} TextureID; + +typedef struct TextureMetaData { + size_t widthOffset; + size_t heightOffset; + TextureFormatID format; + uint32_t arraySize; + uint32_t numMips; + const char *name; +} TextureMetaData; + +static const TextureMetaData TEXTURE_META_DATA[NUM_TEXTURES] = { +#define TEXTURE(name, width, height, format, array_size, num_mips) { FFX_CACAO_OFFSET_OF(FFX_CACAO_BufferSizeInfo, width), FFX_CACAO_OFFSET_OF(FFX_CACAO_BufferSizeInfo, height), format, array_size, num_mips, "FFX_CACAO_" #name }, + TEXTURES +#undef TEXTURE +}; + +// DESCRIPTOR_SET_LAYOUT(name, num_inputs, num_outputs) +#define DESCRIPTOR_SET_LAYOUTS \ + DESCRIPTOR_SET_LAYOUT(CLEAR_LOAD_COUNTER, 0, 1) \ + DESCRIPTOR_SET_LAYOUT(PREPARE_DEPTHS, 1, 1) \ + DESCRIPTOR_SET_LAYOUT(PREPARE_DEPTHS_MIPS, 1, 4) \ + DESCRIPTOR_SET_LAYOUT(PREPARE_POINTS, 1, 1) \ + DESCRIPTOR_SET_LAYOUT(PREPARE_POINTS_MIPS, 1, 4) \ + DESCRIPTOR_SET_LAYOUT(PREPARE_NORMALS, 1, 1) \ + DESCRIPTOR_SET_LAYOUT(PREPARE_NORMALS_FROM_INPUT_NORMALS, 1, 1) \ + DESCRIPTOR_SET_LAYOUT(GENERATE, 2, 1) \ + DESCRIPTOR_SET_LAYOUT(GENERATE_ADAPTIVE, 5, 1) \ + DESCRIPTOR_SET_LAYOUT(GENERATE_IMPORTANCE_MAP, 1, 1) \ + DESCRIPTOR_SET_LAYOUT(POSTPROCESS_IMPORTANCE_MAP_A, 1, 1) \ + DESCRIPTOR_SET_LAYOUT(POSTPROCESS_IMPORTANCE_MAP_B, 1, 2) \ + DESCRIPTOR_SET_LAYOUT(EDGE_SENSITIVE_BLUR, 1, 1) \ + DESCRIPTOR_SET_LAYOUT(APPLY, 1, 1) \ + DESCRIPTOR_SET_LAYOUT(BILATERAL_UPSAMPLE, 4, 1) + +typedef enum DescriptorSetLayoutID { +#define DESCRIPTOR_SET_LAYOUT(name, _num_inputs, _num_outputs) DSL_##name, + DESCRIPTOR_SET_LAYOUTS +#undef DESCRIPTOR_SET_LAYOUT + NUM_DESCRIPTOR_SET_LAYOUTS +} DescriptorSetLayoutID; + +typedef struct DescriptorSetLayoutMetaData { + uint32_t numInputs; + uint32_t numOutputs; + const char *name; +} DescriptorSetLayoutMetaData; + +static const DescriptorSetLayoutMetaData DESCRIPTOR_SET_LAYOUT_META_DATA[NUM_DESCRIPTOR_SET_LAYOUTS] = { +#define DESCRIPTOR_SET_LAYOUT(name, num_inputs, num_outputs) { num_inputs, num_outputs, "FFX_CACAO_DSL_" #name }, + DESCRIPTOR_SET_LAYOUTS +#undef DESCRIPTOR_SET_LAYOUT +}; + +// DESCRIPTOR_SET(name, layout_name, pass) +#define DESCRIPTOR_SETS \ + DESCRIPTOR_SET(CLEAR_LOAD_COUNTER, CLEAR_LOAD_COUNTER, 0) \ + DESCRIPTOR_SET(PREPARE_DEPTHS, PREPARE_DEPTHS, 0) \ + DESCRIPTOR_SET(PREPARE_DEPTHS_MIPS, PREPARE_DEPTHS_MIPS, 0) \ + DESCRIPTOR_SET(PREPARE_POINTS, PREPARE_POINTS, 0) \ + DESCRIPTOR_SET(PREPARE_POINTS_MIPS, PREPARE_POINTS_MIPS, 0) \ + DESCRIPTOR_SET(PREPARE_NORMALS, PREPARE_NORMALS, 0) \ + DESCRIPTOR_SET(PREPARE_NORMALS_FROM_INPUT_NORMALS, PREPARE_NORMALS_FROM_INPUT_NORMALS, 0) \ + DESCRIPTOR_SET(GENERATE_ADAPTIVE_BASE_0, GENERATE, 0) \ + DESCRIPTOR_SET(GENERATE_ADAPTIVE_BASE_1, GENERATE, 1) \ + DESCRIPTOR_SET(GENERATE_ADAPTIVE_BASE_2, GENERATE, 2) \ + DESCRIPTOR_SET(GENERATE_ADAPTIVE_BASE_3, GENERATE, 3) \ + DESCRIPTOR_SET(GENERATE_0, GENERATE, 0) \ + DESCRIPTOR_SET(GENERATE_1, GENERATE, 1) \ + DESCRIPTOR_SET(GENERATE_2, GENERATE, 2) \ + DESCRIPTOR_SET(GENERATE_3, GENERATE, 3) \ + DESCRIPTOR_SET(GENERATE_ADAPTIVE_0, GENERATE_ADAPTIVE, 0) \ + DESCRIPTOR_SET(GENERATE_ADAPTIVE_1, GENERATE_ADAPTIVE, 1) \ + DESCRIPTOR_SET(GENERATE_ADAPTIVE_2, GENERATE_ADAPTIVE, 2) \ + DESCRIPTOR_SET(GENERATE_ADAPTIVE_3, GENERATE_ADAPTIVE, 3) \ + DESCRIPTOR_SET(GENERATE_IMPORTANCE_MAP, GENERATE_IMPORTANCE_MAP, 0) \ + DESCRIPTOR_SET(POSTPROCESS_IMPORTANCE_MAP_A, POSTPROCESS_IMPORTANCE_MAP_A, 0) \ + DESCRIPTOR_SET(POSTPROCESS_IMPORTANCE_MAP_B, POSTPROCESS_IMPORTANCE_MAP_B, 0) \ + DESCRIPTOR_SET(EDGE_SENSITIVE_BLUR_0, EDGE_SENSITIVE_BLUR, 0) \ + DESCRIPTOR_SET(EDGE_SENSITIVE_BLUR_1, EDGE_SENSITIVE_BLUR, 1) \ + DESCRIPTOR_SET(EDGE_SENSITIVE_BLUR_2, EDGE_SENSITIVE_BLUR, 2) \ + DESCRIPTOR_SET(EDGE_SENSITIVE_BLUR_3, EDGE_SENSITIVE_BLUR, 3) \ + DESCRIPTOR_SET(APPLY_PING, APPLY, 0) \ + DESCRIPTOR_SET(APPLY_PONG, APPLY, 0) \ + DESCRIPTOR_SET(BILATERAL_UPSAMPLE_PING, BILATERAL_UPSAMPLE, 0) \ + DESCRIPTOR_SET(BILATERAL_UPSAMPLE_PONG, BILATERAL_UPSAMPLE, 0) + +typedef enum DescriptorSetID { +#define DESCRIPTOR_SET(name, _layout_name, _pass) DS_##name, + DESCRIPTOR_SETS +#undef DESCRIPTOR_SET + NUM_DESCRIPTOR_SETS +} DescriptorSetID; + +typedef struct DescriptorSetMetaData { + DescriptorSetLayoutID descriptorSetLayoutID; + uint32_t pass; + const char *name; +} DescriptorSetMetaData; + +static const DescriptorSetMetaData DESCRIPTOR_SET_META_DATA[NUM_DESCRIPTOR_SETS] = { +#define DESCRIPTOR_SET(name, layout_name, pass) { DSL_##layout_name, pass, "FFX_CACAO_DS_" #name }, + DESCRIPTOR_SETS +#undef DESCRIPTOR_SET +}; + +// VIEW_TYPE(name, vulkan_view_type, d3d12_view_type_srv) +#define VIEW_TYPES \ + VIEW_TYPE(2D, VK_IMAGE_VIEW_TYPE_2D, D3D12_SRV_DIMENSION_TEXTURE2D, D3D12_UAV_DIMENSION_TEXTURE2D) \ + VIEW_TYPE(2D_ARRAY, VK_IMAGE_VIEW_TYPE_2D_ARRAY, D3D12_SRV_DIMENSION_TEXTURE2DARRAY, D3D12_UAV_DIMENSION_TEXTURE2DARRAY) + +typedef enum ViewTypeID { +#define VIEW_TYPE(name, _vulkan_view_type, _d3d12_view_type_srv, _d3d12_view_type_uav) VIEW_TYPE_##name, + VIEW_TYPES +#undef VIEW_TYPE +} ViewTypeID; + +#ifdef FFX_CACAO_ENABLE_VULKAN +static const VkImageViewType VIEW_TYPE_LOOKUP_VK[] = { +#define VIEW_TYPE(_name, vulkan_view_type, _d3d12_view_type_srv, _d3d12_view_type_uav) vulkan_view_type, + VIEW_TYPES +#undef VIEW_TYPE +}; +#endif + +#ifdef FFX_CACAO_ENABLE_D3D12 +static const D3D12_SRV_DIMENSION VIEW_TYPE_LOOKUP_D3D12_SRV[] = { +#define VIEW_TYPE(_name, _vulkan_view_type, d3d12_view_type_srv, _d3d12_view_type_uav) d3d12_view_type_srv, + VIEW_TYPES +#undef VIEW_TYPE +}; + +static const D3D12_UAV_DIMENSION VIEW_TYPE_LOOKUP_D3D12_UAV[] = { +#define VIEW_TYPE(_name, _vulkan_view_type, _d3d12_view_type_srv, d3d12_view_type_uav) d3d12_view_type_uav, + VIEW_TYPES +#undef VIEW_TYPE +}; +#endif + +// SHADER_RESOURCE_VIEW(name, texture, view_dimension, most_detailed_mip, mip_levels, first_array_slice, array_size) +#define SHADER_RESOURCE_VIEWS \ + SHADER_RESOURCE_VIEW(DEINTERLEAVED_DEPTHS, DEINTERLEAVED_DEPTHS, VIEW_TYPE_2D_ARRAY, 0, 4, 0, 4) \ + SHADER_RESOURCE_VIEW(DEINTERLEAVED_DEPTHS_0, DEINTERLEAVED_DEPTHS, VIEW_TYPE_2D_ARRAY, 0, 4, 0, 1) \ + SHADER_RESOURCE_VIEW(DEINTERLEAVED_DEPTHS_1, DEINTERLEAVED_DEPTHS, VIEW_TYPE_2D_ARRAY, 0, 4, 1, 1) \ + SHADER_RESOURCE_VIEW(DEINTERLEAVED_DEPTHS_2, DEINTERLEAVED_DEPTHS, VIEW_TYPE_2D_ARRAY, 0, 4, 2, 1) \ + SHADER_RESOURCE_VIEW(DEINTERLEAVED_DEPTHS_3, DEINTERLEAVED_DEPTHS, VIEW_TYPE_2D_ARRAY, 0, 4, 3, 1) \ + SHADER_RESOURCE_VIEW(DEINTERLEAVED_NORMALS, DEINTERLEAVED_NORMALS, VIEW_TYPE_2D_ARRAY, 0, 1, 0, 4) \ + SHADER_RESOURCE_VIEW(IMPORTANCE_MAP, IMPORTANCE_MAP, VIEW_TYPE_2D, 0, 1, 0, 1) \ + SHADER_RESOURCE_VIEW(IMPORTANCE_MAP_PONG, IMPORTANCE_MAP_PONG, VIEW_TYPE_2D, 0, 1, 0, 1) \ + SHADER_RESOURCE_VIEW(SSAO_BUFFER_PING, SSAO_BUFFER_PING, VIEW_TYPE_2D_ARRAY, 0, 1, 0, 4) \ + SHADER_RESOURCE_VIEW(SSAO_BUFFER_PING_0, SSAO_BUFFER_PING, VIEW_TYPE_2D_ARRAY, 0, 1, 0, 1) \ + SHADER_RESOURCE_VIEW(SSAO_BUFFER_PING_1, SSAO_BUFFER_PING, VIEW_TYPE_2D_ARRAY, 0, 1, 1, 1) \ + SHADER_RESOURCE_VIEW(SSAO_BUFFER_PING_2, SSAO_BUFFER_PING, VIEW_TYPE_2D_ARRAY, 0, 1, 2, 1) \ + SHADER_RESOURCE_VIEW(SSAO_BUFFER_PING_3, SSAO_BUFFER_PING, VIEW_TYPE_2D_ARRAY, 0, 1, 3, 1) \ + SHADER_RESOURCE_VIEW(SSAO_BUFFER_PONG, SSAO_BUFFER_PONG, VIEW_TYPE_2D_ARRAY, 0, 1, 0, 4) \ + SHADER_RESOURCE_VIEW(SSAO_BUFFER_PONG_0, SSAO_BUFFER_PONG, VIEW_TYPE_2D_ARRAY, 0, 1, 0, 1) \ + SHADER_RESOURCE_VIEW(SSAO_BUFFER_PONG_1, SSAO_BUFFER_PONG, VIEW_TYPE_2D_ARRAY, 0, 1, 1, 1) \ + SHADER_RESOURCE_VIEW(SSAO_BUFFER_PONG_2, SSAO_BUFFER_PONG, VIEW_TYPE_2D_ARRAY, 0, 1, 2, 1) \ + SHADER_RESOURCE_VIEW(SSAO_BUFFER_PONG_3, SSAO_BUFFER_PONG, VIEW_TYPE_2D_ARRAY, 0, 1, 3, 1) + +typedef enum ShaderResourceViewID { +#define SHADER_RESOURCE_VIEW(name, _texture, _view_dimension, _most_detailed_mip, _mip_levels, _first_array_slice, _array_size) SRV_##name, + SHADER_RESOURCE_VIEWS +#undef SHADER_RESOURCE_VIEW + NUM_SHADER_RESOURCE_VIEWS +} ShaderResourceViewID; + +typedef struct ShaderResourceViewMetaData { + TextureID texture; + ViewTypeID viewType; + uint32_t mostDetailedMip; + uint32_t mipLevels; + uint32_t firstArraySlice; + uint32_t arraySize; +} ShaderResourceViewMetaData; + +static const ShaderResourceViewMetaData SRV_META_DATA[NUM_SHADER_RESOURCE_VIEWS] = { +#define SHADER_RESOURCE_VIEW(_name, texture, view_dimension, most_detailed_mip, mip_levels, first_array_slice, array_size) { TEXTURE_##texture, view_dimension, most_detailed_mip, mip_levels, first_array_slice, array_size }, + SHADER_RESOURCE_VIEWS +#undef SHADER_RESOURCE_VIEW +}; + +// UNORDERED_ACCESS_VIEW(name, texture, view_dimension, mip_slice, first_array_slice, array_size) +#define UNORDERED_ACCESS_VIEWS \ + UNORDERED_ACCESS_VIEW(DEINTERLEAVED_DEPTHS_MIP_0, DEINTERLEAVED_DEPTHS, VIEW_TYPE_2D_ARRAY, 0, 0, 4) \ + UNORDERED_ACCESS_VIEW(DEINTERLEAVED_DEPTHS_MIP_1, DEINTERLEAVED_DEPTHS, VIEW_TYPE_2D_ARRAY, 1, 0, 4) \ + UNORDERED_ACCESS_VIEW(DEINTERLEAVED_DEPTHS_MIP_2, DEINTERLEAVED_DEPTHS, VIEW_TYPE_2D_ARRAY, 2, 0, 4) \ + UNORDERED_ACCESS_VIEW(DEINTERLEAVED_DEPTHS_MIP_3, DEINTERLEAVED_DEPTHS, VIEW_TYPE_2D_ARRAY, 3, 0, 4) \ + UNORDERED_ACCESS_VIEW(DEINTERLEAVED_NORMALS, DEINTERLEAVED_NORMALS, VIEW_TYPE_2D_ARRAY, 0, 0, 4) \ + UNORDERED_ACCESS_VIEW(IMPORTANCE_MAP, IMPORTANCE_MAP, VIEW_TYPE_2D, 0, 0, 1) \ + UNORDERED_ACCESS_VIEW(IMPORTANCE_MAP_PONG, IMPORTANCE_MAP_PONG, VIEW_TYPE_2D, 0, 0, 1) \ + UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PING, SSAO_BUFFER_PING, VIEW_TYPE_2D_ARRAY, 0, 0, 4) \ + UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PING_0, SSAO_BUFFER_PING, VIEW_TYPE_2D_ARRAY, 0, 0, 1) \ + UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PING_1, SSAO_BUFFER_PING, VIEW_TYPE_2D_ARRAY, 0, 1, 1) \ + UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PING_2, SSAO_BUFFER_PING, VIEW_TYPE_2D_ARRAY, 0, 2, 1) \ + UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PING_3, SSAO_BUFFER_PING, VIEW_TYPE_2D_ARRAY, 0, 3, 1) \ + UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PONG, SSAO_BUFFER_PONG, VIEW_TYPE_2D_ARRAY, 0, 0, 4) \ + UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PONG_0, SSAO_BUFFER_PONG, VIEW_TYPE_2D_ARRAY, 0, 0, 1) \ + UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PONG_1, SSAO_BUFFER_PONG, VIEW_TYPE_2D_ARRAY, 0, 1, 1) \ + UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PONG_2, SSAO_BUFFER_PONG, VIEW_TYPE_2D_ARRAY, 0, 2, 1) \ + UNORDERED_ACCESS_VIEW(SSAO_BUFFER_PONG_3, SSAO_BUFFER_PONG, VIEW_TYPE_2D_ARRAY, 0, 3, 1) + +typedef enum UnorderedAccessViewID { +#define UNORDERED_ACCESS_VIEW(name, _texture, _view_dimension, _mip_slice, _first_array_slice, _array_size) UAV_##name, + UNORDERED_ACCESS_VIEWS +#undef UNORDERED_ACCESS_VIEW + NUM_UNORDERED_ACCESS_VIEWS +} UnorderedAccessViewID; + +typedef struct UnorderedAccessViewMetaData { + TextureID textureID; + ViewTypeID viewType; + uint32_t mostDetailedMip; + uint32_t firstArraySlice; + uint32_t arraySize; +} UnorderedAccessViewMetaData; + +static const UnorderedAccessViewMetaData UAV_META_DATA[NUM_UNORDERED_ACCESS_VIEWS] = { +#define UNORDERED_ACCESS_VIEW(_name, texture, view_dimension, mip_slice, first_array_slice, array_size) { TEXTURE_##texture, view_dimension, mip_slice, first_array_slice, array_size }, + UNORDERED_ACCESS_VIEWS +#undef UNORDERED_ACCESS_VIEW +}; + +// INPUT_DESCRIPTOR(descriptor_set_name, srv_name, binding_num) +#define INPUT_DESCRIPTOR_BINDINGS \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_0, DEINTERLEAVED_DEPTHS_0, 0) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_0, DEINTERLEAVED_NORMALS, 1) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_1, DEINTERLEAVED_DEPTHS_1, 0) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_1, DEINTERLEAVED_NORMALS, 1) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_2, DEINTERLEAVED_DEPTHS_2, 0) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_2, DEINTERLEAVED_NORMALS, 1) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_3, DEINTERLEAVED_DEPTHS_3, 0) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_3, DEINTERLEAVED_NORMALS, 1) \ + \ + INPUT_DESCRIPTOR_BINDING(GENERATE_0, DEINTERLEAVED_DEPTHS_0, 0) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_0, DEINTERLEAVED_NORMALS, 1) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_1, DEINTERLEAVED_DEPTHS_1, 0) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_1, DEINTERLEAVED_NORMALS, 1) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_2, DEINTERLEAVED_DEPTHS_2, 0) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_2, DEINTERLEAVED_NORMALS, 1) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_3, DEINTERLEAVED_DEPTHS_3, 0) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_3, DEINTERLEAVED_NORMALS, 1) \ + \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_0, DEINTERLEAVED_DEPTHS_0, 0) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_0, DEINTERLEAVED_NORMALS, 1) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_0, IMPORTANCE_MAP, 3) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_0, SSAO_BUFFER_PONG_0, 4) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_1, DEINTERLEAVED_DEPTHS_1, 0) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_1, DEINTERLEAVED_NORMALS, 1) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_1, IMPORTANCE_MAP, 3) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_1, SSAO_BUFFER_PONG_1, 4) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_2, DEINTERLEAVED_DEPTHS_2, 0) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_2, DEINTERLEAVED_NORMALS, 1) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_2, IMPORTANCE_MAP, 3) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_2, SSAO_BUFFER_PONG_2, 4) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_3, DEINTERLEAVED_DEPTHS_3, 0) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_3, DEINTERLEAVED_NORMALS, 1) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_3, IMPORTANCE_MAP, 3) \ + INPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_3, SSAO_BUFFER_PONG_3, 4) \ + \ + INPUT_DESCRIPTOR_BINDING(GENERATE_IMPORTANCE_MAP, SSAO_BUFFER_PONG, 0) \ + INPUT_DESCRIPTOR_BINDING(POSTPROCESS_IMPORTANCE_MAP_A, IMPORTANCE_MAP, 0) \ + INPUT_DESCRIPTOR_BINDING(POSTPROCESS_IMPORTANCE_MAP_B, IMPORTANCE_MAP_PONG, 0) \ + \ + INPUT_DESCRIPTOR_BINDING(EDGE_SENSITIVE_BLUR_0, SSAO_BUFFER_PING_0, 0) \ + INPUT_DESCRIPTOR_BINDING(EDGE_SENSITIVE_BLUR_1, SSAO_BUFFER_PING_1, 0) \ + INPUT_DESCRIPTOR_BINDING(EDGE_SENSITIVE_BLUR_2, SSAO_BUFFER_PING_2, 0) \ + INPUT_DESCRIPTOR_BINDING(EDGE_SENSITIVE_BLUR_3, SSAO_BUFFER_PING_3, 0) \ + \ + INPUT_DESCRIPTOR_BINDING(BILATERAL_UPSAMPLE_PING, SSAO_BUFFER_PING, 0) \ + INPUT_DESCRIPTOR_BINDING(BILATERAL_UPSAMPLE_PING, DEINTERLEAVED_DEPTHS, 2) \ + INPUT_DESCRIPTOR_BINDING(BILATERAL_UPSAMPLE_PONG, SSAO_BUFFER_PONG, 0) \ + INPUT_DESCRIPTOR_BINDING(BILATERAL_UPSAMPLE_PONG, DEINTERLEAVED_DEPTHS, 2) \ + \ + INPUT_DESCRIPTOR_BINDING(APPLY_PING, SSAO_BUFFER_PING, 0) \ + INPUT_DESCRIPTOR_BINDING(APPLY_PONG, SSAO_BUFFER_PONG, 0) + +// need this to define NUM_INPUT_DESCRIPTOR_BINDINGS +typedef enum InputDescriptorBindingID { +#define INPUT_DESCRIPTOR_BINDING(descriptor_set_name, srv_name, _binding_num) INPUT_DESCRIPTOR_BINDING_##descriptor_set_name##_##srv_name, + INPUT_DESCRIPTOR_BINDINGS +#undef INPUT_DESCRIPTOR_BINDING + NUM_INPUT_DESCRIPTOR_BINDINGS +} InputDescriptorBindingID; + +typedef struct InputDescriptorBindingMetaData { + DescriptorSetID descriptorID; + ShaderResourceViewID srvID; + uint32_t bindingNumber; +} InputDescriptorBindingMetaData; + +static const InputDescriptorBindingMetaData INPUT_DESCRIPTOR_BINDING_META_DATA[NUM_INPUT_DESCRIPTOR_BINDINGS] = { +#define INPUT_DESCRIPTOR_BINDING(descriptor_set_name, srv_name, binding_num) { DS_##descriptor_set_name, SRV_##srv_name, binding_num }, + INPUT_DESCRIPTOR_BINDINGS +#undef INPUT_DESCRIPTOR_BINDING +}; + +// OUTPUT_DESCRIPTOR(descriptor_set_name, uav_name, binding_num) +#define OUTPUT_DESCRIPTOR_BINDINGS \ + OUTPUT_DESCRIPTOR_BINDING(PREPARE_DEPTHS, DEINTERLEAVED_DEPTHS_MIP_0, 0) \ + OUTPUT_DESCRIPTOR_BINDING(PREPARE_DEPTHS_MIPS, DEINTERLEAVED_DEPTHS_MIP_0, 0) \ + OUTPUT_DESCRIPTOR_BINDING(PREPARE_DEPTHS_MIPS, DEINTERLEAVED_DEPTHS_MIP_1, 1) \ + OUTPUT_DESCRIPTOR_BINDING(PREPARE_DEPTHS_MIPS, DEINTERLEAVED_DEPTHS_MIP_2, 2) \ + OUTPUT_DESCRIPTOR_BINDING(PREPARE_DEPTHS_MIPS, DEINTERLEAVED_DEPTHS_MIP_3, 3) \ + OUTPUT_DESCRIPTOR_BINDING(PREPARE_NORMALS, DEINTERLEAVED_NORMALS, 0) \ + OUTPUT_DESCRIPTOR_BINDING(PREPARE_NORMALS_FROM_INPUT_NORMALS, DEINTERLEAVED_NORMALS, 0) \ + OUTPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_0, SSAO_BUFFER_PONG_0, 0) \ + OUTPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_1, SSAO_BUFFER_PONG_1, 0) \ + OUTPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_2, SSAO_BUFFER_PONG_2, 0) \ + OUTPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_BASE_3, SSAO_BUFFER_PONG_3, 0) \ + OUTPUT_DESCRIPTOR_BINDING(GENERATE_0, SSAO_BUFFER_PING_0, 0) \ + OUTPUT_DESCRIPTOR_BINDING(GENERATE_1, SSAO_BUFFER_PING_1, 0) \ + OUTPUT_DESCRIPTOR_BINDING(GENERATE_2, SSAO_BUFFER_PING_2, 0) \ + OUTPUT_DESCRIPTOR_BINDING(GENERATE_3, SSAO_BUFFER_PING_3, 0) \ + OUTPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_0, SSAO_BUFFER_PING_0, 0) \ + OUTPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_1, SSAO_BUFFER_PING_1, 0) \ + OUTPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_2, SSAO_BUFFER_PING_2, 0) \ + OUTPUT_DESCRIPTOR_BINDING(GENERATE_ADAPTIVE_3, SSAO_BUFFER_PING_3, 0) \ + OUTPUT_DESCRIPTOR_BINDING(GENERATE_IMPORTANCE_MAP, IMPORTANCE_MAP, 0) \ + OUTPUT_DESCRIPTOR_BINDING(POSTPROCESS_IMPORTANCE_MAP_A, IMPORTANCE_MAP_PONG, 0) \ + OUTPUT_DESCRIPTOR_BINDING(POSTPROCESS_IMPORTANCE_MAP_B, IMPORTANCE_MAP, 0) \ + OUTPUT_DESCRIPTOR_BINDING(EDGE_SENSITIVE_BLUR_0, SSAO_BUFFER_PONG_0, 0) \ + OUTPUT_DESCRIPTOR_BINDING(EDGE_SENSITIVE_BLUR_1, SSAO_BUFFER_PONG_1, 0) \ + OUTPUT_DESCRIPTOR_BINDING(EDGE_SENSITIVE_BLUR_2, SSAO_BUFFER_PONG_2, 0) \ + OUTPUT_DESCRIPTOR_BINDING(EDGE_SENSITIVE_BLUR_3, SSAO_BUFFER_PONG_3, 0) + +typedef enum OutputDescriptorBindingID { +#define OUTPUT_DESCRIPTOR_BINDING(descriptor_set_name, uav_name, _binding_num) OUTPUT_DESCRIPTOR_BINDING_##descriptor_set_name##_##uav_name, + OUTPUT_DESCRIPTOR_BINDINGS +#undef OUTPUT_DESCRIPTOR_BINDING + NUM_OUTPUT_DESCRIPTOR_BINDINGS +} OutputDescriptorBindingID; + +typedef struct OutputDescriptorBindingMetaData { + DescriptorSetID descriptorID; + UnorderedAccessViewID uavID; + uint32_t bindingNumber; +} OutputDescriptorBindingMetaData; + +static const OutputDescriptorBindingMetaData OUTPUT_DESCRIPTOR_BINDING_META_DATA[NUM_OUTPUT_DESCRIPTOR_BINDINGS] = { +#define OUTPUT_DESCRIPTOR_BINDING(descriptor_set_name, uav_name, binding_num) { DS_##descriptor_set_name, UAV_##uav_name, binding_num }, + OUTPUT_DESCRIPTOR_BINDINGS +#undef OUTPUT_DESCRIPTOR_BINDING +}; + +// define all the data for compute shaders +// COMPUTE_SHADER(enum_name, pascal_case_name, descriptor_set) +#define COMPUTE_SHADERS \ + COMPUTE_SHADER(CLEAR_LOAD_COUNTER, ClearLoadCounter, CLEAR_LOAD_COUNTER) \ + \ + COMPUTE_SHADER(PREPARE_DOWNSAMPLED_DEPTHS, PrepareDownsampledDepths, PREPARE_DEPTHS) \ + COMPUTE_SHADER(PREPARE_NATIVE_DEPTHS, PrepareNativeDepths, PREPARE_DEPTHS) \ + COMPUTE_SHADER(PREPARE_DOWNSAMPLED_DEPTHS_AND_MIPS, PrepareDownsampledDepthsAndMips, PREPARE_DEPTHS_MIPS) \ + COMPUTE_SHADER(PREPARE_NATIVE_DEPTHS_AND_MIPS, PrepareNativeDepthsAndMips, PREPARE_DEPTHS_MIPS) \ + COMPUTE_SHADER(PREPARE_DOWNSAMPLED_NORMALS, PrepareDownsampledNormals, PREPARE_NORMALS) \ + COMPUTE_SHADER(PREPARE_NATIVE_NORMALS, PrepareNativeNormals, PREPARE_NORMALS) \ + COMPUTE_SHADER(PREPARE_DOWNSAMPLED_NORMALS_FROM_INPUT_NORMALS, PrepareDownsampledNormalsFromInputNormals, PREPARE_NORMALS_FROM_INPUT_NORMALS) \ + COMPUTE_SHADER(PREPARE_NATIVE_NORMALS_FROM_INPUT_NORMALS, PrepareNativeNormalsFromInputNormals, PREPARE_NORMALS_FROM_INPUT_NORMALS) \ + COMPUTE_SHADER(PREPARE_DOWNSAMPLED_DEPTHS_HALF, PrepareDownsampledDepthsHalf, PREPARE_DEPTHS) \ + COMPUTE_SHADER(PREPARE_NATIVE_DEPTHS_HALF, PrepareNativeDepthsHalf, PREPARE_DEPTHS) \ + \ + COMPUTE_SHADER(GENERATE_Q0, GenerateQ0, GENERATE) \ + COMPUTE_SHADER(GENERATE_Q1, GenerateQ1, GENERATE) \ + COMPUTE_SHADER(GENERATE_Q2, GenerateQ2, GENERATE) \ + COMPUTE_SHADER(GENERATE_Q3, GenerateQ3, GENERATE_ADAPTIVE) \ + COMPUTE_SHADER(GENERATE_Q3_BASE, GenerateQ3Base, GENERATE) \ + \ + COMPUTE_SHADER(GENERATE_IMPORTANCE_MAP, GenerateImportanceMap, GENERATE_IMPORTANCE_MAP) \ + COMPUTE_SHADER(POSTPROCESS_IMPORTANCE_MAP_A, PostprocessImportanceMapA, POSTPROCESS_IMPORTANCE_MAP_A) \ + COMPUTE_SHADER(POSTPROCESS_IMPORTANCE_MAP_B, PostprocessImportanceMapB, POSTPROCESS_IMPORTANCE_MAP_B) \ + \ + COMPUTE_SHADER(EDGE_SENSITIVE_BLUR_1, EdgeSensitiveBlur1, EDGE_SENSITIVE_BLUR) \ + COMPUTE_SHADER(EDGE_SENSITIVE_BLUR_2, EdgeSensitiveBlur2, EDGE_SENSITIVE_BLUR) \ + COMPUTE_SHADER(EDGE_SENSITIVE_BLUR_3, EdgeSensitiveBlur3, EDGE_SENSITIVE_BLUR) \ + COMPUTE_SHADER(EDGE_SENSITIVE_BLUR_4, EdgeSensitiveBlur4, EDGE_SENSITIVE_BLUR) \ + COMPUTE_SHADER(EDGE_SENSITIVE_BLUR_5, EdgeSensitiveBlur5, EDGE_SENSITIVE_BLUR) \ + COMPUTE_SHADER(EDGE_SENSITIVE_BLUR_6, EdgeSensitiveBlur6, EDGE_SENSITIVE_BLUR) \ + COMPUTE_SHADER(EDGE_SENSITIVE_BLUR_7, EdgeSensitiveBlur7, EDGE_SENSITIVE_BLUR) \ + COMPUTE_SHADER(EDGE_SENSITIVE_BLUR_8, EdgeSensitiveBlur8, EDGE_SENSITIVE_BLUR) \ + \ + COMPUTE_SHADER(APPLY, Apply, APPLY) \ + COMPUTE_SHADER(NON_SMART_APPLY, NonSmartApply, APPLY) \ + COMPUTE_SHADER(NON_SMART_HALF_APPLY, NonSmartHalfApply, APPLY) \ + \ + COMPUTE_SHADER(UPSCALE_BILATERAL_5X5_SMART, UpscaleBilateral5x5Smart, BILATERAL_UPSAMPLE) \ + COMPUTE_SHADER(UPSCALE_BILATERAL_5X5_NON_SMART, UpscaleBilateral5x5NonSmart, BILATERAL_UPSAMPLE) \ + COMPUTE_SHADER(UPSCALE_BILATERAL_5X5_HALF, UpscaleBilateral5x5Half, BILATERAL_UPSAMPLE) + +typedef enum ComputeShaderID { +#define COMPUTE_SHADER(name, _pascal_name, _descriptor_set) CS_##name, + COMPUTE_SHADERS +#undef COMPUTE_SHADER + NUM_COMPUTE_SHADERS +} ComputeShaderID; + +typedef struct ComputeShaderMetaData { + const char *name; + DescriptorSetLayoutID descriptorSetLayoutID; + const char *objectName; + const char *rootSignatureName; +} ComputeShaderMetaData; + +typedef struct ComputeShaderSPIRV { + const uint32_t *spirv; + size_t len; +} ComputeShaderSPIRV; + +typedef struct ComputeShaderDXIL { + const void *dxil; + size_t len; +} ComputeShaderDXIL; + +#ifdef FFX_CACAO_ENABLE_VULKAN +static const ComputeShaderSPIRV COMPUTE_SHADER_SPIRV_32[] = { +#define COMPUTE_SHADER(name, pascal_name, descriptor_set_layout) { (uint32_t*)CS##pascal_name##SPIRV32, FFX_CACAO_ARRAY_SIZE(CS##pascal_name##SPIRV32) }, + COMPUTE_SHADERS +#undef COMPUTE_SHADER +}; + +static const ComputeShaderSPIRV COMPUTE_SHADER_SPIRV_16[] = { +#define COMPUTE_SHADER(name, pascal_name, descriptor_set_layout) { (uint32_t*)CS##pascal_name##SPIRV16, FFX_CACAO_ARRAY_SIZE(CS##pascal_name##SPIRV16) }, + COMPUTE_SHADERS +#undef COMPUTE_SHADER +}; +#endif + +#ifdef FFX_CACAO_ENABLE_D3D12 +static const ComputeShaderDXIL COMPUTE_SHADER_DXIL[] = { +#define COMPUTE_SHADER(name, pascal_name, descriptor_set_layout) { CS##pascal_name##DXIL, sizeof(CS##pascal_name##DXIL) }, + COMPUTE_SHADERS +#undef COMPUTE_SHADER +}; +#endif + +static const ComputeShaderMetaData COMPUTE_SHADER_META_DATA[] = { +#define COMPUTE_SHADER(name, pascal_name, descriptor_set_layout) { "FFX_CACAO_"#pascal_name, DSL_##descriptor_set_layout, "FFX_CACAO_CS_"#name, "FFX_CACAO_RS_"#name }, + COMPUTE_SHADERS +#undef COMPUTE_SHADER +}; + + +// ================================================================================= +// DirectX 12 +// ================================================================================= + +#ifdef FFX_CACAO_ENABLE_D3D12 + +static inline FFX_CACAO_Status hresultToFFX_CACAO_Status(HRESULT hr) +{ + switch (hr) + { + case E_FAIL: return FFX_CACAO_STATUS_FAILED; + case E_INVALIDARG: return FFX_CACAO_STATUS_INVALID_ARGUMENT; + case E_OUTOFMEMORY: return FFX_CACAO_STATUS_OUT_OF_MEMORY; + case E_NOTIMPL: return FFX_CACAO_STATUS_INVALID_ARGUMENT; + case S_FALSE: return FFX_CACAO_STATUS_OK; + case S_OK: return FFX_CACAO_STATUS_OK; + default: return FFX_CACAO_STATUS_FAILED; + } +} + +static inline void SetName(ID3D12Object* obj, const char* name) +{ + if (name == NULL) + { + return; + } + + FFX_CACAO_ASSERT(obj != NULL); + wchar_t buffer[1024]; + swprintf(buffer, FFX_CACAO_ARRAY_SIZE(buffer), L"%S", name); + obj->SetName(buffer); +} + +static inline size_t AlignOffset(size_t uOffset, size_t uAlign) +{ + return ((uOffset + (uAlign - 1)) & ~(uAlign - 1)); +} + +static size_t GetPixelByteSize(DXGI_FORMAT fmt) +{ + switch (fmt) + { + case(DXGI_FORMAT_R10G10B10A2_TYPELESS): + case(DXGI_FORMAT_R10G10B10A2_UNORM): + case(DXGI_FORMAT_R10G10B10A2_UINT): + case(DXGI_FORMAT_R11G11B10_FLOAT): + case(DXGI_FORMAT_R8G8B8A8_TYPELESS): + case(DXGI_FORMAT_R8G8B8A8_UNORM): + case(DXGI_FORMAT_R8G8B8A8_UNORM_SRGB): + case(DXGI_FORMAT_R8G8B8A8_UINT): + case(DXGI_FORMAT_R8G8B8A8_SNORM): + case(DXGI_FORMAT_R8G8B8A8_SINT): + case(DXGI_FORMAT_B8G8R8A8_UNORM): + case(DXGI_FORMAT_B8G8R8X8_UNORM): + case(DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM): + case(DXGI_FORMAT_B8G8R8A8_TYPELESS): + case(DXGI_FORMAT_B8G8R8A8_UNORM_SRGB): + case(DXGI_FORMAT_B8G8R8X8_TYPELESS): + case(DXGI_FORMAT_B8G8R8X8_UNORM_SRGB): + case(DXGI_FORMAT_R16G16_TYPELESS): + case(DXGI_FORMAT_R16G16_FLOAT): + case(DXGI_FORMAT_R16G16_UNORM): + case(DXGI_FORMAT_R16G16_UINT): + case(DXGI_FORMAT_R16G16_SNORM): + case(DXGI_FORMAT_R16G16_SINT): + case(DXGI_FORMAT_R32_TYPELESS): + case(DXGI_FORMAT_D32_FLOAT): + case(DXGI_FORMAT_R32_FLOAT): + case(DXGI_FORMAT_R32_UINT): + case(DXGI_FORMAT_R32_SINT): + return 4; + + case(DXGI_FORMAT_BC1_TYPELESS): + case(DXGI_FORMAT_BC1_UNORM): + case(DXGI_FORMAT_BC1_UNORM_SRGB): + case(DXGI_FORMAT_BC4_TYPELESS): + case(DXGI_FORMAT_BC4_UNORM): + case(DXGI_FORMAT_BC4_SNORM): + case(DXGI_FORMAT_R16G16B16A16_FLOAT): + case(DXGI_FORMAT_R16G16B16A16_TYPELESS): + return 8; + + case(DXGI_FORMAT_BC2_TYPELESS): + case(DXGI_FORMAT_BC2_UNORM): + case(DXGI_FORMAT_BC2_UNORM_SRGB): + case(DXGI_FORMAT_BC3_TYPELESS): + case(DXGI_FORMAT_BC3_UNORM): + case(DXGI_FORMAT_BC3_UNORM_SRGB): + case(DXGI_FORMAT_BC5_TYPELESS): + case(DXGI_FORMAT_BC5_UNORM): + case(DXGI_FORMAT_BC5_SNORM): + case(DXGI_FORMAT_BC6H_TYPELESS): + case(DXGI_FORMAT_BC6H_UF16): + case(DXGI_FORMAT_BC6H_SF16): + case(DXGI_FORMAT_BC7_TYPELESS): + case(DXGI_FORMAT_BC7_UNORM): + case(DXGI_FORMAT_BC7_UNORM_SRGB): + case(DXGI_FORMAT_R32G32B32A32_FLOAT): + case(DXGI_FORMAT_R32G32B32A32_TYPELESS): + return 16; + + default: + FFX_CACAO_ASSERT(0); + break; + } + return 0; +} + +// ================================================================================================= +// GpuTimer implementation +// ================================================================================================= + +#ifdef FFX_CACAO_ENABLE_PROFILING +#define GPU_TIMER_MAX_VALUES_PER_FRAME (FFX_CACAO_ARRAY_SIZE(((FFX_CACAO_DetailedTiming*)0)->timestamps)) + +typedef struct D3D12Timestamp { + TimestampID timestampID; + uint64_t value; +} D3D12Timestamp; + +typedef struct GpuTimer { + ID3D12Resource *buffer; + ID3D12QueryHeap *queryHeap; + uint32_t currentFrame; + uint32_t collectFrame; + struct { + uint32_t len; + D3D12Timestamp timestamps[NUM_TIMESTAMPS]; + } timestampBuffers[NUM_TIMESTAMP_BUFFERS]; + +} GpuTimer; + +static FFX_CACAO_Status gpuTimerInit(GpuTimer* gpuTimer, ID3D12Device* device) +{ + memset(gpuTimer, 0, sizeof(*gpuTimer)); + + D3D12_QUERY_HEAP_DESC queryHeapDesc = {}; + queryHeapDesc.Type = D3D12_QUERY_HEAP_TYPE_TIMESTAMP; + queryHeapDesc.Count = GPU_TIMER_MAX_VALUES_PER_FRAME * NUM_TIMESTAMP_BUFFERS; + queryHeapDesc.NodeMask = 0; + HRESULT hr = device->CreateQueryHeap(&queryHeapDesc, IID_PPV_ARGS(&gpuTimer->queryHeap)); + if (FAILED(hr)) + { + return hresultToFFX_CACAO_Status(hr); + } + + hr = device->CreateCommittedResource( + &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_READBACK), + D3D12_HEAP_FLAG_NONE, + &CD3DX12_RESOURCE_DESC::Buffer(sizeof(uint64_t) * NUM_TIMESTAMP_BUFFERS * GPU_TIMER_MAX_VALUES_PER_FRAME), + D3D12_RESOURCE_STATE_COPY_DEST, + nullptr, + IID_PPV_ARGS(&gpuTimer->buffer)); + if (FAILED(hr)) + { + FFX_CACAO_ASSERT(gpuTimer->queryHeap); + gpuTimer->queryHeap->Release(); + return hresultToFFX_CACAO_Status(hr); + } + + SetName(gpuTimer->buffer, "CACAO::GPUTimer::buffer"); + + return FFX_CACAO_STATUS_OK; +} + +static void gpuTimerDestroy(GpuTimer* gpuTimer) +{ + FFX_CACAO_ASSERT(gpuTimer->buffer); + FFX_CACAO_ASSERT(gpuTimer->queryHeap); + gpuTimer->buffer->Release(); + gpuTimer->queryHeap->Release(); +} + +static void gpuTimerStartFrame(GpuTimer* gpuTimer) +{ + uint32_t frame = gpuTimer->currentFrame = (gpuTimer->currentFrame + 1) % NUM_TIMESTAMP_BUFFERS; + gpuTimer->timestampBuffers[frame].len = 0; + + uint32_t collectFrame = gpuTimer->collectFrame = (frame + 1) % NUM_TIMESTAMP_BUFFERS; + + uint32_t numMeasurements = gpuTimer->timestampBuffers[collectFrame].len; + if (!numMeasurements) + { + return; + } + + uint32_t start = GPU_TIMER_MAX_VALUES_PER_FRAME * collectFrame; + uint32_t end = GPU_TIMER_MAX_VALUES_PER_FRAME * (collectFrame + 1); + + D3D12_RANGE readRange; + readRange.Begin = start * sizeof(uint64_t); + readRange.End = end * sizeof(uint64_t); + uint64_t *timingsInTicks = NULL; + gpuTimer->buffer->Map(0, &readRange, (void**)&timingsInTicks); + + for (uint32_t i = 0; i < numMeasurements; ++i) + { + gpuTimer->timestampBuffers[collectFrame].timestamps[i].value = timingsInTicks[start + i]; + } + + D3D12_RANGE writtenRange = {}; + writtenRange.Begin = 0; + writtenRange.End = 0; + gpuTimer->buffer->Unmap(0, &writtenRange); +} + +static void gpuTimerGetTimestamp(GpuTimer* gpuTimer, ID3D12GraphicsCommandList* commandList, TimestampID timestampID) +{ + uint32_t frame = gpuTimer->currentFrame; + uint32_t curTimestamp = gpuTimer->timestampBuffers[frame].len++; + FFX_CACAO_ASSERT(curTimestamp < GPU_TIMER_MAX_VALUES_PER_FRAME); + gpuTimer->timestampBuffers[frame].timestamps[curTimestamp].timestampID = timestampID; + commandList->EndQuery(gpuTimer->queryHeap, D3D12_QUERY_TYPE_TIMESTAMP, frame * GPU_TIMER_MAX_VALUES_PER_FRAME + curTimestamp); +} + +static void gpuTimerEndFrame(GpuTimer* gpuTimer, ID3D12GraphicsCommandList* commandList) +{ + uint32_t frame = gpuTimer->currentFrame; + uint32_t numTimestamps = gpuTimer->timestampBuffers[frame].len; + commandList->ResolveQueryData( + gpuTimer->queryHeap, + D3D12_QUERY_TYPE_TIMESTAMP, + frame * GPU_TIMER_MAX_VALUES_PER_FRAME, + numTimestamps, + gpuTimer->buffer, + frame * GPU_TIMER_MAX_VALUES_PER_FRAME * sizeof(uint64_t)); +} + +static void gpuTimerCollectTimings(GpuTimer* gpuTimer, FFX_CACAO_DetailedTiming* timings) +{ + uint32_t frame = gpuTimer->collectFrame; + uint32_t numTimestamps = timings->numTimestamps = gpuTimer->timestampBuffers[frame].len; + + uint64_t prevTimeTicks = gpuTimer->timestampBuffers[frame].timestamps[0].value; + for (uint32_t i = 1; i < numTimestamps; ++i) + { + uint64_t thisTimeTicks = gpuTimer->timestampBuffers[frame].timestamps[i].value; + FFX_CACAO_Timestamp *t = &timings->timestamps[i]; + t->label = TIMESTAMP_NAMES[gpuTimer->timestampBuffers[frame].timestamps[i].timestampID]; + t->ticks = thisTimeTicks - prevTimeTicks; + prevTimeTicks = thisTimeTicks; + } + + timings->timestamps[0].label = "FFX_CACAO_TOTAL"; + timings->timestamps[0].ticks = prevTimeTicks - gpuTimer->timestampBuffers[frame].timestamps[0].value; +} +#endif + +// ================================================================================================= +// CbvSrvUav implementation +// ================================================================================================= + +typedef struct CbvSrvUav { + uint32_t size; + uint32_t descriptorSize; + D3D12_CPU_DESCRIPTOR_HANDLE cpuDescriptor; + D3D12_GPU_DESCRIPTOR_HANDLE gpuDescriptor; + D3D12_CPU_DESCRIPTOR_HANDLE cpuVisibleCpuDescriptor; +} CbvSrvUav; + +static D3D12_CPU_DESCRIPTOR_HANDLE cbvSrvUavGetCpu(CbvSrvUav* cbvSrvUav, uint32_t i) +{ + D3D12_CPU_DESCRIPTOR_HANDLE cpuDescriptor = cbvSrvUav->cpuDescriptor; + cpuDescriptor.ptr += i * cbvSrvUav->descriptorSize; + return cpuDescriptor; +} + +static D3D12_CPU_DESCRIPTOR_HANDLE cbvSrvUavGetCpuVisibleCpu(CbvSrvUav* cbvSrvUav, uint32_t i) +{ + D3D12_CPU_DESCRIPTOR_HANDLE cpuDescriptor = cbvSrvUav->cpuVisibleCpuDescriptor; + cpuDescriptor.ptr += i * cbvSrvUav->descriptorSize; + return cpuDescriptor; +} + +static D3D12_GPU_DESCRIPTOR_HANDLE cbvSrvUavGetGpu(CbvSrvUav* cbvSrvUav, uint32_t i) +{ + D3D12_GPU_DESCRIPTOR_HANDLE gpuDescriptor = cbvSrvUav->gpuDescriptor; + gpuDescriptor.ptr += i * cbvSrvUav->descriptorSize; + return gpuDescriptor; +} + +// ================================================================================================= +// CbvSrvUavHeap implementation +// ================================================================================================= + +typedef struct CbvSrvUavHeap { + uint32_t index; + uint32_t descriptorCount; + uint32_t descriptorElementSize; + ID3D12DescriptorHeap *heap; + ID3D12DescriptorHeap *cpuVisibleHeap; +} ResourceViewHeap; + +static FFX_CACAO_Status cbvSrvUavHeapInit(CbvSrvUavHeap* cbvSrvUavHeap, ID3D12Device* device, uint32_t descriptorCount) +{ + FFX_CACAO_ASSERT(cbvSrvUavHeap); + FFX_CACAO_ASSERT(device); + + cbvSrvUavHeap->descriptorCount = descriptorCount; + cbvSrvUavHeap->index = 0; + + cbvSrvUavHeap->descriptorElementSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + + D3D12_DESCRIPTOR_HEAP_DESC descHeap; + descHeap.NumDescriptors = descriptorCount; + descHeap.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; + descHeap.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; + descHeap.NodeMask = 0; + + HRESULT hr = device->CreateDescriptorHeap(&descHeap, IID_PPV_ARGS(&cbvSrvUavHeap->heap)); + if (FAILED(hr)) + { + return hresultToFFX_CACAO_Status(hr); + } + + SetName(cbvSrvUavHeap->heap, "FFX_CACAO_CbvSrvUavHeap"); + + D3D12_DESCRIPTOR_HEAP_DESC cpuVisibleDescHeap; + cpuVisibleDescHeap.NumDescriptors = descriptorCount; + cpuVisibleDescHeap.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; + cpuVisibleDescHeap.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; + cpuVisibleDescHeap.NodeMask = 0; + + hr = device->CreateDescriptorHeap(&cpuVisibleDescHeap, IID_PPV_ARGS(&cbvSrvUavHeap->cpuVisibleHeap)); + if (FAILED(hr)) + { + FFX_CACAO_ASSERT(cbvSrvUavHeap->heap); + cbvSrvUavHeap->heap->Release(); + return hresultToFFX_CACAO_Status(hr); + } + + SetName(cbvSrvUavHeap->cpuVisibleHeap, "FFX_CACAO_CbvSrvUavCpuVisibleHeap"); + return FFX_CACAO_STATUS_OK; +} + +static void cbvSrvUavHeapDestroy(CbvSrvUavHeap* cbvSrvUavHeap) +{ + FFX_CACAO_ASSERT(cbvSrvUavHeap); + FFX_CACAO_ASSERT(cbvSrvUavHeap->heap); + FFX_CACAO_ASSERT(cbvSrvUavHeap->cpuVisibleHeap); + cbvSrvUavHeap->heap->Release(); + cbvSrvUavHeap->cpuVisibleHeap->Release(); +} + +static void cbvSrvUavHeapAllocDescriptor(CbvSrvUavHeap* cbvSrvUavHeap, CbvSrvUav* cbvSrvUav, uint32_t size) +{ + FFX_CACAO_ASSERT(cbvSrvUavHeap); + FFX_CACAO_ASSERT(cbvSrvUav); + FFX_CACAO_ASSERT(cbvSrvUavHeap->index + size <= cbvSrvUavHeap->descriptorCount); + + D3D12_CPU_DESCRIPTOR_HANDLE cpuView = cbvSrvUavHeap->heap->GetCPUDescriptorHandleForHeapStart(); + cpuView.ptr += cbvSrvUavHeap->index * cbvSrvUavHeap->descriptorElementSize; + + D3D12_GPU_DESCRIPTOR_HANDLE gpuView = cbvSrvUavHeap->heap->GetGPUDescriptorHandleForHeapStart(); + gpuView.ptr += cbvSrvUavHeap->index * cbvSrvUavHeap->descriptorElementSize; + + D3D12_CPU_DESCRIPTOR_HANDLE cpuVisibleCpuView = cbvSrvUavHeap->cpuVisibleHeap->GetCPUDescriptorHandleForHeapStart(); + cpuVisibleCpuView.ptr += cbvSrvUavHeap->index * cbvSrvUavHeap->descriptorElementSize; + + cbvSrvUavHeap->index += size; + + cbvSrvUav->size = size; + cbvSrvUav->descriptorSize = cbvSrvUavHeap->descriptorElementSize; + cbvSrvUav->cpuDescriptor = cpuView; + cbvSrvUav->gpuDescriptor = gpuView; + cbvSrvUav->cpuVisibleCpuDescriptor = cpuVisibleCpuView; +} + +// ================================================================================================= +// ConstantBufferRing implementation +// ================================================================================================= + +typedef struct ConstantBufferRing { + size_t pageSize; + size_t totalSize; + size_t currentOffset; + uint32_t currentPage; + uint32_t numPages; + char *data; + ID3D12Resource *buffer; +} ConstantBufferRing; + +static FFX_CACAO_Status constantBufferRingInit(ConstantBufferRing* constantBufferRing, ID3D12Device* device, uint32_t numPages, size_t pageSize) +{ + FFX_CACAO_ASSERT(constantBufferRing); + FFX_CACAO_ASSERT(device); + + pageSize = AlignOffset(pageSize, 256); + size_t totalSize = numPages * pageSize; + char *data = NULL; + ID3D12Resource *buffer = NULL; + + HRESULT hr = device->CreateCommittedResource( + &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD), + D3D12_HEAP_FLAG_NONE, + &CD3DX12_RESOURCE_DESC::Buffer(totalSize), + D3D12_RESOURCE_STATE_GENERIC_READ, + nullptr, + IID_PPV_ARGS(&buffer)); + if (FAILED(hr)) + { + return hresultToFFX_CACAO_Status(hr); + } + + SetName(buffer, "DynamicBufferRing::m_pBuffer"); + + buffer->Map(0, NULL, (void**)&data); + + constantBufferRing->pageSize = pageSize; + constantBufferRing->totalSize = totalSize; + constantBufferRing->currentOffset = 0; + constantBufferRing->currentPage = 0; + constantBufferRing->numPages = numPages; + constantBufferRing->data = data; + constantBufferRing->buffer = buffer; + + return FFX_CACAO_STATUS_OK; +} + +static void constantBufferRingDestroy(ConstantBufferRing* constantBufferRing) +{ + FFX_CACAO_ASSERT(constantBufferRing); + FFX_CACAO_ASSERT(constantBufferRing->buffer); + constantBufferRing->buffer->Release(); +} + +static void constantBufferRingStartFrame(ConstantBufferRing* constantBufferRing) +{ + FFX_CACAO_ASSERT(constantBufferRing); + constantBufferRing->currentPage = (constantBufferRing->currentPage + 1) % constantBufferRing->numPages; + constantBufferRing->currentOffset = 0; +} + +static void constantBufferRingAlloc(ConstantBufferRing* constantBufferRing, size_t size, void **data, D3D12_GPU_VIRTUAL_ADDRESS *bufferViewDesc) +{ + FFX_CACAO_ASSERT(constantBufferRing); + size = AlignOffset(size, 256); + FFX_CACAO_ASSERT(constantBufferRing->currentOffset + size <= constantBufferRing->pageSize); + + size_t memOffset = constantBufferRing->pageSize * constantBufferRing->currentPage + constantBufferRing->currentOffset; + *data = constantBufferRing->data + memOffset; + constantBufferRing->currentOffset += size; + + *bufferViewDesc = constantBufferRing->buffer->GetGPUVirtualAddress() + memOffset; +} + +// ================================================================================================= +// Texture implementation +// ================================================================================================= + +typedef struct Texture { + ID3D12Resource *resource; + DXGI_FORMAT format; + uint32_t width; + uint32_t height; + uint32_t arraySize; + uint32_t mipMapCount; +} Texture; + +static FFX_CACAO_Status textureInit(Texture* texture, ID3D12Device* device, const char* name, const CD3DX12_RESOURCE_DESC* desc, D3D12_RESOURCE_STATES initialState, const D3D12_CLEAR_VALUE* clearValue) +{ + FFX_CACAO_ASSERT(texture); + FFX_CACAO_ASSERT(device); + FFX_CACAO_ASSERT(name); + FFX_CACAO_ASSERT(desc); + + HRESULT hr = device->CreateCommittedResource( + &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), + D3D12_HEAP_FLAG_NONE, + desc, + initialState, + clearValue, + IID_PPV_ARGS(&texture->resource)); + if (FAILED(hr)) + { + return hresultToFFX_CACAO_Status(hr); + } + + texture->format = desc->Format; + texture->width = (uint32_t)desc->Width; + texture->height = desc->Height; + texture->arraySize = desc->DepthOrArraySize; + texture->mipMapCount = desc->MipLevels; + + SetName(texture->resource, name); + + return FFX_CACAO_STATUS_OK; +} + +static void textureDestroy(Texture* texture) +{ + FFX_CACAO_ASSERT(texture); + FFX_CACAO_ASSERT(texture->resource); + texture->resource->Release(); +} + +static void textureCreateSrvFromDesc(Texture* texture, uint32_t index, CbvSrvUav* srv, const D3D12_SHADER_RESOURCE_VIEW_DESC* srvDesc) +{ + FFX_CACAO_ASSERT(texture); + FFX_CACAO_ASSERT(srv); + FFX_CACAO_ASSERT(srvDesc); + + ID3D12Device* device; + texture->resource->GetDevice(__uuidof(*device), (void**)&device); + + device->CreateShaderResourceView(texture->resource, srvDesc, cbvSrvUavGetCpu(srv, index)); + device->CreateShaderResourceView(texture->resource, srvDesc, cbvSrvUavGetCpuVisibleCpu(srv, index)); + + device->Release(); +} + +static void textureCreateUavFromDesc(Texture* texture, uint32_t index, CbvSrvUav* uav, const D3D12_UNORDERED_ACCESS_VIEW_DESC* uavDesc) +{ + FFX_CACAO_ASSERT(texture); + FFX_CACAO_ASSERT(uav); + FFX_CACAO_ASSERT(uavDesc); + + ID3D12Device* device; + texture->resource->GetDevice(__uuidof(*device), (void**)&device); + + device->CreateUnorderedAccessView(texture->resource, NULL, uavDesc, cbvSrvUavGetCpu(uav, index)); + device->CreateUnorderedAccessView(texture->resource, NULL, uavDesc, cbvSrvUavGetCpuVisibleCpu(uav, index)); + + device->Release(); +} + +// ================================================================================================= +// CACAO implementation +// ================================================================================================= + +struct FFX_CACAO_D3D12Context { + FFX_CACAO_Settings settings; + FFX_CACAO_Bool useDownsampledSsao; + + ID3D12Device *device; + CbvSrvUavHeap cbvSrvUavHeap; + +#ifdef FFX_CACAO_ENABLE_PROFILING + GpuTimer gpuTimer; +#endif + + ConstantBufferRing constantBufferRing; + FFX_CACAO_BufferSizeInfo bufferSizeInfo; + ID3D12Resource *outputResource; + + Texture loadCounter; + + CbvSrvUav loadCounterUav; // required for LoadCounter clear + + ID3D12RootSignature *csRootSignatures[NUM_COMPUTE_SHADERS]; + ID3D12PipelineState *computeShader[NUM_COMPUTE_SHADERS]; + + ID3D12Resource *textures[NUM_TEXTURES]; + CbvSrvUav inputDescriptors[NUM_DESCRIPTOR_SETS]; + CbvSrvUav outputDescriptors[NUM_DESCRIPTOR_SETS]; +}; + +static inline FFX_CACAO_D3D12Context* getAlignedD3D12ContextPointer(FFX_CACAO_D3D12Context* ptr) +{ + uintptr_t tmp = (uintptr_t)ptr; + tmp = (tmp + alignof(FFX_CACAO_D3D12Context) - 1) & (~(alignof(FFX_CACAO_D3D12Context) - 1)); + return (FFX_CACAO_D3D12Context*)tmp; +} +#endif + +#ifdef FFX_CACAO_ENABLE_VULKAN +// ================================================================================================= +// CACAO vulkan implementation +// ================================================================================================= + + + +#define MAX_DESCRIPTOR_BINDINGS 32 + + + +#define NUM_BACK_BUFFERS 3 +#define NUM_SAMPLERS 5 +typedef struct FFX_CACAO_VkContext { + FFX_CACAO_Settings settings; + FFX_CACAO_Bool useDownsampledSsao; + FFX_CACAO_BufferSizeInfo bufferSizeInfo; + +#ifdef FFX_CACAO_ENABLE_PROFILING + VkQueryPool timestampQueryPool; + uint32_t collectBuffer; + struct { + TimestampID timestamps[NUM_TIMESTAMPS]; + uint64_t timings[NUM_TIMESTAMPS]; + uint32_t numTimestamps; + } timestampQueries[NUM_BACK_BUFFERS]; +#endif + + VkPhysicalDevice physicalDevice; + VkDevice device; + PFN_vkCmdDebugMarkerBeginEXT vkCmdDebugMarkerBegin; + PFN_vkCmdDebugMarkerEndEXT vkCmdDebugMarkerEnd; + PFN_vkSetDebugUtilsObjectNameEXT vkSetDebugUtilsObjectName; + + + VkDescriptorSetLayout descriptorSetLayouts[NUM_DESCRIPTOR_SET_LAYOUTS]; + VkPipelineLayout pipelineLayouts[NUM_DESCRIPTOR_SET_LAYOUTS]; + + VkShaderModule computeShaders[NUM_COMPUTE_SHADERS]; + VkPipeline computePipelines[NUM_COMPUTE_SHADERS]; + + VkDescriptorSet descriptorSets[NUM_BACK_BUFFERS][NUM_DESCRIPTOR_SETS]; + VkDescriptorPool descriptorPool; + + VkSampler samplers[NUM_SAMPLERS]; + + VkImage textures[NUM_TEXTURES]; + VkDeviceMemory textureMemory[NUM_TEXTURES]; + VkImageView shaderResourceViews[NUM_SHADER_RESOURCE_VIEWS]; + VkImageView unorderedAccessViews[NUM_UNORDERED_ACCESS_VIEWS]; + + VkImage loadCounter; + VkDeviceMemory loadCounterMemory; + VkImageView loadCounterView; + + VkImage output; + + uint32_t currentConstantBuffer; + VkBuffer constantBuffer[NUM_BACK_BUFFERS][4]; + VkDeviceMemory constantBufferMemory[NUM_BACK_BUFFERS][4]; +} FFX_CACAO_VkContext; + +static inline FFX_CACAO_VkContext* getAlignedVkContextPointer(FFX_CACAO_VkContext* ptr) +{ + uintptr_t tmp = (uintptr_t)ptr; + tmp = (tmp + alignof(FFX_CACAO_VkContext) - 1) & (~(alignof(FFX_CACAO_VkContext) - 1)); + return (FFX_CACAO_VkContext*)tmp; +} +#endif + +// ================================================================================= +// Interface +// ================================================================================= + +#ifdef __cplusplus +extern "C" +{ +#endif + +#ifdef FFX_CACAO_ENABLE_D3D12 +size_t FFX_CACAO_D3D12GetContextSize() +{ + return sizeof(FFX_CACAO_D3D12Context) + alignof(FFX_CACAO_D3D12Context) - 1; +} + +FFX_CACAO_Status FFX_CACAO_D3D12InitContext(FFX_CACAO_D3D12Context* context, ID3D12Device* device) +{ + if (context == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + if (device == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + context = getAlignedD3D12ContextPointer(context); + + uint32_t numInputDescriptorsInited = 0; + uint32_t numOutputDescriptorsInited = 0; + uint32_t numRootSignaturesInited = 0; + uint32_t numComputeShadersInited = 0; + +#define COMPUTE_SHADER_INIT(name, entryPoint, uavSize, srvSize) \ + errorStatus = computeShaderInit(&context->name, device, #entryPoint, entryPoint ## DXIL, sizeof(entryPoint ## DXIL), uavSize, srvSize, samplers, FFX_CACAO_ARRAY_SIZE(samplers)); \ + if (errorStatus) \ + { \ + goto error_create_ ## entryPoint; \ + } +#define ERROR_COMPUTE_SHADER_DESTROY(name, entryPoint) \ + computeShaderDestroy(&context->name); \ +error_create_ ## entryPoint: + + FFX_CACAO_Status errorStatus = FFX_CACAO_STATUS_FAILED; + + context->device = device; + CbvSrvUavHeap *cbvSrvUavHeap = &context->cbvSrvUavHeap; + errorStatus = cbvSrvUavHeapInit(cbvSrvUavHeap, device, 512); + if (errorStatus) + { + goto error_create_cbv_srv_uav_heap; + } + errorStatus = constantBufferRingInit(&context->constantBufferRing, device, 5, 1024 * 5); + if (errorStatus) + { + goto error_create_constant_buffer_ring; + } +#ifdef FFX_CACAO_ENABLE_PROFILING + errorStatus = gpuTimerInit(&context->gpuTimer, device); + if (errorStatus) + { + goto error_create_gpu_timer; + } +#endif + + D3D12_STATIC_SAMPLER_DESC samplers[5] = { }; + + samplers[0].Filter = D3D12_FILTER_MIN_MAG_MIP_POINT; + samplers[0].AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + samplers[0].AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + samplers[0].AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + samplers[0].ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; + samplers[0].BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK; + samplers[0].MinLOD = 0.0f; + samplers[0].MaxLOD = D3D12_FLOAT32_MAX; + samplers[0].MipLODBias = 0; + samplers[0].MaxAnisotropy = 1; + samplers[0].ShaderRegister = 0; + samplers[0].RegisterSpace = 0; + samplers[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + + samplers[1].Filter = D3D12_FILTER_MIN_MAG_MIP_POINT; + samplers[1].AddressU = D3D12_TEXTURE_ADDRESS_MODE_MIRROR; + samplers[1].AddressV = D3D12_TEXTURE_ADDRESS_MODE_MIRROR; + samplers[1].AddressW = D3D12_TEXTURE_ADDRESS_MODE_MIRROR; + samplers[1].ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; + samplers[1].BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK; + samplers[1].MinLOD = 0.0f; + samplers[1].MaxLOD = D3D12_FLOAT32_MAX; + samplers[1].MipLODBias = 0; + samplers[1].MaxAnisotropy = 1; + samplers[1].ShaderRegister = 1; + samplers[1].RegisterSpace = 0; + samplers[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + + samplers[2].Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR; + samplers[2].AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + samplers[2].AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + samplers[2].AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + samplers[2].ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; + samplers[2].BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK; + samplers[2].MinLOD = 0.0f; + samplers[2].MaxLOD = D3D12_FLOAT32_MAX; + samplers[2].MipLODBias = 0; + samplers[2].MaxAnisotropy = 1; + samplers[2].ShaderRegister = 2; + samplers[2].RegisterSpace = 0; + samplers[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + + samplers[3].Filter = D3D12_FILTER_MIN_MAG_MIP_POINT; + samplers[3].AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + samplers[3].AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + samplers[3].AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + samplers[3].ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; + samplers[3].BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK; + samplers[3].MinLOD = 0.0f; + samplers[3].MaxLOD = D3D12_FLOAT32_MAX; + samplers[3].MipLODBias = 0; + samplers[3].MaxAnisotropy = 1; + samplers[3].ShaderRegister = 3; + samplers[3].RegisterSpace = 0; + samplers[3].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + + samplers[4].Filter = D3D12_FILTER_MIN_MAG_MIP_POINT; + samplers[4].AddressU = D3D12_TEXTURE_ADDRESS_MODE_BORDER; + samplers[4].AddressV = D3D12_TEXTURE_ADDRESS_MODE_BORDER; + samplers[4].AddressW = D3D12_TEXTURE_ADDRESS_MODE_BORDER; + samplers[4].ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; + samplers[4].BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK; + samplers[4].MinLOD = 0.0f; + samplers[4].MaxLOD = D3D12_FLOAT32_MAX; + samplers[4].MipLODBias = 0; + samplers[4].MaxAnisotropy = 1; + samplers[4].ShaderRegister = 4; + samplers[4].RegisterSpace = 0; + samplers[4].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + + errorStatus = textureInit(&context->loadCounter, device, "CACAO::m_loadCounter", &CD3DX12_RESOURCE_DESC::Tex1D(DXGI_FORMAT_R32_UINT, 1, 1, 1, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, NULL); + if (errorStatus) + { + goto error_create_load_counter_texture; + } + + // create uav for load counter + { + D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc = {}; + uavDesc.Format = DXGI_FORMAT_R32_UINT; + uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE1D; + uavDesc.Texture1D.MipSlice = 0; + + cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->loadCounterUav, 1); // required for clearing the load counter + textureCreateUavFromDesc(&context->loadCounter, 0, &context->loadCounterUav, &uavDesc); + } + + for (; numInputDescriptorsInited < NUM_DESCRIPTOR_SETS; ++numInputDescriptorsInited) + { + uint32_t size = DESCRIPTOR_SET_LAYOUT_META_DATA[DESCRIPTOR_SET_META_DATA[numInputDescriptorsInited].descriptorSetLayoutID].numInputs; + cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->inputDescriptors[numInputDescriptorsInited], size); + } + + for (; numOutputDescriptorsInited < NUM_DESCRIPTOR_SETS; ++numOutputDescriptorsInited) + { + uint32_t size = DESCRIPTOR_SET_LAYOUT_META_DATA[DESCRIPTOR_SET_META_DATA[numOutputDescriptorsInited].descriptorSetLayoutID].numOutputs; + cbvSrvUavHeapAllocDescriptor(cbvSrvUavHeap, &context->outputDescriptors[numOutputDescriptorsInited], size); + } + + for (; numRootSignaturesInited < NUM_COMPUTE_SHADERS; ++numRootSignaturesInited) + { + ComputeShaderMetaData metaData = COMPUTE_SHADER_META_DATA[numRootSignaturesInited]; + DescriptorSetLayoutMetaData dslMetaData = DESCRIPTOR_SET_LAYOUT_META_DATA[metaData.descriptorSetLayoutID]; + + CD3DX12_DESCRIPTOR_RANGE DescRange[4]; + CD3DX12_ROOT_PARAMETER RTSlot[4]; + + // we'll always have a constant buffer + int parameterCount = 0; + DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0); + RTSlot[parameterCount++].InitAsConstantBufferView(0, 0, D3D12_SHADER_VISIBILITY_ALL); + + // if we have a UAV table + if (dslMetaData.numOutputs > 0) + { + DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, dslMetaData.numOutputs, 0); + RTSlot[parameterCount].InitAsDescriptorTable(1, &DescRange[parameterCount], D3D12_SHADER_VISIBILITY_ALL); + ++parameterCount; + } + + // if we have a SRV table + if (dslMetaData.numInputs > 0) + { + DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, dslMetaData.numInputs, 0); + RTSlot[parameterCount].InitAsDescriptorTable(1, &DescRange[parameterCount], D3D12_SHADER_VISIBILITY_ALL); + ++parameterCount; + } + + // the root signature contains 3 slots to be used + CD3DX12_ROOT_SIGNATURE_DESC descRootSignature = CD3DX12_ROOT_SIGNATURE_DESC(); + descRootSignature.NumParameters = parameterCount; + descRootSignature.pParameters = RTSlot; + descRootSignature.NumStaticSamplers = FFX_CACAO_ARRAY_SIZE(samplers); + descRootSignature.pStaticSamplers = samplers; + + // deny uneccessary access to certain pipeline stages + descRootSignature.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE; + + ID3DBlob *outBlob, *errorBlob = NULL; + + HRESULT hr = D3D12SerializeRootSignature(&descRootSignature, D3D_ROOT_SIGNATURE_VERSION_1, &outBlob, &errorBlob); + if (FAILED(hr)) + { + errorStatus = hresultToFFX_CACAO_Status(hr); + goto error_init_root_signature; + } + + if (errorBlob) + { + errorBlob->Release(); + if (outBlob) + { + outBlob->Release(); + } + errorStatus = FFX_CACAO_STATUS_FAILED; + goto error_init_root_signature; + } + + hr = device->CreateRootSignature(0, outBlob->GetBufferPointer(), outBlob->GetBufferSize(), IID_PPV_ARGS(&context->csRootSignatures[numRootSignaturesInited])); + if (FAILED(hr)) + { + outBlob->Release(); + errorStatus = hresultToFFX_CACAO_Status(hr); + goto error_init_root_signature; + } + + SetName(context->csRootSignatures[numRootSignaturesInited], metaData.rootSignatureName); + + outBlob->Release(); + } + + for (; numComputeShadersInited < NUM_COMPUTE_SHADERS; ++numComputeShadersInited) + { + ComputeShaderMetaData metaData = COMPUTE_SHADER_META_DATA[numComputeShadersInited]; + + D3D12_SHADER_BYTECODE shaderByteCode = {}; + shaderByteCode.pShaderBytecode = COMPUTE_SHADER_DXIL[numComputeShadersInited].dxil; + shaderByteCode.BytecodeLength = COMPUTE_SHADER_DXIL[numComputeShadersInited].len; + + D3D12_COMPUTE_PIPELINE_STATE_DESC descPso = {}; + descPso.CS = shaderByteCode; + descPso.Flags = D3D12_PIPELINE_STATE_FLAG_NONE; + descPso.pRootSignature = context->csRootSignatures[numComputeShadersInited]; + descPso.NodeMask = 0; + + HRESULT hr = device->CreateComputePipelineState(&descPso, IID_PPV_ARGS(&context->computeShader[numComputeShadersInited])); + if (FAILED(hr)) + { + goto error_init_compute_shader; + } + + SetName(context->computeShader[numComputeShadersInited], metaData.objectName); + } + + return FFX_CACAO_STATUS_OK; + +error_init_compute_shader: + for (uint32_t i = 0; i < numComputeShadersInited; ++i) + { + context->computeShader[i]->Release(); + } + +error_init_root_signature: + for (uint32_t i = 0; i < numRootSignaturesInited; ++i) + { + context->csRootSignatures[i]->Release(); + } + +error_create_load_counter_texture: + + +#ifdef FFX_CACAO_ENABLE_PROFILING + gpuTimerDestroy(&context->gpuTimer); +error_create_gpu_timer: +#endif + constantBufferRingDestroy(&context->constantBufferRing); +error_create_constant_buffer_ring: + cbvSrvUavHeapDestroy(&context->cbvSrvUavHeap); +error_create_cbv_srv_uav_heap: + + return errorStatus; +} + +FFX_CACAO_Status FFX_CACAO_D3D12DestroyContext(FFX_CACAO_D3D12Context* context) +{ + if (context == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + context = getAlignedD3D12ContextPointer(context); + + for (uint32_t i = 0; i < NUM_COMPUTE_SHADERS; ++i) + { + context->computeShader[i]->Release(); + } + + for (uint32_t i = 0; i < NUM_COMPUTE_SHADERS; ++i) + { + context->csRootSignatures[i]->Release(); + } + + textureDestroy(&context->loadCounter); + +#ifdef FFX_CACAO_ENABLE_PROFILING + gpuTimerDestroy(&context->gpuTimer); +#endif + constantBufferRingDestroy(&context->constantBufferRing); + cbvSrvUavHeapDestroy(&context->cbvSrvUavHeap); + + return FFX_CACAO_STATUS_OK; +} + +FFX_CACAO_Status FFX_CACAO_D3D12InitScreenSizeDependentResources(FFX_CACAO_D3D12Context* context, const FFX_CACAO_D3D12ScreenSizeInfo* info) +{ + if (context == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + if (info == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + context = getAlignedD3D12ContextPointer(context); + + uint32_t numTexturesInited = 0; + uint32_t numInputBindingsInited = 0; + uint32_t numOutputBindingsInited = 0; + + FFX_CACAO_Bool useDownsampledSsao = info->useDownsampledSsao; + context->useDownsampledSsao = useDownsampledSsao; + FFX_CACAO_Status errorStatus; + + ID3D12Device *device = context->device; + + FFX_CACAO_BufferSizeInfo *bsi = &context->bufferSizeInfo; + FFX_CACAO_UpdateBufferSizeInfo(info->width, info->height, useDownsampledSsao, bsi); + + // ======================================= + // Init debug SRVs/UAVs + + context->outputResource = info->outputResource; + + // ======================================= + // Init textures + + for (; numTexturesInited < NUM_TEXTURES; ++numTexturesInited) + { + TextureMetaData metaData = TEXTURE_META_DATA[numTexturesInited]; + + DXGI_FORMAT format = TEXTURE_FORMAT_LOOKUP_D3D12[metaData.format]; + uint32_t width = *(uint32_t*)(((uint8_t*)bsi) + metaData.widthOffset); + uint32_t height = *(uint32_t*)(((uint8_t*)bsi) + metaData.heightOffset); + + D3D12_HEAP_PROPERTIES heapProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT); + D3D12_RESOURCE_DESC desc = CD3DX12_RESOURCE_DESC::Tex2D(format, width, height, metaData.arraySize, metaData.numMips, 1, 0, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS); + + HRESULT hr = device->CreateCommittedResource(&heapProperties, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, NULL, IID_PPV_ARGS(&context->textures[numTexturesInited])); + + if (FAILED(hr)) + { + errorStatus = hresultToFFX_CACAO_Status(hr); + goto error_init_textures; + } + + SetName(context->textures[numTexturesInited], metaData.name); + } + + for (; numInputBindingsInited < NUM_INPUT_DESCRIPTOR_BINDINGS; ++numInputBindingsInited) + { + InputDescriptorBindingMetaData metaData = INPUT_DESCRIPTOR_BINDING_META_DATA[numInputBindingsInited]; + DescriptorSetID ds = metaData.descriptorID; + ShaderResourceViewMetaData srvMetaData = SRV_META_DATA[metaData.srvID]; + + D3D12_CPU_DESCRIPTOR_HANDLE descriptor = context->inputDescriptors[ds].cpuDescriptor; + descriptor.ptr += metaData.bindingNumber * context->inputDescriptors[ds].descriptorSize; + + D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = TEXTURE_FORMAT_LOOKUP_D3D12[TEXTURE_META_DATA[srvMetaData.texture].format]; + srvDesc.ViewDimension = VIEW_TYPE_LOOKUP_D3D12_SRV[srvMetaData.viewType]; + srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + switch (srvDesc.ViewDimension) + { + case D3D12_SRV_DIMENSION_TEXTURE2D: + srvDesc.Texture2D.MostDetailedMip = srvMetaData.mostDetailedMip; + srvDesc.Texture2D.MipLevels = srvMetaData.mipLevels; + break; + case D3D12_SRV_DIMENSION_TEXTURE2DARRAY: + srvDesc.Texture2DArray.MostDetailedMip = srvMetaData.mostDetailedMip; + srvDesc.Texture2DArray.MipLevels = srvMetaData.mipLevels; + srvDesc.Texture2DArray.FirstArraySlice = srvMetaData.firstArraySlice; + srvDesc.Texture2DArray.ArraySize = srvMetaData.arraySize; + break; + default: + FFX_CACAO_ASSERT(0); + break; + } + device->CreateShaderResourceView(context->textures[srvMetaData.texture], &srvDesc, descriptor); + } + + for (; numOutputBindingsInited < NUM_OUTPUT_DESCRIPTOR_BINDINGS; ++numOutputBindingsInited) + { + OutputDescriptorBindingMetaData metaData = OUTPUT_DESCRIPTOR_BINDING_META_DATA[numOutputBindingsInited]; + DescriptorSetID ds = metaData.descriptorID; + UnorderedAccessViewMetaData uavMetaData = UAV_META_DATA[metaData.uavID]; + + D3D12_CPU_DESCRIPTOR_HANDLE descriptor = context->outputDescriptors[ds].cpuDescriptor; + descriptor.ptr += metaData.bindingNumber * context->outputDescriptors[ds].descriptorSize; + + D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc = {}; + uavDesc.Format = TEXTURE_FORMAT_LOOKUP_D3D12[TEXTURE_META_DATA[uavMetaData.textureID].format]; + uavDesc.ViewDimension = VIEW_TYPE_LOOKUP_D3D12_UAV[uavMetaData.viewType]; + switch (uavDesc.ViewDimension) + { + case D3D12_UAV_DIMENSION_TEXTURE2D: + uavDesc.Texture2D.MipSlice = uavMetaData.mostDetailedMip; + break; + case D3D12_UAV_DIMENSION_TEXTURE2DARRAY: + uavDesc.Texture2DArray.MipSlice = uavMetaData.mostDetailedMip; + uavDesc.Texture2DArray.FirstArraySlice = uavMetaData.firstArraySlice; + uavDesc.Texture2DArray.ArraySize = uavMetaData.arraySize; + break; + default: + FFX_CACAO_ASSERT(0); + break; + } + device->CreateUnorderedAccessView(context->textures[uavMetaData.textureID], NULL, &uavDesc, descriptor); + } + + // misc inputs + { + D3D12_CPU_DESCRIPTOR_HANDLE descriptor; + + // depth buffer input + descriptor = context->inputDescriptors[DS_PREPARE_DEPTHS].cpuDescriptor; + device->CreateShaderResourceView(info->depthBufferResource, &info->depthBufferSrvDesc, descriptor); + + descriptor = context->inputDescriptors[DS_PREPARE_DEPTHS_MIPS].cpuDescriptor; + device->CreateShaderResourceView(info->depthBufferResource, &info->depthBufferSrvDesc, descriptor); + + descriptor = context->inputDescriptors[DS_PREPARE_NORMALS].cpuDescriptor; + device->CreateShaderResourceView(info->depthBufferResource, &info->depthBufferSrvDesc, descriptor); + + descriptor = context->inputDescriptors[DS_BILATERAL_UPSAMPLE_PING].cpuDescriptor; + descriptor.ptr += context->cbvSrvUavHeap.descriptorElementSize; + device->CreateShaderResourceView(info->depthBufferResource, &info->depthBufferSrvDesc, descriptor); + + descriptor = context->inputDescriptors[DS_BILATERAL_UPSAMPLE_PONG].cpuDescriptor; + descriptor.ptr += context->cbvSrvUavHeap.descriptorElementSize; + device->CreateShaderResourceView(info->depthBufferResource, &info->depthBufferSrvDesc, descriptor); + + // normal buffer input + if (info->normalBufferResource) + { + descriptor = context->inputDescriptors[DS_PREPARE_NORMALS_FROM_INPUT_NORMALS].cpuDescriptor; + device->CreateShaderResourceView(info->normalBufferResource, &info->normalBufferSrvDesc, descriptor); + } + + // ssao buffer output + descriptor = context->outputDescriptors[DS_BILATERAL_UPSAMPLE_PING].cpuDescriptor; + device->CreateUnorderedAccessView(info->outputResource, NULL, &info->outputUavDesc, descriptor); + + descriptor = context->outputDescriptors[DS_BILATERAL_UPSAMPLE_PONG].cpuDescriptor; + device->CreateUnorderedAccessView(info->outputResource, NULL, &info->outputUavDesc, descriptor); + + descriptor = context->outputDescriptors[DS_APPLY_PING].cpuDescriptor; + device->CreateUnorderedAccessView(info->outputResource, NULL, &info->outputUavDesc, descriptor); + + descriptor = context->outputDescriptors[DS_APPLY_PONG].cpuDescriptor; + device->CreateUnorderedAccessView(info->outputResource, NULL, &info->outputUavDesc, descriptor); + + // load counter input + D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = context->loadCounter.format; + srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE1D; + srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + srvDesc.Texture1D.MostDetailedMip = 0; + srvDesc.Texture1D.MipLevels = 1; + + for (uint32_t pass = 0; pass < 4; ++pass) + { + descriptor = context->inputDescriptors[DS_GENERATE_ADAPTIVE_0 + pass].cpuDescriptor; + descriptor.ptr += 2 * context->cbvSrvUavHeap.descriptorElementSize; + device->CreateShaderResourceView(context->loadCounter.resource, &srvDesc, descriptor); + } + + // load counter output + D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc = {}; + uavDesc.Format = context->loadCounter.format; + uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE1D; + uavDesc.Texture1D.MipSlice = 0; + + descriptor = context->outputDescriptors[DS_POSTPROCESS_IMPORTANCE_MAP_B].cpuDescriptor; + descriptor.ptr += 1 * context->cbvSrvUavHeap.descriptorElementSize; + device->CreateUnorderedAccessView(context->loadCounter.resource, NULL, &uavDesc, descriptor); + + descriptor = context->outputDescriptors[DS_CLEAR_LOAD_COUNTER].cpuDescriptor; + device->CreateUnorderedAccessView(context->loadCounter.resource, NULL, &uavDesc, descriptor); + } + + + return FFX_CACAO_STATUS_OK; + +error_init_textures: + for (uint32_t i = 0; i < numTexturesInited; ++i) + { + context->textures[i]->Release(); + } + + return errorStatus; +} + +FFX_CACAO_Status FFX_CACAO_D3D12DestroyScreenSizeDependentResources(FFX_CACAO_D3D12Context* context) +{ + if (context == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + context = getAlignedD3D12ContextPointer(context); + + for (uint32_t i = 0; i < NUM_TEXTURES; ++i) + { + context->textures[i]->Release(); + } + + return FFX_CACAO_STATUS_OK; +} + +FFX_CACAO_Status FFX_CACAO_D3D12UpdateSettings(FFX_CACAO_D3D12Context* context, const FFX_CACAO_Settings* settings) +{ + if (context == NULL || settings == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + context = getAlignedD3D12ContextPointer(context); + + memcpy(&context->settings, settings, sizeof(*settings)); + + return FFX_CACAO_STATUS_OK; +} + +static inline void computeShaderDraw(FFX_CACAO_D3D12Context* context, ComputeShaderID computeShaderID, ID3D12GraphicsCommandList* commandList, D3D12_GPU_VIRTUAL_ADDRESS constantBuffer, DescriptorSetID descriptorSetID, uint32_t width, uint32_t height, uint32_t depth) +{ + FFX_CACAO_ASSERT(computeShaderID); + FFX_CACAO_ASSERT(commandList); + + DescriptorSetMetaData dsMetaData = DESCRIPTOR_SET_META_DATA[descriptorSetID]; + DescriptorSetLayoutMetaData dslMetaData = DESCRIPTOR_SET_LAYOUT_META_DATA[dsMetaData.descriptorSetLayoutID]; + + commandList->SetComputeRootSignature(context->csRootSignatures[computeShaderID]); + + int params = 0; + commandList->SetComputeRootConstantBufferView(params++, constantBuffer); + if (dslMetaData.numOutputs) + { + commandList->SetComputeRootDescriptorTable(params++, context->outputDescriptors[descriptorSetID].gpuDescriptor); + } + if (dslMetaData.numInputs) + { + commandList->SetComputeRootDescriptorTable(params++, context->inputDescriptors[descriptorSetID].gpuDescriptor); + } + + commandList->SetPipelineState(context->computeShader[computeShaderID]); + commandList->Dispatch(width, height, depth); +} + +FFX_CACAO_Status FFX_CACAO_D3D12Draw(FFX_CACAO_D3D12Context* context, ID3D12GraphicsCommandList* commandList, const FFX_CACAO_Matrix4x4* proj, const FFX_CACAO_Matrix4x4* normalsToView) +{ + if (context == NULL || commandList == NULL || proj == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + context = getAlignedD3D12ContextPointer(context); + + +#ifdef FFX_CACAO_ENABLE_PROFILING +#define GET_TIMESTAMP(name) gpuTimerGetTimestamp(&context->gpuTimer, commandList, TIMESTAMP_##name) +#else +#define GET_TIMESTAMP(name) +#endif + FFX_CACAO_BufferSizeInfo *bsi = &context->bufferSizeInfo; + + + USER_MARKER("FidelityFX CACAO"); + + constantBufferRingStartFrame(&context->constantBufferRing); + +#ifdef FFX_CACAO_ENABLE_PROFILING + gpuTimerStartFrame(&context->gpuTimer); +#endif + + GET_TIMESTAMP(BEGIN); + + // set the descriptor heaps + { + ID3D12DescriptorHeap *descriptorHeaps[] = { context->cbvSrvUavHeap.heap }; + commandList->SetDescriptorHeaps(FFX_CACAO_ARRAY_SIZE(descriptorHeaps), descriptorHeaps); + } + + // clear load counter + { + UINT clearValue[] = { 0, 0, 0, 0 }; + commandList->ClearUnorderedAccessViewUint(context->loadCounterUav.gpuDescriptor, context->loadCounterUav.cpuVisibleCpuDescriptor, context->loadCounter.resource, clearValue, 0, NULL); + } + + // move this to initialisation + D3D12_GPU_VIRTUAL_ADDRESS cbCACAOHandle; + FFX_CACAO_Constants *pCACAOConsts; + D3D12_GPU_VIRTUAL_ADDRESS cbCACAOPerPassHandle[4]; + FFX_CACAO_Constants *pPerPassConsts[4]; + + // upload constant buffers + { + constantBufferRingAlloc(&context->constantBufferRing, sizeof(*pCACAOConsts), (void**)&pCACAOConsts, &cbCACAOHandle); + FFX_CACAO_UpdateConstants(pCACAOConsts, &context->settings, bsi, proj, normalsToView); + + for (int i = 0; i < 4; ++i) + { + constantBufferRingAlloc(&context->constantBufferRing, sizeof(*pPerPassConsts[0]), (void**)&pPerPassConsts[i], &cbCACAOPerPassHandle[i]); + FFX_CACAO_UpdateConstants(pPerPassConsts[i], &context->settings, bsi, proj, normalsToView); + FFX_CACAO_UpdatePerPassConstants(pPerPassConsts[i], &context->settings, &context->bufferSizeInfo, i); + } + } + + // prepare depths, normals and mips + { + USER_MARKER("Prepare downsampled depths, normals and mips"); + + + switch (context->settings.qualityLevel) + { + case FFX_CACAO_QUALITY_LOWEST: { + uint32_t dispatchWidth = dispatchSize(FFX_CACAO_PREPARE_DEPTHS_HALF_WIDTH, bsi->deinterleavedDepthBufferWidth); + uint32_t dispatchHeight = dispatchSize(FFX_CACAO_PREPARE_DEPTHS_HALF_HEIGHT, bsi->deinterleavedDepthBufferHeight); + ComputeShaderID prepareDepthsHalf = context->useDownsampledSsao ? CS_PREPARE_DOWNSAMPLED_DEPTHS_HALF : CS_PREPARE_NATIVE_DEPTHS_HALF; + computeShaderDraw(context, prepareDepthsHalf, commandList, cbCACAOHandle, DS_PREPARE_DEPTHS, dispatchWidth, dispatchHeight, 1); + break; + } + case FFX_CACAO_QUALITY_LOW: { + uint32_t dispatchWidth = dispatchSize(FFX_CACAO_PREPARE_DEPTHS_WIDTH, bsi->deinterleavedDepthBufferWidth); + uint32_t dispatchHeight = dispatchSize(FFX_CACAO_PREPARE_DEPTHS_HEIGHT, bsi->deinterleavedDepthBufferHeight); + ComputeShaderID prepareDepths = context->useDownsampledSsao ? CS_PREPARE_DOWNSAMPLED_DEPTHS : CS_PREPARE_NATIVE_DEPTHS; + computeShaderDraw(context, prepareDepths, commandList, cbCACAOHandle, DS_PREPARE_DEPTHS, dispatchWidth, dispatchHeight, 1); + break; + } + default: { + uint32_t dispatchWidth = dispatchSize(FFX_CACAO_PREPARE_DEPTHS_AND_MIPS_WIDTH, bsi->deinterleavedDepthBufferWidth); + uint32_t dispatchHeight = dispatchSize(FFX_CACAO_PREPARE_DEPTHS_AND_MIPS_HEIGHT, bsi->deinterleavedDepthBufferHeight); + ComputeShaderID prepareDepthsAndMips = context->useDownsampledSsao ? CS_PREPARE_DOWNSAMPLED_DEPTHS_AND_MIPS : CS_PREPARE_NATIVE_DEPTHS_AND_MIPS; + computeShaderDraw(context, prepareDepthsAndMips, commandList, cbCACAOHandle, DS_PREPARE_DEPTHS_MIPS, dispatchWidth, dispatchHeight, 1); + break; + } + } + + if (context->settings.generateNormals) + { + uint32_t dispatchWidth = dispatchSize(FFX_CACAO_PREPARE_NORMALS_WIDTH, bsi->ssaoBufferWidth); + uint32_t dispatchHeight = dispatchSize(FFX_CACAO_PREPARE_NORMALS_HEIGHT, bsi->ssaoBufferHeight); + ComputeShaderID prepareNormals = context->useDownsampledSsao ? CS_PREPARE_DOWNSAMPLED_NORMALS : CS_PREPARE_NATIVE_NORMALS; + computeShaderDraw(context, prepareNormals, commandList, cbCACAOHandle, DS_PREPARE_NORMALS, dispatchWidth, dispatchHeight, 1); + } + else + { + uint32_t dispatchWidth = dispatchSize(PREPARE_NORMALS_FROM_INPUT_NORMALS_WIDTH, bsi->ssaoBufferWidth); + uint32_t dispatchHeight = dispatchSize(PREPARE_NORMALS_FROM_INPUT_NORMALS_HEIGHT, bsi->ssaoBufferHeight); + ComputeShaderID prepareNormalsFromInputNormals = context->useDownsampledSsao ? CS_PREPARE_DOWNSAMPLED_NORMALS_FROM_INPUT_NORMALS : CS_PREPARE_NATIVE_NORMALS_FROM_INPUT_NORMALS; + computeShaderDraw(context, prepareNormalsFromInputNormals, commandList, cbCACAOHandle, DS_PREPARE_NORMALS_FROM_INPUT_NORMALS, dispatchWidth, dispatchHeight, 1); + } + + GET_TIMESTAMP(PREPARE); + } + + // deinterleaved depths and normals are now read only resources, also used in the next stage + { + D3D12_RESOURCE_BARRIER resourceBarriers[] = { + CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_DEINTERLEAVED_DEPTHS], D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE), + CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_DEINTERLEAVED_NORMALS], D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE), + }; + commandList->ResourceBarrier(FFX_CACAO_ARRAY_SIZE(resourceBarriers), resourceBarriers); + } + + // base pass for highest quality setting + if (context->settings.qualityLevel == FFX_CACAO_QUALITY_HIGHEST) + { + USER_MARKER("Generate High Quality Base Pass"); + + // SSAO + { + USER_MARKER("SSAO"); + + for (int pass = 0; pass < 4; ++pass) + { + uint32_t dispatchWidth = dispatchSize(FFX_CACAO_GENERATE_WIDTH, bsi->ssaoBufferWidth); + uint32_t dispatchHeight = dispatchSize(FFX_CACAO_GENERATE_HEIGHT, bsi->ssaoBufferHeight); + DescriptorSetID ds = (DescriptorSetID)(DS_GENERATE_ADAPTIVE_BASE_0 + pass); + computeShaderDraw(context, CS_GENERATE_Q3_BASE, commandList, cbCACAOPerPassHandle[pass], ds, dispatchWidth, dispatchHeight, 1); + } + GET_TIMESTAMP(BASE_SSAO_PASS); + } + + // results written by base pass are now a reaad only resource, used in next stage + commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_SSAO_BUFFER_PONG], D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE)); + + // generate importance map + { + USER_MARKER("Importance Map"); + + CD3DX12_RESOURCE_BARRIER barriers[2]; + UINT barrierCount; + + uint32_t dispatchWidth = dispatchSize(IMPORTANCE_MAP_WIDTH, bsi->importanceMapWidth); + uint32_t dispatchHeight = dispatchSize(IMPORTANCE_MAP_HEIGHT, bsi->importanceMapHeight); + + computeShaderDraw(context, CS_GENERATE_IMPORTANCE_MAP, commandList, cbCACAOHandle, DS_GENERATE_IMPORTANCE_MAP, dispatchWidth, dispatchHeight, 1); + + barrierCount = 0; + barriers[barrierCount++] = CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_IMPORTANCE_MAP], D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); + commandList->ResourceBarrier(barrierCount, barriers); + + computeShaderDraw(context, CS_POSTPROCESS_IMPORTANCE_MAP_A, commandList, cbCACAOHandle, DS_POSTPROCESS_IMPORTANCE_MAP_A, dispatchWidth, dispatchHeight, 1); + + barrierCount = 0; + barriers[barrierCount++] = CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_IMPORTANCE_MAP], D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + barriers[barrierCount++] = CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_IMPORTANCE_MAP_PONG], D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); + commandList->ResourceBarrier(barrierCount, barriers); + + computeShaderDraw(context, CS_POSTPROCESS_IMPORTANCE_MAP_B, commandList, cbCACAOHandle, DS_POSTPROCESS_IMPORTANCE_MAP_B, dispatchWidth, dispatchHeight, 1); + + barrierCount = 0; + barriers[barrierCount++] = CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_IMPORTANCE_MAP], D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); + barriers[barrierCount++] = CD3DX12_RESOURCE_BARRIER::Transition(context->loadCounter.resource, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); + commandList->ResourceBarrier(barrierCount, barriers); + + GET_TIMESTAMP(IMPORTANCE_MAP); + } + } + + int blurPassCount = context->settings.blurPassCount; + blurPassCount = FFX_CACAO_CLAMP(blurPassCount, 0, MAX_BLUR_PASSES); + + // main ssao generation + { + USER_MARKER("Generate SSAO"); + + // ComputeShader *generate = &context->generateSSAO[FFX_CACAO_MAX(0, context->settings.qualityLevel - 1)]; + ComputeShaderID generate = (ComputeShaderID)(CS_GENERATE_Q0 + FFX_CACAO_MAX(0, context->settings.qualityLevel - 1)); + + uint32_t dispatchWidth, dispatchHeight, dispatchDepth; + + switch (context->settings.qualityLevel) + { + case FFX_CACAO_QUALITY_LOWEST: + case FFX_CACAO_QUALITY_LOW: + case FFX_CACAO_QUALITY_MEDIUM: + dispatchWidth = dispatchSize(FFX_CACAO_GENERATE_SPARSE_WIDTH, bsi->ssaoBufferWidth); + dispatchWidth = (dispatchWidth + 4) / 5; + dispatchHeight = dispatchSize(FFX_CACAO_GENERATE_SPARSE_HEIGHT, bsi->ssaoBufferHeight); + dispatchDepth = 5; + break; + case FFX_CACAO_QUALITY_HIGH: + case FFX_CACAO_QUALITY_HIGHEST: + dispatchWidth = dispatchSize(FFX_CACAO_GENERATE_WIDTH, bsi->ssaoBufferWidth); + dispatchHeight = dispatchSize(FFX_CACAO_GENERATE_HEIGHT, bsi->ssaoBufferHeight); + dispatchDepth = 1; + break; + } + + for (int pass = 0; pass < 4; ++pass) + { + if (context->settings.qualityLevel == FFX_CACAO_QUALITY_LOWEST && (pass == 1 || pass == 2)) + { + continue; + } + + DescriptorSetID ds = context->settings.qualityLevel == FFX_CACAO_QUALITY_HIGHEST ? (DescriptorSetID)(DS_GENERATE_ADAPTIVE_0 + pass) : (DescriptorSetID)(DS_GENERATE_0 + pass); + computeShaderDraw(context, generate, commandList, cbCACAOPerPassHandle[pass], ds, dispatchWidth, dispatchHeight, dispatchDepth); + } + + GET_TIMESTAMP(GENERATE_SSAO); + } + + // de-interleaved blur + if (blurPassCount) + { + // only need to transition pong to writable if we didn't already use it in the base pass + CD3DX12_RESOURCE_BARRIER barriers[] = { + CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_SSAO_BUFFER_PING], D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE), + CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_SSAO_BUFFER_PONG], D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS), + }; + commandList->ResourceBarrier(context->settings.qualityLevel == FFX_CACAO_QUALITY_HIGHEST ? 2 : 1, barriers); + + USER_MARKER("Deinterleaved blur"); + + for (int pass = 0; pass < 4; ++pass) + { + if (context->settings.qualityLevel == FFX_CACAO_QUALITY_LOWEST && (pass == 1 || pass == 2)) + { + continue; + } + + uint32_t w = 4 * FFX_CACAO_BLUR_WIDTH - 2 * blurPassCount; + uint32_t h = 3 * FFX_CACAO_BLUR_HEIGHT - 2 * blurPassCount; + uint32_t blurPassIndex = blurPassCount - 1; + uint32_t dispatchWidth = dispatchSize(w, bsi->ssaoBufferWidth); + uint32_t dispatchHeight = dispatchSize(h, bsi->ssaoBufferHeight); + ComputeShaderID edgeSensitiveBlur = (ComputeShaderID)(CS_EDGE_SENSITIVE_BLUR_1 + blurPassCount - 1); + DescriptorSetID ds = (DescriptorSetID)(DS_EDGE_SENSITIVE_BLUR_0 + pass); + computeShaderDraw(context, edgeSensitiveBlur, commandList, cbCACAOPerPassHandle[pass], ds, dispatchWidth, dispatchHeight, 1); + } + + GET_TIMESTAMP(EDGE_SENSITIVE_BLUR); + + commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_SSAO_BUFFER_PONG], D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE)); + } + else + { + commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_SSAO_BUFFER_PING], D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE)); + } + + + if (context->useDownsampledSsao) + { + USER_MARKER("Upscale"); + + DescriptorSetID ds = blurPassCount ? DS_BILATERAL_UPSAMPLE_PONG : DS_BILATERAL_UPSAMPLE_PING; + ComputeShaderID upscaler; + switch (context->settings.qualityLevel) + { + case FFX_CACAO_QUALITY_LOWEST: + upscaler = CS_UPSCALE_BILATERAL_5X5_HALF; + break; + case FFX_CACAO_QUALITY_LOW: + case FFX_CACAO_QUALITY_MEDIUM: + upscaler = CS_UPSCALE_BILATERAL_5X5_NON_SMART; + break; + case FFX_CACAO_QUALITY_HIGH: + case FFX_CACAO_QUALITY_HIGHEST: + upscaler = CS_UPSCALE_BILATERAL_5X5_SMART; + break; + } + uint32_t dispatchWidth = dispatchSize(2 * FFX_CACAO_BILATERAL_UPSCALE_WIDTH, bsi->inputOutputBufferWidth); + uint32_t dispatchHeight = dispatchSize(2 * FFX_CACAO_BILATERAL_UPSCALE_HEIGHT, bsi->inputOutputBufferHeight); + computeShaderDraw(context, upscaler, commandList, cbCACAOHandle, ds, dispatchWidth, dispatchHeight, 1); + + GET_TIMESTAMP(BILATERAL_UPSAMPLE); + } + else + { + USER_MARKER("Create Output"); + DescriptorSetID ds = blurPassCount ? DS_APPLY_PONG : DS_APPLY_PING; + uint32_t dispatchWidth = dispatchSize(FFX_CACAO_APPLY_WIDTH, bsi->inputOutputBufferWidth); + uint32_t dispatchHeight = dispatchSize(FFX_CACAO_APPLY_HEIGHT, bsi->inputOutputBufferHeight); + switch (context->settings.qualityLevel) + { + case FFX_CACAO_QUALITY_LOWEST: + computeShaderDraw(context, CS_NON_SMART_HALF_APPLY, commandList, cbCACAOHandle, ds, dispatchWidth, dispatchHeight, 1); + break; + case FFX_CACAO_QUALITY_LOW: + computeShaderDraw(context, CS_NON_SMART_APPLY, commandList, cbCACAOHandle, ds, dispatchWidth, dispatchHeight, 1); + break; + default: + computeShaderDraw(context, CS_APPLY, commandList, cbCACAOHandle, ds, dispatchWidth, dispatchHeight, 1); + break; + } + GET_TIMESTAMP(APPLY); + } + + // end frame resource barrier + { + uint32_t numBarriers = 0; + D3D12_RESOURCE_BARRIER resourceBarriers[10] = {}; + resourceBarriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_DEINTERLEAVED_DEPTHS], D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + resourceBarriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_DEINTERLEAVED_NORMALS], D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + resourceBarriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::Transition(context->outputResource, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_GENERIC_READ); + resourceBarriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_SSAO_BUFFER_PING], D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + if (context->settings.qualityLevel == FFX_CACAO_QUALITY_HIGHEST || blurPassCount) + { + resourceBarriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_SSAO_BUFFER_PONG], D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + } + if (context->settings.qualityLevel == FFX_CACAO_QUALITY_HIGHEST) + { + resourceBarriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_IMPORTANCE_MAP], D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + resourceBarriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::Transition(context->textures[TEXTURE_IMPORTANCE_MAP_PONG], D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + resourceBarriers[numBarriers++] = CD3DX12_RESOURCE_BARRIER::Transition(context->loadCounter.resource, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + } + commandList->ResourceBarrier(numBarriers, resourceBarriers); + } + +#ifdef FFX_CACAO_ENABLE_PROFILING + gpuTimerEndFrame(&context->gpuTimer, commandList); +#endif + + return FFX_CACAO_STATUS_OK; + +#undef GET_TIMESTAMP +} + +#ifdef FFX_CACAO_ENABLE_PROFILING +FFX_CACAO_Status FFX_CACAO_D3D12GetDetailedTimings(FFX_CACAO_D3D12Context* context, FFX_CACAO_DetailedTiming* timings) +{ + if (context == NULL || timings == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + context = getAlignedD3D12ContextPointer(context); + + gpuTimerCollectTimings(&context->gpuTimer, timings); + + return FFX_CACAO_STATUS_OK; +} +#endif +#endif + +#ifdef FFX_CACAO_ENABLE_VULKAN +inline static void setObjectName(VkDevice device, FFX_CACAO_VkContext* context, VkObjectType type, uint64_t handle, const char* name) +{ + if (!context->vkSetDebugUtilsObjectName) + { + return; + } + + VkDebugUtilsObjectNameInfoEXT info = {}; + info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT; + info.pNext = NULL; + info.objectType = type; + info.objectHandle = handle; + info.pObjectName = name; + + VkResult result = context->vkSetDebugUtilsObjectName(device, &info); + FFX_CACAO_ASSERT(result == VK_SUCCESS); +} + +inline static uint32_t getBestMemoryHeapIndex(VkPhysicalDevice physicalDevice, VkMemoryRequirements memoryRequirements, VkMemoryPropertyFlags desiredProperties) +{ + VkPhysicalDeviceMemoryProperties memoryProperties; + vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memoryProperties); + + uint32_t chosenMemoryTypeIndex = VK_MAX_MEMORY_TYPES; + for (uint32_t i = 0; i < memoryProperties.memoryTypeCount; ++i) + { + uint32_t typeBit = 1 << i; + // can we allocate to memory of this type + if (memoryRequirements.memoryTypeBits & typeBit) + { + VkMemoryType currentMemoryType = memoryProperties.memoryTypes[i]; + // do we want to allocate to memory of this type + if ((currentMemoryType.propertyFlags & desiredProperties) == desiredProperties) + { + chosenMemoryTypeIndex = i; + break; + } + } + } + return chosenMemoryTypeIndex; +} + +size_t FFX_CACAO_VkGetContextSize() +{ + return sizeof(FFX_CACAO_VkContext) + alignof(FFX_CACAO_VkContext) - 1; +} + +FFX_CACAO_Status FFX_CACAO_VkInitContext(FFX_CACAO_VkContext* context, const FFX_CACAO_VkCreateInfo* info) +{ + if (context == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + if (info == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + context = getAlignedVkContextPointer(context); + memset(context, 0, sizeof(*context)); + + VkDevice device = info->device; + VkPhysicalDevice physicalDevice = info->physicalDevice; + VkResult result; + FFX_CACAO_Bool use16Bit = info->flags & FFX_CACAO_VK_CREATE_USE_16_BIT ? FFX_CACAO_TRUE : FFX_CACAO_FALSE; + FFX_CACAO_Status errorStatus = FFX_CACAO_STATUS_FAILED; + + context->device = device; + context->physicalDevice = physicalDevice; + + if (info->flags & FFX_CACAO_VK_CREATE_USE_DEBUG_MARKERS) + { + context->vkCmdDebugMarkerBegin = (PFN_vkCmdDebugMarkerBeginEXT)vkGetDeviceProcAddr(device, "vkCmdDebugMarkerBeginEXT"); + context->vkCmdDebugMarkerEnd = (PFN_vkCmdDebugMarkerEndEXT)vkGetDeviceProcAddr(device, "vkCmdDebugMarkerEndEXT"); + } + if (info->flags & FFX_CACAO_VK_CREATE_USE_DEBUG_MARKERS) + { + context->vkSetDebugUtilsObjectName = (PFN_vkSetDebugUtilsObjectNameEXT)vkGetDeviceProcAddr(device, "vkSetDebugUtilsObjectNameEXT"); + } + + uint32_t numSamplersInited = 0; + uint32_t numDescriptorSetLayoutsInited = 0; + uint32_t numPipelineLayoutsInited = 0; + uint32_t numShaderModulesInited = 0; + uint32_t numPipelinesInited = 0; + uint32_t numConstantBackBuffersInited = 0; + + VkSampler samplers[NUM_SAMPLERS]; + { + VkSamplerCreateInfo samplerCreateInfo = {}; + samplerCreateInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerCreateInfo.pNext = NULL; + samplerCreateInfo.flags = 0; + samplerCreateInfo.magFilter = VK_FILTER_LINEAR; + samplerCreateInfo.minFilter = VK_FILTER_LINEAR; + samplerCreateInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + samplerCreateInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerCreateInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerCreateInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; + samplerCreateInfo.mipLodBias = 0.0f; + samplerCreateInfo.anisotropyEnable = VK_FALSE; + samplerCreateInfo.compareEnable = VK_FALSE; + samplerCreateInfo.minLod = -1000.0f; + samplerCreateInfo.maxLod = 1000.0f; + samplerCreateInfo.unnormalizedCoordinates = VK_FALSE; + + result = vkCreateSampler(device, &samplerCreateInfo, NULL, &samplers[numSamplersInited]); + if (result != VK_SUCCESS) + { + goto error_init_samplers; + } + setObjectName(device, context, VK_OBJECT_TYPE_SAMPLER, (uint64_t)samplers[numSamplersInited], "FFX_CACAO_POINT_CLAMP_SAMPLER"); + ++numSamplersInited; + + samplerCreateInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT; + samplerCreateInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT; + samplerCreateInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT; + + result = vkCreateSampler(device, &samplerCreateInfo, NULL, &samplers[numSamplersInited]); + if (result != VK_SUCCESS) + { + goto error_init_samplers; + } + setObjectName(device, context, VK_OBJECT_TYPE_SAMPLER, (uint64_t)samplers[numSamplersInited], "FFX_CACAO_POINT_MIRROR_SAMPLER"); + ++numSamplersInited; + + samplerCreateInfo.magFilter = VK_FILTER_LINEAR; + samplerCreateInfo.minFilter = VK_FILTER_LINEAR; + samplerCreateInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + samplerCreateInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerCreateInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerCreateInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + + result = vkCreateSampler(device, &samplerCreateInfo, NULL, &samplers[numSamplersInited]); + if (result != VK_SUCCESS) + { + goto error_init_samplers; + } + setObjectName(device, context, VK_OBJECT_TYPE_SAMPLER, (uint64_t)samplers[numSamplersInited], "FFX_CACAO_LINEAR_CLAMP_SAMPLER"); + ++numSamplersInited; + + samplerCreateInfo.magFilter = VK_FILTER_NEAREST; + samplerCreateInfo.minFilter = VK_FILTER_NEAREST; + samplerCreateInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + + result = vkCreateSampler(device, &samplerCreateInfo, NULL, &samplers[numSamplersInited]); + if (result != VK_SUCCESS) + { + goto error_init_samplers; + } + setObjectName(device, context, VK_OBJECT_TYPE_SAMPLER, (uint64_t)samplers[numSamplersInited], "FFX_CACAO_VIEWSPACE_DEPTH_TAP_SAMPLER"); + ++numSamplersInited; + + samplerCreateInfo.magFilter = VK_FILTER_NEAREST; + samplerCreateInfo.minFilter = VK_FILTER_NEAREST; + samplerCreateInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + samplerCreateInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT; + samplerCreateInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT; + samplerCreateInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT; + + result = vkCreateSampler(device, &samplerCreateInfo, NULL, &samplers[numSamplersInited]); + if (result != VK_SUCCESS) + { + goto error_init_samplers; + } + setObjectName(device, context, VK_OBJECT_TYPE_SAMPLER, (uint64_t)samplers[numSamplersInited], "FFX_CACAO_REAL_POINT_CLAMP_SAMPLER"); + ++numSamplersInited; + + for (uint32_t i = 0; i < FFX_CACAO_ARRAY_SIZE(samplers); ++i) + { + context->samplers[i] = samplers[i]; + } + } + + // create descriptor set layouts + for ( ; numDescriptorSetLayoutsInited < NUM_DESCRIPTOR_SET_LAYOUTS; ++numDescriptorSetLayoutsInited) + { + VkDescriptorSetLayout descriptorSetLayout; + DescriptorSetLayoutMetaData dslMetaData = DESCRIPTOR_SET_LAYOUT_META_DATA[numDescriptorSetLayoutsInited]; + + VkDescriptorSetLayoutBinding bindings[MAX_DESCRIPTOR_BINDINGS] = {}; + uint32_t numBindings = 0; + for (uint32_t samplerBinding = 0; samplerBinding < FFX_CACAO_ARRAY_SIZE(samplers); ++samplerBinding) + { + VkDescriptorSetLayoutBinding binding = {}; + binding.binding = samplerBinding; + binding.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + binding.pImmutableSamplers = &samplers[samplerBinding]; + bindings[numBindings++] = binding; + } + + // constant buffer binding + { + VkDescriptorSetLayoutBinding binding = {}; + binding.binding = 10; + binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + binding.pImmutableSamplers = NULL; + bindings[numBindings++] = binding; + } + + for (uint32_t inputBinding = 0; inputBinding < dslMetaData.numInputs; ++inputBinding) + { + VkDescriptorSetLayoutBinding binding = {}; + binding.binding = 20 + inputBinding; + binding.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + binding.pImmutableSamplers = NULL; + bindings[numBindings++] = binding; + } + + for (uint32_t outputBinding = 0; outputBinding < dslMetaData.numOutputs; ++outputBinding) + { + VkDescriptorSetLayoutBinding binding = {}; + binding.binding = 30 + outputBinding; // g_PrepareDepthsOut register(u0) + binding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + binding.pImmutableSamplers = NULL; + bindings[numBindings++] = binding; + } + + VkDescriptorSetLayoutCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + info.pNext = NULL; + info.flags = 0; + info.bindingCount = numBindings; + info.pBindings = bindings; + + result = vkCreateDescriptorSetLayout(device, &info, NULL, &descriptorSetLayout); + if (result != VK_SUCCESS) + { + goto error_init_descriptor_set_layouts; + } + setObjectName(device, context, VK_OBJECT_TYPE_DESCRIPTOR_SET_LAYOUT, (uint64_t)descriptorSetLayout, dslMetaData.name); + + context->descriptorSetLayouts[numDescriptorSetLayoutsInited] = descriptorSetLayout; + } + + // create pipeline layouts + for ( ; numPipelineLayoutsInited < NUM_DESCRIPTOR_SET_LAYOUTS; ++numPipelineLayoutsInited) + { + VkPipelineLayout pipelineLayout; + + DescriptorSetLayoutMetaData dslMetaData = DESCRIPTOR_SET_LAYOUT_META_DATA[numPipelineLayoutsInited]; + + VkPipelineLayoutCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + info.pNext = NULL; + info.flags = 0; + info.setLayoutCount = 1; + info.pSetLayouts = &context->descriptorSetLayouts[numPipelineLayoutsInited]; + info.pushConstantRangeCount = 0; + info.pPushConstantRanges = NULL; + + result = vkCreatePipelineLayout(device, &info, NULL, &pipelineLayout); + if (result != VK_SUCCESS) + { + goto error_init_pipeline_layouts; + } + setObjectName(device, context, VK_OBJECT_TYPE_PIPELINE_LAYOUT, (uint64_t)pipelineLayout, dslMetaData.name); + + context->pipelineLayouts[numPipelineLayoutsInited] = pipelineLayout; + } + + for ( ; numShaderModulesInited < NUM_COMPUTE_SHADERS; ++numShaderModulesInited) + { + VkShaderModule shaderModule; + ComputeShaderMetaData csMetaData = COMPUTE_SHADER_META_DATA[numShaderModulesInited]; + + VkShaderModuleCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + info.pNext = 0; + info.flags = 0; + ComputeShaderSPIRV spirv = use16Bit ? COMPUTE_SHADER_SPIRV_16[numShaderModulesInited] : COMPUTE_SHADER_SPIRV_32[numShaderModulesInited]; + info.codeSize = spirv.len; + info.pCode = spirv.spirv; + + result = vkCreateShaderModule(device, &info, NULL, &shaderModule); + if (result != VK_SUCCESS) + { + goto error_init_shader_modules; + } + setObjectName(device, context, VK_OBJECT_TYPE_SHADER_MODULE, (uint64_t)shaderModule, csMetaData.objectName); + + context->computeShaders[numShaderModulesInited] = shaderModule; + } + + for ( ; numPipelinesInited < NUM_COMPUTE_SHADERS; ++numPipelinesInited) + { + VkPipeline pipeline; + ComputeShaderMetaData csMetaData = COMPUTE_SHADER_META_DATA[numPipelinesInited]; + + VkPipelineShaderStageCreateInfo stageInfo = {}; + stageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stageInfo.pNext = NULL; + stageInfo.flags = 0; + stageInfo.stage = VK_SHADER_STAGE_COMPUTE_BIT; + stageInfo.module = context->computeShaders[numPipelinesInited]; + stageInfo.pName = csMetaData.name; + stageInfo.pSpecializationInfo = NULL; + + VkComputePipelineCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; + info.pNext = NULL; + info.flags = 0; + info.stage = stageInfo; + info.layout = context->pipelineLayouts[csMetaData.descriptorSetLayoutID]; + info.basePipelineHandle = VK_NULL_HANDLE; + info.basePipelineIndex = 0; + + result = vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &info, NULL, &pipeline); + if (result != VK_SUCCESS) + { + goto error_init_pipelines; + } + setObjectName(device, context, VK_OBJECT_TYPE_PIPELINE, (uint64_t)pipeline, csMetaData.objectName); + + context->computePipelines[numPipelinesInited] = pipeline; + } + + // create descriptor pool + { + VkDescriptorPool descriptorPool; + + VkDescriptorPoolSize poolSizes[4] = {}; + poolSizes[0].type = VK_DESCRIPTOR_TYPE_SAMPLER; + poolSizes[0].descriptorCount = NUM_BACK_BUFFERS * NUM_DESCRIPTOR_SETS * 5; + poolSizes[1].type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + poolSizes[1].descriptorCount = NUM_BACK_BUFFERS * NUM_DESCRIPTOR_SETS * 7; + poolSizes[2].type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + poolSizes[2].descriptorCount = NUM_BACK_BUFFERS * NUM_DESCRIPTOR_SETS * 4; + poolSizes[3].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + poolSizes[3].descriptorCount = NUM_BACK_BUFFERS * NUM_DESCRIPTOR_SETS * 1; + + VkDescriptorPoolCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + info.pNext = NULL; + info.flags = 0; + info.maxSets = NUM_BACK_BUFFERS * NUM_DESCRIPTOR_SETS; + info.poolSizeCount = FFX_CACAO_ARRAY_SIZE(poolSizes); + info.pPoolSizes = poolSizes; + + result = vkCreateDescriptorPool(device, &info, NULL, &descriptorPool); + if (result != VK_SUCCESS) + { + goto error_init_descriptor_pool; + } + setObjectName(device, context, VK_OBJECT_TYPE_DESCRIPTOR_POOL, (uint64_t)descriptorPool, "FFX_CACAO_DESCRIPTOR_POOL"); + + context->descriptorPool = descriptorPool; + } + + // allocate descriptor sets + { + VkDescriptorSetLayout descriptorSetLayouts[NUM_DESCRIPTOR_SETS]; + for (uint32_t i = 0; i < NUM_DESCRIPTOR_SETS; ++i) { + descriptorSetLayouts[i] = context->descriptorSetLayouts[DESCRIPTOR_SET_META_DATA[i].descriptorSetLayoutID]; + } + + for (uint32_t i = 0; i < NUM_BACK_BUFFERS; ++i) { + VkDescriptorSetAllocateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + info.pNext = NULL; + info.descriptorPool = context->descriptorPool; + info.descriptorSetCount = FFX_CACAO_ARRAY_SIZE(descriptorSetLayouts); // FFX_CACAO_ARRAY_SIZE(context->descriptorSetLayouts); + info.pSetLayouts = descriptorSetLayouts; // context->descriptorSetLayouts; + + result = vkAllocateDescriptorSets(device, &info, context->descriptorSets[i]); + if (result != VK_SUCCESS) + { + goto error_allocate_descriptor_sets; + } + } + + char name[1024]; + for (uint32_t j = 0; j < NUM_BACK_BUFFERS; ++j) { + for (uint32_t i = 0; i < NUM_DESCRIPTOR_SETS; ++i) { + DescriptorSetMetaData dsMetaData = DESCRIPTOR_SET_META_DATA[i]; + snprintf(name, FFX_CACAO_ARRAY_SIZE(name), "%s_%u", dsMetaData.name, j); + setObjectName(device, context, VK_OBJECT_TYPE_DESCRIPTOR_SET, (uint64_t)context->descriptorSets[j][i], name); + } + } + } + + // assign memory to constant buffers + for ( ; numConstantBackBuffersInited < NUM_BACK_BUFFERS; ++numConstantBackBuffersInited) + { + for (uint32_t j = 0; j < 4; ++j) + { + VkBuffer buffer = context->constantBuffer[numConstantBackBuffersInited][j]; + + VkBufferCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + info.pNext = NULL; + info.flags = 0; + info.size = sizeof(FFX_CACAO_Constants); + info.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.queueFamilyIndexCount = 0; + info.pQueueFamilyIndices = NULL; + + result = vkCreateBuffer(device, &info, NULL, &buffer); + if (result != VK_SUCCESS) + { + goto error_init_constant_buffers; + } + char name[1024]; + snprintf(name, FFX_CACAO_ARRAY_SIZE(name), "FFX_CACAO_CONSTANT_BUFFER_PASS_%u_BACK_BUFFER_%u", j, numConstantBackBuffersInited); + setObjectName(device, context, VK_OBJECT_TYPE_BUFFER, (uint64_t)buffer, name); + + VkMemoryRequirements memoryRequirements; + vkGetBufferMemoryRequirements(device, buffer, &memoryRequirements); + + uint32_t chosenMemoryTypeIndex = getBestMemoryHeapIndex(physicalDevice, memoryRequirements, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + if (chosenMemoryTypeIndex == VK_MAX_MEMORY_TYPES) + { + vkDestroyBuffer(device, buffer, NULL); + goto error_init_constant_buffers; + } + + VkMemoryAllocateInfo allocationInfo = {}; + allocationInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocationInfo.pNext = NULL; + allocationInfo.allocationSize = memoryRequirements.size; + allocationInfo.memoryTypeIndex = chosenMemoryTypeIndex; + + VkDeviceMemory memory; + result = vkAllocateMemory(device, &allocationInfo, NULL, &memory); + if (result != VK_SUCCESS) + { + vkDestroyBuffer(device, buffer, NULL); + goto error_init_constant_buffers; + } + + result = vkBindBufferMemory(device, buffer, memory, 0); + if (result != VK_SUCCESS) + { + vkDestroyBuffer(device, buffer, NULL); + goto error_init_constant_buffers; + } + + context->constantBufferMemory[numConstantBackBuffersInited][j] = memory; + context->constantBuffer[numConstantBackBuffersInited][j] = buffer; + } + } + + // create load counter VkImage + { + VkImage image = VK_NULL_HANDLE; + + VkImageCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + info.pNext = NULL; + info.flags = 0; + info.imageType = VK_IMAGE_TYPE_1D; + info.format = VK_FORMAT_R32_UINT; + info.extent.width = 1; + info.extent.height = 1; + info.extent.depth = 1; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.queueFamilyIndexCount = 0; + info.pQueueFamilyIndices = NULL; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + result = vkCreateImage(device, &info, NULL, &image); + if (result != VK_SUCCESS) + { + goto error_init_load_counter_image; + } + + setObjectName(device, context, VK_OBJECT_TYPE_IMAGE, (uint64_t)image, "FFX_CACAO_LOAD_COUNTER"); + + VkMemoryRequirements memoryRequirements; + vkGetImageMemoryRequirements(device, image, &memoryRequirements); + + uint32_t chosenMemoryTypeIndex = getBestMemoryHeapIndex(physicalDevice, memoryRequirements, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + if (chosenMemoryTypeIndex == VK_MAX_MEMORY_TYPES) + { + vkDestroyImage(device, image, NULL); + goto error_init_load_counter_image; + } + + VkMemoryAllocateInfo allocationInfo = {}; + allocationInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocationInfo.pNext = NULL; + allocationInfo.allocationSize = memoryRequirements.size; + allocationInfo.memoryTypeIndex = chosenMemoryTypeIndex; + + VkDeviceMemory memory; + result = vkAllocateMemory(device, &allocationInfo, NULL, &memory); + if (result != VK_SUCCESS) + { + vkDestroyImage(device, image, NULL); + goto error_init_load_counter_image; + } + + result = vkBindImageMemory(device, image, memory, 0); + if (result != VK_SUCCESS) + { + vkDestroyImage(device, image, NULL); + goto error_init_load_counter_image; + } + + context->loadCounter = image; + context->loadCounterMemory = memory; + } + + // create load counter view + { + VkImageView imageView; + + VkImageViewCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + info.pNext = NULL; + info.flags = 0; + info.image = context->loadCounter; + info.viewType = VK_IMAGE_VIEW_TYPE_1D; + info.format = VK_FORMAT_R32_UINT; + info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; + info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; + info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; + info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; + info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + info.subresourceRange.baseMipLevel = 0; + info.subresourceRange.levelCount = 1; + info.subresourceRange.baseArrayLayer = 0; + info.subresourceRange.layerCount = 1; + + result = vkCreateImageView(device, &info, NULL, &imageView); + if (result != VK_SUCCESS) + { + goto error_init_load_counter_view; + } + + context->loadCounterView = imageView; + } + +#ifdef FFX_CACAO_ENABLE_PROFILING + // create timestamp query pool + { + VkQueryPool queryPool = VK_NULL_HANDLE; + + VkQueryPoolCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + info.pNext = NULL; + info.flags = 0; + info.queryType = VK_QUERY_TYPE_TIMESTAMP; + info.queryCount = NUM_TIMESTAMPS * NUM_BACK_BUFFERS; + + result = vkCreateQueryPool(device, &info, NULL, &queryPool); + if (result != VK_SUCCESS) + { + goto error_init_query_pool; + } + + context->timestampQueryPool = queryPool; + } +#endif + + return FFX_CACAO_STATUS_OK; + +#ifdef FFX_CACAO_ENABLE_PROFILING + vkDestroyQueryPool(device, context->timestampQueryPool, NULL); +error_init_query_pool: +#endif + + vkDestroyImageView(device, context->loadCounterView, NULL); +error_init_load_counter_view: + vkDestroyImage(device, context->loadCounter, NULL); + vkFreeMemory(device, context->loadCounterMemory, NULL); +error_init_load_counter_image: + +error_init_constant_buffers: + for (uint32_t i = 0; i < numConstantBackBuffersInited; ++i) + { + for (uint32_t j = 0; j < 4; ++j) + { + vkDestroyBuffer(device, context->constantBuffer[i][j], NULL); + vkFreeMemory(device, context->constantBufferMemory[i][j], NULL); + } + } + +error_allocate_descriptor_sets: + vkDestroyDescriptorPool(device, context->descriptorPool, NULL); +error_init_descriptor_pool: + +error_init_pipelines: + for (uint32_t i = 0; i < numPipelinesInited; ++i) + { + vkDestroyPipeline(device, context->computePipelines[i], NULL); + } + +error_init_shader_modules: + for (uint32_t i = 0; i < numShaderModulesInited; ++i) + { + vkDestroyShaderModule(device, context->computeShaders[i], NULL); + } + +error_init_pipeline_layouts: + for (uint32_t i = 0; i < numPipelineLayoutsInited; ++i) + { + vkDestroyPipelineLayout(device, context->pipelineLayouts[i], NULL); + } + +error_init_descriptor_set_layouts: + for (uint32_t i = 0; i < numDescriptorSetLayoutsInited; ++i) + { + vkDestroyDescriptorSetLayout(device, context->descriptorSetLayouts[i], NULL); + } + + +error_init_samplers: + for (uint32_t i = 0; i < numSamplersInited; ++i) + { + vkDestroySampler(device, context->samplers[i], NULL); + } + + return errorStatus; +} + +FFX_CACAO_Status FFX_CACAO_VkDestroyContext(FFX_CACAO_VkContext* context) +{ + if (context == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + context = getAlignedVkContextPointer(context); + + VkDevice device = context->device; + +#ifdef FFX_CACAO_ENABLE_PROFILING + vkDestroyQueryPool(device, context->timestampQueryPool, NULL); +#endif + + vkDestroyImageView(device, context->loadCounterView, NULL); + vkDestroyImage(device, context->loadCounter, NULL); + vkFreeMemory(device, context->loadCounterMemory, NULL); + + for (uint32_t i = 0; i < NUM_BACK_BUFFERS; ++i) + { + for (uint32_t j = 0; j < 4; ++j) + { + vkDestroyBuffer(device, context->constantBuffer[i][j], NULL); + vkFreeMemory(device, context->constantBufferMemory[i][j], NULL); + } + } + + vkDestroyDescriptorPool(device, context->descriptorPool, NULL); + + for (uint32_t i = 0; i < NUM_COMPUTE_SHADERS; ++i) + { + vkDestroyPipeline(device, context->computePipelines[i], NULL); + } + + for (uint32_t i = 0; i < NUM_COMPUTE_SHADERS; ++i) + { + vkDestroyShaderModule(device, context->computeShaders[i], NULL); + } + + for (uint32_t i = 0; i < NUM_DESCRIPTOR_SET_LAYOUTS; ++i) + { + vkDestroyPipelineLayout(device, context->pipelineLayouts[i], NULL); + } + + for(uint32_t i = 0; i < NUM_DESCRIPTOR_SET_LAYOUTS; ++i) + { + vkDestroyDescriptorSetLayout(device, context->descriptorSetLayouts[i], NULL); + } + + + for (uint32_t i = 0; i < FFX_CACAO_ARRAY_SIZE(context->samplers); ++i) + { + vkDestroySampler(device, context->samplers[i], NULL); + } + + return FFX_CACAO_STATUS_OK; +} + +FFX_CACAO_Status FFX_CACAO_VkInitScreenSizeDependentResources(FFX_CACAO_VkContext* context, const FFX_CACAO_VkScreenSizeInfo* info) +{ + if (context == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + if (info == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + context = getAlignedVkContextPointer(context); + + FFX_CACAO_Bool useDownsampledSsao = info->useDownsampledSsao; + context->useDownsampledSsao = useDownsampledSsao; + context->output = info->output; + + VkDevice device = context->device; + VkPhysicalDevice physicalDevice = context->physicalDevice; + VkPhysicalDeviceMemoryProperties memoryProperties; + vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memoryProperties); + VkResult result; + + FFX_CACAO_BufferSizeInfo *bsi = &context->bufferSizeInfo; + FFX_CACAO_UpdateBufferSizeInfo(info->width, info->height, useDownsampledSsao, bsi); + + FFX_CACAO_Status errorStatus = FFX_CACAO_STATUS_FAILED; + uint32_t numTextureImagesInited = 0; + uint32_t numTextureMemoriesInited = 0; + uint32_t numSrvsInited = 0; + uint32_t numUavsInited = 0; + + // create images for textures + for ( ; numTextureImagesInited < NUM_TEXTURES; ++numTextureImagesInited) + { + TextureMetaData metaData = TEXTURE_META_DATA[numTextureImagesInited]; + VkImage image = VK_NULL_HANDLE; + + VkImageCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + info.pNext = NULL; + info.flags = 0; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = TEXTURE_FORMAT_LOOKUP_VK[metaData.format]; + info.extent.width = *(uint32_t*)((uint8_t*)bsi + metaData.widthOffset); + info.extent.height = *(uint32_t*)((uint8_t*)bsi + metaData.heightOffset); + info.extent.depth = 1; + info.mipLevels = metaData.numMips; + info.arrayLayers = metaData.arraySize; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.queueFamilyIndexCount = 0; + info.pQueueFamilyIndices = NULL; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + result = vkCreateImage(device, &info, NULL, &image); + if (result != VK_SUCCESS) + { + goto error_init_texture_images; + } + + setObjectName(device, context, VK_OBJECT_TYPE_IMAGE, (uint64_t)image, metaData.name); + + context->textures[numTextureImagesInited] = image; + } + + // allocate memory for textures + for ( ; numTextureMemoriesInited < NUM_TEXTURES; ++numTextureMemoriesInited) + { + VkImage image = context->textures[numTextureMemoriesInited]; + + VkMemoryRequirements memoryRequirements; + vkGetImageMemoryRequirements(device, image, &memoryRequirements); + + uint32_t chosenMemoryTypeIndex = getBestMemoryHeapIndex(physicalDevice, memoryRequirements, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + if (chosenMemoryTypeIndex == VK_MAX_MEMORY_TYPES) + { + goto error_init_texture_memories; + } + + VkMemoryAllocateInfo allocationInfo = {}; + allocationInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocationInfo.pNext = NULL; + allocationInfo.allocationSize = memoryRequirements.size; + allocationInfo.memoryTypeIndex = chosenMemoryTypeIndex; + + VkDeviceMemory memory; + result = vkAllocateMemory(device, &allocationInfo, NULL, &memory); + if (result != VK_SUCCESS) + { + goto error_init_texture_memories; + } + + result = vkBindImageMemory(device, image, memory, 0); + if (result != VK_SUCCESS) + { + vkFreeMemory(device, memory, NULL); + goto error_init_texture_memories; + } + + context->textureMemory[numTextureMemoriesInited] = memory; + } + + // create srv image views + for ( ; numSrvsInited < NUM_SHADER_RESOURCE_VIEWS; ++numSrvsInited) + { + VkImageView imageView; + ShaderResourceViewMetaData srvMetaData = SRV_META_DATA[numSrvsInited]; + + VkImageViewCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + info.pNext = NULL; + info.flags = 0; + info.image = context->textures[srvMetaData.texture]; + info.viewType = VIEW_TYPE_LOOKUP_VK[srvMetaData.viewType]; + info.format = TEXTURE_FORMAT_LOOKUP_VK[TEXTURE_META_DATA[srvMetaData.texture].format]; + info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; + info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; + info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; + info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; + info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + info.subresourceRange.baseMipLevel = srvMetaData.mostDetailedMip; + info.subresourceRange.levelCount = srvMetaData.mipLevels; + info.subresourceRange.baseArrayLayer = srvMetaData.firstArraySlice; + info.subresourceRange.layerCount = srvMetaData.arraySize; + + result = vkCreateImageView(device, &info, NULL, &imageView); + if (result != VK_SUCCESS) + { + goto error_init_srvs; + } + + context->shaderResourceViews[numSrvsInited] = imageView; + } + + // create uav image views + for ( ; numUavsInited < NUM_UNORDERED_ACCESS_VIEWS; ++numUavsInited) + { + VkImageView imageView; + UnorderedAccessViewMetaData uavMetaData = UAV_META_DATA[numUavsInited]; + + VkImageViewCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + info.pNext = NULL; + info.flags = 0; + info.image = context->textures[uavMetaData.textureID]; + info.viewType = VIEW_TYPE_LOOKUP_VK[uavMetaData.viewType]; + info.format = TEXTURE_FORMAT_LOOKUP_VK[TEXTURE_META_DATA[uavMetaData.textureID].format]; + info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; + info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; + info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; + info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; + info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + info.subresourceRange.baseMipLevel = uavMetaData.mostDetailedMip; + info.subresourceRange.levelCount = 1; + info.subresourceRange.baseArrayLayer = uavMetaData.firstArraySlice; + info.subresourceRange.layerCount = uavMetaData.arraySize; + + result = vkCreateImageView(device, &info, NULL, &imageView); + if (result != VK_SUCCESS) + { + goto error_init_uavs; + } + + context->unorderedAccessViews[numUavsInited] = imageView; + } + + // update descriptor sets from table + for (uint32_t i = 0; i < NUM_BACK_BUFFERS; ++i) { + VkDescriptorImageInfo imageInfos[NUM_INPUT_DESCRIPTOR_BINDINGS + NUM_OUTPUT_DESCRIPTOR_BINDINGS] = {}; + VkDescriptorImageInfo *curImageInfo = imageInfos; + VkWriteDescriptorSet writes[NUM_INPUT_DESCRIPTOR_BINDINGS + NUM_OUTPUT_DESCRIPTOR_BINDINGS] = {}; + VkWriteDescriptorSet *curWrite = writes; + + // write input descriptor bindings + for (uint32_t j = 0; j < NUM_INPUT_DESCRIPTOR_BINDINGS; ++j) + { + InputDescriptorBindingMetaData bindingMetaData = INPUT_DESCRIPTOR_BINDING_META_DATA[j]; + + curImageInfo->sampler = VK_NULL_HANDLE; + curImageInfo->imageView = context->shaderResourceViews[bindingMetaData.srvID]; + curImageInfo->imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + curWrite->sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + curWrite->pNext = NULL; + curWrite->dstSet = context->descriptorSets[i][bindingMetaData.descriptorID]; + curWrite->dstBinding = 20 + bindingMetaData.bindingNumber; + curWrite->descriptorCount = 1; + curWrite->descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + curWrite->pImageInfo = curImageInfo; + + ++curWrite; ++curImageInfo; + } + + // write output descriptor bindings + for (uint32_t j = 0; j < NUM_OUTPUT_DESCRIPTOR_BINDINGS; ++j) + { + OutputDescriptorBindingMetaData bindingMetaData = OUTPUT_DESCRIPTOR_BINDING_META_DATA[j]; + + curImageInfo->sampler = VK_NULL_HANDLE; + curImageInfo->imageView = context->unorderedAccessViews[bindingMetaData.uavID]; + curImageInfo->imageLayout = VK_IMAGE_LAYOUT_GENERAL; + + curWrite->sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + curWrite->pNext = VK_NULL_HANDLE; + curWrite->dstSet = context->descriptorSets[i][bindingMetaData.descriptorID]; + curWrite->dstBinding = 30 + bindingMetaData.bindingNumber; + curWrite->descriptorCount = 1; + curWrite->descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + curWrite->pImageInfo = curImageInfo; + + ++curWrite; ++curImageInfo; + } + + vkUpdateDescriptorSets(device, FFX_CACAO_ARRAY_SIZE(writes), writes, 0, NULL); + } + + // update descriptor sets with inputs + for (uint32_t i = 0; i < NUM_BACK_BUFFERS; ++i) { +#define MAX_NUM_MISC_INPUT_DESCRIPTORS 32 + + VkDescriptorImageInfo imageInfos[MAX_NUM_MISC_INPUT_DESCRIPTORS] = {}; + VkWriteDescriptorSet writes[MAX_NUM_MISC_INPUT_DESCRIPTORS] = {}; + + for (uint32_t i = 0; i < FFX_CACAO_ARRAY_SIZE(writes); ++i) + { + VkDescriptorImageInfo *imageInfo = imageInfos + i; + VkWriteDescriptorSet *write = writes + i; + + imageInfo->sampler = VK_NULL_HANDLE; + + write->sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write->pNext = NULL; + write->descriptorCount = 1; + write->pImageInfo = imageInfo; + } + + uint32_t cur = 0; + + // register(t0) -> 20 + // register(u0) -> 30 + imageInfos[cur].imageView = info->depthView; + imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + writes[cur].dstSet = context->descriptorSets[i][DS_PREPARE_DEPTHS]; + writes[cur].dstBinding = 20; + writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + ++cur; + + imageInfos[cur].imageView = info->depthView; + imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + writes[cur].dstSet = context->descriptorSets[i][DS_PREPARE_DEPTHS_MIPS]; + writes[cur].dstBinding = 20; + writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + ++cur; + + imageInfos[cur].imageView = info->depthView; + imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + writes[cur].dstSet = context->descriptorSets[i][DS_PREPARE_NORMALS]; + writes[cur].dstBinding = 20; + writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + ++cur; + + imageInfos[cur].imageView = info->depthView; + imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + writes[cur].dstSet = context->descriptorSets[i][DS_BILATERAL_UPSAMPLE_PING]; + writes[cur].dstBinding = 21; + writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + ++cur; + + imageInfos[cur].imageView = info->depthView; + imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + writes[cur].dstSet = context->descriptorSets[i][DS_BILATERAL_UPSAMPLE_PONG]; + writes[cur].dstBinding = 21; + writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + ++cur; + + imageInfos[cur].imageView = info->outputView; + imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_GENERAL; + writes[cur].dstSet = context->descriptorSets[i][DS_BILATERAL_UPSAMPLE_PING]; + writes[cur].dstBinding = 30; + writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + ++cur; + + imageInfos[cur].imageView = info->outputView; + imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_GENERAL; + writes[cur].dstSet = context->descriptorSets[i][DS_BILATERAL_UPSAMPLE_PONG]; + writes[cur].dstBinding = 30; + writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + ++cur; + + imageInfos[cur].imageView = info->outputView; + imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_GENERAL; + writes[cur].dstSet = context->descriptorSets[i][DS_APPLY_PING]; + writes[cur].dstBinding = 30; + writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + ++cur; + + imageInfos[cur].imageView = info->outputView; + imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_GENERAL; + writes[cur].dstSet = context->descriptorSets[i][DS_APPLY_PONG]; + writes[cur].dstBinding = 30; + writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + ++cur; + + imageInfos[cur].imageView = context->loadCounterView; + imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_GENERAL; + writes[cur].dstSet = context->descriptorSets[i][DS_POSTPROCESS_IMPORTANCE_MAP_B]; + writes[cur].dstBinding = 31; + writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + ++cur; + + imageInfos[cur].imageView = context->loadCounterView; + imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_GENERAL; + writes[cur].dstSet = context->descriptorSets[i][DS_CLEAR_LOAD_COUNTER]; + writes[cur].dstBinding = 30; + writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + ++cur; + + for (uint32_t pass = 0; pass < 4; ++pass) + { + imageInfos[cur].imageView = context->loadCounterView; + imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_GENERAL; + writes[cur].dstSet = context->descriptorSets[i][(DescriptorSetID)(DS_GENERATE_ADAPTIVE_0 + pass)]; + writes[cur].dstBinding = 22; + writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + ++cur; + } + + if (info->normalsView) { + imageInfos[cur].imageView = info->normalsView; + imageInfos[cur].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + writes[cur].dstSet = context->descriptorSets[i][DS_PREPARE_NORMALS_FROM_INPUT_NORMALS]; + writes[cur].dstBinding = 20; + writes[cur].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + ++cur; + } + + FFX_CACAO_ASSERT(cur <= MAX_NUM_MISC_INPUT_DESCRIPTORS); + vkUpdateDescriptorSets(device, cur, writes, 0, NULL); + } + + // update descriptor sets with constant buffers + for (uint32_t i = 0; i < NUM_BACK_BUFFERS; ++i) { + VkDescriptorBufferInfo bufferInfos[NUM_DESCRIPTOR_SETS] = {}; + VkDescriptorBufferInfo *curBufferInfo = bufferInfos; + VkWriteDescriptorSet writes[NUM_DESCRIPTOR_SETS] = {}; + VkWriteDescriptorSet *curWrite = writes; + + for (uint32_t j = 0; j < NUM_DESCRIPTOR_SETS; ++j) + { + DescriptorSetMetaData dsMetaData = DESCRIPTOR_SET_META_DATA[j]; + + curBufferInfo->buffer = context->constantBuffer[i][dsMetaData.pass]; + curBufferInfo->offset = 0; + curBufferInfo->range = VK_WHOLE_SIZE; + + curWrite->sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + curWrite->pNext = NULL; + curWrite->dstSet = context->descriptorSets[i][j]; + curWrite->dstBinding = 10; + curWrite->dstArrayElement = 0; + curWrite->descriptorCount = 1; + curWrite->descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + curWrite->pBufferInfo = curBufferInfo; + + ++curWrite; + ++curBufferInfo; + } + + vkUpdateDescriptorSets(device, FFX_CACAO_ARRAY_SIZE(writes), writes, 0, NULL); + } + + return FFX_CACAO_STATUS_OK; + +error_init_uavs: + for (uint32_t i = 0; i < numUavsInited; ++i) + { + vkDestroyImageView(device, context->unorderedAccessViews[i], NULL); + } + +error_init_srvs: + for (uint32_t i = 0; i < numSrvsInited; ++i) + { + vkDestroyImageView(device, context->shaderResourceViews[i], NULL); + } + +error_init_texture_memories: + for (uint32_t i = 0; i < numTextureMemoriesInited; ++i) + { + vkFreeMemory(device, context->textureMemory[i], NULL); + } + +error_init_texture_images: + for (uint32_t i = 0; i < numTextureImagesInited; ++i) + { + vkDestroyImage(device, context->textures[i], NULL); + } + + return errorStatus; +} + +FFX_CACAO_Status FFX_CACAO_VkDestroyScreenSizeDependentResources(FFX_CACAO_VkContext* context) +{ + if (context == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + context = getAlignedVkContextPointer(context); + + VkDevice device = context->device; + + for (uint32_t i = 0; i < NUM_UNORDERED_ACCESS_VIEWS; ++i) + { + vkDestroyImageView(device, context->unorderedAccessViews[i], NULL); + } + + for (uint32_t i = 0; i < NUM_SHADER_RESOURCE_VIEWS; ++i) + { + vkDestroyImageView(device, context->shaderResourceViews[i], NULL); + } + + for (uint32_t i = 0; i < NUM_TEXTURES; ++i) + { + vkFreeMemory(device, context->textureMemory[i], NULL); + } + + for (uint32_t i = 0; i < NUM_TEXTURES; ++i) + { + vkDestroyImage(device, context->textures[i], NULL); + } + + return FFX_CACAO_STATUS_OK; +} + +FFX_CACAO_Status FFX_CACAO_VkUpdateSettings(FFX_CACAO_VkContext* context, const FFX_CACAO_Settings* settings) +{ + if (context == NULL || settings == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + context = getAlignedVkContextPointer(context); + + memcpy(&context->settings, settings, sizeof(*settings)); + + return FFX_CACAO_STATUS_OK; +} + +static inline void computeDispatch(FFX_CACAO_VkContext* context, VkCommandBuffer cb, DescriptorSetID ds, ComputeShaderID cs, uint32_t width, uint32_t height, uint32_t depth) +{ + DescriptorSetLayoutID dsl = DESCRIPTOR_SET_META_DATA[ds].descriptorSetLayoutID; + vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_COMPUTE, context->pipelineLayouts[dsl], 0, 1, &context->descriptorSets[context->currentConstantBuffer][ds], 0, NULL); + vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, context->computePipelines[cs]); + vkCmdDispatch(cb, width, height, depth); +} + +typedef struct BarrierList +{ + uint32_t len; + VkImageMemoryBarrier barriers[32]; +} BarrierList; + +static inline void pushBarrier(BarrierList* barrierList, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkAccessFlags srcAccessFlags, VkAccessFlags dstAccessFlags) +{ + FFX_CACAO_ASSERT(barrierList->len < FFX_CACAO_ARRAY_SIZE(barrierList->barriers)); + VkImageMemoryBarrier barrier = {}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.pNext = NULL; + barrier.srcAccessMask = srcAccessFlags; + barrier.dstAccessMask = dstAccessFlags; + barrier.oldLayout = oldLayout; + barrier.newLayout = newLayout; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = VK_REMAINING_MIP_LEVELS; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS; + barrier.image = image; + barrierList->barriers[barrierList->len++] = barrier; +} + +static inline void beginDebugMarker(FFX_CACAO_VkContext* context, VkCommandBuffer cb, const char* name) +{ + if (context->vkCmdDebugMarkerBegin) + { + VkDebugMarkerMarkerInfoEXT info = {}; + info.sType = VK_STRUCTURE_TYPE_DEBUG_MARKER_MARKER_INFO_EXT; + info.pNext = NULL; + info.pMarkerName = name; + info.color[0] = 1.0f; + info.color[1] = 0.0f; + info.color[2] = 0.0f; + info.color[3] = 1.0f; + + context->vkCmdDebugMarkerBegin(cb, &info); + } +} + +static inline void endDebugMarker(FFX_CACAO_VkContext* context, VkCommandBuffer cb) +{ + if (context->vkCmdDebugMarkerEnd) + { + context->vkCmdDebugMarkerEnd(cb); + } +} + +FFX_CACAO_Status FFX_CACAO_VkDraw(FFX_CACAO_VkContext* context, VkCommandBuffer cb, const FFX_CACAO_Matrix4x4* proj, const FFX_CACAO_Matrix4x4* normalsToView) +{ + if (context == NULL || cb == VK_NULL_HANDLE || proj == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + context = getAlignedVkContextPointer(context); + + FFX_CACAO_Settings *settings = &context->settings; + FFX_CACAO_BufferSizeInfo *bsi = &context->bufferSizeInfo; + VkDevice device = context->device; + VkDescriptorSet *ds = context->descriptorSets[context->currentConstantBuffer]; + VkImage *tex = context->textures; + VkResult result; + BarrierList barrierList; + + uint32_t curBuffer = context->currentConstantBuffer; + curBuffer = (curBuffer + 1) % NUM_BACK_BUFFERS; + context->currentConstantBuffer = curBuffer; +#ifdef FFX_CACAO_ENABLE_PROFILING + { + uint32_t collectBuffer = context->collectBuffer = (curBuffer + 1) % NUM_BACK_BUFFERS; + if (uint32_t numQueries = context->timestampQueries[collectBuffer].numTimestamps) + { + uint32_t offset = collectBuffer * NUM_TIMESTAMPS; + vkGetQueryPoolResults(device, context->timestampQueryPool, offset, numQueries, numQueries * sizeof(uint64_t), context->timestampQueries[collectBuffer].timings, sizeof(uint64_t), VK_QUERY_RESULT_64_BIT); + } + } +#endif + + beginDebugMarker(context, cb, "FidelityFX CACAO"); + + // update constant buffer + + for (uint32_t i = 0; i < 4; ++i) + { + VkDeviceMemory memory = context->constantBufferMemory[curBuffer][i]; + void *data = NULL; + result = vkMapMemory(device, memory, 0, VK_WHOLE_SIZE, 0, &data); + FFX_CACAO_ASSERT(result == VK_SUCCESS); + FFX_CACAO_UpdateConstants((FFX_CACAO_Constants*)data, settings, bsi, proj, normalsToView); + FFX_CACAO_UpdatePerPassConstants((FFX_CACAO_Constants*)data, settings, bsi, i); + vkUnmapMemory(device, memory); + } + +#ifdef FFX_CACAO_ENABLE_PROFILING + uint32_t queryPoolOffset = curBuffer * NUM_TIMESTAMPS; + uint32_t numTimestamps = 0; + vkCmdResetQueryPool(cb, context->timestampQueryPool, queryPoolOffset, NUM_TIMESTAMPS); +#define GET_TIMESTAMP(name) \ + context->timestampQueries[curBuffer].timestamps[numTimestamps] = TIMESTAMP_##name; \ + vkCmdWriteTimestamp(cb, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, context->timestampQueryPool, queryPoolOffset + numTimestamps++); +#else +#define GET_TIMESTAMP(name) +#endif + + GET_TIMESTAMP(BEGIN) + + barrierList.len = 0; + pushBarrier(&barrierList, tex[TEXTURE_DEINTERLEAVED_DEPTHS], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); + pushBarrier(&barrierList, tex[TEXTURE_DEINTERLEAVED_NORMALS], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); + pushBarrier(&barrierList, tex[TEXTURE_SSAO_BUFFER_PING], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); + pushBarrier(&barrierList, tex[TEXTURE_SSAO_BUFFER_PONG], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); + pushBarrier(&barrierList, tex[TEXTURE_IMPORTANCE_MAP], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); + pushBarrier(&barrierList, tex[TEXTURE_IMPORTANCE_MAP_PONG], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); + pushBarrier(&barrierList, context->loadCounter, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); + + // prepare depths, normals and mips + { + beginDebugMarker(context, cb, "Prepare downsampled depths, normals and mips"); + + // clear load counter + computeDispatch(context, cb, DS_CLEAR_LOAD_COUNTER, CS_CLEAR_LOAD_COUNTER, 1, 1, 1); + + switch (context->settings.qualityLevel) + { + case FFX_CACAO_QUALITY_LOWEST: { + uint32_t dispatchWidth = dispatchSize(FFX_CACAO_PREPARE_DEPTHS_HALF_WIDTH, bsi->deinterleavedDepthBufferWidth); + uint32_t dispatchHeight = dispatchSize(FFX_CACAO_PREPARE_DEPTHS_HALF_HEIGHT, bsi->deinterleavedDepthBufferHeight); + ComputeShaderID csPrepareDepthsHalf = context->useDownsampledSsao ? CS_PREPARE_DOWNSAMPLED_DEPTHS_HALF : CS_PREPARE_NATIVE_DEPTHS_HALF; + computeDispatch(context, cb, DS_PREPARE_DEPTHS, csPrepareDepthsHalf, dispatchWidth, dispatchHeight, 1); + break; + } + case FFX_CACAO_QUALITY_LOW: { + uint32_t dispatchWidth = dispatchSize(FFX_CACAO_PREPARE_DEPTHS_WIDTH, bsi->deinterleavedDepthBufferWidth); + uint32_t dispatchHeight = dispatchSize(FFX_CACAO_PREPARE_DEPTHS_HEIGHT, bsi->deinterleavedDepthBufferHeight); + ComputeShaderID csPrepareDepths = context->useDownsampledSsao ? CS_PREPARE_DOWNSAMPLED_DEPTHS : CS_PREPARE_NATIVE_DEPTHS; + computeDispatch(context, cb, DS_PREPARE_DEPTHS, csPrepareDepths, dispatchWidth, dispatchHeight, 1); + break; + } + default: { + uint32_t dispatchWidth = dispatchSize(FFX_CACAO_PREPARE_DEPTHS_AND_MIPS_WIDTH, bsi->deinterleavedDepthBufferWidth); + uint32_t dispatchHeight = dispatchSize(FFX_CACAO_PREPARE_DEPTHS_AND_MIPS_HEIGHT, bsi->deinterleavedDepthBufferHeight); + ComputeShaderID csPrepareDepthsAndMips = context->useDownsampledSsao ? CS_PREPARE_DOWNSAMPLED_DEPTHS_AND_MIPS : CS_PREPARE_NATIVE_DEPTHS_AND_MIPS; + computeDispatch(context, cb, DS_PREPARE_DEPTHS_MIPS, csPrepareDepthsAndMips, dispatchWidth, dispatchHeight, 1); + break; + } + } + + if (context->settings.generateNormals) + { + uint32_t dispatchWidth = dispatchSize(FFX_CACAO_PREPARE_NORMALS_WIDTH, bsi->ssaoBufferWidth); + uint32_t dispatchHeight = dispatchSize(FFX_CACAO_PREPARE_NORMALS_HEIGHT, bsi->ssaoBufferHeight); + ComputeShaderID csPrepareNormals = context->useDownsampledSsao ? CS_PREPARE_DOWNSAMPLED_NORMALS : CS_PREPARE_NATIVE_NORMALS; + computeDispatch(context, cb, DS_PREPARE_NORMALS, csPrepareNormals, dispatchWidth, dispatchHeight, 1); + } + else + { + uint32_t dispatchWidth = dispatchSize(PREPARE_NORMALS_FROM_INPUT_NORMALS_WIDTH, bsi->ssaoBufferWidth); + uint32_t dispatchHeight = dispatchSize(PREPARE_NORMALS_FROM_INPUT_NORMALS_HEIGHT, bsi->ssaoBufferHeight); + ComputeShaderID csPrepareNormalsFromInputNormals = context->useDownsampledSsao ? CS_PREPARE_DOWNSAMPLED_NORMALS_FROM_INPUT_NORMALS : CS_PREPARE_NATIVE_NORMALS_FROM_INPUT_NORMALS; + computeDispatch(context, cb, DS_PREPARE_NORMALS_FROM_INPUT_NORMALS, csPrepareNormalsFromInputNormals, dispatchWidth, dispatchHeight, 1); + } + + endDebugMarker(context, cb); + GET_TIMESTAMP(PREPARE) + } + + barrierList.len = 0; + pushBarrier(&barrierList, tex[TEXTURE_DEINTERLEAVED_DEPTHS], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + pushBarrier(&barrierList, tex[TEXTURE_DEINTERLEAVED_NORMALS], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + pushBarrier(&barrierList, context->loadCounter, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT); + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); + + // base pass for highest quality setting + if (context->settings.qualityLevel == FFX_CACAO_QUALITY_HIGHEST) + { + beginDebugMarker(context, cb, "Generate High Quality Base Pass"); + + // SSAO + { + beginDebugMarker(context, cb, "Base SSAO"); + + uint32_t dispatchWidth = dispatchSize(FFX_CACAO_GENERATE_WIDTH, bsi->ssaoBufferWidth); + uint32_t dispatchHeight = dispatchSize(FFX_CACAO_GENERATE_HEIGHT, bsi->ssaoBufferHeight); + + for (int pass = 0; pass < 4; ++pass) + { + computeDispatch(context, cb, (DescriptorSetID)(DS_GENERATE_ADAPTIVE_BASE_0 + pass), CS_GENERATE_Q3_BASE, dispatchWidth, dispatchHeight, 1); + } + + endDebugMarker(context, cb); + } + + GET_TIMESTAMP(BASE_SSAO_PASS) + + barrierList.len = 0; + pushBarrier(&barrierList, tex[TEXTURE_SSAO_BUFFER_PONG], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); + + // generate importance map + { + beginDebugMarker(context, cb, "Importance Map"); + + uint32_t dispatchWidth = dispatchSize(IMPORTANCE_MAP_WIDTH, bsi->importanceMapWidth); + uint32_t dispatchHeight = dispatchSize(IMPORTANCE_MAP_HEIGHT, bsi->importanceMapHeight); + + computeDispatch(context, cb, DS_GENERATE_IMPORTANCE_MAP, CS_GENERATE_IMPORTANCE_MAP, dispatchWidth, dispatchHeight, 1); + + barrierList.len = 0; + pushBarrier(&barrierList, tex[TEXTURE_IMPORTANCE_MAP], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); + + computeDispatch(context, cb, DS_POSTPROCESS_IMPORTANCE_MAP_A, CS_POSTPROCESS_IMPORTANCE_MAP_A, dispatchWidth, dispatchHeight, 1); + + barrierList.len = 0; + pushBarrier(&barrierList, tex[TEXTURE_IMPORTANCE_MAP], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_SHADER_WRITE_BIT); + pushBarrier(&barrierList, tex[TEXTURE_IMPORTANCE_MAP_PONG], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); + + computeDispatch(context, cb, DS_POSTPROCESS_IMPORTANCE_MAP_B, CS_POSTPROCESS_IMPORTANCE_MAP_B, dispatchWidth, dispatchHeight, 1); + + endDebugMarker(context, cb); + } + + endDebugMarker(context, cb); + GET_TIMESTAMP(IMPORTANCE_MAP) + + barrierList.len = 0; + pushBarrier(&barrierList, tex[TEXTURE_IMPORTANCE_MAP], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + pushBarrier(&barrierList, context->loadCounter, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_SHADER_READ_BIT); + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); + } + + // main ssao generation + { + beginDebugMarker(context, cb, "Generate SSAO"); + + ComputeShaderID generateCS = (ComputeShaderID)(CS_GENERATE_Q0 + FFX_CACAO_MAX(0, context->settings.qualityLevel - 1)); + + uint32_t dispatchWidth, dispatchHeight, dispatchDepth; + + switch (context->settings.qualityLevel) + { + case FFX_CACAO_QUALITY_LOWEST: + case FFX_CACAO_QUALITY_LOW: + case FFX_CACAO_QUALITY_MEDIUM: + dispatchWidth = dispatchSize(FFX_CACAO_GENERATE_SPARSE_WIDTH, bsi->ssaoBufferWidth); + dispatchWidth = (dispatchWidth + 4) / 5; + dispatchHeight = dispatchSize(FFX_CACAO_GENERATE_SPARSE_HEIGHT, bsi->ssaoBufferHeight); + dispatchDepth = 5; + break; + case FFX_CACAO_QUALITY_HIGH: + case FFX_CACAO_QUALITY_HIGHEST: + dispatchWidth = dispatchSize(FFX_CACAO_GENERATE_WIDTH, bsi->ssaoBufferWidth); + dispatchHeight = dispatchSize(FFX_CACAO_GENERATE_HEIGHT, bsi->ssaoBufferHeight); + dispatchDepth = 1; + break; + } + + for (int pass = 0; pass < 4; ++pass) + { + if (context->settings.qualityLevel == FFX_CACAO_QUALITY_LOWEST && (pass == 1 || pass == 2)) + { + continue; + } + + DescriptorSetID descriptorSetID = context->settings.qualityLevel == FFX_CACAO_QUALITY_HIGHEST ? DS_GENERATE_ADAPTIVE_0 : DS_GENERATE_0; + descriptorSetID = (DescriptorSetID)(descriptorSetID + pass); + + computeDispatch(context, cb, descriptorSetID, generateCS, dispatchWidth, dispatchHeight, dispatchDepth); + } + + endDebugMarker(context, cb); + GET_TIMESTAMP(GENERATE_SSAO) + } + + uint32_t blurPassCount = context->settings.blurPassCount; + blurPassCount = FFX_CACAO_CLAMP(blurPassCount, 0, MAX_BLUR_PASSES); + + // de-interleaved blur + if (blurPassCount) + { + barrierList.len = 0; + pushBarrier(&barrierList, tex[TEXTURE_SSAO_BUFFER_PING], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + pushBarrier(&barrierList, tex[TEXTURE_SSAO_BUFFER_PONG], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); + + beginDebugMarker(context, cb, "Deinterleaved Blur"); + + uint32_t w = 4 * FFX_CACAO_BLUR_WIDTH - 2 * blurPassCount; + uint32_t h = 3 * FFX_CACAO_BLUR_HEIGHT - 2 * blurPassCount; + uint32_t dispatchWidth = dispatchSize(w, bsi->ssaoBufferWidth); + uint32_t dispatchHeight = dispatchSize(h, bsi->ssaoBufferHeight); + + for (int pass = 0; pass < 4; ++pass) + { + if (context->settings.qualityLevel == FFX_CACAO_QUALITY_LOWEST && (pass == 1 || pass == 2)) + { + continue; + } + + ComputeShaderID blurShaderID = (ComputeShaderID)(CS_EDGE_SENSITIVE_BLUR_1 + blurPassCount - 1); + DescriptorSetID descriptorSetID = (DescriptorSetID)(DS_EDGE_SENSITIVE_BLUR_0 + pass); + computeDispatch(context, cb, descriptorSetID, blurShaderID, dispatchWidth, dispatchHeight, 1); + } + + endDebugMarker(context, cb); + GET_TIMESTAMP(EDGE_SENSITIVE_BLUR) + + barrierList.len = 0; + pushBarrier(&barrierList, tex[TEXTURE_SSAO_BUFFER_PONG], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + pushBarrier(&barrierList, context->output, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); + } + else + { + barrierList.len = 0; + pushBarrier(&barrierList, tex[TEXTURE_SSAO_BUFFER_PING], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + pushBarrier(&barrierList, context->output, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_SHADER_WRITE_BIT); + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); + } + + + if (context->useDownsampledSsao) + { + beginDebugMarker(context, cb, "Bilateral Upsample"); + + uint32_t dispatchWidth = dispatchSize(2 * FFX_CACAO_BILATERAL_UPSCALE_WIDTH, bsi->inputOutputBufferWidth); + uint32_t dispatchHeight = dispatchSize(2 * FFX_CACAO_BILATERAL_UPSCALE_HEIGHT, bsi->inputOutputBufferHeight); + + DescriptorSetID descriptorSetID = blurPassCount ? DS_BILATERAL_UPSAMPLE_PONG : DS_BILATERAL_UPSAMPLE_PING; + ComputeShaderID upscaler; + switch (context->settings.qualityLevel) + { + case FFX_CACAO_QUALITY_LOWEST: + upscaler = CS_UPSCALE_BILATERAL_5X5_HALF; + break; + case FFX_CACAO_QUALITY_LOW: + case FFX_CACAO_QUALITY_MEDIUM: + upscaler = CS_UPSCALE_BILATERAL_5X5_NON_SMART; + break; + case FFX_CACAO_QUALITY_HIGH: + case FFX_CACAO_QUALITY_HIGHEST: + upscaler = CS_UPSCALE_BILATERAL_5X5_SMART; + break; + } + + computeDispatch(context, cb, descriptorSetID, upscaler, dispatchWidth, dispatchHeight, 1); + + endDebugMarker(context, cb); + GET_TIMESTAMP(BILATERAL_UPSAMPLE) + } + else + { + beginDebugMarker(context, cb, "Reinterleave"); + + uint32_t dispatchWidth = dispatchSize(FFX_CACAO_APPLY_WIDTH, bsi->inputOutputBufferWidth); + uint32_t dispatchHeight = dispatchSize(FFX_CACAO_APPLY_HEIGHT, bsi->inputOutputBufferHeight); + + DescriptorSetID descriptorSetID = blurPassCount ? DS_APPLY_PONG : DS_APPLY_PING; + + switch (context->settings.qualityLevel) + { + case FFX_CACAO_QUALITY_LOWEST: + computeDispatch(context, cb, descriptorSetID, CS_NON_SMART_HALF_APPLY, dispatchWidth, dispatchHeight, 1); + break; + case FFX_CACAO_QUALITY_LOW: + computeDispatch(context, cb, descriptorSetID, CS_NON_SMART_APPLY, dispatchWidth, dispatchHeight, 1); + break; + default: + computeDispatch(context, cb, descriptorSetID, CS_APPLY, dispatchWidth, dispatchHeight, 1); + break; + } + + endDebugMarker(context, cb); + GET_TIMESTAMP(APPLY) + } + + endDebugMarker(context, cb); + + barrierList.len = 0; + pushBarrier(&barrierList, context->output, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, barrierList.len, barrierList.barriers); + +#ifdef FFX_CACAO_ENABLE_PROFILING + context->timestampQueries[curBuffer].numTimestamps = numTimestamps; +#endif + + return FFX_CACAO_STATUS_OK; +} + +#ifdef FFX_CACAO_ENABLE_PROFILING +FFX_CACAO_Status FFX_CACAO_VkGetDetailedTimings(FFX_CACAO_VkContext* context, FFX_CACAO_DetailedTiming* timings) +{ + if (context == NULL || timings == NULL) + { + return FFX_CACAO_STATUS_INVALID_POINTER; + } + context = getAlignedVkContextPointer(context); + + uint32_t bufferIndex = context->collectBuffer; + uint32_t numTimestamps = context->timestampQueries[bufferIndex].numTimestamps; + uint64_t prevTime = context->timestampQueries[bufferIndex].timings[0]; + for (uint32_t i = 1; i < numTimestamps; ++i) + { + TimestampID timestampID = context->timestampQueries[bufferIndex].timestamps[i]; + timings->timestamps[i].label = TIMESTAMP_NAMES[timestampID]; + uint64_t time = context->timestampQueries[bufferIndex].timings[i]; + timings->timestamps[i].ticks = time - prevTime; + prevTime = time; + } + timings->timestamps[0].label = "FFX_CACAO_TOTAL"; + timings->timestamps[0].ticks = prevTime - context->timestampQueries[bufferIndex].timings[0]; + timings->numTimestamps = numTimestamps; + + return FFX_CACAO_STATUS_OK; +} +#endif +#endif + +#ifdef __cplusplus +} +#endif diff --git a/license.txt b/license.txt index 6adade0..cc2870e 100644 --- a/license.txt +++ b/license.txt @@ -1,4 +1,4 @@ -Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +Copyright (c) 2020-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 diff --git a/readme.md b/readme.md index dbbcd64..f59a2bd 100644 --- a/readme.md +++ b/readme.md @@ -1,14 +1,17 @@ # FidelityFX CACAO -Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +Copyright (c) 2020-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 @@ -28,12 +31,8 @@ You can find the binaries for FidelityFX CACAO in the release section on GitHub. # Sponza Model Issue -In the provided Sponza model for the FFX CACAO sample, at the bottom of some curtains, -there is an issue with incorrect normals -in the mesh causing ambient occlusion to be incorrectly calculated as light in places where occlusion -should be dark. This is a known issue with the mesh, and not an issue with FFX CACAO itself. +In the provided Sponza model for the FidelityFX CACAO sample, at the bottom of some curtains, there is an issue with incorrect normals in the mesh causing ambient occlusion to be incorrectly calculated as light in places where occlusion should be dark. This is a known issue with the mesh, and not an issue with FidelityFX CACAO itself. # Notices -CACAO is a modification of the Adaptive Screen Space Ambient Occlusion (ASSAO) algorithm that was developed by Intel. -The original implementation can be found [here](https://github.com/GameTechDev/ASSAO). +FidelityFX CACAO is a modification of the Adaptive Screen Space Ambient Occlusion (ASSAO) algorithm that was developed by Intel. The original implementation can be found [here](https://github.com/GameTechDev/ASSAO). diff --git a/sample/libs/cauldron b/sample/libs/cauldron index e850540..5a82a0c 160000 --- a/sample/libs/cauldron +++ b/sample/libs/cauldron @@ -1 +1 @@ -Subproject commit e85054054ae65f92c52f8cdde988d1b448f8dbf2 +Subproject commit 5a82a0ce20ea74e27e5e8818b78f0317d2e36157 diff --git a/sample/src/Common/FFX_CACAO_Common.h b/sample/src/Common/Common.h similarity index 83% rename from sample/src/Common/FFX_CACAO_Common.h rename to sample/src/Common/Common.h index b5a53af..62a0c49 100644 --- a/sample/src/Common/FFX_CACAO_Common.h +++ b/sample/src/Common/Common.h @@ -1,6 +1,6 @@ // AMD Sample sample code // -// Copyright(c) 2020 Advanced Micro Devices, Inc.All rights reserved. +// 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 @@ -20,20 +20,29 @@ #include "ffx_cacao.h" -struct FfxCacaoPreset +struct Preset { -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION bool useDownsampledSsao; -#endif - FfxCacaoSettings settings; + FFX_CACAO_Settings settings; }; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION -static const char *FFX_CACAO_PRESET_NAMES[] = { "Native - High Quality", "Native - Medium Quality", "Native - Low Quality", "Downsampled - High Quality", "Downsampled - Medium Quality", "Downsampled - Low Quality", "Custom" }; +static const char *FFX_CACAO_PRESET_NAMES[] = { + "Native - Adaptive Quality", + "Native - High Quality", + "Native - Medium Quality", + "Native - Low Quality", + "Native - Lowest Quality", + "Downsampled - Adaptive Quality", + "Downsampled - High Quality", + "Downsampled - Medium Quality", + "Downsampled - Low Quality", + "Downsampled - Lowest Quality", + "Custom" +}; -static const FfxCacaoPreset FFX_CACAO_PRESETS[] = { - // Native - High Quality +static const Preset FFX_CACAO_PRESETS[] = { + // Native - Adaptive Quality { /* useDownsampledSsao */ false, { @@ -56,7 +65,7 @@ static const FfxCacaoPreset FFX_CACAO_PRESETS[] = { /* bilateralSimilarityDistanceSigma */ 0.1f, } }, - // Native - Medium Quality + // Native - High Quality { /* useDownsampledSsao */ false, { @@ -67,7 +76,7 @@ static const FfxCacaoPreset FFX_CACAO_PRESETS[] = { /* horizonAngleThreshold */ 0.06f, /* fadeOutFrom */ 20.0f, /* fadeOutTo */ 40.0f, - /* qualityLevel */ FFX_CACAO_QUALITY_MEDIUM, + /* qualityLevel */ FFX_CACAO_QUALITY_HIGH, /* adaptiveQualityLimit */ 0.75f, /* blurPassCount */ 2, /* sharpness */ 0.98f, @@ -79,7 +88,7 @@ static const FfxCacaoPreset FFX_CACAO_PRESETS[] = { /* bilateralSimilarityDistanceSigma */ 0.1f, } }, - // Native - Low Quality + // Native - Medium Quality { /* useDownsampledSsao */ false, { @@ -90,9 +99,9 @@ static const FfxCacaoPreset FFX_CACAO_PRESETS[] = { /* horizonAngleThreshold */ 0.06f, /* fadeOutFrom */ 20.0f, /* fadeOutTo */ 40.0f, - /* qualityLevel */ FFX_CACAO_QUALITY_LOWEST, + /* qualityLevel */ FFX_CACAO_QUALITY_MEDIUM, /* adaptiveQualityLimit */ 0.75f, - /* blurPassCount */ 4, + /* blurPassCount */ 2, /* sharpness */ 0.98f, /* temporalSupersamplingAngleOffset */ 0.0f, /* temporalSupersamplingRadiusOffset */ 0.0f, @@ -102,9 +111,9 @@ static const FfxCacaoPreset FFX_CACAO_PRESETS[] = { /* bilateralSimilarityDistanceSigma */ 0.1f, } }, - // Downsampled - High Quality + // Native - Low Quality { - /* useDownsampledSsao */ true, + /* useDownsampledSsao */ false, { /* radius */ 1.2f, /* shadowMultiplier */ 1.0f, @@ -113,9 +122,9 @@ static const FfxCacaoPreset FFX_CACAO_PRESETS[] = { /* horizonAngleThreshold */ 0.06f, /* fadeOutFrom */ 20.0f, /* fadeOutTo */ 40.0f, - /* qualityLevel */ FFX_CACAO_QUALITY_HIGHEST, + /* qualityLevel */ FFX_CACAO_QUALITY_LOW, /* adaptiveQualityLimit */ 0.75f, - /* blurPassCount */ 2, + /* blurPassCount */ 6, /* sharpness */ 0.98f, /* temporalSupersamplingAngleOffset */ 0.0f, /* temporalSupersamplingRadiusOffset */ 0.0f, @@ -125,9 +134,9 @@ static const FfxCacaoPreset FFX_CACAO_PRESETS[] = { /* bilateralSimilarityDistanceSigma */ 0.1f, } }, - // Downsampled - Medium Quality + // Native - Lowest Quality { - /* useDownsampledSsao */ true, + /* useDownsampledSsao */ false, { /* radius */ 1.2f, /* shadowMultiplier */ 1.0f, @@ -136,19 +145,19 @@ static const FfxCacaoPreset FFX_CACAO_PRESETS[] = { /* horizonAngleThreshold */ 0.06f, /* fadeOutFrom */ 20.0f, /* fadeOutTo */ 40.0f, - /* qualityLevel */ FFX_CACAO_QUALITY_MEDIUM, + /* qualityLevel */ FFX_CACAO_QUALITY_LOWEST, /* adaptiveQualityLimit */ 0.75f, - /* blurPassCount */ 3, + /* blurPassCount */ 6, /* sharpness */ 0.98f, /* temporalSupersamplingAngleOffset */ 0.0f, /* temporalSupersamplingRadiusOffset */ 0.0f, /* detailShadowStrength */ 0.5f, /* generateNormals */ FFX_CACAO_FALSE, /* bilateralSigmaSquared */ 5.0f, - /* bilateralSimilarityDistanceSigma */ 0.2f, + /* bilateralSimilarityDistanceSigma */ 0.1f, } }, - // Downsampled - Low Quality + // Downsampled - Highest Quality { /* useDownsampledSsao */ true, { @@ -159,25 +168,21 @@ static const FfxCacaoPreset FFX_CACAO_PRESETS[] = { /* horizonAngleThreshold */ 0.06f, /* fadeOutFrom */ 20.0f, /* fadeOutTo */ 40.0f, - /* qualityLevel */ FFX_CACAO_QUALITY_LOWEST, + /* qualityLevel */ FFX_CACAO_QUALITY_HIGHEST, /* adaptiveQualityLimit */ 0.75f, - /* blurPassCount */ 6, + /* blurPassCount */ 2, /* sharpness */ 0.98f, /* temporalSupersamplingAngleOffset */ 0.0f, /* temporalSupersamplingRadiusOffset */ 0.0f, /* detailShadowStrength */ 0.5f, /* generateNormals */ FFX_CACAO_FALSE, - /* bilateralSigmaSquared */ 8.0f, - /* bilateralSimilarityDistanceSigma */ 0.8f, + /* bilateralSigmaSquared */ 5.0f, + /* bilateralSimilarityDistanceSigma */ 0.1f, } - } -}; -#else -static const char *FFX_CACAO_PRESET_NAMES[] = { "High Quality", "Medium Quality", "Low Quality", "Custom" }; - -static const FfxCacaoPreset FFX_CACAO_PRESETS[] = { - // High Quality + }, + // Downsampled - High Quality { + /* useDownsampledSsao */ true, { /* radius */ 1.2f, /* shadowMultiplier */ 1.0f, @@ -186,7 +191,7 @@ static const FfxCacaoPreset FFX_CACAO_PRESETS[] = { /* horizonAngleThreshold */ 0.06f, /* fadeOutFrom */ 20.0f, /* fadeOutTo */ 40.0f, - /* qualityLevel */ FFX_CACAO_QUALITY_HIGHEST, + /* qualityLevel */ FFX_CACAO_QUALITY_HIGH, /* adaptiveQualityLimit */ 0.75f, /* blurPassCount */ 2, /* sharpness */ 0.98f, @@ -198,8 +203,9 @@ static const FfxCacaoPreset FFX_CACAO_PRESETS[] = { /* bilateralSimilarityDistanceSigma */ 0.1f, } }, - // Medium Quality + // Downsampled - Medium Quality { + /* useDownsampledSsao */ true, { /* radius */ 1.2f, /* shadowMultiplier */ 1.0f, @@ -220,8 +226,32 @@ static const FfxCacaoPreset FFX_CACAO_PRESETS[] = { /* bilateralSimilarityDistanceSigma */ 0.2f, } }, - // Low Quality + // Downsampled - Low Quality { + /* useDownsampledSsao */ true, + { + /* radius */ 1.2f, + /* shadowMultiplier */ 1.0f, + /* shadowPower */ 1.50f, + /* shadowClamp */ 0.98f, + /* horizonAngleThreshold */ 0.06f, + /* fadeOutFrom */ 20.0f, + /* fadeOutTo */ 40.0f, + /* qualityLevel */ FFX_CACAO_QUALITY_LOW, + /* adaptiveQualityLimit */ 0.75f, + /* blurPassCount */ 6, + /* sharpness */ 0.98f, + /* temporalSupersamplingAngleOffset */ 0.0f, + /* temporalSupersamplingRadiusOffset */ 0.0f, + /* detailShadowStrength */ 0.5f, + /* generateNormals */ FFX_CACAO_FALSE, + /* bilateralSigmaSquared */ 8.0f, + /* bilateralSimilarityDistanceSigma */ 0.8f, + } + }, + // Downsampled - Lowest Quality + { + /* useDownsampledSsao */ true, { /* radius */ 1.2f, /* shadowMultiplier */ 1.0f, @@ -243,4 +273,3 @@ static const FfxCacaoPreset FFX_CACAO_PRESETS[] = { } } }; -#endif \ No newline at end of file diff --git a/sample/src/Common/FFX_CACAO_Sample.json b/sample/src/Common/SampleSettings.json similarity index 100% rename from sample/src/Common/FFX_CACAO_Sample.json rename to sample/src/Common/SampleSettings.json diff --git a/sample/src/DX12/CMakeLists.txt b/sample/src/DX12/CMakeLists.txt index 34a3656..fd9e8f6 100644 --- a/sample/src/DX12/CMakeLists.txt +++ b/sample/src/DX12/CMakeLists.txt @@ -5,14 +5,16 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/../../common.cmake) add_compile_options(/MP) set(sources - FFX_CACAO_Sample.cpp - FFX_CACAO_Sample.h + Sample.cpp + Sample.h SampleRenderer.cpp SampleRenderer.h ../../../ffx-cacao/src/ffx_cacao_defines.h ../../../ffx-cacao/src/ffx_cacao.cpp ../../../ffx-cacao/inc/ffx_cacao.h - ../Common/FFX_CACAO_Common.h + ../../../ffx-cacao/src/ffx_cacao_impl.cpp + ../../../ffx-cacao/inc/ffx_cacao_impl.h + ../Common/Common.h stdafx.cpp stdafx.h) @@ -22,7 +24,7 @@ set(shaders ${CMAKE_CURRENT_SOURCE_DIR}/Apply_CACAO.hlsl) set(config - ${CMAKE_CURRENT_SOURCE_DIR}/../Common/FFX_CACAO_Sample.json + ${CMAKE_CURRENT_SOURCE_DIR}/../Common/SampleSettings.json ) source_group("Sources" FILES ${sources}) diff --git a/sample/src/DX12/FFX_CACAO_Sample.cpp b/sample/src/DX12/Sample.cpp similarity index 68% rename from sample/src/DX12/FFX_CACAO_Sample.cpp rename to sample/src/DX12/Sample.cpp index 7940cd9..72d284b 100644 --- a/sample/src/DX12/FFX_CACAO_Sample.cpp +++ b/sample/src/DX12/Sample.cpp @@ -1,6 +1,6 @@ // AMD SampleDX12 sample code -// -// Copyright(c) 2018 Advanced Micro Devices, Inc.All rights reserved. +// +// 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 @@ -19,8 +19,10 @@ #include "stdafx.h" -#include "FFX_CACAO_Sample.h" -#include "FFX_CACAO_Common.h" +#include "Sample.h" +#include "Common.h" + +#include "ffx_cacao_impl.h" static inline void SetWindowClientSize(HWND hWnd, LONG width, LONG height) { @@ -39,13 +41,13 @@ static inline void SetWindowClientSize(HWND hWnd, LONG width, LONG height) const bool VALIDATION_ENABLED = false; -FfxCacaoSample::FfxCacaoSample(LPCSTR name) : FrameworkWindows(name) +Sample::Sample(LPCSTR name) : FrameworkWindows(name) { - m_lastFrameTime = MillisecondsNow(); - m_time = 0; - m_bPlay = true; + m_lastFrameTime = MillisecondsNow(); + m_time = 0; + m_bPlay = true; - m_pGltfLoader = NULL; + m_pGltfLoader = NULL; } //-------------------------------------------------------------------------------------- @@ -53,7 +55,7 @@ FfxCacaoSample::FfxCacaoSample(LPCSTR name) : FrameworkWindows(name) // OnParseCommandLine // //-------------------------------------------------------------------------------------- -void FfxCacaoSample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint32_t* pHeight, bool *pbFullScreen) +void Sample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint32_t* pHeight, bool *pbFullScreen) { // set some default values *pWidth = 1920; @@ -85,7 +87,7 @@ void FfxCacaoSample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint3 // read config file (and override values from commandline if so) // { - std::ifstream f("FFX_CACAO_Sample.json"); + std::ifstream f("SampleSettings.json"); if (!f) { MessageBox(NULL, "Config file not found!\n", "Cauldron Panic!", MB_ICONERROR); @@ -130,18 +132,12 @@ void FfxCacaoSample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint3 #ifdef FFX_CACAO_ENABLE_PROFILING if (m_isBenchmarking) { -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION bool downsampled = FFX_CACAO_PRESETS[m_presetIndex].useDownsampledSsao; -#endif uint32_t quality = FFX_CACAO_PRESETS[m_presetIndex].settings.qualityLevel; m_benchmarkScreenWidth = *pWidth; m_benchmarkScreenHeight = *pHeight; m_benchmarkWarmUpFramesToRun = 100; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION snprintf(m_benchmarkFilename, _countof(m_benchmarkFilename), "FFX_CACAO_DX12_Benchmark_%s_%ux%u_Q%u.csv", downsampled ? "downsampled" : "native", *pWidth, *pHeight, quality); -#else - snprintf(m_benchmarkFilename, _countof(m_benchmarkFilename), "FFX_CACAO_DX12_Benchmark_downsampled_%ux%u_Q%u.csv", *pWidth, *pHeight, quality); -#endif m_vsyncEnabled = false; m_isGpuValidationLayerEnabled = false; m_isCpuValidationLayerEnabled = false; @@ -154,7 +150,7 @@ void FfxCacaoSample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint3 // OnCreate // //-------------------------------------------------------------------------------------- -void FfxCacaoSample::OnCreate(HWND hWnd) +void Sample::OnCreate(HWND hWnd) { m_hWnd = hWnd; @@ -162,68 +158,69 @@ void FfxCacaoSample::OnCreate(HWND hWnd) m_cameraControlSelected = 1; m_state.cacaoSettings = FFX_CACAO_PRESETS[m_presetIndex].settings; + m_state.useDownsampledSSAO = FFX_CACAO_PRESETS[m_presetIndex].useDownsampledSsao; DWORD dwAttrib = GetFileAttributes("..\\media\\"); - if ((dwAttrib == INVALID_FILE_ATTRIBUTES) || ((dwAttrib & FILE_ATTRIBUTE_DIRECTORY)) == 0) - { - MessageBox(NULL, "Media files not found!\n\nPlease check the readme on how to get the media files.", "Cauldron Panic!", MB_ICONERROR); - exit(0); - } + if ((dwAttrib == INVALID_FILE_ATTRIBUTES) || ((dwAttrib & FILE_ATTRIBUTE_DIRECTORY)) == 0) + { + MessageBox(NULL, "Media files not found!\n\nPlease check the readme on how to get the media files.", "Cauldron Panic!", MB_ICONERROR); + exit(0); + } m_fullscreen = false; - // Create Device - // - m_device.OnCreate("myapp", "myEngine", m_isCpuValidationLayerEnabled, m_isGpuValidationLayerEnabled, hWnd); - m_device.CreatePipelineCache(); + // Create Device + // + m_device.OnCreate("FfxCacaoSample", "Cauldron", m_isCpuValidationLayerEnabled, m_isGpuValidationLayerEnabled, hWnd); + m_device.CreatePipelineCache(); - //init the shader compiler + //init the shader compiler InitDirectXCompiler(); - CreateShaderCache(); - - // Create Swapchain - // - - // Init FS2 and choose format - fsHdrInit(m_device.GetAGSContext(), m_device.GetAGSGPUInfo(), hWnd); - - uint32_t dwNumberOfBackBuffers = 2; - m_swapChain.OnCreate(&m_device, dwNumberOfBackBuffers, hWnd); - - // Create a instance of the renderer and initialize it, we need to do that for each GPU - // - m_Node = new SampleRenderer(); - m_Node->OnCreate(&m_device, &m_swapChain); - - // init GUI (non gfx stuff) - // - ImGUI_Init((void *)hWnd); - - // Init Camera, looking at the origin - // - m_roll = 0.0f; - m_pitch = 0.0f; - m_distance = 3.5f; - - // init GUI state - m_state.toneMapper = 0; - m_state.skyDomeType = 0; - m_state.exposure = 1.0f; - m_state.iblFactor = 10.0f; - m_state.emmisiveFactor = 1.0f; - m_state.bDrawLightFrustum = false; - m_state.bDrawBoundingBoxes = false; - m_state.camera.LookAt(m_roll, m_pitch, m_distance, XMVectorSet(0, 0, 0, 0)); - - m_state.spotlightCount = 1; - - m_state.spotlight[0].intensity = 5.0f; - m_state.spotlight[0].color = XMVectorSet(1.0f, 1.0f, 1.0f, 0.0f); - m_state.spotlight[0].light.SetFov(XM_PI / 2.0f, 1024, 1024, 0.1f, 100.0f); - m_state.spotlight[0].light.LookAt(XM_PI / 2.0f, 0.58f, 3.5f, XMVectorSet(0, 0, 0, 0)); - - m_state.bUseCACAO = true; - m_state.bDisplayCacaoDirectly = true; + CreateShaderCache(); + + // Create Swapchain + // + + // Init FS2 and choose format + fsHdrInit(m_device.GetAGSContext(), m_device.GetAGSGPUInfo(), hWnd); + + uint32_t dwNumberOfBackBuffers = 2; + m_swapChain.OnCreate(&m_device, dwNumberOfBackBuffers, hWnd); + + // Create a instance of the renderer and initialize it, we need to do that for each GPU + // + m_node = new SampleRenderer(); + m_node->OnCreate(&m_device, &m_swapChain); + + // init GUI (non gfx stuff) + // + ImGUI_Init((void *)hWnd); + + // Init Camera, looking at the origin + // + m_roll = 0.0f; + m_pitch = 0.0f; + m_distance = 3.5f; + + // init GUI state + m_state.toneMapper = 0; + m_state.skyDomeType = 0; + m_state.exposure = 1.0f; + m_state.iblFactor = 10.0f; + m_state.emmisiveFactor = 1.0f; + m_state.drawLightFrustum = false; + m_state.drawBoundingBoxes = false; + m_state.camera.LookAt(m_roll, m_pitch, m_distance, XMVectorSet(0, 0, 0, 0)); + + m_state.spotlightCount = 1; + + m_state.spotlight[0].intensity = 5.0f; + m_state.spotlight[0].color = XMVectorSet(1.0f, 1.0f, 1.0f, 0.0f); + m_state.spotlight[0].light.SetFov(XM_PI / 2.0f, 1024, 1024, 0.1f, 100.0f); + m_state.spotlight[0].light.LookAt(XM_PI / 2.0f, 0.58f, 3.5f, XMVectorSet(0, 0, 0, 0)); + + m_state.useCACAO = true; + m_state.displayCacaoDirectly = true; } //-------------------------------------------------------------------------------------- @@ -231,38 +228,38 @@ void FfxCacaoSample::OnCreate(HWND hWnd) // OnDestroy // //-------------------------------------------------------------------------------------- -void FfxCacaoSample::OnDestroy() +void Sample::OnDestroy() { #ifdef FFX_CACAO_ENABLE_PROFILING m_isBenchmarking = false; #endif - ImGUI_Shutdown(); + ImGUI_Shutdown(); - m_device.GPUFlush(); + m_device.GPUFlush(); - // Fullscreen state should always be false before exiting the app. - m_swapChain.SetFullScreen(false); + // Fullscreen state should always be false before exiting the app. + m_swapChain.SetFullScreen(false); - m_Node->UnloadScene(); - m_Node->OnDestroyWindowSizeDependentResources(); - m_Node->OnDestroy(); + m_node->UnloadScene(); + m_node->OnDestroyWindowSizeDependentResources(); + m_node->OnDestroy(); - delete m_Node; + delete m_node; - m_swapChain.OnDestroyWindowSizeDependentResources(); - m_swapChain.OnDestroy(); + m_swapChain.OnDestroyWindowSizeDependentResources(); + m_swapChain.OnDestroy(); - //shut down the shader compiler - DestroyShaderCache(&m_device); + //shut down the shader compiler + DestroyShaderCache(&m_device); - if (m_pGltfLoader) - { - delete m_pGltfLoader; - m_pGltfLoader = NULL; - } + if (m_pGltfLoader) + { + delete m_pGltfLoader; + m_pGltfLoader = NULL; + } - m_device.OnDestroy(); + m_device.OnDestroy(); } //-------------------------------------------------------------------------------------- @@ -270,10 +267,10 @@ void FfxCacaoSample::OnDestroy() // OnEvent // //-------------------------------------------------------------------------------------- -bool FfxCacaoSample::OnEvent(MSG msg) +bool Sample::OnEvent(MSG msg) { - if (ImGUI_WndProcHandler(msg.hwnd, msg.message, msg.wParam, msg.lParam)) - return true; + if (ImGUI_WndProcHandler(msg.hwnd, msg.message, msg.wParam, msg.lParam)) + return true; return true; } @@ -283,11 +280,11 @@ bool FfxCacaoSample::OnEvent(MSG msg) // SetFullScreen // //-------------------------------------------------------------------------------------- -void FfxCacaoSample::SetFullScreen(bool fullscreen) +void Sample::SetFullScreen(bool fullscreen) { - m_device.GPUFlush(); + m_device.GPUFlush(); - m_swapChain.SetFullScreen(fullscreen); + m_swapChain.SetFullScreen(fullscreen); } //-------------------------------------------------------------------------------------- @@ -295,7 +292,7 @@ void FfxCacaoSample::SetFullScreen(bool fullscreen) // OnResize // //-------------------------------------------------------------------------------------- -void FfxCacaoSample::OnResize(uint32_t width, uint32_t height, bool force) +void Sample::OnResize(uint32_t width, uint32_t height, bool force) { #ifdef FFX_CACAO_ENABLE_PROFILING if (m_isBenchmarking && !m_benchmarkWarmUpFramesToRun) @@ -308,38 +305,38 @@ void FfxCacaoSample::OnResize(uint32_t width, uint32_t height, bool force) } #endif - if (m_Width != width || m_Height != height || force) - { - // Flush GPU - // - m_device.GPUFlush(); - - // If resizing but no minimizing - // - if (m_Width > 0 && m_Height > 0) - { - if (m_Node!=NULL) - { - m_Node->OnDestroyWindowSizeDependentResources(); - } - m_swapChain.OnDestroyWindowSizeDependentResources(); - } - - m_Width = width; - m_Height = height; - - // if resizing but not minimizing the recreate it with the new size - // - if (m_Width > 0 && m_Height > 0) - { - m_swapChain.OnCreateWindowSizeDependentResources(m_Width, m_Height, m_vsyncEnabled, DISPLAYMODE_SDR); - if (m_Node != NULL) - { - m_Node->OnCreateWindowSizeDependentResources(&m_swapChain, m_Width, m_Height); - } - } - } - m_state.camera.SetFov(XM_PI / 4, m_Width, m_Height, 0.1f, 1000.0f); + if (m_Width != width || m_Height != height || force) + { + // Flush GPU + // + m_device.GPUFlush(); + + // If resizing but no minimizing + // + if (m_Width > 0 && m_Height > 0) + { + if (m_node!=NULL) + { + m_node->OnDestroyWindowSizeDependentResources(); + } + m_swapChain.OnDestroyWindowSizeDependentResources(); + } + + m_Width = width; + m_Height = height; + + // if resizing but not minimizing the recreate it with the new size + // + if (m_Width > 0 && m_Height > 0) + { + m_swapChain.OnCreateWindowSizeDependentResources(m_Width, m_Height, m_vsyncEnabled, DISPLAYMODE_SDR); + if (m_node != NULL) + { + m_node->OnCreateWindowSizeDependentResources(&m_swapChain, m_Width, m_Height); + } + } + } + m_state.camera.SetFov(XM_PI / 4, m_Width, m_Height, 0.1f, 1000.0f); } //-------------------------------------------------------------------------------------- @@ -347,7 +344,7 @@ void FfxCacaoSample::OnResize(uint32_t width, uint32_t height, bool force) // BuildUI, also loads the scene! // //-------------------------------------------------------------------------------------- -void FfxCacaoSample::BuildUI() +void Sample::BuildUI() { if (m_requiresLoad) { @@ -357,12 +354,12 @@ void FfxCacaoSample::BuildUI() // release everything and load the GLTF, just the light json data, the rest (textures and geometry) will be done in the main loop if (m_pGltfLoader != NULL) { - m_Node->UnloadScene(); - m_Node->OnDestroyWindowSizeDependentResources(); - m_Node->OnDestroy(); + m_node->UnloadScene(); + m_node->OnDestroyWindowSizeDependentResources(); + m_node->OnDestroy(); m_pGltfLoader->Unload(); - m_Node->OnCreate(&m_device, &m_swapChain); - m_Node->OnCreateWindowSizeDependentResources(&m_swapChain, m_Width, m_Height); + m_node->OnCreate(&m_device, &m_swapChain); + m_node->OnCreateWindowSizeDependentResources(&m_swapChain, m_Width, m_Height); } delete(m_pGltfLoader); @@ -458,12 +455,10 @@ void FfxCacaoSample::BuildUI() if (ImGui::Combo("Preset", &m_presetIndex, FFX_CACAO_PRESET_NAMES, _countof(FFX_CACAO_PRESET_NAMES)) && m_presetIndex < _countof(FFX_CACAO_PRESETS)) { m_state.cacaoSettings = FFX_CACAO_PRESETS[m_presetIndex].settings; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - m_state.bUseDownsampledSSAO = FFX_CACAO_PRESETS[m_presetIndex].useDownsampledSsao; -#endif + m_state.useDownsampledSSAO = FFX_CACAO_PRESETS[m_presetIndex].useDownsampledSsao; } - FfxCacaoSettings *settings = &m_state.cacaoSettings; + FFX_CACAO_Settings *settings = &m_state.cacaoSettings; ImGui::SliderFloat("Radius", &settings->radius, 0.0f, 10.0f); ImGui::SliderFloat("Shadow Multiplier", &settings->shadowMultiplier, 0.0f, 5.0f); ImGui::SliderFloat("Shadow Power", &settings->shadowPower, 0.5f, 5.0f); @@ -474,7 +469,7 @@ void FfxCacaoSample::BuildUI() const char *qualityLevels[] = { "Lowest", "Low", "Medium", "High", "Highest" }; int idx = (int)settings->qualityLevel; ImGui::Combo("Quality Level", &idx, qualityLevels, _countof(qualityLevels)); - settings->qualityLevel = (FfxCacaoQuality)idx; + settings->qualityLevel = (FFX_CACAO_Quality)idx; if (settings->qualityLevel == FFX_CACAO_QUALITY_HIGHEST) { ImGui::SliderFloat("Adaptive Quality Limit", &settings->adaptiveQualityLimit, 0.5f, 1.0f); @@ -486,49 +481,43 @@ void FfxCacaoSample::BuildUI() bool generateNormals = settings->generateNormals ? true : false; ImGui::Checkbox("Generate Normal Buffer From Depth Buffer", &generateNormals); settings->generateNormals = generateNormals ? FFX_CACAO_TRUE : FFX_CACAO_FALSE; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - ImGui::Checkbox("Use Downsampled SSAO", &m_state.bUseDownsampledSSAO); - if (m_state.bUseDownsampledSSAO) -#endif + ImGui::Checkbox("Use Downsampled SSAO", &m_state.useDownsampledSSAO); + if (m_state.useDownsampledSSAO) { ImGui::SliderFloat("Bilateral Sigma Squared", &settings->bilateralSigmaSquared, 0.0f, 10.0f); ImGui::SliderFloat("Bilateral Similarity Distance Sigma", &settings->bilateralSimilarityDistanceSigma, 0.1f, 1.0f); } - ImGui::Checkbox("Display FFX CACAO Output Directly", &m_state.bDisplayCacaoDirectly); - if (!m_state.bDisplayCacaoDirectly) + ImGui::Checkbox("Display FFX CACAO Output Directly", &m_state.displayCacaoDirectly); + if (!m_state.displayCacaoDirectly) { - ImGui::Checkbox("Use FFX CACAO", &m_state.bUseCACAO); + ImGui::Checkbox("Use FFX CACAO", &m_state.useCACAO); } - m_state.bUseCACAO |= m_state.bDisplayCacaoDirectly; + m_state.useCACAO |= m_state.displayCacaoDirectly; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - if (m_presetIndex < _countof(FFX_CACAO_PRESETS) && (memcmp(&m_state.cacaoSettings, &FFX_CACAO_PRESETS[m_presetIndex].settings, sizeof(m_state.cacaoSettings)) || m_state.bUseDownsampledSSAO != FFX_CACAO_PRESETS[m_presetIndex].useDownsampledSsao)) -#else - if (m_presetIndex < _countof(FFX_CACAO_PRESETS) && memcmp(&m_state.cacaoSettings, &FFX_CACAO_PRESETS[m_presetIndex].settings, sizeof(m_state.cacaoSettings))) -#endif + if (m_presetIndex < _countof(FFX_CACAO_PRESETS) && (memcmp(&m_state.cacaoSettings, &FFX_CACAO_PRESETS[m_presetIndex].settings, sizeof(m_state.cacaoSettings)) || m_state.useDownsampledSSAO != FFX_CACAO_PRESETS[m_presetIndex].useDownsampledSsao)) { m_presetIndex = _countof(FFX_CACAO_PRESETS); } } #ifdef FFX_CACAO_ENABLE_PROFILING - if (m_vsyncEnabled || !m_state.bUseCACAO) + if (m_vsyncEnabled || !m_state.useCACAO || m_isCpuValidationLayerEnabled || m_isGpuValidationLayerEnabled) { - ImGui::CollapsingHeader("Profiler Disabled (enable CACAO and turn off vsync)"); + ImGui::CollapsingHeader("Profiler Disabled (enable CACAO and turn off vsync and validation)"); } else { bool displayProfiling = ImGui::CollapsingHeader("Profiler", ImGuiTreeNodeFlags_DefaultOpen); - FfxCacaoDetailedTiming timings; + FFX_CACAO_DetailedTiming timings; uint64_t gpuTicksPerMicrosecond; - m_Node->GetCacaoTimings(&m_state, &timings, &gpuTicksPerMicrosecond); + m_node->GetCacaoTimings(&m_state, &timings, &gpuTicksPerMicrosecond); gpuTicksPerMicrosecond /= 1000000; for (uint32_t i = 0; i < timings.numTimestamps; ++i) { - FfxCacaoTimestamp *t = &timings.timestamps[i]; + FFX_CACAO_Timestamp *t = &timings.timestamps[i]; if (displayProfiling) { ImGui::Text("%-32s: %7.1f us", t->label, ((double)t->ticks) / ((double)gpuTicksPerMicrosecond)); @@ -573,7 +562,7 @@ void FfxCacaoSample::BuildUI() else if (m_cameraControlSelected > 1) { // Use a camera from the GLTF - // + // m_pGltfLoader->GetCamera(m_cameraControlSelected - 2, &m_state.camera); m_roll = m_state.camera.GetYaw(); m_pitch = m_state.camera.GetPitch(); @@ -586,7 +575,7 @@ void FfxCacaoSample::BuildUI() // OnRender, updates the state from the UI, animates, transforms and renders the scene // //-------------------------------------------------------------------------------------- -void FfxCacaoSample::OnRender() +void Sample::OnRender() { // Get timings // @@ -608,14 +597,14 @@ void FfxCacaoSample::OnRender() if (m_loadingStage) { - // LoadScene needs to be called a number of times, the scene is not fully loaded until it returns 0 - // This is done so we can display a progress bar when the scene is loading - m_loadingStage = m_Node->LoadScene(m_pGltfLoader, m_loadingStage); + // LoadScene needs to be called a number of times, the scene is not fully loaded until it returns 0 + // This is done so we can display a progress bar when the scene is loading + m_loadingStage = m_node->LoadScene(m_pGltfLoader, m_loadingStage); if (m_loadingStage == 0) { m_time = 0.0f; } - } + } #if FFX_CACAO_ENABLE_PROFILING else if (m_pGltfLoader && m_isBenchmarking) { @@ -634,8 +623,8 @@ void FfxCacaoSample::OnRender() } uint64_t gpuTicksPerSecond; - FfxCacaoDetailedTiming timings = {}; - m_Node->GetCacaoTimings(&m_state, &timings, &gpuTicksPerSecond); + FFX_CACAO_DetailedTiming timings = {}; + m_node->GetCacaoTimings(&m_state, &timings, &gpuTicksPerSecond); double microsecondsPerGpuTick = 1000000.0 / (double)gpuTicksPerSecond; if (timings.numTimestamps) @@ -667,21 +656,21 @@ void FfxCacaoSample::OnRender() } - // Animate and transform the scene - // - if (m_pGltfLoader) - { - m_pGltfLoader->SetAnimationTime(0, m_time); - m_pGltfLoader->TransformScene(0, XMMatrixIdentity()); - } + // Animate and transform the scene + // + if (m_pGltfLoader) + { + m_pGltfLoader->SetAnimationTime(0, m_time); + m_pGltfLoader->TransformScene(0, XMMatrixIdentity()); + } - m_state.time = m_time; + m_state.time = m_time; - // Do Render frame using AFR - // - m_Node->OnRender(&m_state, &m_swapChain); + // Do Render frame using AFR + // + m_node->OnRender(&m_state, &m_swapChain); - m_swapChain.Present(); + m_swapChain.Present(); } //-------------------------------------------------------------------------------------- @@ -690,14 +679,14 @@ void FfxCacaoSample::OnRender() // //-------------------------------------------------------------------------------------- int WINAPI WinMain(HINSTANCE hInstance, - HINSTANCE hPrevInstance, - LPSTR lpCmdLine, - int nCmdShow) + HINSTANCE hPrevInstance, + LPSTR lpCmdLine, + int nCmdShow) { - LPCSTR Name = "FFX CACAO DirectX 12 Sample v1.0"; - uint32_t Width = 1280; - uint32_t Height = 720; + LPCSTR Name = "FFX CACAO DirectX 12 Sample v1.2"; + uint32_t Width = 1280; + uint32_t Height = 720; - // create new DX sample - return RunFramework(hInstance, lpCmdLine, nCmdShow, new FfxCacaoSample(Name)); + // create new DX sample + return RunFramework(hInstance, lpCmdLine, nCmdShow, new Sample(Name)); } diff --git a/sample/src/DX12/FFX_CACAO_Sample.h b/sample/src/DX12/Sample.h similarity index 53% rename from sample/src/DX12/FFX_CACAO_Sample.h rename to sample/src/DX12/Sample.h index c313d02..302c377 100644 --- a/sample/src/DX12/FFX_CACAO_Sample.h +++ b/sample/src/DX12/Sample.h @@ -1,6 +1,6 @@ // AMD SampleDX12 sample code // -// Copyright(c) 2017 Advanced Micro Devices, Inc.All rights reserved. +// 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 @@ -20,51 +20,49 @@ #include "SampleRenderer.h" -#include "ffx_cacao.h" - -class FfxCacaoSample : public FrameworkWindows +class Sample : public FrameworkWindows { public: - FfxCacaoSample(LPCSTR name); - void OnCreate(HWND hWnd); - void OnDestroy(); + Sample(LPCSTR name); + void OnCreate(HWND hWnd); + void OnDestroy(); void BuildUI(); void OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint32_t* pHeight, bool *pbFullScreen); void OnRender(); - bool OnEvent(MSG msg); + bool OnEvent(MSG msg); void OnResize(uint32_t width, uint32_t height) { OnResize(width, height, false); } - void OnResize(uint32_t Width, uint32_t Height, bool force); - void SetFullScreen(bool fullscreen); - + void OnResize(uint32_t Width, uint32_t Height, bool force); + void SetFullScreen(bool fullscreen); + private: - HWND m_hWnd; + HWND m_hWnd; - Device m_device; - SwapChain m_swapChain; + Device m_device; + SwapChain m_swapChain; - GLTFCommon *m_pGltfLoader = NULL; + GLTFCommon *m_pGltfLoader = NULL; - SampleRenderer *m_Node = NULL; - SampleRenderer::State m_state; + SampleRenderer *m_node = NULL; + SampleRenderer::State m_state; - int m_loadingStage = 0; - bool m_requiresLoad = true; - int m_preset; + int m_loadingStage = 0; + bool m_requiresLoad = true; + int m_preset; - float m_distance; - float m_roll; - float m_pitch; + float m_distance; + float m_roll; + float m_pitch; - float m_time; // WallClock in seconds. - double m_deltaTime; // The elapsed time in milliseconds since the previous frame. - double m_lastFrameTime; + float m_time; // WallClock in seconds. + double m_deltaTime; // The elapsed time in milliseconds since the previous frame. + double m_lastFrameTime; - bool m_isCapturing = false; - bool m_vsyncEnabled = false; - int m_cameraControlSelected = 0; - bool m_bPlay; - bool m_displayGUI; - bool m_fullscreen; + bool m_isCapturing = false; + bool m_vsyncEnabled = false; + int m_cameraControlSelected = 0; + bool m_bPlay; + bool m_displayGUI; + bool m_fullscreen; // json config file json m_jsonConfigFile; @@ -77,10 +75,10 @@ class FfxCacaoSample : public FrameworkWindows int m_presetIndex = 0; #ifdef FFX_CACAO_ENABLE_PROFILING - char m_benchmarkFilename[1024]; - bool m_isBenchmarking; - uint32_t m_benchmarkScreenWidth; - uint32_t m_benchmarkScreenHeight; - uint32_t m_benchmarkWarmUpFramesToRun; + char m_benchmarkFilename[1024]; + bool m_isBenchmarking; + uint32_t m_benchmarkScreenWidth; + uint32_t m_benchmarkScreenHeight; + uint32_t m_benchmarkWarmUpFramesToRun; #endif }; diff --git a/sample/src/DX12/SampleRenderer.cpp b/sample/src/DX12/SampleRenderer.cpp index 8154356..5055f2f 100644 --- a/sample/src/DX12/SampleRenderer.cpp +++ b/sample/src/DX12/SampleRenderer.cpp @@ -1,6 +1,6 @@ // AMD SampleDX12 sample code // -// Copyright(c) 2018 Advanced Micro Devices, Inc.All rights reserved. +// 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 @@ -28,70 +28,68 @@ //-------------------------------------------------------------------------------------- void SampleRenderer::OnCreate(Device* pDevice, SwapChain *pSwapChain) { - m_pDevice = pDevice; - - // Initialize helpers - - // Create all the heaps for the resources views - const uint32_t cbvDescriptorCount = 3000; - const uint32_t srvDescriptorCount = 3000; - const uint32_t uavDescriptorCount = 100; - const uint32_t dsvDescriptorCount = 100; - const uint32_t rtvDescriptorCount = 1000; - const uint32_t samplerDescriptorCount = 50; - m_resourceViewHeaps.OnCreate(pDevice, cbvDescriptorCount, srvDescriptorCount, uavDescriptorCount, dsvDescriptorCount, rtvDescriptorCount, samplerDescriptorCount); - - // Create a commandlist ring for the Direct queue - // We are queuing (backBufferCount + 0.5) frames, so we need to triple buffer the command lists - uint32_t commandListsPerBackBuffer = 8; - m_CommandListRing.OnCreate(pDevice, backBufferCount + 1, commandListsPerBackBuffer, pDevice->GetGraphicsQueue()->GetDesc()); - - // Create a 'dynamic' constant buffer - const uint32_t constantBuffersMemSize = 20 * 1024 * 1024; - m_ConstantBufferRing.OnCreate(pDevice, backBufferCount, constantBuffersMemSize, &m_resourceViewHeaps); - - // Create a 'static' pool for vertices, indices and constant buffers - const uint32_t staticGeometryMemSize = 128 * 1024 * 1024; - m_VidMemBufferPool.OnCreate(pDevice, staticGeometryMemSize, USE_VID_MEM, "StaticGeom"); - - // initialize the GPU time stamps module - m_GPUTimer.OnCreate(pDevice, backBufferCount); - - // Quick helper to upload resources, it has it's own commandList and uses suballocation. - // for 4K textures we'll need 100Megs - const uint32_t uploadHeapMemSize = 1000 * 1024 * 1024; - m_UploadHeap.OnCreate(pDevice, uploadHeapMemSize); // initialize an upload heap (uses suballocation for faster results) - - // Create the depth buffer views - m_resourceViewHeaps.AllocDSVDescriptor(1, &m_depthBufferDSV); - m_resourceViewHeaps.AllocCBV_SRV_UAVDescriptor(1, &m_depthBufferSRV); - - // Create a Shadowmap atlas to hold 4 cascades/spotlights - m_ShadowMap.InitDepthStencil(pDevice, "m_pShadowMap", &CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_R32_TYPELESS, 2 * 1024, 2 * 1024, 1, 1, 1, 0, D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL)); - m_resourceViewHeaps.AllocDSVDescriptor(1, &m_ShadowMapDSV); - m_resourceViewHeaps.AllocCBV_SRV_UAVDescriptor(1, &m_ShadowMapSRV); - m_ShadowMap.CreateDSV(0, &m_ShadowMapDSV); - m_ShadowMap.CreateSRV(0, &m_ShadowMapSRV); - - m_skyDome.OnCreate(pDevice, &m_UploadHeap, &m_resourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool, "..\\media\\envmaps\\papermill\\diffuse.dds", "..\\media\\envmaps\\papermill\\specular.dds", DXGI_FORMAT_R16G16B16A16_FLOAT, 4); - m_skyDomeProc.OnCreate(pDevice, &m_resourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool, DXGI_FORMAT_R16G16B16A16_FLOAT, 4); - m_wireframe.OnCreate(pDevice, &m_resourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool, DXGI_FORMAT_R16G16B16A16_FLOAT, 4); - m_wireframeBox.OnCreate(pDevice, &m_resourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool); - m_downSample.OnCreate(pDevice, &m_resourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool, DXGI_FORMAT_R16G16B16A16_FLOAT); - m_bloom.OnCreate(pDevice, &m_resourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool, DXGI_FORMAT_R16G16B16A16_FLOAT); - m_motionBlur.OnCreate(pDevice, &m_resourceViewHeaps, "motionBlur.hlsl", "main", 1, 2, 8, 8, 1); - - size_t cacaoSize = ffxCacaoD3D12GetContextSize(); - FfxCacaoStatus status; - -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - m_pFfxCacaoContextNative = (FfxCacaoD3D12Context*)malloc(cacaoSize); - status = ffxCacaoD3D12InitContext(m_pFfxCacaoContextNative, pDevice->GetDevice()); + m_pDevice = pDevice; + + // Initialize helpers + + // Create all the heaps for the resources views + const uint32_t cbvDescriptorCount = 3000; + const uint32_t srvDescriptorCount = 3000; + const uint32_t uavDescriptorCount = 100; + const uint32_t dsvDescriptorCount = 100; + const uint32_t rtvDescriptorCount = 1000; + const uint32_t samplerDescriptorCount = 50; + m_resourceViewHeaps.OnCreate(pDevice, cbvDescriptorCount, srvDescriptorCount, uavDescriptorCount, dsvDescriptorCount, rtvDescriptorCount, samplerDescriptorCount); + + // Create a commandlist ring for the Direct queue + // We are queuing (backBufferCount + 0.5) frames, so we need to triple buffer the command lists + uint32_t commandListsPerBackBuffer = 8; + m_commandListRing.OnCreate(pDevice, backBufferCount + 1, commandListsPerBackBuffer, pDevice->GetGraphicsQueue()->GetDesc()); + + // Create a 'dynamic' constant buffer + const uint32_t constantBuffersMemSize = 20 * 1024 * 1024; + m_constantBufferRing.OnCreate(pDevice, backBufferCount, constantBuffersMemSize, &m_resourceViewHeaps); + + // Create a 'static' pool for vertices, indices and constant buffers + const uint32_t staticGeometryMemSize = 128 * 1024 * 1024; + m_vidMemBufferPool.OnCreate(pDevice, staticGeometryMemSize, USE_VID_MEM, "StaticGeom"); + + // initialize the GPU time stamps module + m_gpuTimer.OnCreate(pDevice, backBufferCount); + + // Quick helper to upload resources, it has it's own commandList and uses suballocation. + // for 4K textures we'll need 100Megs + const uint32_t uploadHeapMemSize = 1000 * 1024 * 1024; + m_uploadHeap.OnCreate(pDevice, uploadHeapMemSize); // initialize an upload heap (uses suballocation for faster results) + + // Create the depth buffer views + m_resourceViewHeaps.AllocDSVDescriptor(1, &m_depthBufferDSV); + m_resourceViewHeaps.AllocCBV_SRV_UAVDescriptor(1, &m_depthBufferSRV); + + // Create a Shadowmap atlas to hold 4 cascades/spotlights + m_shadowMap.InitDepthStencil(pDevice, "m_pShadowMap", &CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_R32_TYPELESS, 2 * 1024, 2 * 1024, 1, 1, 1, 0, D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL)); + m_resourceViewHeaps.AllocDSVDescriptor(1, &m_shadowMapDSV); + m_resourceViewHeaps.AllocCBV_SRV_UAVDescriptor(1, &m_shadowMapSRV); + m_shadowMap.CreateDSV(0, &m_shadowMapDSV); + m_shadowMap.CreateSRV(0, &m_shadowMapSRV); + + m_skyDome.OnCreate(pDevice, &m_uploadHeap, &m_resourceViewHeaps, &m_constantBufferRing, &m_vidMemBufferPool, "..\\media\\envmaps\\papermill\\diffuse.dds", "..\\media\\envmaps\\papermill\\specular.dds", DXGI_FORMAT_R16G16B16A16_FLOAT, 4); + m_skyDomeProc.OnCreate(pDevice, &m_resourceViewHeaps, &m_constantBufferRing, &m_vidMemBufferPool, DXGI_FORMAT_R16G16B16A16_FLOAT, 4); + m_wireframe.OnCreate(pDevice, &m_resourceViewHeaps, &m_constantBufferRing, &m_vidMemBufferPool, DXGI_FORMAT_R16G16B16A16_FLOAT, 4); + m_wireframeBox.OnCreate(pDevice, &m_resourceViewHeaps, &m_constantBufferRing, &m_vidMemBufferPool); + m_downSample.OnCreate(pDevice, &m_resourceViewHeaps, &m_constantBufferRing, &m_vidMemBufferPool, DXGI_FORMAT_R16G16B16A16_FLOAT); + m_bloom.OnCreate(pDevice, &m_resourceViewHeaps, &m_constantBufferRing, &m_vidMemBufferPool, DXGI_FORMAT_R16G16B16A16_FLOAT); + m_motionBlur.OnCreate(pDevice, &m_resourceViewHeaps, "motionBlur.hlsl", "main", 1, 2, 8, 8, 1); + + size_t cacaoSize = FFX_CACAO_D3D12GetContextSize(); + FFX_CACAO_Status status; + + m_pCACAOContextNative = (FFX_CACAO_D3D12Context*)malloc(cacaoSize); + status = FFX_CACAO_D3D12InitContext(m_pCACAOContextNative, pDevice->GetDevice()); assert(status == FFX_CACAO_STATUS_OK); -#endif - m_pFfxCacaoContextDownsampled = (FfxCacaoD3D12Context*)malloc(cacaoSize); - status = ffxCacaoD3D12InitContext(m_pFfxCacaoContextDownsampled, pDevice->GetDevice()); + m_pCACAOContextDownsampled = (FFX_CACAO_D3D12Context*)malloc(cacaoSize); + status = FFX_CACAO_D3D12InitContext(m_pCACAOContextDownsampled, pDevice->GetDevice()); assert(status == FFX_CACAO_STATUS_OK); D3D12_STATIC_SAMPLER_DESC SamplerDesc = {}; @@ -109,23 +107,23 @@ void SampleRenderer::OnCreate(Device* pDevice, SwapChain *pSwapChain) SamplerDesc.RegisterSpace = 0; SamplerDesc.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; - m_applyDirect.OnCreate(pDevice, "Apply_CACAO.hlsl", &m_resourceViewHeaps, &m_VidMemBufferPool, 1, 1, &SamplerDesc, pSwapChain->GetFormat()); // DXGI_FORMAT_R16G16B16A16_FLOAT); + m_cacaoApplyDirect.OnCreate(pDevice, "Apply_CACAO.hlsl", &m_resourceViewHeaps, &m_vidMemBufferPool, 1, 1, &SamplerDesc, pSwapChain->GetFormat()); // DXGI_FORMAT_R16G16B16A16_FLOAT); // Create tonemapping pass - m_cacaoUavClear.OnCreate(pDevice, &m_resourceViewHeaps, "Apply_CACAO.hlsl", "CSClear", 1, 0, 8, 8, 1); - m_toneMapping.OnCreate(pDevice, &m_resourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool, pSwapChain->GetFormat()); + m_cacaoUAVClear.OnCreate(pDevice, &m_resourceViewHeaps, "Apply_CACAO.hlsl", "CSClear", 1, 0, 8, 8, 1); + m_toneMapping.OnCreate(pDevice, &m_resourceViewHeaps, &m_constantBufferRing, &m_vidMemBufferPool, pSwapChain->GetFormat()); - // Initialize UI rendering resources - m_ImGUI.OnCreate(pDevice, &m_UploadHeap, &m_resourceViewHeaps, &m_ConstantBufferRing, pSwapChain->GetFormat()); + // Initialize UI rendering resources + m_imGUI.OnCreate(pDevice, &m_uploadHeap, &m_resourceViewHeaps, &m_constantBufferRing, pSwapChain->GetFormat()); - m_resourceViewHeaps.AllocRTVDescriptor(1, &m_HDRRTV); - m_resourceViewHeaps.AllocRTVDescriptor(1, &m_HDRRTVMSAA); + m_resourceViewHeaps.AllocRTVDescriptor(1, &m_hdrRTV); + m_resourceViewHeaps.AllocRTVDescriptor(1, &m_hdrRTVMSAA); - m_resourceViewHeaps.AllocCBV_SRV_UAVDescriptor(1, &m_HDRSRV); + m_resourceViewHeaps.AllocCBV_SRV_UAVDescriptor(1, &m_hdrSRV); // CACAO stuff - m_resourceViewHeaps.AllocCBV_SRV_UAVDescriptor(1, &m_applyDirectInput); - m_resourceViewHeaps.AllocCBV_SRV_UAVDescriptor(1, &m_FfxCacaoOutputSRV); - m_resourceViewHeaps.AllocCBV_SRV_UAVDescriptor(1, &m_FfxCacaoOutputUAV); + m_resourceViewHeaps.AllocCBV_SRV_UAVDescriptor(1, &m_cacaoApplyDirectInput); + m_resourceViewHeaps.AllocCBV_SRV_UAVDescriptor(1, &m_cacaoOutputSRV); + m_resourceViewHeaps.AllocCBV_SRV_UAVDescriptor(1, &m_cacaoOutputUAV); // Deferred non msaa pass m_resourceViewHeaps.AllocDSVDescriptor(1, &m_depthBufferNonMsaaDSV); @@ -133,10 +131,10 @@ void SampleRenderer::OnCreate(Device* pDevice, SwapChain *pSwapChain) m_resourceViewHeaps.AllocRTVDescriptor(1, &m_normalBufferNonMsaaRTV); m_resourceViewHeaps.AllocCBV_SRV_UAVDescriptor(1, &m_normalBufferNonMsaaSRV); - // Make sure upload heap has finished uploading before continuing + // Make sure upload heap has finished uploading before continuing #if (USE_VID_MEM==true) - m_VidMemBufferPool.UploadData(m_UploadHeap.GetCommandList()); - m_UploadHeap.FlushAndFinish(); + m_vidMemBufferPool.UploadData(m_uploadHeap.GetCommandList()); + m_uploadHeap.FlushAndFinish(); #endif } @@ -147,38 +145,36 @@ void SampleRenderer::OnCreate(Device* pDevice, SwapChain *pSwapChain) //-------------------------------------------------------------------------------------- void SampleRenderer::OnDestroy() { - m_ImGUI.OnDestroy(); - m_toneMapping.OnDestroy(); - m_taa.OnDestroy(); - m_motionBlur.OnDestroy(); - m_sharpen.OnDestroy(); - m_bloom.OnDestroy(); - -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - ffxCacaoD3D12DestroyContext(m_pFfxCacaoContextNative); - free(m_pFfxCacaoContextNative); -#endif - ffxCacaoD3D12DestroyContext(m_pFfxCacaoContextDownsampled); - free(m_pFfxCacaoContextDownsampled); - m_cacaoUavClear.OnDestroy(); - m_applyDirect.OnDestroy(); + m_imGUI.OnDestroy(); + m_toneMapping.OnDestroy(); + m_taa.OnDestroy(); + m_motionBlur.OnDestroy(); + m_sharpen.OnDestroy(); + m_bloom.OnDestroy(); + + FFX_CACAO_D3D12DestroyContext(m_pCACAOContextNative); + free(m_pCACAOContextNative); + FFX_CACAO_D3D12DestroyContext(m_pCACAOContextDownsampled); + free(m_pCACAOContextDownsampled); + m_cacaoUAVClear.OnDestroy(); + m_cacaoApplyDirect.OnDestroy(); m_downSample.OnDestroy(); - m_wireframeBox.OnDestroy(); - m_wireframe.OnDestroy(); - m_skyDomeProc.OnDestroy(); - m_skyDome.OnDestroy(); - m_ShadowMap.OnDestroy(); + m_wireframeBox.OnDestroy(); + m_wireframe.OnDestroy(); + m_skyDomeProc.OnDestroy(); + m_skyDome.OnDestroy(); + m_shadowMap.OnDestroy(); #if USE_SHADOWMASK - m_shadowResolve.OnDestroy(); + m_shadowResolve.OnDestroy(); #endif - m_UploadHeap.OnDestroy(); - m_GPUTimer.OnDestroy(); - m_VidMemBufferPool.OnDestroy(); - m_ConstantBufferRing.OnDestroy(); - m_CommandListRing.OnDestroy(); - m_resourceViewHeaps.OnDestroy(); + m_uploadHeap.OnDestroy(); + m_gpuTimer.OnDestroy(); + m_vidMemBufferPool.OnDestroy(); + m_constantBufferRing.OnDestroy(); + m_commandListRing.OnDestroy(); + m_resourceViewHeaps.OnDestroy(); } //-------------------------------------------------------------------------------------- @@ -188,22 +184,22 @@ void SampleRenderer::OnDestroy() //-------------------------------------------------------------------------------------- void SampleRenderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, uint32_t Width, uint32_t Height) { - m_Width = Width; - m_Height = Height; + m_width = Width; + m_height = Height; - // Set the viewport - // - m_viewPort = { 0.0f, 0.0f, static_cast<float>(Width), static_cast<float>(Height), 0.0f, 1.0f }; + // Set the viewport + // + m_viewPort = { 0.0f, 0.0f, static_cast<float>(Width), static_cast<float>(Height), 0.0f, 1.0f }; - // Create scissor rectangle - // - m_RectScissor = { 0, 0, (LONG)Width, (LONG)Height }; + // Create scissor rectangle + // + m_rectScissor = { 0, 0, (LONG)Width, (LONG)Height }; - // Create depth buffer - // + // Create depth buffer + // m_depthBuffer.InitDepthStencil(m_pDevice, "depthbuffer", &CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_R32_TYPELESS, Width, Height, 1, 1, 4, 0, D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL)); m_depthBuffer.CreateDSV(0, &m_depthBufferDSV); - m_depthBuffer.CreateSRV(0, &m_depthBufferSRV); + m_depthBuffer.CreateSRV(0, &m_depthBufferSRV); m_depthBufferNonMsaa.InitDepthStencil(m_pDevice, "depthBufferNonMSAA", &CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_R32_TYPELESS, Width, Height, 1, 1, 1, 0, D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL)); m_depthBufferNonMsaa.CreateDSV(0, &m_depthBufferNonMsaaDSV); @@ -213,29 +209,29 @@ void SampleRenderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, m_normalBufferNonMsaa.CreateRTV(0, &m_normalBufferNonMsaaRTV); m_normalBufferNonMsaa.CreateSRV(0, &m_normalBufferNonMsaaSRV); - // Create Texture + RTV with x4 MSAA - // - CD3DX12_RESOURCE_DESC RDescMSAA = CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_R16G16B16A16_FLOAT, Width, Height, 1, 1, 4, 0, D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET); - m_HDRMSAA.InitRenderTarget(m_pDevice, "HDRMSAA", &RDescMSAA, D3D12_RESOURCE_STATE_RENDER_TARGET); - m_HDRMSAA.CreateRTV(0, &m_HDRRTVMSAA); + // Create Texture + RTV with x4 MSAA + // + CD3DX12_RESOURCE_DESC RDescMSAA = CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_R16G16B16A16_FLOAT, Width, Height, 1, 1, 4, 0, D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET); + m_hdrMSAA.InitRenderTarget(m_pDevice, "HDRMSAA", &RDescMSAA, D3D12_RESOURCE_STATE_RENDER_TARGET); + m_hdrMSAA.CreateRTV(0, &m_hdrRTVMSAA); - // Create Texture + RTV, to hold the resolved scene - // - CD3DX12_RESOURCE_DESC RDesc = CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_R16G16B16A16_FLOAT, Width, Height, 1, 1, 1, 0, D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET | D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS); - m_HDR.InitRenderTarget(m_pDevice, "HDR", &RDesc, D3D12_RESOURCE_STATE_RENDER_TARGET); - m_HDR.CreateSRV(0, &m_HDRSRV); - m_HDR.CreateRTV(0, &m_HDRRTV); + // Create Texture + RTV, to hold the resolved scene + // + CD3DX12_RESOURCE_DESC RDesc = CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_R16G16B16A16_FLOAT, Width, Height, 1, 1, 1, 0, D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET | D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS); + m_hdr.InitRenderTarget(m_pDevice, "HDR", &RDesc, D3D12_RESOURCE_STATE_RENDER_TARGET); + m_hdr.CreateSRV(0, &m_hdrSRV); + m_hdr.CreateRTV(0, &m_hdrRTV); - m_FfxCacaoOutput.Init(m_pDevice, "cacaoOutput", &CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_R8_UNORM, Width, Height, 1, 1, 1, 0, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS), D3D12_RESOURCE_STATE_GENERIC_READ, NULL); - m_FfxCacaoOutput.CreateSRV(0, &m_FfxCacaoOutputSRV); - m_FfxCacaoOutput.CreateUAV(0, &m_FfxCacaoOutputUAV); + m_cacaoOutput.Init(m_pDevice, "cacaoOutput", &CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_R8_UNORM, Width, Height, 1, 1, 1, 0, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS), D3D12_RESOURCE_STATE_GENERIC_READ, NULL); + m_cacaoOutput.CreateSRV(0, &m_cacaoOutputSRV); + m_cacaoOutput.CreateUAV(0, &m_cacaoOutputUAV); if (m_gltfPBR) { - m_gltfPBR->OnUpdateWindowSizeDependentResources(&m_FfxCacaoOutput); + m_gltfPBR->OnUpdateWindowSizeDependentResources(&m_cacaoOutput); } - FfxCacaoD3D12ScreenSizeInfo cacaoScreenSizeDependentInfo; + FFX_CACAO_D3D12ScreenSizeInfo cacaoScreenSizeDependentInfo; cacaoScreenSizeDependentInfo.width = Width; cacaoScreenSizeDependentInfo.height = Height; @@ -249,8 +245,8 @@ void SampleRenderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, cacaoScreenSizeDependentInfo.normalBufferSrvDesc.Texture2D.PlaneSlice = 0; cacaoScreenSizeDependentInfo.normalBufferSrvDesc.Texture2D.ResourceMinLODClamp = 0.0f; - cacaoScreenSizeDependentInfo.outputResource = m_FfxCacaoOutput.GetResource(); - cacaoScreenSizeDependentInfo.outputUavDesc.Format = m_FfxCacaoOutput.GetFormat(); + cacaoScreenSizeDependentInfo.outputResource = m_cacaoOutput.GetResource(); + cacaoScreenSizeDependentInfo.outputUavDesc.Format = m_cacaoOutput.GetFormat(); cacaoScreenSizeDependentInfo.outputUavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D; cacaoScreenSizeDependentInfo.outputUavDesc.Texture2D.MipSlice = 0; cacaoScreenSizeDependentInfo.outputUavDesc.Texture2D.PlaneSlice = 0; @@ -264,25 +260,21 @@ void SampleRenderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, cacaoScreenSizeDependentInfo.depthBufferSrvDesc.Texture2D.PlaneSlice = 0; cacaoScreenSizeDependentInfo.depthBufferSrvDesc.Texture2D.ResourceMinLODClamp = 0.0f; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION cacaoScreenSizeDependentInfo.useDownsampledSsao = FFX_CACAO_FALSE; - ffxCacaoD3D12InitScreenSizeDependentResources(m_pFfxCacaoContextNative, &cacaoScreenSizeDependentInfo); + FFX_CACAO_D3D12InitScreenSizeDependentResources(m_pCACAOContextNative, &cacaoScreenSizeDependentInfo); cacaoScreenSizeDependentInfo.useDownsampledSsao = FFX_CACAO_TRUE; - ffxCacaoD3D12InitScreenSizeDependentResources(m_pFfxCacaoContextDownsampled, &cacaoScreenSizeDependentInfo); -#else - ffxCacaoD3D12InitScreenSizeDependentResources(m_pFfxCacaoContextDownsampled, &cacaoScreenSizeDependentInfo); -#endif + FFX_CACAO_D3D12InitScreenSizeDependentResources(m_pCACAOContextDownsampled, &cacaoScreenSizeDependentInfo); - m_FfxCacaoOutput.CreateSRV(0, &m_applyDirectInput); + m_cacaoOutput.CreateSRV(0, &m_cacaoApplyDirectInput); - m_applyDirect.UpdatePipeline(pSwapChain->GetFormat()); + m_cacaoApplyDirect.UpdatePipeline(pSwapChain->GetFormat()); - // update bloom and downscaling effect - // - m_downSample.OnCreateWindowSizeDependentResources(m_Width, m_Height, &m_HDR, 5); //downsample the HDR texture 5 times - m_bloom.OnCreateWindowSizeDependentResources(m_Width / 2, m_Height / 2, m_downSample.GetTexture(), 5, &m_HDR); - m_toneMapping.UpdatePipelines(pSwapChain->GetFormat()); - m_ImGUI.UpdatePipeline(pSwapChain->GetFormat()); + // update bloom and downscaling effect + // + m_downSample.OnCreateWindowSizeDependentResources(m_width, m_height, &m_hdr, 5); //downsample the HDR texture 5 times + m_bloom.OnCreateWindowSizeDependentResources(m_width / 2, m_height / 2, m_downSample.GetTexture(), 5, &m_hdr); + m_toneMapping.UpdatePipelines(pSwapChain->GetFormat()); + m_imGUI.UpdatePipeline(pSwapChain->GetFormat()); } //-------------------------------------------------------------------------------------- @@ -292,26 +284,24 @@ void SampleRenderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, //-------------------------------------------------------------------------------------- void SampleRenderer::OnDestroyWindowSizeDependentResources() { - m_bloom.OnDestroyWindowSizeDependentResources(); - m_downSample.OnDestroyWindowSizeDependentResources(); + m_bloom.OnDestroyWindowSizeDependentResources(); + m_downSample.OnDestroyWindowSizeDependentResources(); -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - ffxCacaoD3D12DestroyScreenSizeDependentResources(m_pFfxCacaoContextNative); -#endif - ffxCacaoD3D12DestroyScreenSizeDependentResources(m_pFfxCacaoContextDownsampled); - m_FfxCacaoOutput.OnDestroy(); + FFX_CACAO_D3D12DestroyScreenSizeDependentResources(m_pCACAOContextNative); + FFX_CACAO_D3D12DestroyScreenSizeDependentResources(m_pCACAOContextDownsampled); + m_cacaoOutput.OnDestroy(); - m_HDR.OnDestroy(); - m_HDRMSAA.OnDestroy(); - m_HistoryBuffer.OnDestroy(); - m_TAABuffer.OnDestroy(); + m_hdr.OnDestroy(); + m_hdrMSAA.OnDestroy(); + m_historyBuffer.OnDestroy(); + m_taaBuffer.OnDestroy(); #if USE_SHADOWMASK - m_ShadowMask.OnDestroy(); + m_ShadowMask.OnDestroy(); #endif m_normalBufferNonMsaa.OnDestroy(); m_depthBufferNonMsaa.OnDestroy(); - m_depthBuffer.OnDestroy(); + m_depthBuffer.OnDestroy(); } @@ -322,53 +312,53 @@ void SampleRenderer::OnDestroyWindowSizeDependentResources() //-------------------------------------------------------------------------------------- int SampleRenderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) { - // show loading progress - // - ImGui::OpenPopup("Loading"); - if (ImGui::BeginPopupModal("Loading", NULL, ImGuiWindowFlags_AlwaysAutoResize)) - { - float progress = (float)stage / 13.0f; - ImGui::ProgressBar(progress, ImVec2(0.f, 0.f), NULL); - ImGui::EndPopup(); - } - - // Loading stages - // - if (stage == 0) - { - } - else if (stage == 5) - { - Profile p("m_pGltfLoader->Load"); - - m_pGLTFTexturesAndBuffers = new GLTFTexturesAndBuffers(); - m_pGLTFTexturesAndBuffers->OnCreate(m_pDevice, pGLTFCommon, &m_UploadHeap, &m_VidMemBufferPool, &m_ConstantBufferRing); - } - else if (stage == 6) - { - Profile p("LoadTextures"); - - // here we are loading onto the GPU all the textures and the inverse matrices - // this data will be used to create the PBR and Depth passes - m_pGLTFTexturesAndBuffers->LoadTextures(); - } - else if (stage == 7) - { - { - Profile p("m_gltfDepth->OnCreate"); - - //create the glTF's textures, VBs, IBs, shaders and descriptors for this particular pass - m_gltfDepth = new GltfDepthPass(); - m_gltfDepth->OnCreate( - m_pDevice, - &m_UploadHeap, - &m_resourceViewHeaps, - &m_ConstantBufferRing, - &m_VidMemBufferPool, - m_pGLTFTexturesAndBuffers - ); - } - } + // show loading progress + // + ImGui::OpenPopup("Loading"); + if (ImGui::BeginPopupModal("Loading", NULL, ImGuiWindowFlags_AlwaysAutoResize)) + { + float progress = (float)stage / 13.0f; + ImGui::ProgressBar(progress, ImVec2(0.f, 0.f), NULL); + ImGui::EndPopup(); + } + + // Loading stages + // + if (stage == 0) + { + } + else if (stage == 5) + { + Profile p("m_pGltfLoader->Load"); + + m_pGLTFTexturesAndBuffers = new GLTFTexturesAndBuffers(); + m_pGLTFTexturesAndBuffers->OnCreate(m_pDevice, pGLTFCommon, &m_uploadHeap, &m_vidMemBufferPool, &m_constantBufferRing); + } + else if (stage == 6) + { + Profile p("LoadTextures"); + + // here we are loading onto the GPU all the textures and the inverse matrices + // this data will be used to create the PBR and Depth passes + m_pGLTFTexturesAndBuffers->LoadTextures(); + } + else if (stage == 7) + { + { + Profile p("m_gltfDepth->OnCreate"); + + //create the glTF's textures, VBs, IBs, shaders and descriptors for this particular pass + m_gltfDepth = new GltfDepthPass(); + m_gltfDepth->OnCreate( + m_pDevice, + &m_uploadHeap, + &m_resourceViewHeaps, + &m_constantBufferRing, + &m_vidMemBufferPool, + m_pGLTFTexturesAndBuffers + ); + } + } else if (stage == 8) { Profile p("m_gltfPBR->OnCreate (Non MSAA)"); @@ -377,10 +367,10 @@ int SampleRenderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) m_gltfPBRNonMsaa = new GltfPbrPass(); m_gltfPBRNonMsaa->OnCreate( m_pDevice, - &m_UploadHeap, + &m_uploadHeap, &m_resourceViewHeaps, - &m_ConstantBufferRing, - &m_VidMemBufferPool, + &m_constantBufferRing, + &m_vidMemBufferPool, m_pGLTFTexturesAndBuffers, &m_skyDome, false, @@ -392,67 +382,67 @@ int SampleRenderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) 1 ); } - else if (stage == 9) - { - Profile p("m_gltfPBR->OnCreate"); - - // same thing as above but for the PBR pass - m_gltfPBR = new GltfPbrPass(); - m_gltfPBR->OnCreate( - m_pDevice, - &m_UploadHeap, - &m_resourceViewHeaps, - &m_ConstantBufferRing, - &m_VidMemBufferPool, - m_pGLTFTexturesAndBuffers, - &m_skyDome, + else if (stage == 9) + { + Profile p("m_gltfPBR->OnCreate"); + + // same thing as above but for the PBR pass + m_gltfPBR = new GltfPbrPass(); + m_gltfPBR->OnCreate( + m_pDevice, + &m_uploadHeap, + &m_resourceViewHeaps, + &m_constantBufferRing, + &m_vidMemBufferPool, + m_pGLTFTexturesAndBuffers, + &m_skyDome, true, - false, - DXGI_FORMAT_R16G16B16A16_FLOAT, + false, + DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_UNKNOWN, 4 - ); - m_gltfPBR->OnUpdateWindowSizeDependentResources(&m_FfxCacaoOutput); - } - else if (stage == 10) - { - Profile p("m_gltfBBox->OnCreate"); - - // just a bounding box pass that will draw boundingboxes instead of the geometry itself - m_gltfBBox = new GltfBBoxPass(); - m_gltfBBox->OnCreate( - m_pDevice, - &m_UploadHeap, - &m_resourceViewHeaps, - &m_ConstantBufferRing, - &m_VidMemBufferPool, - m_pGLTFTexturesAndBuffers, - &m_wireframe - ); + ); + m_gltfPBR->OnUpdateWindowSizeDependentResources(&m_cacaoOutput); + } + else if (stage == 10) + { + Profile p("m_gltfBBox->OnCreate"); + + // just a bounding box pass that will draw boundingboxes instead of the geometry itself + m_gltfBBox = new GltfBBoxPass(); + m_gltfBBox->OnCreate( + m_pDevice, + &m_uploadHeap, + &m_resourceViewHeaps, + &m_constantBufferRing, + &m_vidMemBufferPool, + m_pGLTFTexturesAndBuffers, + &m_wireframe + ); #if (USE_VID_MEM==true) - // we are borrowing the upload heap command list for uploading to the GPU the IBs and VBs - m_VidMemBufferPool.UploadData(m_UploadHeap.GetCommandList()); + // we are borrowing the upload heap command list for uploading to the GPU the IBs and VBs + m_vidMemBufferPool.UploadData(m_uploadHeap.GetCommandList()); #endif - } - else if (stage == 11) - { - Profile p("Flush"); + } + else if (stage == 11) + { + Profile p("Flush"); - m_UploadHeap.FlushAndFinish(); + m_uploadHeap.FlushAndFinish(); #if (USE_VID_MEM==true) - //once everything is uploaded we dont need he upload heaps anymore - m_VidMemBufferPool.FreeUploadHeap(); + //once everything is uploaded we dont need he upload heaps anymore + m_vidMemBufferPool.FreeUploadHeap(); #endif - // tell caller that we are done loading the map - return 0; - } + // tell caller that we are done loading the map + return 0; + } - stage++; - return stage; + stage++; + return stage; } //-------------------------------------------------------------------------------------- @@ -468,34 +458,34 @@ void SampleRenderer::UnloadScene() delete m_gltfPBRNonMsaa; m_gltfPBRNonMsaa = NULL; } - + if (m_gltfPBR) - { - m_gltfPBR->OnDestroy(); - delete m_gltfPBR; - m_gltfPBR = NULL; - } - - if (m_gltfDepth) - { - m_gltfDepth->OnDestroy(); - delete m_gltfDepth; - m_gltfDepth = NULL; - } - - if (m_gltfBBox) - { - m_gltfBBox->OnDestroy(); - delete m_gltfBBox; - m_gltfBBox = NULL; - } - - if (m_pGLTFTexturesAndBuffers) - { - m_pGLTFTexturesAndBuffers->OnDestroy(); - delete m_pGLTFTexturesAndBuffers; - m_pGLTFTexturesAndBuffers = NULL; - } + { + m_gltfPBR->OnDestroy(); + delete m_gltfPBR; + m_gltfPBR = NULL; + } + + if (m_gltfDepth) + { + m_gltfDepth->OnDestroy(); + delete m_gltfDepth; + m_gltfDepth = NULL; + } + + if (m_gltfBBox) + { + m_gltfBBox->OnDestroy(); + delete m_gltfBBox; + m_gltfBBox = NULL; + } + + if (m_pGLTFTexturesAndBuffers) + { + m_pGLTFTexturesAndBuffers->OnDestroy(); + delete m_pGLTFTexturesAndBuffers; + m_pGLTFTexturesAndBuffers = NULL; + } } @@ -506,147 +496,147 @@ void SampleRenderer::UnloadScene() //-------------------------------------------------------------------------------------- void SampleRenderer::OnRender(State *pState, SwapChain *pSwapChain) { - // Timing values - // - UINT64 gpuTicksPerSecond; - m_pDevice->GetGraphicsQueue()->GetTimestampFrequency(&gpuTicksPerSecond); - - // Let our resource managers do some house keeping - // - m_ConstantBufferRing.OnBeginFrame(); - m_GPUTimer.OnBeginFrame(gpuTicksPerSecond, &m_TimeStamps); - - // Sets the perFrame data (Camera and lights data), override as necessary and set them as constant buffers -------------- - // - per_frame *pPerFrame = NULL; - if (m_pGLTFTexturesAndBuffers) - { - pPerFrame = m_pGLTFTexturesAndBuffers->m_pGLTFCommon->SetPerFrameData(pState->camera); - pPerFrame->invScreenResolution[0] = 1.0f / (float)m_Width; - pPerFrame->invScreenResolution[1] = 1.0f / (float)m_Height; - - //apply jittering to the camera - if (m_HasTAA) - { - static uint32_t sampleIndex=0; - - static const auto CalculateHaltonNumber = [](uint32_t index, uint32_t base) - { - float f = 1.0f, result = 0.0f; - - for (uint32_t i = index; i > 0;) - { - f /= static_cast<float>(base); - result = result + f * static_cast<float>(i % base); - i = static_cast<uint32_t>(floorf(static_cast<float>(i) / static_cast<float>(base))); - } - - return result; - }; - - sampleIndex = (sampleIndex + 1) % 16; // 16x TAA - } - - //override gltf camera with ours - pPerFrame->mCameraViewProj = pState->camera.GetView() * pState->camera.GetProjection(); - pPerFrame->mInverseCameraViewProj = XMMatrixInverse(NULL, pPerFrame->mCameraViewProj); - pPerFrame->cameraPos = pState->camera.GetPosition(); - pPerFrame->iblFactor = pState->iblFactor; - pPerFrame->emmisiveFactor = pState->emmisiveFactor; - - //if the gltf doesn't have any lights set some spotlights - if (pPerFrame->lightCount == 0) - { - pPerFrame->lightCount = pState->spotlightCount; - for (uint32_t i = 0; i < pState->spotlightCount; i++) - { - GetXYZ(pPerFrame->lights[i].color, pState->spotlight[i].color); - GetXYZ(pPerFrame->lights[i].position, pState->spotlight[i].light.GetPosition()); - GetXYZ(pPerFrame->lights[i].direction, pState->spotlight[i].light.GetDirection()); - - pPerFrame->lights[i].range = 15.0f; // in meters - pPerFrame->lights[i].type = LightType_Spot; - pPerFrame->lights[i].intensity = pState->spotlight[i].intensity; - pPerFrame->lights[i].innerConeCos = cosf(pState->spotlight[i].light.GetFovV() * 0.9f / 2.0f); - pPerFrame->lights[i].outerConeCos = cosf(pState->spotlight[i].light.GetFovV() / 2.0f); - pPerFrame->lights[i].mLightViewProj = pState->spotlight[i].light.GetView() * pState->spotlight[i].light.GetProjection(); - } - } - - // Up to 4 spotlights can have shadowmaps. Each spot the light has a shadowMap index which is used to find the shadowmap in the atlas - // Additionally, directional lights shadows can be raytraced. - uint32_t shadowMapIndex = 0; - for (uint32_t i = 0; i < pPerFrame->lightCount; i++) - { - if ((shadowMapIndex < 4) && (pPerFrame->lights[i].type == LightType_Spot)) - { - pPerFrame->lights[i].shadowMapIndex = shadowMapIndex++; // set the shadowmap index so the color pass knows which shadow map to use - pPerFrame->lights[i].depthBias = 70.0f / 100000.0f; - } - else - { - pPerFrame->lights[i].shadowMapIndex = -1; // no shadow for this light - } - } - - m_pGLTFTexturesAndBuffers->SetPerFrameConstants(); - - m_pGLTFTexturesAndBuffers->SetSkinningMatricesForSkeletons(); - } - - // command buffer calls - // - ID3D12GraphicsCommandList* pCmdLst1 = m_CommandListRing.GetNewCommandList(); - - m_GPUTimer.GetTimeStamp(pCmdLst1, "Begin Frame"); - - // Clear GBuffer and depth stencil - // - pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(pSwapChain->GetCurrentBackBufferResource(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET)); - - // Clears ----------------------------------------------------------------------- - // - pCmdLst1->ClearDepthStencilView(m_ShadowMapDSV.GetCPU(), D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr); - m_GPUTimer.GetTimeStamp(pCmdLst1, "Clear shadow map"); - - float clearColor[] = { 0.0f, 0.0f, 0.0f, 0.0f }; - pCmdLst1->ClearRenderTargetView(m_HDRRTVMSAA.GetCPU(), clearColor, 0, nullptr); - m_GPUTimer.GetTimeStamp(pCmdLst1, "Clear HDR"); - - pCmdLst1->ClearDepthStencilView(m_depthBufferDSV.GetCPU(), D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr); - m_GPUTimer.GetTimeStamp(pCmdLst1, "Clear depth"); + // Timing values + // + UINT64 gpuTicksPerSecond; + m_pDevice->GetGraphicsQueue()->GetTimestampFrequency(&gpuTicksPerSecond); + + // Let our resource managers do some house keeping + // + m_constantBufferRing.OnBeginFrame(); + m_gpuTimer.OnBeginFrame(gpuTicksPerSecond, &m_timeStamps); + + // Sets the perFrame data (Camera and lights data), override as necessary and set them as constant buffers -------------- + // + per_frame *pPerFrame = NULL; + if (m_pGLTFTexturesAndBuffers) + { + pPerFrame = m_pGLTFTexturesAndBuffers->m_pGLTFCommon->SetPerFrameData(pState->camera); + pPerFrame->invScreenResolution[0] = 1.0f / (float)m_width; + pPerFrame->invScreenResolution[1] = 1.0f / (float)m_height; + + //apply jittering to the camera + if (m_hasTAA) + { + static uint32_t sampleIndex=0; + + static const auto CalculateHaltonNumber = [](uint32_t index, uint32_t base) + { + float f = 1.0f, result = 0.0f; + + for (uint32_t i = index; i > 0;) + { + f /= static_cast<float>(base); + result = result + f * static_cast<float>(i % base); + i = static_cast<uint32_t>(floorf(static_cast<float>(i) / static_cast<float>(base))); + } + + return result; + }; + + sampleIndex = (sampleIndex + 1) % 16; // 16x TAA + } + + //override gltf camera with ours + pPerFrame->mCameraViewProj = pState->camera.GetView() * pState->camera.GetProjection(); + pPerFrame->mInverseCameraViewProj = XMMatrixInverse(NULL, pPerFrame->mCameraViewProj); + pPerFrame->cameraPos = pState->camera.GetPosition(); + pPerFrame->iblFactor = pState->iblFactor; + pPerFrame->emmisiveFactor = pState->emmisiveFactor; + + //if the gltf doesn't have any lights set some spotlights + if (pPerFrame->lightCount == 0) + { + pPerFrame->lightCount = pState->spotlightCount; + for (uint32_t i = 0; i < pState->spotlightCount; i++) + { + GetXYZ(pPerFrame->lights[i].color, pState->spotlight[i].color); + GetXYZ(pPerFrame->lights[i].position, pState->spotlight[i].light.GetPosition()); + GetXYZ(pPerFrame->lights[i].direction, pState->spotlight[i].light.GetDirection()); + + pPerFrame->lights[i].range = 15.0f; // in meters + pPerFrame->lights[i].type = LightType_Spot; + pPerFrame->lights[i].intensity = pState->spotlight[i].intensity; + pPerFrame->lights[i].innerConeCos = cosf(pState->spotlight[i].light.GetFovV() * 0.9f / 2.0f); + pPerFrame->lights[i].outerConeCos = cosf(pState->spotlight[i].light.GetFovV() / 2.0f); + pPerFrame->lights[i].mLightViewProj = pState->spotlight[i].light.GetView() * pState->spotlight[i].light.GetProjection(); + } + } + + // Up to 4 spotlights can have shadowmaps. Each spot the light has a shadowMap index which is used to find the shadowmap in the atlas + // Additionally, directional lights shadows can be raytraced. + uint32_t shadowMapIndex = 0; + for (uint32_t i = 0; i < pPerFrame->lightCount; i++) + { + if ((shadowMapIndex < 4) && (pPerFrame->lights[i].type == LightType_Spot)) + { + pPerFrame->lights[i].shadowMapIndex = shadowMapIndex++; // set the shadowmap index so the color pass knows which shadow map to use + pPerFrame->lights[i].depthBias = 70.0f / 100000.0f; + } + else + { + pPerFrame->lights[i].shadowMapIndex = -1; // no shadow for this light + } + } + + m_pGLTFTexturesAndBuffers->SetPerFrameConstants(); + + m_pGLTFTexturesAndBuffers->SetSkinningMatricesForSkeletons(); + } + + // command buffer calls + // + ID3D12GraphicsCommandList* pCmdLst1 = m_commandListRing.GetNewCommandList(); + + m_gpuTimer.GetTimeStamp(pCmdLst1, "Begin Frame"); + + // Clear GBuffer and depth stencil + // + pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(pSwapChain->GetCurrentBackBufferResource(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET)); + + // Clears ----------------------------------------------------------------------- + // + pCmdLst1->ClearDepthStencilView(m_shadowMapDSV.GetCPU(), D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr); + m_gpuTimer.GetTimeStamp(pCmdLst1, "Clear shadow map"); + + float clearColor[] = { 0.0f, 0.0f, 0.0f, 0.0f }; + pCmdLst1->ClearRenderTargetView(m_hdrRTVMSAA.GetCPU(), clearColor, 0, nullptr); + m_gpuTimer.GetTimeStamp(pCmdLst1, "Clear HDR"); + + pCmdLst1->ClearDepthStencilView(m_depthBufferDSV.GetCPU(), D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr); + m_gpuTimer.GetTimeStamp(pCmdLst1, "Clear depth"); pCmdLst1->ClearDepthStencilView(m_depthBufferNonMsaaDSV.GetCPU(), D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, NULL); - m_GPUTimer.GetTimeStamp(pCmdLst1, "Clear depth (Non MSAA)"); - - // Render to shadow map atlas for spot lights ------------------------------------------ - // - if (m_gltfDepth && pPerFrame != NULL) - { - uint32_t shadowMapIndex = 0; - for (uint32_t i = 0; i < pPerFrame->lightCount; i++) - { - if (pPerFrame->lights[i].type != LightType_Spot) - continue; - - // Set the RT's quadrant where to render the shadomap (these viewport offsets need to match the ones in shadowFiltering.h) - uint32_t viewportOffsetsX[4] = { 0, 1, 0, 1 }; - uint32_t viewportOffsetsY[4] = { 0, 0, 1, 1 }; - uint32_t viewportWidth = m_ShadowMap.GetWidth() / 2; - uint32_t viewportHeight = m_ShadowMap.GetHeight() / 2; - SetViewportAndScissor(pCmdLst1, viewportOffsetsX[i] * viewportWidth, viewportOffsetsY[i] * viewportHeight, viewportWidth, viewportHeight); - pCmdLst1->OMSetRenderTargets(0, NULL, true, &m_ShadowMapDSV.GetCPU()); - - GltfDepthPass::per_frame *cbDepthPerFrame = m_gltfDepth->SetPerFrameConstants(); - cbDepthPerFrame->mViewProj = pPerFrame->lights[i].mLightViewProj; - - m_gltfDepth->Draw(pCmdLst1); - - m_GPUTimer.GetTimeStamp(pCmdLst1, "Shadow map"); - shadowMapIndex++; - } - } - pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_ShadowMap.GetResource(), D3D12_RESOURCE_STATE_DEPTH_WRITE, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE)); + m_gpuTimer.GetTimeStamp(pCmdLst1, "Clear depth (Non MSAA)"); + + // Render to shadow map atlas for spot lights ------------------------------------------ + // + if (m_gltfDepth && pPerFrame != NULL) + { + uint32_t shadowMapIndex = 0; + for (uint32_t i = 0; i < pPerFrame->lightCount; i++) + { + if (pPerFrame->lights[i].type != LightType_Spot) + continue; + + // Set the RT's quadrant where to render the shadomap (these viewport offsets need to match the ones in shadowFiltering.h) + uint32_t viewportOffsetsX[4] = { 0, 1, 0, 1 }; + uint32_t viewportOffsetsY[4] = { 0, 0, 1, 1 }; + uint32_t viewportWidth = m_shadowMap.GetWidth() / 2; + uint32_t viewportHeight = m_shadowMap.GetHeight() / 2; + SetViewportAndScissor(pCmdLst1, viewportOffsetsX[i] * viewportWidth, viewportOffsetsY[i] * viewportHeight, viewportWidth, viewportHeight); + pCmdLst1->OMSetRenderTargets(0, NULL, true, &m_shadowMapDSV.GetCPU()); + + GltfDepthPass::per_frame *cbDepthPerFrame = m_gltfDepth->SetPerFrameConstants(); + cbDepthPerFrame->mViewProj = pPerFrame->lights[i].mLightViewProj; + + m_gltfDepth->Draw(pCmdLst1); + + m_gpuTimer.GetTimeStamp(pCmdLst1, "Shadow map"); + shadowMapIndex++; + } + } + pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_shadowMap.GetResource(), D3D12_RESOURCE_STATE_DEPTH_WRITE, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE)); // Render normal/depth buffer if (pPerFrame) @@ -654,13 +644,13 @@ void SampleRenderer::OnRender(State *pState, SwapChain *pSwapChain) // Render Scene to the MSAA HDR RT ------------------------------------------------ // pCmdLst1->RSSetViewports(1, &m_viewPort); - pCmdLst1->RSSetScissorRects(1, &m_RectScissor); + pCmdLst1->RSSetScissorRects(1, &m_rectScissor); pCmdLst1->OMSetRenderTargets(1, &m_normalBufferNonMsaaRTV.GetCPU(), true, &m_depthBufferNonMsaaDSV.GetCPU()); // Render normal/depth buffer if (m_gltfPBRNonMsaa) { - m_gltfPBRNonMsaa->Draw(pCmdLst1, &m_ShadowMapSRV); + m_gltfPBRNonMsaa->Draw(pCmdLst1, &m_shadowMapSRV); } // resource barriers @@ -672,9 +662,9 @@ void SampleRenderer::OnRender(State *pState, SwapChain *pSwapChain) pCmdLst1->ResourceBarrier(_countof(barriers), barriers); } - if (pState->bUseCACAO) + if (pState->useCACAO) { - FfxCacaoMatrix4x4 proj, normalsWorldToView; + FFX_CACAO_Matrix4x4 proj, normalsWorldToView; { XMFLOAT4X4 p; XMMATRIX xProj = pState->camera.GetProjection(); @@ -692,25 +682,21 @@ void SampleRenderer::OnRender(State *pState, SwapChain *pSwapChain) normalsWorldToView.elements[3][0] = p._41; normalsWorldToView.elements[3][1] = p._42; normalsWorldToView.elements[3][2] = p._43; normalsWorldToView.elements[3][3] = p._44; } - FfxCacaoD3D12Context *context = NULL; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - context = pState->bUseDownsampledSSAO ? m_pFfxCacaoContextDownsampled : m_pFfxCacaoContextNative; -#else - context = m_pFfxCacaoContextDownsampled; -#endif + FFX_CACAO_D3D12Context *context = NULL; + context = pState->useDownsampledSSAO ? m_pCACAOContextDownsampled : m_pCACAOContextNative; - pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_FfxCacaoOutput.GetResource(), D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_STATE_UNORDERED_ACCESS)); + pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_cacaoOutput.GetResource(), D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_STATE_UNORDERED_ACCESS)); - ffxCacaoD3D12UpdateSettings(context, &pState->cacaoSettings); - ffxCacaoD3D12Draw(context, pCmdLst1, &proj, &normalsWorldToView); + FFX_CACAO_D3D12UpdateSettings(context, &pState->cacaoSettings); + FFX_CACAO_D3D12Draw(context, pCmdLst1, &proj, &normalsWorldToView); } else { - pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_FfxCacaoOutput.GetResource(), D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_STATE_UNORDERED_ACCESS)); + pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_cacaoOutput.GetResource(), D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_STATE_UNORDERED_ACCESS)); uint32_t dummy = 0; - D3D12_GPU_VIRTUAL_ADDRESS dummyConstantBufferAddress = m_ConstantBufferRing.AllocConstantBuffer(sizeof(dummy), &dummy); - m_cacaoUavClear.Draw(pCmdLst1, dummyConstantBufferAddress, &m_FfxCacaoOutputUAV, NULL, m_Width, m_Height, 1); - pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_FfxCacaoOutput.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_GENERIC_READ)); + D3D12_GPU_VIRTUAL_ADDRESS dummyConstantBufferAddress = m_constantBufferRing.AllocConstantBuffer(sizeof(dummy), &dummy); + m_cacaoUAVClear.Draw(pCmdLst1, dummyConstantBufferAddress, &m_cacaoOutputUAV, NULL, m_width, m_height, 1); + pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_cacaoOutput.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_GENERIC_READ)); } // resource barriers @@ -723,199 +709,195 @@ void SampleRenderer::OnRender(State *pState, SwapChain *pSwapChain) } } - // Render Scene to the MSAA HDR RT ------------------------------------------------ - // - pCmdLst1->RSSetViewports(1, &m_viewPort); - pCmdLst1->RSSetScissorRects(1, &m_RectScissor); - pCmdLst1->OMSetRenderTargets(1, &m_HDRRTVMSAA.GetCPU(), true, &m_depthBufferDSV.GetCPU()); - - if (pPerFrame != NULL) - { - // Render skydome - // - if (pState->skyDomeType == 1) - { - XMMATRIX clipToView = XMMatrixInverse(NULL, pPerFrame->mCameraViewProj); - m_skyDome.Draw(pCmdLst1, clipToView); - m_GPUTimer.GetTimeStamp(pCmdLst1, "Skydome"); - } - else if (pState->skyDomeType == 0) - { - SkyDomeProc::Constants skyDomeConstants; - skyDomeConstants.invViewProj = XMMatrixInverse(NULL, pPerFrame->mCameraViewProj); - skyDomeConstants.vSunDirection = XMVectorSet(1.0f, 0.05f, 0.0f, 0.0f); - skyDomeConstants.turbidity = 10.0f; - skyDomeConstants.rayleigh = 2.0f; - skyDomeConstants.mieCoefficient = 0.005f; - skyDomeConstants.mieDirectionalG = 0.8f; - skyDomeConstants.luminance = 1.0f; - skyDomeConstants.sun = false; - m_skyDomeProc.Draw(pCmdLst1, skyDomeConstants); - - m_GPUTimer.GetTimeStamp(pCmdLst1, "Skydome proc"); - } - - // Render scene to color buffer - // - if (m_gltfPBR && pPerFrame != NULL) - { - //set per frame constant buffer values - m_gltfPBR->Draw(pCmdLst1, &m_ShadowMapSRV); - } - - // draw object's bounding boxes - // - if (m_gltfBBox && pPerFrame != NULL) - { - if (pState->bDrawBoundingBoxes) - { - m_gltfBBox->Draw(pCmdLst1, pPerFrame->mCameraViewProj); - - m_GPUTimer.GetTimeStamp(pCmdLst1, "Bounding Box"); - } - } - - // draw light's frustums - // - if (pState->bDrawLightFrustum && pPerFrame != NULL) - { - UserMarker marker(pCmdLst1, "light frustrums"); - - XMVECTOR vCenter = XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f); - XMVECTOR vRadius = XMVectorSet(1.0f, 1.0f, 1.0f, 0.0f); - XMVECTOR vColor = XMVectorSet(1.0f, 1.0f, 1.0f, 1.0f); - for (uint32_t i = 0; i < pPerFrame->lightCount; i++) - { - XMMATRIX spotlightMatrix = XMMatrixInverse(NULL, pPerFrame->lights[i].mLightViewProj); - XMMATRIX worldMatrix = spotlightMatrix * pPerFrame->mCameraViewProj; - m_wireframeBox.Draw(pCmdLst1, &m_wireframe, worldMatrix, vCenter, vRadius, vColor); - } - - m_GPUTimer.GetTimeStamp(pCmdLst1, "Light's frustum"); - } - } - pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_ShadowMap.GetResource(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_DEPTH_WRITE)); - // pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_depthBuffer.GetResource(), D3D12_RESOURCE_STATE_DEPTH_WRITE, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE)); - - m_GPUTimer.GetTimeStamp(pCmdLst1, "Rendering scene"); - - // Resolve MSAA ------------------------------------------------------------------------ - // - { - UserMarker marker(pCmdLst1, "Resolving MSAA"); - - D3D12_RESOURCE_BARRIER preResolve[2] = { - CD3DX12_RESOURCE_BARRIER::Transition(m_HDR.GetResource(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_RESOLVE_DEST), - CD3DX12_RESOURCE_BARRIER::Transition(m_HDRMSAA.GetResource(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_RESOLVE_SOURCE) - }; - pCmdLst1->ResourceBarrier(2, preResolve); - - pCmdLst1->ResolveSubresource(m_HDR.GetResource(), 0, m_HDRMSAA.GetResource(), 0, DXGI_FORMAT_R16G16B16A16_FLOAT); - - D3D12_RESOURCE_BARRIER postResolve[2] = { - CD3DX12_RESOURCE_BARRIER::Transition(m_HDR.GetResource(), D3D12_RESOURCE_STATE_RESOLVE_DEST, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE), - CD3DX12_RESOURCE_BARRIER::Transition(m_HDRMSAA.GetResource(), D3D12_RESOURCE_STATE_RESOLVE_SOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET) - }; - pCmdLst1->ResourceBarrier(2, postResolve); - - m_GPUTimer.GetTimeStamp(pCmdLst1, "Resolve MSAA"); - } - - // Post proc--------------------------------------------------------------------------- - // + // Render Scene to the MSAA HDR RT ------------------------------------------------ + // + pCmdLst1->RSSetViewports(1, &m_viewPort); + pCmdLst1->RSSetScissorRects(1, &m_rectScissor); + pCmdLst1->OMSetRenderTargets(1, &m_hdrRTVMSAA.GetCPU(), true, &m_depthBufferDSV.GetCPU()); + + if (pPerFrame != NULL) + { + // Render skydome + // + if (pState->skyDomeType == 1) + { + XMMATRIX clipToView = XMMatrixInverse(NULL, pPerFrame->mCameraViewProj); + m_skyDome.Draw(pCmdLst1, clipToView); + m_gpuTimer.GetTimeStamp(pCmdLst1, "Skydome"); + } + else if (pState->skyDomeType == 0) + { + SkyDomeProc::Constants skyDomeConstants; + skyDomeConstants.invViewProj = XMMatrixInverse(NULL, pPerFrame->mCameraViewProj); + skyDomeConstants.vSunDirection = XMVectorSet(1.0f, 0.05f, 0.0f, 0.0f); + skyDomeConstants.turbidity = 10.0f; + skyDomeConstants.rayleigh = 2.0f; + skyDomeConstants.mieCoefficient = 0.005f; + skyDomeConstants.mieDirectionalG = 0.8f; + skyDomeConstants.luminance = 1.0f; + skyDomeConstants.sun = false; + m_skyDomeProc.Draw(pCmdLst1, skyDomeConstants); + + m_gpuTimer.GetTimeStamp(pCmdLst1, "Skydome proc"); + } + + // Render scene to color buffer + // + if (m_gltfPBR && pPerFrame != NULL) + { + //set per frame constant buffer values + m_gltfPBR->Draw(pCmdLst1, &m_shadowMapSRV); + } + + // draw object's bounding boxes + // + if (m_gltfBBox && pPerFrame != NULL) + { + if (pState->drawBoundingBoxes) + { + m_gltfBBox->Draw(pCmdLst1, pPerFrame->mCameraViewProj); + + m_gpuTimer.GetTimeStamp(pCmdLst1, "Bounding Box"); + } + } + + // draw light's frustums + // + if (pState->drawLightFrustum && pPerFrame != NULL) + { + UserMarker marker(pCmdLst1, "light frustrums"); + + XMVECTOR vCenter = XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f); + XMVECTOR vRadius = XMVectorSet(1.0f, 1.0f, 1.0f, 0.0f); + XMVECTOR vColor = XMVectorSet(1.0f, 1.0f, 1.0f, 1.0f); + for (uint32_t i = 0; i < pPerFrame->lightCount; i++) + { + XMMATRIX spotlightMatrix = XMMatrixInverse(NULL, pPerFrame->lights[i].mLightViewProj); + XMMATRIX worldMatrix = spotlightMatrix * pPerFrame->mCameraViewProj; + m_wireframeBox.Draw(pCmdLst1, &m_wireframe, worldMatrix, vCenter, vRadius, vColor); + } + + m_gpuTimer.GetTimeStamp(pCmdLst1, "Light's frustum"); + } + } + pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_shadowMap.GetResource(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_DEPTH_WRITE)); + // pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_depthBuffer.GetResource(), D3D12_RESOURCE_STATE_DEPTH_WRITE, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE)); + + m_gpuTimer.GetTimeStamp(pCmdLst1, "Rendering scene"); + + // Resolve MSAA ------------------------------------------------------------------------ + // + { + UserMarker marker(pCmdLst1, "Resolving MSAA"); + + D3D12_RESOURCE_BARRIER preResolve[2] = { + CD3DX12_RESOURCE_BARRIER::Transition(m_hdr.GetResource(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_RESOLVE_DEST), + CD3DX12_RESOURCE_BARRIER::Transition(m_hdrMSAA.GetResource(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_RESOLVE_SOURCE) + }; + pCmdLst1->ResourceBarrier(2, preResolve); + + pCmdLst1->ResolveSubresource(m_hdr.GetResource(), 0, m_hdrMSAA.GetResource(), 0, DXGI_FORMAT_R16G16B16A16_FLOAT); + + D3D12_RESOURCE_BARRIER postResolve[2] = { + CD3DX12_RESOURCE_BARRIER::Transition(m_hdr.GetResource(), D3D12_RESOURCE_STATE_RESOLVE_DEST, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE), + CD3DX12_RESOURCE_BARRIER::Transition(m_hdrMSAA.GetResource(), D3D12_RESOURCE_STATE_RESOLVE_SOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET) + }; + pCmdLst1->ResourceBarrier(2, postResolve); + + m_gpuTimer.GetTimeStamp(pCmdLst1, "Resolve MSAA"); + } + + // Post proc--------------------------------------------------------------------------- + // if (0) - { - // Bloom, takes HDR as input and applies bloom to it. - // - { - D3D12_CPU_DESCRIPTOR_HANDLE renderTargets[] = { m_HDRRTV.GetCPU() }; - pCmdLst1->OMSetRenderTargets(ARRAYSIZE(renderTargets), renderTargets, false, NULL); + { + // Bloom, takes HDR as input and applies bloom to it. + // + { + D3D12_CPU_DESCRIPTOR_HANDLE renderTargets[] = { m_hdrRTV.GetCPU() }; + pCmdLst1->OMSetRenderTargets(ARRAYSIZE(renderTargets), renderTargets, false, NULL); - m_downSample.Draw(pCmdLst1); - //m_downSample.Gui(); - m_GPUTimer.GetTimeStamp(pCmdLst1, "Downsample"); + m_downSample.Draw(pCmdLst1); + //m_downSample.Gui(); + m_gpuTimer.GetTimeStamp(pCmdLst1, "Downsample"); - m_bloom.Draw(pCmdLst1, &m_HDR); - //m_bloom.Gui(); - m_GPUTimer.GetTimeStamp(pCmdLst1, "Bloom"); - } - } + m_bloom.Draw(pCmdLst1, &m_hdr); + //m_bloom.Gui(); + m_gpuTimer.GetTimeStamp(pCmdLst1, "Bloom"); + } + } - // submit command buffer + // submit command buffer - ThrowIfFailed(pCmdLst1->Close()); - ID3D12CommandList* CmdListList1[] = { pCmdLst1 }; - m_pDevice->GetGraphicsQueue()->ExecuteCommandLists(1, CmdListList1); + ThrowIfFailed(pCmdLst1->Close()); + ID3D12CommandList* CmdListList1[] = { pCmdLst1 }; + m_pDevice->GetGraphicsQueue()->ExecuteCommandLists(1, CmdListList1); - // Wait for swapchain (we are going to render to it) ----------------------------------- - // - pSwapChain->WaitForSwapChain(); + // Wait for swapchain (we are going to render to it) ----------------------------------- + // + pSwapChain->WaitForSwapChain(); - m_CommandListRing.OnBeginFrame(); + m_commandListRing.OnBeginFrame(); - ID3D12GraphicsCommandList* pCmdLst2 = m_CommandListRing.GetNewCommandList(); + ID3D12GraphicsCommandList* pCmdLst2 = m_commandListRing.GetNewCommandList(); - // Tonemapping ------------------------------------------------------------------------ - // - if (pState->bDisplayCacaoDirectly) + // Tonemapping ------------------------------------------------------------------------ + // + if (pState->displayCacaoDirectly) { pCmdLst2->RSSetViewports(1, &m_viewPort); - pCmdLst2->RSSetScissorRects(1, &m_RectScissor); + pCmdLst2->RSSetScissorRects(1, &m_rectScissor); pCmdLst2->OMSetRenderTargets(1, pSwapChain->GetCurrentBackBufferRTV(), true, NULL); - m_applyDirect.Draw(pCmdLst2, 1, &m_applyDirectInput, NULL); + m_cacaoApplyDirect.Draw(pCmdLst2, 1, &m_cacaoApplyDirectInput, NULL); } else { - pCmdLst2->RSSetViewports(1, &m_viewPort); - pCmdLst2->RSSetScissorRects(1, &m_RectScissor); - pCmdLst2->OMSetRenderTargets(1, pSwapChain->GetCurrentBackBufferRTV(), true, NULL); + pCmdLst2->RSSetViewports(1, &m_viewPort); + pCmdLst2->RSSetScissorRects(1, &m_rectScissor); + pCmdLst2->OMSetRenderTargets(1, pSwapChain->GetCurrentBackBufferRTV(), true, NULL); - m_toneMapping.Draw(pCmdLst2, &m_HDRSRV, pState->exposure, pState->toneMapper); - m_GPUTimer.GetTimeStamp(pCmdLst2, "Tone mapping"); + m_toneMapping.Draw(pCmdLst2, &m_hdrSRV, pState->exposure, pState->toneMapper); + m_gpuTimer.GetTimeStamp(pCmdLst2, "Tone mapping"); - pCmdLst2->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_HDR.GetResource(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET)); - } + pCmdLst2->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_hdr.GetResource(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET)); + } - // Render HUD ------------------------------------------------------------------------ - // - { - pCmdLst2->RSSetViewports(1, &m_viewPort); - pCmdLst2->RSSetScissorRects(1, &m_RectScissor); - pCmdLst2->OMSetRenderTargets(1, pSwapChain->GetCurrentBackBufferRTV(), true, NULL); + // Render HUD ------------------------------------------------------------------------ + // + { + pCmdLst2->RSSetViewports(1, &m_viewPort); + pCmdLst2->RSSetScissorRects(1, &m_rectScissor); + pCmdLst2->OMSetRenderTargets(1, pSwapChain->GetCurrentBackBufferRTV(), true, NULL); - m_ImGUI.Draw(pCmdLst2); + m_imGUI.Draw(pCmdLst2); - m_GPUTimer.GetTimeStamp(pCmdLst2, "ImGUI rendering"); - } + m_gpuTimer.GetTimeStamp(pCmdLst2, "ImGUI rendering"); + } - // Transition swapchain into present mode + // Transition swapchain into present mode - pCmdLst2->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(pSwapChain->GetCurrentBackBufferResource(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT)); + pCmdLst2->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(pSwapChain->GetCurrentBackBufferResource(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT)); - m_GPUTimer.OnEndFrame(); + m_gpuTimer.OnEndFrame(); - m_GPUTimer.CollectTimings(pCmdLst2); + m_gpuTimer.CollectTimings(pCmdLst2); - // Close & Submit the command list ---------------------------------------------------- - // - ThrowIfFailed(pCmdLst2->Close()); + // Close & Submit the command list ---------------------------------------------------- + // + ThrowIfFailed(pCmdLst2->Close()); - ID3D12CommandList* CmdListList2[] = { pCmdLst2 }; - m_pDevice->GetGraphicsQueue()->ExecuteCommandLists(1, CmdListList2); + ID3D12CommandList* CmdListList2[] = { pCmdLst2 }; + m_pDevice->GetGraphicsQueue()->ExecuteCommandLists(1, CmdListList2); } #ifdef FFX_CACAO_ENABLE_PROFILING -void SampleRenderer::GetCacaoTimings(State *pState, FfxCacaoDetailedTiming* timings, uint64_t* gpuTicksPerSecond) +void SampleRenderer::GetCacaoTimings(State *pState, FFX_CACAO_DetailedTiming* timings, uint64_t* gpuTicksPerSecond) { - FfxCacaoD3D12Context *context = NULL; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - context = pState->bUseDownsampledSSAO ? m_pFfxCacaoContextDownsampled : m_pFfxCacaoContextNative; -#else - context = m_pFfxCacaoContextDownsampled; -#endif + FFX_CACAO_D3D12Context *context = NULL; + context = pState->useDownsampledSSAO ? m_pCACAOContextDownsampled : m_pCACAOContextNative; - ffxCacaoD3D12GetDetailedTimings(context, timings); + FFX_CACAO_D3D12GetDetailedTimings(context, timings); m_pDevice->GetGraphicsQueue()->GetTimestampFrequency(gpuTicksPerSecond); } #endif diff --git a/sample/src/DX12/SampleRenderer.h b/sample/src/DX12/SampleRenderer.h index 221828c..be6840b 100644 --- a/sample/src/DX12/SampleRenderer.h +++ b/sample/src/DX12/SampleRenderer.h @@ -1,6 +1,6 @@ // AMD SampleDX12 sample code -// -// Copyright(c) 2018 Advanced Micro Devices, Inc.All rights reserved. +// +// 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 @@ -18,7 +18,7 @@ // THE SOFTWARE. #pragma once -#include "ffx_cacao.h" +#include "ffx_cacao_impl.h" static const int backBufferCount = 2; @@ -32,148 +32,144 @@ using namespace CAULDRON_DX12; class SampleRenderer { public: - struct Spotlight - { - Camera light; - XMVECTOR color; - float intensity; - }; - - struct State - { - float time; - Camera camera; - - float exposure; - float iblFactor; - float emmisiveFactor; - - int toneMapper; - int skyDomeType; - bool bDrawBoundingBoxes; - - uint32_t spotlightCount; - Spotlight spotlight[4]; - bool bDrawLightFrustum; - -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - bool bUseDownsampledSSAO; -#endif - bool bDisplayCacaoDirectly; - bool bUseCACAO; - FfxCacaoSettings cacaoSettings; - }; + struct Spotlight + { + Camera light; + XMVECTOR color; + float intensity; + }; + + struct State + { + float time; + Camera camera; + + float exposure; + float iblFactor; + float emmisiveFactor; - void OnCreate(Device* pDevice, SwapChain *pSwapChain); - void OnDestroy(); + int toneMapper; + int skyDomeType; + bool drawBoundingBoxes; - void OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, uint32_t Width, uint32_t Height); - void OnDestroyWindowSizeDependentResources(); + uint32_t spotlightCount; + Spotlight spotlight[4]; + bool drawLightFrustum; - int LoadScene(GLTFCommon *pGLTFCommon, int stage = 0); - void UnloadScene(); + bool useDownsampledSSAO; + bool displayCacaoDirectly; + bool useCACAO; + FFX_CACAO_Settings cacaoSettings; + }; - const std::vector<TimeStamp> &GetTimingValues() { return m_TimeStamps; } + void OnCreate(Device* pDevice, SwapChain *pSwapChain); + void OnDestroy(); - void OnRender(State *pState, SwapChain *pSwapChain); + void OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, uint32_t Width, uint32_t Height); + void OnDestroyWindowSizeDependentResources(); + + int LoadScene(GLTFCommon *pGLTFCommon, int stage = 0); + void UnloadScene(); + + const std::vector<TimeStamp> &GetTimingValues() { return m_timeStamps; } + + void OnRender(State *pState, SwapChain *pSwapChain); #ifdef FFX_CACAO_ENABLE_PROFILING - void GetCacaoTimings(State *pState, FfxCacaoDetailedTiming* timings, uint64_t* gpuTicksPerSecong); + void GetCacaoTimings(State *pState, FFX_CACAO_DetailedTiming* timings, uint64_t* gpuTicksPerSecong); #endif private: - Device *m_pDevice; - - uint32_t m_Width; - uint32_t m_Height; - uint32_t m_HalfWidth; - uint32_t m_HalfHeight; - D3D12_VIEWPORT m_viewPort; - D3D12_RECT m_RectScissor; - bool m_HasTAA = false; - - // Initialize helper classes - ResourceViewHeaps m_resourceViewHeaps; - UploadHeap m_UploadHeap; - DynamicBufferRing m_ConstantBufferRing; - StaticBufferPool m_VidMemBufferPool; - CommandListRing m_CommandListRing; - GPUTimestamps m_GPUTimer; - - //gltf passes + Device *m_pDevice; + + uint32_t m_width; + uint32_t m_height; + uint32_t m_halfWidth; + uint32_t m_halfHeight; + D3D12_VIEWPORT m_viewPort; + D3D12_RECT m_rectScissor; + bool m_hasTAA = false; + + // Initialize helper classes + ResourceViewHeaps m_resourceViewHeaps; + UploadHeap m_uploadHeap; + DynamicBufferRing m_constantBufferRing; + StaticBufferPool m_vidMemBufferPool; + CommandListRing m_commandListRing; + GPUTimestamps m_gpuTimer; + + //gltf passes GltfPbrPass *m_gltfPBRNonMsaa; - GltfPbrPass *m_gltfPBR; - GltfBBoxPass *m_gltfBBox; - GltfDepthPass *m_gltfDepth; - GLTFTexturesAndBuffers *m_pGLTFTexturesAndBuffers; - - // effects - Bloom m_bloom; - SkyDome m_skyDome; - DownSamplePS m_downSample; - SkyDomeProc m_skyDomeProc; - ToneMapping m_toneMapping; - PostProcCS m_motionBlur; - Sharpen m_sharpen; - TAA m_taa; + GltfPbrPass *m_gltfPBR; + GltfBBoxPass *m_gltfBBox; + GltfDepthPass *m_gltfDepth; + GLTFTexturesAndBuffers *m_pGLTFTexturesAndBuffers; + + // effects + Bloom m_bloom; + SkyDome m_skyDome; + DownSamplePS m_downSample; + SkyDomeProc m_skyDomeProc; + ToneMapping m_toneMapping; + PostProcCS m_motionBlur; + Sharpen m_sharpen; + TAA m_taa; // ================================ // CACAO stuff - CBV_SRV_UAV m_applyDirectInput; - PostProcPS m_applyDirect; - PostProcCS m_cacaoUavClear; + CBV_SRV_UAV m_cacaoApplyDirectInput; + PostProcPS m_cacaoApplyDirect; + PostProcCS m_cacaoUAVClear; - Texture m_FfxCacaoOutput; - CBV_SRV_UAV m_FfxCacaoOutputUAV; - CBV_SRV_UAV m_FfxCacaoOutputSRV; + Texture m_cacaoOutput; + CBV_SRV_UAV m_cacaoOutputUAV; + CBV_SRV_UAV m_cacaoOutputSRV; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - FfxCacaoD3D12Context *m_pFfxCacaoContextNative; -#endif - FfxCacaoD3D12Context *m_pFfxCacaoContextDownsampled; + FFX_CACAO_D3D12Context *m_pCACAOContextNative; + FFX_CACAO_D3D12Context *m_pCACAOContextDownsampled; - // GUI - ImGUI m_ImGUI; + // GUI + ImGUI m_imGUI; // Deferred pass buffers - Texture m_depthBufferNonMsaa; - DSV m_depthBufferNonMsaaDSV; - CBV_SRV_UAV m_depthBufferNonMsaaSRV; - - Texture m_normalBufferNonMsaa; - RTV m_normalBufferNonMsaaRTV; - CBV_SRV_UAV m_normalBufferNonMsaaSRV; - - // depth buffer - Texture m_depthBuffer; - DSV m_depthBufferDSV; - CBV_SRV_UAV m_depthBufferSRV; - - // TAA buffer - Texture m_TAABuffer; - CBV_SRV_UAV m_TAABufferSRV; - CBV_SRV_UAV m_TAABufferUAV; - CBV_SRV_UAV m_TAAInputsSRV; - Texture m_HistoryBuffer; - RTV m_HistoryBufferRTV; - - // shadowmaps - Texture m_ShadowMap; - DSV m_ShadowMapDSV; - CBV_SRV_UAV m_ShadowMapSRV; - - // MSAA RT - Texture m_HDRMSAA; - RTV m_HDRRTVMSAA; - - // Resolved RT - Texture m_HDR; - CBV_SRV_UAV m_HDRSRV; - RTV m_HDRRTV; - - // widgets - Wireframe m_wireframe; - WireframeBox m_wireframeBox; - - std::vector<TimeStamp> m_TimeStamps; + Texture m_depthBufferNonMsaa; + DSV m_depthBufferNonMsaaDSV; + CBV_SRV_UAV m_depthBufferNonMsaaSRV; + + Texture m_normalBufferNonMsaa; + RTV m_normalBufferNonMsaaRTV; + CBV_SRV_UAV m_normalBufferNonMsaaSRV; + + // depth buffer + Texture m_depthBuffer; + DSV m_depthBufferDSV; + CBV_SRV_UAV m_depthBufferSRV; + + // TAA buffer + Texture m_taaBuffer; + CBV_SRV_UAV m_taaBufferSRV; + CBV_SRV_UAV m_taaBufferUAV; + CBV_SRV_UAV m_taaInputsSRV; + Texture m_historyBuffer; + RTV m_historyBufferRTV; + + // shadowmaps + Texture m_shadowMap; + DSV m_shadowMapDSV; + CBV_SRV_UAV m_shadowMapSRV; + + // MSAA RT + Texture m_hdrMSAA; + RTV m_hdrRTVMSAA; + + // Resolved RT + Texture m_hdr; + CBV_SRV_UAV m_hdrSRV; + RTV m_hdrRTV; + + // widgets + Wireframe m_wireframe; + WireframeBox m_wireframeBox; + + std::vector<TimeStamp> m_timeStamps; }; diff --git a/sample/src/VK/CMakeLists.txt b/sample/src/VK/CMakeLists.txt index ece063b..583084c 100644 --- a/sample/src/VK/CMakeLists.txt +++ b/sample/src/VK/CMakeLists.txt @@ -4,14 +4,16 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/../../common.cmake) add_compile_options(/MP) set(sources - FFX_CACAO_Sample.cpp - FFX_CACAO_Sample.h + Sample.cpp + Sample.h SampleRenderer.cpp SampleRenderer.h ../../../ffx-cacao/src/ffx_cacao_defines.h ../../../ffx-cacao/src/ffx_cacao.cpp ../../../ffx-cacao/inc/ffx_cacao.h - ../Common/FFX_CACAO_Common.h + ../../../ffx-cacao/src/ffx_cacao_impl.cpp + ../../../ffx-cacao/inc/ffx_cacao_impl.h + ../Common/Common.h stdafx.cpp stdafx.h) @@ -21,7 +23,7 @@ set(shaders ${CMAKE_CURRENT_SOURCE_DIR}/Apply_CACAO_Direct.glsl) set(config - ${CMAKE_CURRENT_SOURCE_DIR}/../Common/FFX_CACAO_Sample.json + ${CMAKE_CURRENT_SOURCE_DIR}/../Common/SampleSettings.json ) copyCommand("${shaders}" ${CMAKE_HOME_DIRECTORY}/bin/ShaderLibVK) diff --git a/sample/src/VK/FFX_CACAO_Sample.h b/sample/src/VK/FFX_CACAO_Sample.h deleted file mode 100644 index 1b7c9ce..0000000 --- a/sample/src/VK/FFX_CACAO_Sample.h +++ /dev/null @@ -1,96 +0,0 @@ -// AMD SampleVK sample code -// -// Copyright(c) 2018 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 "SampleRenderer.h" - -// -// This is the main class, it manages the state of the sample and does all the high level work without touching the GPU directly. -// This class uses the GPU via the the SampleRenderer class. We would have a SampleRenderer instance for each GPU. -// -// This class takes care of: -// -// - loading a scene (just the CPU data) -// - updating the camera -// - keeping track of time -// - handling the keyboard -// - updating the animation -// - building the UI (but do not renders it) -// - uses the SampleRenderer to update all the state to the GPU and do the rendering -// - -class FfxCacaoSample : public FrameworkWindows -{ -public: - FfxCacaoSample(LPCSTR name); - void OnCreate(HWND hWnd); - void OnDestroy(); - void BuildUI(); - void OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint32_t* pHeight, bool *pbFullScreen); - void OnRender(); - bool OnEvent(MSG msg); - void OnResize(uint32_t Width, uint32_t Height) { OnResize(Width, Height, DISPLAYMODE_SDR, false); } - void OnResize(uint32_t Width, uint32_t Height, DisplayModes displayMode, bool force); - void SetFullScreen(bool fullscreen); - -private: - Device m_device; - SwapChain m_swapChain; - - DisplayModes m_currentDisplayMode; - std::vector<DisplayModes> m_displayModesAvailable; - std::vector<const char *> m_displayModesNamesAvailable; - - GLTFCommon *m_pGltfLoader = NULL; - bool m_loadingScene = false; - - SampleRenderer *m_Node = NULL; - SampleRenderer::State m_state; - - float m_distance; - float m_roll; - float m_pitch; - - float m_microsecondsPerGpuTick; - float m_time; // WallClock in seconds. - double m_lastFrameTime; - float m_timeStep = 0; - int m_cameraControlSelected = 0; - - // json config file - json m_jsonConfigFile; - std::vector<std::string> m_sceneNames; - int m_activeScene; - int m_activeCamera; - bool m_isCpuValidationLayerEnabled; - bool m_isGpuValidationLayerEnabled; - - bool m_vsyncEnabled = false; - bool m_bPlay; - bool m_requiresLoad = true; - int m_presetIndex = 3; - -#ifdef FFX_CACAO_ENABLE_PROFILING - char m_benchmarkFilename[1024]; - bool m_isBenchmarking; - uint32_t m_benchmarkScreenWidth; - uint32_t m_benchmarkScreenHeight; - uint32_t m_benchmarkWarmUpFramesToRun; -#endif -}; \ No newline at end of file diff --git a/sample/src/VK/FFX_CACAO_Sample.cpp b/sample/src/VK/Sample.cpp similarity index 57% rename from sample/src/VK/FFX_CACAO_Sample.cpp rename to sample/src/VK/Sample.cpp index 2fc5a38..3924664 100644 --- a/sample/src/VK/FFX_CACAO_Sample.cpp +++ b/sample/src/VK/Sample.cpp @@ -1,6 +1,6 @@ // AMD SampleVK sample code -// -// Copyright(c) 2018 Advanced Micro Devices, Inc.All rights reserved. +// +// 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 @@ -19,17 +19,17 @@ #include "stdafx.h" -#include "FFX_CACAO_Sample.h" -#include "FFX_CACAO_Common.h" +#include "Sample.h" +#include "Common.h" -FfxCacaoSample::FfxCacaoSample(LPCSTR name) : FrameworkWindows(name) +Sample::Sample(LPCSTR name) : FrameworkWindows(name) { - m_lastFrameTime = MillisecondsNow(); - m_time = 0; - m_bPlay = true; + m_lastFrameTime = MillisecondsNow(); + m_time = 0; + m_bPlay = true; - m_pGltfLoader = NULL; - m_currentDisplayMode = DISPLAYMODE_SDR; + m_pGltfLoader = NULL; + m_currentDisplayMode = DISPLAYMODE_SDR; } //-------------------------------------------------------------------------------------- @@ -37,7 +37,7 @@ FfxCacaoSample::FfxCacaoSample(LPCSTR name) : FrameworkWindows(name) // OnParseCommandLine // //-------------------------------------------------------------------------------------- -void FfxCacaoSample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint32_t* pHeight, bool *pbFullScreen) +void Sample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint32_t* pHeight, bool *pbFullScreen) { // set some default values *pWidth = 1920; @@ -68,7 +68,7 @@ void FfxCacaoSample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint3 // read config file (and override values from commandline if so) // { - std::ifstream f("FFX_CACAO_Sample.json"); + std::ifstream f("SampleSettings.json"); if (!f) { MessageBox(NULL, "Config file not found!\n", "Cauldron Panic!", MB_ICONERROR); @@ -89,7 +89,7 @@ void FfxCacaoSample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint3 json globals = m_jsonConfigFile["globals"]; process(globals); - + // get the list of scenes for (const auto & scene : m_jsonConfigFile["scenes"]) m_sceneNames.push_back(scene["name"]); @@ -113,18 +113,12 @@ void FfxCacaoSample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint3 #ifdef FFX_CACAO_ENABLE_PROFILING if (m_isBenchmarking) { -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION bool downsampled = FFX_CACAO_PRESETS[m_presetIndex].useDownsampledSsao; -#endif uint32_t quality = FFX_CACAO_PRESETS[m_presetIndex].settings.qualityLevel; m_benchmarkScreenWidth = *pWidth; m_benchmarkScreenHeight = *pHeight; m_benchmarkWarmUpFramesToRun = 100; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION snprintf(m_benchmarkFilename, _countof(m_benchmarkFilename), "FFX_CACAO_Vulkan_Benchmark_%s_%ux%u_Q%u.csv", downsampled ? "downsampled" : "native", *pWidth, *pHeight, quality); -#else - snprintf(m_benchmarkFilename, _countof(m_benchmarkFilename), "FFX_CACAO_Vulkan_Benchmark_downsampled_%ux%u_Q%u.csv", *pWidth, *pHeight, quality); -#endif m_vsyncEnabled = false; m_isGpuValidationLayerEnabled = false; m_isCpuValidationLayerEnabled = false; @@ -138,60 +132,58 @@ void FfxCacaoSample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint3 // OnCreate // //-------------------------------------------------------------------------------------- -void FfxCacaoSample::OnCreate(HWND hWnd) +void Sample::OnCreate(HWND hWnd) { - // Create Device - // - m_device.OnCreate("myapp", "myEngine", m_isCpuValidationLayerEnabled, m_isGpuValidationLayerEnabled, hWnd); - m_device.CreatePipelineCache(); - + // Create Device + // + m_device.OnCreate("FfxCacaoSample", "Cauldron", m_isCpuValidationLayerEnabled, m_isGpuValidationLayerEnabled, hWnd); + m_device.CreatePipelineCache(); + VkPhysicalDeviceProperties physicalDeviceProperties; vkGetPhysicalDeviceProperties(m_device.GetPhysicalDevice(), &physicalDeviceProperties); m_microsecondsPerGpuTick = 1e-3f * physicalDeviceProperties.limits.timestampPeriod; - //init the shader compiler + //init the shader compiler InitDirectXCompiler(); - CreateShaderCache(); + CreateShaderCache(); - // Create Swapchain - // + // Create Swapchain + // uint32_t dwNumberOfBackBuffers = 2; - m_swapChain.OnCreate(&m_device, dwNumberOfBackBuffers, hWnd); - - // Create a instance of the renderer and initialize it, we need to do that for each GPU - // - m_Node = new SampleRenderer(); - m_Node->OnCreate(&m_device, &m_swapChain); - - // init GUI (non gfx stuff) - // - ImGUI_Init((void *)hWnd); - - // Init Camera, looking at the origin - // - m_roll = 0.0f; - m_pitch = 0.0f; - m_distance = 3.5f; - - // init GUI state - m_state.toneMapper = 0; - m_state.m_useTAA = false; // no TAA in VK - m_state.skyDomeType = 0; - m_state.exposure = 1.0f; - m_state.iblFactor = 2.0f; - m_state.emmisiveFactor = 1.0f; - m_state.bDrawLightFrustum = false; - m_state.bDrawBoundingBoxes = false; - m_state.camera.LookAt(m_roll, m_pitch, m_distance, XMVectorSet(0, 0, 0, 0)); - - m_state.m_useCacao = true; - m_state.m_dispalyCacaoDirectly = true; - - m_state.m_cacaoSettings = FFX_CACAO_PRESETS[m_presetIndex].settings; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - m_state.m_useDownsampledSsao = FFX_CACAO_PRESETS[m_presetIndex].useDownsampledSsao; -#endif + m_swapChain.OnCreate(&m_device, dwNumberOfBackBuffers, hWnd); + + // Create a instance of the renderer and initialize it, we need to do that for each GPU + // + m_node = new SampleRenderer(); + m_node->OnCreate(&m_device, &m_swapChain); + + // init GUI (non gfx stuff) + // + ImGUI_Init((void *)hWnd); + + // Init Camera, looking at the origin + // + m_roll = 0.0f; + m_pitch = 0.0f; + m_distance = 3.5f; + + // init GUI state + m_state.toneMapper = 0; + m_state.useTAA = false; // no TAA in VK + m_state.skyDomeType = 0; + m_state.exposure = 1.0f; + m_state.iblFactor = 2.0f; + m_state.emmisiveFactor = 1.0f; + m_state.drawLightFrustum = false; + m_state.drawBoundingBoxes = false; + m_state.camera.LookAt(m_roll, m_pitch, m_distance, XMVectorSet(0, 0, 0, 0)); + + m_state.useCacao = true; + m_state.dispalyCacaoDirectly = true; + + m_state.cacaoSettings = FFX_CACAO_PRESETS[m_presetIndex].settings; + m_state.useDownsampledSsao = FFX_CACAO_PRESETS[m_presetIndex].useDownsampledSsao; } //-------------------------------------------------------------------------------------- @@ -199,39 +191,39 @@ void FfxCacaoSample::OnCreate(HWND hWnd) // OnDestroy // //-------------------------------------------------------------------------------------- -void FfxCacaoSample::OnDestroy() +void Sample::OnDestroy() { #ifdef FFX_CACAO_ENABLE_PROFILING m_isBenchmarking = false; #endif - ImGUI_Shutdown(); + ImGUI_Shutdown(); - m_device.GPUFlush(); + m_device.GPUFlush(); - // Fullscreen state should always be false before exiting the app. - m_swapChain.SetFullScreen(false); + // Fullscreen state should always be false before exiting the app. + m_swapChain.SetFullScreen(false); - m_Node->UnloadScene(); - m_Node->OnDestroyWindowSizeDependentResources(); - m_Node->OnDestroy(); + m_node->UnloadScene(); + m_node->OnDestroyWindowSizeDependentResources(); + m_node->OnDestroy(); - delete m_Node; + delete m_node; - m_swapChain.OnDestroyWindowSizeDependentResources(); - m_swapChain.OnDestroy(); + m_swapChain.OnDestroyWindowSizeDependentResources(); + m_swapChain.OnDestroy(); - //shut down the shader compiler - DestroyShaderCache(&m_device); + //shut down the shader compiler + DestroyShaderCache(&m_device); - if (m_pGltfLoader) - { - delete m_pGltfLoader; - m_pGltfLoader = NULL; - } + if (m_pGltfLoader) + { + delete m_pGltfLoader; + m_pGltfLoader = NULL; + } - m_device.DestroyPipelineCache(); - m_device.OnDestroy(); + m_device.DestroyPipelineCache(); + m_device.OnDestroy(); } //-------------------------------------------------------------------------------------- @@ -239,11 +231,11 @@ void FfxCacaoSample::OnDestroy() // OnEvent, win32 sends us events and we forward them to ImGUI // //-------------------------------------------------------------------------------------- -bool FfxCacaoSample::OnEvent(MSG msg) +bool Sample::OnEvent(MSG msg) { - if (ImGUI_WndProcHandler(msg.hwnd, msg.message, msg.wParam, msg.lParam)) - return true; - return true; + if (ImGUI_WndProcHandler(msg.hwnd, msg.message, msg.wParam, msg.lParam)) + return true; + return true; } //-------------------------------------------------------------------------------------- @@ -251,16 +243,16 @@ bool FfxCacaoSample::OnEvent(MSG msg) // SetFullScreen // //-------------------------------------------------------------------------------------- -void FfxCacaoSample::SetFullScreen(bool fullscreen) +void Sample::SetFullScreen(bool fullscreen) { - m_device.GPUFlush(); + m_device.GPUFlush(); if (!fullscreen) { m_currentDisplayMode = DISPLAYMODE_SDR; } - m_swapChain.SetFullScreen(fullscreen); + m_swapChain.SetFullScreen(fullscreen); } //-------------------------------------------------------------------------------------- @@ -268,7 +260,7 @@ void FfxCacaoSample::SetFullScreen(bool fullscreen) // OnResize // //-------------------------------------------------------------------------------------- -void FfxCacaoSample::OnResize(uint32_t width, uint32_t height, DisplayModes displayMode, bool force) +void Sample::OnResize(uint32_t width, uint32_t height, DisplayModes displayMode, bool force) { #ifdef FFX_CACAO_ENABLE_PROFILING if (m_isBenchmarking && !m_benchmarkWarmUpFramesToRun) @@ -281,39 +273,39 @@ void FfxCacaoSample::OnResize(uint32_t width, uint32_t height, DisplayModes disp } #endif - if (m_Width != width || m_Height != height || m_currentDisplayMode != displayMode || force) - { - // Flush GPU - // - m_device.GPUFlush(); - - // If resizing but no minimizing - // - if (m_Width > 0 && m_Height > 0) - { - if (m_Node != NULL) - { - m_Node->OnDestroyWindowSizeDependentResources(); - } - m_swapChain.OnDestroyWindowSizeDependentResources(); - } - - m_Width = width; - m_Height = height; - m_currentDisplayMode = displayMode; - - // if resizing but not minimizing the recreate it with the new size - // - if (m_Width > 0 && m_Height > 0) - { - m_swapChain.OnCreateWindowSizeDependentResources(m_Width, m_Height, m_vsyncEnabled, m_currentDisplayMode); - if (m_Node != NULL) - { - m_Node->OnCreateWindowSizeDependentResources(&m_swapChain, m_Width, m_Height); - } - } - } - m_state.camera.SetFov(XM_PI / 4, m_Width, m_Height, 0.1f, 1000.0f); + if (m_Width != width || m_Height != height || m_currentDisplayMode != displayMode || force) + { + // Flush GPU + // + m_device.GPUFlush(); + + // If resizing but no minimizing + // + if (m_Width > 0 && m_Height > 0) + { + if (m_node != NULL) + { + m_node->OnDestroyWindowSizeDependentResources(); + } + m_swapChain.OnDestroyWindowSizeDependentResources(); + } + + m_Width = width; + m_Height = height; + m_currentDisplayMode = displayMode; + + // if resizing but not minimizing the recreate it with the new size + // + if (m_Width > 0 && m_Height > 0) + { + m_swapChain.OnCreateWindowSizeDependentResources(m_Width, m_Height, m_vsyncEnabled, m_currentDisplayMode); + if (m_node != NULL) + { + m_node->OnCreateWindowSizeDependentResources(&m_swapChain, m_Width, m_Height); + } + } + } + m_state.camera.SetFov(XM_PI / 4, m_Width, m_Height, 0.1f, 1000.0f); } //-------------------------------------------------------------------------------------- @@ -321,20 +313,20 @@ void FfxCacaoSample::OnResize(uint32_t width, uint32_t height, DisplayModes disp // BuildUI, also loads the scene! // //-------------------------------------------------------------------------------------- -void FfxCacaoSample::BuildUI() +void Sample::BuildUI() { - ImGuiStyle& style = ImGui::GetStyle(); - style.FrameBorderSize = 1.0f; + ImGuiStyle& style = ImGui::GetStyle(); + style.FrameBorderSize = 1.0f; - ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSize(ImVec2(250, 700), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(250, 700), ImGuiCond_FirstUseEver); - bool opened = true; - ImGui::Begin("CACAO Sample", &opened); + bool opened = true; + ImGui::Begin("CACAO Sample", &opened); - if (ImGui::CollapsingHeader("Sample Settings", ImGuiTreeNodeFlags_DefaultOpen)) - { - ImGui::Text("Resolution : %ix%i", m_Width, m_Height); + if (ImGui::CollapsingHeader("Sample Settings", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::Text("Resolution : %ix%i", m_Width, m_Height); const char *cameraControls = "Orbit\0WASD\0"; ImGui::Combo("Camera", &m_cameraControlSelected, cameraControls); @@ -343,7 +335,7 @@ void FfxCacaoSample::BuildUI() { OnResize(m_Width, m_Height, DISPLAYMODE_SDR, true); } - } + } if (m_requiresLoad) { @@ -353,12 +345,12 @@ void FfxCacaoSample::BuildUI() // release everything and load the GLTF, just the light json data, the rest (textures and geometry) will be done in the main loop if (m_pGltfLoader != NULL) { - m_Node->UnloadScene(); - m_Node->OnDestroyWindowSizeDependentResources(); - m_Node->OnDestroy(); + m_node->UnloadScene(); + m_node->OnDestroyWindowSizeDependentResources(); + m_node->OnDestroy(); m_pGltfLoader->Unload(); - m_Node->OnCreate(&m_device, &m_swapChain); - m_Node->OnCreateWindowSizeDependentResources(&m_swapChain, m_Width, m_Height); + m_node->OnCreate(&m_device, &m_swapChain); + m_node->OnCreateWindowSizeDependentResources(&m_swapChain, m_Width, m_Height); } delete(m_pGltfLoader); @@ -374,7 +366,7 @@ void FfxCacaoSample::BuildUI() #define LOAD(j, key, val) val = j.value(key, val) // global settings - LOAD(scene, "TAA", m_state.m_useTAA); + LOAD(scene, "TAA", m_state.useTAA); LOAD(scene, "toneMapper", m_state.toneMapper); LOAD(scene, "skyDomeType", m_state.skyDomeType); LOAD(scene, "exposure", m_state.exposure); @@ -435,14 +427,12 @@ void FfxCacaoSample::BuildUI() { if (ImGui::Combo("Preset", &m_presetIndex, FFX_CACAO_PRESET_NAMES, _countof(FFX_CACAO_PRESET_NAMES)) && m_presetIndex < _countof(FFX_CACAO_PRESETS)) { - FfxCacaoPreset preset = FFX_CACAO_PRESETS[m_presetIndex]; - m_state.m_cacaoSettings = preset.settings; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - m_state.m_useDownsampledSsao = preset.useDownsampledSsao; -#endif + Preset preset = FFX_CACAO_PRESETS[m_presetIndex]; + m_state.cacaoSettings = preset.settings; + m_state.useDownsampledSsao = preset.useDownsampledSsao; } - FfxCacaoSettings *settings = &m_state.m_cacaoSettings; + FFX_CACAO_Settings *settings = &m_state.cacaoSettings; ImGui::SliderFloat("Radius", &settings->radius, 0.0f, 10.0f); ImGui::SliderFloat("Shadow Multiplier", &settings->shadowMultiplier, 0.0f, 5.0f); ImGui::SliderFloat("Shadow Power", &settings->shadowPower, 0.5f, 5.0f); @@ -453,7 +443,7 @@ void FfxCacaoSample::BuildUI() int qualityIndex = settings->qualityLevel; char *qualityLevels = "Lowest\0Low\0Medium\0High\0Highest\0" ; ImGui::Combo("Quality Level", &qualityIndex, qualityLevels); - settings->qualityLevel = (FfxCacaoQuality)qualityIndex; + settings->qualityLevel = (FFX_CACAO_Quality)qualityIndex; if (settings->qualityLevel == FFX_CACAO_QUALITY_HIGHEST) { ImGui::SliderFloat("Adaptive Quality Level", &settings->adaptiveQualityLimit, 0.5f, 1.0f); @@ -464,39 +454,33 @@ void FfxCacaoSample::BuildUI() bool generateNormals = settings->generateNormals; ImGui::Checkbox("Generate Normal Buffer From Depth Buffer", &generateNormals); settings->generateNormals = generateNormals ? FFX_CACAO_TRUE : FFX_CACAO_FALSE; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - ImGui::Checkbox("Use Downsampled SSAO", &m_state.m_useDownsampledSsao); - if (m_state.m_useDownsampledSsao) -#endif + ImGui::Checkbox("Use Downsampled SSAO", &m_state.useDownsampledSsao); + if (m_state.useDownsampledSsao) { ImGui::SliderFloat("Bilateral Sigma Squared", &settings->bilateralSigmaSquared, 0.0f, 10.0f); ImGui::SliderFloat("Bilateral Similarity Distance Sigma", &settings->bilateralSimilarityDistanceSigma, 0.1f, 1.0f); } - ImGui::Checkbox("Display FFX CACAO Output Directly", &m_state.m_dispalyCacaoDirectly); - if (!m_state.m_dispalyCacaoDirectly) + ImGui::Checkbox("Display FFX CACAO Output Directly", &m_state.dispalyCacaoDirectly); + if (!m_state.dispalyCacaoDirectly) { - ImGui::Checkbox("Use FFX CACAO", &m_state.m_useCacao); + ImGui::Checkbox("Use FFX CACAO", &m_state.useCacao); } - m_state.m_useCacao |= m_state.m_dispalyCacaoDirectly; + m_state.useCacao |= m_state.dispalyCacaoDirectly; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - if (m_presetIndex < _countof(FFX_CACAO_PRESETS) && (memcmp(&FFX_CACAO_PRESETS[m_presetIndex].settings, &m_state.m_cacaoSettings, sizeof(m_state.m_cacaoSettings)) || (FFX_CACAO_PRESETS[m_presetIndex].useDownsampledSsao != m_state.m_useDownsampledSsao))) -#else - if (m_presetIndex < _countof(FFX_CACAO_PRESETS) && memcmp(&FFX_CACAO_PRESETS[m_presetIndex].settings, &m_state.m_cacaoSettings, sizeof(m_state.m_cacaoSettings))) -#endif + if (m_presetIndex < _countof(FFX_CACAO_PRESETS) && (memcmp(&FFX_CACAO_PRESETS[m_presetIndex].settings, &m_state.cacaoSettings, sizeof(m_state.cacaoSettings)) || (FFX_CACAO_PRESETS[m_presetIndex].useDownsampledSsao != m_state.useDownsampledSsao))) { m_presetIndex = _countof(FFX_CACAO_PRESETS); } } #ifdef FFX_CACAO_ENABLE_PROFILING - if (m_state.m_useCacao && !m_vsyncEnabled) + if (m_state.useCacao && !m_vsyncEnabled && !m_isCpuValidationLayerEnabled && !m_isGpuValidationLayerEnabled) { if (ImGui::CollapsingHeader("Profiler", ImGuiTreeNodeFlags_DefaultOpen)) { - FfxCacaoDetailedTiming timings = {}; - m_Node->GetCacaoTimingValues(&m_state, &timings); + FFX_CACAO_DetailedTiming timings = {}; + m_node->GetCacaoTimingValues(&m_state, &timings); for (uint32_t i = 0; i < timings.numTimestamps; ++i) { const char *name = timings.timestamps[i].label; @@ -508,58 +492,58 @@ void FfxCacaoSample::BuildUI() } else { - ImGui::CollapsingHeader("Profiler Disabled (enable CACAO and turn off vsync)"); + ImGui::CollapsingHeader("Profiler Disabled (enable CACAO and turn off vsync and validation)"); } #endif - ImGui::End(); - - // Sets Camera based on UI selection (WASD, Orbit or any of the GLTF cameras) - // - ImGuiIO& io = ImGui::GetIO(); - { - //If the mouse was not used by the GUI then it's for the camera - // - if (io.WantCaptureMouse) - { - io.MouseDelta.x = 0; - io.MouseDelta.y = 0; - io.MouseWheel = 0; - } - else if ((io.KeyCtrl == false) && (io.MouseDown[0] == true)) - { - m_roll -= io.MouseDelta.x / 100.f; - m_pitch += io.MouseDelta.y / 100.f; - } - - // Choose camera movement depending on setting - // - if (m_cameraControlSelected == 0) - { - // Orbiting - // - m_distance -= (float)io.MouseWheel / 3.0f; - m_distance = std::max<float>(m_distance, 0.1f); - - bool panning = (io.KeyCtrl == true) && (io.MouseDown[0] == true); - - m_state.camera.UpdateCameraPolar(m_roll, m_pitch, panning ? -io.MouseDelta.x / 100.0f : 0.0f, panning ? io.MouseDelta.y / 100.0f : 0.0f, m_distance); - } - else if (m_cameraControlSelected == 1) - { - // WASD - // - m_state.camera.UpdateCameraWASD(m_roll, m_pitch, io.KeysDown, io.DeltaTime); - } - else if (m_cameraControlSelected > 1) - { - // Use a camera from the GLTF - // - m_pGltfLoader->GetCamera(m_cameraControlSelected - 2, &m_state.camera); - m_roll = m_state.camera.GetYaw(); - m_pitch = m_state.camera.GetPitch(); - } - } + ImGui::End(); + + // Sets Camera based on UI selection (WASD, Orbit or any of the GLTF cameras) + // + ImGuiIO& io = ImGui::GetIO(); + { + //If the mouse was not used by the GUI then it's for the camera + // + if (io.WantCaptureMouse) + { + io.MouseDelta.x = 0; + io.MouseDelta.y = 0; + io.MouseWheel = 0; + } + else if ((io.KeyCtrl == false) && (io.MouseDown[0] == true)) + { + m_roll -= io.MouseDelta.x / 100.f; + m_pitch += io.MouseDelta.y / 100.f; + } + + // Choose camera movement depending on setting + // + if (m_cameraControlSelected == 0) + { + // Orbiting + // + m_distance -= (float)io.MouseWheel / 3.0f; + m_distance = std::max<float>(m_distance, 0.1f); + + bool panning = (io.KeyCtrl == true) && (io.MouseDown[0] == true); + + m_state.camera.UpdateCameraPolar(m_roll, m_pitch, panning ? -io.MouseDelta.x / 100.0f : 0.0f, panning ? io.MouseDelta.y / 100.0f : 0.0f, m_distance); + } + else if (m_cameraControlSelected == 1) + { + // WASD + // + m_state.camera.UpdateCameraWASD(m_roll, m_pitch, io.KeysDown, io.DeltaTime); + } + else if (m_cameraControlSelected > 1) + { + // Use a camera from the GLTF + // + m_pGltfLoader->GetCamera(m_cameraControlSelected - 2, &m_state.camera); + m_roll = m_state.camera.GetYaw(); + m_pitch = m_state.camera.GetPitch(); + } + } } //-------------------------------------------------------------------------------------- @@ -567,39 +551,39 @@ void FfxCacaoSample::BuildUI() // OnRender, updates the state from the UI, animates, transforms and renders the scene // //-------------------------------------------------------------------------------------- -void FfxCacaoSample::OnRender() +void Sample::OnRender() { - // Get timings - // - double timeNow = MillisecondsNow(); - float deltaTime = (m_timeStep == 0.0f) ? (float)(timeNow - m_lastFrameTime) : m_timeStep; - m_lastFrameTime = timeNow; - - // Set animation time - // - if (m_bPlay) - { - m_time += (float)deltaTime / 1000.0f; - } - - ImGUI_UpdateIO(); - ImGui::NewFrame(); - - if (m_loadingScene) - { - // the scene loads in chuncks, that way we can show a progress bar - static int loadingStage = 0; - loadingStage = m_Node->LoadScene(m_pGltfLoader, loadingStage); - if (loadingStage == 0) - { - m_time = 0; - m_loadingScene = false; - } - } + // Get timings + // + double timeNow = MillisecondsNow(); + float deltaTime = (m_timeStep == 0.0f) ? (float)(timeNow - m_lastFrameTime) : m_timeStep; + m_lastFrameTime = timeNow; + + // Set animation time + // + if (m_bPlay) + { + m_time += (float)deltaTime / 1000.0f; + } + + ImGUI_UpdateIO(); + ImGui::NewFrame(); + + if (m_loadingScene) + { + // the scene loads in chuncks, that way we can show a progress bar + static int loadingStage = 0; + loadingStage = m_node->LoadScene(m_pGltfLoader, loadingStage); + if (loadingStage == 0) + { + m_time = 0; + m_loadingScene = false; + } + } #ifdef FFX_CACAO_ENABLE_PROFILING - else if (m_pGltfLoader && m_isBenchmarking) - { - // benchmarking takes control of the time, and exits the app when the animation is done + else if (m_pGltfLoader && m_isBenchmarking) + { + // benchmarking takes control of the time, and exits the app when the animation is done if (m_benchmarkWarmUpFramesToRun) { @@ -613,8 +597,8 @@ void FfxCacaoSample::OnRender() exit(0); } - FfxCacaoDetailedTiming timings = {}; - m_Node->GetCacaoTimingValues(&m_state, &timings); + FFX_CACAO_DetailedTiming timings = {}; + m_node->GetCacaoTimingValues(&m_state, &timings); if (timings.numTimestamps) { @@ -633,35 +617,35 @@ void FfxCacaoSample::OnRender() m_time = BenchmarkLoop(timestamps, &m_state.camera, (const std::string**)&pFilename); } } - } + } #endif - else - { - // Build the UI. Note that the rendering of the UI happens later. - BuildUI(); + else + { + // Build the UI. Note that the rendering of the UI happens later. + BuildUI(); if (m_bPlay) { m_time += (float)deltaTime / 1000.0f; } - } + } - // Animate and transform the scene - // - if (m_pGltfLoader) - { - m_pGltfLoader->SetAnimationTime(0, m_time); - m_pGltfLoader->TransformScene(0, XMMatrixIdentity()); - } + // Animate and transform the scene + // + if (m_pGltfLoader) + { + m_pGltfLoader->SetAnimationTime(0, m_time); + m_pGltfLoader->TransformScene(0, XMMatrixIdentity()); + } - m_state.time = m_time; + m_state.time = m_time; - // Do Render frame using AFR - // - m_Node->OnRender(&m_state, &m_swapChain); + // Do Render frame using AFR + // + m_node->OnRender(&m_state, &m_swapChain); - m_swapChain.Present(); + m_swapChain.Present(); } @@ -671,13 +655,13 @@ void FfxCacaoSample::OnRender() // //-------------------------------------------------------------------------------------- int WINAPI WinMain(HINSTANCE hInstance, - HINSTANCE hPrevInstance, - LPSTR lpCmdLine, - int nCmdShow) + HINSTANCE hPrevInstance, + LPSTR lpCmdLine, + int nCmdShow) { - LPCSTR Name = "FFX CACAO Vulkan Sample v1.0"; + LPCSTR Name = "FFX CACAO Vulkan Sample v1.2"; - // create new Vulkan sample - return RunFramework(hInstance, lpCmdLine, nCmdShow, new FfxCacaoSample(Name)); + // create new Vulkan sample + return RunFramework(hInstance, lpCmdLine, nCmdShow, new Sample(Name)); } diff --git a/sample/src/VK/Sample.h b/sample/src/VK/Sample.h new file mode 100644 index 0000000..560276f --- /dev/null +++ b/sample/src/VK/Sample.h @@ -0,0 +1,96 @@ +// AMD SampleVK sample code +// +// 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 "SampleRenderer.h" + +// +// This is the main class, it manages the state of the sample and does all the high level work without touching the GPU directly. +// This class uses the GPU via the the SampleRenderer class. We would have a SampleRenderer instance for each GPU. +// +// This class takes care of: +// +// - loading a scene (just the CPU data) +// - updating the camera +// - keeping track of time +// - handling the keyboard +// - updating the animation +// - building the UI (but do not renders it) +// - uses the SampleRenderer to update all the state to the GPU and do the rendering +// + +class Sample : public FrameworkWindows +{ +public: + Sample(LPCSTR name); + void OnCreate(HWND hWnd); + void OnDestroy(); + void BuildUI(); + void OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint32_t* pHeight, bool *pbFullScreen); + void OnRender(); + bool OnEvent(MSG msg); + void OnResize(uint32_t Width, uint32_t Height) { OnResize(Width, Height, DISPLAYMODE_SDR, false); } + void OnResize(uint32_t Width, uint32_t Height, DisplayModes displayMode, bool force); + void SetFullScreen(bool fullscreen); + +private: + Device m_device; + SwapChain m_swapChain; + + DisplayModes m_currentDisplayMode; + std::vector<DisplayModes> m_displayModesAvailable; + std::vector<const char *> m_displayModesNamesAvailable; + + GLTFCommon *m_pGltfLoader = NULL; + bool m_loadingScene = false; + + SampleRenderer *m_node = NULL; + SampleRenderer::State m_state; + + float m_distance; + float m_roll; + float m_pitch; + + float m_microsecondsPerGpuTick; + float m_time; // WallClock in seconds. + double m_lastFrameTime; + float m_timeStep = 0; + int m_cameraControlSelected = 0; + + // json config file + json m_jsonConfigFile; + std::vector<std::string> m_sceneNames; + int m_activeScene; + int m_activeCamera; + bool m_isCpuValidationLayerEnabled; + bool m_isGpuValidationLayerEnabled; + + bool m_vsyncEnabled = false; + bool m_bPlay; + bool m_requiresLoad = true; + int m_presetIndex = 3; + +#ifdef FFX_CACAO_ENABLE_PROFILING + char m_benchmarkFilename[1024]; + bool m_isBenchmarking; + uint32_t m_benchmarkScreenWidth; + uint32_t m_benchmarkScreenHeight; + uint32_t m_benchmarkWarmUpFramesToRun; +#endif +}; diff --git a/sample/src/VK/SampleRenderer.cpp b/sample/src/VK/SampleRenderer.cpp index ae4f518..f5c36d8 100644 --- a/sample/src/VK/SampleRenderer.cpp +++ b/sample/src/VK/SampleRenderer.cpp @@ -1,6 +1,6 @@ // AMD SampleVK sample code // -// Copyright(c) 2018 Advanced Micro Devices, Inc.All rights reserved. +// 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 @@ -28,75 +28,75 @@ //-------------------------------------------------------------------------------------- void SampleRenderer::OnCreate(Device *pDevice, SwapChain *pSwapChain) { - m_pDevice = pDevice; - - // Initialize helpers - - // Create all the heaps for the resources views - const uint32_t cbvDescriptorCount = 2000; - const uint32_t srvDescriptorCount = 2000; - const uint32_t uavDescriptorCount = 10; - const uint32_t samplerDescriptorCount = 20; - m_resourceViewHeaps.OnCreate(pDevice, cbvDescriptorCount, srvDescriptorCount, uavDescriptorCount, samplerDescriptorCount); - - // Create a commandlist ring for the Direct queue - uint32_t commandListsPerBackBuffer = 8; - m_CommandListRing.OnCreate(pDevice, backBufferCount, commandListsPerBackBuffer); - - // Create a 'dynamic' constant buffer - const uint32_t constantBuffersMemSize = 20 * 1024 * 1024; - m_ConstantBufferRing.OnCreate(pDevice, backBufferCount, constantBuffersMemSize, "Uniforms"); - - // Create a 'static' pool for vertices and indices - const uint32_t staticGeometryMemSize = 128 * 1024 * 1024; - const uint32_t systemGeometryMemSize = 32 * 1024; - m_VidMemBufferPool.OnCreate(pDevice, staticGeometryMemSize, USE_VID_MEM, "StaticGeom"); - m_SysMemBufferPool.OnCreate(pDevice, systemGeometryMemSize, false, "PostProcGeom"); - - // initialize the GPU time stamps module - m_GPUTimer.OnCreate(pDevice, backBufferCount); - - // Quick helper to upload resources, it has it's own commandList and uses suballocation. - // for 4K textures we'll need 100Megs - const uint32_t uploadHeapMemSize = 1000 * 1024 * 1024; - m_UploadHeap.OnCreate(pDevice, staticGeometryMemSize); // initialize an upload heap (uses suballocation for faster results) - - // Create a 2Kx2K Shadowmap atlas to hold 4 cascades/spotlights - m_shadowMap.InitDepthStencil(m_pDevice, 2 * 1024, 2 * 1024, VK_FORMAT_D32_SFLOAT, VK_SAMPLE_COUNT_1_BIT, "ShadowMap"); - m_shadowMap.CreateSRV(&m_shadowMapSRV); - m_shadowMap.CreateDSV(&m_shadowMapDSV); - - // Create render pass shadow, will clear contents - // - { - VkAttachmentDescription depthAttachments; - AttachClearBeforeUse(m_shadowMap.GetFormat(), 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); - - // Create frame buffer, its size is now window dependant so we can do this here. - // - VkImageView attachmentViews[1] = { m_shadowMapDSV }; - VkFramebufferCreateInfo fb_info = {}; - fb_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; - fb_info.pNext = NULL; - fb_info.renderPass = m_render_pass_shadow; - fb_info.attachmentCount = 1; - fb_info.pAttachments = attachmentViews; - fb_info.width = m_shadowMap.GetWidth(); - fb_info.height = m_shadowMap.GetHeight(); - fb_info.layers = 1; - VkResult res = vkCreateFramebuffer(m_pDevice->GetDevice(), &fb_info, NULL, &m_pFrameBuffer_shadow); - assert(res == VK_SUCCESS); - } - - // Create HDR MSAA render pass + clear, for the sky, PBR and Wireframe passes - // - { - VkAttachmentDescription colorAttachment, depthAttachment; - AttachClearBeforeUse(VK_FORMAT_R16G16B16A16_SFLOAT, VK_SAMPLE_COUNT_4_BIT, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, &colorAttachment); + m_pDevice = pDevice; + + // Initialize helpers + + // Create all the heaps for the resources views + const uint32_t cbvDescriptorCount = 2000; + const uint32_t srvDescriptorCount = 2000; + const uint32_t uavDescriptorCount = 10; + const uint32_t samplerDescriptorCount = 20; + m_resourceViewHeaps.OnCreate(pDevice, cbvDescriptorCount, srvDescriptorCount, uavDescriptorCount, samplerDescriptorCount); + + // Create a commandlist ring for the Direct queue + uint32_t commandListsPerBackBuffer = 8; + m_commandListRing.OnCreate(pDevice, backBufferCount, commandListsPerBackBuffer); + + // Create a 'dynamic' constant buffer + const uint32_t constantBuffersMemSize = 20 * 1024 * 1024; + m_constantBufferRing.OnCreate(pDevice, backBufferCount, constantBuffersMemSize, "Uniforms"); + + // Create a 'static' pool for vertices and indices + const uint32_t staticGeometryMemSize = 128 * 1024 * 1024; + const uint32_t systemGeometryMemSize = 32 * 1024; + m_vidMemBufferPool.OnCreate(pDevice, staticGeometryMemSize, USE_VID_MEM, "StaticGeom"); + m_sysMemBufferPool.OnCreate(pDevice, systemGeometryMemSize, false, "PostProcGeom"); + + // initialize the GPU time stamps module + m_gpuTimer.OnCreate(pDevice, backBufferCount); + + // Quick helper to upload resources, it has it's own commandList and uses suballocation. + // for 4K textures we'll need 100Megs + const uint32_t uploadHeapMemSize = 1000 * 1024 * 1024; + m_uploadHeap.OnCreate(pDevice, staticGeometryMemSize); // initialize an upload heap (uses suballocation for faster results) + + // Create a 2Kx2K Shadowmap atlas to hold 4 cascades/spotlights + m_shadowMap.InitDepthStencil(m_pDevice, 2 * 1024, 2 * 1024, VK_FORMAT_D32_SFLOAT, VK_SAMPLE_COUNT_1_BIT, "ShadowMap"); + m_shadowMap.CreateSRV(&m_shadowMapSRV); + m_shadowMap.CreateDSV(&m_shadowMapDSV); + + // Create render pass shadow, will clear contents + // + { + VkAttachmentDescription depthAttachments; + AttachClearBeforeUse(m_shadowMap.GetFormat(), VK_SAMPLE_COUNT_1_BIT, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, &depthAttachments); + m_renderPassShadow = CreateRenderPassOptimal(m_pDevice->GetDevice(), 0, NULL, &depthAttachments); + + // Create frame buffer, its size is now window dependant so we can do this here. + // + VkImageView attachmentViews[1] = { m_shadowMapDSV }; + VkFramebufferCreateInfo fb_info = {}; + fb_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fb_info.pNext = NULL; + fb_info.renderPass = m_renderPassShadow; + fb_info.attachmentCount = 1; + fb_info.pAttachments = attachmentViews; + fb_info.width = m_shadowMap.GetWidth(); + fb_info.height = m_shadowMap.GetHeight(); + fb_info.layers = 1; + VkResult res = vkCreateFramebuffer(m_pDevice->GetDevice(), &fb_info, NULL, &m_pFrameBufferShadow); + assert(res == VK_SUCCESS); + } + + // Create HDR MSAA render pass + clear, for the sky, PBR and Wireframe passes + // + { + VkAttachmentDescription colorAttachment, depthAttachment; + AttachClearBeforeUse(VK_FORMAT_R16G16B16A16_SFLOAT, VK_SAMPLE_COUNT_4_BIT, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, &colorAttachment); AttachClearBeforeUse(VK_FORMAT_D32_SFLOAT, VK_SAMPLE_COUNT_4_BIT, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, &depthAttachment); - m_render_pass_HDR_MSAA = CreateRenderPassOptimal(m_pDevice->GetDevice(), 1, &colorAttachment, &depthAttachment); - } + m_renderPassHDRMSAA = CreateRenderPassOptimal(m_pDevice->GetDevice(), 1, &colorAttachment, &depthAttachment); + } // Create non msaa render pass // @@ -104,43 +104,37 @@ void SampleRenderer::OnCreate(Device *pDevice, SwapChain *pSwapChain) VkAttachmentDescription colorAttachment, depthAttachment; AttachClearBeforeUse(VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_SAMPLE_COUNT_1_BIT, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, &colorAttachment); AttachClearBeforeUse(VK_FORMAT_D32_SFLOAT, VK_SAMPLE_COUNT_1_BIT, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, &depthAttachment); - m_render_pass_non_msaa = CreateRenderPassOptimal(m_pDevice->GetDevice(), 1, &colorAttachment, &depthAttachment); + m_renderPassNonMSAA = CreateRenderPassOptimal(m_pDevice->GetDevice(), 1, &colorAttachment, &depthAttachment); } - // Create HDR render pass, for the GUI - // - { - VkAttachmentDescription colorAttachment; - AttachBlending(VK_FORMAT_R16G16B16A16_SFLOAT, VK_SAMPLE_COUNT_1_BIT, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, &colorAttachment); - m_render_pass_PBR_HDR = CreateRenderPassOptimal(m_pDevice->GetDevice(), 1, &colorAttachment, NULL); - } - - m_skyDome.OnCreate(pDevice, m_render_pass_HDR_MSAA, &m_UploadHeap, VK_FORMAT_R16G16B16A16_SFLOAT, &m_resourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool, "..\\media\\envmaps\\papermill\\diffuse.dds", "..\\media\\envmaps\\papermill\\specular.dds", VK_SAMPLE_COUNT_4_BIT); - m_skyDomeProc.OnCreate(pDevice, m_render_pass_HDR_MSAA, &m_UploadHeap, VK_FORMAT_R16G16B16A16_SFLOAT, &m_resourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool, VK_SAMPLE_COUNT_4_BIT); - m_wireframe.OnCreate(pDevice, m_render_pass_HDR_MSAA, &m_resourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool, VK_SAMPLE_COUNT_4_BIT); - m_wireframeBox.OnCreate(pDevice, &m_resourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool); - m_downSample.OnCreate(pDevice, &m_resourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool, VK_FORMAT_R16G16B16A16_SFLOAT); - m_bloom.OnCreate(pDevice, &m_resourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool, VK_FORMAT_R16G16B16A16_SFLOAT); - - // Create tonemapping pass - m_toneMappingCS.OnCreate(pDevice, &m_resourceViewHeaps, &m_ConstantBufferRing); - m_toneMappingPS.OnCreate(m_pDevice, pSwapChain->GetRenderPass(), &m_resourceViewHeaps, &m_VidMemBufferPool, &m_ConstantBufferRing); - m_colorConversionPS.OnCreate(pDevice, pSwapChain->GetRenderPass(), &m_resourceViewHeaps, &m_VidMemBufferPool, &m_ConstantBufferRing); - - // Initialize UI rendering resources - m_ImGUI.OnCreate(m_pDevice, pSwapChain->GetRenderPass(), &m_UploadHeap, &m_ConstantBufferRing); - - // Make sure upload heap has finished uploading before continuing -#if (USE_VID_MEM==true) - m_VidMemBufferPool.UploadData(m_UploadHeap.GetCommandList()); - m_UploadHeap.FlushAndFinish(); -#endif + // Create HDR render pass, for the GUI + // + { + VkAttachmentDescription colorAttachment; + AttachBlending(VK_FORMAT_R16G16B16A16_SFLOAT, VK_SAMPLE_COUNT_1_BIT, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, &colorAttachment); + m_renderPassPBRHDR = CreateRenderPassOptimal(m_pDevice->GetDevice(), 1, &colorAttachment, NULL); + } + + m_skyDome.OnCreate(pDevice, m_renderPassHDRMSAA, &m_uploadHeap, VK_FORMAT_R16G16B16A16_SFLOAT, &m_resourceViewHeaps, &m_constantBufferRing, &m_vidMemBufferPool, "..\\media\\envmaps\\papermill\\diffuse.dds", "..\\media\\envmaps\\papermill\\specular.dds", VK_SAMPLE_COUNT_4_BIT); + m_skyDomeProc.OnCreate(pDevice, m_renderPassHDRMSAA, &m_uploadHeap, VK_FORMAT_R16G16B16A16_SFLOAT, &m_resourceViewHeaps, &m_constantBufferRing, &m_vidMemBufferPool, VK_SAMPLE_COUNT_4_BIT); + m_wireframe.OnCreate(pDevice, m_renderPassHDRMSAA, &m_resourceViewHeaps, &m_constantBufferRing, &m_vidMemBufferPool, VK_SAMPLE_COUNT_4_BIT); + m_wireframeBox.OnCreate(pDevice, &m_resourceViewHeaps, &m_constantBufferRing, &m_vidMemBufferPool); + m_downSample.OnCreate(pDevice, &m_resourceViewHeaps, &m_constantBufferRing, &m_vidMemBufferPool, VK_FORMAT_R16G16B16A16_SFLOAT); + m_bloom.OnCreate(pDevice, &m_resourceViewHeaps, &m_constantBufferRing, &m_vidMemBufferPool, VK_FORMAT_R16G16B16A16_SFLOAT); + + // Create tonemapping pass + m_toneMappingCS.OnCreate(pDevice, &m_resourceViewHeaps, &m_constantBufferRing); + m_toneMappingPS.OnCreate(m_pDevice, pSwapChain->GetRenderPass(), &m_resourceViewHeaps, &m_vidMemBufferPool, &m_constantBufferRing); + m_colorConversionPS.OnCreate(pDevice, pSwapChain->GetRenderPass(), &m_resourceViewHeaps, &m_vidMemBufferPool, &m_constantBufferRing); + + // Initialize UI rendering resources + m_imGUI.OnCreate(m_pDevice, pSwapChain->GetRenderPass(), &m_uploadHeap, &m_constantBufferRing); // ======================================================================= // CACAO - size_t cacaoContextSize = ffxCacaoVkGetContextSize(); - FfxCacaoVkCreateInfo info = {}; + size_t cacaoContextSize = FFX_CACAO_VkGetContextSize(); + FFX_CACAO_VkCreateInfo info = {}; info.physicalDevice = pDevice->GetPhysicalDevice(); info.device = pDevice->GetDevice(); info.flags = FFX_CACAO_VK_CREATE_USE_DEBUG_MARKERS | FFX_CACAO_VK_CREATE_NAME_OBJECTS; @@ -148,12 +142,10 @@ void SampleRenderer::OnCreate(Device *pDevice, SwapChain *pSwapChain) { info.flags |= FFX_CACAO_VK_CREATE_USE_16_BIT; } -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - m_cacaoContextNative = (FfxCacaoVkContext*)malloc(cacaoContextSize); - ffxCacaoVkInitContext(m_cacaoContextNative, &info); -#endif - m_cacaoContextDownsampled = (FfxCacaoVkContext*)malloc(cacaoContextSize); - ffxCacaoVkInitContext(m_cacaoContextDownsampled, &info); + m_cacaoContextNative = (FFX_CACAO_VkContext*)malloc(cacaoContextSize); + FFX_CACAO_VkInitContext(m_cacaoContextNative, &info); + m_cacaoContextDownsampled = (FFX_CACAO_VkContext*)malloc(cacaoContextSize); + FFX_CACAO_VkInitContext(m_cacaoContextDownsampled, &info); // create direct output PS descriptor set layout { @@ -171,7 +163,7 @@ void SampleRenderer::OnCreate(Device *pDevice, SwapChain *pSwapChain) bindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; bindings[1].pImmutableSamplers = NULL; - bool succeeded = m_resourceViewHeaps.CreateDescriptorSetLayout(&bindings, &m_directOutputDescriptorSetLayout); + bool succeeded = m_resourceViewHeaps.CreateDescriptorSetLayout(&bindings, &m_cacaoApplyDirectDescriptorSetLayout); assert(succeeded); } @@ -188,17 +180,23 @@ void SampleRenderer::OnCreate(Device *pDevice, SwapChain *pSwapChain) info.minLod = -1000; info.maxLod = 1000; info.maxAnisotropy = 1.0f; - VkResult res = vkCreateSampler(m_pDevice->GetDevice(), &info, NULL, &m_directOutputSampler); + VkResult res = vkCreateSampler(m_pDevice->GetDevice(), &info, NULL, &m_cacaoApplyDirectSampler); assert(res == VK_SUCCESS); } // alloc direct output PS descriptor sets - for (uint32_t i = 0; i < _countof(m_directOutputDescriptorSets); ++i) + for (uint32_t i = 0; i < _countof(m_cacaoApplyDirectDescriptorSets); ++i) { - m_resourceViewHeaps.AllocDescriptor(m_directOutputDescriptorSetLayout, &m_directOutputDescriptorSets[i]); + m_resourceViewHeaps.AllocDescriptor(m_cacaoApplyDirectDescriptorSetLayout, &m_cacaoApplyDirectDescriptorSets[i]); } - m_directOutputPS.OnCreate(m_pDevice, pSwapChain->GetRenderPass(), "Apply_CACAO_Direct.glsl", "main", "", &m_VidMemBufferPool, &m_ConstantBufferRing, m_directOutputDescriptorSetLayout); + m_cacaoApplyDirectPS.OnCreate(m_pDevice, pSwapChain->GetRenderPass(), "Apply_CACAO_Direct.glsl", "main", "", &m_vidMemBufferPool, &m_constantBufferRing, m_cacaoApplyDirectDescriptorSetLayout); + + // Make sure upload heap has finished uploading before continuing +#if (USE_VID_MEM==true) + m_vidMemBufferPool.UploadData(m_uploadHeap.GetCommandList()); + m_uploadHeap.FlushAndFinish(); +#endif } //-------------------------------------------------------------------------------------- @@ -208,45 +206,43 @@ void SampleRenderer::OnCreate(Device *pDevice, SwapChain *pSwapChain) //-------------------------------------------------------------------------------------- void SampleRenderer::OnDestroy() { - m_directOutputPS.OnDestroy(); - vkDestroySampler(m_pDevice->GetDevice(), m_directOutputSampler, NULL); + m_cacaoApplyDirectPS.OnDestroy(); + vkDestroySampler(m_pDevice->GetDevice(), m_cacaoApplyDirectSampler, NULL); - ffxCacaoVkDestroyContext(m_cacaoContextDownsampled); + FFX_CACAO_VkDestroyContext(m_cacaoContextDownsampled); free(m_cacaoContextDownsampled); -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - ffxCacaoVkDestroyContext(m_cacaoContextNative); + FFX_CACAO_VkDestroyContext(m_cacaoContextNative); free(m_cacaoContextNative); -#endif - m_ImGUI.OnDestroy(); - m_colorConversionPS.OnDestroy(); - m_toneMappingPS.OnDestroy(); - m_toneMappingCS.OnDestroy(); - m_bloom.OnDestroy(); - m_downSample.OnDestroy(); - m_wireframeBox.OnDestroy(); - m_wireframe.OnDestroy(); - m_skyDomeProc.OnDestroy(); - m_skyDome.OnDestroy(); - m_shadowMap.OnDestroy(); - - vkDestroyImageView(m_pDevice->GetDevice(), m_shadowMapDSV, nullptr); - vkDestroyImageView(m_pDevice->GetDevice(), m_shadowMapSRV, nullptr); - - vkDestroyRenderPass(m_pDevice->GetDevice(), m_render_pass_non_msaa, NULL); - vkDestroyRenderPass(m_pDevice->GetDevice(), m_render_pass_shadow, nullptr); - vkDestroyRenderPass(m_pDevice->GetDevice(), m_render_pass_PBR_HDR, nullptr); - vkDestroyRenderPass(m_pDevice->GetDevice(), m_render_pass_HDR_MSAA, nullptr); - - vkDestroyFramebuffer(m_pDevice->GetDevice(), m_pFrameBuffer_shadow, nullptr); - - m_UploadHeap.OnDestroy(); - m_GPUTimer.OnDestroy(); - m_VidMemBufferPool.OnDestroy(); - m_SysMemBufferPool.OnDestroy(); - m_ConstantBufferRing.OnDestroy(); - m_resourceViewHeaps.OnDestroy(); - m_CommandListRing.OnDestroy(); + m_imGUI.OnDestroy(); + m_colorConversionPS.OnDestroy(); + m_toneMappingPS.OnDestroy(); + m_toneMappingCS.OnDestroy(); + m_bloom.OnDestroy(); + m_downSample.OnDestroy(); + m_wireframeBox.OnDestroy(); + m_wireframe.OnDestroy(); + m_skyDomeProc.OnDestroy(); + m_skyDome.OnDestroy(); + m_shadowMap.OnDestroy(); + + vkDestroyImageView(m_pDevice->GetDevice(), m_shadowMapDSV, nullptr); + vkDestroyImageView(m_pDevice->GetDevice(), m_shadowMapSRV, nullptr); + + vkDestroyRenderPass(m_pDevice->GetDevice(), m_renderPassNonMSAA, NULL); + vkDestroyRenderPass(m_pDevice->GetDevice(), m_renderPassShadow, nullptr); + vkDestroyRenderPass(m_pDevice->GetDevice(), m_renderPassPBRHDR, nullptr); + vkDestroyRenderPass(m_pDevice->GetDevice(), m_renderPassHDRMSAA, nullptr); + + vkDestroyFramebuffer(m_pDevice->GetDevice(), m_pFrameBufferShadow, nullptr); + + m_uploadHeap.OnDestroy(); + m_gpuTimer.OnDestroy(); + m_vidMemBufferPool.OnDestroy(); + m_sysMemBufferPool.OnDestroy(); + m_constantBufferRing.OnDestroy(); + m_resourceViewHeaps.OnDestroy(); + m_commandListRing.OnDestroy(); } //-------------------------------------------------------------------------------------- @@ -256,27 +252,27 @@ void SampleRenderer::OnDestroy() //-------------------------------------------------------------------------------------- void SampleRenderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, uint32_t Width, uint32_t Height) { - m_Width = Width; - m_Height = Height; - - // Set the viewport - // - m_viewport.x = 0; - m_viewport.y = (float)Height; - m_viewport.width = (float)Width; - m_viewport.height = -(float)(Height); - m_viewport.minDepth = (float)0.0f; - m_viewport.maxDepth = (float)1.0f; - - // Create scissor rectangle - // - m_rectScissor.extent.width = Width; - m_rectScissor.extent.height = Height; - m_rectScissor.offset.x = 0; - m_rectScissor.offset.y = 0; - - // Create depth buffer - // + m_width = Width; + m_height = Height; + + // Set the viewport + // + m_viewport.x = 0; + m_viewport.y = (float)Height; + m_viewport.width = (float)Width; + m_viewport.height = -(float)(Height); + m_viewport.minDepth = (float)0.0f; + m_viewport.maxDepth = (float)1.0f; + + // Create scissor rectangle + // + m_rectScissor.extent.width = Width; + m_rectScissor.extent.height = Height; + m_rectScissor.offset.x = 0; + m_rectScissor.offset.y = 0; + + // Create depth buffer + // { VkImageCreateInfo image_info = {}; image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; @@ -298,72 +294,72 @@ void SampleRenderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, image_info.tiling = VK_IMAGE_TILING_OPTIMAL; m_depthBuffer.Init(m_pDevice, &image_info, "DepthBuffer"); } - m_depthBuffer.CreateDSV(&m_depthBufferDSV); - - // Create Texture + RTV with x4 MSAA - // - m_HDRMSAA.InitRenderTarget(m_pDevice, m_Width, m_Height, VK_FORMAT_R16G16B16A16_SFLOAT, VK_SAMPLE_COUNT_4_BIT, (VkImageUsageFlags)(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT), false, "HDRMSAA"); - m_HDRMSAA.CreateRTV(&m_HDRMSAASRV); - - // Create Texture + RTV, to hold the resolved scene - // - m_HDR.InitRenderTarget(m_pDevice, m_Width, m_Height, VK_FORMAT_R16G16B16A16_SFLOAT, VK_SAMPLE_COUNT_1_BIT, (VkImageUsageFlags)(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT), false, "HDR"); - m_HDR.CreateSRV(&m_HDRSRV); - m_HDR.CreateSRV(&m_HDRUAV); - - // Create framebuffer for the MSAA RT - // - { - VkImageView attachments_PBR_HDR_MSAA[] = { m_HDRMSAASRV, m_depthBufferDSV }; - VkImageView attachments_PBR_HDR[1] = { m_HDRSRV }; - - VkFramebufferCreateInfo fb_info = {}; - fb_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; - fb_info.pNext = NULL; - fb_info.attachmentCount = _countof(attachments_PBR_HDR_MSAA); - fb_info.pAttachments = attachments_PBR_HDR_MSAA; - fb_info.width = Width; - fb_info.height = Height; - fb_info.layers = 1; - - VkResult res; - - fb_info.renderPass = m_render_pass_HDR_MSAA; - res = vkCreateFramebuffer(m_pDevice->GetDevice(), &fb_info, NULL, &m_pFrameBuffer_HDR_MSAA); - assert(res == VK_SUCCESS); - - fb_info.attachmentCount = 1; - fb_info.pAttachments = attachments_PBR_HDR; - fb_info.renderPass = m_render_pass_PBR_HDR; - res = vkCreateFramebuffer(m_pDevice->GetDevice(), &fb_info, NULL, &m_pFrameBuffer_PBR_HDR); - assert(res == VK_SUCCESS); - } - - // update bloom and downscaling effect - // - m_downSample.OnCreateWindowSizeDependentResources(m_Width, m_Height, &m_HDR, 6); //downsample the HDR texture 6 times - m_bloom.OnCreateWindowSizeDependentResources(m_Width / 2, m_Height / 2, m_downSample.GetTexture(), 6, &m_HDR); - - // update the pipelines if the swapchain render pass has changed (for example when the format of the swapchain changes) - // - m_colorConversionPS.UpdatePipelines(pSwapChain->GetRenderPass(), pSwapChain->GetDisplayMode()); - m_toneMappingPS.UpdatePipelines(pSwapChain->GetRenderPass()); - - m_ImGUI.UpdatePipeline((pSwapChain->GetDisplayMode() == DISPLAYMODE_SDR) ? pSwapChain->GetRenderPass() : m_render_pass_PBR_HDR); + m_depthBuffer.CreateDSV(&m_depthBufferDSV); + + // Create Texture + RTV with x4 MSAA + // + m_hdrMSAA.InitRenderTarget(m_pDevice, m_width, m_height, VK_FORMAT_R16G16B16A16_SFLOAT, VK_SAMPLE_COUNT_4_BIT, (VkImageUsageFlags)(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT), false, "HDRMSAA"); + m_hdrMSAA.CreateRTV(&m_hdrMSAASRV); + + // Create Texture + RTV, to hold the resolved scene + // + m_hdr.InitRenderTarget(m_pDevice, m_width, m_height, VK_FORMAT_R16G16B16A16_SFLOAT, VK_SAMPLE_COUNT_1_BIT, (VkImageUsageFlags)(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT), false, "HDR"); + m_hdr.CreateSRV(&m_hdrSRV); + m_hdr.CreateSRV(&m_hdrUAV); + + // Create framebuffer for the MSAA RT + // + { + VkImageView attachments_PBR_HDR_MSAA[] = { m_hdrMSAASRV, m_depthBufferDSV }; + VkImageView attachments_PBR_HDR[1] = { m_hdrSRV }; + + VkFramebufferCreateInfo fb_info = {}; + fb_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fb_info.pNext = NULL; + fb_info.attachmentCount = _countof(attachments_PBR_HDR_MSAA); + fb_info.pAttachments = attachments_PBR_HDR_MSAA; + fb_info.width = Width; + fb_info.height = Height; + fb_info.layers = 1; + + VkResult res; + + fb_info.renderPass = m_renderPassHDRMSAA; + res = vkCreateFramebuffer(m_pDevice->GetDevice(), &fb_info, NULL, &m_pFrameBufferHDRMSAA); + assert(res == VK_SUCCESS); + + fb_info.attachmentCount = 1; + fb_info.pAttachments = attachments_PBR_HDR; + fb_info.renderPass = m_renderPassPBRHDR; + res = vkCreateFramebuffer(m_pDevice->GetDevice(), &fb_info, NULL, &m_pFrameBufferPBRHDR); + assert(res == VK_SUCCESS); + } + + // update bloom and downscaling effect + // + m_downSample.OnCreateWindowSizeDependentResources(m_width, m_height, &m_hdr, 6); //downsample the HDR texture 6 times + m_bloom.OnCreateWindowSizeDependentResources(m_width / 2, m_height / 2, m_downSample.GetTexture(), 6, &m_hdr); + + // update the pipelines if the swapchain render pass has changed (for example when the format of the swapchain changes) + // + m_colorConversionPS.UpdatePipelines(pSwapChain->GetRenderPass(), pSwapChain->GetDisplayMode()); + m_toneMappingPS.UpdatePipelines(pSwapChain->GetRenderPass()); + + m_imGUI.UpdatePipeline((pSwapChain->GetDisplayMode() == DISPLAYMODE_SDR) ? pSwapChain->GetRenderPass() : m_renderPassPBRHDR); // ========================================================== // CACAO - m_NormalBufferNonMsaa.InitRenderTarget(m_pDevice, m_Width, m_Height, VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_SAMPLE_COUNT_1_BIT, (VkImageUsageFlags)(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT), false, "NormalBufferNonMsaa"); - m_NormalBufferNonMsaa.CreateRTV(&m_NormalBufferNonMsaaView); + m_normalBufferNonMsaa.InitRenderTarget(m_pDevice, m_width, m_height, VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_SAMPLE_COUNT_1_BIT, (VkImageUsageFlags)(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT), false, "NormalBufferNonMsaa"); + m_normalBufferNonMsaa.CreateRTV(&m_normalBufferNonMsaaView); - m_DepthBufferNonMsaa.InitDepthStencil(m_pDevice, Width, Height, VK_FORMAT_D32_SFLOAT, VK_SAMPLE_COUNT_1_BIT, "DepthBufferNonMsaa"); - m_DepthBufferNonMsaa.CreateSRV(&m_DepthBufferNonMsaaView); + m_depthBufferNonMsaa.InitDepthStencil(m_pDevice, Width, Height, VK_FORMAT_D32_SFLOAT, VK_SAMPLE_COUNT_1_BIT, "DepthBufferNonMsaa"); + m_depthBufferNonMsaa.CreateSRV(&m_depthBufferNonMsaaView); // Create framebuffer for the MSAA RT // { - VkImageView attachments[] = { m_NormalBufferNonMsaaView, m_DepthBufferNonMsaaView }; + VkImageView attachments[] = { m_normalBufferNonMsaaView, m_depthBufferNonMsaaView }; VkFramebufferCreateInfo fb_info = {}; fb_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; @@ -373,9 +369,9 @@ void SampleRenderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, fb_info.width = Width; fb_info.height = Height; fb_info.layers = 1; - fb_info.renderPass = m_render_pass_non_msaa; + fb_info.renderPass = m_renderPassNonMSAA; - VkResult res = vkCreateFramebuffer(m_pDevice->GetDevice(), &fb_info, NULL, &m_pFrameBuffer_non_msaa); + VkResult res = vkCreateFramebuffer(m_pDevice->GetDevice(), &fb_info, NULL, &m_pFrameBufferNonMSAA); assert(res == VK_SUCCESS); } @@ -407,25 +403,21 @@ void SampleRenderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, m_gltfPBR->OnUpdateWindowSizeDependentResources(m_cacaoOutputSRV); } - FfxCacaoVkScreenSizeInfo ssi = {}; + FFX_CACAO_VkScreenSizeInfo ssi = {}; ssi.width = Width; ssi.height = Height; - ssi.depthView = m_DepthBufferNonMsaaView; - ssi.normalsView = m_NormalBufferNonMsaaView; + ssi.depthView = m_depthBufferNonMsaaView; + ssi.normalsView = m_normalBufferNonMsaaView; ssi.output = m_cacaoOutput.Resource(); ssi.outputView = m_cacaoOutputSRV; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION ssi.useDownsampledSsao = FFX_CACAO_TRUE; - ffxCacaoVkInitScreenSizeDependentResources(m_cacaoContextDownsampled, &ssi); + FFX_CACAO_VkInitScreenSizeDependentResources(m_cacaoContextDownsampled, &ssi); ssi.useDownsampledSsao = FFX_CACAO_FALSE; - ffxCacaoVkInitScreenSizeDependentResources(m_cacaoContextNative, &ssi); -#else - ffxCacaoVkInitScreenSizeDependentResources(m_cacaoContextDownsampled, &ssi); -#endif + FFX_CACAO_VkInitScreenSizeDependentResources(m_cacaoContextNative, &ssi); - m_directOutputPS.UpdatePipeline(pSwapChain->GetRenderPass()); + m_cacaoApplyDirectPS.UpdatePipeline(pSwapChain->GetRenderPass()); } //-------------------------------------------------------------------------------------- @@ -435,34 +427,32 @@ void SampleRenderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, //-------------------------------------------------------------------------------------- void SampleRenderer::OnDestroyWindowSizeDependentResources() { -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - ffxCacaoVkDestroyScreenSizeDependentResources(m_cacaoContextNative); -#endif - ffxCacaoVkDestroyScreenSizeDependentResources(m_cacaoContextDownsampled); + FFX_CACAO_VkDestroyScreenSizeDependentResources(m_cacaoContextNative); + FFX_CACAO_VkDestroyScreenSizeDependentResources(m_cacaoContextDownsampled); - vkDestroyImageView(m_pDevice->GetDevice(), m_NormalBufferNonMsaaView, NULL); - m_NormalBufferNonMsaa.OnDestroy(); - vkDestroyImageView(m_pDevice->GetDevice(), m_DepthBufferNonMsaaView, NULL); - m_DepthBufferNonMsaa.OnDestroy(); + vkDestroyImageView(m_pDevice->GetDevice(), m_normalBufferNonMsaaView, NULL); + m_normalBufferNonMsaa.OnDestroy(); + vkDestroyImageView(m_pDevice->GetDevice(), m_depthBufferNonMsaaView, NULL); + m_depthBufferNonMsaa.OnDestroy(); vkDestroyImageView(m_pDevice->GetDevice(), m_cacaoOutputSRV, NULL); m_cacaoOutput.OnDestroy(); - m_bloom.OnDestroyWindowSizeDependentResources(); - m_downSample.OnDestroyWindowSizeDependentResources(); + m_bloom.OnDestroyWindowSizeDependentResources(); + m_downSample.OnDestroyWindowSizeDependentResources(); - m_HDR.OnDestroy(); - m_HDRMSAA.OnDestroy(); - m_depthBuffer.OnDestroy(); + m_hdr.OnDestroy(); + m_hdrMSAA.OnDestroy(); + m_depthBuffer.OnDestroy(); - vkDestroyFramebuffer(m_pDevice->GetDevice(), m_pFrameBuffer_non_msaa, NULL); - vkDestroyFramebuffer(m_pDevice->GetDevice(), m_pFrameBuffer_HDR_MSAA, nullptr); - vkDestroyFramebuffer(m_pDevice->GetDevice(), m_pFrameBuffer_PBR_HDR, nullptr); + vkDestroyFramebuffer(m_pDevice->GetDevice(), m_pFrameBufferNonMSAA, NULL); + vkDestroyFramebuffer(m_pDevice->GetDevice(), m_pFrameBufferHDRMSAA, nullptr); + vkDestroyFramebuffer(m_pDevice->GetDevice(), m_pFrameBufferPBRHDR, nullptr); - vkDestroyImageView(m_pDevice->GetDevice(), m_depthBufferDSV, nullptr); - vkDestroyImageView(m_pDevice->GetDevice(), m_HDRMSAASRV, nullptr); - vkDestroyImageView(m_pDevice->GetDevice(), m_HDRSRV, nullptr); - vkDestroyImageView(m_pDevice->GetDevice(), m_HDRUAV, nullptr); + vkDestroyImageView(m_pDevice->GetDevice(), m_depthBufferDSV, nullptr); + vkDestroyImageView(m_pDevice->GetDevice(), m_hdrMSAASRV, nullptr); + vkDestroyImageView(m_pDevice->GetDevice(), m_hdrSRV, nullptr); + vkDestroyImageView(m_pDevice->GetDevice(), m_hdrUAV, nullptr); } //-------------------------------------------------------------------------------------- @@ -472,86 +462,86 @@ void SampleRenderer::OnDestroyWindowSizeDependentResources() //-------------------------------------------------------------------------------------- int SampleRenderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) { - // show loading progress - // - ImGui::OpenPopup("Loading"); - if (ImGui::BeginPopupModal("Loading", NULL, ImGuiWindowFlags_AlwaysAutoResize)) - { - float progress = (float)stage / 12.0f; - ImGui::ProgressBar(progress, ImVec2(0.f, 0.f), NULL); - ImGui::EndPopup(); - } - - // Loading stages - // - if (stage == 0) - { - } - else if (stage == 5) - { - Profile p("m_pGltfLoader->Load"); - - m_pGLTFTexturesAndBuffers = new GLTFTexturesAndBuffers(); - m_pGLTFTexturesAndBuffers->OnCreate(m_pDevice, pGLTFCommon, &m_UploadHeap, &m_VidMemBufferPool, &m_ConstantBufferRing); - } - else if (stage == 6) - { - Profile p("LoadTextures"); - - // here we are loading onto the GPU all the textures and the inverse matrices - // this data will be used to create the PBR and Depth passes - m_pGLTFTexturesAndBuffers->LoadTextures(); - } - else if (stage == 7) - { - Profile p("m_gltfDepth->OnCreate"); - - //create the glTF's textures, VBs, IBs, shaders and descriptors for this particular pass - m_gltfDepth = new GltfDepthPass(); - m_gltfDepth->OnCreate( - m_pDevice, - m_render_pass_shadow, - &m_UploadHeap, - &m_resourceViewHeaps, - &m_ConstantBufferRing, - &m_VidMemBufferPool, - m_pGLTFTexturesAndBuffers - ); + // show loading progress + // + ImGui::OpenPopup("Loading"); + if (ImGui::BeginPopupModal("Loading", NULL, ImGuiWindowFlags_AlwaysAutoResize)) + { + float progress = (float)stage / 12.0f; + ImGui::ProgressBar(progress, ImVec2(0.f, 0.f), NULL); + ImGui::EndPopup(); + } + + // Loading stages + // + if (stage == 0) + { + } + else if (stage == 5) + { + Profile p("m_pGltfLoader->Load"); + + m_pGLTFTexturesAndBuffers = new GLTFTexturesAndBuffers(); + m_pGLTFTexturesAndBuffers->OnCreate(m_pDevice, pGLTFCommon, &m_uploadHeap, &m_vidMemBufferPool, &m_constantBufferRing); + } + else if (stage == 6) + { + Profile p("LoadTextures"); + + // here we are loading onto the GPU all the textures and the inverse matrices + // this data will be used to create the PBR and Depth passes + m_pGLTFTexturesAndBuffers->LoadTextures(); + } + else if (stage == 7) + { + Profile p("m_gltfDepth->OnCreate"); + + //create the glTF's textures, VBs, IBs, shaders and descriptors for this particular pass + m_gltfDepth = new GltfDepthPass(); + m_gltfDepth->OnCreate( + m_pDevice, + m_renderPassShadow, + &m_uploadHeap, + &m_resourceViewHeaps, + &m_constantBufferRing, + &m_vidMemBufferPool, + m_pGLTFTexturesAndBuffers + ); #if (USE_VID_MEM==true) - m_VidMemBufferPool.UploadData(m_UploadHeap.GetCommandList()); - m_UploadHeap.FlushAndFinish(); + m_vidMemBufferPool.UploadData(m_uploadHeap.GetCommandList()); + m_uploadHeap.FlushAndFinish(); #endif - } - else if (stage == 8) - { - Profile p("m_gltfPBR->OnCreate"); - - // same thing as above but for the PBR pass - m_gltfPBR = new GltfPbrPass(); - m_gltfPBR->OnCreate( - m_pDevice, - m_render_pass_HDR_MSAA, - &m_UploadHeap, - &m_resourceViewHeaps, - &m_ConstantBufferRing, - &m_VidMemBufferPool, - m_pGLTFTexturesAndBuffers, - &m_skyDome, + } + else if (stage == 8) + { + Profile p("m_gltfPBR->OnCreate"); + + // same thing as above but for the PBR pass + m_gltfPBR = new GltfPbrPass(); + m_gltfPBR->OnCreate( + m_pDevice, + m_renderPassHDRMSAA, + &m_uploadHeap, + &m_resourceViewHeaps, + &m_constantBufferRing, + &m_vidMemBufferPool, + m_pGLTFTexturesAndBuffers, + &m_skyDome, true, // we will pass in a buffer with AO - m_shadowMapSRV, - true, // Exports ForwardPass - false, // Won't export Specular Roughness - false, // Won't export Diffuse Color + m_shadowMapSRV, + true, // Exports ForwardPass + false, // Won't export Specular Roughness + false, // Won't export Diffuse Color false, // export normals - VK_SAMPLE_COUNT_4_BIT - ); + VK_SAMPLE_COUNT_4_BIT + ); m_gltfPBR->OnUpdateWindowSizeDependentResources(m_cacaoOutputSRV); #if (USE_VID_MEM==true) - m_VidMemBufferPool.UploadData(m_UploadHeap.GetCommandList()); - m_UploadHeap.FlushAndFinish(); + m_vidMemBufferPool.UploadData(m_uploadHeap.GetCommandList()); + m_uploadHeap.FlushAndFinish(); #endif - } + } else if (stage == 9) { Profile p("m_gltfPBR->OnCreate (Non MSAA)"); @@ -559,11 +549,11 @@ int SampleRenderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) m_gltfPbrNonMsaa = new GltfPbrPass(); m_gltfPbrNonMsaa->OnCreate( m_pDevice, - m_render_pass_non_msaa, - &m_UploadHeap, + m_renderPassNonMSAA, + &m_uploadHeap, &m_resourceViewHeaps, - &m_ConstantBufferRing, - &m_VidMemBufferPool, + &m_constantBufferRing, + &m_vidMemBufferPool, m_pGLTFTexturesAndBuffers, &m_skyDome, false, // We won't pass in a buffer with AO @@ -575,42 +565,42 @@ int SampleRenderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) VK_SAMPLE_COUNT_1_BIT ); } - else if (stage == 10) - { - Profile p("m_gltfBBox->OnCreate"); - - // just a bounding box pass that will draw boundingboxes instead of the geometry itself - m_gltfBBox = new GltfBBoxPass(); - m_gltfBBox->OnCreate( - m_pDevice, - m_render_pass_HDR_MSAA, - &m_resourceViewHeaps, - &m_ConstantBufferRing, - &m_VidMemBufferPool, - m_pGLTFTexturesAndBuffers, - &m_wireframe - ); + else if (stage == 10) + { + Profile p("m_gltfBBox->OnCreate"); + + // just a bounding box pass that will draw boundingboxes instead of the geometry itself + m_gltfBBox = new GltfBBoxPass(); + m_gltfBBox->OnCreate( + m_pDevice, + m_renderPassHDRMSAA, + &m_resourceViewHeaps, + &m_constantBufferRing, + &m_vidMemBufferPool, + m_pGLTFTexturesAndBuffers, + &m_wireframe + ); #if (USE_VID_MEM==true) - // we are borrowing the upload heap command list for uploading to the GPU the IBs and VBs - m_VidMemBufferPool.UploadData(m_UploadHeap.GetCommandList()); + // we are borrowing the upload heap command list for uploading to the GPU the IBs and VBs + m_vidMemBufferPool.UploadData(m_uploadHeap.GetCommandList()); #endif - } - else if (stage == 11) - { - Profile p("Flush"); + } + else if (stage == 11) + { + Profile p("Flush"); - m_UploadHeap.FlushAndFinish(); + m_uploadHeap.FlushAndFinish(); #if (USE_VID_MEM==true) - //once everything is uploaded we dont need the upload heaps anymore - m_VidMemBufferPool.FreeUploadHeap(); + //once everything is uploaded we dont need the upload heaps anymore + m_vidMemBufferPool.FreeUploadHeap(); #endif - // tell caller that we are done loading the map - return 0; - } + // tell caller that we are done loading the map + return 0; + } - stage++; - return stage; + stage++; + return stage; } //-------------------------------------------------------------------------------------- @@ -620,14 +610,14 @@ int SampleRenderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) //-------------------------------------------------------------------------------------- void SampleRenderer::UnloadScene() { - m_pDevice->GPUFlush(); + m_pDevice->GPUFlush(); - if (m_gltfPBR) - { - m_gltfPBR->OnDestroy(); - delete m_gltfPBR; - m_gltfPBR = NULL; - } + if (m_gltfPBR) + { + m_gltfPBR->OnDestroy(); + delete m_gltfPBR; + m_gltfPBR = NULL; + } if (m_gltfPbrNonMsaa) { @@ -636,26 +626,26 @@ void SampleRenderer::UnloadScene() m_gltfPbrNonMsaa = NULL; } - if (m_gltfDepth) - { - m_gltfDepth->OnDestroy(); - delete m_gltfDepth; - m_gltfDepth = NULL; - } - - if (m_gltfBBox) - { - m_gltfBBox->OnDestroy(); - delete m_gltfBBox; - m_gltfBBox = NULL; - } - - if (m_pGLTFTexturesAndBuffers) - { - m_pGLTFTexturesAndBuffers->OnDestroy(); - delete m_pGLTFTexturesAndBuffers; - m_pGLTFTexturesAndBuffers = NULL; - } + if (m_gltfDepth) + { + m_gltfDepth->OnDestroy(); + delete m_gltfDepth; + m_gltfDepth = NULL; + } + + if (m_gltfBBox) + { + m_gltfBBox->OnDestroy(); + delete m_gltfBBox; + m_gltfBBox = NULL; + } + + if (m_pGLTFTexturesAndBuffers) + { + m_pGLTFTexturesAndBuffers->OnDestroy(); + delete m_pGLTFTexturesAndBuffers; + m_pGLTFTexturesAndBuffers = NULL; + } } //-------------------------------------------------------------------------------------- @@ -665,117 +655,117 @@ void SampleRenderer::UnloadScene() //-------------------------------------------------------------------------------------- void SampleRenderer::OnRender(State *pState, SwapChain *pSwapChain) { - // Let our resource managers do some house keeping - // - m_ConstantBufferRing.OnBeginFrame(); - - // command buffer calls - // - VkCommandBuffer cmdBuf1 = m_CommandListRing.GetNewCommandList(); - - { - VkCommandBufferBeginInfo cmd_buf_info; - cmd_buf_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - cmd_buf_info.pNext = NULL; - cmd_buf_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - cmd_buf_info.pInheritanceInfo = NULL; - VkResult res = vkBeginCommandBuffer(cmdBuf1, &cmd_buf_info); - assert(res == VK_SUCCESS); - } - - m_GPUTimer.OnBeginFrame(cmdBuf1, &m_TimeStamps); - - // Sets the perFrame data - // - per_frame *pPerFrame = NULL; - if (m_pGLTFTexturesAndBuffers) - { - // fill as much as possible using the GLTF (camera, lights, ...) - pPerFrame = m_pGLTFTexturesAndBuffers->m_pGLTFCommon->SetPerFrameData(pState->camera); - - // Set some lighting factors - pPerFrame->iblFactor = pState->iblFactor; - pPerFrame->emmisiveFactor = pState->emmisiveFactor; - pPerFrame->invScreenResolution[0] = 1.0f / ((float)m_Width); - pPerFrame->invScreenResolution[1] = 1.0f / ((float)m_Height); - - // Set shadowmaps bias and an index that indicates the rectangle of the atlas in which depth will be rendered - uint32_t shadowMapIndex = 0; - for (uint32_t i = 0; i < pPerFrame->lightCount; i++) - { - if ((shadowMapIndex < 4) && (pPerFrame->lights[i].type == LightType_Spot)) - { - pPerFrame->lights[i].shadowMapIndex = shadowMapIndex++; // set the shadowmap index - pPerFrame->lights[i].depthBias = 70.0f / 100000.0f; - } - else if ((shadowMapIndex < 4) && (pPerFrame->lights[i].type == LightType_Directional)) - { - pPerFrame->lights[i].shadowMapIndex = shadowMapIndex++; // set the shadowmap index - pPerFrame->lights[i].depthBias = 1000.0f / 100000.0f; - } - else - { - pPerFrame->lights[i].shadowMapIndex = -1; // no shadow for this light - } - } - - m_pGLTFTexturesAndBuffers->SetPerFrameConstants(); - m_pGLTFTexturesAndBuffers->SetSkinningMatricesForSkeletons(); - } - - // Render to shadow map atlas for spot lights ------------------------------------------ - // - if (m_gltfDepth && pPerFrame != NULL) - { - SetPerfMarkerBegin(cmdBuf1, "ShadowPass"); - - VkClearValue depth_clear_values[1]; - depth_clear_values[0].depthStencil.depth = 1.0f; - depth_clear_values[0].depthStencil.stencil = 0; - - { - VkRenderPassBeginInfo rp_begin; - rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; - rp_begin.pNext = NULL; - rp_begin.renderPass = m_render_pass_shadow; - rp_begin.framebuffer = m_pFrameBuffer_shadow; - rp_begin.renderArea.offset.x = 0; - rp_begin.renderArea.offset.y = 0; - rp_begin.renderArea.extent.width = m_shadowMap.GetWidth(); - rp_begin.renderArea.extent.height = m_shadowMap.GetHeight(); - rp_begin.clearValueCount = 1; - rp_begin.pClearValues = depth_clear_values; - - vkCmdBeginRenderPass(cmdBuf1, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); - m_GPUTimer.GetTimeStamp(cmdBuf1, "Clear Shadow Map"); - } - - uint32_t shadowMapIndex = 0; - for (uint32_t i = 0; i < pPerFrame->lightCount; i++) - { - if (!(pPerFrame->lights[i].type == LightType_Spot || pPerFrame->lights[i].type == LightType_Directional)) - continue; - - // Set the RT's quadrant where to render the shadomap (these viewport offsets need to match the ones in shadowFiltering.h) - uint32_t viewportOffsetsX[4] = { 0, 1, 0, 1 }; - uint32_t viewportOffsetsY[4] = { 0, 0, 1, 1 }; - uint32_t viewportWidth = m_shadowMap.GetWidth() / 2; - uint32_t viewportHeight = m_shadowMap.GetHeight() / 2; - SetViewportAndScissor(cmdBuf1, viewportOffsetsX[shadowMapIndex] * viewportWidth, viewportOffsetsY[shadowMapIndex] * viewportHeight, viewportWidth, viewportHeight); - - //set per frame constant buffer values - GltfDepthPass::per_frame *cbPerFrame = m_gltfDepth->SetPerFrameConstants(); - cbPerFrame->mViewProj = pPerFrame->lights[i].mLightViewProj; - - m_gltfDepth->Draw(cmdBuf1); - - m_GPUTimer.GetTimeStamp(cmdBuf1, "Shadow maps"); - shadowMapIndex++; - } - vkCmdEndRenderPass(cmdBuf1); - - SetPerfMarkerEnd(cmdBuf1); - } + // Let our resource managers do some house keeping + // + m_constantBufferRing.OnBeginFrame(); + + // command buffer calls + // + VkCommandBuffer cmdBuf1 = m_commandListRing.GetNewCommandList(); + + { + VkCommandBufferBeginInfo cmd_buf_info; + cmd_buf_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + cmd_buf_info.pNext = NULL; + cmd_buf_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + cmd_buf_info.pInheritanceInfo = NULL; + VkResult res = vkBeginCommandBuffer(cmdBuf1, &cmd_buf_info); + assert(res == VK_SUCCESS); + } + + m_gpuTimer.OnBeginFrame(cmdBuf1, &m_timeStamps); + + // Sets the perFrame data + // + per_frame *pPerFrame = NULL; + if (m_pGLTFTexturesAndBuffers) + { + // fill as much as possible using the GLTF (camera, lights, ...) + pPerFrame = m_pGLTFTexturesAndBuffers->m_pGLTFCommon->SetPerFrameData(pState->camera); + + // Set some lighting factors + pPerFrame->iblFactor = pState->iblFactor; + pPerFrame->emmisiveFactor = pState->emmisiveFactor; + pPerFrame->invScreenResolution[0] = 1.0f / ((float)m_width); + pPerFrame->invScreenResolution[1] = 1.0f / ((float)m_height); + + // Set shadowmaps bias and an index that indicates the rectangle of the atlas in which depth will be rendered + uint32_t shadowMapIndex = 0; + for (uint32_t i = 0; i < pPerFrame->lightCount; i++) + { + if ((shadowMapIndex < 4) && (pPerFrame->lights[i].type == LightType_Spot)) + { + pPerFrame->lights[i].shadowMapIndex = shadowMapIndex++; // set the shadowmap index + pPerFrame->lights[i].depthBias = 70.0f / 100000.0f; + } + else if ((shadowMapIndex < 4) && (pPerFrame->lights[i].type == LightType_Directional)) + { + pPerFrame->lights[i].shadowMapIndex = shadowMapIndex++; // set the shadowmap index + pPerFrame->lights[i].depthBias = 1000.0f / 100000.0f; + } + else + { + pPerFrame->lights[i].shadowMapIndex = -1; // no shadow for this light + } + } + + m_pGLTFTexturesAndBuffers->SetPerFrameConstants(); + m_pGLTFTexturesAndBuffers->SetSkinningMatricesForSkeletons(); + } + + // Render to shadow map atlas for spot lights ------------------------------------------ + // + if (m_gltfDepth && pPerFrame != NULL) + { + SetPerfMarkerBegin(cmdBuf1, "ShadowPass"); + + VkClearValue depth_clear_values[1]; + depth_clear_values[0].depthStencil.depth = 1.0f; + depth_clear_values[0].depthStencil.stencil = 0; + + { + VkRenderPassBeginInfo rp_begin; + rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rp_begin.pNext = NULL; + rp_begin.renderPass = m_renderPassShadow; + rp_begin.framebuffer = m_pFrameBufferShadow; + rp_begin.renderArea.offset.x = 0; + rp_begin.renderArea.offset.y = 0; + rp_begin.renderArea.extent.width = m_shadowMap.GetWidth(); + rp_begin.renderArea.extent.height = m_shadowMap.GetHeight(); + rp_begin.clearValueCount = 1; + rp_begin.pClearValues = depth_clear_values; + + vkCmdBeginRenderPass(cmdBuf1, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); + m_gpuTimer.GetTimeStamp(cmdBuf1, "Clear Shadow Map"); + } + + uint32_t shadowMapIndex = 0; + for (uint32_t i = 0; i < pPerFrame->lightCount; i++) + { + if (!(pPerFrame->lights[i].type == LightType_Spot || pPerFrame->lights[i].type == LightType_Directional)) + continue; + + // Set the RT's quadrant where to render the shadomap (these viewport offsets need to match the ones in shadowFiltering.h) + uint32_t viewportOffsetsX[4] = { 0, 1, 0, 1 }; + uint32_t viewportOffsetsY[4] = { 0, 0, 1, 1 }; + uint32_t viewportWidth = m_shadowMap.GetWidth() / 2; + uint32_t viewportHeight = m_shadowMap.GetHeight() / 2; + SetViewportAndScissor(cmdBuf1, viewportOffsetsX[shadowMapIndex] * viewportWidth, viewportOffsetsY[shadowMapIndex] * viewportHeight, viewportWidth, viewportHeight); + + //set per frame constant buffer values + GltfDepthPass::per_frame *cbPerFrame = m_gltfDepth->SetPerFrameConstants(); + cbPerFrame->mViewProj = pPerFrame->lights[i].mLightViewProj; + + m_gltfDepth->Draw(cmdBuf1); + + m_gpuTimer.GetTimeStamp(cmdBuf1, "Shadow maps"); + shadowMapIndex++; + } + vkCmdEndRenderPass(cmdBuf1); + + SetPerfMarkerEnd(cmdBuf1); + } // =============================================================================================== // CACAO stuff @@ -786,7 +776,7 @@ void SampleRenderer::OnRender(State *pState, SwapChain *pSwapChain) // if (m_gltfPbrNonMsaa && pPerFrame) { - m_GPUTimer.GetTimeStamp(cmdBuf1, "PBR Non MSAA"); + m_gpuTimer.GetTimeStamp(cmdBuf1, "PBR Non MSAA"); SetPerfMarkerBegin(cmdBuf1, "PBR Non MSAA pass"); { @@ -801,23 +791,23 @@ void SampleRenderer::OnRender(State *pState, SwapChain *pSwapChain) VkRenderPassBeginInfo rp_begin; rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; rp_begin.pNext = NULL; - rp_begin.renderPass = m_render_pass_non_msaa; - rp_begin.framebuffer = m_pFrameBuffer_non_msaa; + rp_begin.renderPass = m_renderPassNonMSAA; + rp_begin.framebuffer = m_pFrameBufferNonMSAA; rp_begin.renderArea.offset.x = 0; rp_begin.renderArea.offset.y = 0; - rp_begin.renderArea.extent.width = m_NormalBufferNonMsaa.GetWidth(); - rp_begin.renderArea.extent.height = m_NormalBufferNonMsaa.GetHeight(); + rp_begin.renderArea.extent.width = m_normalBufferNonMsaa.GetWidth(); + rp_begin.renderArea.extent.height = m_normalBufferNonMsaa.GetHeight(); rp_begin.clearValueCount = _countof(clearValues); rp_begin.pClearValues = clearValues; vkCmdBeginRenderPass(cmdBuf1, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); - m_GPUTimer.GetTimeStamp(cmdBuf1, "Clear Depth Buffer Non MSAA"); + m_gpuTimer.GetTimeStamp(cmdBuf1, "Clear Depth Buffer Non MSAA"); } - SetViewportAndScissor(cmdBuf1, 0, 0, m_NormalBufferNonMsaa.GetWidth(), m_NormalBufferNonMsaa.GetHeight()); + SetViewportAndScissor(cmdBuf1, 0, 0, m_normalBufferNonMsaa.GetWidth(), m_normalBufferNonMsaa.GetHeight()); m_gltfPbrNonMsaa->Draw(cmdBuf1); - m_GPUTimer.GetTimeStamp(cmdBuf1, "GLTF PBR Non MSAA"); + m_gpuTimer.GetTimeStamp(cmdBuf1, "GLTF PBR Non MSAA"); vkCmdEndRenderPass(cmdBuf1); @@ -825,8 +815,8 @@ void SampleRenderer::OnRender(State *pState, SwapChain *pSwapChain) } // call CACAO - if (pState->m_useCacao && m_gltfPbrNonMsaa && pPerFrame) { - FfxCacaoMatrix4x4 proj, normalsWorldToView; + if (pState->useCacao && m_gltfPbrNonMsaa && pPerFrame) { + FFX_CACAO_Matrix4x4 proj, normalsWorldToView; { XMFLOAT4X4 p; XMMATRIX xProj = pState->camera.GetProjection(); @@ -844,24 +834,18 @@ void SampleRenderer::OnRender(State *pState, SwapChain *pSwapChain) normalsWorldToView.elements[3][0] = p._41; normalsWorldToView.elements[3][1] = p._42; normalsWorldToView.elements[3][2] = p._43; normalsWorldToView.elements[3][3] = p._44; } -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - ffxCacaoVkUpdateSettings(m_cacaoContextNative, &pState->m_cacaoSettings); -#endif - ffxCacaoVkUpdateSettings(m_cacaoContextDownsampled, &pState->m_cacaoSettings); + FFX_CACAO_VkUpdateSettings(m_cacaoContextNative, &pState->cacaoSettings); + FFX_CACAO_VkUpdateSettings(m_cacaoContextDownsampled, &pState->cacaoSettings); - FfxCacaoStatus status = FFX_CACAO_STATUS_OK; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - if (pState->m_useDownsampledSsao) + FFX_CACAO_Status status = FFX_CACAO_STATUS_OK; + if (pState->useDownsampledSsao) { - status = ffxCacaoVkDraw(m_cacaoContextDownsampled, cmdBuf1, &proj, &normalsWorldToView); + status = FFX_CACAO_VkDraw(m_cacaoContextDownsampled, cmdBuf1, &proj, &normalsWorldToView); } else { - status = ffxCacaoVkDraw(m_cacaoContextNative, cmdBuf1, &proj, &normalsWorldToView); + status = FFX_CACAO_VkDraw(m_cacaoContextNative, cmdBuf1, &proj, &normalsWorldToView); } -#else - status = ffxCacaoVkDraw(m_cacaoContextDownsampled, cmdBuf1, &proj, &normalsWorldToView); -#endif assert(status == FFX_CACAO_STATUS_OK); } else @@ -901,374 +885,374 @@ void SampleRenderer::OnRender(State *pState, SwapChain *pSwapChain) vkCmdPipelineBarrier(cmdBuf1, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, 1, &barrier); } - // Render Scene to the MSAA HDR RT ------------------------------------------------ - // - - { - SetPerfMarkerBegin(cmdBuf1, "Color pass"); - m_GPUTimer.GetTimeStamp(cmdBuf1, "before color RP"); - VkClearValue clear_values[2]; - clear_values[0].color.float32[0] = 0.0f; - clear_values[0].color.float32[1] = 0.0f; - clear_values[0].color.float32[2] = 0.0f; - clear_values[0].color.float32[3] = 0.0f; - clear_values[1].depthStencil.depth = 1.0f; - clear_values[1].depthStencil.stencil = 0; - - VkRenderPassBeginInfo rp_begin; - rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; - rp_begin.pNext = NULL; - rp_begin.renderPass = m_render_pass_HDR_MSAA; - rp_begin.framebuffer = m_pFrameBuffer_HDR_MSAA; - rp_begin.renderArea.offset.x = 0; - rp_begin.renderArea.offset.y = 0; - rp_begin.renderArea.extent.width = m_Width; - rp_begin.renderArea.extent.height = m_Height; - rp_begin.clearValueCount = 2; - rp_begin.pClearValues = clear_values; - - vkCmdBeginRenderPass(cmdBuf1, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); - - vkCmdSetScissor(cmdBuf1, 0, 1, &m_rectScissor); - vkCmdSetViewport(cmdBuf1, 0, 1, &m_viewport); - m_GPUTimer.GetTimeStamp(cmdBuf1, "after color RP"); - } - - if (pPerFrame != NULL) - { - // Render skydome - // - if (pState->skyDomeType == 1) - { - XMMATRIX clipToView = XMMatrixInverse(NULL, pPerFrame->mCameraViewProj); - m_skyDome.Draw(cmdBuf1, clipToView); - - m_GPUTimer.GetTimeStamp(cmdBuf1, "Skydome cube"); - } - else if (pState->skyDomeType == 0) - { - SkyDomeProc::Constants skyDomeConstants; - skyDomeConstants.invViewProj = XMMatrixInverse(NULL, pPerFrame->mCameraViewProj); - skyDomeConstants.vSunDirection = XMVectorSet(1.0f, 0.05f, 0.0f, 0.0f); - skyDomeConstants.turbidity = 10.0f; - skyDomeConstants.rayleigh = 2.0f; - skyDomeConstants.mieCoefficient = 0.005f; - skyDomeConstants.mieDirectionalG = 0.8f; - skyDomeConstants.luminance = 1.0f; - skyDomeConstants.sun = false; - m_skyDomeProc.Draw(cmdBuf1, skyDomeConstants); - - m_GPUTimer.GetTimeStamp(cmdBuf1, "Skydome Proc"); - } - - // Render scene to color buffer - // - if (m_gltfPBR && pPerFrame != NULL) - { - m_gltfPBR->Draw(cmdBuf1); - m_GPUTimer.GetTimeStamp(cmdBuf1, "PBR Forward"); - } - - // draw object's bounding boxes - // - if (m_gltfBBox && pPerFrame != NULL) - { - if (pState->bDrawBoundingBoxes) - { - m_gltfBBox->Draw(cmdBuf1, pPerFrame->mCameraViewProj); - - m_GPUTimer.GetTimeStamp(cmdBuf1, "Bounding Box"); - } - } - - // draw light's frustums - // - if (pState->bDrawLightFrustum && pPerFrame != NULL) - { - SetPerfMarkerBegin(cmdBuf1, "light frustrums"); - - XMVECTOR vCenter = XMVectorSet(0.0f, 0.0f, 0.5f, 0.0f); - XMVECTOR vRadius = XMVectorSet(1.0f, 1.0f, 0.5f, 0.0f); - XMVECTOR vColor = XMVectorSet(1.0f, 1.0f, 1.0f, 1.0f); - for (uint32_t i = 0; i < pPerFrame->lightCount; i++) - { - XMMATRIX spotlightMatrix = XMMatrixInverse(NULL, pPerFrame->lights[i].mLightViewProj); - XMMATRIX worldMatrix = spotlightMatrix * pPerFrame->mCameraViewProj; - m_wireframeBox.Draw(cmdBuf1, &m_wireframe, worldMatrix, vCenter, vRadius, vColor); - } - - m_GPUTimer.GetTimeStamp(cmdBuf1, "Light's frustum"); - - SetPerfMarkerEnd(cmdBuf1); - } - } - - { - vkCmdEndRenderPass(cmdBuf1); - SetPerfMarkerEnd(cmdBuf1); - } - - // Resolve MSAA ------------------------------------------------------------------------ - // Ideally this resolve should be part of the previous rende pass, that would save a decompression - // - { - SetPerfMarkerBegin(cmdBuf1, "Resolving MSAA"); - { - VkImageMemoryBarrier barrier[2] = {}; - barrier[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier[0].pNext = NULL; - barrier[0].srcAccessMask = 0; - barrier[0].dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - barrier[0].oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - barrier[0].newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - barrier[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier[0].subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - barrier[0].subresourceRange.baseMipLevel = 0; - barrier[0].subresourceRange.levelCount = 1; - barrier[0].subresourceRange.baseArrayLayer = 0; - barrier[0].subresourceRange.layerCount = 1; - barrier[0].image = m_HDR.Resource(); - - barrier[1].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier[1].pNext = NULL; - barrier[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - barrier[1].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - barrier[1].oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - barrier[1].newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - barrier[1].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier[1].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier[1].subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - barrier[1].subresourceRange.baseMipLevel = 0; - barrier[1].subresourceRange.levelCount = 1; - barrier[1].subresourceRange.baseArrayLayer = 0; - barrier[1].subresourceRange.layerCount = 1; - barrier[1].image = m_HDRMSAA.Resource(); - - vkCmdPipelineBarrier(cmdBuf1, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 2, barrier); - } - - { - VkImageResolve re = {}; - re.srcOffset.x = 0; - re.srcOffset.y = 0; - re.extent.width = m_Width; - re.extent.height = m_Height; - re.extent.depth = 1; - re.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - re.srcSubresource.layerCount = 1; - re.dstOffset.x = 0; - re.dstOffset.y = 0; - re.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - re.dstSubresource.layerCount = 1; - vkCmdResolveImage(cmdBuf1, m_HDRMSAA.Resource(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_HDR.Resource(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &re); - } - - { - VkImageMemoryBarrier barrier[2] = {}; - barrier[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier[0].pNext = NULL; - barrier[0].srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - barrier[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - barrier[0].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - barrier[0].newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - barrier[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier[0].subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - barrier[0].subresourceRange.baseMipLevel = 0; - barrier[0].subresourceRange.levelCount = 1; - barrier[0].subresourceRange.baseArrayLayer = 0; - barrier[0].subresourceRange.layerCount = 1; - barrier[0].image = m_HDR.Resource(); - - barrier[1].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier[1].pNext = NULL; - barrier[1].srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - barrier[1].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - barrier[1].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - barrier[1].newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - barrier[1].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier[1].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier[1].subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - barrier[1].subresourceRange.baseMipLevel = 0; - barrier[1].subresourceRange.levelCount = 1; - barrier[1].subresourceRange.baseArrayLayer = 0; - barrier[1].subresourceRange.layerCount = 1; - barrier[1].image = m_HDRMSAA.Resource(); - - vkCmdPipelineBarrier(cmdBuf1, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, NULL, 0, NULL, 2, barrier); - } - - m_GPUTimer.GetTimeStamp(cmdBuf1, "Resolve MSAA"); - SetPerfMarkerEnd(cmdBuf1); - } - - // Post proc--------------------------------------------------------------------------- - // - - // Bloom, takes HDR as input and applies bloom to it. - // + // Render Scene to the MSAA HDR RT ------------------------------------------------ + // + + { + SetPerfMarkerBegin(cmdBuf1, "Color pass"); + m_gpuTimer.GetTimeStamp(cmdBuf1, "before color RP"); + VkClearValue clear_values[2]; + clear_values[0].color.float32[0] = 0.0f; + clear_values[0].color.float32[1] = 0.0f; + clear_values[0].color.float32[2] = 0.0f; + clear_values[0].color.float32[3] = 0.0f; + clear_values[1].depthStencil.depth = 1.0f; + clear_values[1].depthStencil.stencil = 0; + + VkRenderPassBeginInfo rp_begin; + rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rp_begin.pNext = NULL; + rp_begin.renderPass = m_renderPassHDRMSAA; + rp_begin.framebuffer = m_pFrameBufferHDRMSAA; + rp_begin.renderArea.offset.x = 0; + rp_begin.renderArea.offset.y = 0; + rp_begin.renderArea.extent.width = m_width; + rp_begin.renderArea.extent.height = m_height; + rp_begin.clearValueCount = 2; + rp_begin.pClearValues = clear_values; + + vkCmdBeginRenderPass(cmdBuf1, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); + + vkCmdSetScissor(cmdBuf1, 0, 1, &m_rectScissor); + vkCmdSetViewport(cmdBuf1, 0, 1, &m_viewport); + m_gpuTimer.GetTimeStamp(cmdBuf1, "after color RP"); + } + + if (pPerFrame != NULL) + { + // Render skydome + // + if (pState->skyDomeType == 1) + { + XMMATRIX clipToView = XMMatrixInverse(NULL, pPerFrame->mCameraViewProj); + m_skyDome.Draw(cmdBuf1, clipToView); + + m_gpuTimer.GetTimeStamp(cmdBuf1, "Skydome cube"); + } + else if (pState->skyDomeType == 0) + { + SkyDomeProc::Constants skyDomeConstants; + skyDomeConstants.invViewProj = XMMatrixInverse(NULL, pPerFrame->mCameraViewProj); + skyDomeConstants.vSunDirection = XMVectorSet(1.0f, 0.05f, 0.0f, 0.0f); + skyDomeConstants.turbidity = 10.0f; + skyDomeConstants.rayleigh = 2.0f; + skyDomeConstants.mieCoefficient = 0.005f; + skyDomeConstants.mieDirectionalG = 0.8f; + skyDomeConstants.luminance = 1.0f; + skyDomeConstants.sun = false; + m_skyDomeProc.Draw(cmdBuf1, skyDomeConstants); + + m_gpuTimer.GetTimeStamp(cmdBuf1, "Skydome Proc"); + } + + // Render scene to color buffer + // + if (m_gltfPBR && pPerFrame != NULL) + { + m_gltfPBR->Draw(cmdBuf1); + m_gpuTimer.GetTimeStamp(cmdBuf1, "PBR Forward"); + } + + // draw object's bounding boxes + // + if (m_gltfBBox && pPerFrame != NULL) + { + if (pState->drawBoundingBoxes) + { + m_gltfBBox->Draw(cmdBuf1, pPerFrame->mCameraViewProj); + + m_gpuTimer.GetTimeStamp(cmdBuf1, "Bounding Box"); + } + } + + // draw light's frustums + // + if (pState->drawLightFrustum && pPerFrame != NULL) + { + SetPerfMarkerBegin(cmdBuf1, "light frustrums"); + + XMVECTOR vCenter = XMVectorSet(0.0f, 0.0f, 0.5f, 0.0f); + XMVECTOR vRadius = XMVectorSet(1.0f, 1.0f, 0.5f, 0.0f); + XMVECTOR vColor = XMVectorSet(1.0f, 1.0f, 1.0f, 1.0f); + for (uint32_t i = 0; i < pPerFrame->lightCount; i++) + { + XMMATRIX spotlightMatrix = XMMatrixInverse(NULL, pPerFrame->lights[i].mLightViewProj); + XMMATRIX worldMatrix = spotlightMatrix * pPerFrame->mCameraViewProj; + m_wireframeBox.Draw(cmdBuf1, &m_wireframe, worldMatrix, vCenter, vRadius, vColor); + } + + m_gpuTimer.GetTimeStamp(cmdBuf1, "Light's frustum"); + + SetPerfMarkerEnd(cmdBuf1); + } + } + + { + vkCmdEndRenderPass(cmdBuf1); + SetPerfMarkerEnd(cmdBuf1); + } + + // Resolve MSAA ------------------------------------------------------------------------ + // Ideally this resolve should be part of the previous rende pass, that would save a decompression + // + { + SetPerfMarkerBegin(cmdBuf1, "Resolving MSAA"); + { + VkImageMemoryBarrier barrier[2] = {}; + barrier[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier[0].pNext = NULL; + barrier[0].srcAccessMask = 0; + barrier[0].dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier[0].oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier[0].newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier[0].subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier[0].subresourceRange.baseMipLevel = 0; + barrier[0].subresourceRange.levelCount = 1; + barrier[0].subresourceRange.baseArrayLayer = 0; + barrier[0].subresourceRange.layerCount = 1; + barrier[0].image = m_hdr.Resource(); + + barrier[1].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier[1].pNext = NULL; + barrier[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + barrier[1].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier[1].oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + barrier[1].newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier[1].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier[1].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier[1].subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier[1].subresourceRange.baseMipLevel = 0; + barrier[1].subresourceRange.levelCount = 1; + barrier[1].subresourceRange.baseArrayLayer = 0; + barrier[1].subresourceRange.layerCount = 1; + barrier[1].image = m_hdrMSAA.Resource(); + + vkCmdPipelineBarrier(cmdBuf1, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 2, barrier); + } + + { + VkImageResolve re = {}; + re.srcOffset.x = 0; + re.srcOffset.y = 0; + re.extent.width = m_width; + re.extent.height = m_height; + re.extent.depth = 1; + re.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + re.srcSubresource.layerCount = 1; + re.dstOffset.x = 0; + re.dstOffset.y = 0; + re.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + re.dstSubresource.layerCount = 1; + vkCmdResolveImage(cmdBuf1, m_hdrMSAA.Resource(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_hdr.Resource(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &re); + } + + { + VkImageMemoryBarrier barrier[2] = {}; + barrier[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier[0].pNext = NULL; + barrier[0].srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + barrier[0].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier[0].newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier[0].subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier[0].subresourceRange.baseMipLevel = 0; + barrier[0].subresourceRange.levelCount = 1; + barrier[0].subresourceRange.baseArrayLayer = 0; + barrier[0].subresourceRange.layerCount = 1; + barrier[0].image = m_hdr.Resource(); + + barrier[1].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier[1].pNext = NULL; + barrier[1].srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier[1].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + barrier[1].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier[1].newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + barrier[1].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier[1].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier[1].subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier[1].subresourceRange.baseMipLevel = 0; + barrier[1].subresourceRange.levelCount = 1; + barrier[1].subresourceRange.baseArrayLayer = 0; + barrier[1].subresourceRange.layerCount = 1; + barrier[1].image = m_hdrMSAA.Resource(); + + vkCmdPipelineBarrier(cmdBuf1, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, NULL, 0, NULL, 2, barrier); + } + + m_gpuTimer.GetTimeStamp(cmdBuf1, "Resolve MSAA"); + SetPerfMarkerEnd(cmdBuf1); + } + + // Post proc--------------------------------------------------------------------------- + // + + // Bloom, takes HDR as input and applies bloom to it. + // if (0) - { - SetPerfMarkerBegin(cmdBuf1, "post proc"); - - // Downsample pass - m_downSample.Draw(cmdBuf1); - // m_downSample.Gui(); - m_GPUTimer.GetTimeStamp(cmdBuf1, "Downsample"); - - // Bloom pass (needs the downsampled data) - m_bloom.Draw(cmdBuf1); - // m_bloom.Gui(); - m_GPUTimer.GetTimeStamp(cmdBuf1, "Bloom"); - - SetPerfMarkerEnd(cmdBuf1); - } - - // If using FreeSyncHDR we need to to the tonemapping in-place and then apply the GUI, later we'll apply the color conversion into the swapchain - // - if (pSwapChain->GetDisplayMode() != DISPLAYMODE_SDR && !pState->m_dispalyCacaoDirectly) - { - // In place Tonemapping ------------------------------------------------------------------------ - // - { - { - VkImageMemoryBarrier barrier = {}; - barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier.pNext = NULL; - barrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; - barrier.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT; - barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; - barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; - barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - barrier.subresourceRange.baseMipLevel = 0; - barrier.subresourceRange.levelCount = 1; - barrier.subresourceRange.baseArrayLayer = 0; - barrier.subresourceRange.layerCount = 1; - barrier.image = m_HDR.Resource(); - vkCmdPipelineBarrier(cmdBuf1, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, 1, &barrier); - } - - m_toneMappingCS.Draw(cmdBuf1, m_HDRUAV, pState->exposure, pState->toneMapper, m_Width, m_Height); - - { - VkImageMemoryBarrier barrier = {}; - barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier.pNext = NULL; - barrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; - barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; - barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - barrier.subresourceRange.baseMipLevel = 0; - barrier.subresourceRange.levelCount = 1; - barrier.subresourceRange.baseArrayLayer = 0; - barrier.subresourceRange.layerCount = 1; - barrier.image = m_HDR.Resource(); - vkCmdPipelineBarrier(cmdBuf1, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, NULL, 0, NULL, 1, &barrier); - } - } - - // Render HUD ------------------------------------------------------------------------ - // - { - // prepare render pass - { - VkRenderPassBeginInfo rp_begin = {}; - rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; - rp_begin.pNext = NULL; - rp_begin.renderPass = m_render_pass_PBR_HDR; - rp_begin.framebuffer = m_pFrameBuffer_PBR_HDR; - rp_begin.renderArea.offset.x = 0; - rp_begin.renderArea.offset.y = 0; - rp_begin.renderArea.extent.width = m_Width; - rp_begin.renderArea.extent.height = m_Height; - rp_begin.clearValueCount = 0; - rp_begin.pClearValues = NULL; - vkCmdBeginRenderPass(cmdBuf1, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); - } - - vkCmdSetScissor(cmdBuf1, 0, 1, &m_rectScissor); - vkCmdSetViewport(cmdBuf1, 0, 1, &m_viewport); - - m_ImGUI.Draw(cmdBuf1); - - vkCmdEndRenderPass(cmdBuf1); - - m_GPUTimer.GetTimeStamp(cmdBuf1, "ImGUI Rendering"); - } - } - - // submit command buffer - { - VkResult res = vkEndCommandBuffer(cmdBuf1); - assert(res == VK_SUCCESS); - - VkSubmitInfo submit_info; - submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submit_info.pNext = NULL; - submit_info.waitSemaphoreCount = 0; - submit_info.pWaitSemaphores = NULL; - submit_info.pWaitDstStageMask = NULL; - submit_info.commandBufferCount = 1; - submit_info.pCommandBuffers = &cmdBuf1; - submit_info.signalSemaphoreCount = 0; - submit_info.pSignalSemaphores = NULL; - res = vkQueueSubmit(m_pDevice->GetGraphicsQueue(), 1, &submit_info, VK_NULL_HANDLE); - assert(res == VK_SUCCESS); - } - - // Wait for swapchain (we are going to render to it) ----------------------------------- - // - int imageIndex = pSwapChain->WaitForSwapChain(); - - m_CommandListRing.OnBeginFrame(); - - VkCommandBuffer cmdBuf2 = m_CommandListRing.GetNewCommandList(); - - { - VkCommandBufferBeginInfo cmd_buf_info; - cmd_buf_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - cmd_buf_info.pNext = NULL; - cmd_buf_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - cmd_buf_info.pInheritanceInfo = NULL; - VkResult res = vkBeginCommandBuffer(cmdBuf2, &cmd_buf_info); - assert(res == VK_SUCCESS); - } - - SetPerfMarkerBegin(cmdBuf2, "rendering to swap chain"); - - // prepare render pass - { - VkRenderPassBeginInfo rp_begin = {}; - rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; - rp_begin.pNext = NULL; - rp_begin.renderPass = pSwapChain->GetRenderPass(); - rp_begin.framebuffer = pSwapChain->GetFramebuffer(imageIndex); - rp_begin.renderArea.offset.x = 0; - rp_begin.renderArea.offset.y = 0; - rp_begin.renderArea.extent.width = m_Width; - rp_begin.renderArea.extent.height = m_Height; - rp_begin.clearValueCount = 0; - rp_begin.pClearValues = NULL; - vkCmdBeginRenderPass(cmdBuf2, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); - } - - vkCmdSetScissor(cmdBuf2, 0, 1, &m_rectScissor); - vkCmdSetViewport(cmdBuf2, 0, 1, &m_viewport); - - if (!pState->m_dispalyCacaoDirectly) + { + SetPerfMarkerBegin(cmdBuf1, "post proc"); + + // Downsample pass + m_downSample.Draw(cmdBuf1); + // m_downSample.Gui(); + m_gpuTimer.GetTimeStamp(cmdBuf1, "Downsample"); + + // Bloom pass (needs the downsampled data) + m_bloom.Draw(cmdBuf1); + // m_bloom.Gui(); + m_gpuTimer.GetTimeStamp(cmdBuf1, "Bloom"); + + SetPerfMarkerEnd(cmdBuf1); + } + + // If using FreeSyncHDR we need to to the tonemapping in-place and then apply the GUI, later we'll apply the color conversion into the swapchain + // + if (pSwapChain->GetDisplayMode() != DISPLAYMODE_SDR && !pState->dispalyCacaoDirectly) + { + // In place Tonemapping ------------------------------------------------------------------------ + // + { + { + VkImageMemoryBarrier barrier = {}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.pNext = NULL; + barrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.image = m_hdr.Resource(); + vkCmdPipelineBarrier(cmdBuf1, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, 1, &barrier); + } + + m_toneMappingCS.Draw(cmdBuf1, m_hdrUAV, pState->exposure, pState->toneMapper, m_width, m_height); + + { + VkImageMemoryBarrier barrier = {}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.pNext = NULL; + barrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.image = m_hdr.Resource(); + vkCmdPipelineBarrier(cmdBuf1, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, NULL, 0, NULL, 1, &barrier); + } + } + + // Render HUD ------------------------------------------------------------------------ + // + { + // prepare render pass + { + VkRenderPassBeginInfo rp_begin = {}; + rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rp_begin.pNext = NULL; + rp_begin.renderPass = m_renderPassPBRHDR; + rp_begin.framebuffer = m_pFrameBufferPBRHDR; + rp_begin.renderArea.offset.x = 0; + rp_begin.renderArea.offset.y = 0; + rp_begin.renderArea.extent.width = m_width; + rp_begin.renderArea.extent.height = m_height; + rp_begin.clearValueCount = 0; + rp_begin.pClearValues = NULL; + vkCmdBeginRenderPass(cmdBuf1, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); + } + + vkCmdSetScissor(cmdBuf1, 0, 1, &m_rectScissor); + vkCmdSetViewport(cmdBuf1, 0, 1, &m_viewport); + + m_imGUI.Draw(cmdBuf1); + + vkCmdEndRenderPass(cmdBuf1); + + m_gpuTimer.GetTimeStamp(cmdBuf1, "ImGUI Rendering"); + } + } + + // submit command buffer + { + VkResult res = vkEndCommandBuffer(cmdBuf1); + assert(res == VK_SUCCESS); + + VkSubmitInfo submit_info; + submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submit_info.pNext = NULL; + submit_info.waitSemaphoreCount = 0; + submit_info.pWaitSemaphores = NULL; + submit_info.pWaitDstStageMask = NULL; + submit_info.commandBufferCount = 1; + submit_info.pCommandBuffers = &cmdBuf1; + submit_info.signalSemaphoreCount = 0; + submit_info.pSignalSemaphores = NULL; + res = vkQueueSubmit(m_pDevice->GetGraphicsQueue(), 1, &submit_info, VK_NULL_HANDLE); + assert(res == VK_SUCCESS); + } + + // Wait for swapchain (we are going to render to it) ----------------------------------- + // + int imageIndex = pSwapChain->WaitForSwapChain(); + + m_commandListRing.OnBeginFrame(); + + VkCommandBuffer cmdBuf2 = m_commandListRing.GetNewCommandList(); + + { + VkCommandBufferBeginInfo cmd_buf_info; + cmd_buf_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + cmd_buf_info.pNext = NULL; + cmd_buf_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + cmd_buf_info.pInheritanceInfo = NULL; + VkResult res = vkBeginCommandBuffer(cmdBuf2, &cmd_buf_info); + assert(res == VK_SUCCESS); + } + + SetPerfMarkerBegin(cmdBuf2, "rendering to swap chain"); + + // prepare render pass + { + VkRenderPassBeginInfo rp_begin = {}; + rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rp_begin.pNext = NULL; + rp_begin.renderPass = pSwapChain->GetRenderPass(); + rp_begin.framebuffer = pSwapChain->GetFramebuffer(imageIndex); + rp_begin.renderArea.offset.x = 0; + rp_begin.renderArea.offset.y = 0; + rp_begin.renderArea.extent.width = m_width; + rp_begin.renderArea.extent.height = m_height; + rp_begin.clearValueCount = 0; + rp_begin.pClearValues = NULL; + vkCmdBeginRenderPass(cmdBuf2, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); + } + + vkCmdSetScissor(cmdBuf2, 0, 1, &m_rectScissor); + vkCmdSetViewport(cmdBuf2, 0, 1, &m_viewport); + + if (!pState->dispalyCacaoDirectly) { if (pSwapChain->GetDisplayMode() != DISPLAYMODE_SDR) { - if (!pState->m_dispalyCacaoDirectly) + if (!pState->dispalyCacaoDirectly) { - m_colorConversionPS.Draw(cmdBuf2, m_HDRSRV); - m_GPUTimer.GetTimeStamp(cmdBuf2, "Color conversion"); + m_colorConversionPS.Draw(cmdBuf2, m_hdrSRV); + m_gpuTimer.GetTimeStamp(cmdBuf2, "Color conversion"); } } else @@ -1277,16 +1261,16 @@ void SampleRenderer::OnRender(State *pState, SwapChain *pSwapChain) // { { - m_toneMappingPS.Draw(cmdBuf2, m_HDRSRV, pState->exposure, pState->toneMapper); - m_GPUTimer.GetTimeStamp(cmdBuf2, "Tone mapping"); + m_toneMappingPS.Draw(cmdBuf2, m_hdrSRV, pState->exposure, pState->toneMapper); + m_gpuTimer.GetTimeStamp(cmdBuf2, "Tone mapping"); } } // Render HUD ------------------------------------------------------------------------ // { - m_ImGUI.Draw(cmdBuf2); - m_GPUTimer.GetTimeStamp(cmdBuf2, "ImGUI Rendering"); + m_imGUI.Draw(cmdBuf2); + m_gpuTimer.GetTimeStamp(cmdBuf2, "ImGUI Rendering"); } } } @@ -1296,72 +1280,68 @@ void SampleRenderer::OnRender(State *pState, SwapChain *pSwapChain) VkDescriptorBufferInfo cbDummyConstantBuffer; uint32_t *dummy; - m_ConstantBufferRing.AllocConstantBuffer(sizeof(*dummy), (void **)&dummy, &cbDummyConstantBuffer); + m_constantBufferRing.AllocConstantBuffer(sizeof(*dummy), (void **)&dummy, &cbDummyConstantBuffer); *dummy = 0; - VkDescriptorSet descriptorSet = m_directOutputDescriptorSets[m_curBackBuffer]; + VkDescriptorSet descriptorSet = m_cacaoApplyDirectDescriptorSets[m_curBackBuffer]; // modify Descriptor set - SetDescriptorSet(m_pDevice->GetDevice(), 1, m_cacaoOutputSRV, &m_directOutputSampler, descriptorSet); - m_ConstantBufferRing.SetDescriptorSet(0, sizeof(*dummy), descriptorSet); + SetDescriptorSet(m_pDevice->GetDevice(), 1, m_cacaoOutputSRV, &m_cacaoApplyDirectSampler, descriptorSet); + m_constantBufferRing.SetDescriptorSet(0, sizeof(*dummy), descriptorSet); // Draw! - m_directOutputPS.Draw(cmdBuf2, cbDummyConstantBuffer, descriptorSet); + m_cacaoApplyDirectPS.Draw(cmdBuf2, cbDummyConstantBuffer, descriptorSet); SetPerfMarkerEnd(cmdBuf2); - m_ImGUI.Draw(cmdBuf2); - m_GPUTimer.GetTimeStamp(cmdBuf2, "ImGUI Rendering"); + m_imGUI.Draw(cmdBuf2); + m_gpuTimer.GetTimeStamp(cmdBuf2, "ImGUI Rendering"); } - SetPerfMarkerEnd(cmdBuf2); - - m_GPUTimer.OnEndFrame(); - - vkCmdEndRenderPass(cmdBuf2); - - // Close & Submit the command list ---------------------------------------------------- - // - { - VkResult res = vkEndCommandBuffer(cmdBuf2); - assert(res == VK_SUCCESS); - - VkSemaphore ImageAvailableSemaphore; - VkSemaphore RenderFinishedSemaphores; - VkFence CmdBufExecutedFences; - pSwapChain->GetSemaphores(&ImageAvailableSemaphore, &RenderFinishedSemaphores, &CmdBufExecutedFences); - - VkPipelineStageFlags submitWaitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - VkSubmitInfo submit_info2; - submit_info2.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submit_info2.pNext = NULL; - submit_info2.waitSemaphoreCount = 1; - submit_info2.pWaitSemaphores = &ImageAvailableSemaphore; - submit_info2.pWaitDstStageMask = &submitWaitStage; - submit_info2.commandBufferCount = 1; - submit_info2.pCommandBuffers = &cmdBuf2; - submit_info2.signalSemaphoreCount = 1; - submit_info2.pSignalSemaphores = &RenderFinishedSemaphores; - - res = vkQueueSubmit(m_pDevice->GetGraphicsQueue(), 1, &submit_info2, CmdBufExecutedFences); - assert(res == VK_SUCCESS); - } + SetPerfMarkerEnd(cmdBuf2); + + m_gpuTimer.OnEndFrame(); + + vkCmdEndRenderPass(cmdBuf2); + + // Close & Submit the command list ---------------------------------------------------- + // + { + VkResult res = vkEndCommandBuffer(cmdBuf2); + assert(res == VK_SUCCESS); + + VkSemaphore ImageAvailableSemaphore; + VkSemaphore RenderFinishedSemaphores; + VkFence CmdBufExecutedFences; + pSwapChain->GetSemaphores(&ImageAvailableSemaphore, &RenderFinishedSemaphores, &CmdBufExecutedFences); + + VkPipelineStageFlags submitWaitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + VkSubmitInfo submit_info2; + submit_info2.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submit_info2.pNext = NULL; + submit_info2.waitSemaphoreCount = 1; + submit_info2.pWaitSemaphores = &ImageAvailableSemaphore; + submit_info2.pWaitDstStageMask = &submitWaitStage; + submit_info2.commandBufferCount = 1; + submit_info2.pCommandBuffers = &cmdBuf2; + submit_info2.signalSemaphoreCount = 1; + submit_info2.pSignalSemaphores = &RenderFinishedSemaphores; + + res = vkQueueSubmit(m_pDevice->GetGraphicsQueue(), 1, &submit_info2, CmdBufExecutedFences); + assert(res == VK_SUCCESS); + } } #ifdef FFX_CACAO_ENABLE_PROFILING -void SampleRenderer::GetCacaoTimingValues(State* pState, FfxCacaoDetailedTiming* timings) +void SampleRenderer::GetCacaoTimingValues(State* pState, FFX_CACAO_DetailedTiming* timings) { -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - if (pState->m_useDownsampledSsao) + if (pState->useDownsampledSsao) { - ffxCacaoVkGetDetailedTimings(m_cacaoContextDownsampled, timings); + FFX_CACAO_VkGetDetailedTimings(m_cacaoContextDownsampled, timings); } else { - ffxCacaoVkGetDetailedTimings(m_cacaoContextNative, timings); + FFX_CACAO_VkGetDetailedTimings(m_cacaoContextNative, timings); } -#else - ffxCacaoVkGetDetailedTimings(m_cacaoContextDownsampled, timings); -#endif } #endif diff --git a/sample/src/VK/SampleRenderer.h b/sample/src/VK/SampleRenderer.h index dd8eec9..f3ab168 100644 --- a/sample/src/VK/SampleRenderer.h +++ b/sample/src/VK/SampleRenderer.h @@ -1,6 +1,6 @@ // AMD SampleVK sample code -// -// Copyright(c) 2018 Advanced Micro Devices, Inc.All rights reserved. +// +// 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 @@ -18,7 +18,7 @@ // THE SOFTWARE. #pragma once -#include "ffx_cacao.h" +#include "ffx_cacao_impl.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; @@ -34,122 +34,118 @@ using namespace CAULDRON_VK; class SampleRenderer { public: - struct Spotlight - { - Camera light; - XMVECTOR color; - float intensity; - }; + struct Spotlight + { + Camera light; + XMVECTOR color; + float intensity; + }; - struct State - { - float time; - Camera camera; + struct State + { + float time; + Camera camera; - float exposure; - float iblFactor; - float emmisiveFactor; + float exposure; + float iblFactor; + float emmisiveFactor; - int toneMapper; - int skyDomeType; - bool bDrawBoundingBoxes; + int toneMapper; + int skyDomeType; + bool drawBoundingBoxes; - bool m_useTAA; + bool useTAA; - bool bDrawLightFrustum; + bool drawLightFrustum; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - bool m_useDownsampledSsao; -#endif - FfxCacaoSettings m_cacaoSettings; - bool m_useCacao; - bool m_dispalyCacaoDirectly; + bool useDownsampledSsao; + FFX_CACAO_Settings cacaoSettings; + bool useCacao; + bool dispalyCacaoDirectly; }; - void OnCreate(Device *pDevice, SwapChain *pSwapChain); - void OnDestroy(); + void OnCreate(Device *pDevice, SwapChain *pSwapChain); + void OnDestroy(); - void OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, uint32_t Width, uint32_t Height); - void OnDestroyWindowSizeDependentResources(); + void OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, uint32_t Width, uint32_t Height); + void OnDestroyWindowSizeDependentResources(); - int LoadScene(GLTFCommon *pGLTFCommon, int stage = 0); - void UnloadScene(); + int LoadScene(GLTFCommon *pGLTFCommon, int stage = 0); + void UnloadScene(); #ifdef FFX_CACAO_ENABLE_PROFILING - void GetCacaoTimingValues(State* pState, FfxCacaoDetailedTiming* timings); + void GetCacaoTimingValues(State* pState, FFX_CACAO_DetailedTiming* timings); #endif - const std::vector<TimeStamp> &GetTimingValues() { return m_TimeStamps; } + const std::vector<TimeStamp> &GetTimingValues() { return m_timeStamps; } - void OnRender(State *pState, SwapChain *pSwapChain); + void OnRender(State *pState, SwapChain *pSwapChain); private: - Device *m_pDevice; + Device *m_pDevice; -#ifdef FFX_CACAO_ENABLE_NATIVE_RESOLUTION - FfxCacaoVkContext *m_cacaoContextNative; -#endif - FfxCacaoVkContext *m_cacaoContextDownsampled; + FFX_CACAO_VkContext *m_cacaoContextNative; + FFX_CACAO_VkContext *m_cacaoContextDownsampled; - uint32_t m_Width; - uint32_t m_Height; + uint32_t m_width; + uint32_t m_height; - VkRect2D m_rectScissor; - VkViewport m_viewport; + VkRect2D m_rectScissor; + VkViewport m_viewport; - // Initialize helper classes - ResourceViewHeaps m_resourceViewHeaps; - UploadHeap m_UploadHeap; - DynamicBufferRing m_ConstantBufferRing; - StaticBufferPool m_VidMemBufferPool; - StaticBufferPool m_SysMemBufferPool; - CommandListRing m_CommandListRing; - GPUTimestamps m_GPUTimer; + // Initialize helper classes + ResourceViewHeaps m_resourceViewHeaps; + UploadHeap m_uploadHeap; + DynamicBufferRing m_constantBufferRing; + StaticBufferPool m_vidMemBufferPool; + StaticBufferPool m_sysMemBufferPool; + CommandListRing m_commandListRing; + GPUTimestamps m_gpuTimer; - //gltf passes - GltfPbrPass *m_gltfPBR; + //gltf passes + GltfPbrPass *m_gltfPBR; GltfPbrPass *m_gltfPbrNonMsaa; - GltfBBoxPass *m_gltfBBox; - GltfDepthPass *m_gltfDepth; - GLTFTexturesAndBuffers *m_pGLTFTexturesAndBuffers; - - // effects - Bloom m_bloom; - SkyDome m_skyDome; - DownSamplePS m_downSample; - SkyDomeProc m_skyDomeProc; - ToneMapping m_toneMappingPS; - ToneMappingCS m_toneMappingCS; - ColorConversionPS m_colorConversionPS; - - // GUI - ImGUI m_ImGUI; - - // Temporary render targets - - // depth buffer - Texture m_depthBuffer; - VkImageView m_depthBufferDSV; - - // shadowmaps - Texture m_shadowMap; - VkImageView m_shadowMapDSV; - VkImageView m_shadowMapSRV; - - // MSAA RT - Texture m_HDRMSAA; - VkImageView m_HDRMSAASRV; + GltfBBoxPass *m_gltfBBox; + GltfDepthPass *m_gltfDepth; + GLTFTexturesAndBuffers *m_pGLTFTexturesAndBuffers; + + // effects + Bloom m_bloom; + SkyDome m_skyDome; + DownSamplePS m_downSample; + SkyDomeProc m_skyDomeProc; + ToneMapping m_toneMappingPS; + ToneMappingCS m_toneMappingCS; + ColorConversionPS m_colorConversionPS; + + // GUI + ImGUI m_imGUI; + + // Temporary render targets + + // depth buffer + Texture m_depthBuffer; + VkImageView m_depthBufferDSV; + + // shadowmaps + Texture m_shadowMap; + VkImageView m_shadowMapDSV; + VkImageView m_shadowMapSRV; + + // MSAA RT + Texture m_hdrMSAA; + VkImageView m_hdrMSAASRV; // Non MSAA - Texture m_NormalBufferNonMsaa; - Texture m_DepthBufferNonMsaa; - VkImageView m_NormalBufferNonMsaaView; - VkImageView m_DepthBufferNonMsaaView; + Texture m_normalBufferNonMsaa; + Texture m_depthBufferNonMsaa; + VkImageView m_normalBufferNonMsaaView; + VkImageView m_depthBufferNonMsaaView; - // Resolved RT - Texture m_HDR; - VkImageView m_HDRSRV; - VkImageView m_HDRUAV; + // Resolved RT + Texture m_hdr; + VkImageView m_hdrSRV; + VkImageView m_hdrUAV; // CACAO Texture m_cacaoOutput; @@ -160,25 +156,25 @@ class SampleRenderer uint32_t m_curBackBuffer; - VkSampler m_directOutputSampler; - VkDescriptorSet m_directOutputDescriptorSets[backBufferCount]; - VkDescriptorSetLayout m_directOutputDescriptorSetLayout; - PostProcPS m_directOutputPS; + VkSampler m_cacaoApplyDirectSampler; + VkDescriptorSet m_cacaoApplyDirectDescriptorSets[backBufferCount]; + VkDescriptorSetLayout m_cacaoApplyDirectDescriptorSetLayout; + PostProcPS m_cacaoApplyDirectPS; - // widgets - Wireframe m_wireframe; - WireframeBox m_wireframeBox; + // widgets + Wireframe m_wireframe; + WireframeBox m_wireframeBox; - VkRenderPass m_render_pass_shadow; - VkRenderPass m_render_pass_HDR_MSAA; - VkRenderPass m_render_pass_PBR_HDR; - VkRenderPass m_render_pass_non_msaa; + VkRenderPass m_renderPassShadow; + VkRenderPass m_renderPassHDRMSAA; + VkRenderPass m_renderPassPBRHDR; + VkRenderPass m_renderPassNonMSAA; - VkFramebuffer m_pFrameBuffer_shadow; - VkFramebuffer m_pFrameBuffer_HDR_MSAA; - VkFramebuffer m_pFrameBuffer_PBR_HDR; - VkFramebuffer m_pFrameBuffer_non_msaa; + VkFramebuffer m_pFrameBufferShadow; + VkFramebuffer m_pFrameBufferHDRMSAA; + VkFramebuffer m_pFrameBufferPBRHDR; + VkFramebuffer m_pFrameBufferNonMSAA; - std::vector<TimeStamp> m_TimeStamps; + std::vector<TimeStamp> m_timeStamps; };