From c52944f547884774a1b33066f740e6bf89f927f5 Mon Sep 17 00:00:00 2001 From: "Kramer, Lou" Date: Wed, 26 Aug 2020 10:35:15 +0000 Subject: [PATCH] v2.0 --- README.md | 40 +- docs/FidelityFX_SPD.pdf | Bin 666136 -> 677937 bytes ffx-spd/ffx_spd.h | 591 +++++++++------- sample/CMakeLists.txt | 4 +- sample/README.md | 27 +- sample/libs/cauldron | 2 +- sample/src/Common/SpdSample.json | 41 ++ sample/src/DX12/CMakeLists.txt | 34 +- sample/src/DX12/CSDownsampler.cpp | 199 +++--- sample/src/DX12/CSDownsampler.h | 38 +- sample/src/DX12/CSDownsampler.hlsl | 16 +- sample/src/DX12/PSDownsampler.cpp | 134 ++-- sample/src/DX12/PSDownsampler.h | 36 +- sample/src/DX12/PSDownsampler.hlsl | 5 +- sample/src/DX12/SPDCS.cpp | 443 ++++++++++++ sample/src/DX12/SPDCS.h | 95 +++ sample/src/DX12/SPDIntegration.hlsl | 189 ++++++ .../src/DX12/SPDIntegrationLinearSampler.hlsl | 200 ++++++ .../{SPD_Renderer.cpp => SPDRenderer.cpp} | 194 +++--- .../DX12/{SPD_Renderer.h => SPDRenderer.h} | 82 +-- sample/src/DX12/SPDSample.cpp | 496 ++++++++++++++ .../src/{VK/SPD_Sample.h => DX12/SPDSample.h} | 50 +- sample/src/DX12/SPDVersions.cpp | 194 ++++++ sample/src/DX12/SPDVersions.h | 56 ++ sample/src/DX12/SPD_CS.cpp | 268 -------- sample/src/DX12/SPD_CS.h | 73 -- sample/src/DX12/SPD_CS_Linear_Sampler.cpp | 285 -------- sample/src/DX12/SPD_CS_Linear_Sampler.h | 73 -- sample/src/DX12/SPD_Integration.hlsl | 120 ---- .../DX12/SPD_Integration_Linear_Sampler.hlsl | 133 ---- sample/src/DX12/SPD_Sample.cpp | 377 ----------- sample/src/DX12/SPD_Versions.cpp | 206 ------ sample/src/DX12/SPD_Versions.h | 75 --- sample/src/VK/CMakeLists.txt | 64 +- sample/src/VK/CSDownsampler.cpp | 282 ++++---- sample/src/VK/CSDownsampler.glsl | 13 +- sample/src/VK/CSDownsampler.h | 40 +- sample/src/VK/PSDownsampler.cpp | 251 ++++--- sample/src/VK/PSDownsampler.h | 45 +- sample/src/VK/SPDCS.cpp | 631 ++++++++++++++++++ sample/src/VK/SPDCS.h | 100 +++ ...inear_Sampler.glsl => SPDIntegration.glsl} | 169 +++-- sample/src/VK/SPDIntegration.hlsl | 189 ++++++ .../src/VK/SPDIntegrationLinearSampler.glsl | 200 ++++++ .../src/VK/SPDIntegrationLinearSampler.hlsl | 196 ++++++ .../VK/{SPD_Renderer.cpp => SPDRenderer.cpp} | 452 ++++++------- .../src/VK/{SPD_Renderer.h => SPDRenderer.h} | 96 +-- sample/src/VK/SPDSample.cpp | 561 ++++++++++++++++ .../src/{DX12/SPD_Sample.h => VK/SPDSample.h} | 25 +- sample/src/VK/SPDVersions.cpp | 204 ++++++ sample/src/VK/SPDVersions.h | 55 ++ sample/src/VK/SPD_CS.cpp | 353 ---------- sample/src/VK/SPD_CS.h | 77 --- sample/src/VK/SPD_CS_Linear_Sampler.cpp | 397 ----------- sample/src/VK/SPD_CS_Linear_Sampler.h | 79 --- sample/src/VK/SPD_Integration.glsl | 124 ---- sample/src/VK/SPD_Integration.hlsl | 117 ---- .../VK/SPD_Integration_Linear_Sampler.hlsl | 131 ---- sample/src/VK/SPD_Sample.cpp | 412 ------------ sample/src/VK/SPD_Versions.cpp | 226 ------- sample/src/VK/SPD_Versions.h | 74 -- 61 files changed, 5434 insertions(+), 4905 deletions(-) create mode 100644 sample/src/Common/SpdSample.json create mode 100644 sample/src/DX12/SPDCS.cpp create mode 100644 sample/src/DX12/SPDCS.h create mode 100644 sample/src/DX12/SPDIntegration.hlsl create mode 100644 sample/src/DX12/SPDIntegrationLinearSampler.hlsl rename sample/src/DX12/{SPD_Renderer.cpp => SPDRenderer.cpp} (78%) rename sample/src/DX12/{SPD_Renderer.h => SPDRenderer.h} (64%) create mode 100644 sample/src/DX12/SPDSample.cpp rename sample/src/{VK/SPD_Sample.h => DX12/SPDSample.h} (57%) create mode 100644 sample/src/DX12/SPDVersions.cpp create mode 100644 sample/src/DX12/SPDVersions.h delete mode 100644 sample/src/DX12/SPD_CS.cpp delete mode 100644 sample/src/DX12/SPD_CS.h delete mode 100644 sample/src/DX12/SPD_CS_Linear_Sampler.cpp delete mode 100644 sample/src/DX12/SPD_CS_Linear_Sampler.h delete mode 100644 sample/src/DX12/SPD_Integration.hlsl delete mode 100644 sample/src/DX12/SPD_Integration_Linear_Sampler.hlsl delete mode 100644 sample/src/DX12/SPD_Sample.cpp delete mode 100644 sample/src/DX12/SPD_Versions.cpp delete mode 100644 sample/src/DX12/SPD_Versions.h create mode 100644 sample/src/VK/SPDCS.cpp create mode 100644 sample/src/VK/SPDCS.h rename sample/src/VK/{SPD_Integration_Linear_Sampler.glsl => SPDIntegration.glsl} (50%) create mode 100644 sample/src/VK/SPDIntegration.hlsl create mode 100644 sample/src/VK/SPDIntegrationLinearSampler.glsl create mode 100644 sample/src/VK/SPDIntegrationLinearSampler.hlsl rename sample/src/VK/{SPD_Renderer.cpp => SPDRenderer.cpp} (66%) rename sample/src/VK/{SPD_Renderer.h => SPDRenderer.h} (58%) create mode 100644 sample/src/VK/SPDSample.cpp rename sample/src/{DX12/SPD_Sample.h => VK/SPDSample.h} (78%) create mode 100644 sample/src/VK/SPDVersions.cpp create mode 100644 sample/src/VK/SPDVersions.h delete mode 100644 sample/src/VK/SPD_CS.cpp delete mode 100644 sample/src/VK/SPD_CS.h delete mode 100644 sample/src/VK/SPD_CS_Linear_Sampler.cpp delete mode 100644 sample/src/VK/SPD_CS_Linear_Sampler.h delete mode 100644 sample/src/VK/SPD_Integration.glsl delete mode 100644 sample/src/VK/SPD_Integration.hlsl delete mode 100644 sample/src/VK/SPD_Integration_Linear_Sampler.hlsl delete mode 100644 sample/src/VK/SPD_Sample.cpp delete mode 100644 sample/src/VK/SPD_Versions.cpp delete mode 100644 sample/src/VK/SPD_Versions.h diff --git a/README.md b/README.md index 508f49d..4bbaf30 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,32 @@ # FidelityFX SPD Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# Changelist v2.0 + +- Added support for cube and array textures. SpdDownsample and SpdDownsampleH shader functions now take index of texture slice as an additional parameter. For regular texture use 0. +- Added support for updating only sub-rectangle of the texture. Additional, optional parameter workGroupOffset added to shader functions SpdDownsample and SpdDownsampleH. +- Added C function SpdSetup that helps to setup constants to be passed as a constant buffer. +- The global atomic counter is automatically reset to 0 by the shader at the end, so you do not need to clear it before every use, just once after creation + # Single Pass Downsampler - SPD FidelityFX Single Pass Downsampler (SPD) provides an RDNA-optimized solution for generating up to 12 MIP levels of a texture. +- Generates up to 12 MIP levels (maximum source texture size is 4096x4096) per slice. +- Supports Texture2DArrays / CubeTextures: downsamples all slices within one single disptach call. +- Single compute dispatch. +- User defined 2x2 reduction function. +- User controlled border handling. +- Supports various image formats. +- HLSL and GLSL versions available. +- Rapid Packed Math support. +- Uses optionally subgroup operations / SM6+ wave operations, which can provide faster performance. +- Supports downsampling of a sub-rectangle from the source texture: useful for atlas textures in which only a known region got updated # Sample Build Instructions 1. Clone submodules by running 'git submodule update --init --recursive' (so you get the Cauldron framework too) 2. Run sample/build/GenerateSolutions.bat -3. open solution, build + run + have fun 😊 +3. Open solution, build + run + have fun 😊 # SPD Files You can find them in ffx-spd @@ -21,15 +38,26 @@ Downsampler - PS: computes each mip in a separate pixel shader pass - Multipass CS: computes each mip in a separate compute shader pass - SPD CS: uses the SPD library, computes all mips (up to a source texture of size 4096²) in a single pass -- SPD CS linear sampler: uses the SPD library and for sampling the source texture a linear sampler -SPD Versions -- NO-WaveOps: uses only LDS to share the data between threads +SPD Load Versions +- Load: uses a load to fetch from the source texture +- Linear Sampler: uses a sampler to fetch from the source texture. Sampler must meet the user defined reduction function. + +SPD WaveOps Versions +- No-WaveOps: uses only LDS to share the data between threads - WaveOps: uses Intrinsics and LDS to share the data between threads -SPD Non-Packed / Packed Version +SPD Non-Packed / Packed Versions - Non-Packed: uses fp32 - Packed: uses fp16, reduced register pressure # Recommendations -We recommend to use the WapeOps path when supported. If higher precision is not needed, you can enable the packed mode - it has less register pressure and can run a bit faster as well. \ No newline at end of file +We recommend to use the WaveOps path when supported. If higher precision is not needed, you can enable the packed mode - it has less register pressure and can run a bit faster as well. +If you compute the average for each 2x2 quad, we also recommend to use a linear sampler to fetch from the source texture instead of four separate loads. + +# Known issues +Please use driver 20.8.3 or newer. There is a known issue on DX12 when using the SPD No-WaveOps Packed version. +It may appear as "Access violation reading location ..." during CreateComputePipelineState, with top of the stack +pointing to amdxc64.dll. +To workaround this issue, you may advise players to update their graphics driver or don't compile and use +a different SPD version, e.g. a Non-Packed version. \ No newline at end of file diff --git a/docs/FidelityFX_SPD.pdf b/docs/FidelityFX_SPD.pdf index 68f343b4bdabdba391cf22bedbbca8b0e39f6440..bc80ecc987e9da3faa2ca8263817f797ec208a85 100644 GIT binary patch delta 166993 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

$l}^u4@dup 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 delta 156551 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 diff --git a/ffx-spd/ffx_spd.h b/ffx-spd/ffx_spd.h index 0a6b13d..e0b58b8 100644 --- a/ffx-spd/ffx_spd.h +++ b/ffx-spd/ffx_spd.h @@ -1,7 +1,7 @@ //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // -// [FFX SPD] Single Pass Downsampler 1.0 +// [FFX SPD] Single Pass Downsampler 2.0 // //============================================================================================================================== // LICENSE @@ -20,17 +20,56 @@ // 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. +// //------------------------------------------------------------------------------------------------------------------------------ - +// CHANGELIST v2.0 +// =============== +// - Added support for cube and array textures. SpdDownsample and SpdDownsampleH shader functions now take index of texture slice +// as an additional parameter. For regular texture use 0. +// - Added support for updating only sub-rectangle of the texture. Additional, optional parameter workGroupOffset added to shader +// functions SpdDownsample and SpdDownsampleH. +// - Added C function SpdSetup that helps to setup constants to be passed as a constant buffer. +// - The global atomic counter is automatically reset to 0 by the shader at the end, so you do not need to clear it before every +// use, just once after creation +// //------------------------------------------------------------------------------------------------------------------------------ // INTEGRATION SUMMARY FOR CPU // =========================== // // you need to provide as constants: // // number of mip levels to be computed (maximum is 12) // // number of total thread groups: ((widthInPixels+63)>>6) * ((heightInPixels+63)>>6) +// // workGroupOffset -> by default 0, if you only downsample a rectancle within the source texture use SpdSetup function to calculate correct offset // ... // // Dispatch the shader such that each thread group works on a 64x64 sub-tile of the source image -// vkCmdDispatch(cmdBuf,(widthInPixels+63)>>6,(heightInPixels+63)>>6,1); +// // for Cube Textures or Texture2DArray, use the z dimension +// vkCmdDispatch(cmdBuf,(widthInPixels+63)>>6,(heightInPixels+63)>>6, slices); + +// // you can also use the SpdSetup function: +// //on top of your cpp file: +// #define A_CPU +// #include "ffx_a.h" +// #include "ffx_spd.h" +// // before your dispatch call, use SpdSetup function to get your constants +// varAU2(dispatchThreadGroupCountXY); // output variable +// varAU2(workGroupOffset); // output variable, this constants are required if Left and Top are not 0,0 +// varAU2(numWorkGroupsAndMips); // output variable +// // input information about your source texture: +// // left and top of the rectancle within your texture you want to downsample +// // width and height of the rectancle you want to downsample +// // if complete source texture should get downsampled: left = 0, top = 0, width = sourceTexture.width, height = sourceTexture.height +// varAU4(rectInfo) = initAU4(0, 0, m_Texture.GetWidth(), m_Texture.GetHeight()); // left, top, width, height +// SpdSetup(dispatchThreadGroupCountXY, workGroupOffset, numWorkGroupsAndMips, rectInfo); +// ... +// // constants: +// data.numWorkGroupsPerSlice = numWorkGroupsAndMips[0]; +// data.mips = numWorkGroupsAndMips[1]; +// data.workGroupOffset[0] = workGroupOffset[0]; +// data.workGroupOffset[1] = workGroupOffset[1]; +// ... +// uint32_t dispatchX = dispatchThreadGroupCountXY[0]; +// uint32_t dispatchY = dispatchThreadGroupCountXY[1]; +// uint32_t dispatchZ = m_CubeTexture.GetArraySize(); // slices - for 2D Texture this is 1, for cube texture 6 +// vkCmdDispatch(cmd_buf, dispatchX, dispatchY, dispatchZ); //------------------------------------------------------------------------------------------------------------------------------ // INTEGRATION SUMMARY FOR GPU @@ -39,37 +78,44 @@ // [SAMPLER] - if you want to use a sampler with linear filtering for loading the source image // follow additionally the instructions marked with [SAMPLER] // add following define: -// #SPD_LINEAR_SAMPLER +// #define SPD_LINEAR_SAMPLER // this is recommended, as using one sample() with linear filter to reduce 2x2 is faster // than 4x load() plus manual averaging // // Setup layout. Example below for VK_FORMAT_R16G16B16A16_SFLOAT. -// // Note: If you use UNORM/SRGB format, you need to convert to linear space +// // Note: If you use SRGB format for UAV load() and store() (if it's supported), you need to convert to and from linear space // // when using UAV load() and store() -// // conversion to linear (load function): x*x -// // conversion from linear (store function): sqrt() +// // approximate conversion to linear (load function): x*x +// // approximate conversion from linear (store function): sqrt() +// // or use more accurate functions from ffx_a.h: AFromSrgbF1(value) and AToSrgbF1(value) +// // Recommendation: use UNORM format instead of SRGB for UAV access, and SRGB for SRV access +// // look in the sample app to see how it's done // // source image +// // if cube texture use image2DArray / Texture2DArray and adapt your load/store/sample calls // GLSL: layout(set=0,binding=0,rgba16f)uniform image2D imgSrc; // [SAMPLER]: layout(set=0,binding=0)uniform texture2D imgSrc; // HLSL: [[vk::binding(0)]] Texture2D imgSrc :register(u0); -// // destination -> 12 is the maximum number of mips supported by DS +// // destination -> 12 is the maximum number of mips supported by SPD // GLSL: layout(set=0,binding=1,rgba16f) uniform coherent image2D imgDst[12]; // HLSL: [[vk::binding(1)]] globallycoherent RWTexture2D imgDst[12] :register(u1); // // global atomic counter - MUST be initialized to 0 +// // SPD resets the counter back after each run by calling SpdResetAtomicCounter(slice) +// // if you have more than 1 slice (== if you downsample a cube texture or a texture2Darray) +// // you have an array of counters: counter[6] -> if you have 6 slices for example // // GLSL: -// layout(std430, set=0, binding=2) coherent buffer globalAtomicBuffer +// layout(std430, set=0, binding=2) coherent buffer SpdGlobalAtomicBuffer // { // uint counter; -// } globalAtomic; +// } spdGlobalAtomic; // // HLSL: -// struct globalAtomicBuffer +// struct SpdGlobalAtomicBuffer // { // uint counter; // }; -// [[vk::binding(2)]] globallycoherent RWStructuredBuffer globalAtomic; +// [[vk::binding(2)]] globallycoherent RWStructuredBuffer spdGlobalAtomic; // // [SAMPLER] add sampler // GLSL: layout(set=0, binding=3) uniform sampler srcSampler; @@ -79,15 +125,19 @@ // // or calculate within shader // // [SAMPLER] when using sampler add inverse source image size // // GLSL: -// layout(push_constant) uniform pushConstants { +// layout(push_constant) uniform SpdConstants { // uint mips; // needed to opt out earlier if mips are < 12 // uint numWorkGroups; // number of total thread groups, so numWorkGroupsX * numWorkGroupsY * 1 +// // it is important to NOT take the number of slices (z dimension) into account here +// // as each slice has its own counter! +// vec2 workGroupOffset; // optional - use SpdSetup() function to calculate correct workgroup offset // } spdConstants; // // HLSL: // [[vk::push_constant]] // cbuffer spdConstants { -// uint mips; -// uint numWorkGroups; +// uint mips; +// uint numWorkGroups; +// float2 workGroupOffset; // optional // }; // ... @@ -105,18 +155,18 @@ // ... // // Define LDS variables -// shared AF4 spd_intermediate[16][16]; // HLSL: groupshared -// shared AU1 spd_counter; // HLSL: groupshared +// shared AF4 spdIntermediate[16][16]; // HLSL: groupshared +// shared AU1 spdCounter; // HLSL: groupshared // // PACKED version -// shared AH4 spd_intermediate[16][16]; // HLSL: groupshared +// shared AH4 spdIntermediate[16][16]; // HLSL: groupshared // // Note: You can also use -// shared AF1 spd_intermediateR[16][16]; -// shared AF1 spd_intermediateG[16][16]; -// shared AF1 spd_intermediateB[16][16]; -// shared AF1 spd_intermediateA[16][16]; +// shared AF1 spdIntermediateR[16][16]; +// shared AF1 spdIntermediateG[16][16]; +// shared AF1 spdIntermediateB[16][16]; +// shared AF1 spdIntermediateA[16][16]; // // or for Packed version: -// shared AH2 spd_intermediateRG[16][16]; -// shared AH2 spd_intermediateBA[16][16]; +// shared AH2 spdIntermediateRG[16][16]; +// shared AH2 spdIntermediateBA[16][16]; // // This is potentially faster // // Adapt your load and store functions accordingly @@ -135,17 +185,19 @@ // // conversion to linear (load function): x*x // // conversion from linear (store function): sqrt() +// AU1 slice parameter is for Cube textures and texture2DArray +// if downsampling Texture2D you can ignore this parameter, otherwise use it to access correct slice // // Load from source image -// GLSL: AF4 SpdLoadSourceImage(ASU2 p){return imageLoad(imgSrc, p);} -// HLSL: AF4 SpdLoadSourceImage(ASU2 tex){return imgSrc[tex];} +// GLSL: AF4 SpdLoadSourceImage(ASU2 p, AU1 slice){return imageLoad(imgSrc, p);} +// HLSL: AF4 SpdLoadSourceImage(ASU2 tex, AU1 slice){return imgSrc[tex];} // [SAMPLER] don't forget to add the define #SPD_LINEAR_SAMPLER :) // GLSL: -// AF4 SpdLoadSourceImage(ASU2 p){ +// AF4 SpdLoadSourceImage(ASU2 p, AU1 slice){ // AF2 textureCoord = p * invInputSize + invInputSize; // return texture(sampler2D(imgSrc, srcSampler), textureCoord); // } // HLSL: -// AF4 SpdLoadSourceImage(ASU2 p){ +// AF4 SpdLoadSourceImage(ASU2 p, AU1 slice){ // AF2 textureCoord = p * invInputSize + invInputSize; // return imgSrc.SampleLevel(srcSampler, textureCoord, 0); // } @@ -153,28 +205,34 @@ // // SpdLoad() takes a 32-bit signed integer 2D coordinate and loads color. // // Loads the 5th mip level, each value is computed by a different thread group // // last thread group will access all its elements and compute the subsequent mips -// GLSL: AF4 SpdLoad(ASU2 p){return imageLoad(imgDst[5],p);} -// HLSL: AF4 SpdLoad(ASU2 tex){return imgDst[5][tex];} +// // reminder: if non-power-of-2 textures, add border controls if you do not want to read zeros past the border +// GLSL: AF4 SpdLoad(ASU2 p, AU1 slice){return imageLoad(imgDst[5],p);} +// HLSL: AF4 SpdLoad(ASU2 tex, AU1 slice){return imgDst[5][tex];} // Define the store function -// GLSL: void SpdStore(ASU2 p, AF4 value, AU1 mip){imageStore(imgDst[mip], p, value);} -// HLSL: void SpdStore(ASU2 pix, AF4 value, AU1 index){imgDst[index][pix] = value;} +// GLSL: void SpdStore(ASU2 p, AF4 value, AU1 mip, AU1 slice){imageStore(imgDst[mip], p, value);} +// HLSL: void SpdStore(ASU2 pix, AF4 value, AU1 mip, AU1 slice){imgDst[mip][pix] = value;} // // Define the atomic counter increase function +// // each slice only reads and stores to its specific slice counter +// // so, if you have several slices it's +// // InterlockedAdd(spdGlobalAtomic[0].counter[slice], 1, spdCounter); // // GLSL: -// void SpdIncreaseAtomicCounter(){spd_counter = atomicAdd(globalAtomic.counter, 1);} -// AU1 SpdGetAtomicCounter() {return spd_counter;} +// void SpdIncreaseAtomicCounter(AU1 slice){spdCounter = atomicAdd(spdGlobalAtomic.counter, 1);} +// AU1 SpdGetAtomicCounter() {return spdCounter;} +// void SpdResetAtomicCounter(AU1 slice){spdGlobalAtomic.counter[slice] = 0;} // // HLSL: -// void SpdIncreaseAtomicCounter(){InterlockedAdd(globalAtomic[0].counter, 1, spd_counter);} -// AU1 SpdGetAtomicCounter(){return spd_counter;} +// void SpdIncreaseAtomicCounter(AU1 slice){InterlockedAdd(spdGlobalAtomic[0].counter, 1, spdCounter);} +// AU1 SpdGetAtomicCounter(){return spdCounter;} +// void SpdResetAtomicCounter(AU1 slice){spdGlobalAtomic[0].counter[slice] = 0;} // // Define the LDS load and store functions // // GLSL: -// AF4 SpdLoadIntermediate(AU1 x, AU1 y){return spd_intermediate[x][y];} -// void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value){spd_intermediate[x][y] = value;} +// AF4 SpdLoadIntermediate(AU1 x, AU1 y){return spdIntermediate[x][y];} +// void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value){spdIntermediate[x][y] = value;} // // HLSL: -// AF4 SpdLoadIntermediate(AU1 x, AU1 y){return spd_intermediate[x][y];} -// void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value){spd_intermediate[x][y] = value;} +// AF4 SpdLoadIntermediate(AU1 x, AU1 y){return spdIntermediate[x][y];} +// void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value){spdIntermediate[x][y] = value;} // // Define your reduction function: takes as input the four 2x2 values and returns 1 output value // Example below: computes the average value @@ -182,16 +240,16 @@ // // PACKED VERSION // Load from source image -// GLSL: AH4 SpdLoadSourceImageH(ASU2 p){return AH4(imageLoad(imgSrc, p));} -// HLSL: AH4 SpdLoadSourceImageH(ASU2 tex){return AH4(imgSrc[tex]);} +// GLSL: AH4 SpdLoadSourceImageH(ASU2 p, AU1 slice){return AH4(imageLoad(imgSrc, p));} +// HLSL: AH4 SpdLoadSourceImageH(ASU2 tex, AU1 slice){return AH4(imgSrc[tex]);} // [SAMPLER] // GLSL: -// AH4 SpdLoadSourceImageH(ASU2 p){ +// AH4 SpdLoadSourceImageH(ASU2 p, AU1 slice){ // AF2 textureCoord = p * invInputSize + invInputSize; // return AH4(texture(sampler2D(imgSrc, srcSampler), textureCoord)); // } // HLSL: -// AH4 SpdLoadSourceImageH(ASU2 p){ +// AH4 SpdLoadSourceImageH(ASU2 p, AU1 slice){ // AF2 textureCoord = p * invInputSize + invInputSize; // return AH4(imgSrc.SampleLevel(srcSampler, textureCoord, 0)); // } @@ -199,28 +257,28 @@ // // SpdLoadH() takes a 32-bit signed integer 2D coordinate and loads color. // // Loads the 5th mip level, each value is computed by a different thread group // // last thread group will access all its elements and compute the subsequent mips -// GLSL: AH4 SpdLoadH(ASU2 p){return AH4(imageLoad(imgDst[5],p));} -// HLSL: AH4 SpdLoadH(ASU2 tex){return AH4(imgDst[5][tex]);} +// GLSL: AH4 SpdLoadH(ASU2 p, AU1 slice){return AH4(imageLoad(imgDst[5],p));} +// HLSL: AH4 SpdLoadH(ASU2 tex, AU1 slice){return AH4(imgDst[5][tex]);} // Define the store function -// GLSL: void SpdStoreH(ASU2 p, AH4 value, AU1 mip){imageStore(imgDst[mip], p, AF4(value));} -// HLSL: void SpdStoreH(ASU2 pix, AH4 value, AU1 index){imgDst[index][pix] = AF4(value);} +// GLSL: void SpdStoreH(ASU2 p, AH4 value, AU1 mip, AU1 slice){imageStore(imgDst[mip], p, AF4(value));} +// HLSL: void SpdStoreH(ASU2 pix, AH4 value, AU1 index, AU1 slice){imgDst[index][pix] = AF4(value);} // // Define the atomic counter increase function // // GLSL: -// void SpdIncreaseAtomicCounter(){spd_counter = atomicAdd(globalAtomic.counter, 1);} -// AU1 SpdGetAtomicCounter() {return spd_counter;} +// void SpdIncreaseAtomicCounter(AU1 slice){spd_counter = atomicAdd(spdGlobalAtomic.counter, 1);} +// AU1 SpdGetAtomicCounter() {return spdCounter;} // // HLSL: -// void SpdIncreaseAtomicCounter(){InterlockedAdd(globalAtomic[0].counter, 1, spd_counter);} -// AU1 SpdGetAtomicCounter(){return spd_counter;} +// void SpdIncreaseAtomicCounter(AU1 slice){InterlockedAdd(spdGlobalAtomic[0].counter, 1, spdCounter);} +// AU1 SpdGetAtomicCounter(){return spdCounter;} -// // Define the lds load and store functions +// // Define the LDS load and store functions // // GLSL: -// AH4 SpdLoadIntermediateH(AU1 x, AU1 y){return spd_intermediate[x][y];} -// void SpdStoreIntermediateH(AU1 x, AU1 y, AH4 value){spd_intermediate[x][y] = value;} +// AH4 SpdLoadIntermediateH(AU1 x, AU1 y){return spdIntermediate[x][y];} +// void SpdStoreIntermediateH(AU1 x, AU1 y, AH4 value){spdIntermediate[x][y] = value;} // // HLSL: -// AH4 SpdLoadIntermediate(AU1 x, AU1 y){return spd_intermediate[x][y];} -// void SpdStoreIntermediate(AU1 x, AU1 y, AH4 value){spd_intermediate[x][y] = value;} +// AH4 SpdLoadIntermediate(AU1 x, AU1 y){return spdIntermediate[x][y];} +// void SpdStoreIntermediate(AU1 x, AU1 y, AH4 value){spdIntermediate[x][y] = value;} // // Define your reduction function: takes as input the four 2x2 values and returns 1 output value // Example below: computes the average value @@ -240,42 +298,80 @@ // layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; // void main(){ // // Call the downsampling function +// // WorkGroupId.z should be 0 if you only downsample a Texture2D! // SpdDownsample(AU2(gl_WorkGroupID.xy), AU1(gl_LocalInvocationIndex), -// AU1(spdConstants.mips), AU1(spdConstants.numWorkGroups)); +// AU1(spdConstants.mips), AU1(spdConstants.numWorkGroups), AU1(WorkGroupId.z)); // // // PACKED: // SpdDownsampleH(AU2(gl_WorkGroupID.xy), AU1(gl_LocalInvocationIndex), -// AU1(spdConstants.mips), AU1(spdConstants.numWorkGroups)); +// AU1(spdConstants.mips), AU1(spdConstants.numWorkGroups), AU1(WorkGroupId.z)); // ... // // HLSL: // [numthreads(256,1,1)] // void main(uint3 WorkGroupId : SV_GroupID, uint LocalThreadIndex : SV_GroupIndex) { // SpdDownsample(AU2(WorkGroupId.xy), AU1(LocalThreadIndex), -// AU1(mips), AU1(numWorkGroups)); +// AU1(mips), AU1(numWorkGroups), AU1(WorkGroupId.z)); // // // PACKED: // SpdDownsampleH(AU2(WorkGroupId.xy), AU1(LocalThreadIndex), -// AU1(mips), AU1(numWorkGroups)); +// AU1(mips), AU1(numWorkGroups), AU1(WorkGroupId.z)); // ... // //------------------------------------------------------------------------------------------------------------------------------ +//============================================================================================================================== +// SPD Setup +//============================================================================================================================== +#ifdef A_CPU +A_STATIC void SpdSetup( +outAU2 dispatchThreadGroupCountXY, // CPU side: dispatch thread group count xy +outAU2 workGroupOffset, // GPU side: pass in as constant +outAU2 numWorkGroupsAndMips, // GPU side: pass in as constant +inAU4 rectInfo, // left, top, width, height +ASU1 mips // optional: if -1, calculate based on rect width and height +){ + workGroupOffset[0] = rectInfo[0] / 64; // rectInfo[0] = left + workGroupOffset[1] = rectInfo[1] / 64; // rectInfo[1] = top + + AU1 endIndexX = (rectInfo[0] + rectInfo[2] - 1) / 64; // rectInfo[0] = left, rectInfo[2] = width + AU1 endIndexY = (rectInfo[1] + rectInfo[3] - 1) / 64; // rectInfo[1] = top, rectInfo[3] = height + + dispatchThreadGroupCountXY[0] = endIndexX + 1 - workGroupOffset[0]; + dispatchThreadGroupCountXY[1] = endIndexY + 1 - workGroupOffset[1]; + + numWorkGroupsAndMips[0] = (dispatchThreadGroupCountXY[0]) * (dispatchThreadGroupCountXY[1]); + + if (mips >= 0) { + numWorkGroupsAndMips[1] = AU1(mips); + } else { // calculate based on rect width and height + AU1 resolution = AMaxU1(rectInfo[2], rectInfo[3]); + numWorkGroupsAndMips[1] = AU1((AMinF1(AFloorF1(ALog2F1(AF1(resolution))), AF1(12)))); + } +} - +A_STATIC void SpdSetup( + outAU2 dispatchThreadGroupCountXY, // CPU side: dispatch thread group count xy + outAU2 workGroupOffset, // GPU side: pass in as constant + outAU2 numWorkGroupsAndMips, // GPU side: pass in as constant + inAU4 rectInfo // left, top, width, height +) { + SpdSetup(dispatchThreadGroupCountXY, workGroupOffset, numWorkGroupsAndMips, rectInfo, -1); +} +#endif // #ifdef A_CPU //============================================================================================================================== // NON-PACKED VERSION //============================================================================================================================== - +#ifdef A_GPU #ifdef SPD_PACKED_ONLY // Avoid compiler error - AF4 SpdLoadSourceImage(ASU2 p){return AF4(0.0,0.0,0.0,0.0);} - AF4 SpdLoad(ASU2 p){return AF4(0.0,0.0,0.0,0.0);} - void SpdStore(ASU2 p, AF4 value, AU1 mip){} + AF4 SpdLoadSourceImage(ASU2 p, AU1 slice){return AF4(0.0,0.0,0.0,0.0);} + AF4 SpdLoad(ASU2 p, AU1 slice){return AF4(0.0,0.0,0.0,0.0);} + void SpdStore(ASU2 p, AF4 value, AU1 mip, AU1 slice){} AF4 SpdLoadIntermediate(AU1 x, AU1 y){return AF4(0.0,0.0,0.0,0.0);} void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value){} AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3){return AF4(0.0,0.0,0.0,0.0);} -#endif +#endif // #ifdef SPD_PACKED_ONLY //_____________________________________________________________/\_______________________________________________________________ #if defined(A_GLSL) && !defined(SPD_NO_WAVE_OPERATIONS) @@ -292,12 +388,12 @@ void SpdWorkgroupShuffleBarrier() { } // Only last active workgroup should proceed -bool SpdExitWorkgroup(AU1 numWorkGroups, AU1 localInvocationIndex) +bool SpdExitWorkgroup(AU1 numWorkGroups, AU1 localInvocationIndex, AU1 slice) { // global atomic counter if (localInvocationIndex == 0) { - SpdIncreaseAtomicCounter(); + SpdIncreaseAtomicCounter(slice); } SpdWorkgroupShuffleBarrier(); return (SpdGetAtomicCounter() != (numWorkGroups - 1)); @@ -306,7 +402,7 @@ bool SpdExitWorkgroup(AU1 numWorkGroups, AU1 localInvocationIndex) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// User defined: AF4 DSReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3); +// User defined: AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3); AF4 SpdReduceQuad(AF4 v) { @@ -326,6 +422,8 @@ AF4 SpdReduceQuad(AF4 v) return SpdReduce4(v0, v1, v2, v3); /* // if SM6.0 is not available, you can use the AMD shader intrinsics + // the AMD shader intrinsics are available in AMD GPU Services (AGS) library: + // https://gpuopen.com/amd-gpu-services-ags-library/ // works for DX11 AF4 v0 = v; AF4 v1; @@ -346,7 +444,7 @@ AF4 SpdReduceQuad(AF4 v) return SpdReduce4(v0, v1, v2, v3); */ #endif - return AF4_x(0.0); + return v; } AF4 SpdReduceIntermediate(AU2 i0, AU2 i1, AU2 i2, AU2 i3) @@ -358,69 +456,71 @@ AF4 SpdReduceIntermediate(AU2 i0, AU2 i1, AU2 i2, AU2 i3) return SpdReduce4(v0, v1, v2, v3); } -AF4 SpdReduceLoad4(AU2 i0, AU2 i1, AU2 i2, AU2 i3) +AF4 SpdReduceLoad4(AU2 i0, AU2 i1, AU2 i2, AU2 i3, AU1 slice) { - AF4 v0 = SpdLoad(ASU2(i0)); - AF4 v1 = SpdLoad(ASU2(i1)); - AF4 v2 = SpdLoad(ASU2(i2)); - AF4 v3 = SpdLoad(ASU2(i3)); + AF4 v0 = SpdLoad(ASU2(i0), slice); + AF4 v1 = SpdLoad(ASU2(i1), slice); + AF4 v2 = SpdLoad(ASU2(i2), slice); + AF4 v3 = SpdLoad(ASU2(i3), slice); return SpdReduce4(v0, v1, v2, v3); } -AF4 SpdReduceLoad4(AU2 base) +AF4 SpdReduceLoad4(AU2 base, AU1 slice) { return SpdReduceLoad4( AU2(base + AU2(0, 0)), AU2(base + AU2(0, 1)), AU2(base + AU2(1, 0)), - AU2(base + AU2(1, 1))); + AU2(base + AU2(1, 1)), + slice); } -AF4 SpdReduceLoadSourceImage4(AU2 i0, AU2 i1, AU2 i2, AU2 i3) +AF4 SpdReduceLoadSourceImage4(AU2 i0, AU2 i1, AU2 i2, AU2 i3, AU1 slice) { - AF4 v0 = SpdLoadSourceImage(ASU2(i0)); - AF4 v1 = SpdLoadSourceImage(ASU2(i1)); - AF4 v2 = SpdLoadSourceImage(ASU2(i2)); - AF4 v3 = SpdLoadSourceImage(ASU2(i3)); + AF4 v0 = SpdLoadSourceImage(ASU2(i0), slice); + AF4 v1 = SpdLoadSourceImage(ASU2(i1), slice); + AF4 v2 = SpdLoadSourceImage(ASU2(i2), slice); + AF4 v3 = SpdLoadSourceImage(ASU2(i3), slice); return SpdReduce4(v0, v1, v2, v3); } -AF4 SpdReduceLoadSourceImage4(AU2 base) +AF4 SpdReduceLoadSourceImage(AU2 base, AU1 slice) { #ifdef SPD_LINEAR_SAMPLER - return SpdLoadSourceImage(ASU2(base)); + return SpdLoadSourceImage(ASU2(base), slice); #else return SpdReduceLoadSourceImage4( AU2(base + AU2(0, 0)), AU2(base + AU2(0, 1)), AU2(base + AU2(1, 0)), - AU2(base + AU2(1, 1))); + AU2(base + AU2(1, 1)), + slice); #endif } -void SpdDownsampleMips_0_1_Intrinsics(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip) +void SpdDownsampleMips_0_1_Intrinsics(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip, AU1 slice) { AF4 v[4]; ASU2 tex = ASU2(workGroupID.xy * 64) + ASU2(x * 2, y * 2); ASU2 pix = ASU2(workGroupID.xy * 32) + ASU2(x, y); - v[0] = SpdReduceLoadSourceImage4(tex); - SpdStore(pix, v[0], 0); + v[0] = SpdReduceLoadSourceImage(tex, slice); + SpdStore(pix, v[0], 0, slice); tex = ASU2(workGroupID.xy * 64) + ASU2(x * 2 + 32, y * 2); pix = ASU2(workGroupID.xy * 32) + ASU2(x + 16, y); - v[1] = SpdReduceLoadSourceImage4(tex); - SpdStore(pix, v[1], 0); + v[1] = SpdReduceLoadSourceImage(tex, slice); + SpdStore(pix, v[1], 0, slice); tex = ASU2(workGroupID.xy * 64) + ASU2(x * 2, y * 2 + 32); pix = ASU2(workGroupID.xy * 32) + ASU2(x, y + 16); - v[2] = SpdReduceLoadSourceImage4(tex); - SpdStore(pix, v[2], 0); + v[2] = SpdReduceLoadSourceImage(tex, slice); + SpdStore(pix, v[2], 0, slice); tex = ASU2(workGroupID.xy * 64) + ASU2(x * 2 + 32, y * 2 + 32); pix = ASU2(workGroupID.xy * 32) + ASU2(x + 16, y + 16); - v[3] = SpdReduceLoadSourceImage4(tex); - SpdStore(pix, v[3], 0); + v[3] = SpdReduceLoadSourceImage(tex, slice); + SpdStore(pix, v[3], 0, slice); if (mip <= 1) return; @@ -433,50 +533,50 @@ void SpdDownsampleMips_0_1_Intrinsics(AU1 x, AU1 y, AU2 workGroupID, AU1 localIn if ((localInvocationIndex % 4) == 0) { SpdStore(ASU2(workGroupID.xy * 16) + - ASU2(x/2, y/2), v[0], 1); + ASU2(x/2, y/2), v[0], 1, slice); SpdStoreIntermediate( x/2, y/2, v[0]); SpdStore(ASU2(workGroupID.xy * 16) + - ASU2(x/2 + 8, y/2), v[1], 1); + ASU2(x/2 + 8, y/2), v[1], 1, slice); SpdStoreIntermediate( x/2 + 8, y/2, v[1]); SpdStore(ASU2(workGroupID.xy * 16) + - ASU2(x/2, y/2 + 8), v[2], 1); + ASU2(x/2, y/2 + 8), v[2], 1, slice); SpdStoreIntermediate( x/2, y/2 + 8, v[2]); SpdStore(ASU2(workGroupID.xy * 16) + - ASU2(x/2 + 8, y/2 + 8), v[3], 1); + ASU2(x/2 + 8, y/2 + 8), v[3], 1, slice); SpdStoreIntermediate( x/2 + 8, y/2 + 8, v[3]); } } -void SpdDownsampleMips_0_1_LDS(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip) +void SpdDownsampleMips_0_1_LDS(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip, AU1 slice) { AF4 v[4]; ASU2 tex = ASU2(workGroupID.xy * 64) + ASU2(x * 2, y * 2); ASU2 pix = ASU2(workGroupID.xy * 32) + ASU2(x, y); - v[0] = SpdReduceLoadSourceImage4(tex); - SpdStore(pix, v[0], 0); + v[0] = SpdReduceLoadSourceImage(tex, slice); + SpdStore(pix, v[0], 0, slice); tex = ASU2(workGroupID.xy * 64) + ASU2(x * 2 + 32, y * 2); pix = ASU2(workGroupID.xy * 32) + ASU2(x + 16, y); - v[1] = SpdReduceLoadSourceImage4(tex); - SpdStore(pix, v[1], 0); + v[1] = SpdReduceLoadSourceImage(tex, slice); + SpdStore(pix, v[1], 0, slice); tex = ASU2(workGroupID.xy * 64) + ASU2(x * 2, y * 2 + 32); pix = ASU2(workGroupID.xy * 32) + ASU2(x, y + 16); - v[2] = SpdReduceLoadSourceImage4(tex); - SpdStore(pix, v[2], 0); + v[2] = SpdReduceLoadSourceImage(tex, slice); + SpdStore(pix, v[2], 0, slice); tex = ASU2(workGroupID.xy * 64) + ASU2(x * 2 + 32, y * 2 + 32); pix = ASU2(workGroupID.xy * 32) + ASU2(x + 16, y + 16); - v[3] = SpdReduceLoadSourceImage4(tex); - SpdStore(pix, v[3], 0); + v[3] = SpdReduceLoadSourceImage(tex, slice); + SpdStore(pix, v[3], 0, slice); if (mip <= 1) return; @@ -493,7 +593,7 @@ void SpdDownsampleMips_0_1_LDS(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocatio AU2(x * 2 + 0, y * 2 + 1), AU2(x * 2 + 1, y * 2 + 1) ); - SpdStore(ASU2(workGroupID.xy * 16) + ASU2(x + (i % 2) * 8, y + (i / 2) * 8), v[i], 1); + SpdStore(ASU2(workGroupID.xy * 16) + ASU2(x + (i % 2) * 8, y + (i / 2) * 8), v[i], 1, slice); } SpdWorkgroupShuffleBarrier(); } @@ -507,28 +607,28 @@ void SpdDownsampleMips_0_1_LDS(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocatio } } -void SpdDownsampleMips_0_1(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip) +void SpdDownsampleMips_0_1(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip, AU1 slice) { #ifdef SPD_NO_WAVE_OPERATIONS - SpdDownsampleMips_0_1_LDS(x, y, workGroupID, localInvocationIndex, mip); + SpdDownsampleMips_0_1_LDS(x, y, workGroupID, localInvocationIndex, mip, slice); #else - SpdDownsampleMips_0_1_Intrinsics(x, y, workGroupID, localInvocationIndex, mip); + SpdDownsampleMips_0_1_Intrinsics(x, y, workGroupID, localInvocationIndex, mip, slice); #endif } -void SpdDownsampleMip_2(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip) +void SpdDownsampleMip_2(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip, AU1 slice) { #ifdef SPD_NO_WAVE_OPERATIONS if (localInvocationIndex < 64) { AF4 v = SpdReduceIntermediate( - AU2(x * 2 + 0 + 0, y * 2 + 0), - AU2(x * 2 + 0 + 1, y * 2 + 0), - AU2(x * 2 + 0 + 0, y * 2 + 1), - AU2(x * 2 + 0 + 1, y * 2 + 1) + AU2(x * 2 + 0, y * 2 + 0), + AU2(x * 2 + 1, y * 2 + 0), + AU2(x * 2 + 0, y * 2 + 1), + AU2(x * 2 + 1, y * 2 + 1) ); - SpdStore(ASU2(workGroupID.xy * 8) + ASU2(x, y), v, mip); + SpdStore(ASU2(workGroupID.xy * 8) + ASU2(x, y), v, mip, slice); // store to LDS, try to reduce bank conflicts // x 0 x 0 x 0 x 0 x 0 x 0 x 0 x 0 // 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 @@ -545,13 +645,13 @@ void SpdDownsampleMip_2(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, // quad index 0 stores result if (localInvocationIndex % 4 == 0) { - SpdStore(ASU2(workGroupID.xy * 8) + ASU2(x/2, y/2), v, mip); + SpdStore(ASU2(workGroupID.xy * 8) + ASU2(x/2, y/2), v, mip, slice); SpdStoreIntermediate(x + (y/2) % 2, y, v); } #endif } -void SpdDownsampleMip_3(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip) +void SpdDownsampleMip_3(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip, AU1 slice) { #ifdef SPD_NO_WAVE_OPERATIONS if (localInvocationIndex < 16) @@ -566,7 +666,7 @@ void SpdDownsampleMip_3(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU2(x * 4 + 0 + 1, y * 4 + 2), AU2(x * 4 + 2 + 1, y * 4 + 2) ); - SpdStore(ASU2(workGroupID.xy * 4) + ASU2(x, y), v, mip); + SpdStore(ASU2(workGroupID.xy * 4) + ASU2(x, y), v, mip, slice); // store to LDS // x 0 0 0 x 0 0 0 x 0 0 0 x 0 0 0 // 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 @@ -588,14 +688,14 @@ void SpdDownsampleMip_3(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, // quad index 0 stores result if (localInvocationIndex % 4 == 0) { - SpdStore(ASU2(workGroupID.xy * 4) + ASU2(x/2, y/2), v, mip); + SpdStore(ASU2(workGroupID.xy * 4) + ASU2(x/2, y/2), v, mip, slice); SpdStoreIntermediate(x * 2 + y/2, y * 2, v); } } #endif } -void SpdDownsampleMip_4(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip) +void SpdDownsampleMip_4(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip, AU1 slice) { #ifdef SPD_NO_WAVE_OPERATIONS if (localInvocationIndex < 4) @@ -609,7 +709,7 @@ void SpdDownsampleMip_4(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU2(x * 8 + 0 + 1 + y * 2, y * 8 + 4), AU2(x * 8 + 4 + 1 + y * 2, y * 8 + 4) ); - SpdStore(ASU2(workGroupID.xy * 2) + ASU2(x, y), v, mip); + SpdStore(ASU2(workGroupID.xy * 2) + ASU2(x, y), v, mip, slice); // store to LDS // x x x x 0 ... // 0 ... @@ -623,14 +723,14 @@ void SpdDownsampleMip_4(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, // quad index 0 stores result if (localInvocationIndex % 4 == 0) { - SpdStore(ASU2(workGroupID.xy * 2) + ASU2(x/2, y/2), v, mip); + SpdStore(ASU2(workGroupID.xy * 2) + ASU2(x/2, y/2), v, mip, slice); SpdStoreIntermediate(x / 2 + y, 0, v); } } #endif } -void SpdDownsampleMip_5(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip) +void SpdDownsampleMip_5(AU2 workGroupID, AU1 localInvocationIndex, AU1 mip, AU1 slice) { #ifdef SPD_NO_WAVE_OPERATIONS if (localInvocationIndex < 1) @@ -643,7 +743,7 @@ void SpdDownsampleMip_5(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU2(2, 0), AU2(3, 0) ); - SpdStore(ASU2(workGroupID.xy), v, mip); + SpdStore(ASU2(workGroupID.xy), v, mip, slice); } #else if (localInvocationIndex < 4) @@ -653,82 +753,96 @@ void SpdDownsampleMip_5(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, // quad index 0 stores result if (localInvocationIndex % 4 == 0) { - SpdStore(ASU2(workGroupID.xy), v, mip); + SpdStore(ASU2(workGroupID.xy), v, mip, slice); } } #endif } -void SpdDownsampleMips_6_7(AU1 x, AU1 y, AU1 mips) +void SpdDownsampleMips_6_7(AU1 x, AU1 y, AU1 mips, AU1 slice) { ASU2 tex = ASU2(x * 4 + 0, y * 4 + 0); ASU2 pix = ASU2(x * 2 + 0, y * 2 + 0); - AF4 v0 = SpdReduceLoad4(tex); - SpdStore(pix, v0, 6); + AF4 v0 = SpdReduceLoad4(tex, slice); + SpdStore(pix, v0, 6, slice); tex = ASU2(x * 4 + 2, y * 4 + 0); pix = ASU2(x * 2 + 1, y * 2 + 0); - AF4 v1 = SpdReduceLoad4(tex); - SpdStore(pix, v1, 6); + AF4 v1 = SpdReduceLoad4(tex, slice); + SpdStore(pix, v1, 6, slice); tex = ASU2(x * 4 + 0, y * 4 + 2); pix = ASU2(x * 2 + 0, y * 2 + 1); - AF4 v2 = SpdReduceLoad4(tex); - SpdStore(pix, v2, 6); + AF4 v2 = SpdReduceLoad4(tex, slice); + SpdStore(pix, v2, 6, slice); tex = ASU2(x * 4 + 2, y * 4 + 2); pix = ASU2(x * 2 + 1, y * 2 + 1); - AF4 v3 = SpdReduceLoad4(tex); - SpdStore(pix, v3, 6); + AF4 v3 = SpdReduceLoad4(tex, slice); + SpdStore(pix, v3, 6, slice); if (mips <= 7) return; // no barrier needed, working on values only from the same thread AF4 v = SpdReduce4(v0, v1, v2, v3); - SpdStore(ASU2(x, y), v, 7); + SpdStore(ASU2(x, y), v, 7, slice); SpdStoreIntermediate(x, y, v); } -void SpdDownsampleNextFour(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 baseMip, AU1 mips) +void SpdDownsampleNextFour(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 baseMip, AU1 mips, AU1 slice) { if (mips <= baseMip) return; SpdWorkgroupShuffleBarrier(); - SpdDownsampleMip_2(x, y, workGroupID, localInvocationIndex, baseMip); + SpdDownsampleMip_2(x, y, workGroupID, localInvocationIndex, baseMip, slice); if (mips <= baseMip + 1) return; SpdWorkgroupShuffleBarrier(); - SpdDownsampleMip_3(x, y, workGroupID, localInvocationIndex, baseMip + 1); + SpdDownsampleMip_3(x, y, workGroupID, localInvocationIndex, baseMip + 1, slice); if (mips <= baseMip + 2) return; SpdWorkgroupShuffleBarrier(); - SpdDownsampleMip_4(x, y, workGroupID, localInvocationIndex, baseMip + 2); + SpdDownsampleMip_4(x, y, workGroupID, localInvocationIndex, baseMip + 2, slice); if (mips <= baseMip + 3) return; SpdWorkgroupShuffleBarrier(); - SpdDownsampleMip_5(x, y, workGroupID, localInvocationIndex, baseMip + 3); + SpdDownsampleMip_5(workGroupID, localInvocationIndex, baseMip + 3, slice); } void SpdDownsample( AU2 workGroupID, AU1 localInvocationIndex, AU1 mips, - AU1 numWorkGroups + AU1 numWorkGroups, + AU1 slice ) { AU2 sub_xy = ARmpRed8x8(localInvocationIndex % 64); AU1 x = sub_xy.x + 8 * ((localInvocationIndex >> 6) % 2); AU1 y = sub_xy.y + 8 * ((localInvocationIndex >> 7)); - SpdDownsampleMips_0_1(x, y, workGroupID, localInvocationIndex, mips); + SpdDownsampleMips_0_1(x, y, workGroupID, localInvocationIndex, mips, slice); - SpdDownsampleNextFour(x, y, workGroupID, localInvocationIndex, 2, mips); + SpdDownsampleNextFour(x, y, workGroupID, localInvocationIndex, 2, mips, slice); if (mips <= 6) return; - if (SpdExitWorkgroup(numWorkGroups, localInvocationIndex)) return; + if (SpdExitWorkgroup(numWorkGroups, localInvocationIndex, slice)) return; + + SpdResetAtomicCounter(slice); // After mip 6 there is only a single workgroup left that downsamples the remaining up to 64x64 texels. - SpdDownsampleMips_6_7(x, y, mips); + SpdDownsampleMips_6_7(x, y, mips, slice); + + SpdDownsampleNextFour(x, y, AU2(0,0), localInvocationIndex, 8, mips, slice); +} - SpdDownsampleNextFour(x, y, AU2(0,0), localInvocationIndex, 8, mips); +void SpdDownsample( + AU2 workGroupID, + AU1 localInvocationIndex, + AU1 mips, + AU1 numWorkGroups, + AU1 slice, + AU2 workGroupOffset +) { + SpdDownsample(workGroupID + workGroupOffset, localInvocationIndex, mips, numWorkGroups, slice); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -738,7 +852,7 @@ void SpdDownsample( // PACKED VERSION //============================================================================================================================== -#ifdef A_HALF // A_HALF +#ifdef A_HALF #ifdef A_GLSL #extension GL_EXT_shader_subgroup_extended_types_float16:require @@ -762,6 +876,8 @@ AH4 SpdReduceQuadH(AH4 v) return SpdReduce4H(v0, v1, v2, v3); /* // if SM6.0 is not available, you can use the AMD shader intrinsics + // the AMD shader intrinsics are available in AMD GPU Services (AGS) library: + // https://gpuopen.com/amd-gpu-services-ags-library/ // works for DX11 AH4 v0 = v; AH4 v1; @@ -795,69 +911,71 @@ AH4 SpdReduceIntermediateH(AU2 i0, AU2 i1, AU2 i2, AU2 i3) return SpdReduce4H(v0, v1, v2, v3); } -AH4 SpdReduceLoad4H(AU2 i0, AU2 i1, AU2 i2, AU2 i3) +AH4 SpdReduceLoad4H(AU2 i0, AU2 i1, AU2 i2, AU2 i3, AU1 slice) { - AH4 v0 = SpdLoadH(ASU2(i0)); - AH4 v1 = SpdLoadH(ASU2(i1)); - AH4 v2 = SpdLoadH(ASU2(i2)); - AH4 v3 = SpdLoadH(ASU2(i3)); + AH4 v0 = SpdLoadH(ASU2(i0), slice); + AH4 v1 = SpdLoadH(ASU2(i1), slice); + AH4 v2 = SpdLoadH(ASU2(i2), slice); + AH4 v3 = SpdLoadH(ASU2(i3), slice); return SpdReduce4H(v0, v1, v2, v3); } -AH4 SpdReduceLoad4H(AU2 base) +AH4 SpdReduceLoad4H(AU2 base, AU1 slice) { return SpdReduceLoad4H( AU2(base + AU2(0, 0)), AU2(base + AU2(0, 1)), AU2(base + AU2(1, 0)), - AU2(base + AU2(1, 1))); + AU2(base + AU2(1, 1)), + slice); } -AH4 SpdReduceLoadSourceImage4H(AU2 i0, AU2 i1, AU2 i2, AU2 i3) +AH4 SpdReduceLoadSourceImage4H(AU2 i0, AU2 i1, AU2 i2, AU2 i3, AU1 slice) { - AH4 v0 = SpdLoadSourceImageH(ASU2(i0)); - AH4 v1 = SpdLoadSourceImageH(ASU2(i1)); - AH4 v2 = SpdLoadSourceImageH(ASU2(i2)); - AH4 v3 = SpdLoadSourceImageH(ASU2(i3)); + AH4 v0 = SpdLoadSourceImageH(ASU2(i0), slice); + AH4 v1 = SpdLoadSourceImageH(ASU2(i1), slice); + AH4 v2 = SpdLoadSourceImageH(ASU2(i2), slice); + AH4 v3 = SpdLoadSourceImageH(ASU2(i3), slice); return SpdReduce4H(v0, v1, v2, v3); } -AH4 SpdReduceLoadSourceImage4H(AU2 base) +AH4 SpdReduceLoadSourceImageH(AU2 base, AU1 slice) { #ifdef SPD_LINEAR_SAMPLER - return SpdLoadSourceImageH(ASU2(base)); + return SpdLoadSourceImageH(ASU2(base), slice); #else return SpdReduceLoadSourceImage4H( AU2(base + AU2(0, 0)), AU2(base + AU2(0, 1)), AU2(base + AU2(1, 0)), - AU2(base + AU2(1, 1))); + AU2(base + AU2(1, 1)), + slice); #endif } -void SpdDownsampleMips_0_1_IntrinsicsH(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mips) +void SpdDownsampleMips_0_1_IntrinsicsH(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mips, AU1 slice) { AH4 v[4]; ASU2 tex = ASU2(workGroupID.xy * 64) + ASU2(x * 2, y * 2); ASU2 pix = ASU2(workGroupID.xy * 32) + ASU2(x, y); - v[0] = SpdReduceLoadSourceImage4H(tex); - SpdStoreH(pix, v[0], 0); + v[0] = SpdReduceLoadSourceImageH(tex, slice); + SpdStoreH(pix, v[0], 0, slice); tex = ASU2(workGroupID.xy * 64) + ASU2(x * 2 + 32, y * 2); pix = ASU2(workGroupID.xy * 32) + ASU2(x + 16, y); - v[1] = SpdReduceLoadSourceImage4H(tex); - SpdStoreH(pix, v[1], 0); + v[1] = SpdReduceLoadSourceImageH(tex, slice); + SpdStoreH(pix, v[1], 0, slice); tex = ASU2(workGroupID.xy * 64) + ASU2(x * 2, y * 2 + 32); pix = ASU2(workGroupID.xy * 32) + ASU2(x, y + 16); - v[2] = SpdReduceLoadSourceImage4H(tex); - SpdStoreH(pix, v[2], 0); + v[2] = SpdReduceLoadSourceImageH(tex, slice); + SpdStoreH(pix, v[2], 0, slice); tex = ASU2(workGroupID.xy * 64) + ASU2(x * 2 + 32, y * 2 + 32); pix = ASU2(workGroupID.xy * 32) + ASU2(x + 16, y + 16); - v[3] = SpdReduceLoadSourceImage4H(tex); - SpdStoreH(pix, v[3], 0); + v[3] = SpdReduceLoadSourceImageH(tex, slice); + SpdStoreH(pix, v[3], 0, slice); if (mips <= 1) return; @@ -869,43 +987,43 @@ void SpdDownsampleMips_0_1_IntrinsicsH(AU1 x, AU1 y, AU2 workGroupID, AU1 localI if ((localInvocationIndex % 4) == 0) { - SpdStoreH(ASU2(workGroupID.xy * 16) + ASU2(x/2, y/2), v[0], 1); + SpdStoreH(ASU2(workGroupID.xy * 16) + ASU2(x/2, y/2), v[0], 1, slice); SpdStoreIntermediateH(x/2, y/2, v[0]); - SpdStoreH(ASU2(workGroupID.xy * 16) + ASU2(x/2 + 8, y/2), v[1], 1); + SpdStoreH(ASU2(workGroupID.xy * 16) + ASU2(x/2 + 8, y/2), v[1], 1, slice); SpdStoreIntermediateH(x/2 + 8, y/2, v[1]); - SpdStoreH(ASU2(workGroupID.xy * 16) + ASU2(x/2, y/2 + 8), v[2], 1); + SpdStoreH(ASU2(workGroupID.xy * 16) + ASU2(x/2, y/2 + 8), v[2], 1, slice); SpdStoreIntermediateH(x/2, y/2 + 8, v[2]); - SpdStoreH(ASU2(workGroupID.xy * 16) + ASU2(x/2 + 8, y/2 + 8), v[3], 1); + SpdStoreH(ASU2(workGroupID.xy * 16) + ASU2(x/2 + 8, y/2 + 8), v[3], 1, slice); SpdStoreIntermediateH(x/2 + 8, y/2 + 8, v[3]); } } -void SpdDownsampleMips_0_1_LDSH(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mips) +void SpdDownsampleMips_0_1_LDSH(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mips, AU1 slice) { AH4 v[4]; ASU2 tex = ASU2(workGroupID.xy * 64) + ASU2(x * 2, y * 2); ASU2 pix = ASU2(workGroupID.xy * 32) + ASU2(x, y); - v[0] = SpdReduceLoadSourceImage4H(tex); - SpdStoreH(pix, v[0], 0); + v[0] = SpdReduceLoadSourceImageH(tex, slice); + SpdStoreH(pix, v[0], 0, slice); tex = ASU2(workGroupID.xy * 64) + ASU2(x * 2 + 32, y * 2); pix = ASU2(workGroupID.xy * 32) + ASU2(x + 16, y); - v[1] = SpdReduceLoadSourceImage4H(tex); - SpdStoreH(pix, v[1], 0); + v[1] = SpdReduceLoadSourceImageH(tex, slice); + SpdStoreH(pix, v[1], 0, slice); tex = ASU2(workGroupID.xy * 64) + ASU2(x * 2, y * 2 + 32); pix = ASU2(workGroupID.xy * 32) + ASU2(x, y + 16); - v[2] = SpdReduceLoadSourceImage4H(tex); - SpdStoreH(pix, v[2], 0); + v[2] = SpdReduceLoadSourceImageH(tex, slice); + SpdStoreH(pix, v[2], 0, slice); tex = ASU2(workGroupID.xy * 64) + ASU2(x * 2 + 32, y * 2 + 32); pix = ASU2(workGroupID.xy * 32) + ASU2(x + 16, y + 16); - v[3] = SpdReduceLoadSourceImage4H(tex); - SpdStoreH(pix, v[3], 0); + v[3] = SpdReduceLoadSourceImageH(tex, slice); + SpdStoreH(pix, v[3], 0, slice); if (mips <= 1) return; @@ -922,7 +1040,7 @@ void SpdDownsampleMips_0_1_LDSH(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocati AU2(x * 2 + 0, y * 2 + 1), AU2(x * 2 + 1, y * 2 + 1) ); - SpdStoreH(ASU2(workGroupID.xy * 16) + ASU2(x + (i % 2) * 8, y + (i / 2) * 8), v[i], 1); + SpdStoreH(ASU2(workGroupID.xy * 16) + ASU2(x + (i % 2) * 8, y + (i / 2) * 8), v[i], 1, slice); } SpdWorkgroupShuffleBarrier(); } @@ -936,28 +1054,28 @@ void SpdDownsampleMips_0_1_LDSH(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocati } } -void SpdDownsampleMips_0_1H(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mips) +void SpdDownsampleMips_0_1H(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mips, AU1 slice) { #ifdef SPD_NO_WAVE_OPERATIONS - SpdDownsampleMips_0_1_LDSH(x, y, workGroupID, localInvocationIndex, mips); + SpdDownsampleMips_0_1_LDSH(x, y, workGroupID, localInvocationIndex, mips, slice); #else - SpdDownsampleMips_0_1_IntrinsicsH(x, y, workGroupID, localInvocationIndex, mips); + SpdDownsampleMips_0_1_IntrinsicsH(x, y, workGroupID, localInvocationIndex, mips, slice); #endif } -void SpdDownsampleMip_2H(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip) +void SpdDownsampleMip_2H(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip, AU1 slice) { #ifdef SPD_NO_WAVE_OPERATIONS if (localInvocationIndex < 64) { AH4 v = SpdReduceIntermediateH( - AU2(x * 2 + 0 + 0, y * 2 + 0), - AU2(x * 2 + 0 + 1, y * 2 + 0), - AU2(x * 2 + 0 + 0, y * 2 + 1), - AU2(x * 2 + 0 + 1, y * 2 + 1) + AU2(x * 2 + 0, y * 2 + 0), + AU2(x * 2 + 1, y * 2 + 0), + AU2(x * 2 + 0, y * 2 + 1), + AU2(x * 2 + 1, y * 2 + 1) ); - SpdStoreH(ASU2(workGroupID.xy * 8) + ASU2(x, y), v, mip); + SpdStoreH(ASU2(workGroupID.xy * 8) + ASU2(x, y), v, mip, slice); // store to LDS, try to reduce bank conflicts // x 0 x 0 x 0 x 0 x 0 x 0 x 0 x 0 // 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 @@ -974,13 +1092,13 @@ void SpdDownsampleMip_2H(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex // quad index 0 stores result if (localInvocationIndex % 4 == 0) { - SpdStoreH(ASU2(workGroupID.xy * 8) + ASU2(x/2, y/2), v, mip); + SpdStoreH(ASU2(workGroupID.xy * 8) + ASU2(x/2, y/2), v, mip, slice); SpdStoreIntermediateH(x + (y/2) % 2, y, v); } #endif } -void SpdDownsampleMip_3H(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip) +void SpdDownsampleMip_3H(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip, AU1 slice) { #ifdef SPD_NO_WAVE_OPERATIONS if (localInvocationIndex < 16) @@ -995,7 +1113,7 @@ void SpdDownsampleMip_3H(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex AU2(x * 4 + 0 + 1, y * 4 + 2), AU2(x * 4 + 2 + 1, y * 4 + 2) ); - SpdStoreH(ASU2(workGroupID.xy * 4) + ASU2(x, y), v, mip); + SpdStoreH(ASU2(workGroupID.xy * 4) + ASU2(x, y), v, mip, slice); // store to LDS // x 0 0 0 x 0 0 0 x 0 0 0 x 0 0 0 // 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 @@ -1017,14 +1135,14 @@ void SpdDownsampleMip_3H(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex // quad index 0 stores result if (localInvocationIndex % 4 == 0) { - SpdStoreH(ASU2(workGroupID.xy * 4) + ASU2(x/2, y/2), v, mip); + SpdStoreH(ASU2(workGroupID.xy * 4) + ASU2(x/2, y/2), v, mip, slice); SpdStoreIntermediateH(x * 2 + y/2, y * 2, v); } } #endif } -void SpdDownsampleMip_4H(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip) +void SpdDownsampleMip_4H(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip, AU1 slice) { #ifdef SPD_NO_WAVE_OPERATIONS if (localInvocationIndex < 4) @@ -1038,7 +1156,7 @@ void SpdDownsampleMip_4H(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex AU2(x * 8 + 0 + 1 + y * 2, y * 8 + 4), AU2(x * 8 + 4 + 1 + y * 2, y * 8 + 4) ); - SpdStoreH(ASU2(workGroupID.xy * 2) + ASU2(x, y), v, mip); + SpdStoreH(ASU2(workGroupID.xy * 2) + ASU2(x, y), v, mip, slice); // store to LDS // x x x x 0 ... // 0 ... @@ -1052,14 +1170,14 @@ void SpdDownsampleMip_4H(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex // quad index 0 stores result if (localInvocationIndex % 4 == 0) { - SpdStoreH(ASU2(workGroupID.xy * 2) + ASU2(x/2, y/2), v, mip); + SpdStoreH(ASU2(workGroupID.xy * 2) + ASU2(x/2, y/2), v, mip, slice); SpdStoreIntermediateH(x / 2 + y, 0, v); } } #endif } -void SpdDownsampleMip_5H(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 mip) +void SpdDownsampleMip_5H(AU2 workGroupID, AU1 localInvocationIndex, AU1 mip, AU1 slice) { #ifdef SPD_NO_WAVE_OPERATIONS if (localInvocationIndex < 1) @@ -1072,7 +1190,7 @@ void SpdDownsampleMip_5H(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex AU2(2, 0), AU2(3, 0) ); - SpdStoreH(ASU2(workGroupID.xy), v, mip); + SpdStoreH(ASU2(workGroupID.xy), v, mip, slice); } #else if (localInvocationIndex < 4) @@ -1082,83 +1200,98 @@ void SpdDownsampleMip_5H(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex // quad index 0 stores result if (localInvocationIndex % 4 == 0) { - SpdStoreH(ASU2(workGroupID.xy), v, mip); + SpdStoreH(ASU2(workGroupID.xy), v, mip, slice); } } #endif } -void SpdDownsampleMips_6_7H(AU1 x, AU1 y, AU1 mips) +void SpdDownsampleMips_6_7H(AU1 x, AU1 y, AU1 mips, AU1 slice) { ASU2 tex = ASU2(x * 4 + 0, y * 4 + 0); ASU2 pix = ASU2(x * 2 + 0, y * 2 + 0); - AH4 v0 = SpdReduceLoad4H(tex); - SpdStoreH(pix, v0, 6); + AH4 v0 = SpdReduceLoad4H(tex, slice); + SpdStoreH(pix, v0, 6, slice); tex = ASU2(x * 4 + 2, y * 4 + 0); pix = ASU2(x * 2 + 1, y * 2 + 0); - AH4 v1 = SpdReduceLoad4H(tex); - SpdStoreH(pix, v1, 6); + AH4 v1 = SpdReduceLoad4H(tex, slice); + SpdStoreH(pix, v1, 6, slice); tex = ASU2(x * 4 + 0, y * 4 + 2); pix = ASU2(x * 2 + 0, y * 2 + 1); - AH4 v2 = SpdReduceLoad4H(tex); - SpdStoreH(pix, v2, 6); + AH4 v2 = SpdReduceLoad4H(tex, slice); + SpdStoreH(pix, v2, 6, slice); tex = ASU2(x * 4 + 2, y * 4 + 2); pix = ASU2(x * 2 + 1, y * 2 + 1); - AH4 v3 = SpdReduceLoad4H(tex); - SpdStoreH(pix, v3, 6); + AH4 v3 = SpdReduceLoad4H(tex, slice); + SpdStoreH(pix, v3, 6, slice); if (mips < 8) return; // no barrier needed, working on values only from the same thread AH4 v = SpdReduce4H(v0, v1, v2, v3); - SpdStoreH(ASU2(x, y), v, 7); + SpdStoreH(ASU2(x, y), v, 7, slice); SpdStoreIntermediateH(x, y, v); } -void SpdDownsampleNextFourH(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 baseMip, AU1 mips) +void SpdDownsampleNextFourH(AU1 x, AU1 y, AU2 workGroupID, AU1 localInvocationIndex, AU1 baseMip, AU1 mips, AU1 slice) { if (mips <= baseMip) return; SpdWorkgroupShuffleBarrier(); - SpdDownsampleMip_2H(x, y, workGroupID, localInvocationIndex, baseMip); + SpdDownsampleMip_2H(x, y, workGroupID, localInvocationIndex, baseMip, slice); if (mips <= baseMip + 1) return; SpdWorkgroupShuffleBarrier(); - SpdDownsampleMip_3H(x, y, workGroupID, localInvocationIndex, baseMip + 1); + SpdDownsampleMip_3H(x, y, workGroupID, localInvocationIndex, baseMip + 1, slice); if (mips <= baseMip + 2) return; SpdWorkgroupShuffleBarrier(); - SpdDownsampleMip_4H(x, y, workGroupID, localInvocationIndex, baseMip + 2); + SpdDownsampleMip_4H(x, y, workGroupID, localInvocationIndex, baseMip + 2, slice); if (mips <= baseMip + 3) return; SpdWorkgroupShuffleBarrier(); - SpdDownsampleMip_5H(x, y, workGroupID, localInvocationIndex, baseMip + 3); + SpdDownsampleMip_5H(workGroupID, localInvocationIndex, baseMip + 3, slice); } void SpdDownsampleH( AU2 workGroupID, AU1 localInvocationIndex, AU1 mips, - AU1 numWorkGroups + AU1 numWorkGroups, + AU1 slice ) { AU2 sub_xy = ARmpRed8x8(localInvocationIndex % 64); AU1 x = sub_xy.x + 8 * ((localInvocationIndex >> 6) % 2); AU1 y = sub_xy.y + 8 * ((localInvocationIndex >> 7)); - SpdDownsampleMips_0_1H(x, y, workGroupID, localInvocationIndex, mips); + SpdDownsampleMips_0_1H(x, y, workGroupID, localInvocationIndex, mips, slice); - SpdDownsampleNextFourH(x, y, workGroupID, localInvocationIndex, 2, mips); + SpdDownsampleNextFourH(x, y, workGroupID, localInvocationIndex, 2, mips, slice); if (mips < 7) return; - if (SpdExitWorkgroup(numWorkGroups, localInvocationIndex)) return; + if (SpdExitWorkgroup(numWorkGroups, localInvocationIndex, slice)) return; + + SpdResetAtomicCounter(slice); // After mip 6 there is only a single workgroup left that downsamples the remaining up to 64x64 texels. - SpdDownsampleMips_6_7H(x, y, mips); + SpdDownsampleMips_6_7H(x, y, mips, slice); - SpdDownsampleNextFourH(x, y, AU2(0,0), localInvocationIndex, 8, mips); + SpdDownsampleNextFourH(x, y, AU2(0,0), localInvocationIndex, 8, mips, slice); +} + +void SpdDownsampleH( + AU2 workGroupID, + AU1 localInvocationIndex, + AU1 mips, + AU1 numWorkGroups, + AU1 slice, + AU2 workGroupOffset +) { + SpdDownsampleH(workGroupID + workGroupOffset, localInvocationIndex, mips, numWorkGroups, slice); } -#endif \ No newline at end of file +#endif // #ifdef A_HALF +#endif // #ifdef A_GPU \ No newline at end of file diff --git a/sample/CMakeLists.txt b/sample/CMakeLists.txt index 95eed3e..21e356b 100644 --- a/sample/CMakeLists.txt +++ b/sample/CMakeLists.txt @@ -3,6 +3,8 @@ set(CMAKE_GENERATOR_PLATFORM x64) project (SPDSample_${GFX_API}) +set(FFX_SPD_OUTPUT_DIRECTORY ${CMAKE_HOME_DIRECTORY}/bin) + # ouput exe to bin directory SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_HOME_DIRECTORY}/bin) foreach( OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES} ) @@ -16,7 +18,7 @@ add_subdirectory(libs/cauldron) set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT ${PROJECT_NAME}) if(GFX_API STREQUAL DX12) - add_subdirectory(src/DX12) + add_subdirectory(src/DX12) elseif(GFX_API STREQUAL VK) find_package(Vulkan REQUIRED) add_subdirectory(src/VK) diff --git a/sample/README.md b/sample/README.md index b009b55..082bed7 100644 --- a/sample/README.md +++ b/sample/README.md @@ -3,13 +3,13 @@ Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. Permission # SPD Sample -A sample using the FidelityFX Singlepass Downsampler (SPD) library. +A sample using the FidelityFX Single Pass Downsampler (SPD) library. # Build Instructions 1. Clone submodules by running 'git submodule update --init --recursive' (so you get the Cauldron framework too) 2. Run sample/build/GenerateSolutions.bat -3. open solution, build + run + have fun 😊 +3. Open solution, build + run + have fun 😊 # SPD Files You can find them in ../ffx-spd @@ -21,15 +21,32 @@ Downsampler - PS: computes each mip in a separate pixel shader pass - Multipass CS: computes each mip in a separate compute shader pass - SPD CS: uses the SPD library, computes all mips (up to a source texture of size 4096²) in a single pass -- SPD CS linear sampler: uses the SPD library and for sampling the source texture a linear sampler +<<<<<<< HEAD +SPD Load Versions +- Load: uses a load to fetch from the source texture +- Linear Sampler: uses a sampler to fetch from the source texture. Sampler must meet the user defined reduction function. +======= SPD Versions - NO-WaveOps: uses only LDS to share the data between threads - WaveOps: uses Intrinsics and LDS to share the data between threads +>>>>>>> 58f8e0f841ad86884e8d2defa8ceea473f44355c -SPD Non-Packed / Packed Version +SPD WaveOps Versions +- No-WaveOps: uses only LDS to share the data between threads +- WaveOps: uses Intrinsics and LDS to share the data between threads + +SPD Non-Packed / Packed Versions - Non-Packed: uses fp32 - Packed: uses fp16, reduced register pressure # Recommendations -We recommend to use the WapeOps path when supported. If higher precision is not needed, you can enable the packed mode - it has less register pressure and can run a bit faster as well. \ No newline at end of file +We recommend to use the WaveOps path when supported. If higher precision is not needed, you can enable the packed mode - it has less register pressure and can run a bit faster as well. +If you compute the average for each 2x2 quad, we also recommend to use a linear sampler to fetch from the source texture instead of four separate loads. + +# Known issues +Please use driver 20.8.3 or newer. There is a known issue on DX12 when using the SPD No-WaveOps Packed version. +It may appear as "Access violation reading location ..." during CreateComputePipelineState, with top of the stack +pointing to amdxc64.dll. +To workaround this issue, you may advise players to update their graphics driver or don't compile and use +a different SPD version, e.g. a Non-Packed version. \ No newline at end of file diff --git a/sample/libs/cauldron b/sample/libs/cauldron index fd91cd7..050b274 160000 --- a/sample/libs/cauldron +++ b/sample/libs/cauldron @@ -1 +1 @@ -Subproject commit fd91cd744d014505daef1780dceee49fd62ce953 +Subproject commit 050b274df95777d688686d017a6926a515a58b30 diff --git a/sample/src/Common/SpdSample.json b/sample/src/Common/SpdSample.json new file mode 100644 index 0000000..07ec87e --- /dev/null +++ b/sample/src/Common/SpdSample.json @@ -0,0 +1,41 @@ +{ + "globals": { + "CpuValidationLayerEnabled": false, + "GpuValidationLayerEnabled": false, + "fullScreen": false, + "width": 1920, + "height": 1080, + "activeScene": 0, + "benchmark": false, + "stablePowerState": false, + "downsampler": 2, + "spdLoad": 0, + "spdWaveOps": 1, + "spdPacked": 0 + }, + "scenes": [ + { + "name": "DamagedHelmet", + "directory": "..\\media\\cauldron-media\\DamagedHelmet\\GLTF\\", + "filename": "DamagedHelmet.gltf", + "TAA": true, + "toneMapper": 0, + "iblFactor": 2, + "emmisiveFactor": 1, + "intensity": 50, + "exposure": 1, + "camera": { + "defaultFrom": [ 0, 0, 3.5 ], + "defaultTo": [ 0, 0, 0 ] + }, + "BenchmarkSettings": { + "timeStep": 1, + "timeStart": 0, + "timeEnd": 10000, + "exitWhenTimeEnds": true, + "resultsFilename": "FidelityFXSpd.csv", + "warmUpFrames": 200 + } + } + ] +} diff --git a/sample/src/DX12/CMakeLists.txt b/sample/src/DX12/CMakeLists.txt index 24ade70..3620699 100644 --- a/sample/src/DX12/CMakeLists.txt +++ b/sample/src/DX12/CMakeLists.txt @@ -1,20 +1,21 @@ project (SPDSample_DX12) include(${CMAKE_HOME_DIRECTORY}/common.cmake) + +add_compile_options(/MP) + set(sources CSDownsampler.cpp CSDownsampler.h PSDownsampler.cpp PSDownsampler.h - SPD_CS.cpp - SPD_CS.h - SPD_CS_Linear_Sampler.cpp - SPD_CS_Linear_Sampler.h - SPD_Versions.cpp - SPD_Versions.h - SPD_Sample.cpp - SPD_Sample.h - SPD_Renderer.cpp - SPD_Renderer.h + SPDCS.cpp + SPDCS.h + SPDVersions.cpp + SPDVersions.h + SPDSample.cpp + SPDSample.h + SPDRenderer.cpp + SPDRenderer.h stdafx.cpp stdafx.h) set(Shaders_src @@ -22,14 +23,20 @@ set(Shaders_src ${CMAKE_CURRENT_SOURCE_DIR}/../../../ffx-spd/ffx_spd.h ${CMAKE_CURRENT_SOURCE_DIR}/CSDownsampler.hlsl ${CMAKE_CURRENT_SOURCE_DIR}/PSDownsampler.hlsl - ${CMAKE_CURRENT_SOURCE_DIR}/SPD_Integration.hlsl - ${CMAKE_CURRENT_SOURCE_DIR}/SPD_Integration_Linear_Sampler.hlsl + ${CMAKE_CURRENT_SOURCE_DIR}/SPDIntegration.hlsl + ${CMAKE_CURRENT_SOURCE_DIR}/SPDIntegrationLinearSampler.hlsl +) +set(Common_src + ${CMAKE_CURRENT_SOURCE_DIR}/../Common/SpdSample.json ) source_group("Sources" FILES ${sources}) source_group("Shaders" FILES ${Shaders_src}) +source_group("Common" FILES ${Common_src}) + # prevent VS from processing/compiling these files set_source_files_properties(${Shaders_src} PROPERTIES VS_TOOL_OVERRIDE "Text") +set_source_files_properties(${Common_src} PROPERTIES VS_TOOL_OVERRIDE "Text") function(copyCommand list dest) foreach(fullFileName ${list}) @@ -49,8 +56,9 @@ endfunction() # copy shaders and media to Bin # include("${CMAKE_HOME_DIRECTORY}/src/Common/Shaders/CMakeList.txt") copyCommand("${Shaders_src}" ${CMAKE_HOME_DIRECTORY}/bin/ShaderLibDX) +copyCommand("${Common_src}" ${CMAKE_HOME_DIRECTORY}/bin) -add_executable(${PROJECT_NAME} WIN32 ${sources} ${Shaders_src}) +add_executable(${PROJECT_NAME} WIN32 ${sources} ${Shaders_src} ${Common_src}) target_link_libraries (${PROJECT_NAME} LINK_PUBLIC Cauldron_DX12 ImGUI amd_ags DXC d3dcompiler D3D12) target_include_directories (${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../../../ffx-spd) set_target_properties(${PROJECT_NAME} PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_HOME_DIRECTORY}/bin") diff --git a/sample/src/DX12/CSDownsampler.cpp b/sample/src/DX12/CSDownsampler.cpp index ed1a7c7..abe7222 100644 --- a/sample/src/DX12/CSDownsampler.cpp +++ b/sample/src/DX12/CSDownsampler.cpp @@ -33,19 +33,18 @@ namespace CAULDRON_DX12 { void CSDownsampler::OnCreate( Device *pDevice, + UploadHeap *pUploadHeap, ResourceViewHeaps *pResourceViewHeaps, - DynamicBufferRing *pConstantBufferRing, - DXGI_FORMAT outFormat + DynamicBufferRing *pConstantBufferRing ) { m_pDevice = pDevice; m_pResourceViewHeaps = pResourceViewHeaps; m_pConstantBufferRing = pConstantBufferRing; - m_outFormat = outFormat; D3D12_SHADER_BYTECODE shaderByteCode = {}; DefineList defines; - CompileShaderFromFile("CSDownsampler.hlsl", &defines, "main", "cs_6_2", 0, &shaderByteCode); + CompileShaderFromFile("CSDownsampler.hlsl", &defines, "main", "-T cs_6_0 /Zi /Zss", &shaderByteCode); // Create root signature // @@ -91,7 +90,7 @@ namespace CAULDRON_DX12 // deny uneccessary access to certain pipeline stages descRootSignature.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE; - ID3DBlob *pOutBlob, *pErrorBlob = NULL; + ID3DBlob* pOutBlob, * pErrorBlob = NULL; ThrowIfFailed(D3D12SerializeRootSignature(&descRootSignature, D3D_ROOT_SIGNATURE_VERSION_1, &pOutBlob, &pErrorBlob)); ThrowIfFailed( pDevice->GetDevice()->CreateRootSignature(0, pOutBlob->GetBufferPointer(), pOutBlob->GetBufferSize(), IID_PPV_ARGS(&m_pRootSignature)) @@ -104,74 +103,53 @@ namespace CAULDRON_DX12 } //{ - D3D12_COMPUTE_PIPELINE_STATE_DESC descPso = {}; - descPso.CS = shaderByteCode; - descPso.Flags = D3D12_PIPELINE_STATE_FLAG_NONE; - descPso.pRootSignature = m_pRootSignature; - descPso.NodeMask = 0; + D3D12_COMPUTE_PIPELINE_STATE_DESC descPso = {}; + descPso.CS = shaderByteCode; + descPso.Flags = D3D12_PIPELINE_STATE_FLAG_NONE; + descPso.pRootSignature = m_pRootSignature; + descPso.NodeMask = 0; - ThrowIfFailed(pDevice->GetDevice()->CreateComputePipelineState(&descPso, IID_PPV_ARGS(&m_pPipeline))); + ThrowIfFailed(pDevice->GetDevice()->CreateComputePipelineState(&descPso, IID_PPV_ARGS(&m_pPipeline))); //} - // Allocate descriptors for the mip chain + m_cubeTexture.InitFromFile(pDevice, pUploadHeap, "..\\media\\envmaps\\papermill\\specular.dds", true, 1.0f, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS); + pUploadHeap->FlushAndFinish(); + + // Allocate and create descriptors for the mip chain // - for (int i = 0; i < CS_MAX_MIP_LEVELS; i++) + for (uint32_t mip = 0; mip < m_cubeTexture.GetMipCount() - 1; mip++) { - m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_mip[i].m_constBuffer); - m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_mip[i].m_SRV); - m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_mip[i].m_RTV); + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_mip[mip].m_constBuffer); + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_mip[mip].m_SRV); + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_mip[mip].m_UAV); + + m_cubeTexture.CreateSRV(0, &m_mip[mip].m_SRV, mip); + + D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc; + uavDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // can't create SRGB UAV + uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2DARRAY; + uavDesc.Texture2DArray.ArraySize = m_cubeTexture.GetArraySize(); + uavDesc.Texture2DArray.FirstArraySlice = 0; + uavDesc.Texture2DArray.MipSlice = mip + 1; + uavDesc.Texture2DArray.PlaneSlice = 0; + + m_cubeTexture.CreateUAV(0, NULL, &m_mip[mip].m_UAV, &uavDesc); } - } - void CSDownsampler::OnCreateWindowSizeDependentResources(uint32_t Width, uint32_t Height, Texture *pInput, int mipCount) - { - m_Width = Width; - m_Height = Height; - m_mipCount = mipCount; - m_pInput = pInput; - - m_result.InitRenderTarget( - m_pDevice, - "CSDownsampler::m_result", - &CD3DX12_RESOURCE_DESC::Tex2D( - m_outFormat, - m_Width >> 1, - m_Height >> 1, - 1, - mipCount, - 1, - 0, - D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS), - D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); - - // Create views for the mip chain - // - for (int i = 0; i < m_mipCount; i++) + for (uint32_t slice = 0; slice < m_cubeTexture.GetArraySize(); slice++) { - // source - // - if (i == 0) + for (uint32_t mip = 0; mip < m_cubeTexture.GetMipCount(); mip++) { - pInput->CreateSRV(0, &m_mip[i].m_SRV, 0); + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_imGUISRV[slice * m_cubeTexture.GetMipCount() + mip]); + m_cubeTexture.CreateSRV(0, &m_imGUISRV[slice * m_cubeTexture.GetMipCount() + mip], mip, 1, slice); } - else - { - m_result.CreateSRV(0, &m_mip[i].m_SRV, i - 1); - } - - // destination - // - m_result.CreateUAV(0, &m_mip[i].m_RTV, i); } } - void CSDownsampler::OnDestroyWindowSizeDependentResources() - { - m_result.OnDestroy(); - } - void CSDownsampler::OnDestroy() { + m_cubeTexture.OnDestroy(); + if (m_pPipeline != NULL) { m_pPipeline->Release(); @@ -185,62 +163,81 @@ namespace CAULDRON_DX12 } } - void CSDownsampler::Draw(ID3D12GraphicsCommandList* pCommandList) + void CSDownsampler::Draw(ID3D12GraphicsCommandList *pCommandList) { UserMarker marker(pCommandList, "CSDownsampler"); // downsample // - for (int i = 0; i < m_mipCount; i++) + for (uint32_t slice = 0; slice < m_cubeTexture.GetArraySize(); slice++) { - pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_result.GetResource(), D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, i)); - - D3D12_GPU_VIRTUAL_ADDRESS cbHandle; - uint32_t* pConstMem; - m_pConstantBufferRing->AllocConstantBuffer(sizeof(cbDownscale), (void**)&pConstMem, &cbHandle); - cbDownscale constants; - constants.outWidth = (m_Width >> (i + 1)); - constants.outHeight = (m_Height >> (i + 1)); - constants.invWidth = 1.0f / (float)(m_Width >> i); - constants.invHeight = 1.0f / (float)(m_Height >> i); - memcpy(pConstMem, &constants, sizeof(cbDownscale)); - - // Bind Descriptor heaps and the root signature - // - ID3D12DescriptorHeap *pDescriptorHeaps[] = { m_pResourceViewHeaps->GetCBV_SRV_UAVHeap(), m_pResourceViewHeaps->GetSamplerHeap() }; - pCommandList->SetDescriptorHeaps(2, pDescriptorHeaps); - pCommandList->SetComputeRootSignature(m_pRootSignature); - - // Bind Descriptor the descriptor sets - // - int params = 0; - pCommandList->SetComputeRootConstantBufferView(params++, cbHandle); - pCommandList->SetComputeRootDescriptorTable(params++, m_mip[i].m_RTV.GetGPU()); - pCommandList->SetComputeRootDescriptorTable(params++, m_mip[i].m_SRV.GetGPU()); - - // Bind Pipeline - // - pCommandList->SetPipelineState(m_pPipeline); - - // Dispatch - // - uint32_t dispatchX = ((m_Width >> (i + 1)) + 7) / 8; - uint32_t dispatchY = ((m_Height >> (i + 1)) + 7) / 8; - uint32_t dispatchZ = 1; - pCommandList->Dispatch(dispatchX, dispatchY, dispatchZ); - - pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_result.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, i)); + for (uint32_t i = 0; i < m_cubeTexture.GetMipCount() - 1; i++) + { + pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_cubeTexture.GetResource(), D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, i + 1)); + + D3D12_GPU_VIRTUAL_ADDRESS cbHandle; + uint32_t* pConstMem; + m_pConstantBufferRing->AllocConstantBuffer(sizeof(cbDownsample), (void**)&pConstMem, &cbHandle); + cbDownsample constants; + constants.outWidth = (m_cubeTexture.GetWidth() >> (i + 1)); + constants.outHeight = (m_cubeTexture.GetHeight() >> (i + 1)); + constants.invWidth = 1.0f / (float)(m_cubeTexture.GetWidth() >> i); + constants.invHeight = 1.0f / (float)(m_cubeTexture.GetHeight() >> i); + constants.slice = slice; + memcpy(pConstMem, &constants, sizeof(cbDownsample)); + + // Bind Descriptor heaps and the root signature + // + ID3D12DescriptorHeap* pDescriptorHeaps[] = { m_pResourceViewHeaps->GetCBV_SRV_UAVHeap(), m_pResourceViewHeaps->GetSamplerHeap() }; + pCommandList->SetDescriptorHeaps(2, pDescriptorHeaps); + pCommandList->SetComputeRootSignature(m_pRootSignature); + + // Bind Descriptor the descriptor sets + // + int params = 0; + pCommandList->SetComputeRootConstantBufferView(params++, cbHandle); + pCommandList->SetComputeRootDescriptorTable(params++, m_mip[i].m_UAV.GetGPU()); + pCommandList->SetComputeRootDescriptorTable(params++, m_mip[i].m_SRV.GetGPU()); + + // Bind Pipeline + // + pCommandList->SetPipelineState(m_pPipeline); + + // Dispatch + // + uint32_t dispatchX = ((m_cubeTexture.GetWidth() >> (i + 1)) + 7) / 8; + uint32_t dispatchY = ((m_cubeTexture.GetHeight() >> (i + 1)) + 7) / 8; + uint32_t dispatchZ = 1; + pCommandList->Dispatch(dispatchX, dispatchY, dispatchZ); + + pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_cubeTexture.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, i + 1)); + } } } - void CSDownsampler::Gui() + void CSDownsampler::GUI(int *pSlice) { bool opened = true; - ImGui::Begin("Downsample", &opened); + std::string header = "Downsample"; + ImGui::Begin(header.c_str(), &opened); - for (int i = 0; i < m_mipCount; i++) + if (ImGui::CollapsingHeader("CS Multipass", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Image((ImTextureID)&m_mip[i].m_SRV, ImVec2(320, 180)); + const char* sliceItemNames[] = + { + "Slice 0", + "Slice 1", + "Slice 2", + "Slice 3", + "Slice 4", + "Slice 5" + }; + ImGui::Combo("Slice of Cube Texture", pSlice, sliceItemNames, _countof(sliceItemNames)); + + for (uint32_t i = 0; i < m_cubeTexture.GetMipCount(); i++) + { + ImGui::Image((ImTextureID)&m_imGUISRV[*pSlice * m_cubeTexture.GetMipCount() + i], ImVec2(static_cast(512 >> i), static_cast(512 >> i))); + } } ImGui::End(); diff --git a/sample/src/DX12/CSDownsampler.h b/sample/src/DX12/CSDownsampler.h index 4a64086..c6be9be 100644 --- a/sample/src/DX12/CSDownsampler.h +++ b/sample/src/DX12/CSDownsampler.h @@ -28,48 +28,42 @@ namespace CAULDRON_DX12 class CSDownsampler { public: - void OnCreate(Device *pDevice, ResourceViewHeaps *pResourceViewHeaps, DynamicBufferRing *pConstantBufferRing, DXGI_FORMAT outFormat); + void OnCreate(Device *pDevice, UploadHeap *pUploadHeap, ResourceViewHeaps *pResourceViewHeaps, DynamicBufferRing *pConstantBufferRing); void OnDestroy(); - void OnCreateWindowSizeDependentResources(uint32_t Width, uint32_t Height, Texture *pInput, int mips); - void OnDestroyWindowSizeDependentResources(); + void Draw(ID3D12GraphicsCommandList *pCommandList); + Texture *GetTexture() { return &m_cubeTexture; } + void GUI(int *pSlice); - void Draw(ID3D12GraphicsCommandList* pCommandList); - Texture *GetTexture() { return &m_result; } - CBV_SRV_UAV GetTextureView(int i) { return m_mip[i].m_SRV; } - void Gui(); - - struct cbDownscale + struct cbDownsample { uint32_t outWidth, outHeight; float invWidth, invHeight; + uint32_t slice; + uint32_t padding[3]; }; private: - Device* m_pDevice = nullptr; - DXGI_FORMAT m_outFormat; + Device *m_pDevice = nullptr; - Texture *m_pInput; - Texture m_result; + Texture m_cubeTexture; struct Pass { CBV_SRV_UAV m_constBuffer; // dimension - CBV_SRV_UAV m_RTV; //dest -> more like UAV x) + CBV_SRV_UAV m_UAV; //dest CBV_SRV_UAV m_SRV; //src }; Pass m_mip[CS_MAX_MIP_LEVELS]; - ResourceViewHeaps *m_pResourceViewHeaps; - DynamicBufferRing *m_pConstantBufferRing; - ID3D12RootSignature *m_pRootSignature; - ID3D12PipelineState *m_pPipeline = NULL; + ResourceViewHeaps *m_pResourceViewHeaps = nullptr; + DynamicBufferRing *m_pConstantBufferRing = nullptr; + ID3D12RootSignature *m_pRootSignature = nullptr; + ID3D12PipelineState *m_pPipeline = nullptr; - SAMPLER m_Sampler; + SAMPLER m_sampler; - uint32_t m_Width; - uint32_t m_Height; - int m_mipCount; + CBV_SRV_UAV m_imGUISRV[CS_MAX_MIP_LEVELS * 6]; }; } \ No newline at end of file diff --git a/sample/src/DX12/CSDownsampler.hlsl b/sample/src/DX12/CSDownsampler.hlsl index 442b178..8e54c00 100644 --- a/sample/src/DX12/CSDownsampler.hlsl +++ b/sample/src/DX12/CSDownsampler.hlsl @@ -22,16 +22,17 @@ //-------------------------------------------------------------------------------------- cbuffer cbPerFrame : register(b0) { - uint2 u_outSize; + uint2 u_outSize; float2 u_invSize; + uint u_slice; } //-------------------------------------------------------------------------------------- // Texture definitions //-------------------------------------------------------------------------------------- -RWTexture2D outputTex : register(u0); -Texture2D inputTex : register(t0); -SamplerState samLinearMirror : register(s0); +RWTexture2DArray outputTex : register(u0); +Texture2DArray inputTex : register(t0); +SamplerState samLinear : register(s0); // Main function //-------------------------------------------------------------------------------------- @@ -43,5 +44,10 @@ void main(uint3 LocalThreadId : SV_GroupThreadID, uint3 WorkGroupId : SV_GroupID return; float2 samplerTexCoord = u_invSize * float2(DispatchId.xy) * 2.0 + u_invSize; - outputTex[DispatchId.xy] = inputTex.SampleLevel(samLinearMirror, samplerTexCoord, 0); + float4 result = inputTex.SampleLevel(samLinear, float3(samplerTexCoord, u_slice), 0); + result.r = max(min(result.r * (12.92), (0.0031308)), (1.055) * pow(result.r, (0.41666)) - (0.055)); + result.g = max(min(result.g * (12.92), (0.0031308)), (1.055) * pow(result.g, (0.41666)) - (0.055)); + result.b = max(min(result.b * (12.92), (0.0031308)), (1.055) * pow(result.b, (0.41666)) - (0.055)); + + outputTex[int3(DispatchId.xy, u_slice)] = result; } \ No newline at end of file diff --git a/sample/src/DX12/PSDownsampler.cpp b/sample/src/DX12/PSDownsampler.cpp index 06dcf05..2bde4f0 100644 --- a/sample/src/DX12/PSDownsampler.cpp +++ b/sample/src/DX12/PSDownsampler.cpp @@ -32,17 +32,16 @@ namespace CAULDRON_DX12 { void PSDownsampler::OnCreate( Device *pDevice, + UploadHeap *pUploadHeap, ResourceViewHeaps *pResourceViewHeaps, DynamicBufferRing *pConstantBufferRing, - StaticBufferPool *pStaticBufferPool, - DXGI_FORMAT outFormat + StaticBufferPool *pStaticBufferPool ) { m_pDevice = pDevice; m_pStaticBufferPool = pStaticBufferPool; m_pResourceViewHeaps = pResourceViewHeaps; m_pConstantBufferRing = pConstantBufferRing; - m_outFormat = outFormat; // Use helper class to create the fullscreen pass // @@ -62,99 +61,104 @@ namespace CAULDRON_DX12 SamplerDesc.RegisterSpace = 0; SamplerDesc.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; - m_downscale.OnCreate(pDevice, "PSDownsampler.hlsl", m_pResourceViewHeaps, - m_pStaticBufferPool, 1, 1, &SamplerDesc, m_outFormat); + m_cubeTexture.InitFromFile(pDevice, pUploadHeap, "..\\media\\envmaps\\papermill\\specular.dds", true, 1.0f, D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET); + pUploadHeap->FlushAndFinish(); + + m_downsample.OnCreate(pDevice, "PSDownsampler.hlsl", m_pResourceViewHeaps, + m_pStaticBufferPool, 1, 1, &SamplerDesc, m_cubeTexture.GetFormat()); // Allocate descriptors for the mip chain // - for (int i = 0; i < DOWNSAMPLEPS_MAX_MIP_LEVELS; i++) + for (uint32_t slice = 0; slice < m_cubeTexture.GetArraySize(); slice++) { - m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_mip[i].m_SRV); - m_pResourceViewHeaps->AllocRTVDescriptor(1, &m_mip[i].m_RTV); - } - - } - - void PSDownsampler::OnCreateWindowSizeDependentResources(uint32_t Width, uint32_t Height, Texture *pInput, int mipCount) - { - m_Width = Width; - m_Height = Height; - m_mipCount = mipCount; - m_pInput = pInput; + for (uint32_t i = 0; i < m_cubeTexture.GetMipCount() - 1; i++) + { + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_mip[slice * (m_cubeTexture.GetMipCount() - 1) + i].m_SRV); + m_pResourceViewHeaps->AllocRTVDescriptor(1, &m_mip[slice * (m_cubeTexture.GetMipCount() - 1) + i].m_RTV); - m_result.InitRenderTarget(m_pDevice, "PSDownsampler::m_result", &CD3DX12_RESOURCE_DESC::Tex2D(m_outFormat, m_Width >> 1, m_Height >> 1, 1, mipCount, 1, 0, D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + m_cubeTexture.CreateSRV(0, &m_mip[slice * (m_cubeTexture.GetMipCount() - 1) + i].m_SRV, i, 1, slice); + m_cubeTexture.CreateRTV(0, &m_mip[slice * (m_cubeTexture.GetMipCount() - 1) + i].m_RTV, i + 1, 1, slice); + } + } - // Create views for the mip chain - // - for (int i = 0; i < m_mipCount; i++) + for (uint32_t slice = 0; slice < m_cubeTexture.GetArraySize(); slice++) { - // source - // - if (i == 0) - { - pInput->CreateSRV(0, &m_mip[i].m_SRV, 0); - } - else + for (uint32_t mip = 0; mip < m_cubeTexture.GetMipCount(); mip++) { - m_result.CreateSRV(0, &m_mip[i].m_SRV, i - 1); + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_imGUISRV[slice * m_cubeTexture.GetMipCount() + mip]); + m_cubeTexture.CreateSRV(0, &m_imGUISRV[slice * m_cubeTexture.GetMipCount() + mip], mip, 1, slice); } - - // destination - // - m_result.CreateRTV(0, &m_mip[i].m_RTV, i); } } - void PSDownsampler::OnDestroyWindowSizeDependentResources() - { - m_result.OnDestroy(); - } - void PSDownsampler::OnDestroy() { - m_downscale.OnDestroy(); + m_cubeTexture.OnDestroy(); + m_downsample.OnDestroy(); } - void PSDownsampler::Draw(ID3D12GraphicsCommandList* pCommandList) + void PSDownsampler::Draw(ID3D12GraphicsCommandList *pCommandList) { UserMarker marker(pCommandList, "PSDownsampler"); // downsample // - for (int i = 0; i < m_mipCount; i++) + for (uint32_t slice = 0; slice < m_cubeTexture.GetArraySize(); slice++) { - pCommandList->OMSetRenderTargets(1, &m_mip[i].m_RTV.GetCPU(), true, NULL); - SetViewportAndScissor(pCommandList, 0, 0, m_Width >> (i + 1), m_Height >> (i + 1)); - - cbDownscale *data; - D3D12_GPU_VIRTUAL_ADDRESS constantBuffer; - m_pConstantBufferRing->AllocConstantBuffer(sizeof(cbDownscale), (void **)&data, &constantBuffer); - data->outWidth = (float)(m_Width >> (i + 1)); - data->outHeight = (float)(m_Height >> (i + 1)); - data->invWidth = 1.0f / (float)(m_Width >> i); - data->invHeight = 1.0f / (float)(m_Height >> i); - - if (i > 0) + for (uint32_t i = 0; i < m_cubeTexture.GetMipCount() - 1; i++) { - pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_result.GetResource(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, i - 1)); + pCommandList->ResourceBarrier(1, + &CD3DX12_RESOURCE_BARRIER::Transition(m_cubeTexture.GetResource(), + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, + D3D12_RESOURCE_STATE_RENDER_TARGET, + slice * m_cubeTexture.GetMipCount() + i + 1)); + + pCommandList->OMSetRenderTargets(1, &m_mip[slice * (m_cubeTexture.GetMipCount() - 1) + i].m_RTV.GetCPU(), true, NULL); + SetViewportAndScissor(pCommandList, 0, 0, m_cubeTexture.GetWidth() >> (i + 1), m_cubeTexture.GetHeight() >> (i + 1)); + + cbDownsample* data; + D3D12_GPU_VIRTUAL_ADDRESS constantBuffer; + m_pConstantBufferRing->AllocConstantBuffer(sizeof(cbDownsample), (void**)&data, &constantBuffer); + data->outWidth = (float)(m_cubeTexture.GetWidth() >> (i + 1)); + data->outHeight = (float)(m_cubeTexture.GetHeight() >> (i + 1)); + data->invWidth = 1.0f / (float)(m_cubeTexture.GetWidth() >> i); + data->invHeight = 1.0f / (float)(m_cubeTexture.GetHeight() >> i); + data->slice = slice; + + m_downsample.Draw(pCommandList, 1, &m_mip[slice * (m_cubeTexture.GetMipCount() - 1) + i].m_SRV, constantBuffer); + + pCommandList->ResourceBarrier(1, + &CD3DX12_RESOURCE_BARRIER::Transition(m_cubeTexture.GetResource(), + D3D12_RESOURCE_STATE_RENDER_TARGET, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, + slice * m_cubeTexture.GetMipCount() + i + 1)); } - - pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_result.GetResource(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET, i)); - - m_downscale.Draw(pCommandList, 1, &m_mip[i].m_SRV, constantBuffer); } - - pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_result.GetResource(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, m_mipCount - 1)); } - void PSDownsampler::Gui() + void PSDownsampler::GUI(int *pSlice) { bool opened = true; - ImGui::Begin("Downsample", &opened); + std::string header = "Downsample"; + ImGui::Begin(header.c_str(), &opened); - for (int i = 0; i < m_mipCount; i++) + if (ImGui::CollapsingHeader("PS", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Image((ImTextureID)&m_mip[i].m_SRV, ImVec2(320, 180)); + const char* sliceItemNames[] = + { + "Slice 0", + "Slice 1", + "Slice 2", + "Slice 3", + "Slice 4", + "Slice 5" + }; + ImGui::Combo("Slice of Cube Texture", pSlice, sliceItemNames, _countof(sliceItemNames)); + + for (uint32_t i = 0; i < m_cubeTexture.GetMipCount(); i++) + { + ImGui::Image((ImTextureID)&m_imGUISRV[*pSlice * m_cubeTexture.GetMipCount() + i], ImVec2(static_cast(512 >> i), static_cast(512 >> i))); + } } ImGui::End(); diff --git a/sample/src/DX12/PSDownsampler.h b/sample/src/DX12/PSDownsampler.h index 71e98b7..e39ebb9 100644 --- a/sample/src/DX12/PSDownsampler.h +++ b/sample/src/DX12/PSDownsampler.h @@ -29,29 +29,25 @@ namespace CAULDRON_DX12 class PSDownsampler { public: - void OnCreate(Device *pDevice, ResourceViewHeaps *pResourceViewHeaps, DynamicBufferRing *pConstantBufferRing, StaticBufferPool *pStaticBufferPool, DXGI_FORMAT outFormat); + void OnCreate(Device *pDevice, UploadHeap *pUploadHeap, ResourceViewHeaps *pResourceViewHeaps, DynamicBufferRing *pConstantBufferRing, StaticBufferPool *pStaticBufferPool); void OnDestroy(); - void OnCreateWindowSizeDependentResources(uint32_t Width, uint32_t Height, Texture *pInput, int mips); - void OnDestroyWindowSizeDependentResources(); + void Draw(ID3D12GraphicsCommandList *pCommandList); + Texture *GetTexture() { return &m_cubeTexture; } + void GUI(int *pSlice); - void Draw(ID3D12GraphicsCommandList* pCommandList); - Texture *GetTexture() { return &m_result; } - CBV_SRV_UAV GetTextureView(int i) { return m_mip[i].m_SRV; } - void Gui(); - - struct cbDownscale + struct cbDownsample { float outWidth, outHeight; float invWidth, invHeight; + int slice; + int padding[3]; }; private: - Device* m_pDevice = nullptr; - DXGI_FORMAT m_outFormat; + Device *m_pDevice = nullptr; - Texture *m_pInput; - Texture m_result; + Texture m_cubeTexture; struct Pass { @@ -59,16 +55,14 @@ namespace CAULDRON_DX12 CBV_SRV_UAV m_SRV; //src }; - Pass m_mip[PS_MAX_MIP_LEVELS]; + Pass m_mip[PS_MAX_MIP_LEVELS * 6]; - StaticBufferPool *m_pStaticBufferPool; - ResourceViewHeaps *m_pResourceViewHeaps; - DynamicBufferRing *m_pConstantBufferRing; + StaticBufferPool *m_pStaticBufferPool = nullptr; + ResourceViewHeaps *m_pResourceViewHeaps = nullptr; + DynamicBufferRing *m_pConstantBufferRing = nullptr; - uint32_t m_Width; - uint32_t m_Height; - int m_mipCount; + PostProcPS m_downsample; - PostProcPS m_downscale; + CBV_SRV_UAV m_imGUISRV[PS_MAX_MIP_LEVELS * 6]; }; } \ No newline at end of file diff --git a/sample/src/DX12/PSDownsampler.hlsl b/sample/src/DX12/PSDownsampler.hlsl index 4bb1185..020e1e3 100644 --- a/sample/src/DX12/PSDownsampler.hlsl +++ b/sample/src/DX12/PSDownsampler.hlsl @@ -38,7 +38,7 @@ struct VERTEX // Texture definitions //-------------------------------------------------------------------------------------- Texture2D inputTex :register(t0); -SamplerState samLinearMirror :register(s0); +SamplerState samLinear :register(s0); //-------------------------------------------------------------------------------------- // Main function @@ -46,7 +46,8 @@ SamplerState samLinearMirror :register(s0); float4 mainPS(VERTEX Input) : SV_Target { + // as compute shader solution float2 texCoord = Input.vTexcoord * u_outSize; texCoord = texCoord * u_invSize * 2.0; - return inputTex.Sample(samLinearMirror, texCoord); + return inputTex.Sample(samLinear, texCoord); } diff --git a/sample/src/DX12/SPDCS.cpp b/sample/src/DX12/SPDCS.cpp new file mode 100644 index 0000000..95a2d63 --- /dev/null +++ b/sample/src/DX12/SPDCS.cpp @@ -0,0 +1,443 @@ +// SPDSample +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include "stdafx.h" +#include "base\Device.h" +#include "base\DynamicBufferRing.h" +#include "base\StaticBufferPool.h" +#include "base\UploadHeap.h" +#include "base\Texture.h" +#include "base\Imgui.h" +#include "base\Helper.h" +#include "Base\ShaderCompilerHelper.h" + +#include "SPDCS.h" + +#define A_CPU +#include "ffx_a.h" +#include "ffx_spd.h" + +namespace CAULDRON_DX12 +{ + void SPDCS::OnCreate( + Device *pDevice, + UploadHeap *pUploadHeap, + ResourceViewHeaps *pResourceViewHeaps, + DynamicBufferRing *pConstantBufferRing, + SPDLoad spdLoad, + SPDWaveOps spdWaveOps, + SPDPacked spdPacked + ) + { + m_pDevice = pDevice; + m_pResourceViewHeaps = pResourceViewHeaps; + m_pConstantBufferRing = pConstantBufferRing; + + m_spdLoad = spdLoad; + m_spdWaveOps = spdWaveOps; + m_spdPacked = spdPacked; + + D3D12_SHADER_BYTECODE shaderByteCode = {}; + DefineList defines; + + if (m_spdWaveOps == SPDWaveOps::SPDNoWaveOps) { + defines["SPD_NO_WAVE_OPERATIONS"] = 1; + } + if (m_spdPacked == SPDPacked::SPDPacked) { + defines["A_HALF"] = 1; + defines["SPD_PACKED_ONLY"] = 1; + } + + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + CompileShaderFromFile("SPDIntegrationLinearSampler.hlsl", &defines, "main", "-T cs_6_2 /Zi /Zss", &shaderByteCode); + } + else { + CompileShaderFromFile("SPDIntegration.hlsl", &defines, "main", "-T cs_6_2 /Zi /Zss", &shaderByteCode); + } + + // Create root signature + // Spd Load version + if (m_spdLoad == SPDLoad::SPDLoad) + { + CD3DX12_DESCRIPTOR_RANGE DescRange[5]; + CD3DX12_ROOT_PARAMETER RTSlot[5]; + + // 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); + + // UAV table + global counter buffer + DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 1); + RTSlot[parameterCount++].InitAsDescriptorTable(1, &DescRange[1], D3D12_SHADER_VISIBILITY_ALL); + + // output mips + DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 2); + RTSlot[parameterCount++].InitAsDescriptorTable(1, &DescRange[2], D3D12_SHADER_VISIBILITY_ALL); + + DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, SPD_MAX_MIP_LEVELS + 1, 3); + RTSlot[parameterCount++].InitAsDescriptorTable(1, &DescRange[3], D3D12_SHADER_VISIBILITY_ALL); + + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + // SRV table + DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0); + RTSlot[parameterCount++].InitAsDescriptorTable(1, &DescRange[4], D3D12_SHADER_VISIBILITY_ALL); + } + + // the root signature contains 4 slots to be used + CD3DX12_ROOT_SIGNATURE_DESC descRootSignature = CD3DX12_ROOT_SIGNATURE_DESC(); + descRootSignature.NumParameters = parameterCount; + descRootSignature.pParameters = RTSlot; + descRootSignature.NumStaticSamplers = 0; + descRootSignature.pStaticSamplers = NULL; + + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + D3D12_STATIC_SAMPLER_DESC SamplerDesc = {}; + SamplerDesc.Filter = D3D12_FILTER_MIN_MAG_LINEAR_MIP_POINT; + SamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + SamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + SamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + SamplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; + SamplerDesc.BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK; + SamplerDesc.MinLOD = 0.0f; + SamplerDesc.MaxLOD = D3D12_FLOAT32_MAX; + SamplerDesc.MipLODBias = 0; + SamplerDesc.MaxAnisotropy = 1; + SamplerDesc.ShaderRegister = 0; + SamplerDesc.RegisterSpace = 0; + SamplerDesc.ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + + descRootSignature.NumStaticSamplers = 1; + descRootSignature.pStaticSamplers = &SamplerDesc; + } + + // deny uneccessary access to certain pipeline stages + descRootSignature.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE; + + ID3DBlob* pOutBlob, * pErrorBlob = NULL; + ThrowIfFailed(D3D12SerializeRootSignature(&descRootSignature, D3D_ROOT_SIGNATURE_VERSION_1, &pOutBlob, &pErrorBlob)); + ThrowIfFailed( + pDevice->GetDevice()->CreateRootSignature(0, pOutBlob->GetBufferPointer(), pOutBlob->GetBufferSize(), IID_PPV_ARGS(&m_pRootSignature)) + ); + SetName(m_pRootSignature, std::string("PostProcCS::") + "SPD_CS"); + + pOutBlob->Release(); + if (pErrorBlob) + pErrorBlob->Release(); + } + + // SPD Linear Sampler version + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + CD3DX12_DESCRIPTOR_RANGE DescRange[5]; + CD3DX12_ROOT_PARAMETER RTSlot[5]; + + // 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); + + // UAV table + global counter buffer + DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 1); + RTSlot[parameterCount++].InitAsDescriptorTable(1, &DescRange[1], D3D12_SHADER_VISIBILITY_ALL); + + // output mips + DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 2); + RTSlot[parameterCount++].InitAsDescriptorTable(1, &DescRange[2], D3D12_SHADER_VISIBILITY_ALL); + + DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, SPD_MAX_MIP_LEVELS + 1, 3); + RTSlot[parameterCount++].InitAsDescriptorTable(1, &DescRange[3], D3D12_SHADER_VISIBILITY_ALL); + + // SRV table + DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0); + RTSlot[parameterCount++].InitAsDescriptorTable(1, &DescRange[4], D3D12_SHADER_VISIBILITY_ALL); + + D3D12_STATIC_SAMPLER_DESC SamplerDesc = {}; + SamplerDesc.Filter = D3D12_FILTER_MIN_MAG_LINEAR_MIP_POINT; + SamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + SamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + SamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + SamplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; + SamplerDesc.BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK; + SamplerDesc.MinLOD = 0.0f; + SamplerDesc.MaxLOD = D3D12_FLOAT32_MAX; + SamplerDesc.MipLODBias = 0; + SamplerDesc.MaxAnisotropy = 1; + SamplerDesc.ShaderRegister = 0; + SamplerDesc.RegisterSpace = 0; + SamplerDesc.ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + + // the root signature contains 4 slots to be used + CD3DX12_ROOT_SIGNATURE_DESC descRootSignature = CD3DX12_ROOT_SIGNATURE_DESC(); + descRootSignature.NumParameters = parameterCount; + descRootSignature.pParameters = RTSlot; + descRootSignature.NumStaticSamplers = 1; + descRootSignature.pStaticSamplers = &SamplerDesc; + + // deny uneccessary access to certain pipeline stages + descRootSignature.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE; + + ID3DBlob* pOutBlob, * pErrorBlob = NULL; + ThrowIfFailed(D3D12SerializeRootSignature(&descRootSignature, D3D_ROOT_SIGNATURE_VERSION_1, &pOutBlob, &pErrorBlob)); + ThrowIfFailed( + pDevice->GetDevice()->CreateRootSignature(0, pOutBlob->GetBufferPointer(), pOutBlob->GetBufferSize(), IID_PPV_ARGS(&m_pRootSignature)) + ); + SetName(m_pRootSignature, std::string("PostProcCS::") + "SPD_CS"); + + pOutBlob->Release(); + if (pErrorBlob) + pErrorBlob->Release(); + } + + { + D3D12_COMPUTE_PIPELINE_STATE_DESC descPso = {}; + descPso.CS = shaderByteCode; + descPso.Flags = D3D12_PIPELINE_STATE_FLAG_NONE; + descPso.pRootSignature = m_pRootSignature; + descPso.NodeMask = 0; + + ThrowIfFailed(pDevice->GetDevice()->CreateComputePipelineState(&descPso, IID_PPV_ARGS(&m_pPipeline))); + } + + m_cubeTexture.InitFromFile(pDevice, pUploadHeap, "..\\media\\envmaps\\papermill\\specular.dds", true, 1.0f, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS); + pUploadHeap->FlushAndFinish(); + + // Allocate descriptors for the mip chain + // + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_constBuffer); + + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_sourceSRV); + m_cubeTexture.CreateSRV(0, &m_sourceSRV, 0, m_cubeTexture.GetArraySize(), 0); + } + + uint32_t numUAVs = m_cubeTexture.GetMipCount(); + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + // we need one UAV less because source texture will be bound as SRV and not as UAV + numUAVs = m_cubeTexture.GetMipCount() - 1; + } + + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(numUAVs, m_UAV); + + // Create views for the mip chain + // + // destination + // + for (uint32_t i = 0; i < numUAVs; i++) + { + D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc; + uavDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // can't create SRGB UAV + uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2DARRAY; + uavDesc.Texture2DArray.ArraySize = m_cubeTexture.GetArraySize(); + uavDesc.Texture2DArray.FirstArraySlice = 0; + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + uavDesc.Texture2DArray.MipSlice = i + 1; + } + else { + uavDesc.Texture2DArray.MipSlice = i; + } + uavDesc.Texture2DArray.PlaneSlice = 0; + + m_cubeTexture.CreateUAV(i, NULL, m_UAV, &uavDesc); + } + + // for GUI + for (uint32_t slice = 0; slice < m_cubeTexture.GetArraySize(); slice++) + { + for (uint32_t mip = 0; mip < m_cubeTexture.GetMipCount(); mip++) + { + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_SRV[slice * m_cubeTexture.GetMipCount() + mip]); + m_cubeTexture.CreateSRV(0, &m_SRV[slice * m_cubeTexture.GetMipCount() + mip], mip, 1, slice); + } + } + + m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_globalCounter); + m_globalCounterBuffer.InitBuffer(m_pDevice, "SPD_CS::m_globalCounterBuffer", + &CD3DX12_RESOURCE_DESC::Buffer(sizeof(uint32_t) * m_cubeTexture.GetArraySize(), // 6 slices + D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS), + sizeof(uint32_t), D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + m_globalCounterBuffer.CreateBufferUAV(0, NULL, &m_globalCounter); + } + + void SPDCS::OnDestroy() + { + m_globalCounterBuffer.OnDestroy(); + m_cubeTexture.OnDestroy(); + + if (m_pPipeline != NULL) + { + m_pPipeline->Release(); + m_pPipeline = NULL; + } + + if (m_pRootSignature != NULL) + { + m_pRootSignature->Release(); + m_pRootSignature = NULL; + } + } + + void SPDCS::Draw(ID3D12GraphicsCommandList2 *pCommandList) + { + UserMarker marker(pCommandList, "SPDCS"); + + varAU2(dispatchThreadGroupCountXY); + varAU2(workGroupOffset); // needed if Left and Top are not 0,0 + varAU2(numWorkGroupsAndMips); + varAU4(rectInfo) = initAU4(0, 0, m_cubeTexture.GetWidth(), m_cubeTexture.GetHeight()); // left, top, width, height + SpdSetup(dispatchThreadGroupCountXY, workGroupOffset, numWorkGroupsAndMips, rectInfo); + + // downsample + uint32_t dispatchX = dispatchThreadGroupCountXY[0]; + uint32_t dispatchY = dispatchThreadGroupCountXY[1]; + uint32_t dispatchZ = m_cubeTexture.GetArraySize(); + + D3D12_GPU_VIRTUAL_ADDRESS cbHandle; + uint32_t* pConstMem; + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + m_pConstantBufferRing->AllocConstantBuffer(sizeof(SpdLinearSamplerConstants), (void**)&pConstMem, &cbHandle); + SpdLinearSamplerConstants constants; + constants.numWorkGroupsPerSlice = numWorkGroupsAndMips[0]; + constants.mips = numWorkGroupsAndMips[1]; + constants.workGroupOffset[0] = workGroupOffset[0]; + constants.workGroupOffset[1] = workGroupOffset[1]; + constants.invInputSize[0] = 1.0f / m_cubeTexture.GetWidth(); + constants.invInputSize[1] = 1.0f / m_cubeTexture.GetHeight(); + memcpy(pConstMem, &constants, sizeof(SpdLinearSamplerConstants)); + } + else { + m_pConstantBufferRing->AllocConstantBuffer(sizeof(SpdConstants), (void**)&pConstMem, &cbHandle); + SpdConstants constants; + constants.numWorkGroupsPerSlice = numWorkGroupsAndMips[0]; + constants.mips = numWorkGroupsAndMips[1]; + constants.workGroupOffset[0] = workGroupOffset[0]; + constants.workGroupOffset[1] = workGroupOffset[1]; + memcpy(pConstMem, &constants, sizeof(SpdConstants)); + } + + // Bind Descriptor heaps and the root signature + // + ID3D12DescriptorHeap *pDescriptorHeaps[] = { m_pResourceViewHeaps->GetCBV_SRV_UAVHeap(), m_pResourceViewHeaps->GetSamplerHeap() }; + pCommandList->SetDescriptorHeaps(2, pDescriptorHeaps); + pCommandList->SetComputeRootSignature(m_pRootSignature); + + // Bind Descriptor the descriptor sets + // + int params = 0; + pCommandList->SetComputeRootConstantBufferView(params++, cbHandle); + pCommandList->SetComputeRootDescriptorTable(params++, m_globalCounter.GetGPU()); + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + pCommandList->SetComputeRootDescriptorTable(params++, m_UAV[0].GetGPU(5)); + } + else { + pCommandList->SetComputeRootDescriptorTable(params++, m_UAV[0].GetGPU(6)); + } + // bind UAVs + pCommandList->SetComputeRootDescriptorTable(params++, m_UAV[0].GetGPU()); + + // bind SRV + if (m_spdLoad == SPDLoad::SPDLinearSampler) { + pCommandList->SetComputeRootDescriptorTable(params++, m_sourceSRV.GetGPU()); + } + // Bind Pipeline + // + pCommandList->SetPipelineState(m_pPipeline); + + // set counter to 0 + pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_globalCounterBuffer.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_COPY_DEST, 0)); + + D3D12_WRITEBUFFERIMMEDIATE_PARAMETER pParams[6]; + for (int i = 0; i < 6; i++) + { + pParams[i] = { m_globalCounterBuffer.GetResource()->GetGPUVirtualAddress() + sizeof(uint32_t) * i, 0 }; + } + pCommandList->WriteBufferImmediate(6, pParams, NULL); // 6 counter per slice, each initialized to 0 + + D3D12_RESOURCE_BARRIER resourceBarriers[2] = { + CD3DX12_RESOURCE_BARRIER::Transition(m_globalCounterBuffer.GetResource(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, 0), + CD3DX12_RESOURCE_BARRIER::Transition(m_cubeTexture.GetResource(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS) + }; + pCommandList->ResourceBarrier(2, resourceBarriers); + + // Dispatch + // + pCommandList->Dispatch(dispatchX, dispatchY, dispatchZ); + pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_cubeTexture.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE)); + } + + void SPDCS::GUI(int *pSlice) + { + bool opened = true; + std::string header = "Downsample"; + ImGui::Begin(header.c_str(), &opened); + + std::string downsampleHeader = "SPD CS"; + if (m_spdLoad == SPDLoad::SPDLoad) { + downsampleHeader += " Load"; + } + else { + downsampleHeader += " Linear Sampler"; + } + + if (m_spdWaveOps == SPDWaveOps::SPDWaveOps) + { + downsampleHeader += " WaveOps"; + } + else { + downsampleHeader += " No WaveOps"; + } + + if (m_spdPacked == SPDPacked::SPDNonPacked) + { + downsampleHeader += " Non Packed"; + } + else { + downsampleHeader += " Packed"; + } + + if (ImGui::CollapsingHeader(downsampleHeader.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) + { + const char* sliceItemNames[] = + { + "Slice 0", + "Slice 1", + "Slice 2", + "Slice 3", + "Slice 4", + "Slice 5" + }; + ImGui::Combo("Slice of Cube Texture", pSlice, sliceItemNames, _countof(sliceItemNames)); + + for (uint32_t i = 0; i < m_cubeTexture.GetMipCount(); i++) + { + ImGui::Image((ImTextureID)&m_SRV[*pSlice * m_cubeTexture.GetMipCount() + i], ImVec2(static_cast(512 >> i), static_cast(512 >> i))); + } + } + + ImGui::End(); + } +} \ No newline at end of file diff --git a/sample/src/DX12/SPDCS.h b/sample/src/DX12/SPDCS.h new file mode 100644 index 0000000..78c9f71 --- /dev/null +++ b/sample/src/DX12/SPDCS.h @@ -0,0 +1,95 @@ +// SPDSample +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +#pragma once + +#include "Base/DynamicBufferRing.h" +#include "Base/Texture.h" + +namespace CAULDRON_DX12 +{ +#define SPD_MAX_MIP_LEVELS 12 + + enum class SPDWaveOps + { + SPDNoWaveOps, + SPDWaveOps, + }; + + enum class SPDPacked + { + SPDNonPacked, + SPDPacked, + }; + + enum class SPDLoad + { + SPDLoad, + SPDLinearSampler, + }; + + class SPDCS + { + public: + void OnCreate(Device *pDevice, UploadHeap *pUploadHeap, ResourceViewHeaps *pResourceViewHeaps, DynamicBufferRing *pConstantBufferRing, + SPDLoad spdLoad, SPDWaveOps spdWaveOps, SPDPacked spdPacked); + void OnDestroy(); + + void Draw(ID3D12GraphicsCommandList2 *pCommandList); + Texture *GetTexture() { return &m_cubeTexture; } + void GUI(int *pSlice); + + struct SpdConstants + { + int mips; + int numWorkGroupsPerSlice; + int workGroupOffset[2]; + }; + + struct SpdLinearSamplerConstants + { + int mips; + int numWorkGroupsPerSlice; + int workGroupOffset[2]; + float invInputSize[2]; + float padding[2]; + }; + + private: + Device *m_pDevice = nullptr; + + Texture m_cubeTexture; + + CBV_SRV_UAV m_constBuffer; // dimension + CBV_SRV_UAV m_UAV[SPD_MAX_MIP_LEVELS + 1]; //src + dest mips + CBV_SRV_UAV m_SRV[SPD_MAX_MIP_LEVELS * 6]; // for display of mips using imGUI + CBV_SRV_UAV m_sourceSRV; // src + + CBV_SRV_UAV m_globalCounter; + Texture m_globalCounterBuffer; + + ResourceViewHeaps *m_pResourceViewHeaps = nullptr; + DynamicBufferRing *m_pConstantBufferRing = nullptr; + ID3D12RootSignature *m_pRootSignature = nullptr; + ID3D12PipelineState *m_pPipeline = nullptr; + + SPDLoad m_spdLoad; + SPDWaveOps m_spdWaveOps; + SPDPacked m_spdPacked; + }; +} \ No newline at end of file diff --git a/sample/src/DX12/SPDIntegration.hlsl b/sample/src/DX12/SPDIntegration.hlsl new file mode 100644 index 0000000..6dfaae9 --- /dev/null +++ b/sample/src/DX12/SPDIntegration.hlsl @@ -0,0 +1,189 @@ +// SPDSample +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// when using amd shader intrinscs +// #include "ags_shader_intrinsics_dx12.h" + +//-------------------------------------------------------------------------------------- +// Constant Buffer +//-------------------------------------------------------------------------------------- +cbuffer spdConstants : register(b0) +{ + uint mips; + uint numWorkGroups; + uint2 workGroupOffset; +} + +//-------------------------------------------------------------------------------------- +// Texture definitions +//-------------------------------------------------------------------------------------- +RWTexture2DArray imgDst[13] : register(u3); // don't access MIP [6] +globallycoherent RWTexture2DArray imgDst6 : register(u2); + +//-------------------------------------------------------------------------------------- +// Buffer definitions - global atomic counter +//-------------------------------------------------------------------------------------- +struct SpdGlobalAtomicBuffer +{ + uint counter[6]; +}; +globallycoherent RWStructuredBuffer spdGlobalAtomic :register(u1); + +#define A_GPU +#define A_HLSL + +#include "ffx_a.h" + +groupshared AU1 spdCounter; + +#ifndef SPD_PACKED_ONLY +groupshared AF1 spdIntermediateR[16][16]; +groupshared AF1 spdIntermediateG[16][16]; +groupshared AF1 spdIntermediateB[16][16]; +groupshared AF1 spdIntermediateA[16][16]; + +AF4 SpdLoadSourceImage(AF2 tex, AU1 slice) +{ + return imgDst[0][float3(tex, slice)]; +} +AF4 SpdLoad(ASU2 tex, AU1 slice) +{ + return imgDst6[uint3(tex, slice)]; +} +void SpdStore(ASU2 pix, AF4 outValue, AU1 index, AU1 slice) +{ + if (index == 5) + { + imgDst6[uint3(pix, slice)] = outValue; + return; + } + imgDst[index + 1][uint3(pix, slice)] = outValue; +} +void SpdIncreaseAtomicCounter(AU1 slice) +{ + InterlockedAdd(spdGlobalAtomic[0].counter[slice], 1, spdCounter); +} +AU1 SpdGetAtomicCounter() +{ + return spdCounter; +} +void SpdResetAtomicCounter(AU1 slice) +{ + spdGlobalAtomic[0].counter[slice] = 0; +} +AF4 SpdLoadIntermediate(AU1 x, AU1 y) +{ + return AF4( + spdIntermediateR[x][y], + spdIntermediateG[x][y], + spdIntermediateB[x][y], + spdIntermediateA[x][y]); +} +void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value) +{ + spdIntermediateR[x][y] = value.x; + spdIntermediateG[x][y] = value.y; + spdIntermediateB[x][y] = value.z; + spdIntermediateA[x][y] = value.w; +} +AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3) +{ + return (v0+v1+v2+v3)*0.25; +} +#endif + +// define fetch and store functions Packed +#ifdef A_HALF +groupshared AH2 spdIntermediateRG[16][16]; +groupshared AH2 spdIntermediateBA[16][16]; + +AH4 SpdLoadSourceImageH(AF2 tex, AU1 slice) +{ + return AH4(imgDst[0][float3(tex, slice)]); +} +AH4 SpdLoadH(ASU2 p, AU1 slice) +{ + return AH4(imgDst6[uint3(p, slice)]); +} +void SpdStoreH(ASU2 p, AH4 value, AU1 mip, AU1 slice) +{ + if (mip == 5) + { + imgDst6[uint3(p, slice)] = AF4(value); + return; + } + imgDst[mip + 1][uint3(p, slice)] = AF4(value); +} +void SpdIncreaseAtomicCounter(AU1 slice) +{ + InterlockedAdd(spdGlobalAtomic[0].counter[slice], 1, spdCounter); +} +AU1 SpdGetAtomicCounter() +{ + return spdCounter; +} +void SpdResetAtomicCounter(AU1 slice) +{ + spdGlobalAtomic[0].counter[slice] = 0; +} +AH4 SpdLoadIntermediateH(AU1 x, AU1 y) +{ + return AH4( + spdIntermediateRG[x][y].x, + spdIntermediateRG[x][y].y, + spdIntermediateBA[x][y].x, + spdIntermediateBA[x][y].y); +} +void SpdStoreIntermediateH(AU1 x, AU1 y, AH4 value) +{ + spdIntermediateRG[x][y] = value.xy; + spdIntermediateBA[x][y] = value.zw; +} +AH4 SpdReduce4H(AH4 v0, AH4 v1, AH4 v2, AH4 v3) +{ + return (v0+v1+v2+v3)*AH1(0.25); +} +#endif + +#include "ffx_spd.h" + +// Main function +//-------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------- +[numthreads(256, 1, 1)] +void main(uint3 WorkGroupId : SV_GroupID, uint LocalThreadIndex : SV_GroupIndex) +{ +#ifndef A_HALF + SpdDownsample( + AU2(WorkGroupId.xy), + AU1(LocalThreadIndex), + AU1(mips), + AU1(numWorkGroups), + AU1(WorkGroupId.z), + AU2(workGroupOffset)); +#else + SpdDownsampleH( + AU2(WorkGroupId.xy), + AU1(LocalThreadIndex), + AU1(mips), + AU1(numWorkGroups), + AU1(WorkGroupId.z), + AU2(workGroupOffset)); +#endif + } \ No newline at end of file diff --git a/sample/src/DX12/SPDIntegrationLinearSampler.hlsl b/sample/src/DX12/SPDIntegrationLinearSampler.hlsl new file mode 100644 index 0000000..9492e35 --- /dev/null +++ b/sample/src/DX12/SPDIntegrationLinearSampler.hlsl @@ -0,0 +1,200 @@ +// SPDSample +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// when using amd shader intrinscs +// #include "ags_shader_intrinsics_dx12.h" + +//-------------------------------------------------------------------------------------- +// Constant Buffer +//-------------------------------------------------------------------------------------- +cbuffer spdConstants : register(b0) +{ + uint mips; + uint numWorkGroups; + uint2 workGroupOffset; + float2 invInputSize; +} + +//-------------------------------------------------------------------------------------- +// Texture definitions +//-------------------------------------------------------------------------------------- +globallycoherent RWTexture2DArray imgDst5 : register(u2); +RWTexture2DArray imgDst[12] : register(u3); // do no access MIP [5] +Texture2DArray imgSrc : register(t0); +SamplerState srcSampler : register(s0); + +//-------------------------------------------------------------------------------------- +// Buffer definitions - global atomic counter +//-------------------------------------------------------------------------------------- +struct SpdGlobalAtomicBuffer +{ + uint counter[6]; +}; +globallycoherent RWStructuredBuffer spdGlobalAtomic :register(u1); + +#define A_GPU +#define A_HLSL + +#include "ffx_a.h" + +groupshared AU1 spdCounter; + +#ifndef SPD_PACKED_ONLY +groupshared AF1 spdIntermediateR[16][16]; +groupshared AF1 spdIntermediateG[16][16]; +groupshared AF1 spdIntermediateB[16][16]; +groupshared AF1 spdIntermediateA[16][16]; + +AF4 SpdLoadSourceImage(ASU2 p, AU1 slice) +{ + AF2 textureCoord = p * invInputSize + invInputSize; + AF4 result = imgSrc.SampleLevel(srcSampler, float3(textureCoord, slice), 0); + result = AF4(AToSrgbF1(result.x), AToSrgbF1(result.y), AToSrgbF1(result.z), result.w); + return result; +} +AF4 SpdLoad(ASU2 tex, AU1 slice) +{ + return imgDst5[uint3(tex, slice)]; +} +void SpdStore(ASU2 pix, AF4 outValue, AU1 index, AU1 slice) +{ + if (index == 5) + { + imgDst5[uint3(pix, slice)] = outValue; + return; + } + imgDst[index][uint3(pix, slice)] = outValue; +} +void SpdIncreaseAtomicCounter(AU1 slice) +{ + InterlockedAdd(spdGlobalAtomic[0].counter[slice], 1, spdCounter); +} +AU1 SpdGetAtomicCounter() +{ + return spdCounter; +} +void SpdResetAtomicCounter(AU1 slice) +{ + spdGlobalAtomic[0].counter[slice] = 0; +} +AF4 SpdLoadIntermediate(AU1 x, AU1 y) +{ + return AF4( + spdIntermediateR[x][y], + spdIntermediateG[x][y], + spdIntermediateB[x][y], + spdIntermediateA[x][y]); +} +void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value) +{ + spdIntermediateR[x][y] = value.x; + spdIntermediateG[x][y] = value.y; + spdIntermediateB[x][y] = value.z; + spdIntermediateA[x][y] = value.w; +} +AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3) +{ + return (v0+v1+v2+v3)*0.25; +} +#endif + +// define fetch and store functions Packed +#ifdef A_HALF +groupshared AH2 spdIntermediateRG[16][16]; +groupshared AH2 spdIntermediateBA[16][16]; + +AH4 SpdLoadSourceImageH(ASU2 p, AU1 slice) +{ + AF2 textureCoord = p * invInputSize + invInputSize; + AF4 result = imgSrc.SampleLevel(srcSampler, float3(textureCoord, slice), 0); + result = AF4(AToSrgbF1(result.x), AToSrgbF1(result.y), AToSrgbF1(result.z), result.w); + return AH4(result); +} +AH4 SpdLoadH(ASU2 p, AU1 slice) +{ + return AH4(imgDst5[uint3(p, slice)]); +} +void SpdStoreH(ASU2 p, AH4 value, AU1 mip, AU1 slice) +{ + if (mip == 5) + { + imgDst5[uint3(p, slice)] = AF4(value); + return; + } + imgDst[mip][uint3(p, slice)] = AF4(value); +} +void SpdIncreaseAtomicCounter(AU1 slice) +{ + InterlockedAdd(spdGlobalAtomic[0].counter[slice], 1, spdCounter); +} +AU1 SpdGetAtomicCounter() +{ + return spdCounter; +} +void SpdResetAtomicCounter(AU1 slice) +{ + spdGlobalAtomic[0].counter[slice] = 0; +} +AH4 SpdLoadIntermediateH(AU1 x, AU1 y) +{ + return AH4( + spdIntermediateRG[x][y].x, + spdIntermediateRG[x][y].y, + spdIntermediateBA[x][y].x, + spdIntermediateBA[x][y].y); +} +void SpdStoreIntermediateH(AU1 x, AU1 y, AH4 value) +{ + spdIntermediateRG[x][y] = value.xy; + spdIntermediateBA[x][y] = value.zw; +} +AH4 SpdReduce4H(AH4 v0, AH4 v1, AH4 v2, AH4 v3) +{ + return (v0+v1+v2+v3)*AH1(0.25); +} +#endif + +#define SPD_LINEAR_SAMPLER + +#include "ffx_spd.h" + +// Main function +//-------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------- +[numthreads(256, 1, 1)] +void main(uint3 WorkGroupId : SV_GroupID, uint LocalThreadIndex : SV_GroupIndex) +{ +#ifndef A_HALF + SpdDownsample( + AU2(WorkGroupId.xy), + AU1(LocalThreadIndex), + AU1(mips), + AU1(numWorkGroups), + AU1(WorkGroupId.z), + AU2(workGroupOffset)); +#else + SpdDownsampleH( + AU2(WorkGroupId.xy), + AU1(LocalThreadIndex), + AU1(mips), + AU1(numWorkGroups), + AU1(WorkGroupId.z), + AU2(workGroupOffset)); +#endif + } \ No newline at end of file diff --git a/sample/src/DX12/SPD_Renderer.cpp b/sample/src/DX12/SPDRenderer.cpp similarity index 78% rename from sample/src/DX12/SPD_Renderer.cpp rename to sample/src/DX12/SPDRenderer.cpp index 2f29311..5c4e0e9 100644 --- a/sample/src/DX12/SPD_Renderer.cpp +++ b/sample/src/DX12/SPDRenderer.cpp @@ -19,19 +19,16 @@ #include "stdafx.h" -#include "SPD_Renderer.h" +#include "SPDRenderer.h" //-------------------------------------------------------------------------------------- // // OnCreate // //-------------------------------------------------------------------------------------- -void SPD_Renderer::OnCreate(Device* pDevice, SwapChain *pSwapChain) +void SPDRenderer::OnCreate(Device *pDevice, SwapChain *pSwapChain) { m_pDevice = pDevice; - - m_format = DXGI_FORMAT_R16G16B16A16_FLOAT; - // Initialize helpers // Create all the heaps for the resources views @@ -45,15 +42,15 @@ void SPD_Renderer::OnCreate(Device* pDevice, SwapChain *pSwapChain) // Create a commandlist ring for the Direct queue uint32_t commandListsPerBackBuffer = 8; - m_CommandListRing.OnCreate(pDevice, backBufferCount, commandListsPerBackBuffer, pDevice->GetGraphicsQueue()->GetDesc()); + m_commandListRing.OnCreate(pDevice, backBufferCount, commandListsPerBackBuffer, pDevice->GetGraphicsQueue()->GetDesc()); // Create a 'dynamic' constant buffer const uint32_t constantBuffersMemSize = 20 * 1024 * 1024; - m_ConstantBufferRing.OnCreate(pDevice, backBufferCount, constantBuffersMemSize, &m_resourceViewHeaps); + 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"); + m_vidMemBufferPool.OnCreate(pDevice, staticGeometryMemSize, USE_VID_MEM, "StaticGeom"); // initialize the GPU time stamps module m_GPUTimer.OnCreate(pDevice, backBufferCount); @@ -61,7 +58,7 @@ void SPD_Renderer::OnCreate(Device* pDevice, SwapChain *pSwapChain) // 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) + m_uploadHeap.OnCreate(pDevice, uploadHeapMemSize); // initialize an upload heap (uses suballocation for faster results) // Create the depth buffer view m_resourceViewHeaps.AllocDSVDescriptor(1, &m_depthBufferDSV); @@ -73,20 +70,20 @@ void SPD_Renderer::OnCreate(Device* pDevice, SwapChain *pSwapChain) 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, m_format, 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_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_PSDownsampler.OnCreate(pDevice, &m_resourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool, m_format); - m_CSDownsampler.OnCreate(pDevice, &m_resourceViewHeaps, &m_ConstantBufferRing, m_format); - m_SPD_Versions.OnCreate(pDevice, &m_resourceViewHeaps, &m_ConstantBufferRing, m_format); + m_PSDownsampler.OnCreate(pDevice, &m_uploadHeap, &m_resourceViewHeaps, &m_constantBufferRing, &m_vidMemBufferPool); + m_CSDownsampler.OnCreate(pDevice, &m_uploadHeap, &m_resourceViewHeaps, &m_constantBufferRing); + m_SPDVersions.OnCreate(pDevice, &m_uploadHeap, &m_resourceViewHeaps, &m_constantBufferRing); // Create tonemapping pass - m_toneMapping.OnCreate(pDevice, &m_resourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool, pSwapChain->GetFormat()); + 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()); + m_imGUI.OnCreate(pDevice, &m_uploadHeap, &m_resourceViewHeaps, &m_constantBufferRing, pSwapChain->GetFormat()); m_resourceViewHeaps.AllocRTVDescriptor(1, &m_HDRRTV); m_resourceViewHeaps.AllocRTVDescriptor(1, &m_HDRRTVMSAA); @@ -95,8 +92,8 @@ void SPD_Renderer::OnCreate(Device* pDevice, SwapChain *pSwapChain) // 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 } @@ -105,14 +102,14 @@ void SPD_Renderer::OnCreate(Device* pDevice, SwapChain *pSwapChain) // OnDestroy // //-------------------------------------------------------------------------------------- -void SPD_Renderer::OnDestroy() +void SPDRenderer::OnDestroy() { m_toneMapping.OnDestroy(); - m_ImGUI.OnDestroy(); + m_imGUI.OnDestroy(); m_PSDownsampler.OnDestroy(); m_CSDownsampler.OnDestroy(); - m_SPD_Versions.OnDestroy(); + m_SPDVersions.OnDestroy(); m_wireframeBox.OnDestroy(); m_wireframe.OnDestroy(); @@ -120,12 +117,12 @@ void SPD_Renderer::OnDestroy() m_skyDome.OnDestroy(); m_shadowMap.OnDestroy(); - m_UploadHeap.OnDestroy(); + m_uploadHeap.OnDestroy(); m_GPUTimer.OnDestroy(); - m_VidMemBufferPool.OnDestroy(); - m_ConstantBufferRing.OnDestroy(); + m_vidMemBufferPool.OnDestroy(); + m_constantBufferRing.OnDestroy(); m_resourceViewHeaps.OnDestroy(); - m_CommandListRing.OnDestroy(); + m_commandListRing.OnDestroy(); } //-------------------------------------------------------------------------------------- @@ -133,10 +130,10 @@ void SPD_Renderer::OnDestroy() // OnCreateWindowSizeDependentResources // //-------------------------------------------------------------------------------------- -void SPD_Renderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, uint32_t Width, uint32_t Height) +void SPDRenderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, uint32_t Width, uint32_t Height) { - m_Width = Width; - m_Height = Height; + m_width = Width; + m_height = Height; // Set the viewport // @@ -144,7 +141,7 @@ void SPD_Renderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, u // Create scissor rectangle // - m_RectScissor = { 0, 0, (LONG)Width, (LONG)Height }; + m_rectScissor = { 0, 0, (LONG)Width, (LONG)Height }; // Create depth buffer // @@ -163,17 +160,6 @@ void SPD_Renderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, u m_HDR.InitRenderTarget(m_pDevice, "HDR", &RDesc, D3D12_RESOURCE_STATE_RENDER_TARGET); m_HDR.CreateSRV(0, &m_HDRSRV); m_HDR.CreateRTV(0, &m_HDRRTV); - - // update downscaling effect - // - { - int resolution = max(m_Width, m_Height); - int mipLevel = (static_cast(min(1.0f + floor(log2(resolution)), 12)) - 1); - - m_PSDownsampler.OnCreateWindowSizeDependentResources(m_Width, m_Height, &m_HDR, mipLevel); - m_CSDownsampler.OnCreateWindowSizeDependentResources(m_Width, m_Height, &m_HDR, mipLevel); - m_SPD_Versions.OnCreateWindowSizeDependentResources(m_Width, m_Height, &m_HDR); - } } //-------------------------------------------------------------------------------------- @@ -181,12 +167,8 @@ void SPD_Renderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, u // OnDestroyWindowSizeDependentResources // //-------------------------------------------------------------------------------------- -void SPD_Renderer::OnDestroyWindowSizeDependentResources() +void SPDRenderer::OnDestroyWindowSizeDependentResources() { - m_PSDownsampler.OnDestroyWindowSizeDependentResources(); - m_CSDownsampler.OnDestroyWindowSizeDependentResources(); - m_SPD_Versions.OnDestroyWindowSizeDependentResources(); - m_HDR.OnDestroy(); m_HDRMSAA.OnDestroy(); m_depthBuffer.OnDestroy(); @@ -198,7 +180,7 @@ void SPD_Renderer::OnDestroyWindowSizeDependentResources() // LoadScene // //-------------------------------------------------------------------------------------- -int SPD_Renderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) +int SPDRenderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) { // show loading progress // @@ -220,7 +202,7 @@ int SPD_Renderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) Profile p("m_pGltfLoader->Load"); m_pGLTFTexturesAndBuffers = new GLTFTexturesAndBuffers(); - m_pGLTFTexturesAndBuffers->OnCreate(m_pDevice, pGLTFCommon, &m_UploadHeap, &m_VidMemBufferPool, &m_ConstantBufferRing); + m_pGLTFTexturesAndBuffers->OnCreate(m_pDevice, pGLTFCommon, &m_uploadHeap, &m_vidMemBufferPool, &m_constantBufferRing); } else if (stage == 6) { @@ -235,13 +217,13 @@ int SPD_Renderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) 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_pGltfDepth = new GltfDepthPass(); + m_pGltfDepth->OnCreate( m_pDevice, - &m_UploadHeap, + &m_uploadHeap, &m_resourceViewHeaps, - &m_ConstantBufferRing, - &m_VidMemBufferPool, + &m_constantBufferRing, + &m_vidMemBufferPool, m_pGLTFTexturesAndBuffers ); } @@ -250,19 +232,21 @@ int SPD_Renderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) Profile p("m_gltfPBR->OnCreate"); // same thing as above but for the PBR pass - m_gltfPBR = new GltfPbrPass(); - m_gltfPBR->OnCreate( + m_pGltfPBR = new GltfPbrPass(); + m_pGltfPBR->OnCreate( m_pDevice, - &m_UploadHeap, + &m_uploadHeap, &m_resourceViewHeaps, - &m_ConstantBufferRing, - &m_VidMemBufferPool, + &m_constantBufferRing, + &m_vidMemBufferPool, m_pGLTFTexturesAndBuffers, &m_skyDome, false, + false, m_HDRMSAA.GetFormat(), DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_UNKNOWN, + DXGI_FORMAT_UNKNOWN, 4 ); } @@ -271,30 +255,30 @@ int SPD_Renderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) 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_pGltfBBox = new GltfBBoxPass(); + m_pGltfBBox->OnCreate( m_pDevice, - &m_UploadHeap, + &m_uploadHeap, &m_resourceViewHeaps, - &m_ConstantBufferRing, - &m_VidMemBufferPool, + &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()); + m_vidMemBufferPool.UploadData(m_uploadHeap.GetCommandList()); #endif } else if (stage == 10) { 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(); + m_vidMemBufferPool.FreeUploadHeap(); #endif // tell caller that we are done loading the map return -1; @@ -309,27 +293,27 @@ int SPD_Renderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) // UnloadScene // //-------------------------------------------------------------------------------------- -void SPD_Renderer::UnloadScene() +void SPDRenderer::UnloadScene() { - if (m_gltfPBR) + if (m_pGltfPBR) { - m_gltfPBR->OnDestroy(); - delete m_gltfPBR; - m_gltfPBR = NULL; + m_pGltfPBR->OnDestroy(); + delete m_pGltfPBR; + m_pGltfPBR = NULL; } - if (m_gltfDepth) + if (m_pGltfDepth) { - m_gltfDepth->OnDestroy(); - delete m_gltfDepth; - m_gltfDepth = NULL; + m_pGltfDepth->OnDestroy(); + delete m_pGltfDepth; + m_pGltfDepth = NULL; } - if (m_gltfBBox) + if (m_pGltfBBox) { - m_gltfBBox->OnDestroy(); - delete m_gltfBBox; - m_gltfBBox = NULL; + m_pGltfBBox->OnDestroy(); + delete m_pGltfBBox; + m_pGltfBBox = NULL; } if (m_pGLTFTexturesAndBuffers) @@ -346,7 +330,7 @@ void SPD_Renderer::UnloadScene() // OnRender // //-------------------------------------------------------------------------------------- -void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) +void SPDRenderer::OnRender(State *pState, SwapChain *pSwapChain) { // Timing values // @@ -355,8 +339,8 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) // Let our resource managers do some house keeping // - m_ConstantBufferRing.OnBeginFrame(); - m_GPUTimer.OnBeginFrame(gpuTicksPerSecond, &m_TimeStamps); + 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 -------------- // @@ -395,7 +379,7 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) // command buffer calls // - ID3D12GraphicsCommandList2* pCmdLst1 = m_CommandListRing.GetNewCommandList(); + ID3D12GraphicsCommandList2* pCmdLst1 = m_commandListRing.GetNewCommandList(); m_GPUTimer.GetTimeStamp(pCmdLst1, "Begin Frame"); @@ -417,7 +401,7 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) // Render to shadow map atlas for spot lights ------------------------------------------ // - if (m_gltfDepth && pPerFrame != NULL) + if (m_pGltfDepth && pPerFrame != NULL) { uint32_t shadowMapIndex = 0; for (uint32_t i = 0; i < pPerFrame->lightCount; i++) @@ -433,10 +417,10 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) 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(); + GltfDepthPass::per_frame *cbDepthPerFrame = m_pGltfDepth->SetPerFrameConstants(); cbDepthPerFrame->mViewProj = pPerFrame->lights[i].mLightViewProj; - m_gltfDepth->Draw(pCmdLst1); + m_pGltfDepth->Draw(pCmdLst1); m_GPUTimer.GetTimeStamp(pCmdLst1, "Shadow map"); shadowMapIndex++; @@ -448,7 +432,7 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) pCmdLst1->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_shadowMap.GetResource(), D3D12_RESOURCE_STATE_DEPTH_WRITE, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE)); pCmdLst1->RSSetViewports(1, &m_viewPort); - pCmdLst1->RSSetScissorRects(1, &m_RectScissor); + pCmdLst1->RSSetScissorRects(1, &m_rectScissor); pCmdLst1->OMSetRenderTargets(1, &m_HDRRTVMSAA.GetCPU(), true, &m_depthBufferDSV.GetCPU()); if (pPerFrame != NULL) @@ -479,19 +463,19 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) // Render scene to color buffer // - if (m_gltfPBR && pPerFrame != NULL) + if (m_pGltfPBR && pPerFrame != NULL) { //set per frame constant buffer values - m_gltfPBR->Draw(pCmdLst1, &m_ShadowMapSRV); + m_pGltfPBR->Draw(pCmdLst1, &m_ShadowMapSRV); } // draw object's bounding boxes // - if (m_gltfBBox && pPerFrame != NULL) + if (m_pGltfBBox && pPerFrame != NULL) { if (pState->bDrawBoundingBoxes) { - m_gltfBBox->Draw(pCmdLst1, pPerFrame->mCameraViewProj); + m_pGltfBBox->Draw(pCmdLst1, pPerFrame->mCameraViewProj); m_GPUTimer.GetTimeStamp(pCmdLst1, "Bounding Box"); } @@ -549,19 +533,15 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) { case Downsampler::PS: m_PSDownsampler.Draw(pCmdLst1); - m_PSDownsampler.Gui(); + m_PSDownsampler.GUI(&pState->downsamplerImGUISlice); break; - case Downsampler::Multipass_CS: + case Downsampler::MultipassCS: m_CSDownsampler.Draw(pCmdLst1); - m_CSDownsampler.Gui(); - break; - case Downsampler::SPD_CS: - m_SPD_Versions.Dispatch(pCmdLst1, pState->spdVersion, pState->spdPacked); - m_SPD_Versions.Gui(pState->spdVersion, pState->spdPacked); + m_CSDownsampler.GUI(&pState->downsamplerImGUISlice); break; - case Downsampler::SPD_CS_Linear_Sampler: - m_SPD_Versions.DispatchLinearSamplerVersion(pCmdLst1, pState->spdVersion, pState->spdPacked); - m_SPD_Versions.GuiLinearSamplerVersion(pState->spdVersion, pState->spdPacked); + case Downsampler::SPDCS: + m_SPDVersions.Dispatch(pCmdLst1, pState->spdLoad, pState->spdWaveOps, pState->spdPacked); + m_SPDVersions.GUI(pState->spdLoad, pState->spdWaveOps, pState->spdPacked, &pState->downsamplerImGUISlice); break; } @@ -576,15 +556,15 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) // pSwapChain->WaitForSwapChain(); m_pDevice->GPUFlush(); - m_CommandListRing.OnBeginFrame(); + m_commandListRing.OnBeginFrame(); - ID3D12GraphicsCommandList* pCmdLst2 = m_CommandListRing.GetNewCommandList(); + ID3D12GraphicsCommandList* pCmdLst2 = m_commandListRing.GetNewCommandList(); // Tonemapping ------------------------------------------------------------------------ // { pCmdLst2->RSSetViewports(1, &m_viewPort); - pCmdLst2->RSSetScissorRects(1, &m_RectScissor); + pCmdLst2->RSSetScissorRects(1, &m_rectScissor); pCmdLst2->OMSetRenderTargets(1, pSwapChain->GetCurrentBackBufferRTV(), true, NULL); m_toneMapping.Draw(pCmdLst2, &m_HDRSRV, pState->exposure, pState->toneMapper); @@ -595,10 +575,10 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) // { pCmdLst2->RSSetViewports(1, &m_viewPort); - pCmdLst2->RSSetScissorRects(1, &m_RectScissor); + 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"); } diff --git a/sample/src/DX12/SPD_Renderer.h b/sample/src/DX12/SPDRenderer.h similarity index 64% rename from sample/src/DX12/SPD_Renderer.h rename to sample/src/DX12/SPDRenderer.h index a5251aa..08bdb1a 100644 --- a/sample/src/DX12/SPD_Renderer.h +++ b/sample/src/DX12/SPDRenderer.h @@ -19,7 +19,7 @@ #pragma once #include "CSDownsampler.h" -#include "SPD_Versions.h" +#include "SPDVersions.h" #include "PSDownsampler.h" static const int backBufferCount = 3; @@ -35,12 +35,12 @@ using namespace CAULDRON_DX12; enum class Downsampler { PS, - Multipass_CS, - SPD_CS, - SPD_CS_Linear_Sampler, + MultipassCS, + SPDCS, + SPDCSLinearSampler, }; -class SPD_Renderer +class SPDRenderer { public: struct Spotlight @@ -52,27 +52,34 @@ class SPD_Renderer struct State { - float time; - Camera camera; + 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 bDrawBoundingBoxes; - uint32_t spotlightCount; - Spotlight spotlight[4]; - bool bDrawLightFrustum; + bool useTAA; - Downsampler downsampler; - SPD_Version spdVersion; - SPD_Packed spdPacked; + bool isBenchmarking; + + uint32_t spotlightCount; + Spotlight spotlight[4]; + bool bDrawLightFrustum; + + Downsampler downsampler; + SPDLoad spdLoad; + SPDWaveOps spdWaveOps; + SPDPacked spdPacked; + + int downsamplerImGUISlice; }; - void OnCreate(Device* pDevice, SwapChain *pSwapChain); + void OnCreate(Device *pDevice, SwapChain *pSwapChain); void OnDestroy(); void OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, uint32_t Width, uint32_t Height); @@ -81,32 +88,32 @@ class SPD_Renderer int LoadScene(GLTFCommon *pGLTFCommon, int stage = 0); void UnloadScene(); - const std::vector &GetTimingValues() { return m_TimeStamps; } + const std::vector &GetTimingValues() { return m_timeStamps; } void OnRender(State *pState, SwapChain *pSwapChain); private: - Device *m_pDevice; + Device *m_pDevice = nullptr; - uint32_t m_Width; - uint32_t m_Height; + uint32_t m_width; + uint32_t m_height; D3D12_VIEWPORT m_viewPort; - D3D12_RECT m_RectScissor; + D3D12_RECT m_rectScissor; // Initialize helper classes ResourceViewHeaps m_resourceViewHeaps; - UploadHeap m_UploadHeap; - DynamicBufferRing m_ConstantBufferRing; - StaticBufferPool m_VidMemBufferPool; - CommandListRing m_CommandListRing; + UploadHeap m_uploadHeap; + DynamicBufferRing m_constantBufferRing; + StaticBufferPool m_vidMemBufferPool; + CommandListRing m_commandListRing; GPUTimestamps m_GPUTimer; //gltf passes - GLTFTexturesAndBuffers *m_pGLTFTexturesAndBuffers; - GltfPbrPass *m_gltfPBR; - GltfDepthPass *m_gltfDepth; - GltfBBoxPass *m_gltfBBox; + GLTFTexturesAndBuffers *m_pGLTFTexturesAndBuffers = nullptr; + GltfPbrPass *m_pGltfPBR = nullptr; + GltfDepthPass *m_pGltfDepth = nullptr; + GltfBBoxPass *m_pGltfBBox = nullptr; // effects SkyDome m_skyDome; @@ -116,10 +123,10 @@ class SPD_Renderer // Downsampling PSDownsampler m_PSDownsampler; CSDownsampler m_CSDownsampler; - SPD_Versions m_SPD_Versions; + SPDVersions m_SPDVersions; // GUI - ImGUI m_ImGUI; + ImGUI m_imGUI; // Temporary render targets @@ -139,14 +146,13 @@ class SPD_Renderer // Resolved RT Texture m_HDR; CBV_SRV_UAV m_HDRSRV; + CBV_SRV_UAV m_HDRUAV; RTV m_HDRRTV; // widgets Wireframe m_wireframe; WireframeBox m_wireframeBox; - std::vector m_TimeStamps; - - DXGI_FORMAT m_format; + std::vector m_timeStamps; }; diff --git a/sample/src/DX12/SPDSample.cpp b/sample/src/DX12/SPDSample.cpp new file mode 100644 index 0000000..813682f --- /dev/null +++ b/sample/src/DX12/SPDSample.cpp @@ -0,0 +1,496 @@ +// SPDSample +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include "stdafx.h" + +#include "SPDSample.h" + +SPDSample::SPDSample(LPCSTR name) : FrameworkWindows(name) +{ + m_lastFrameTime = MillisecondsNow(); + m_time = 0; + m_bPlay = true; + + m_pGltfLoader = NULL; +} + +//-------------------------------------------------------------------------------------- +// +// OnParseCommandLine +// +//-------------------------------------------------------------------------------------- +void SPDSample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t *pWidth, uint32_t *pHeight, bool *pbFullScreen) +{ + // set some default values + *pWidth = 1920; + *pHeight = 1080; + *pbFullScreen = false; + m_state.isBenchmarking = true; + m_isCpuValidationLayerEnabled = false; + m_isGpuValidationLayerEnabled = false; + m_stablePowerState = false; + + //read globals + auto process = [&](json jData) + { + *pWidth = jData.value("width", *pWidth); + *pHeight = jData.value("height", *pHeight); + *pbFullScreen = jData.value("fullScreen", *pbFullScreen); + m_isCpuValidationLayerEnabled = jData.value("CpuValidationLayerEnabled", m_isCpuValidationLayerEnabled); + m_isGpuValidationLayerEnabled = jData.value("GpuValidationLayerEnabled", m_isGpuValidationLayerEnabled); + m_state.isBenchmarking = jData.value("benchmark", m_state.isBenchmarking); + m_state.downsampler = jData.value("downsampler", m_state.downsampler); + m_state.spdLoad = jData.value("spdLoad", m_state.spdLoad); + m_state.spdWaveOps = jData.value("spdWaveOps", m_state.spdWaveOps); + m_state.spdPacked = jData.value("spdPacked", m_state.spdPacked); + }; + + //read json globals from commandline + // + try + { + if (strlen(lpCmdLine) > 0) + { + auto j3 = json::parse(lpCmdLine); + process(j3); + } + } + catch (json::parse_error) + { + Trace("Error parsing commandline\n"); + exit(0); + } + + // read config file (and override values from commandline if so) + // + { + std::ifstream f("SpdSample.json"); + if (!f) + { + MessageBox(NULL, "Config file not found!\n", "Cauldron Panic!", MB_ICONERROR); + exit(0); + } + + try + { + f >> m_jsonConfigFile; + } + catch (json::parse_error) + { + MessageBox(NULL, "Error parsing GLTFSample.json!\n", "Cauldron Panic!", MB_ICONERROR); + exit(0); + } + } + + json globals = m_jsonConfigFile["globals"]; + process(globals); +} + + +//-------------------------------------------------------------------------------------- +// +// OnCreate +// +//-------------------------------------------------------------------------------------- +void SPDSample::OnCreate(HWND hWnd) +{ + // Create Device + // + m_device.OnCreate("FFX_SPD_Sample", "Cauldron", m_isCpuValidationLayerEnabled, m_isGpuValidationLayerEnabled, hWnd); + m_device.CreatePipelineCache(); + + //init the shader compiler + InitDirectXCompiler(); + CreateShaderCache(); + + // 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_pNode = new SPDRenderer(); + m_pNode->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 = 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.spotlightCount = 1; + + m_state.spotlight[0].intensity = 10.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.downsamplerImGUISlice = 0; +} + +//-------------------------------------------------------------------------------------- +// +// OnDestroy +// +//-------------------------------------------------------------------------------------- +void SPDSample::OnDestroy() +{ + ImGUI_Shutdown(); + + m_device.GPUFlush(); + + // Fullscreen state should always be false before exiting the app. + m_swapChain.SetFullScreen(false); + + m_pNode->UnloadScene(); + m_pNode->OnDestroyWindowSizeDependentResources(); + m_pNode->OnDestroy(); + + delete m_pNode; + + m_swapChain.OnDestroyWindowSizeDependentResources(); + m_swapChain.OnDestroy(); + + //shut down the shader compiler + DestroyShaderCache(&m_device); + + if (m_pGltfLoader) + { + delete m_pGltfLoader; + m_pGltfLoader = NULL; + } + + m_device.OnDestroy(); +} + +//-------------------------------------------------------------------------------------- +// +// OnEvent +// +//-------------------------------------------------------------------------------------- +bool SPDSample::OnEvent(MSG msg) +{ + if (ImGUI_WndProcHandler(msg.hwnd, msg.message, msg.wParam, msg.lParam)) + return true; + + return true; +} + +//-------------------------------------------------------------------------------------- +// +// SetFullScreen +// +//-------------------------------------------------------------------------------------- +void SPDSample::SetFullScreen(bool fullscreen) +{ + m_device.GPUFlush(); + + m_swapChain.SetFullScreen(fullscreen); +} + +//-------------------------------------------------------------------------------------- +// +// OnResize +// +//-------------------------------------------------------------------------------------- +void SPDSample::OnResize(uint32_t width, uint32_t height) +{ + if (m_Width != width || m_Height != height) + { + // Flush GPU + // + m_device.GPUFlush(); + + // If resizing but no minimizing + // + if (m_Width > 0 && m_Height > 0) + { + if (m_pNode != NULL) + { + m_pNode->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, false, DISPLAYMODE_SDR); + if (m_pNode != NULL) + { + m_pNode->OnCreateWindowSizeDependentResources(&m_swapChain, m_Width, m_Height); + } + } + } + m_state.camera.SetFov(XM_PI / 4, m_Width, m_Height, 0.1f, 1000.0f); +} + +void SPDSample::BuildUI() +{ + ImGuiStyle& style = ImGui::GetStyle(); + style.FrameBorderSize = 1.0f; + + bool opened = true; + ImGui::Begin("Stats", &opened); + + if (ImGui::CollapsingHeader("Info", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::Text("Resolution : %ix%i", m_Width, m_Height); + } + + if (ImGui::CollapsingHeader("Downsampler", ImGuiTreeNodeFlags_DefaultOpen)) + { + // Downsample settings + const char* downsampleItemNames[] = + { + "PS", + "Multipass CS", + "SPD CS", + }; + ImGui::Combo("Downsampler Options", (int*)&m_state.downsampler, downsampleItemNames, _countof(downsampleItemNames)); + + // SPD Version + // Use load or linear sample to fetch data from source texture + const char* spdLoadItemNames[] = + { + "Load", + "Linear Sampler", + }; + ImGui::Combo("SPD Load / Linear Sampler", (int*)&m_state.spdLoad, spdLoadItemNames, _countof(spdLoadItemNames)); + + // enable the usage of wave operations + const char* spdWaveOpsItemNames[] = + { + "No-WaveOps", + "WaveOps", + }; + ImGui::Combo("SPD Version", (int*)&m_state.spdWaveOps, spdWaveOpsItemNames, _countof(spdWaveOpsItemNames)); + + // Non-Packed or Packed Version + const char* spdPackedItemNames[] = + { + "Non-Packed", + "Packed", + }; + ImGui::Combo("SPD Non-Packed / Packed Version", (int*)&m_state.spdPacked, spdPackedItemNames, _countof(spdPackedItemNames)); + } + + if (ImGui::CollapsingHeader("Lighting", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::SliderFloat("exposure", &m_state.exposure, 0.0f, 2.0f); + ImGui::SliderFloat("emmisive", &m_state.emmisiveFactor, 1.0f, 1000.0f, NULL, 1.0f); + ImGui::SliderFloat("iblFactor", &m_state.iblFactor, 0.0f, 2.0f); + } + + const char* tonemappers[] = { "Timothy", "DX11DSK", "Reinhard", "Uncharted2Tonemap", "ACES", "No tonemapper" }; + ImGui::Combo("tone mapper", &m_state.toneMapper, tonemappers, _countof(tonemappers)); + + const char* skyDomeType[] = { "Procedural Sky", "cubemap", "Simple clear" }; + ImGui::Combo("SkyDome", &m_state.skyDomeType, skyDomeType, _countof(skyDomeType)); + + const char* cameraControl[] = { "WASD", "Orbit" }; + static int cameraControlSelected = 1; + ImGui::Combo("Camera", &cameraControlSelected, cameraControl, _countof(cameraControl)); + + if (ImGui::CollapsingHeader("Profiler", ImGuiTreeNodeFlags_DefaultOpen)) + { + std::vector timeStamps = m_pNode->GetTimingValues(); + if (timeStamps.size() > 0) + { + for (uint32_t i = 1; i < timeStamps.size(); i++) + { + ImGui::Text("%-22s: %7.1f", timeStamps[i].m_label.c_str(), timeStamps[i].m_microseconds); + } + + //scrolling data and average computing + static float values[128]; + values[127] = timeStamps.back().m_microseconds; + for (uint32_t i = 0; i < 128 - 1; i++) { values[i] = values[i + 1]; } + ImGui::PlotLines("", values, 128, 0, "GPU frame time (us)", 0.0f, 30000.0f, ImVec2(0, 80)); + + } + } + + ImGui::End(); + + // If the mouse was not used by the GUI then it's for the camera + // + ImGuiIO& io = ImGui::GetIO(); + if (io.WantCaptureMouse == false) + { + 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 (cameraControlSelected == 0) + { + // WASD + // + m_state.camera.UpdateCameraWASD(m_roll, m_pitch, io.KeysDown, io.DeltaTime); + } + else if (cameraControlSelected == 1) + { + // Orbiting + // + m_distance -= (float)io.MouseWheel / 3.0f; + m_distance = std::max(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); + } + } +} + +//-------------------------------------------------------------------------------------- +// +// OnRender, updates the state from the UI, animates, transforms and renders the scene +// +//-------------------------------------------------------------------------------------- +void SPDSample::OnRender() +{ + // Get timings + // + double timeNow = MillisecondsNow(); + float deltaTime = (float)(timeNow - m_lastFrameTime); + m_lastFrameTime = timeNow; + + // Build UI and set the scene state. Note that the rendering of the UI happens later. + // + ImGUI_UpdateIO(); + ImGui::NewFrame(); + + static int loadingStage = 0; + if (loadingStage >= 0) + { + // LoadScene needs to be called a number of times, the scene is not fully loaded until it returns -1 + // This is done so we can display a progress bar when the scene is loading + if (m_pGltfLoader == NULL) + { + m_pGltfLoader = new GLTFCommon(); + m_pGltfLoader->Load("..\\media\\DamagedHelmet\\glTF\\", "DamagedHelmet.gltf"); + loadingStage = 0; + + // set benchmarking state if enabled + // + json scene = m_jsonConfigFile["scenes"][0]; + + // set default camera + // + json camera = scene["camera"]; + XMVECTOR from = GetVector(GetElementJsonArray(camera, "defaultFrom", { 0.0, 0.0, 10.0 })); + XMVECTOR to = GetVector(GetElementJsonArray(camera, "defaultTo", { 0.0, 0.0, 0.0 })); + m_state.camera.LookAt(from, to); + m_roll = m_state.camera.GetYaw(); + m_pitch = m_state.camera.GetPitch(); + m_distance = m_state.camera.GetDistance(); + + // set benchmarking state if enabled + + if (m_state.isBenchmarking) + { + BenchmarkConfig(scene["BenchmarkSettings"], -1, m_pGltfLoader); + } + } + loadingStage = m_pNode->LoadScene(m_pGltfLoader, loadingStage); + if (loadingStage == 0) + { + m_time = 0; + m_loadingScene = false; + } + } + else if (m_pGltfLoader && m_state.isBenchmarking) + { + // benchmarking takes control of the time, and exits the app when the animation is done + std::vector timeStamps = m_pNode->GetTimingValues(); + + const std::string* pFilename; + m_time = BenchmarkLoop(timeStamps, &m_state.camera, &pFilename); + + BuildUI(); + } + else + { + BuildUI(); + } + + // Set animation time + // + if (m_bPlay) + { + m_time += (float)m_deltaTime / 1000.0f; + } + + // Animate and transform the scene + // + if (m_pGltfLoader) + { + m_pGltfLoader->SetAnimationTime(0, m_time); + m_pGltfLoader->TransformScene(0, XMMatrixIdentity()); + } + + m_state.time = m_time; + + // Do Render frame using AFR + // + m_pNode->OnRender(&m_state, &m_swapChain); + + m_swapChain.Present(); +} + + +//-------------------------------------------------------------------------------------- +// +// WinMain +// +//-------------------------------------------------------------------------------------- +int WINAPI WinMain(HINSTANCE hInstance, + HINSTANCE hPrevInstance, + LPSTR lpCmdLine, + int nCmdShow) +{ + LPCSTR Name = "FFX SPD SampleDX12 v2.0"; + + // create new DX sample + return RunFramework(hInstance, lpCmdLine, nCmdShow, new SPDSample(Name)); +} diff --git a/sample/src/VK/SPD_Sample.h b/sample/src/DX12/SPDSample.h similarity index 57% rename from sample/src/VK/SPD_Sample.h rename to sample/src/DX12/SPDSample.h index f23f7e3..92ed977 100644 --- a/sample/src/VK/SPD_Sample.h +++ b/sample/src/DX12/SPDSample.h @@ -18,7 +18,7 @@ // THE SOFTWARE. #pragma once -#include "SPD_Renderer.h" +#include "SPDRenderer.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. @@ -35,33 +35,45 @@ // - uses the SampleRenderer to update all the state to the GPU and do the rendering // -class SPD_Sample : public FrameworkWindows +class SPDSample : public FrameworkWindows { public: - SPD_Sample(LPCSTR name); + SPDSample(LPCSTR name); + void OnParseCommandLine(LPSTR lpCmdLine, uint32_t *pWidth, uint32_t *pHeight, bool *pbFullScreen); void OnCreate(HWND hWnd); void OnDestroy(); + void BuildUI(); void OnRender(); bool OnEvent(MSG msg); void OnResize(uint32_t Width, uint32_t Height); void SetFullScreen(bool fullscreen); private: - Device m_device; - SwapChain m_swapChain; + Device m_device; + SwapChain m_swapChain; + + GLTFCommon *m_pGltfLoader = nullptr; + bool m_loadingScene = false; + + SPDRenderer *m_pNode = nullptr; + SPDRenderer::State m_state; + + 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; - GLTFCommon *m_pGltfLoader; + // json config file + json m_jsonConfigFile; + std::vector m_sceneNames; + int m_activeScene; + int m_activeCamera; + bool m_stablePowerState; + bool m_isCpuValidationLayerEnabled; + bool m_isGpuValidationLayerEnabled; - SPD_Renderer *m_Node; - SPD_Renderer::State m_state; - - float m_distance; - float m_roll; - float m_pitch; - - float m_time; // WallClock in seconds. - double m_lastFrameTime; - float m_timeStep = 0; - - bool m_bPlay; -}; \ No newline at end of file + bool m_bPlay; +}; diff --git a/sample/src/DX12/SPDVersions.cpp b/sample/src/DX12/SPDVersions.cpp new file mode 100644 index 0000000..7c85709 --- /dev/null +++ b/sample/src/DX12/SPDVersions.cpp @@ -0,0 +1,194 @@ +// SPDSample +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include "stdafx.h" +#include "base/DynamicBufferRing.h" +#include "base/StaticBufferPool.h" +#include "base/UploadHeap.h" +#include "base/Texture.h" +#include "base/Helper.h" +#include "SPDVersions.h" + +namespace CAULDRON_DX12 +{ + void SPDVersions::OnCreate( + Device *pDevice, + UploadHeap *pUploadHeap, + ResourceViewHeaps *pResourceViewHeaps, + DynamicBufferRing *pConstantBufferRing + ) + { + m_pDevice = pDevice; + + m_spd_WaveOps_NonPacked.OnCreate(pDevice, pUploadHeap, pResourceViewHeaps, pConstantBufferRing, SPDLoad::SPDLoad, SPDWaveOps::SPDWaveOps, SPDPacked::SPDNonPacked); + m_spd_WaveOps_Packed.OnCreate(pDevice, pUploadHeap, pResourceViewHeaps, pConstantBufferRing, SPDLoad::SPDLoad, SPDWaveOps::SPDWaveOps, SPDPacked::SPDPacked); + m_spd_No_WaveOps_NonPacked.OnCreate(pDevice, pUploadHeap, pResourceViewHeaps, pConstantBufferRing, SPDLoad::SPDLoad, SPDWaveOps::SPDNoWaveOps, SPDPacked::SPDNonPacked); + m_spd_No_WaveOps_Packed.OnCreate(pDevice, pUploadHeap, pResourceViewHeaps, pConstantBufferRing, SPDLoad::SPDLoad, SPDWaveOps::SPDNoWaveOps, SPDPacked::SPDPacked); + + m_spd_WaveOps_NonPacked_Linear_Sampler.OnCreate(pDevice, pUploadHeap, pResourceViewHeaps, pConstantBufferRing, SPDLoad::SPDLinearSampler, SPDWaveOps::SPDWaveOps, SPDPacked::SPDNonPacked); + m_spd_WaveOps_Packed_Linear_Sampler.OnCreate(pDevice, pUploadHeap, pResourceViewHeaps, pConstantBufferRing, SPDLoad::SPDLinearSampler, SPDWaveOps::SPDWaveOps, SPDPacked::SPDPacked); + m_spd_No_WaveOps_NonPacked_Linear_Sampler.OnCreate(pDevice, pUploadHeap, pResourceViewHeaps, pConstantBufferRing, SPDLoad::SPDLinearSampler, SPDWaveOps::SPDNoWaveOps, SPDPacked::SPDNonPacked); + m_spd_No_WaveOps_Packed_Linear_Sampler.OnCreate(pDevice, pUploadHeap, pResourceViewHeaps, pConstantBufferRing, SPDLoad::SPDLinearSampler, SPDWaveOps::SPDNoWaveOps, SPDPacked::SPDPacked); + } + + uint32_t SPDVersions::GetMaxMIPLevelCount(uint32_t Width, uint32_t Height) + { + uint32_t resolution = max(Width, Height); + return (static_cast(min(floor(log2(resolution)), 12))); + } + + void SPDVersions::OnDestroy() + { + m_spd_WaveOps_NonPacked.OnDestroy(); + m_spd_WaveOps_Packed.OnDestroy(); + m_spd_No_WaveOps_NonPacked.OnDestroy(); + m_spd_No_WaveOps_Packed.OnDestroy(); + + m_spd_WaveOps_NonPacked_Linear_Sampler.OnDestroy(); + m_spd_WaveOps_Packed_Linear_Sampler.OnDestroy(); + m_spd_No_WaveOps_NonPacked_Linear_Sampler.OnDestroy(); + m_spd_No_WaveOps_Packed_Linear_Sampler.OnDestroy(); + } + + void SPDVersions::Dispatch(ID3D12GraphicsCommandList2 *pCommandList, SPDLoad spdLoad, SPDWaveOps spdWaveOps, SPDPacked spdPacked) + { + switch (spdLoad) + { + case SPDLoad::SPDLoad: + { + switch (spdWaveOps) + { + case SPDWaveOps::SPDWaveOps: + switch (spdPacked) + { + case SPDPacked::SPDNonPacked: + m_spd_WaveOps_NonPacked.Draw(pCommandList); + break; + case SPDPacked::SPDPacked: + m_spd_WaveOps_Packed.Draw(pCommandList); + break; + } + break; + case SPDWaveOps::SPDNoWaveOps: + switch (spdPacked) + { + case SPDPacked::SPDNonPacked: + m_spd_No_WaveOps_NonPacked.Draw(pCommandList); + break; + case SPDPacked::SPDPacked: + m_spd_No_WaveOps_Packed.Draw(pCommandList); + break; + } + } + break; + } + case SPDLoad::SPDLinearSampler: + { + switch (spdWaveOps) + { + case SPDWaveOps::SPDWaveOps: + switch (spdPacked) + { + case SPDPacked::SPDNonPacked: + m_spd_WaveOps_NonPacked_Linear_Sampler.Draw(pCommandList); + break; + case SPDPacked::SPDPacked: + m_spd_WaveOps_Packed_Linear_Sampler.Draw(pCommandList); + break; + } + break; + case SPDWaveOps::SPDNoWaveOps: + switch (spdPacked) + { + case SPDPacked::SPDNonPacked: + m_spd_No_WaveOps_NonPacked_Linear_Sampler.Draw(pCommandList); + break; + case SPDPacked::SPDPacked: + m_spd_No_WaveOps_Packed_Linear_Sampler.Draw(pCommandList); + break; + } + } + break; + } + } + } + + void SPDVersions::GUI(SPDLoad spdLoad, SPDWaveOps spdWaveOps, SPDPacked spdPacked, int *pSlice) + { + switch (spdLoad) + { + case SPDLoad::SPDLoad: + { + switch (spdWaveOps) + { + case SPDWaveOps::SPDWaveOps: + switch (spdPacked) + { + case SPDPacked::SPDNonPacked: + m_spd_WaveOps_NonPacked.GUI(pSlice); + break; + case SPDPacked::SPDPacked: + m_spd_WaveOps_Packed.GUI(pSlice); + break; + } + break; + case SPDWaveOps::SPDNoWaveOps: + switch (spdPacked) + { + case SPDPacked::SPDNonPacked: + m_spd_No_WaveOps_NonPacked.GUI(pSlice); + break; + case SPDPacked::SPDPacked: + m_spd_No_WaveOps_Packed.GUI(pSlice); + break; + } + } + break; + } + case SPDLoad::SPDLinearSampler: + { + switch (spdWaveOps) + { + case SPDWaveOps::SPDWaveOps: + switch (spdPacked) + { + case SPDPacked::SPDNonPacked: + m_spd_WaveOps_NonPacked_Linear_Sampler.GUI(pSlice); + break; + case SPDPacked::SPDPacked: + m_spd_WaveOps_Packed_Linear_Sampler.GUI(pSlice); + break; + } + break; + case SPDWaveOps::SPDNoWaveOps: + switch (spdPacked) + { + case SPDPacked::SPDNonPacked: + m_spd_No_WaveOps_NonPacked_Linear_Sampler.GUI(pSlice); + break; + case SPDPacked::SPDPacked: + m_spd_No_WaveOps_Packed_Linear_Sampler.GUI(pSlice); + break; + } + } + break; + } + } + } +} \ No newline at end of file diff --git a/sample/src/DX12/SPDVersions.h b/sample/src/DX12/SPDVersions.h new file mode 100644 index 0000000..52ba9aa --- /dev/null +++ b/sample/src/DX12/SPDVersions.h @@ -0,0 +1,56 @@ +// SPDSample +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#pragma once +#include "SPDCS.h" + +namespace CAULDRON_DX12 +{ + class SPDVersions + { + public: + void OnCreate( + Device *pDevice, + UploadHeap *pUploadHeap, + ResourceViewHeaps *pResourceViewHeaps, + DynamicBufferRing *pConstantBufferRing + ); + void OnDestroy(); + + void Dispatch(ID3D12GraphicsCommandList2 *pCommandList, SPDLoad spdLoad, SPDWaveOps spdWaveOps, SPDPacked spdPacked); + void GUI(SPDLoad spdLoad, SPDWaveOps spdWaveOps, SPDPacked spdPacked, int *pSlice); + + private: + Device *m_pDevice = nullptr; + + SPDCS m_spd_WaveOps_NonPacked; + SPDCS m_spd_No_WaveOps_NonPacked; + + SPDCS m_spd_WaveOps_Packed; + SPDCS m_spd_No_WaveOps_Packed; + + SPDCS m_spd_WaveOps_NonPacked_Linear_Sampler; + SPDCS m_spd_No_WaveOps_NonPacked_Linear_Sampler; + + SPDCS m_spd_WaveOps_Packed_Linear_Sampler; + SPDCS m_spd_No_WaveOps_Packed_Linear_Sampler; + + uint32_t GetMaxMIPLevelCount(uint32_t Width, uint32_t Height); + }; +} \ No newline at end of file diff --git a/sample/src/DX12/SPD_CS.cpp b/sample/src/DX12/SPD_CS.cpp deleted file mode 100644 index 1eb92c2..0000000 --- a/sample/src/DX12/SPD_CS.cpp +++ /dev/null @@ -1,268 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#include "stdafx.h" -#include "base\Device.h" -#include "base\DynamicBufferRing.h" -#include "base\StaticBufferPool.h" -#include "base\UploadHeap.h" -#include "base\Texture.h" -#include "base\Imgui.h" -#include "base\Helper.h" -#include "Base\ShaderCompilerHelper.h" - -#include "SPD_CS.h" - -namespace CAULDRON_DX12 -{ - void SPD_CS::OnCreate( - Device *pDevice, - ResourceViewHeaps *pResourceViewHeaps, - DynamicBufferRing *pConstantBufferRing, - DXGI_FORMAT outFormat, - bool fallback, - bool packed - ) - { - m_pDevice = pDevice; - m_pResourceViewHeaps = pResourceViewHeaps; - m_pConstantBufferRing = pConstantBufferRing; - m_outFormat = outFormat; - - D3D12_SHADER_BYTECODE shaderByteCode = {}; - DefineList defines; - - if (fallback) { - defines["SPD_NO_WAVE_OPERATIONS"] = std::to_string(1); - } - if (packed) { - defines["A_HALF"] = std::to_string(1); - defines["SPD_PACKED_ONLY"] = std::to_string(1); - } - - CompileShaderFromFile("SPD_Integration.hlsl", &defines, "main", "cs_6_0", 0, &shaderByteCode); - - // 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); - - // SRV table - DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0); - RTSlot[parameterCount++].InitAsDescriptorTable(1, &DescRange[1], D3D12_SHADER_VISIBILITY_ALL); - - // UAV table + global counter buffer - DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 1); - RTSlot[parameterCount++].InitAsDescriptorTable(1, &DescRange[2], D3D12_SHADER_VISIBILITY_ALL); - - // output mips - DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, SPD_MAX_MIP_LEVELS, 2); - RTSlot[parameterCount++].InitAsDescriptorTable(1, &DescRange[3], D3D12_SHADER_VISIBILITY_ALL); - - // when using AMD shader intrinsics - /*if (!fallback) - { - //*** add AMD Intrinsic Resource *** - DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, AGS_DX12_SHADER_INSTRINSICS_SPACE_ID); // u0 - RTSlot[parameterCount++].InitAsDescriptorTable(1, &DescRange[4], D3D12_SHADER_VISIBILITY_ALL); - }*/ - - // the root signature contains 4 slots to be used - CD3DX12_ROOT_SIGNATURE_DESC descRootSignature = CD3DX12_ROOT_SIGNATURE_DESC(); - descRootSignature.NumParameters = parameterCount; - descRootSignature.pParameters = RTSlot; - descRootSignature.NumStaticSamplers = 0; - descRootSignature.pStaticSamplers = NULL; - - // deny uneccessary access to certain pipeline stages - descRootSignature.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE; - - ID3DBlob *pOutBlob, *pErrorBlob = NULL; - ThrowIfFailed(D3D12SerializeRootSignature(&descRootSignature, D3D_ROOT_SIGNATURE_VERSION_1, &pOutBlob, &pErrorBlob)); - ThrowIfFailed( - pDevice->GetDevice()->CreateRootSignature(0, pOutBlob->GetBufferPointer(), pOutBlob->GetBufferSize(), IID_PPV_ARGS(&m_pRootSignature)) - ); - SetName(m_pRootSignature, std::string("PostProcCS::") + "SPD_CS"); - - pOutBlob->Release(); - if (pErrorBlob) - pErrorBlob->Release(); - } - - { - D3D12_COMPUTE_PIPELINE_STATE_DESC descPso = {}; - descPso.CS = shaderByteCode; - descPso.Flags = D3D12_PIPELINE_STATE_FLAG_NONE; - descPso.pRootSignature = m_pRootSignature; - descPso.NodeMask = 0; - - ThrowIfFailed(pDevice->GetDevice()->CreateComputePipelineState(&descPso, IID_PPV_ARGS(&m_pPipeline))); - } - - // Allocate descriptors for the mip chain - // - m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_constBuffer); - m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_sourceSRV); - m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(SPD_MAX_MIP_LEVELS, m_UAV); - for (int i = 0; i < SPD_MAX_MIP_LEVELS; i++) - { - m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_SRV[i]); - } - - m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_globalCounter); - } - - void SPD_CS::OnCreateWindowSizeDependentResources(uint32_t Width, uint32_t Height, Texture *pInput, int mipCount) - { - m_Width = Width; - m_Height = Height; - m_mipCount = mipCount; - m_pInput = pInput; - - m_result.InitRenderTarget( - m_pDevice, - "SPD_CS::m_result", - &CD3DX12_RESOURCE_DESC::Tex2D( - m_outFormat, - m_Width >> 1, - m_Height >> 1, - 1, - mipCount, - 1, - 0, - D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS), - D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); - - // Create views for the mip chain - // - - // source - // - pInput->CreateSRV(0, &m_sourceSRV, 0); - - // destination - // - for (int i = 0; i < m_mipCount; i++) - { - m_result.CreateUAV(i, m_UAV, i); - m_result.CreateSRV(0, &m_SRV[i], i); - } - - m_globalCounterBuffer.InitBuffer(m_pDevice, "SPD_CS::m_globalCounterBuffer", - &CD3DX12_RESOURCE_DESC::Buffer(sizeof(uint32_t), D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS), - sizeof(uint32_t), D3D12_RESOURCE_STATE_UNORDERED_ACCESS); - m_globalCounterBuffer.CreateBufferUAV(0, NULL, &m_globalCounter); - } - - void SPD_CS::OnDestroyWindowSizeDependentResources() - { - m_globalCounterBuffer.OnDestroy(); - m_result.OnDestroy(); - } - - void SPD_CS::OnDestroy() - { - if (m_pPipeline != NULL) - { - m_pPipeline->Release(); - m_pPipeline = NULL; - } - - if (m_pRootSignature != NULL) - { - m_pRootSignature->Release(); - m_pRootSignature = NULL; - } - } - - void SPD_CS::Draw(ID3D12GraphicsCommandList2* pCommandList) - { - UserMarker marker(pCommandList, "SPD_CS"); - - // downsample - uint32_t dispatchX = (((m_Width + 63) >> (6))); - uint32_t dispatchY = (((m_Height + 63) >> (6))); - uint32_t dispatchZ = 1; - - D3D12_GPU_VIRTUAL_ADDRESS cbHandle; - uint32_t* pConstMem; - m_pConstantBufferRing->AllocConstantBuffer(sizeof(cbDownscale), (void**)&pConstMem, &cbHandle); - cbDownscale constants; - constants.mips = m_mipCount; - constants.numWorkGroups = dispatchX * dispatchY * dispatchZ; - memcpy(pConstMem, &constants, sizeof(cbDownscale)); - - D3D12_RANGE range = { 0, sizeof(uint32_t) }; - - // Bind Descriptor heaps and the root signature - // - ID3D12DescriptorHeap *pDescriptorHeaps[] = { m_pResourceViewHeaps->GetCBV_SRV_UAVHeap(), m_pResourceViewHeaps->GetSamplerHeap() }; - pCommandList->SetDescriptorHeaps(2, pDescriptorHeaps); - pCommandList->SetComputeRootSignature(m_pRootSignature); - - // Bind Descriptor the descriptor sets - // - int params = 0; - pCommandList->SetComputeRootConstantBufferView(params++, cbHandle); - pCommandList->SetComputeRootDescriptorTable(params++, m_sourceSRV.GetGPU()); - pCommandList->SetComputeRootDescriptorTable(params++, m_globalCounter.GetGPU()); - pCommandList->SetComputeRootDescriptorTable(params++, m_UAV[0].GetGPU()); - - // Bind Pipeline - // - pCommandList->SetPipelineState(m_pPipeline); - - // set counter to 0 - pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_globalCounterBuffer.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_COPY_DEST, 0)); - - D3D12_WRITEBUFFERIMMEDIATE_PARAMETER pParams = { m_globalCounterBuffer.GetResource()->GetGPUVirtualAddress(), 0 }; - pCommandList->WriteBufferImmediate(1, &pParams, NULL); - - D3D12_RESOURCE_BARRIER resourceBarriers[2] = { - CD3DX12_RESOURCE_BARRIER::Transition(m_globalCounterBuffer.GetResource(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, 0), - CD3DX12_RESOURCE_BARRIER::Transition(m_result.GetResource(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS) - }; - pCommandList->ResourceBarrier(2, resourceBarriers); - - // Dispatch - // - pCommandList->Dispatch(dispatchX, dispatchY, dispatchZ); - pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_result.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE)); - } - - void SPD_CS::Gui() - { - bool opened = true; - ImGui::Begin("Downsample", &opened); - - ImGui::Image((ImTextureID)&m_sourceSRV, ImVec2(320, 180)); - for (int i = 0; i < m_mipCount; i++) - { - ImGui::Image((ImTextureID)&m_SRV[i], ImVec2(320, 180)); - } - - ImGui::End(); - } -} \ No newline at end of file diff --git a/sample/src/DX12/SPD_CS.h b/sample/src/DX12/SPD_CS.h deleted file mode 100644 index 674c5c4..0000000 --- a/sample/src/DX12/SPD_CS.h +++ /dev/null @@ -1,73 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -#pragma once - -#include "Base/DynamicBufferRing.h" -#include "Base/Texture.h" - -namespace CAULDRON_DX12 -{ -#define SPD_MAX_MIP_LEVELS 12 - - class SPD_CS - { - public: - void OnCreate(Device *pDevice, ResourceViewHeaps *pResourceViewHeaps, DynamicBufferRing *pConstantBufferRing, DXGI_FORMAT outFormat, bool fallback, bool packed); - void OnDestroy(); - - void OnCreateWindowSizeDependentResources(uint32_t Width, uint32_t Height, Texture *pInput, int mips); - void OnDestroyWindowSizeDependentResources(); - - void Draw(ID3D12GraphicsCommandList2* pCommandList); - Texture *GetTexture() { return &m_result; } - CBV_SRV_UAV GetTextureView(int i) { if (i == 0) { return m_sourceSRV; } else { return m_SRV[i]; } } - void Gui(); - - struct cbDownscale - { - int mips; - int numWorkGroups; - int padding[2]; - }; - - private: - Device* m_pDevice = nullptr; - DXGI_FORMAT m_outFormat; - - Texture *m_pInput; - Texture m_result; - - CBV_SRV_UAV m_constBuffer; // dimension - CBV_SRV_UAV m_UAV[SPD_MAX_MIP_LEVELS]; //dest - CBV_SRV_UAV m_SRV[SPD_MAX_MIP_LEVELS]; // for display of mips using imGUI - CBV_SRV_UAV m_sourceSRV; //src - - CBV_SRV_UAV m_globalCounter; - Texture m_globalCounterBuffer; - - ResourceViewHeaps *m_pResourceViewHeaps; - DynamicBufferRing *m_pConstantBufferRing; - ID3D12RootSignature *m_pRootSignature; - ID3D12PipelineState *m_pPipeline = NULL; - - uint32_t m_Width; - uint32_t m_Height; - int m_mipCount; - }; -} \ No newline at end of file diff --git a/sample/src/DX12/SPD_CS_Linear_Sampler.cpp b/sample/src/DX12/SPD_CS_Linear_Sampler.cpp deleted file mode 100644 index 6e99db3..0000000 --- a/sample/src/DX12/SPD_CS_Linear_Sampler.cpp +++ /dev/null @@ -1,285 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#include "stdafx.h" -#include "base\Device.h" -#include "base\DynamicBufferRing.h" -#include "base\StaticBufferPool.h" -#include "base\UploadHeap.h" -#include "base\Texture.h" -#include "base\Imgui.h" -#include "base\Helper.h" -#include "Base\ShaderCompilerHelper.h" - -#include "SPD_CS_Linear_Sampler.h" - -namespace CAULDRON_DX12 -{ - void SPD_CS_Linear_Sampler::OnCreate( - Device *pDevice, - ResourceViewHeaps *pResourceViewHeaps, - DynamicBufferRing *pConstantBufferRing, - DXGI_FORMAT outFormat, - bool fallback, - bool packed - ) - { - m_pDevice = pDevice; - m_pResourceViewHeaps = pResourceViewHeaps; - m_pConstantBufferRing = pConstantBufferRing; - m_outFormat = outFormat; - - D3D12_SHADER_BYTECODE shaderByteCode = {}; - DefineList defines; - - if (fallback) { - defines["SPD_NO_WAVE_OPERATIONS"] = std::to_string(1); - } - if (packed) { - defines["A_HALF"] = std::to_string(1); - defines["SPD_PACKED_ONLY"] = std::to_string(1); - } - - CompileShaderFromFile("SPD_Integration_Linear_Sampler.hlsl", &defines, "main", "cs_6_0", 0, &shaderByteCode); - - // 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); - - // SRV table - DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0); - RTSlot[parameterCount++].InitAsDescriptorTable(1, &DescRange[1], D3D12_SHADER_VISIBILITY_ALL); - - // UAV table + global counter buffer (== also an UAV)? - DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 1); - RTSlot[parameterCount++].InitAsDescriptorTable(1, &DescRange[2], D3D12_SHADER_VISIBILITY_ALL); - - // output mips - DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, SPD_MAX_MIP_LEVELS, 2); - RTSlot[parameterCount++].InitAsDescriptorTable(1, &DescRange[3], D3D12_SHADER_VISIBILITY_ALL); - - // when using AMD shader intrinsics - /*if (!fallback) - { - //*** add AMD Intrinsic Resource *** - DescRange[parameterCount].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, AGS_DX12_SHADER_INSTRINSICS_SPACE_ID); // u0 - RTSlot[parameterCount++].InitAsDescriptorTable(1, &DescRange[4], D3D12_SHADER_VISIBILITY_ALL); - }*/ - - D3D12_STATIC_SAMPLER_DESC SamplerDesc = {}; - SamplerDesc.Filter = D3D12_FILTER_MIN_MAG_LINEAR_MIP_POINT; - SamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - SamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - SamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - SamplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; - SamplerDesc.BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK; - SamplerDesc.MinLOD = 0.0f; - SamplerDesc.MaxLOD = D3D12_FLOAT32_MAX; - SamplerDesc.MipLODBias = 0; - SamplerDesc.MaxAnisotropy = 1; - SamplerDesc.ShaderRegister = 0; - SamplerDesc.RegisterSpace = 0; - SamplerDesc.ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; - - // the root signature contains 4 slots to be used - CD3DX12_ROOT_SIGNATURE_DESC descRootSignature = CD3DX12_ROOT_SIGNATURE_DESC(); - descRootSignature.NumParameters = parameterCount; - descRootSignature.pParameters = RTSlot; - descRootSignature.NumStaticSamplers = 1; // numStaticSamplers; - descRootSignature.pStaticSamplers = &SamplerDesc; //pStaticSamplers; - - // deny uneccessary access to certain pipeline stages - descRootSignature.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE; - - ID3DBlob *pOutBlob, *pErrorBlob = NULL; - ThrowIfFailed(D3D12SerializeRootSignature(&descRootSignature, D3D_ROOT_SIGNATURE_VERSION_1, &pOutBlob, &pErrorBlob)); - ThrowIfFailed( - pDevice->GetDevice()->CreateRootSignature(0, pOutBlob->GetBufferPointer(), pOutBlob->GetBufferSize(), IID_PPV_ARGS(&m_pRootSignature)) - ); - SetName(m_pRootSignature, std::string("PostProcCS::") + "SPD_CS"); - - pOutBlob->Release(); - if (pErrorBlob) - pErrorBlob->Release(); - } - - { - D3D12_COMPUTE_PIPELINE_STATE_DESC descPso = {}; - descPso.CS = shaderByteCode; - descPso.Flags = D3D12_PIPELINE_STATE_FLAG_NONE; - descPso.pRootSignature = m_pRootSignature; - descPso.NodeMask = 0; - - ThrowIfFailed(pDevice->GetDevice()->CreateComputePipelineState(&descPso, IID_PPV_ARGS(&m_pPipeline))); - } - - // Allocate descriptors for the mip chain - // - m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_constBuffer); - m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_sourceSRV); - m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(SPD_MAX_MIP_LEVELS, m_UAV); - for (int i = 0; i < SPD_MAX_MIP_LEVELS; i++) - { - m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_SRV[i]); - } - - m_pResourceViewHeaps->AllocCBV_SRV_UAVDescriptor(1, &m_globalCounter); - } - - void SPD_CS_Linear_Sampler::OnCreateWindowSizeDependentResources(uint32_t Width, uint32_t Height, Texture *pInput, int mipCount) - { - m_Width = Width; - m_Height = Height; - m_mipCount = mipCount; - m_pInput = pInput; - - m_result.InitRenderTarget( - m_pDevice, - "SPD_CS::m_result", - &CD3DX12_RESOURCE_DESC::Tex2D( - m_outFormat, - m_Width >> 1, - m_Height >> 1, - 1, - mipCount, - 1, - 0, - D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS), - D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); - - // Create views for the mip chain - // - - // source - // - pInput->CreateSRV(0, &m_sourceSRV, 0); - - // destination - // - for (int i = 0; i < m_mipCount; i++) - { - m_result.CreateUAV(i, m_UAV, i); - m_result.CreateSRV(0, &m_SRV[i], i); - } - - m_globalCounterBuffer.InitBuffer(m_pDevice, "SPD_CS::m_globalCounterBuffer", - &CD3DX12_RESOURCE_DESC::Buffer(sizeof(uint32_t), D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS), - sizeof(uint32_t), D3D12_RESOURCE_STATE_UNORDERED_ACCESS); - m_globalCounterBuffer.CreateBufferUAV(0, NULL, &m_globalCounter); - } - - void SPD_CS_Linear_Sampler::OnDestroyWindowSizeDependentResources() - { - m_globalCounterBuffer.OnDestroy(); - m_result.OnDestroy(); - } - - void SPD_CS_Linear_Sampler::OnDestroy() - { - if (m_pPipeline != NULL) - { - m_pPipeline->Release(); - m_pPipeline = NULL; - } - - if (m_pRootSignature != NULL) - { - m_pRootSignature->Release(); - m_pRootSignature = NULL; - } - } - - void SPD_CS_Linear_Sampler::Draw(ID3D12GraphicsCommandList2* pCommandList) - { - UserMarker marker(pCommandList, "SPD_CS_Linear_Sampler"); - - // downsample - uint32_t dispatchX = (((m_Width + 63) >> (6))); - uint32_t dispatchY = (((m_Height + 63) >> (6))); - uint32_t dispatchZ = 1; - - D3D12_GPU_VIRTUAL_ADDRESS cbHandle; - uint32_t* pConstMem; - m_pConstantBufferRing->AllocConstantBuffer(sizeof(cbDownscale), (void**)&pConstMem, &cbHandle); - cbDownscale constants; - constants.mips = m_mipCount; - constants.numWorkGroups = dispatchX * dispatchY * dispatchZ; - constants.invInputSize[0] = 1.0f / m_Width; - constants.invInputSize[1] = 1.0f / m_Height; - memcpy(pConstMem, &constants, sizeof(cbDownscale)); - - D3D12_RANGE range = { 0, sizeof(uint32_t) }; - - // Bind Descriptor heaps and the root signature - // - ID3D12DescriptorHeap *pDescriptorHeaps[] = { m_pResourceViewHeaps->GetCBV_SRV_UAVHeap(), m_pResourceViewHeaps->GetSamplerHeap() }; - pCommandList->SetDescriptorHeaps(2, pDescriptorHeaps); - pCommandList->SetComputeRootSignature(m_pRootSignature); - - // Bind Descriptor the descriptor sets - // - int params = 0; - pCommandList->SetComputeRootConstantBufferView(params++, cbHandle); - pCommandList->SetComputeRootDescriptorTable(params++, m_sourceSRV.GetGPU()); - pCommandList->SetComputeRootDescriptorTable(params++, m_globalCounter.GetGPU()); - pCommandList->SetComputeRootDescriptorTable(params++, m_UAV[0].GetGPU()); - - // Bind Pipeline - // - pCommandList->SetPipelineState(m_pPipeline); - - // set counter to 0 - pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_globalCounterBuffer.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_COPY_DEST, 0)); - - D3D12_WRITEBUFFERIMMEDIATE_PARAMETER pParams = { m_globalCounterBuffer.GetResource()->GetGPUVirtualAddress(), 0 }; - pCommandList->WriteBufferImmediate(1, &pParams, NULL); - - D3D12_RESOURCE_BARRIER resourceBarriers[2] = { - CD3DX12_RESOURCE_BARRIER::Transition(m_globalCounterBuffer.GetResource(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, 0), - CD3DX12_RESOURCE_BARRIER::Transition(m_result.GetResource(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS) - }; - pCommandList->ResourceBarrier(2, resourceBarriers); - - // Dispatch - // - pCommandList->Dispatch(dispatchX, dispatchY, dispatchZ); - pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_result.GetResource(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE)); - } - - void SPD_CS_Linear_Sampler::Gui() - { - bool opened = true; - ImGui::Begin("Downsample", &opened); - - ImGui::Image((ImTextureID)&m_sourceSRV, ImVec2(320, 180)); - for (int i = 0; i < m_mipCount; i++) - { - ImGui::Image((ImTextureID)&m_SRV[i], ImVec2(320, 180)); - } - - ImGui::End(); - } -} \ No newline at end of file diff --git a/sample/src/DX12/SPD_CS_Linear_Sampler.h b/sample/src/DX12/SPD_CS_Linear_Sampler.h deleted file mode 100644 index 1dc1a3c..0000000 --- a/sample/src/DX12/SPD_CS_Linear_Sampler.h +++ /dev/null @@ -1,73 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -#pragma once - -#include "Base/DynamicBufferRing.h" -#include "Base/Texture.h" - -namespace CAULDRON_DX12 -{ -#define SPD_MAX_MIP_LEVELS 12 - - class SPD_CS_Linear_Sampler - { - public: - void OnCreate(Device *pDevice, ResourceViewHeaps *pResourceViewHeaps, DynamicBufferRing *pConstantBufferRing, DXGI_FORMAT outFormat, bool fallback, bool packed); - void OnDestroy(); - - void OnCreateWindowSizeDependentResources(uint32_t Width, uint32_t Height, Texture *pInput, int mips); - void OnDestroyWindowSizeDependentResources(); - - void Draw(ID3D12GraphicsCommandList2* pCommandList); - Texture *GetTexture() { return &m_result; } - CBV_SRV_UAV GetTextureView(int i) { if (i == 0) { return m_sourceSRV; } else { return m_SRV[i]; } } - void Gui(); - - struct cbDownscale - { - int mips; - int numWorkGroups; - float invInputSize[2]; - }; - - private: - Device* m_pDevice = nullptr; - DXGI_FORMAT m_outFormat; - - Texture *m_pInput; - Texture m_result; - - CBV_SRV_UAV m_constBuffer; // dimension - CBV_SRV_UAV m_UAV[SPD_MAX_MIP_LEVELS]; //dest - CBV_SRV_UAV m_SRV[SPD_MAX_MIP_LEVELS]; // for display of mips using imGUI - CBV_SRV_UAV m_sourceSRV; //src - - CBV_SRV_UAV m_globalCounter; - Texture m_globalCounterBuffer; - - ResourceViewHeaps *m_pResourceViewHeaps; - DynamicBufferRing *m_pConstantBufferRing; - ID3D12RootSignature *m_pRootSignature; - ID3D12PipelineState *m_pPipeline = NULL; - - uint32_t m_Width; - uint32_t m_Height; - int m_mipCount; - }; -} \ No newline at end of file diff --git a/sample/src/DX12/SPD_Integration.hlsl b/sample/src/DX12/SPD_Integration.hlsl deleted file mode 100644 index 9f57ed3..0000000 --- a/sample/src/DX12/SPD_Integration.hlsl +++ /dev/null @@ -1,120 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -// when using amd shader intrinscs -// #include "ags_shader_intrinsics_dx12.h" - -//-------------------------------------------------------------------------------------- -// Constant Buffer -//-------------------------------------------------------------------------------------- -cbuffer spdConstants : register(b0) -{ - uint mips; - uint numWorkGroups; -} - -//-------------------------------------------------------------------------------------- -// Texture definitions -//-------------------------------------------------------------------------------------- -/*globallycoherent*/ RWTexture2D imgDst[12] : register(u2); -Texture2D imgSrc : register(t0); - -//-------------------------------------------------------------------------------------- -// Buffer definitions - global atomic counter -//-------------------------------------------------------------------------------------- -struct globalAtomicBuffer -{ - uint counter; -}; -globallycoherent RWStructuredBuffer globalAtomic :register(u1); - -#define A_GPU -#define A_HLSL - -#include "ffx_a.h" - -groupshared AU1 spd_counter; - -#ifndef SPD_PACKED_ONLY -groupshared AF1 spd_intermediateR[16][16]; -groupshared AF1 spd_intermediateG[16][16]; -groupshared AF1 spd_intermediateB[16][16]; -groupshared AF1 spd_intermediateA[16][16]; -AF4 SpdLoadSourceImage(AF2 tex){return imgSrc[tex];} -AF4 SpdLoad(ASU2 tex){return imgDst[5][tex];} -void SpdStore(ASU2 pix, AF4 outValue, AU1 index){imgDst[index][pix] = outValue;} -void SpdIncreaseAtomicCounter(){InterlockedAdd(globalAtomic[0].counter, 1, spd_counter);} -AU1 SpdGetAtomicCounter(){return spd_counter;} -AF4 SpdLoadIntermediate(AU1 x, AU1 y){ - return AF4( - spd_intermediateR[x][y], - spd_intermediateG[x][y], - spd_intermediateB[x][y], - spd_intermediateA[x][y]);} -void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value){ - spd_intermediateR[x][y] = value.x; - spd_intermediateG[x][y] = value.y; - spd_intermediateB[x][y] = value.z; - spd_intermediateA[x][y] = value.w;} -AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3){return (v0+v1+v2+v3)*0.25;} -#endif - -// define fetch and store functions Packed -#ifdef A_HALF -groupshared AH2 spd_intermediateRG[16][16]; -groupshared AH2 spd_intermediateBA[16][16]; -AH4 SpdLoadSourceImageH(AF2 tex){return AH4(imgSrc[tex]);} -AH4 SpdLoadH(ASU2 p){return AH4(imgDst[5][p]);} -void SpdStoreH(ASU2 p, AH4 value, AU1 mip){imgDst[mip][p] = AF4(value);} -void SpdIncreaseAtomicCounter(){InterlockedAdd(globalAtomic[0].counter, 1, spd_counter);} -AU1 SpdGetAtomicCounter(){return spd_counter;} -AH4 SpdLoadIntermediateH(AU1 x, AU1 y){ - return AH4( - spd_intermediateRG[x][y].x, - spd_intermediateRG[x][y].y, - spd_intermediateBA[x][y].x, - spd_intermediateBA[x][y].y);} -void SpdStoreIntermediateH(AU1 x, AU1 y, AH4 value){ - spd_intermediateRG[x][y] = value.xy; - spd_intermediateBA[x][y] = value.zw;} -AH4 SpdReduce4H(AH4 v0, AH4 v1, AH4 v2, AH4 v3){return (v0+v1+v2+v3)*AH1(0.25);} -#endif - -#include "ffx_spd.h" - -// Main function -//-------------------------------------------------------------------------------------- -//-------------------------------------------------------------------------------------- -[numthreads(256, 1, 1)] -void main(uint3 WorkGroupId : SV_GroupID, uint LocalThreadIndex : SV_GroupIndex) -{ -#ifndef A_HALF - SpdDownsample( - AU2(WorkGroupId.xy), - AU1(LocalThreadIndex), - AU1(mips), - AU1(numWorkGroups)); -#else - SpdDownsampleH( - AU2(WorkGroupId.xy), - AU1(LocalThreadIndex), - AU1(mips), - AU1(numWorkGroups)); -#endif - } \ No newline at end of file diff --git a/sample/src/DX12/SPD_Integration_Linear_Sampler.hlsl b/sample/src/DX12/SPD_Integration_Linear_Sampler.hlsl deleted file mode 100644 index ca9c59b..0000000 --- a/sample/src/DX12/SPD_Integration_Linear_Sampler.hlsl +++ /dev/null @@ -1,133 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -// when using amd shader intrinscs -// #include "ags_shader_intrinsics_dx12.h" - -//-------------------------------------------------------------------------------------- -// Constant Buffer -//-------------------------------------------------------------------------------------- -cbuffer spdConstants : register(b0) -{ - uint mips; - uint numWorkGroups; - // [SAMPLER] - float2 invInputSize; -} - -//-------------------------------------------------------------------------------------- -// Texture definitions -//-------------------------------------------------------------------------------------- -/*globallycoherent*/ RWTexture2D imgDst[12] : register(u2); -Texture2D imgSrc : register(t0); -SamplerState srcSampler : register(s0); - -//-------------------------------------------------------------------------------------- -// Buffer definitions - global atomic counter -//-------------------------------------------------------------------------------------- -struct globalAtomicBuffer -{ - uint counter; -}; -globallycoherent RWStructuredBuffer globalAtomic :register(u1); - -#define A_GPU -#define A_HLSL - -#include "ffx_a.h" - -groupshared AU1 spd_counter; - -#ifndef SPD_PACKED_ONLY -groupshared AF1 spd_intermediateR[16][16]; -groupshared AF1 spd_intermediateG[16][16]; -groupshared AF1 spd_intermediateB[16][16]; -groupshared AF1 spd_intermediateA[16][16]; -//AF4 DSLoadSourceImage(AF2 tex){return imgSrc[tex];} -// [SAMPLER] -AF4 SpdLoadSourceImage(ASU2 p){ - AF2 textureCoord = p * invInputSize + invInputSize; - return imgSrc.SampleLevel(srcSampler, textureCoord, 0); -} -AF4 SpdLoad(ASU2 tex){return imgDst[5][tex];} -void SpdStore(ASU2 pix, AF4 outValue, AU1 index){imgDst[index][pix] = outValue;} -void SpdIncreaseAtomicCounter(){InterlockedAdd(globalAtomic[0].counter, 1, spd_counter);} -AU1 SpdGetAtomicCounter(){return spd_counter;} -AF4 SpdLoadIntermediate(AU1 x, AU1 y){ - return AF4( - spd_intermediateR[x][y], - spd_intermediateG[x][y], - spd_intermediateB[x][y], - spd_intermediateA[x][y]);} -void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value){ - spd_intermediateR[x][y] = value.x; - spd_intermediateG[x][y] = value.y; - spd_intermediateB[x][y] = value.z; - spd_intermediateA[x][y] = value.w;} -AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3){return (v0+v1+v2+v3)*0.25;} -#endif - -// define fetch and store functions Packed -#ifdef A_HALF -groupshared AH2 spd_intermediateRG[16][16]; -groupshared AH2 spd_intermediateBA[16][16]; -AH4 SpdLoadSourceImageH(ASU2 p){ - AF2 textureCoord = p * invInputSize + invInputSize; - return AH4(imgSrc.SampleLevel(srcSampler, textureCoord, 0)); -} -AH4 SpdLoadH(ASU2 p){return AH4(imgDst[5][p]);} -void SpdStoreH(ASU2 p, AH4 value, AU1 mip){imgDst[mip][p] = AF4(value);} -void SpdIncreaseAtomicCounter(){InterlockedAdd(globalAtomic[0].counter, 1, spd_counter);} -AU1 SpdGetAtomicCounter(){return spd_counter;} -AH4 SpdLoadIntermediateH(AU1 x, AU1 y){ - return AH4( - spd_intermediateRG[x][y].x, - spd_intermediateRG[x][y].y, - spd_intermediateBA[x][y].x, - spd_intermediateBA[x][y].y);} -void SpdStoreIntermediateH(AU1 x, AU1 y, AH4 value){ - spd_intermediateRG[x][y] = value.xy; - spd_intermediateBA[x][y] = value.zw;} -AH4 SpdReduce4H(AH4 v0, AH4 v1, AH4 v2, AH4 v3){return (v0+v1+v2+v3)*AH1(0.25);} -#endif - -#define SPD_LINEAR_SAMPLER - -#include "ffx_spd.h" - -// Main function -//-------------------------------------------------------------------------------------- -//-------------------------------------------------------------------------------------- -[numthreads(256, 1, 1)] -void main(uint3 WorkGroupId : SV_GroupID, uint LocalThreadIndex : SV_GroupIndex) -{ -#ifndef A_HALF - SpdDownsample( - AU2(WorkGroupId.xy), - AU1(LocalThreadIndex), - AU1(mips), - AU1(numWorkGroups)); -#else - SpdDownsampleH( - AU2(WorkGroupId.xy), - AU1(LocalThreadIndex), - AU1(mips), - AU1(numWorkGroups)); -#endif - } \ No newline at end of file diff --git a/sample/src/DX12/SPD_Sample.cpp b/sample/src/DX12/SPD_Sample.cpp deleted file mode 100644 index 7f71f53..0000000 --- a/sample/src/DX12/SPD_Sample.cpp +++ /dev/null @@ -1,377 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#include "stdafx.h" - -#include "SPD_Sample.h" - -const bool VALIDATION_ENABLED = true; - -SPD_Sample::SPD_Sample(LPCSTR name) : FrameworkWindows(name) -{ - m_lastFrameTime = MillisecondsNow(); - m_time = 0; - m_bPlay = true; - - m_pGltfLoader = NULL; -} - -//-------------------------------------------------------------------------------------- -// -// OnCreate -// -//-------------------------------------------------------------------------------------- -void SPD_Sample::OnCreate(HWND hWnd) -{ - // Create Device - // - m_device.OnCreate("FFX_SPD_Sample", "Cauldron", VALIDATION_ENABLED, hWnd); - m_device.CreatePipelineCache(); - - //init the shader compiler - CreateShaderCache(); - - // 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 SPD_Renderer(); - 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 = 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.spotlightCount = 1; - - m_state.spotlight[0].intensity = 10.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.downsampler = Downsampler::SPD_CS; - m_state.spdVersion = SPD_Version::SPD_WaveOps; - m_state.spdPacked = SPD_Packed::SPD_Non_Packed; -} - -//-------------------------------------------------------------------------------------- -// -// OnDestroy -// -//-------------------------------------------------------------------------------------- -void SPD_Sample::OnDestroy() -{ - ImGUI_Shutdown(); - - m_device.GPUFlush(); - - // Fullscreen state should always be false before exiting the app. - m_swapChain.SetFullScreen(false); - - m_Node->UnloadScene(); - m_Node->OnDestroyWindowSizeDependentResources(); - m_Node->OnDestroy(); - - delete m_Node; - - m_swapChain.OnDestroyWindowSizeDependentResources(); - m_swapChain.OnDestroy(); - - //shut down the shader compiler - DestroyShaderCache(&m_device); - - if (m_pGltfLoader) - { - delete m_pGltfLoader; - m_pGltfLoader = NULL; - } - - m_device.OnDestroy(); -} - -//-------------------------------------------------------------------------------------- -// -// OnEvent -// -//-------------------------------------------------------------------------------------- -bool SPD_Sample::OnEvent(MSG msg) -{ - if (ImGUI_WndProcHandler(msg.hwnd, msg.message, msg.wParam, msg.lParam)) - return true; - - return true; -} - -//-------------------------------------------------------------------------------------- -// -// SetFullScreen -// -//-------------------------------------------------------------------------------------- -void SPD_Sample::SetFullScreen(bool fullscreen) -{ - m_device.GPUFlush(); - - m_swapChain.SetFullScreen(fullscreen); -} - -//-------------------------------------------------------------------------------------- -// -// OnResize -// -//-------------------------------------------------------------------------------------- -void SPD_Sample::OnResize(uint32_t width, uint32_t height) -{ - if (m_Width != width || m_Height != height) - { - // Flush GPU - // - m_device.GPUFlush(); - - // If resizing but no minimizing - // - if (m_Width > 0 && m_Height > 0) - { - 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, false, DISPLAYMODE_SDR); - m_Node->OnCreateWindowSizeDependentResources(&m_swapChain, m_Width, m_Height); - } - } - m_state.camera.SetFov(XM_PI / 4, m_Width, m_Height, 0.1f, 1000.0f); -} - -//-------------------------------------------------------------------------------------- -// -// OnRender, updates the state from the UI, animates, transforms and renders the scene -// -//-------------------------------------------------------------------------------------- -void SPD_Sample::OnRender() -{ - // Get timings - // - double timeNow = MillisecondsNow(); - m_deltaTime = timeNow - m_lastFrameTime; - m_lastFrameTime = timeNow; - - // Build UI and set the scene state. Note that the rendering of the UI happens later. - // - ImGUI_UpdateIO(); - ImGui::NewFrame(); - - static int loadingStage = 0; - if (loadingStage >= 0) - { - // LoadScene needs to be called a number of times, the scene is not fully loaded until it returns -1 - // This is done so we can display a progress bar when the scene is loading - if (m_pGltfLoader == NULL) - { - m_pGltfLoader = new GLTFCommon(); - m_pGltfLoader->Load("..\\media\\DamagedHelmet\\glTF\\", "DamagedHelmet.gltf"); - loadingStage = 0; - } - loadingStage = m_Node->LoadScene(m_pGltfLoader, loadingStage); - } - else - { - ImGuiStyle& style = ImGui::GetStyle(); - style.FrameBorderSize = 1.0f; - - bool opened = true; - ImGui::Begin("Stats", &opened); - - if (ImGui::CollapsingHeader("Info", ImGuiTreeNodeFlags_DefaultOpen)) - { - ImGui::Text("Resolution : %ix%i", m_Width, m_Height); - } - - if (ImGui::CollapsingHeader("Downsampler", ImGuiTreeNodeFlags_DefaultOpen)) - { - // Downsample settings - const char* downsampleItemNames[] = - { - "PS", - "Multipass CS", - "SPD CS", - "SPD CS Linear Sampler", - }; - ImGui::Combo("Downsampler Options", (int*)&m_state.downsampler, downsampleItemNames, _countof(downsampleItemNames)); - - // Downsample settings - const char* spdVersionItemNames[] = - { - "No-WaveOps", - "WaveOps", - }; - ImGui::Combo("SPD Version", (int*)&m_state.spdVersion, spdVersionItemNames, _countof(spdVersionItemNames)); - - // NON-PACKED or PACKED Version - const char* spdPackedNames[] = - { - "Non-Packed", - "Packed", - }; - ImGui::Combo("SPD Non-Packed / Packed Version", (int*)&m_state.spdPacked, spdPackedNames, _countof(spdPackedNames)); - } - - if (ImGui::CollapsingHeader("Lighting", ImGuiTreeNodeFlags_DefaultOpen)) - { - ImGui::SliderFloat("exposure", &m_state.exposure, 0.0f, 2.0f); - ImGui::SliderFloat("emmisive", &m_state.emmisiveFactor, 1.0f, 1000.0f, NULL, 1.0f); - ImGui::SliderFloat("iblFactor", &m_state.iblFactor, 0.0f, 2.0f); - } - - const char * tonemappers[] = { "Timothy", "DX11DSK", "Reinhard", "Uncharted2Tonemap", "ACES", "No tonemapper" }; - ImGui::Combo("tone mapper", &m_state.toneMapper, tonemappers, _countof(tonemappers)); - - const char * skyDomeType[] = { "Procedural Sky", "cubemap", "Simple clear" }; - ImGui::Combo("SkyDome", &m_state.skyDomeType, skyDomeType, _countof(skyDomeType)); - - const char * cameraControl[] = { "WASD", "Orbit" }; - static int cameraControlSelected = 1; - ImGui::Combo("Camera", &cameraControlSelected, cameraControl, _countof(cameraControl)); - - if (ImGui::CollapsingHeader("Profiler", ImGuiTreeNodeFlags_DefaultOpen)) - { - std::vector timeStamps = m_Node->GetTimingValues(); - if (timeStamps.size() > 0) - { - for (uint32_t i = 1; i < timeStamps.size(); i++) - { - float DeltaTime = ((float)(timeStamps[i].m_microseconds - timeStamps[i - 1].m_microseconds)); - ImGui::Text("%-17s: %7.1f us", timeStamps[i].m_label.c_str(), DeltaTime); - } - - //scrolling data and average computing - static float values[128]; - values[127] = (float)(timeStamps.back().m_microseconds - timeStamps.front().m_microseconds); - float average = values[0]; - for (uint32_t i = 0; i < 128 - 1; i++) { values[i] = values[i + 1]; average += values[i]; } - average /= 128; - - ImGui::Text("%-17s: %7.1f us", "TotalGPUTime", average); - ImGui::PlotLines("", values, 128, 0, "", 0.0f, 30000.0f, ImVec2(0, 80)); - } - } - - ImGui::End(); - - // If the mouse was not used by the GUI then it's for the camera - // - ImGuiIO& io = ImGui::GetIO(); - if (io.WantCaptureMouse == false) - { - 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 (cameraControlSelected == 0) - { - // WASD - // - m_state.camera.UpdateCameraWASD(m_roll, m_pitch, io.KeysDown, io.DeltaTime); - } - else if (cameraControlSelected == 1) - { - // Orbiting - // - m_distance -= (float)io.MouseWheel / 3.0f; - m_distance = std::max(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 ); - } - } - } - - // Set animation time - // - if (m_bPlay) - { - m_time += (float)m_deltaTime / 1000.0f; - } - - // Animate and transform the scene - // - if (m_pGltfLoader) - { - m_pGltfLoader->SetAnimationTime(0, m_time); - m_pGltfLoader->TransformScene(0, XMMatrixIdentity()); - } - - m_state.time = m_time; - - // Do Render frame using AFR - // - m_Node->OnRender(&m_state, &m_swapChain); - - m_swapChain.Present(); -} - - -//-------------------------------------------------------------------------------------- -// -// WinMain -// -//-------------------------------------------------------------------------------------- -int WINAPI WinMain(HINSTANCE hInstance, - HINSTANCE hPrevInstance, - LPSTR lpCmdLine, - int nCmdShow) -{ - LPCSTR Name = "FFX SPD SampleDX12 v1.0"; - uint32_t Width = 1920; - uint32_t Height = 1080; - - // create new DX sample - return RunFramework(hInstance, lpCmdLine, nCmdShow, Width, Height, new SPD_Sample(Name)); -} diff --git a/sample/src/DX12/SPD_Versions.cpp b/sample/src/DX12/SPD_Versions.cpp deleted file mode 100644 index aba0ce8..0000000 --- a/sample/src/DX12/SPD_Versions.cpp +++ /dev/null @@ -1,206 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#include "stdafx.h" -#include "base/DynamicBufferRing.h" -#include "base/StaticBufferPool.h" -#include "base/UploadHeap.h" -#include "base/Texture.h" -#include "base/Helper.h" -#include "SPD_Versions.h" - -namespace CAULDRON_DX12 -{ - void SPD_Versions::OnCreate( - Device *pDevice, - ResourceViewHeaps *pResourceViewHeaps, - DynamicBufferRing *pConstantBufferRing, - DXGI_FORMAT outFormat - ) - { - m_pDevice = pDevice; - - m_spd_WaveOps_NonPacked.OnCreate(pDevice, pResourceViewHeaps, pConstantBufferRing, outFormat, false, false); - m_spd_WaveOps_Packed.OnCreate(pDevice, pResourceViewHeaps, pConstantBufferRing, outFormat, false, true); - m_spd_No_WaveOps_NonPacked.OnCreate(pDevice, pResourceViewHeaps, pConstantBufferRing, outFormat, true, false); - m_spd_No_WaveOps_Packed.OnCreate(pDevice, pResourceViewHeaps, pConstantBufferRing, outFormat, true, true); - - m_spd_WaveOps_NonPacked_Linear_Sampler.OnCreate(pDevice, pResourceViewHeaps, pConstantBufferRing, outFormat, false, false); - m_spd_WaveOps_Packed_Linear_Sampler.OnCreate(pDevice, pResourceViewHeaps, pConstantBufferRing, outFormat, false, true); - m_spd_No_WaveOps_NonPacked_Linear_Sampler.OnCreate(pDevice, pResourceViewHeaps, pConstantBufferRing, outFormat, true, false); - m_spd_No_WaveOps_Packed_Linear_Sampler.OnCreate(pDevice, pResourceViewHeaps, pConstantBufferRing, outFormat, true, true); - } - - int SPD_Versions::GetMaxMipLevelCount(int Width, int Height) - { - int resolution = max(Width, Height); - return (static_cast(min(1.0f + floor(log2(resolution)), 12)) - 1); - } - - void SPD_Versions::OnCreateWindowSizeDependentResources(int Width, int Height, Texture *pInput) - { - m_spd_WaveOps_NonPacked.OnCreateWindowSizeDependentResources(Width, Height, pInput, GetMaxMipLevelCount(Width, Height)); - m_spd_WaveOps_Packed.OnCreateWindowSizeDependentResources(Width, Height, pInput, GetMaxMipLevelCount(Width, Height)); - m_spd_No_WaveOps_NonPacked.OnCreateWindowSizeDependentResources(Width, Height, pInput, GetMaxMipLevelCount(Width, Height)); - m_spd_No_WaveOps_Packed.OnCreateWindowSizeDependentResources(Width, Height, pInput, GetMaxMipLevelCount(Width, Height)); - - m_spd_WaveOps_NonPacked_Linear_Sampler.OnCreateWindowSizeDependentResources(Width, Height, pInput, GetMaxMipLevelCount(Width, Height)); - m_spd_WaveOps_Packed_Linear_Sampler.OnCreateWindowSizeDependentResources(Width, Height, pInput, GetMaxMipLevelCount(Width, Height)); - m_spd_No_WaveOps_NonPacked_Linear_Sampler.OnCreateWindowSizeDependentResources(Width, Height, pInput, GetMaxMipLevelCount(Width, Height)); - m_spd_No_WaveOps_Packed_Linear_Sampler.OnCreateWindowSizeDependentResources(Width, Height, pInput, GetMaxMipLevelCount(Width, Height)); - } - - void SPD_Versions::OnDestroyWindowSizeDependentResources() - { - m_spd_WaveOps_NonPacked.OnDestroyWindowSizeDependentResources(); - m_spd_WaveOps_Packed.OnDestroyWindowSizeDependentResources(); - m_spd_No_WaveOps_NonPacked.OnDestroyWindowSizeDependentResources(); - m_spd_No_WaveOps_Packed.OnDestroyWindowSizeDependentResources(); - - m_spd_WaveOps_NonPacked_Linear_Sampler.OnDestroyWindowSizeDependentResources(); - m_spd_WaveOps_Packed_Linear_Sampler.OnDestroyWindowSizeDependentResources(); - m_spd_No_WaveOps_NonPacked_Linear_Sampler.OnDestroyWindowSizeDependentResources(); - m_spd_No_WaveOps_Packed_Linear_Sampler.OnDestroyWindowSizeDependentResources(); - } - - void SPD_Versions::OnDestroy() - { - m_spd_WaveOps_NonPacked.OnDestroy(); - m_spd_WaveOps_Packed.OnDestroy(); - m_spd_No_WaveOps_NonPacked.OnDestroy(); - m_spd_No_WaveOps_Packed.OnDestroy(); - - m_spd_WaveOps_NonPacked_Linear_Sampler.OnDestroy(); - m_spd_WaveOps_Packed_Linear_Sampler.OnDestroy(); - m_spd_No_WaveOps_NonPacked_Linear_Sampler.OnDestroy(); - m_spd_No_WaveOps_Packed_Linear_Sampler.OnDestroy(); - } - - void SPD_Versions::Dispatch(ID3D12GraphicsCommandList2* pCommandList, SPD_Version spdVersion, SPD_Packed spdPacked) - { - switch (spdVersion) - { - case SPD_Version::SPD_WaveOps: - switch (spdPacked) - { - case SPD_Packed::SPD_Non_Packed: - m_spd_WaveOps_NonPacked.Draw(pCommandList); - break; - case SPD_Packed::SPD_Packed: - m_spd_WaveOps_Packed.Draw(pCommandList); - break; - } - break; - case SPD_Version::SPD_No_WaveOps: - switch (spdPacked) - { - case SPD_Packed::SPD_Non_Packed: - m_spd_No_WaveOps_NonPacked.Draw(pCommandList); - break; - case SPD_Packed::SPD_Packed: - m_spd_No_WaveOps_Packed.Draw(pCommandList); - break; - } - } - } - - void SPD_Versions::DispatchLinearSamplerVersion(ID3D12GraphicsCommandList2* pCommandList, SPD_Version spdVersion, SPD_Packed spdPacked) - { - switch (spdVersion) - { - case SPD_Version::SPD_WaveOps: - switch (spdPacked) - { - case SPD_Packed::SPD_Non_Packed: - m_spd_WaveOps_NonPacked_Linear_Sampler.Draw(pCommandList); - break; - case SPD_Packed::SPD_Packed: - m_spd_WaveOps_Packed_Linear_Sampler.Draw(pCommandList); - break; - } - break; - case SPD_Version::SPD_No_WaveOps: - switch (spdPacked) - { - case SPD_Packed::SPD_Non_Packed: - m_spd_No_WaveOps_NonPacked_Linear_Sampler.Draw(pCommandList); - break; - case SPD_Packed::SPD_Packed: - m_spd_No_WaveOps_Packed_Linear_Sampler.Draw(pCommandList); - break; - } - } - } - - void SPD_Versions::Gui(SPD_Version spdVersion, SPD_Packed spdPacked) - { - switch (spdVersion) - { - case SPD_Version::SPD_WaveOps: - switch (spdPacked) - { - case SPD_Packed::SPD_Non_Packed: - m_spd_WaveOps_NonPacked.Gui(); - break; - case SPD_Packed::SPD_Packed: - m_spd_WaveOps_Packed.Gui(); - break; - } - break; - case SPD_Version::SPD_No_WaveOps: - switch (spdPacked) - { - case SPD_Packed::SPD_Non_Packed: - m_spd_No_WaveOps_NonPacked.Gui(); - break; - case SPD_Packed::SPD_Packed: - m_spd_No_WaveOps_Packed.Gui(); - break; - } - } - } - - void SPD_Versions::GuiLinearSamplerVersion(SPD_Version spdVersion, SPD_Packed spdPacked) - { - switch (spdVersion) - { - case SPD_Version::SPD_WaveOps: - switch (spdPacked) - { - case SPD_Packed::SPD_Non_Packed: - m_spd_WaveOps_NonPacked_Linear_Sampler.Gui(); - break; - case SPD_Packed::SPD_Packed: - m_spd_WaveOps_Packed_Linear_Sampler.Gui(); - break; - } - break; - case SPD_Version::SPD_No_WaveOps: - switch (spdPacked) - { - case SPD_Packed::SPD_Non_Packed: - m_spd_No_WaveOps_NonPacked_Linear_Sampler.Gui(); - break; - case SPD_Packed::SPD_Packed: - m_spd_No_WaveOps_Packed_Linear_Sampler.Gui(); - break; - } - } - } -} \ No newline at end of file diff --git a/sample/src/DX12/SPD_Versions.h b/sample/src/DX12/SPD_Versions.h deleted file mode 100644 index bd1c1dc..0000000 --- a/sample/src/DX12/SPD_Versions.h +++ /dev/null @@ -1,75 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#pragma once -#include "SPD_CS.h" -#include "SPD_CS_Linear_Sampler.h" - -namespace CAULDRON_DX12 -{ - enum class SPD_Version - { - SPD_No_WaveOps, - SPD_WaveOps, - }; - - enum class SPD_Packed - { - SPD_Non_Packed, - SPD_Packed, - }; - - class SPD_Versions - { - public: - void OnCreate( - Device *pDevice, - ResourceViewHeaps *pResourceViewHeaps, - DynamicBufferRing *pConstantBufferRing, - DXGI_FORMAT outFormat - ); - void OnDestroy(); - - void OnCreateWindowSizeDependentResources(int Width, int Height, Texture *pInput); - void OnDestroyWindowSizeDependentResources(); - - void Dispatch(ID3D12GraphicsCommandList2* pCommandList, SPD_Version dsVersion, SPD_Packed dsPacked); - void Gui(SPD_Version dsVersion, SPD_Packed dsPacked); - - void DispatchLinearSamplerVersion(ID3D12GraphicsCommandList2* pCommandList, SPD_Version dsVersion, SPD_Packed dsPacked); - void GuiLinearSamplerVersion(SPD_Version dsVersion, SPD_Packed dsPacked); - - private: - Device *m_pDevice; - - SPD_CS m_spd_WaveOps_NonPacked; - SPD_CS m_spd_No_WaveOps_NonPacked; - - SPD_CS m_spd_WaveOps_Packed; - SPD_CS m_spd_No_WaveOps_Packed; - - SPD_CS_Linear_Sampler m_spd_WaveOps_NonPacked_Linear_Sampler; - SPD_CS_Linear_Sampler m_spd_No_WaveOps_NonPacked_Linear_Sampler; - - SPD_CS_Linear_Sampler m_spd_WaveOps_Packed_Linear_Sampler; - SPD_CS_Linear_Sampler m_spd_No_WaveOps_Packed_Linear_Sampler; - - int GetMaxMipLevelCount(int Width, int Height); - }; -} \ No newline at end of file diff --git a/sample/src/VK/CMakeLists.txt b/sample/src/VK/CMakeLists.txt index e68ff55..376e046 100644 --- a/sample/src/VK/CMakeLists.txt +++ b/sample/src/VK/CMakeLists.txt @@ -1,20 +1,21 @@ -project (SPDSample_VK) -include(${CMAKE_HOME_DIRECTORY}/common.cmake) +project(${PROJECT_NAME}) +include(${CMAKE_CURRENT_SOURCE_DIR}/../../common.cmake) + +add_compile_options(/MP) + set(sources CSDownsampler.cpp CSDownsampler.h PSDownsampler.cpp PSDownsampler.h - SPD_CS.cpp - SPD_CS.h - SPD_CS_Linear_Sampler.cpp - SPD_CS_Linear_Sampler.h - SPD_Sample.cpp - SPD_Sample.h - SPD_Renderer.cpp - SPD_Renderer.h - SPD_Versions.cpp - SPD_Versions.h + SPDCS.cpp + SPDCS.h + SPDSample.cpp + SPDSample.h + SPDRenderer.cpp + SPDRenderer.h + SPDVersions.cpp + SPDVersions.h stdafx.cpp stdafx.h) set(Shaders_src @@ -22,37 +23,44 @@ set(Shaders_src ${CMAKE_CURRENT_SOURCE_DIR}/../../../ffx-spd/ffx_spd.h ${CMAKE_CURRENT_SOURCE_DIR}/CSDownsampler.glsl ${CMAKE_CURRENT_SOURCE_DIR}/PSDownsampler.glsl - ${CMAKE_CURRENT_SOURCE_DIR}/SPD_Integration.glsl - ${CMAKE_CURRENT_SOURCE_DIR}/SPD_Integration.hlsl - ${CMAKE_CURRENT_SOURCE_DIR}/SPD_Integration_Linear_Sampler.glsl - ${CMAKE_CURRENT_SOURCE_DIR}/SPD_Integration_Linear_Sampler.hlsl + ${CMAKE_CURRENT_SOURCE_DIR}/SPDIntegration.glsl + ${CMAKE_CURRENT_SOURCE_DIR}/SPDIntegration.hlsl + ${CMAKE_CURRENT_SOURCE_DIR}/SPDIntegrationLinearSampler.glsl + ${CMAKE_CURRENT_SOURCE_DIR}/SPDIntegrationLinearSampler.hlsl +) +set(Common_src + ${CMAKE_CURRENT_SOURCE_DIR}/../Common/SpdSample.json ) source_group("Sources" FILES ${sources}) source_group("Shaders" FILES ${Shaders_src}) +source_group("Common" FILES ${Common_src}) + # prevent VS from processing/compiling these files set_source_files_properties(${Shaders_src} PROPERTIES VS_TOOL_OVERRIDE "Text") +set_source_files_properties(${Common_src} PROPERTIES VS_TOOL_OVERRIDE "Text") function(copyCommand list dest) foreach(fullFileName ${list}) - get_filename_component(file ${fullFileName} NAME) - message("Generating custom command for ${fullFileName}") - add_custom_command( - OUTPUT ${dest}/${file} - PRE_BUILD - COMMAND cmake -E make_directory ${dest} - COMMAND cmake -E copy ${fullFileName} ${dest} - MAIN_DEPENDENCY ${fullFileName} - COMMENT "Updating ${file} into ${dest}" - ) + get_filename_component(file ${fullFileName} NAME) + message("Generating custom command for ${fullFileName}") + add_custom_command( + OUTPUT ${dest}/${file} + PRE_BUILD + COMMAND cmake -E make_directory ${dest} + COMMAND cmake -E copy ${fullFileName} ${dest} + MAIN_DEPENDENCY ${fullFileName} + COMMENT "Updating ${file} into ${dest}" + ) endforeach() endfunction() # copy shaders and media to Bin # include("${CMAKE_HOME_DIRECTORY}/src/Common/Shaders/CMakeList.txt") copyCommand("${Shaders_src}" ${CMAKE_HOME_DIRECTORY}/bin/ShaderLibVK) - -add_executable(${PROJECT_NAME} WIN32 ${sources} ${Shaders_src}) +copyCommand("${Common_src}" ${CMAKE_HOME_DIRECTORY}/bin) + +add_executable(${PROJECT_NAME} WIN32 ${sources} ${Shaders_src} ${Common_src}) target_link_libraries (${PROJECT_NAME} LINK_PUBLIC Cauldron_VK ImGUI Vulkan::Vulkan) target_include_directories (${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../../../ffx-spd) set_target_properties(${PROJECT_NAME} PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_HOME_DIRECTORY}/bin") diff --git a/sample/src/VK/CSDownsampler.cpp b/sample/src/VK/CSDownsampler.cpp index 1a3ee50..b2604e7 100644 --- a/sample/src/VK/CSDownsampler.cpp +++ b/sample/src/VK/CSDownsampler.cpp @@ -28,14 +28,13 @@ namespace CAULDRON_VK { void CSDownsampler::OnCreate( - Device* pDevice, - ResourceViewHeaps* pResourceViewHeaps, - VkFormat outFormat + Device *pDevice, + UploadHeap *pUploadHeap, + ResourceViewHeaps *pResourceViewHeaps ) { m_pDevice = pDevice; m_pResourceViewHeaps = pResourceViewHeaps; - m_outFormat = outFormat; // create the descriptor set layout // the shader needs @@ -91,12 +90,14 @@ namespace CAULDRON_VK assert(res == VK_SUCCESS); } - // Do this stuff by yourself due to special requirements: push constants + m_cubeTexture.InitFromFile(pDevice, pUploadHeap , "..\\media\\envmaps\\papermill\\specular.dds", true, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT); + pUploadHeap->FlushAndFinish(); + VkPipelineShaderStageCreateInfo computeShader; DefineList defines; VkResult res = VKCompileFromFile(m_pDevice->GetDevice(), VK_SHADER_STAGE_COMPUTE_BIT, - "CSDownsampler.glsl", "main", &defines, &computeShader); + "CSDownsampler.glsl", "main", "", &defines, &computeShader); assert(res == VK_SUCCESS); // Create pipeline layout @@ -108,7 +109,7 @@ namespace CAULDRON_VK // push constants: input size, inverse output size VkPushConstantRange pushConstantRange = {}; pushConstantRange.offset = 0; - pushConstantRange.size = sizeof(PushConstantsCSSimple); + pushConstantRange.size = sizeof(cbDownsample); pushConstantRange.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; pPipelineLayoutCreateInfo.pushConstantRangeCount = 1; pPipelineLayoutCreateInfo.pPushConstantRanges = &pushConstantRange; @@ -139,78 +140,12 @@ namespace CAULDRON_VK { m_pResourceViewHeaps->AllocDescriptor(m_descriptorSetLayout, &m_mip[i].m_descriptorSet); } - } - - void CSDownsampler::OnCreateWindowSizeDependentResources( - VkCommandBuffer cmd_buf, - uint32_t Width, - uint32_t Height, - Texture* pInput, - int mips - ) - { - m_Width = Width; - m_Height = Height; - m_mipCount = mips; - - VkImageCreateInfo image_info = {}; - image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; - image_info.pNext = NULL; - image_info.imageType = VK_IMAGE_TYPE_2D; - image_info.format = m_outFormat; - image_info.extent.width = m_Width >> 1; - image_info.extent.height = m_Height >> 1; - image_info.extent.depth = 1; - image_info.mipLevels = mips; - image_info.arrayLayers = 1; - image_info.samples = VK_SAMPLE_COUNT_1_BIT; - image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - image_info.queueFamilyIndexCount = 0; - image_info.pQueueFamilyIndices = NULL; - image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - image_info.usage = (VkImageUsageFlags)(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT); - image_info.flags = 0; - image_info.tiling = VK_IMAGE_TILING_OPTIMAL; - m_result.Init(m_pDevice, &image_info, "DownsampleMipCS"); - - // transition layout undefined to general layout? - VkImageMemoryBarrier imageMemoryBarrier = {}; - imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - imageMemoryBarrier.pNext = NULL; - imageMemoryBarrier.srcAccessMask = 0; - imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; - imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - imageMemoryBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - imageMemoryBarrier.subresourceRange.baseMipLevel = 0; - imageMemoryBarrier.subresourceRange.levelCount = m_mipCount; - imageMemoryBarrier.subresourceRange.baseArrayLayer = 0; - imageMemoryBarrier.subresourceRange.layerCount = 1; - imageMemoryBarrier.image = m_result.Resource(); - - // transition general layout if detination image to shader read only for source image - vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier); - // Create views for the mip chain - // - for (int i = 0; i < m_mipCount; i++) + // populate descriptor sets + for (uint32_t i = 0; i < m_cubeTexture.GetMipCount() - 1; i++) { - // source ----------- - // - if (i == 0) - { - pInput->CreateSRV(&m_mip[i].m_SRV, 0); - } - else - { - m_result.CreateSRV(&m_mip[i].m_SRV, i - 1); - } - - // destination ----------- - m_result.CreateRTV(&m_mip[i].m_RTV, i); + m_cubeTexture.CreateSRV(&m_mip[i].m_SRV, i); // texture2DArray + m_cubeTexture.CreateRTV(&m_mip[i].m_UAV, i + 1); // texture2DArray // Create and initialize the Descriptor Sets (all of them use the same Descriptor Layout) // Create and initialize descriptor set for sampled image @@ -247,7 +182,7 @@ namespace CAULDRON_VK // Create and initialize descriptor set for storage image VkDescriptorImageInfo desc_storage_image = {}; desc_storage_image.sampler = VK_NULL_HANDLE; - desc_storage_image.imageView = m_mip[i].m_RTV; + desc_storage_image.imageView = m_mip[i].m_UAV; desc_storage_image.imageLayout = VK_IMAGE_LAYOUT_GENERAL; writes[2] = {}; @@ -262,30 +197,67 @@ namespace CAULDRON_VK vkUpdateDescriptorSets(m_pDevice->GetDevice(), (uint32_t)writes.size(), writes.data(), 0, NULL); } - } - void CSDownsampler::OnDestroyWindowSizeDependentResources() - { - for (int i = 0; i < m_mipCount; i++) + for (uint32_t slice = 0; slice < m_cubeTexture.GetArraySize(); slice++) { - vkDestroyImageView(m_pDevice->GetDevice(), m_mip[i].m_SRV, NULL); - vkDestroyImageView(m_pDevice->GetDevice(), m_mip[i].m_RTV, NULL); + for (uint32_t mip = 0; mip < m_cubeTexture.GetMipCount(); mip++) + { + VkImageViewUsageCreateInfo imageViewUsageInfo = {}; + imageViewUsageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO; + imageViewUsageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT; + + VkImageViewCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + info.pNext = &imageViewUsageInfo; + info.image = m_cubeTexture.Resource(); + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.subresourceRange.baseArrayLayer = slice; + info.subresourceRange.layerCount = 1; + + switch (m_cubeTexture.GetFormat()) + { + case VK_FORMAT_B8G8R8A8_UNORM: info.format = VK_FORMAT_B8G8R8A8_SRGB; break; + case VK_FORMAT_R8G8B8A8_UNORM: info.format = VK_FORMAT_R8G8B8A8_SRGB; break; + case VK_FORMAT_BC1_RGB_UNORM_BLOCK: info.format = VK_FORMAT_BC1_RGB_SRGB_BLOCK; break; + case VK_FORMAT_BC2_UNORM_BLOCK: info.format = VK_FORMAT_BC2_SRGB_BLOCK; break; + case VK_FORMAT_BC3_UNORM_BLOCK: info.format = VK_FORMAT_BC3_SRGB_BLOCK; break; + default: info.format = m_cubeTexture.GetFormat(); + } + + info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + info.subresourceRange.baseMipLevel = mip; + info.subresourceRange.levelCount = 1; + + VkResult res = vkCreateImageView(m_pDevice->GetDevice(), &info, NULL, + &m_imGUISRV[slice * m_cubeTexture.GetMipCount() + mip]); + assert(res == VK_SUCCESS); + } } - - m_result.OnDestroy(); } void CSDownsampler::OnDestroy() { + for (uint32_t i = 0; i < m_cubeTexture.GetMipCount() * 6; i++) + { + vkDestroyImageView(m_pDevice->GetDevice(), m_imGUISRV[i], NULL); + } + + for (uint32_t i = 0; i < m_cubeTexture.GetMipCount() - 1; i++) + { + vkDestroyImageView(m_pDevice->GetDevice(), m_mip[i].m_SRV, NULL); + vkDestroyImageView(m_pDevice->GetDevice(), m_mip[i].m_UAV, NULL); + } + for (int i = 0; i < CS_MAX_MIP_LEVELS; i++) { m_pResourceViewHeaps->FreeDescriptor(m_mip[i].m_descriptorSet); } - vkDestroyPipeline(m_pDevice->GetDevice(), m_pipeline, nullptr); - vkDestroyPipelineLayout(m_pDevice->GetDevice(), m_pipelineLayout, nullptr); + vkDestroyPipeline(m_pDevice->GetDevice(), m_pipeline, NULL); + vkDestroyPipelineLayout(m_pDevice->GetDevice(), m_pipelineLayout, NULL); vkDestroyDescriptorSetLayout(m_pDevice->GetDevice(), m_descriptorSetLayout, NULL); vkDestroySampler(m_pDevice->GetDevice(), m_sampler, nullptr); + m_cubeTexture.OnDestroy(); } void CSDownsampler::Draw(VkCommandBuffer cmd_buf) @@ -295,20 +267,21 @@ namespace CAULDRON_VK // transition layout undefined to general layout? VkImageMemoryBarrier imageMemoryBarrier = {}; + imageMemoryBarrier = {}; imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; imageMemoryBarrier.pNext = NULL; imageMemoryBarrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; - imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT; - imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; - imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; + imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; imageMemoryBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; imageMemoryBarrier.subresourceRange.baseMipLevel = 0; - imageMemoryBarrier.subresourceRange.levelCount = m_mipCount; + imageMemoryBarrier.subresourceRange.levelCount = 1; imageMemoryBarrier.subresourceRange.baseArrayLayer = 0; - imageMemoryBarrier.subresourceRange.layerCount = 1; - imageMemoryBarrier.image = m_result.Resource(); + imageMemoryBarrier.subresourceRange.layerCount = 6; + imageMemoryBarrier.image = m_cubeTexture.Resource(); // transition general layout if detination image to shader read only for source image vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, @@ -320,60 +293,99 @@ namespace CAULDRON_VK // vkCmdBindPipeline(cmd_buf, VK_PIPELINE_BIND_POINT_COMPUTE, m_pipeline); - for (int i = 0; i < m_mipCount; i++) + for (uint32_t slice = 0; slice < m_cubeTexture.GetArraySize(); slice++) { - uint32_t dispatchX = ((m_Width >> (i + 1)) + 7) / 8; - uint32_t dispatchY = ((m_Height >> (i + 1)) + 7) / 8; - uint32_t dispatchZ = 1; - - vkCmdBindDescriptorSets(cmd_buf, VK_PIPELINE_BIND_POINT_COMPUTE, m_pipelineLayout, 0, 1, &m_mip[i].m_descriptorSet, 0, nullptr); - - // Bind push constants - // - PushConstantsCSSimple data; - data.outputSize[0] = (float)(m_Width >> (i + 1)); - data.outputSize[1] = (float)(m_Height >> (i + 1)); - data.invInputSize[0] = 1.0f / (float)(m_Width >> i); - data.invInputSize[1] = 1.0f / (float)(m_Height >> i); - vkCmdPushConstants(cmd_buf, m_pipelineLayout, - VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(PushConstantsCSSimple), (void*)&data); - - // Draw - // - vkCmdDispatch(cmd_buf, dispatchX, dispatchY, dispatchZ); - - VkImageMemoryBarrier imageMemoryBarrier = {}; - imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - imageMemoryBarrier.pNext = NULL; - imageMemoryBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; - imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; - imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; - imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - imageMemoryBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - imageMemoryBarrier.subresourceRange.baseMipLevel = i; - imageMemoryBarrier.subresourceRange.levelCount = 1; - imageMemoryBarrier.subresourceRange.baseArrayLayer = 0; - imageMemoryBarrier.subresourceRange.layerCount = 1; - imageMemoryBarrier.image = m_result.Resource(); + VkImageMemoryBarrier imageMemoryBarrierArray = {}; + imageMemoryBarrierArray.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + imageMemoryBarrierArray.pNext = NULL; + imageMemoryBarrierArray.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + imageMemoryBarrierArray.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + imageMemoryBarrierArray.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imageMemoryBarrierArray.newLayout = VK_IMAGE_LAYOUT_GENERAL; + imageMemoryBarrierArray.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + imageMemoryBarrierArray.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + imageMemoryBarrierArray.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + imageMemoryBarrierArray.subresourceRange.baseMipLevel = 1; + imageMemoryBarrierArray.subresourceRange.levelCount = m_cubeTexture.GetMipCount() - 1; + imageMemoryBarrierArray.subresourceRange.baseArrayLayer = 0; + imageMemoryBarrierArray.subresourceRange.layerCount = 6; + imageMemoryBarrierArray.image = m_cubeTexture.Resource(); // transition general layout if destination image to shader read only for source image vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier); + 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrierArray); + + for (uint32_t i = 0; i < m_cubeTexture.GetMipCount() - 1; i++) + { + uint32_t dispatchX = ((m_cubeTexture.GetWidth() >> (i + 1)) + 7) / 8; + uint32_t dispatchY = ((m_cubeTexture.GetHeight() >> (i + 1)) + 7) / 8; + uint32_t dispatchZ = 1; + + vkCmdBindDescriptorSets(cmd_buf, VK_PIPELINE_BIND_POINT_COMPUTE, m_pipelineLayout, 0, 1, &m_mip[i].m_descriptorSet, 0, nullptr); + + // Bind push constants + // + cbDownsample data; + data.outputSize[0] = (float)(m_cubeTexture.GetWidth() >> (i + 1)); + data.outputSize[1] = (float)(m_cubeTexture.GetHeight() >> (i + 1)); + data.invInputSize[0] = 1.0f / (float)(m_cubeTexture.GetWidth() >> i); + data.invInputSize[1] = 1.0f / (float)(m_cubeTexture.GetHeight() >> i); + data.slice = slice; + vkCmdPushConstants(cmd_buf, m_pipelineLayout, + VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(cbDownsample), (void*)&data); + + // Draw + // + vkCmdDispatch(cmd_buf, dispatchX, dispatchY, dispatchZ); + + VkImageMemoryBarrier imageMemoryBarrier = {}; + imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + imageMemoryBarrier.pNext = NULL; + imageMemoryBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; + imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + imageMemoryBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + imageMemoryBarrier.subresourceRange.baseMipLevel = i + 1; + imageMemoryBarrier.subresourceRange.levelCount = 1; + imageMemoryBarrier.subresourceRange.baseArrayLayer = 0; + imageMemoryBarrier.subresourceRange.layerCount = 6; + imageMemoryBarrier.image = m_cubeTexture.Resource(); + + // transition general layout if destination image to shader read only for source image + vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier); + } } SetPerfMarkerEnd(cmd_buf); } - void CSDownsampler::Gui() + void CSDownsampler::GUI(int* pSlice) { bool opened = true; - ImGui::Begin("Downsample", &opened); + std::string header = "Downsample"; + ImGui::Begin(header.c_str(), &opened); - for (int i = 0; i < m_mipCount; i++) + if (ImGui::CollapsingHeader("CS Multipass", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Image((ImTextureID)m_mip[i].m_SRV, ImVec2(320, 180)); + const char* sliceItemNames[] = + { + "Slice 0", + "Slice 1", + "Slice 2", + "Slice 3", + "Slice 4", + "Slice 5" + }; + ImGui::Combo("Slice of Cube Texture", pSlice, sliceItemNames, _countof(sliceItemNames)); + + for (uint32_t i = 0; i < m_cubeTexture.GetMipCount(); i++) + { + ImGui::Image((ImTextureID)m_imGUISRV[*pSlice * m_cubeTexture.GetMipCount() + i], ImVec2(static_cast(512 >> i), static_cast(512 >> i))); + } } ImGui::End(); diff --git a/sample/src/VK/CSDownsampler.glsl b/sample/src/VK/CSDownsampler.glsl index f4408c7..8121021 100644 --- a/sample/src/VK/CSDownsampler.glsl +++ b/sample/src/VK/CSDownsampler.glsl @@ -28,18 +28,20 @@ layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; //-------------------------------------------------------------------------------------- // Push Constants //-------------------------------------------------------------------------------------- -layout(push_constant) uniform pushConstants { +layout(push_constant) uniform pushConstants +{ vec2 u_outputTextureSize; vec2 u_inputInvTextureSize; + int u_slice; } myPerMip; //-------------------------------------------------------------------------------------- // Texture definitions //-------------------------------------------------------------------------------------- -layout(set=0, binding=0) uniform texture2D inputTexture; +layout(set=0, binding=0) uniform texture2DArray inputTexture; layout(set=0, binding=1) uniform sampler inputSampler; -layout(set=0, binding=2, rgba16f) uniform writeonly image2D outputTexture; +layout(set=0, binding=2, rgba16f) uniform writeonly image2DArray outputTexture; // Main function //-------------------------------------------------------------------------------------- @@ -52,5 +54,8 @@ void main() ivec2 pixel_coord = ivec2(gl_GlobalInvocationID.xy); vec2 texcoord = myPerMip.u_inputInvTextureSize.xy * gl_GlobalInvocationID.xy * 2.0f + myPerMip.u_inputInvTextureSize.xy; - imageStore(outputTexture, pixel_coord, texture(sampler2D(inputTexture, inputSampler), texcoord)); + imageStore(outputTexture, + ivec3(pixel_coord, myPerMip.u_slice), + texture(sampler2DArray(inputTexture, inputSampler), vec3(texcoord, myPerMip.u_slice)) + ); } \ No newline at end of file diff --git a/sample/src/VK/CSDownsampler.h b/sample/src/VK/CSDownsampler.h index 3a86856..c638c85 100644 --- a/sample/src/VK/CSDownsampler.h +++ b/sample/src/VK/CSDownsampler.h @@ -28,49 +28,45 @@ namespace CAULDRON_VK class CSDownsampler { public: - void OnCreate(Device* pDevice, ResourceViewHeaps *pResourceViewHeaps, VkFormat outFormat); + void OnCreate(Device *pDevice, UploadHeap *pUploadHeap, ResourceViewHeaps *pResourceViewHeaps); void OnDestroy(); - void OnCreateWindowSizeDependentResources(VkCommandBuffer cmd_buf, uint32_t Width, uint32_t Height, Texture *pInput, int mips); - void OnDestroyWindowSizeDependentResources(); - void Draw(VkCommandBuffer cmd_buf); - Texture *GetTexture() { return &m_result; } - VkImageView GetTextureView(int i) { return m_mip[i].m_SRV; } - void Gui(); + Texture *GetTexture() { return &m_cubeTexture; } + void GUI(int *pSlice); - struct PushConstantsCSSimple + struct cbDownsample { float outputSize[2]; float invInputSize[2]; + uint32_t slice; + uint32_t padding[3]; }; private: - Device *m_pDevice; - VkFormat m_outFormat; + Device *m_pDevice = nullptr; - Texture m_result; + Texture m_cubeTexture; struct Pass { - VkImageView m_RTV; - VkImageView m_SRV; + VkImageView m_UAV; + VkImageView m_SRV; VkDescriptorSet m_descriptorSet; }; - Pass m_mip[CS_MAX_MIP_LEVELS]; + // for each mip for each array slice + Pass m_mip[CS_MAX_MIP_LEVELS] = {}; - ResourceViewHeaps *m_pResourceViewHeaps; + ResourceViewHeaps *m_pResourceViewHeaps = nullptr; - uint32_t m_Width; - uint32_t m_Height; - int m_mipCount; + VkDescriptorSetLayout m_descriptorSetLayout = VK_NULL_HANDLE; - VkDescriptorSetLayout m_descriptorSetLayout; + VkPipelineLayout m_pipelineLayout = VK_NULL_HANDLE; + VkPipeline m_pipeline = VK_NULL_HANDLE; - VkPipelineLayout m_pipelineLayout; - VkPipeline m_pipeline; + VkSampler m_sampler = VK_NULL_HANDLE; - VkSampler m_sampler; + VkImageView m_imGUISRV[CS_MAX_MIP_LEVELS * 6] = {}; }; } diff --git a/sample/src/VK/PSDownsampler.cpp b/sample/src/VK/PSDownsampler.cpp index 12faf02..6640b10 100644 --- a/sample/src/VK/PSDownsampler.cpp +++ b/sample/src/VK/PSDownsampler.cpp @@ -32,18 +32,17 @@ namespace CAULDRON_VK { void PSDownsampler::OnCreate( - Device* pDevice, + Device *pDevice, + UploadHeap *pUploadHeap, ResourceViewHeaps *pResourceViewHeaps, DynamicBufferRing *pConstantBufferRing, - StaticBufferPool *pStaticBufferPool, - VkFormat outFormat + StaticBufferPool *pStaticBufferPool ) { m_pDevice = pDevice; m_pStaticBufferPool = pStaticBufferPool; m_pResourceViewHeaps = pResourceViewHeaps; m_pConstantBufferRing = pConstantBufferRing; - m_outFormat = outFormat; // Create Descriptor Set Layout, the shader needs a uniform dynamic buffer and a texture + sampler // The Descriptor Sets will be created and initialized once we know the input to the shader, that happens in OnCreateWindowSizeDependentResources() @@ -71,9 +70,58 @@ namespace CAULDRON_VK assert(res == VK_SUCCESS); } + m_cubeTexture.InitFromFile(pDevice, pUploadHeap, "..\\media\\envmaps\\papermill\\specular.dds", true, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT); + pUploadHeap->FlushAndFinish(); + // In Render pass // - m_in = SimpleColorWriteRenderPass(pDevice->GetDevice(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + // color RT + VkAttachmentDescription attachments[1]; + attachments[0].format = m_cubeTexture.GetFormat(); + attachments[0].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; // we don't care about the previous contents, this is for a full screen pass with no blending + attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[0].finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + attachments[0].flags = 0; + + VkAttachmentReference color_reference = { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; + + VkSubpassDescription subpass = {}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.flags = 0; + subpass.inputAttachmentCount = 0; + subpass.pInputAttachments = NULL; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &color_reference; + subpass.pResolveAttachments = NULL; + subpass.pDepthStencilAttachment = NULL; + subpass.preserveAttachmentCount = 0; + subpass.pPreserveAttachments = NULL; + + VkSubpassDependency dep = {}; + dep.dependencyFlags = 0; + dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT; + dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + dep.dstSubpass = VK_SUBPASS_EXTERNAL; + dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.srcSubpass = 0; + + VkRenderPassCreateInfo rp_info = {}; + rp_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + rp_info.pNext = NULL; + rp_info.attachmentCount = 1; + rp_info.pAttachments = attachments; + rp_info.subpassCount = 1; + rp_info.pSubpasses = &subpass; + rp_info.dependencyCount = 1; + rp_info.pDependencies = &dep; + + VkResult res = vkCreateRenderPass(pDevice->GetDevice(), &rp_info, NULL, &m_in); + assert(res == VK_SUCCESS); // The sampler we want to use for downsampling, all linear // @@ -95,68 +143,46 @@ namespace CAULDRON_VK // Use helper class to create the fullscreen pass // - m_downscale.OnCreate(pDevice, m_in, "PSDownsampler.glsl", pStaticBufferPool, pConstantBufferRing, m_descriptorSetLayout); + m_downsample.OnCreate(pDevice, m_in, "PSDownsampler.glsl", "main", "", pStaticBufferPool, pConstantBufferRing, m_descriptorSetLayout); // Allocate descriptors for the mip chain // - for (int i = 0; i < DOWNSAMPLEPS_MAX_MIP_LEVELS; i++) + for (int i = 0; i < DOWNSAMPLEPS_MAX_MIP_LEVELS * 6; i++) { - m_pResourceViewHeaps->AllocDescriptor(m_descriptorSetLayout, &m_mip[i].descriptorSet); + m_pResourceViewHeaps->AllocDescriptor(m_descriptorSetLayout, &m_mip[i].m_descriptorSet); } - } - void PSDownsampler::OnCreateWindowSizeDependentResources(uint32_t Width, uint32_t Height, Texture *pInput, int mipCount) - { - m_Width = Width; - m_Height = Height; - m_mipCount = mipCount; - - VkImageCreateInfo image_info = {}; - image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; - image_info.pNext = NULL; - image_info.imageType = VK_IMAGE_TYPE_2D; - image_info.format = m_outFormat; - image_info.extent.width = m_Width >> 1; - image_info.extent.height = m_Height >> 1; - image_info.extent.depth = 1; - image_info.mipLevels = mipCount; - image_info.arrayLayers = 1; - image_info.samples = VK_SAMPLE_COUNT_1_BIT; - image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - image_info.queueFamilyIndexCount = 0; - image_info.pQueueFamilyIndices = NULL; - image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - image_info.usage = (VkImageUsageFlags)(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); //TODO - image_info.flags = 0; - image_info.tiling = VK_IMAGE_TILING_OPTIMAL; - m_result.Init(m_pDevice, &image_info, "DownsampleMip"); - - // Create views for the mip chain - // - for (int i = 0; i < m_mipCount; i++) + for (uint32_t slice = 0; slice < m_cubeTexture.GetArraySize(); slice++) { - // source ----------- - // - if (i == 0) - { - pInput->CreateSRV(&m_mip[i].m_SRV, 0); - } - else + for (uint32_t mip = 0; mip < m_cubeTexture.GetMipCount() - 1; mip++) { - m_result.CreateSRV(&m_mip[i].m_SRV, i - 1); - } - // Create and initialize the Descriptor Sets (all of them use the same Descriptor Layout) - m_pConstantBufferRing->SetDescriptorSet(0, sizeof(DownSamplePS::cbDownscale), m_mip[i].descriptorSet); - SetDescriptorSet(m_pDevice->GetDevice(), 1, m_mip[i].m_SRV, &m_sampler, m_mip[i].descriptorSet); + VkImageViewCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + info.image = m_cubeTexture.Resource(); + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.subresourceRange.baseArrayLayer = slice; + info.subresourceRange.layerCount = 1; + info.format = m_cubeTexture.GetFormat(); + info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + info.subresourceRange.baseMipLevel = mip; + info.subresourceRange.levelCount = 1; + + VkResult res = vkCreateImageView(m_pDevice->GetDevice(), &info, NULL, + &m_mip[slice * m_cubeTexture.GetMipCount() + mip].m_SRV); + assert(res == VK_SUCCESS); - // destination ----------- - // - m_result.CreateRTV(&m_mip[i].RTV, i); + // Create and initialize the Descriptor Sets (all of them use the same Descriptor Layout) + m_pConstantBufferRing->SetDescriptorSet(0, sizeof(DownSamplePS::cbDownscale), m_mip[slice * m_cubeTexture.GetMipCount() + mip].m_descriptorSet); + SetDescriptorSet(m_pDevice->GetDevice(), 1, m_mip[slice * m_cubeTexture.GetMipCount() + mip].m_SRV, &m_sampler, m_mip[slice * m_cubeTexture.GetMipCount() + mip].m_descriptorSet); - // Create framebuffer - { - VkImageView attachments[1] = { m_mip[i].RTV }; + info.subresourceRange.baseMipLevel = mip + 1; + + res = vkCreateImageView(m_pDevice->GetDevice(), &info, NULL, + &m_mip[slice * m_cubeTexture.GetMipCount() + mip].m_RTV); + assert(res == VK_SUCCESS); + + VkImageView attachments[1] = { m_mip[slice * m_cubeTexture.GetMipCount() + mip].m_RTV }; VkFramebufferCreateInfo fb_info = {}; fb_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; @@ -164,37 +190,37 @@ namespace CAULDRON_VK fb_info.renderPass = m_in; fb_info.attachmentCount = 1; fb_info.pAttachments = attachments; - fb_info.width = m_Width >> (i + 1); - fb_info.height = m_Height >> (i + 1); + fb_info.width = m_cubeTexture.GetWidth() >> (mip + 1); + fb_info.height = m_cubeTexture.GetHeight() >> (mip + 1); fb_info.layers = 1; - VkResult res = vkCreateFramebuffer(m_pDevice->GetDevice(), &fb_info, NULL, &m_mip[i].frameBuffer); + res = vkCreateFramebuffer(m_pDevice->GetDevice(), &fb_info, NULL, &m_mip[slice * m_cubeTexture.GetMipCount() + mip].m_frameBuffer); assert(res == VK_SUCCESS); } } } - void PSDownsampler::OnDestroyWindowSizeDependentResources() + void PSDownsampler::OnDestroy() { - for (int i = 0; i < m_mipCount; i++) + for (uint32_t slice = 0; slice < m_cubeTexture.GetArraySize(); slice++) { - vkDestroyImageView(m_pDevice->GetDevice(), m_mip[i].m_SRV, NULL); - vkDestroyImageView(m_pDevice->GetDevice(), m_mip[i].RTV, NULL); - vkDestroyFramebuffer(m_pDevice->GetDevice(), m_mip[i].frameBuffer, NULL); + for (uint32_t i = 0; i < m_cubeTexture.GetMipCount() - 1; i++) + { + vkDestroyImageView(m_pDevice->GetDevice(), m_mip[slice * m_cubeTexture.GetMipCount() + i].m_SRV, NULL); + vkDestroyImageView(m_pDevice->GetDevice(), m_mip[slice * m_cubeTexture.GetMipCount() + i].m_RTV, NULL); + vkDestroyFramebuffer(m_pDevice->GetDevice(), m_mip[slice * m_cubeTexture.GetMipCount() + i].m_frameBuffer, NULL); + } } - m_result.OnDestroy(); - } + m_cubeTexture.OnDestroy(); - void PSDownsampler::OnDestroy() - { - for (int i = 0; i < DOWNSAMPLEPS_MAX_MIP_LEVELS; i++) + for (int i = 0; i < DOWNSAMPLEPS_MAX_MIP_LEVELS * 6; i++) { - m_pResourceViewHeaps->FreeDescriptor(m_mip[i].descriptorSet); + m_pResourceViewHeaps->FreeDescriptor(m_mip[i].m_descriptorSet); } - m_downscale.OnDestroy(); + m_downsample.OnDestroy(); vkDestroyDescriptorSetLayout(m_pDevice->GetDevice(), m_descriptorSetLayout, NULL); - vkDestroySampler(m_pDevice->GetDevice(), m_sampler, nullptr); + vkDestroySampler(m_pDevice->GetDevice(), m_sampler, NULL); vkDestroyRenderPass(m_pDevice->GetDevice(), m_in, NULL); } @@ -205,49 +231,68 @@ namespace CAULDRON_VK // downsample // - for (int i = 0; i < m_mipCount; i++) + for (uint32_t slice = 0; slice < m_cubeTexture.GetArraySize(); slice++) { + for (uint32_t i = 0; i < m_cubeTexture.GetMipCount() - 1; i++) + { - VkRenderPassBeginInfo rp_begin = {}; - rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; - rp_begin.pNext = NULL; - rp_begin.renderPass = m_in; - rp_begin.framebuffer = m_mip[i].frameBuffer; - rp_begin.renderArea.offset.x = 0; - rp_begin.renderArea.offset.y = 0; - rp_begin.renderArea.extent.width = m_Width >> (i + 1); - rp_begin.renderArea.extent.height = m_Height >> (i + 1); - rp_begin.clearValueCount = 0; - rp_begin.pClearValues = NULL; - vkCmdBeginRenderPass(cmd_buf, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); - SetViewportAndScissor(cmd_buf, 0, 0, m_Width >> (i + 1), m_Height >> (i + 1)); - - cbDownscale *data; - VkDescriptorBufferInfo constantBuffer; - m_pConstantBufferRing->AllocConstantBuffer(sizeof(cbDownscale), (void **)&data, &constantBuffer); - data->outWidth = (float)(m_Width >> (i + 1)); - data->outHeight = (float)(m_Height >> (i + 1)); - data->invWidth = 1.0f / (float)(m_Width >> i); - data->invHeight = 1.0f / (float)(m_Height >> i); - - m_downscale.Draw(cmd_buf, constantBuffer, m_mip[i].descriptorSet); - - vkCmdEndRenderPass(cmd_buf); + VkRenderPassBeginInfo rp_begin = {}; + rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rp_begin.pNext = NULL; + rp_begin.renderPass = m_in; + rp_begin.framebuffer = m_mip[slice * m_cubeTexture.GetMipCount() + i].m_frameBuffer; + rp_begin.renderArea.offset.x = 0; + rp_begin.renderArea.offset.y = 0; + rp_begin.renderArea.extent.width = m_cubeTexture.GetWidth() >> (i + 1); + rp_begin.renderArea.extent.height = m_cubeTexture.GetHeight() >> (i + 1); + rp_begin.clearValueCount = 0; + rp_begin.pClearValues = NULL; + vkCmdBeginRenderPass(cmd_buf, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); + SetViewportAndScissor(cmd_buf, 0, 0, m_cubeTexture.GetWidth() >> (i + 1), m_cubeTexture.GetHeight() >> (i + 1)); + + cbDownsample* data; + VkDescriptorBufferInfo constantBuffer; + m_pConstantBufferRing->AllocConstantBuffer(sizeof(cbDownsample), (void**)&data, &constantBuffer); + data->outWidth = (float)(m_cubeTexture.GetWidth() >> (i + 1)); + data->outHeight = (float)(m_cubeTexture.GetHeight() >> (i + 1)); + data->invWidth = 1.0f / (float)(m_cubeTexture.GetWidth() >> i); + data->invHeight = 1.0f / (float)(m_cubeTexture.GetHeight() >> i); + data->slice = slice; + + m_downsample.Draw(cmd_buf, constantBuffer, m_mip[slice * m_cubeTexture.GetMipCount() + i].m_descriptorSet); + + vkCmdEndRenderPass(cmd_buf); + } } SetPerfMarkerEnd(cmd_buf); } - void PSDownsampler::Gui() + void PSDownsampler::GUI(int* pSlice) { bool opened = true; - ImGui::Begin("Downsample", &opened); + std::string header = "Downsample"; + ImGui::Begin(header.c_str(), &opened); - for (int i = 0; i < m_mipCount; i++) + if (ImGui::CollapsingHeader("PS", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Image((ImTextureID)m_mip[i].m_SRV, ImVec2(320, 180)); + const char* sliceItemNames[] = + { + "Slice 0", + "Slice 1", + "Slice 2", + "Slice 3", + "Slice 4", + "Slice 5" + }; + ImGui::Combo("Slice of Cube Texture", pSlice, sliceItemNames, _countof(sliceItemNames)); + + for (uint32_t i = 0; i < m_cubeTexture.GetMipCount(); i++) + { + ImGui::Image((ImTextureID)m_mip[*pSlice * m_cubeTexture.GetMipCount() + i].m_SRV, ImVec2(static_cast(512 >> i), static_cast(512 >> i))); + } } ImGui::End(); } -} +} \ No newline at end of file diff --git a/sample/src/VK/PSDownsampler.h b/sample/src/VK/PSDownsampler.h index c70297b..effdf73 100644 --- a/sample/src/VK/PSDownsampler.h +++ b/sample/src/VK/PSDownsampler.h @@ -29,54 +29,47 @@ namespace CAULDRON_VK class PSDownsampler { public: - void OnCreate(Device* pDevice, ResourceViewHeaps *pResourceViewHeaps, DynamicBufferRing *m_pConstantBufferRing, StaticBufferPool *pStaticBufferPool, VkFormat outFormat); + void OnCreate(Device *pDevice, UploadHeap *pUploadHeap, ResourceViewHeaps *pResourceViewHeaps, DynamicBufferRing *m_pConstantBufferRing, StaticBufferPool *pStaticBufferPool); void OnDestroy(); - void OnCreateWindowSizeDependentResources(uint32_t Width, uint32_t Height, Texture *pInput, int mips); - void OnDestroyWindowSizeDependentResources(); - void Draw(VkCommandBuffer cmd_buf); - Texture *GetTexture() { return &m_result; } - VkImageView GetTextureView(int i) { return m_mip[i].m_SRV; } - void Gui(); + Texture *GetTexture() { return &m_cubeTexture; } + void GUI(int *pSlice); - struct cbDownscale + struct cbDownsample { float outWidth, outHeight; float invWidth, invHeight; + uint32_t slice; + uint32_t padding[3]; }; private: - Device *m_pDevice; - VkFormat m_outFormat; + Device *m_pDevice = nullptr; - Texture m_result; + Texture m_cubeTexture; struct Pass { - VkImageView RTV; //dest VkImageView m_SRV; //src - VkFramebuffer frameBuffer; - VkDescriptorSet descriptorSet; + VkImageView m_RTV; //dst + VkFramebuffer m_frameBuffer; + VkDescriptorSet m_descriptorSet; }; - Pass m_mip[PS_MAX_MIP_LEVELS]; - - StaticBufferPool *m_pStaticBufferPool; - ResourceViewHeaps *m_pResourceViewHeaps; - DynamicBufferRing *m_pConstantBufferRing; + Pass m_mip[PS_MAX_MIP_LEVELS * 6] = {}; - uint32_t m_Width; - uint32_t m_Height; - int m_mipCount; + StaticBufferPool *m_pStaticBufferPool = nullptr; + ResourceViewHeaps *m_pResourceViewHeaps = nullptr; + DynamicBufferRing *m_pConstantBufferRing = nullptr; - VkDescriptorSetLayout m_descriptorSetLayout; + VkDescriptorSetLayout m_descriptorSetLayout = VK_NULL_HANDLE; - PostProcPS m_downscale; + PostProcPS m_downsample; - VkRenderPass m_in; + VkRenderPass m_in = VK_NULL_HANDLE; - VkSampler m_sampler; + VkSampler m_sampler = VK_NULL_HANDLE; }; } diff --git a/sample/src/VK/SPDCS.cpp b/sample/src/VK/SPDCS.cpp new file mode 100644 index 0000000..24ab345 --- /dev/null +++ b/sample/src/VK/SPDCS.cpp @@ -0,0 +1,631 @@ +// SPDSample +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include "stdafx.h" +#include "Base\Device.h" +#include "Base\ShaderCompilerHelper.h" +#include "Base\ExtDebugMarkers.h" +#include "Base\Imgui.h" + +#include "SPDCS.h" + +#define A_CPU +#include "ffx_a.h" +#include "ffx_spd.h" + +namespace CAULDRON_VK +{ + void SPDCS::OnCreate( + Device *pDevice, + UploadHeap *pUploadHeap, + ResourceViewHeaps *pResourceViewHeaps, + SPDLoad spdLoad, + SPDWaveOps spdWaveOps, + SPDPacked spdPacked + ) + { + m_pDevice = pDevice; + m_pResourceViewHeaps = pResourceViewHeaps; + + m_spdLoad = spdLoad; + m_spdWaveOps = spdWaveOps; + m_spdPacked = spdPacked; + + uint32_t bindingCount = 3; + + // create the descriptor set layout + // the shader needs + // image: source image + destination mips + // global atomic counter: storage buffer + { + VkDescriptorSetLayoutBinding layoutBindings[5]; + layoutBindings[0].binding = 0; + layoutBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + layoutBindings[0].descriptorCount = SPD_MAX_MIP_LEVELS + 1; + layoutBindings[0].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + layoutBindings[0].pImmutableSamplers = NULL; + + layoutBindings[1].binding = 1; + layoutBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + layoutBindings[1].descriptorCount = 1; + layoutBindings[1].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + layoutBindings[1].pImmutableSamplers = NULL; + + layoutBindings[2].binding = 2; + layoutBindings[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + layoutBindings[2].descriptorCount = 1; + layoutBindings[2].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + layoutBindings[2].pImmutableSamplers = NULL; + + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + bindingCount = 5; + + // bind source texture as sampled image and sampler + layoutBindings[3].binding = 3; + layoutBindings[3].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + layoutBindings[3].descriptorCount = 1; + layoutBindings[3].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + layoutBindings[3].pImmutableSamplers = NULL; + + layoutBindings[4].binding = 4; + layoutBindings[4].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER; + layoutBindings[4].descriptorCount = 1; + layoutBindings[4].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + layoutBindings[4].pImmutableSamplers = NULL; + } + + VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlagsInfo = {}; + bindingFlagsInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO; + bindingFlagsInfo.bindingCount = bindingCount; + std::vector bindingFlags(bindingCount); + bindingFlags[0] = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT; + bindingFlagsInfo.pBindingFlags = bindingFlags.data(); + + VkDescriptorSetLayoutCreateInfo descriptor_layout = {}; + descriptor_layout.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + descriptor_layout.pNext = &bindingFlagsInfo; + descriptor_layout.bindingCount = bindingCount; + descriptor_layout.pBindings = layoutBindings; + + VkResult res = vkCreateDescriptorSetLayout(pDevice->GetDevice(), &descriptor_layout, NULL, &m_descriptorSetLayout); + assert(res == VK_SUCCESS); + } + + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + // The sampler we want to use, needs to match the SPD Reduction function in the shader + // linear sampler: + // -> AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3){return (v0+v1+v2+v3)*0.25;} + // point sampler: + // -> AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3){return v3;} + { + VkSamplerCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + info.magFilter = VK_FILTER_LINEAR; + info.minFilter = VK_FILTER_LINEAR; + info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + info.minLod = -1000; + info.maxLod = 1000; + info.maxAnisotropy = 1.0f; + VkResult res = vkCreateSampler(pDevice->GetDevice(), &info, NULL, &m_sampler); + assert(res == VK_SUCCESS); + } + } + + m_cubeTexture.InitFromFile(pDevice, pUploadHeap, "..\\media\\envmaps\\papermill\\specular.dds", true, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT); + pUploadHeap->FlushAndFinish(); + + // Create global atomic counter + { + VkBufferCreateInfo bufferInfo = {}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.flags = 0; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + bufferInfo.queueFamilyIndexCount = 0; + bufferInfo.pQueueFamilyIndices = NULL; + bufferInfo.size = sizeof(int) * m_cubeTexture.GetArraySize(); // number of slices + bufferInfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + + VmaAllocationCreateInfo bufferAllocCreateInfo = {}; + bufferAllocCreateInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + bufferAllocCreateInfo.flags = VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT; + bufferAllocCreateInfo.pUserData = "SpdGlobalAtomicCounter"; + VmaAllocationInfo bufferAllocInfo = {}; + vmaCreateBuffer(m_pDevice->GetAllocator(), &bufferInfo, &bufferAllocCreateInfo, &m_globalCounter, + &m_globalCounterAllocation, &bufferAllocInfo); + + // initialize global atomic counter to 0 + uint32_t pCounter[6]; // one counter per slice + vmaMapMemory(m_pDevice->GetAllocator(), m_globalCounterAllocation, (void**)&pCounter); + for (uint32_t i = 0; i < m_cubeTexture.GetArraySize(); i++) + { + pCounter[i] = 0; + } + vmaUnmapMemory(m_pDevice->GetAllocator(), m_globalCounterAllocation); + } + + VkPipelineShaderStageCreateInfo computeShader; + DefineList defines; + + if (m_spdWaveOps == SPDWaveOps::SPDNoWaveOps) { + defines["SPD_NO_WAVE_OPERATIONS"] = 1; + } + if (m_spdPacked == SPDPacked::SPDPacked) { + defines["A_HALF"] = 1; + defines["SPD_PACKED_ONLY"] = 1; + } + + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + VkResult res = VKCompileFromFile(m_pDevice->GetDevice(), VK_SHADER_STAGE_COMPUTE_BIT, + "SPDIntegrationLinearSampler.hlsl", "main", "-T cs_6_0", &defines, &computeShader); + assert(res == VK_SUCCESS); + } + else { + VkResult res = VKCompileFromFile(m_pDevice->GetDevice(), VK_SHADER_STAGE_COMPUTE_BIT, + "SPDIntegration.hlsl", "main", "-T cs_6_0", &defines, &computeShader); + assert(res == VK_SUCCESS); + } + + // Create pipeline layout + // + VkPipelineLayoutCreateInfo pPipelineLayoutCreateInfo = {}; + pPipelineLayoutCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + pPipelineLayoutCreateInfo.pNext = NULL; + + // push constants + VkPushConstantRange pushConstantRange = {}; + pushConstantRange.offset = 0; + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + pushConstantRange.size = sizeof(SpdLinearSamplerConstants); + } + else { + pushConstantRange.size = sizeof(SpdConstants); + } + pushConstantRange.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + pPipelineLayoutCreateInfo.pushConstantRangeCount = 1; + pPipelineLayoutCreateInfo.pPushConstantRanges = &pushConstantRange; + + pPipelineLayoutCreateInfo.setLayoutCount = 1; + pPipelineLayoutCreateInfo.pSetLayouts = &m_descriptorSetLayout; + + VkResult res = vkCreatePipelineLayout(pDevice->GetDevice(), &pPipelineLayoutCreateInfo, NULL, &m_pipelineLayout); + assert(res == VK_SUCCESS); + + // Create pipeline + // + VkComputePipelineCreateInfo pipeline = {}; + pipeline.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; + pipeline.pNext = NULL; + pipeline.flags = 0; + pipeline.layout = m_pipelineLayout; + pipeline.stage = computeShader; + pipeline.basePipelineHandle = VK_NULL_HANDLE; + pipeline.basePipelineIndex = 0; + + res = vkCreateComputePipelines(pDevice->GetDevice(), pDevice->GetPipelineCache(), 1, &pipeline, NULL, &m_pipeline); + assert(res == VK_SUCCESS); + + m_pResourceViewHeaps->AllocDescriptor(m_descriptorSetLayout, &m_descriptorSet); + + // Create and initialize descriptor set for storage image + // std::vector desc_storage_images(SPD_MAX_MIP_LEVELS + 1); + + uint32_t numUAVs = m_cubeTexture.GetMipCount(); + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + // we need one UAV less because source texture will be bound as SRV and not as UAV + numUAVs = m_cubeTexture.GetMipCount() - 1; + } + + std::vector desc_storage_images(numUAVs); + for (uint32_t i = 0; i < numUAVs; i++) + { + // destination ----------- + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + // first UAV is MIP 1 + m_cubeTexture.CreateRTV(&m_UAV[i], i + 1); + } + else { + // first UAV is source texture, MIP 0 + m_cubeTexture.CreateRTV(&m_UAV[i], i); + } + + desc_storage_images[i] = {}; + desc_storage_images[i].sampler = VK_NULL_HANDLE; + desc_storage_images[i].imageView = m_UAV[i]; + desc_storage_images[i].imageLayout = VK_IMAGE_LAYOUT_GENERAL; + } + + // update descriptors + // SPD Load version + if (m_spdLoad == SPDLoad::SPDLoad) + { + VkWriteDescriptorSet writes[3]; + writes[0] = {}; + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].pNext = NULL; + writes[0].dstSet = m_descriptorSet; + writes[0].descriptorCount = numUAVs; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + writes[0].pImageInfo = desc_storage_images.data(); + writes[0].dstBinding = 0; + writes[0].dstArrayElement = 0; + + writes[1] = {}; + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].pNext = NULL; + writes[1].dstSet = m_descriptorSet; + writes[1].descriptorCount = 1; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + writes[1].pImageInfo = &desc_storage_images[6]; + writes[1].dstBinding = 1; + writes[1].dstArrayElement = 0; + + VkDescriptorBufferInfo desc_buffer = {}; + desc_buffer.buffer = m_globalCounter; + desc_buffer.offset = 0; + desc_buffer.range = sizeof(int) * m_cubeTexture.GetArraySize(); // number of slices + + writes[2] = {}; + writes[2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[2].pNext = NULL; + writes[2].dstSet = m_descriptorSet; + writes[2].descriptorCount = 1; + writes[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + writes[2].pBufferInfo = &desc_buffer; + writes[2].dstBinding = 2; + writes[2].dstArrayElement = 0; + + vkUpdateDescriptorSets(m_pDevice->GetDevice(), 3, writes, 0, NULL); + } + + // update descriptors + // SPD Linear Sampler version + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + VkWriteDescriptorSet writes[5]; + writes[0] = {}; + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].pNext = NULL; + writes[0].dstSet = m_descriptorSet; + writes[0].descriptorCount = numUAVs; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + writes[0].pImageInfo = desc_storage_images.data(); + writes[0].dstBinding = 0; + writes[0].dstArrayElement = 0; + + writes[1] = {}; + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].pNext = NULL; + writes[1].dstSet = m_descriptorSet; + writes[1].descriptorCount = 1; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + writes[1].pImageInfo = &desc_storage_images[5]; + writes[1].dstBinding = 1; + writes[1].dstArrayElement = 0; + + VkDescriptorBufferInfo desc_buffer = {}; + desc_buffer.buffer = m_globalCounter; + desc_buffer.offset = 0; + desc_buffer.range = sizeof(int) * m_cubeTexture.GetArraySize(); // number of slices + + writes[2] = {}; + writes[2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[2].pNext = NULL; + writes[2].dstSet = m_descriptorSet; + writes[2].descriptorCount = 1; + writes[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + writes[2].pBufferInfo = &desc_buffer; + writes[2].dstBinding = 2; + writes[2].dstArrayElement = 0; + + m_cubeTexture.CreateSRV(&m_sourceSRV, 0); + + VkDescriptorImageInfo desc_sampled_image = {}; + desc_sampled_image.sampler = VK_NULL_HANDLE; + desc_sampled_image.imageView = m_sourceSRV; + desc_sampled_image.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + writes[3] = {}; + writes[3].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[3].pNext = NULL; + writes[3].dstSet = m_descriptorSet; + writes[3].descriptorCount = 1; + writes[3].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + writes[3].pImageInfo = &desc_sampled_image; + writes[3].dstBinding = 3; + writes[3].dstArrayElement = 0; + + // Create and initialize descriptor set for sampler + VkDescriptorImageInfo desc_sampler = {}; + desc_sampler.sampler = m_sampler; + + writes[4] = {}; + writes[4].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[4].pNext = NULL; + writes[4].dstSet = m_descriptorSet; + writes[4].descriptorCount = 1; + writes[4].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER; + writes[4].pImageInfo = &desc_sampler; + writes[4].dstBinding = 4; + writes[4].dstArrayElement = 0; + + vkUpdateDescriptorSets(m_pDevice->GetDevice(), 5, writes, 0, NULL); + } + + for (uint32_t slice = 0; slice < m_cubeTexture.GetArraySize(); slice++) + { + for (uint32_t mip = 0; mip < m_cubeTexture.GetMipCount(); mip++) + { + VkImageViewUsageCreateInfo imageViewUsageInfo = {}; + imageViewUsageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO; + imageViewUsageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT; + + VkImageViewCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + info.pNext = &imageViewUsageInfo; + info.image = m_cubeTexture.Resource(); + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.subresourceRange.baseArrayLayer = slice; + info.subresourceRange.layerCount = 1; + + switch (m_cubeTexture.GetFormat()) + { + case VK_FORMAT_B8G8R8A8_UNORM: info.format = VK_FORMAT_B8G8R8A8_SRGB; break; + case VK_FORMAT_R8G8B8A8_UNORM: info.format = VK_FORMAT_R8G8B8A8_SRGB; break; + case VK_FORMAT_BC1_RGB_UNORM_BLOCK: info.format = VK_FORMAT_BC1_RGB_SRGB_BLOCK; break; + case VK_FORMAT_BC2_UNORM_BLOCK: info.format = VK_FORMAT_BC2_SRGB_BLOCK; break; + case VK_FORMAT_BC3_UNORM_BLOCK: info.format = VK_FORMAT_BC3_SRGB_BLOCK; break; + default: info.format = m_cubeTexture.GetFormat(); + } + + info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + info.subresourceRange.baseMipLevel = mip; + info.subresourceRange.levelCount = 1; + + VkResult res = vkCreateImageView(m_pDevice->GetDevice(), &info, NULL, + &m_SRV[slice * m_cubeTexture.GetMipCount() + mip]); + assert(res == VK_SUCCESS); + } + } + } + + void SPDCS::OnDestroy() + { + for (uint32_t i = 0; i < m_cubeTexture.GetMipCount() * m_cubeTexture.GetArraySize(); i++) + { + vkDestroyImageView(m_pDevice->GetDevice(), m_SRV[i], NULL); + } + + uint32_t numUAVs = m_cubeTexture.GetMipCount(); + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + // we needed one UAV less because source texture is bound as SRV and not as UAV + numUAVs = m_cubeTexture.GetMipCount() - 1; + + // also destroy SRV and sampler + vkDestroyImageView(m_pDevice->GetDevice(), m_sourceSRV, NULL); + vkDestroySampler(m_pDevice->GetDevice(), m_sampler, NULL); + } + + for (uint32_t i = 0; i < numUAVs; i++) + { + vkDestroyImageView(m_pDevice->GetDevice(), m_UAV[i], NULL); + } + + m_cubeTexture.OnDestroy(); + + m_pResourceViewHeaps->FreeDescriptor(m_descriptorSet); + + vmaDestroyBuffer(m_pDevice->GetAllocator(), m_globalCounter, m_globalCounterAllocation); + + vkDestroyPipeline(m_pDevice->GetDevice(), m_pipeline, nullptr); + vkDestroyPipelineLayout(m_pDevice->GetDevice(), m_pipelineLayout, nullptr); + vkDestroyDescriptorSetLayout(m_pDevice->GetDevice(), m_descriptorSetLayout, NULL); + } + + void SPDCS::Draw(VkCommandBuffer cmd_buf) + { + // downsample + // + varAU2(dispatchThreadGroupCountXY); + varAU2(workGroupOffset); // needed if Left and Top are not 0,0 + varAU2(numWorkGroupsAndMips); + varAU4(rectInfo) = initAU4(0, 0, m_cubeTexture.GetWidth(), m_cubeTexture.GetHeight()); // left, top, width, height + SpdSetup(dispatchThreadGroupCountXY, workGroupOffset, numWorkGroupsAndMips, rectInfo); + + VkImageMemoryBarrier imageMemoryBarrier[2]; + + uint32_t numBarriers = 1; + imageMemoryBarrier[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + imageMemoryBarrier[0].pNext = NULL; + imageMemoryBarrier[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; + imageMemoryBarrier[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; + imageMemoryBarrier[0].oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imageMemoryBarrier[0].newLayout = VK_IMAGE_LAYOUT_GENERAL; + imageMemoryBarrier[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + imageMemoryBarrier[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + imageMemoryBarrier[0].subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + imageMemoryBarrier[0].subresourceRange.baseMipLevel = 1; + imageMemoryBarrier[0].subresourceRange.levelCount = m_cubeTexture.GetMipCount() - 1; + } + else { + imageMemoryBarrier[0].subresourceRange.baseMipLevel = 0; + imageMemoryBarrier[0].subresourceRange.levelCount = m_cubeTexture.GetMipCount(); + } + imageMemoryBarrier[0].subresourceRange.baseArrayLayer = 0; + imageMemoryBarrier[0].subresourceRange.layerCount = m_cubeTexture.GetArraySize(); + imageMemoryBarrier[0].image = m_cubeTexture.Resource(); + + if (m_spdLoad == SPDLoad::SPDLinearSampler) { + numBarriers = 2; + imageMemoryBarrier[1].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + imageMemoryBarrier[1].pNext = NULL; + imageMemoryBarrier[1].srcAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; + imageMemoryBarrier[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; + imageMemoryBarrier[1].oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imageMemoryBarrier[1].newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + imageMemoryBarrier[1].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + imageMemoryBarrier[1].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + imageMemoryBarrier[1].subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + imageMemoryBarrier[1].subresourceRange.baseMipLevel = 0; + imageMemoryBarrier[1].subresourceRange.levelCount = 1; + imageMemoryBarrier[1].subresourceRange.baseArrayLayer = 0; + imageMemoryBarrier[1].subresourceRange.layerCount = m_cubeTexture.GetArraySize(); + imageMemoryBarrier[1].image = m_cubeTexture.Resource(); + } + + // transition general layout + vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, numBarriers, imageMemoryBarrier); + + SetPerfMarkerBegin(cmd_buf, "SPDCS"); + + // Bind Pipeline + // + vkCmdBindPipeline(cmd_buf, VK_PIPELINE_BIND_POINT_COMPUTE, m_pipeline); + + // should be / 64 + uint32_t dispatchX = dispatchThreadGroupCountXY[0]; + uint32_t dispatchY = dispatchThreadGroupCountXY[1]; + uint32_t dispatchZ = m_cubeTexture.GetArraySize(); // slices + + // single pass for storage buffer? + //uint32_t uniformOffsets[1] = { (uint32_t)constantBuffer.offset }; + vkCmdBindDescriptorSets(cmd_buf, VK_PIPELINE_BIND_POINT_COMPUTE, m_pipelineLayout, 0, 1, &m_descriptorSet, 0, nullptr); + + // Bind push constants + // + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + SpdLinearSamplerConstants data; + data.numWorkGroupsPerSlice = numWorkGroupsAndMips[0]; + data.mips = numWorkGroupsAndMips[1]; + data.workGroupOffset[0] = workGroupOffset[0]; + data.workGroupOffset[1] = workGroupOffset[1]; + data.invInputSize[0] = 1.0f / m_cubeTexture.GetWidth(); + data.invInputSize[1] = 1.0f / m_cubeTexture.GetHeight(); + vkCmdPushConstants(cmd_buf, m_pipelineLayout, + VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(SpdLinearSamplerConstants), (void*)&data); + } + else { + SpdConstants data; + data.numWorkGroupsPerSlice = numWorkGroupsAndMips[0]; + data.mips = numWorkGroupsAndMips[1]; + data.workGroupOffset[0] = workGroupOffset[0]; + data.workGroupOffset[1] = workGroupOffset[1]; + vkCmdPushConstants(cmd_buf, m_pipelineLayout, + VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(SpdConstants), (void*)&data); + } + + // Draw + // + vkCmdDispatch(cmd_buf, dispatchX, dispatchY, dispatchZ); + + imageMemoryBarrier[0] = {}; + imageMemoryBarrier[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + imageMemoryBarrier[0].pNext = NULL; + imageMemoryBarrier[0].srcAccessMask = 0; + imageMemoryBarrier[0].dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT; + imageMemoryBarrier[0].oldLayout = VK_IMAGE_LAYOUT_GENERAL; + imageMemoryBarrier[0].newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + imageMemoryBarrier[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + imageMemoryBarrier[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + imageMemoryBarrier[0].subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + if (m_spdLoad == SPDLoad::SPDLinearSampler) + { + imageMemoryBarrier[0].subresourceRange.baseMipLevel = 1; + imageMemoryBarrier[0].subresourceRange.levelCount = m_cubeTexture.GetMipCount() - 1; + } + else { + imageMemoryBarrier[0].subresourceRange.baseMipLevel = 0; + imageMemoryBarrier[0].subresourceRange.levelCount = m_cubeTexture.GetMipCount(); + } + imageMemoryBarrier[0].subresourceRange.baseArrayLayer = 0; + imageMemoryBarrier[0].subresourceRange.layerCount = m_cubeTexture.GetArraySize(); + imageMemoryBarrier[0].image = m_cubeTexture.Resource(); + + // transition general layout if detination image to shader read only for source image + vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, imageMemoryBarrier); + + SetPerfMarkerEnd(cmd_buf); + } + + void SPDCS::GUI(int* pSlice) + { + bool opened = true; + std::string header = "Downsample"; + ImGui::Begin(header.c_str(), &opened); + + std::string downsampleHeader = "SPD CS"; + if (m_spdLoad == SPDLoad::SPDLoad) { + downsampleHeader += " Load"; + } + else { + downsampleHeader += " Linear Sampler"; + } + + if (m_spdWaveOps == SPDWaveOps::SPDWaveOps) + { + downsampleHeader += " WaveOps"; + } + else { + downsampleHeader += " No WaveOps"; + } + + if (m_spdPacked == SPDPacked::SPDNonPacked) + { + downsampleHeader += " Non Packed"; + } + else { + downsampleHeader += " Packed"; + } + + if (ImGui::CollapsingHeader(downsampleHeader.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) + { + const char* sliceItemNames[] = + { + "Slice 0", + "Slice 1", + "Slice 2", + "Slice 3", + "Slice 4", + "Slice 5" + }; + ImGui::Combo("Slice of Cube Texture", pSlice, sliceItemNames, _countof(sliceItemNames)); + + for (uint32_t i = 0; i < m_cubeTexture.GetMipCount(); i++) + { + ImGui::Image((ImTextureID)m_SRV[*pSlice * m_cubeTexture.GetMipCount() + i], ImVec2(static_cast(512 >> i), static_cast(512 >> i))); + } + } + + ImGui::End(); + } +} \ No newline at end of file diff --git a/sample/src/VK/SPDCS.h b/sample/src/VK/SPDCS.h new file mode 100644 index 0000000..2420f14 --- /dev/null +++ b/sample/src/VK/SPDCS.h @@ -0,0 +1,100 @@ +// SPDSample +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +#pragma once + +#include "Base/StaticBufferPool.h" +#include "Base/Texture.h" +#include "Base/DynamicBufferRing.h" + +namespace CAULDRON_VK +{ +#define SPD_MAX_MIP_LEVELS 12 + + enum class SPDWaveOps + { + SPDNoWaveOps, + SPDWaveOps, + }; + + enum class SPDPacked + { + SPDNonPacked, + SPDPacked, + }; + + enum class SPDLoad + { + SPDLoad, + SPDLinearSampler, + }; + + class SPDCS + { + public: + void OnCreate(Device *pDevice, UploadHeap *pUploadHeap, ResourceViewHeaps *pResourceViewHeaps, + SPDLoad spdLoad, SPDWaveOps spdWaveOps, SPDPacked spdPacked); + void OnDestroy(); + + void Draw(VkCommandBuffer cmd_buf); + Texture *GetTexture() { return &m_cubeTexture; } + void GUI(int* pSlice); + + struct SpdConstants + { + int mips; + int numWorkGroupsPerSlice; + int workGroupOffset[2]; + }; + + struct SpdLinearSamplerConstants + { + int mips; + int numWorkGroupsPerSlice; + int workGroupOffset[2]; + float invInputSize[2]; + float padding[2]; + }; + + private: + Device *m_pDevice = nullptr; + + Texture m_cubeTexture; + + VkImageView m_UAV[SPD_MAX_MIP_LEVELS + 1] = {}; // source + destinations (mips) + VkImageView m_SRV[SPD_MAX_MIP_LEVELS * 6] = {}; // for display of MIPS using imGUI + VkImageView m_sourceSRV = VK_NULL_HANDLE; // source when linear sampler is used + VkSampler m_sampler = VK_NULL_HANDLE; // linear sampler + VkDescriptorSet m_descriptorSet = VK_NULL_HANDLE; + + ResourceViewHeaps *m_pResourceViewHeaps = nullptr; + DynamicBufferRing *m_pConstantBufferRing = nullptr; + + VkDescriptorSetLayout m_descriptorSetLayout = VK_NULL_HANDLE; + + VkPipelineLayout m_pipelineLayout = VK_NULL_HANDLE; + VkPipeline m_pipeline = VK_NULL_HANDLE; + + VkBuffer m_globalCounter = VK_NULL_HANDLE; + VmaAllocation m_globalCounterAllocation; + + SPDLoad m_spdLoad; + SPDWaveOps m_spdWaveOps; + SPDPacked m_spdPacked; + }; +} \ No newline at end of file diff --git a/sample/src/VK/SPD_Integration_Linear_Sampler.glsl b/sample/src/VK/SPDIntegration.glsl similarity index 50% rename from sample/src/VK/SPD_Integration_Linear_Sampler.glsl rename to sample/src/VK/SPDIntegration.glsl index 379c7fa..7768032 100644 --- a/sample/src/VK/SPD_Integration_Linear_Sampler.glsl +++ b/sample/src/VK/SPDIntegration.glsl @@ -29,93 +29,142 @@ layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; //-------------------------------------------------------------------------------------- // Push Constants //-------------------------------------------------------------------------------------- -layout(push_constant) uniform pushConstants { +layout(push_constant) uniform SpdConstants +{ uint mips; uint numWorkGroups; - // [SAMPLER] - vec2 invInputSize; + ivec2 workGroupOffset; } spdConstants; //-------------------------------------------------------------------------------------- // Texture definitions //-------------------------------------------------------------------------------------- -//layout(set=0, binding=0, rgba16f) uniform image2D imgSrc; -// [SAMPLER] image2D -> texture2D -layout(set=0, binding=0) uniform texture2D imgSrc; -layout(set=0, binding=1, rgba16f) coherent uniform image2D imgDst[12]; -// [SAMPLER] add sampler -layout(set=0, binding=3) uniform sampler srcSampler; +layout(set=0, binding=0, rgba16f) uniform image2DArray imgDst[13]; // don't access mip [6] +layout(set=0, binding=1, rgba16f) coherent uniform image2DArray imgDst6; + //-------------------------------------------------------------------------------------- // Buffer definitions - global atomic counter //-------------------------------------------------------------------------------------- -layout(std430, binding=2) coherent buffer globalAtomicBuffer +layout(std430, binding=2) coherent buffer spdGlobalAtomicBuffer { - uint counter; -} globalAtomic; + uint counter[6]; +} spdGlobalAtomic; #define A_GPU #define A_GLSL #include "ffx_a.h" -shared AU1 spd_counter; +shared AU1 spdCounter; // define fetch and store functions Non-Packed #ifndef SPD_PACKED_ONLY -shared AF1 spd_intermediateR[16][16]; -shared AF1 spd_intermediateG[16][16]; -shared AF1 spd_intermediateB[16][16]; -shared AF1 spd_intermediateA[16][16]; -//AF4 SPDLoadSourceImage(ASU2 p){return imageLoad(imgSrc, p);} -// [SAMPLER] use sampler for accessing source image -AF4 SpdLoadSourceImage(ASU2 p){ - AF2 textureCoord = p * spdConstants.invInputSize + spdConstants.invInputSize; - return texture(sampler2D(imgSrc, srcSampler), textureCoord); +shared AF1 spdIntermediateR[16][16]; +shared AF1 spdIntermediateG[16][16]; +shared AF1 spdIntermediateB[16][16]; +shared AF1 spdIntermediateA[16][16]; + +AF4 SpdLoadSourceImage(ASU2 p, AU1 slice) +{ + return imageLoad(imgDst[0], ivec3(p,slice)); +} +AF4 SpdLoad(ASU2 p, AU1 slice) +{ + return imageLoad(imgDst6,ivec3(p,slice)); +} +void SpdStore(ASU2 p, AF4 value, AU1 mip, AU1 slice) +{ + if (mip == 5) + { + imageStore(imgDst6, ivec3(p,slice), value); + return; } -AF4 SpdLoad(ASU2 p){return imageLoad(imgDst[5],p);} -void SpdStore(ASU2 p, AF4 value, AU1 mip){imageStore(imgDst[mip], p, value);} -void SpdIncreaseAtomicCounter(){spd_counter = atomicAdd(globalAtomic.counter, 1);} -AU1 SpdGetAtomicCounter(){return spd_counter;} -AF4 SpdLoadIntermediate(AU1 x, AU1 y){ + imageStore(imgDst[mip+1], ivec3(p,slice), value); +} +void SpdIncreaseAtomicCounter(AU1 slice) +{ + spdCounter = atomicAdd(spdGlobalAtomic.counter[slice], 1); +} +AU1 SpdGetAtomicCounter() +{ + return spdCounter; +} +void SpdResetAtomicCounter(AU1 slice) +{ + spdGlobalAtomic.counter[slice] = 0; +} +AF4 SpdLoadIntermediate(AU1 x, AU1 y) +{ return AF4( - spd_intermediateR[x][y], - spd_intermediateG[x][y], - spd_intermediateB[x][y], - spd_intermediateA[x][y]);} -void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value){ - spd_intermediateR[x][y] = value.x; - spd_intermediateG[x][y] = value.y; - spd_intermediateB[x][y] = value.z; - spd_intermediateA[x][y] = value.w;} -AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3){return (v0+v1+v2+v3)*0.25;} + spdIntermediateR[x][y], + spdIntermediateG[x][y], + spdIntermediateB[x][y], + spdIntermediateA[x][y]); +} +void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value) +{ + spdIntermediateR[x][y] = value.x; + spdIntermediateG[x][y] = value.y; + spdIntermediateB[x][y] = value.z; + spdIntermediateA[x][y] = value.w; +} +AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3) +{ + return (v0+v1+v2+v3)*0.25; +} #endif // define fetch and store functions Packed #ifdef A_HALF -shared AH2 spd_intermediateRG[16][16]; -shared AH2 spd_intermediateBA[16][16]; -AH4 SpdLoadSourceImageH(ASU2 p){ - AF2 textureCoord = p * spdConstants.invInputSize + spdConstants.invInputSize; - return AH4(texture(sampler2D(imgSrc, srcSampler), textureCoord)); +shared AH2 spdIntermediateRG[16][16]; +shared AH2 spdIntermediateBA[16][16]; + +AH4 SpdLoadSourceImageH(ASU2 p, AU1 slice) +{ + return AH4(imageLoad(imgDst[0], ivec3(p,slice))); +} +AH4 SpdLoadH(ASU2 p, AU1 slice) +{ + return AH4(imageLoad(imgDst6, ivec3(p,slice))); +} +void SpdStoreH(ASU2 p, AH4 value, AU1 mip, AU1 slice) +{ + if (mip == 5) + { + imageStore(imgDst6, ivec3(p,slice), AF4(value)); + return; } -AH4 SpdLoadH(ASU2 p){return AH4(imageLoad(imgDst[5],p));} -void SpdStoreH(ASU2 p, AH4 value, AU1 mip){imageStore(imgDst[mip], p, AF4(value));} -void SpdIncreaseAtomicCounter(){spd_counter = atomicAdd(globalAtomic.counter, 1);} -AU1 SpdGetAtomicCounter(){return spd_counter;} -AH4 SpdLoadIntermediateH(AU1 x, AU1 y){ + imageStore(imgDst[mip+1], ivec3(p,slice), AF4(value)); +} +void SpdIncreaseAtomicCounter(AU1 slice) +{ + spdCounter = atomicAdd(spdGlobalAtomic.counter[slice], 1); +} +AU1 SpdGetAtomicCounter() +{ + return spdCounter; +} +void SpdResetAtomicCounter(AU1 slice) +{ + spdGlobalAtomic.counter[slice] = 0; +} +AH4 SpdLoadIntermediateH(AU1 x, AU1 y) +{ return AH4( - spd_intermediateRG[x][y].x, - spd_intermediateRG[x][y].y, - spd_intermediateBA[x][y].x, - spd_intermediateBA[x][y].y);} + spdIntermediateRG[x][y].x, + spdIntermediateRG[x][y].y, + spdIntermediateBA[x][y].x, + spdIntermediateBA[x][y].y);} void SpdStoreIntermediateH(AU1 x, AU1 y, AH4 value){ - spd_intermediateRG[x][y] = value.xy; - spd_intermediateBA[x][y] = value.zw;} -AH4 SpdReduce4H(AH4 v0, AH4 v1, AH4 v2, AH4 v3){return (v0+v1+v2+v3)*AH1(0.25f);} + spdIntermediateRG[x][y] = value.xy; + spdIntermediateBA[x][y] = value.zw; +} +AH4 SpdReduce4H(AH4 v0, AH4 v1, AH4 v2, AH4 v3) +{ + return (v0+v1+v2+v3)*AH1(0.25f); +} #endif -#define SPD_LINEAR_SAMPLER - #include "ffx_spd.h" // Main function @@ -128,12 +177,16 @@ void main() AU2(gl_WorkGroupID.xy), AU1(gl_LocalInvocationIndex), AU1(spdConstants.mips), - AU1(spdConstants.numWorkGroups)); + AU1(spdConstants.numWorkGroups), + AU1(gl_WorkGroupID.z), + AU2(spdConstants.workGroupOffset)); #else SpdDownsampleH( AU2(gl_WorkGroupID.xy), AU1(gl_LocalInvocationIndex), AU1(spdConstants.mips), - AU1(spdConstants.numWorkGroups)); + AU1(spdConstants.numWorkGroups), + AU1(gl_WorkGroupID.z), + AU2(spdConstants.workGroupOffset)); #endif } \ No newline at end of file diff --git a/sample/src/VK/SPDIntegration.hlsl b/sample/src/VK/SPDIntegration.hlsl new file mode 100644 index 0000000..543dc94 --- /dev/null +++ b/sample/src/VK/SPDIntegration.hlsl @@ -0,0 +1,189 @@ +// SPDSample +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +//-------------------------------------------------------------------------------------- +// Push Constants +//-------------------------------------------------------------------------------------- +struct SpdConstants +{ + uint mips; + uint numWorkGroups; + uint2 workGroupOffset; +}; + +[[vk::push_constant]] +ConstantBuffer spdConstants; +//-------------------------------------------------------------------------------------- +// Texture definitions +//-------------------------------------------------------------------------------------- +[[vk::binding(0)]] RWTexture2DArray imgDst[13] :register(u0); // don't access mip [6] +[[vk::binding(1)]] globallycoherent RWTexture2DArray imgDst6 :register(u1); + +//-------------------------------------------------------------------------------------- +// Buffer definitions - global atomic counter +//-------------------------------------------------------------------------------------- +struct SpdGlobalAtomicBuffer +{ + uint counter[6]; +}; +[[vk::binding(2)]] globallycoherent RWStructuredBuffer spdGlobalAtomic; + +#define A_GPU +#define A_HLSL + +#include "ffx_a.h" + +groupshared AU1 spdCounter; + +// define fetch and store functions +#ifndef SPD_PACKED_ONLY +groupshared AF1 spdIntermediateR[16][16]; +groupshared AF1 spdIntermediateG[16][16]; +groupshared AF1 spdIntermediateB[16][16]; +groupshared AF1 spdIntermediateA[16][16]; + +AF4 SpdLoadSourceImage(ASU2 tex, AU1 slice) +{ + return imgDst[0][int3(tex,slice)]; +} +AF4 SpdLoad(ASU2 tex, AU1 slice) +{ + return imgDst6[int3(tex,slice)]; +} +void SpdStore(ASU2 pix, AF4 outValue, AU1 index, AU1 slice) +{ + if (index == 5) + { + imgDst6[int3(pix, slice)] = outValue; + return; + } + imgDst[index + 1][int3(pix, slice)] = outValue; +} +void SpdIncreaseAtomicCounter(AU1 slice) +{ + InterlockedAdd(spdGlobalAtomic[0].counter[slice], 1, spdCounter); +} +AU1 SpdGetAtomicCounter() +{ + return spdCounter; +} +void SpdResetAtomicCounter(AU1 slice) +{ + spdGlobalAtomic[0].counter[slice] = 0; +} +AF4 SpdLoadIntermediate(AU1 x, AU1 y) +{ + return AF4( + spdIntermediateR[x][y], + spdIntermediateG[x][y], + spdIntermediateB[x][y], + spdIntermediateA[x][y]); +} +void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value) +{ + spdIntermediateR[x][y] = value.x; + spdIntermediateG[x][y] = value.y; + spdIntermediateB[x][y] = value.z; + spdIntermediateA[x][y] = value.w; +} +AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3) +{ + return (v0+v1+v2+v3)*0.25; +} +#endif + +// define fetch and store functions Packed +#ifdef A_HALF +groupshared AH2 spdIntermediateRG[16][16]; +groupshared AH2 spdIntermediateBA[16][16]; + +AH4 SpdLoadSourceImageH(ASU2 tex, AU1 slice) +{ + return AH4(imgDst[0][int3(tex,slice)]); +} +AH4 SpdLoadH(ASU2 p, AU1 slice) +{ + return AH4(imgDst6[int3(p,slice)]); +} +void SpdStoreH(ASU2 p, AH4 value, AU1 mip, AU1 slice) +{ + if (mip == 5) + { + imgDst6[int3(p, slice)] = AF4(value); + return; + } + imgDst[mip + 1][int3(p, slice)] = AF4(value); +} +void SpdIncreaseAtomicCounter(AU1 slice) +{ + InterlockedAdd(spdGlobalAtomic[0].counter[slice], 1, spdCounter); +} +AU1 SpdGetAtomicCounter() +{ + return spdCounter; +} +void SpdResetAtomicCounter(AU1 slice) +{ + spdGlobalAtomic[0].counter[slice] = 0; +} +AH4 SpdLoadIntermediateH(AU1 x, AU1 y) +{ + return AH4( + spdIntermediateRG[x][y].x, + spdIntermediateRG[x][y].y, + spdIntermediateBA[x][y].x, + spdIntermediateBA[x][y].y); +} +void SpdStoreIntermediateH(AU1 x, AU1 y, AH4 value) +{ + spdIntermediateRG[x][y] = value.xy; + spdIntermediateBA[x][y] = value.zw; +} +AH4 SpdReduce4H(AH4 v0, AH4 v1, AH4 v2, AH4 v3) +{ + return (v0+v1+v2+v3)*AH1(0.25); +} +#endif + +#include "ffx_spd.h" + +// Main function +//-------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------- +[numthreads(256,1,1)] +void main(uint3 WorkGroupId : SV_GroupID, uint LocalThreadIndex : SV_GroupIndex) +{ +#ifndef A_HALF + SpdDownsample( + AU2(WorkGroupId.xy), + AU1(LocalThreadIndex), + AU1(spdConstants.mips), + AU1(spdConstants.numWorkGroups), + AU1(WorkGroupId.z), + AU2(spdConstants.workGroupOffset)); +#else + SpdDownsampleH( + AU2(WorkGroupId.xy), + AU1(LocalThreadIndex), + AU1(spdConstants.mips), + AU1(spdConstants.numWorkGroups), + AU1(WorkGroupId.z), + AU2(spdConstants.workGroupOffset)); +#endif +} \ No newline at end of file diff --git a/sample/src/VK/SPDIntegrationLinearSampler.glsl b/sample/src/VK/SPDIntegrationLinearSampler.glsl new file mode 100644 index 0000000..97c5b1e --- /dev/null +++ b/sample/src/VK/SPDIntegrationLinearSampler.glsl @@ -0,0 +1,200 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable +#extension GL_ARB_compute_shader : enable +#extension GL_ARB_shader_group_vote : enable + +// SPDSample +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +//-------------------------------------------------------------------------------------- +// Push Constants +//-------------------------------------------------------------------------------------- +layout(push_constant) uniform SpdConstants +{ + uint mips; + uint numWorkGroups; + ivec2 workGroupOffset; + vec2 invInputSize; +} spdConstants; + +//-------------------------------------------------------------------------------------- +// Texture definitions +//-------------------------------------------------------------------------------------- +layout(set=0, binding=3) uniform texture2DArray imgSrc; +layout(set=0, binding=0, rgba16f) uniform image2DArray imgDst[12]; // don't access MIP [5] +layout(set=0, binding=1, rgba16f) coherent uniform image2DArray imgDst5; +layout(set=0, binding=4) uniform sampler srcSampler; +//-------------------------------------------------------------------------------------- +// Buffer definitions - global atomic counter +//-------------------------------------------------------------------------------------- +layout(std430, binding=2) coherent buffer SpdGlobalAtomicBuffer +{ + uint counter[6]; +} spdGlobalAtomic; + +#define A_GPU +#define A_GLSL + +#include "ffx_a.h" + +shared AU1 spdCounter; + +// define fetch and store functions Non-Packed +#ifndef SPD_PACKED_ONLY +shared AF1 spdIntermediateR[16][16]; +shared AF1 spdIntermediateG[16][16]; +shared AF1 spdIntermediateB[16][16]; +shared AF1 spdIntermediateA[16][16]; + +AF4 SpdLoadSourceImage(ASU2 p, AU1 slice) +{ + AF2 textureCoord = p * spdConstants.invInputSize + spdConstants.invInputSize; + return texture(sampler2DArray(imgSrc, srcSampler), vec3(textureCoord,slice)); +} +AF4 SpdLoad(ASU2 p, AU1 slice) +{ + return imageLoad(imgDst5,ivec3(p,slice)); + } +void SpdStore(ASU2 p, AF4 value, AU1 mip, AU1 slice) +{ + if (mip == 5) + { + imageStore(imgDst5, ivec3(p,slice), value); + return; + } + imageStore(imgDst[mip], ivec3(p,slice), value); +} +void SpdIncreaseAtomicCounter(AU1 slice) +{ + spdCounter = atomicAdd(spdGlobalAtomic.counter[slice], 1); +} +AU1 SpdGetAtomicCounter() +{ + return spdCounter; +} +void SpdResetAtomicCounter(AU1 slice) +{ + spdGlobalAtomic.counter[slice] = 0; +} +AF4 SpdLoadIntermediate(AU1 x, AU1 y) +{ + return AF4( + spdIntermediateR[x][y], + spdIntermediateG[x][y], + spdIntermediateB[x][y], + spdIntermediateA[x][y]); +} +void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value) +{ + spdIntermediateR[x][y] = value.x; + spdIntermediateG[x][y] = value.y; + spdIntermediateB[x][y] = value.z; + spdIntermediateA[x][y] = value.w; +} +AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3) +{ + return (v0+v1+v2+v3)*0.25; +} +#endif + +// define fetch and store functions Packed +#ifdef A_HALF +shared AH2 spdIntermediateRG[16][16]; +shared AH2 spdIntermediateBA[16][16]; + +AH4 SpdLoadSourceImageH(ASU2 p, AU1 slice) +{ + AF2 textureCoord = p * spdConstants.invInputSize + spdConstants.invInputSize; + return AH4(texture(sampler2DArray(imgSrc, srcSampler), vec3(textureCoord, slice))); +} +AH4 SpdLoadH(ASU2 p, AU1 slice) +{ + return AH4(imageLoad(imgDst5,ivec3(p, slice))); +} +void SpdStoreH(ASU2 p, AH4 value, AU1 mip, AU1 slice) +{ + if (mip == 5) + { + imageStore(imgDst5, ivec3(p,slice), AF4(value)); + return; + } + imageStore(imgDst[mip], ivec3(p,slice), AF4(value)); +} +void SpdIncreaseAtomicCounter(AU1 slice) +{ + spdCounter = atomicAdd(spdGlobalAtomic.counter[slice], 1); +} +AU1 SpdGetAtomicCounter() +{ + return spdCounter; +} +void SpdResetAtomicCounter(AU1 slice) +{ + spdGlobalAtomic.counter[slice] = 0; +} +AH4 SpdLoadIntermediateH(AU1 x, AU1 y) +{ + return AH4( + spdIntermediateRG[x][y].x, + spdIntermediateRG[x][y].y, + spdIntermediateBA[x][y].x, + spdIntermediateBA[x][y].y); +} +void SpdStoreIntermediateH(AU1 x, AU1 y, AH4 value) +{ + spdIntermediateRG[x][y] = value.xy; + spdIntermediateBA[x][y] = value.zw; +} +AH4 SpdReduce4H(AH4 v0, AH4 v1, AH4 v2, AH4 v3) +{ + return (v0+v1+v2+v3)*AH1(0.25f); +} +#endif + +#define SPD_LINEAR_SAMPLER + +#include "ffx_spd.h" + +// Main function +//-------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------- +void main() +{ +#ifndef A_HALF + SpdDownsample( + AU2(gl_WorkGroupID.xy), + AU1(gl_LocalInvocationIndex), + AU1(spdConstants.mips), + AU1(spdConstants.numWorkGroups), + AU1(gl_WorkGroupID.z), + AU2(spdConstants.workGroupOffset)); +#else + SpdDownsampleH( + AU2(gl_WorkGroupID.xy), + AU1(gl_LocalInvocationIndex), + AU1(spdConstants.mips), + AU1(spdConstants.numWorkGroups), + AU1(gl_WorkGroupID.z), + AU2(spdConstants.workGroupOffset)); +#endif +} \ No newline at end of file diff --git a/sample/src/VK/SPDIntegrationLinearSampler.hlsl b/sample/src/VK/SPDIntegrationLinearSampler.hlsl new file mode 100644 index 0000000..f2ce10d --- /dev/null +++ b/sample/src/VK/SPDIntegrationLinearSampler.hlsl @@ -0,0 +1,196 @@ +// SPDSample +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +//-------------------------------------------------------------------------------------- +// Push Constants +//-------------------------------------------------------------------------------------- +struct SpdConstants +{ + uint mips; + uint numWorkGroups; + uint2 workGroupOffset; + float2 invInputSize; +}; + +[[vk::push_constant]] +ConstantBuffer spdConstants; +//-------------------------------------------------------------------------------------- +// Texture definitions +//-------------------------------------------------------------------------------------- +[[vk::binding(3)]] Texture2DArray imgSrc :register(t0); +[[vk::binding(0)]] RWTexture2DArray imgDst[12] :register(u1); // don't access mip [5] +[[vk::binding(1)]] globallycoherent RWTexture2DArray imgDst5 :register(u1); +[[vk::binding(4)]] SamplerState srcSampler :register(s0); + +//-------------------------------------------------------------------------------------- +// Buffer definitions - global atomic counter +//-------------------------------------------------------------------------------------- +struct SpdGlobalAtomicBuffer +{ + uint counter[6]; +}; +[[vk::binding(2)]] globallycoherent RWStructuredBuffer spdGlobalAtomic; + +#define A_GPU +#define A_HLSL + +#include "ffx_a.h" + +groupshared AU1 spdCounter; + +// define fetch and store functions +#ifndef SPD_PACKED_ONLY +groupshared AF1 spdIntermediateR[16][16]; +groupshared AF1 spdIntermediateG[16][16]; +groupshared AF1 spdIntermediateB[16][16]; +groupshared AF1 spdIntermediateA[16][16]; + +AF4 SpdLoadSourceImage(ASU2 p, AU1 slice) +{ + AF2 textureCoord = p * spdConstants.invInputSize + spdConstants.invInputSize; + return imgSrc.SampleLevel(srcSampler, float3(textureCoord, slice), 0); +} +AF4 SpdLoad(ASU2 tex, AU1 slice) +{ + return imgDst5[int3(tex, slice)]; +} +void SpdStore(ASU2 pix, AF4 outValue, AU1 index, AU1 slice) +{ + if (index == 5) + { + imgDst5[int3(pix, slice)] = outValue; + return; + } + imgDst[index][int3(pix, slice)] = outValue; +} +void SpdIncreaseAtomicCounter(AU1 slice) +{ + InterlockedAdd(spdGlobalAtomic[0].counter[slice], 1, spdCounter); +} +AU1 SpdGetAtomicCounter() +{ + return spdCounter; +} +void SpdResetAtomicCounter(AU1 slice) +{ + spdGlobalAtomic[0].counter[slice] = 0; +} +AF4 SpdLoadIntermediate(AU1 x, AU1 y) +{ + return AF4( + spdIntermediateR[x][y], + spdIntermediateG[x][y], + spdIntermediateB[x][y], + spdIntermediateA[x][y]); +} +void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value) +{ + spdIntermediateR[x][y] = value.x; + spdIntermediateG[x][y] = value.y; + spdIntermediateB[x][y] = value.z; + spdIntermediateA[x][y] = value.w; +} +AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3) +{ + return (v0+v1+v2+v3)*0.25; +} +#endif + +// define fetch and store functions Packed +#ifdef A_HALF +groupshared AH2 spdIntermediateRG[16][16]; +groupshared AH2 spdIntermediateBA[16][16]; + +AH4 SpdLoadSourceImageH(ASU2 p, AU1 slice) +{ + AF2 textureCoord = p * spdConstants.invInputSize + spdConstants.invInputSize; + return AH4(imgSrc.SampleLevel(srcSampler, float3(textureCoord, slice), 0)); +} +AH4 SpdLoadH(ASU2 p, AU1 slice) +{ + return AH4(imgDst5[int3(p, slice)]); +} +void SpdStoreH(ASU2 p, AH4 value, AU1 mip, AU1 slice) +{ + if (mip == 5) + { + imgDst5[int3(p, slice)] = AF4(value); + return; + } + imgDst[mip][int3(p, slice)] = AF4(value); +} +void SpdIncreaseAtomicCounter(AU1 slice) +{ + InterlockedAdd(spdGlobalAtomic[0].counter[slice], 1, spdCounter); +} +AU1 SpdGetAtomicCounter() +{ + return spdCounter; +} +void SpdResetAtomicCounter(AU1 slice) +{ + spdGlobalAtomic[0].counter[slice] = 0; +} +AH4 SpdLoadIntermediateH(AU1 x, AU1 y) +{ + return AH4( + spdIntermediateRG[x][y].x, + spdIntermediateRG[x][y].y, + spdIntermediateBA[x][y].x, + spdIntermediateBA[x][y].y); +} +void SpdStoreIntermediateH(AU1 x, AU1 y, AH4 value) +{ + spdIntermediateRG[x][y] = value.xy; + spdIntermediateBA[x][y] = value.zw; +} +AH4 SpdReduce4H(AH4 v0, AH4 v1, AH4 v2, AH4 v3) +{ + return (v0+v1+v2+v3)*AH1(0.25); +} +#endif + +#define SPD_LINEAR_SAMPLER + +#include "ffx_spd.h" + +// Main function +//-------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------- +[numthreads(256,1,1)] +void main(uint3 WorkGroupId : SV_GroupID, uint LocalThreadIndex : SV_GroupIndex) +{ +#ifndef A_HALF + SpdDownsample( + AU2(WorkGroupId.xy), + AU1(LocalThreadIndex), + AU1(spdConstants.mips), + AU1(spdConstants.numWorkGroups), + AU1(WorkGroupId.z), + AU2(spdConstants.workGroupOffset)); +#else + SpdDownsampleH( + AU2(WorkGroupId.xy), + AU1(LocalThreadIndex), + AU1(spdConstants.mips), + AU1(spdConstants.numWorkGroups), + AU1(WorkGroupId.z), + AU2(spdConstants.workGroupOffset)); +#endif +} \ No newline at end of file diff --git a/sample/src/VK/SPD_Renderer.cpp b/sample/src/VK/SPDRenderer.cpp similarity index 66% rename from sample/src/VK/SPD_Renderer.cpp rename to sample/src/VK/SPDRenderer.cpp index b5c3759..39cd352 100644 --- a/sample/src/VK/SPD_Renderer.cpp +++ b/sample/src/VK/SPDRenderer.cpp @@ -19,19 +19,23 @@ #include "stdafx.h" -#include "SPD_Renderer.h" +#include "SPDRenderer.h" //-------------------------------------------------------------------------------------- // // OnCreate // //-------------------------------------------------------------------------------------- -void SPD_Renderer::OnCreate(Device *pDevice, SwapChain *pSwapChain) +void SPDRenderer::OnCreate(Device *pDevice, SwapChain *pSwapChain, bool usingDescriptorIndexing) { - m_Format = VK_FORMAT_R16G16B16A16_SFLOAT; - m_pDevice = pDevice; + // we set this as requirement for SPD + // technically it's not, but it requires some integration side changes + // in case the resolution of the source texture is of variable size + // and the # of output MIPS is set to 12 by default regardless of source texture resolution + m_usingDescriptorIndexing = usingDescriptorIndexing; + // Initialize helpers // Create all the heaps for the resources views @@ -43,17 +47,17 @@ void SPD_Renderer::OnCreate(Device *pDevice, SwapChain *pSwapChain) // Create a commandlist ring for the Direct queue uint32_t commandListsPerBackBuffer = 8; - m_CommandListRing.OnCreate(pDevice, backBufferCount, commandListsPerBackBuffer); + 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"); + 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 staticGeometryMemSize = ( 2 * 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"); + 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); @@ -61,20 +65,19 @@ void SPD_Renderer::OnCreate(Device *pDevice, SwapChain *pSwapChain) // 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) + m_uploadHeap.OnCreate(pDevice, uploadHeapMemSize); // 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 + // Create render pass shadow, will clear contents // { - /* Need attachments for render target and depth buffer */ 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); + m_renderPassShadow = CreateRenderPassOptimal(m_pDevice->GetDevice(), 0, NULL, &depthAttachments); // Create frame buffer, its size is now window dependant so we can do this here. // @@ -82,13 +85,13 @@ void SPD_Renderer::OnCreate(Device *pDevice, SwapChain *pSwapChain) 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.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_pFrameBuffer_shadow); + VkResult res = vkCreateFramebuffer(m_pDevice->GetDevice(), &fb_info, NULL, &m_frameBufferShadow); assert(res == VK_SUCCESS); } @@ -96,62 +99,42 @@ void SPD_Renderer::OnCreate(Device *pDevice, SwapChain *pSwapChain) // { VkAttachmentDescription colorAttachment, depthAttachment; - AttachClearBeforeUse(m_Format, VK_SAMPLE_COUNT_4_BIT, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, &colorAttachment); + 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 HDR render pass, for the GUI // { VkAttachmentDescription colorAttachment; - AttachBlending(m_Format, 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); + 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_render_pass_HDR_MSAA, &m_UploadHeap, m_Format, &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, m_Format, &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_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); // Create downsampling passes - - m_PSDownsampler.OnCreate(pDevice, &m_resourceViewHeaps, &m_ConstantBufferRing, &m_VidMemBufferPool, m_Format); - m_CSDownsampler.OnCreate(pDevice, &m_resourceViewHeaps, m_Format); - m_SPD_Versions.OnCreate(pDevice, &m_resourceViewHeaps, m_Format); + m_PSDownsampler.OnCreate(pDevice, &m_uploadHeap, &m_resourceViewHeaps, &m_constantBufferRing, &m_vidMemBufferPool); + m_CSDownsampler.OnCreate(pDevice, &m_uploadHeap, &m_resourceViewHeaps); + if (usingDescriptorIndexing) { + m_SPDVersions.OnCreate(pDevice, &m_uploadHeap, &m_resourceViewHeaps); + } // Create tonemapping pass - m_toneMapping.OnCreate(m_pDevice, pSwapChain->GetRenderPass(), &m_resourceViewHeaps, &m_VidMemBufferPool, &m_ConstantBufferRing); + m_toneMapping.OnCreate(m_pDevice, pSwapChain->GetRenderPass(), &m_resourceViewHeaps, &m_vidMemBufferPool, &m_constantBufferRing); // Initialize UI rendering resources - m_ImGUI.OnCreate(m_pDevice, pSwapChain->GetRenderPass(), &m_UploadHeap, &m_ConstantBufferRing); + 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(); + m_vidMemBufferPool.UploadData(m_uploadHeap.GetCommandList()); + m_uploadHeap.FlushAndFinish(); #endif - - // Create allocator - // - VkCommandPoolCreateInfo cmd_pool_info = {}; - cmd_pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; - cmd_pool_info.pNext = NULL; - cmd_pool_info.queueFamilyIndex = pDevice->GetGraphicsQueueFamilyIndex(); - cmd_pool_info.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT; - VkResult res = vkCreateCommandPool(pDevice->GetDevice(), &cmd_pool_info, NULL, &m_CommandPool); - assert(res == VK_SUCCESS); - - // Create command buffers - // - VkCommandBufferAllocateInfo cmd = {}; - cmd.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - cmd.pNext = NULL; - cmd.commandPool = m_CommandPool; - cmd.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - cmd.commandBufferCount = 1; - res = vkAllocateCommandBuffers(pDevice->GetDevice(), &cmd, &m_CommandBufferInit); - assert(res == VK_SUCCESS); } //-------------------------------------------------------------------------------------- @@ -159,9 +142,9 @@ void SPD_Renderer::OnCreate(Device *pDevice, SwapChain *pSwapChain) // OnDestroy // //-------------------------------------------------------------------------------------- -void SPD_Renderer::OnDestroy() +void SPDRenderer::OnDestroy() { - m_ImGUI.OnDestroy(); + m_imGUI.OnDestroy(); m_toneMapping.OnDestroy(); m_wireframeBox.OnDestroy(); m_wireframe.OnDestroy(); @@ -171,27 +154,26 @@ void SPD_Renderer::OnDestroy() m_PSDownsampler.OnDestroy(); m_CSDownsampler.OnDestroy(); - m_SPD_Versions.OnDestroy(); + if (m_usingDescriptorIndexing) { + m_SPDVersions.OnDestroy(); + } vkDestroyImageView(m_pDevice->GetDevice(), m_shadowMapDSV, nullptr); vkDestroyImageView(m_pDevice->GetDevice(), m_shadowMapSRV, nullptr); - vkDestroyRenderPass(m_pDevice->GetDevice(), m_render_pass_shadow, nullptr); - vkDestroyRenderPass(m_pDevice->GetDevice(), m_render_pass_HDR_MSAA, nullptr); - vkDestroyRenderPass(m_pDevice->GetDevice(), m_render_pass_PBR_HDR, nullptr); + vkDestroyRenderPass(m_pDevice->GetDevice(), m_renderPassShadow, nullptr); + vkDestroyRenderPass(m_pDevice->GetDevice(), m_renderPassHDRMSAA, nullptr); + vkDestroyRenderPass(m_pDevice->GetDevice(), m_renderPassPBRHDR, nullptr); - vkDestroyFramebuffer(m_pDevice->GetDevice(), m_pFrameBuffer_shadow, nullptr); + vkDestroyFramebuffer(m_pDevice->GetDevice(), m_frameBufferShadow, nullptr); - m_UploadHeap.OnDestroy(); + m_uploadHeap.OnDestroy(); m_GPUTimer.OnDestroy(); - m_VidMemBufferPool.OnDestroy(); - m_SysMemBufferPool.OnDestroy(); - m_ConstantBufferRing.OnDestroy(); + m_vidMemBufferPool.OnDestroy(); + m_sysMemBufferPool.OnDestroy(); + m_constantBufferRing.OnDestroy(); m_resourceViewHeaps.OnDestroy(); - m_CommandListRing.OnDestroy(); - - vkFreeCommandBuffers(m_pDevice->GetDevice(), m_CommandPool, 1, &m_CommandBufferInit); - vkDestroyCommandPool(m_pDevice->GetDevice(), m_CommandPool, NULL); + m_commandListRing.OnDestroy(); } //-------------------------------------------------------------------------------------- @@ -199,7 +181,7 @@ void SPD_Renderer::OnDestroy() // OnCreateWindowSizeDependentResources // //-------------------------------------------------------------------------------------- -void SPD_Renderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, uint32_t Width, uint32_t Height) +void SPDRenderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, uint32_t Width, uint32_t Height) { m_Width = Width; m_Height = Height; @@ -223,87 +205,49 @@ void SPD_Renderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, u // Create depth buffer // m_depthBuffer.InitDepthStencil(m_pDevice, Width, Height, VK_FORMAT_D32_SFLOAT, VK_SAMPLE_COUNT_4_BIT, "DepthBuffer"); - m_depthBuffer.CreateDSV(&m_depthBufferView); + m_depthBuffer.CreateDSV(&m_depthBufferDSV); // Create Texture + RTV with x4 MSAA // - m_HDRMSAA.InitRenderTarget(m_pDevice, m_Width, m_Height, m_Format, 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); + 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_HDRMSAARTV); // Create Texture + RTV, to hold the resolved scene // - m_HDR.InitRenderTarget(m_pDevice, m_Width, m_Height, m_Format, 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.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[2] = { m_HDRMSAASRV, m_depthBufferView }; - 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.renderPass = m_render_pass_HDR_MSAA; - fb_info.attachmentCount = 2; - fb_info.pAttachments = attachments; fb_info.width = Width; fb_info.height = Height; fb_info.layers = 1; - VkResult res = vkCreateFramebuffer(m_pDevice->GetDevice(), &fb_info, NULL, &m_pFrameBuffer_HDR_MSAA); + VkResult res; + + VkImageView attachments_PBR_HDR_MSAA[] = { m_HDRMSAARTV, m_depthBufferDSV }; + fb_info.attachmentCount = _countof(attachments_PBR_HDR_MSAA); + fb_info.pAttachments = attachments_PBR_HDR_MSAA; + fb_info.renderPass = m_renderPassHDRMSAA; + res = vkCreateFramebuffer(m_pDevice->GetDevice(), &fb_info, NULL, &m_frameBufferHDRMSAA); assert(res == VK_SUCCESS); - fb_info.attachmentCount = 1; + VkImageView attachments_PBR_HDR[1] = { m_HDRSRV }; + fb_info.attachmentCount = _countof(attachments_PBR_HDR); 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); + fb_info.renderPass = m_renderPassPBRHDR; + res = vkCreateFramebuffer(m_pDevice->GetDevice(), &fb_info, NULL, &m_frameBufferPBRHDR); assert(res == VK_SUCCESS); } - // update downscaling effect - // - { - int resolution = max(m_Width, m_Height); - int mipLevel = (static_cast(min(1.0f + floor(log2(resolution)), 12)) - 1); - - { - 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(m_CommandBufferInit, &cmd_buf_info); - assert(res == VK_SUCCESS); - } - - m_PSDownsampler.OnCreateWindowSizeDependentResources(m_Width, m_Height, &m_HDR, mipLevel); - m_CSDownsampler.OnCreateWindowSizeDependentResources(m_CommandBufferInit, m_Width, m_Height, &m_HDR, mipLevel); - m_SPD_Versions.OnCreateWindowSizeDependentResources(m_CommandBufferInit, m_Width, m_Height, &m_HDR); - - { - VkResult res = vkEndCommandBuffer(m_CommandBufferInit); - 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 = &m_CommandBufferInit; - submit_info.signalSemaphoreCount = 0; - submit_info.pSignalSemaphores = NULL; - res = vkQueueSubmit(m_pDevice->GetGraphicsQueue(), 1, &submit_info, VK_NULL_HANDLE); - assert(res == VK_SUCCESS); - } - } - m_toneMapping.UpdatePipelines(pSwapChain->GetRenderPass()), - m_ImGUI.UpdatePipeline(pSwapChain->GetRenderPass()); + m_imGUI.UpdatePipeline(pSwapChain->GetRenderPass()); } //-------------------------------------------------------------------------------------- @@ -311,21 +255,17 @@ void SPD_Renderer::OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, u // OnDestroyWindowSizeDependentResources // //-------------------------------------------------------------------------------------- -void SPD_Renderer::OnDestroyWindowSizeDependentResources() +void SPDRenderer::OnDestroyWindowSizeDependentResources() { - m_PSDownsampler.OnDestroyWindowSizeDependentResources(); - m_CSDownsampler.OnDestroyWindowSizeDependentResources(); - m_SPD_Versions.OnDestroyWindowSizeDependentResources(); - m_HDR.OnDestroy(); m_HDRMSAA.OnDestroy(); m_depthBuffer.OnDestroy(); - vkDestroyFramebuffer(m_pDevice->GetDevice(), m_pFrameBuffer_HDR_MSAA, nullptr); - vkDestroyFramebuffer(m_pDevice->GetDevice(), m_pFrameBuffer_PBR_HDR, nullptr); + vkDestroyFramebuffer(m_pDevice->GetDevice(), m_frameBufferHDRMSAA, nullptr); + vkDestroyFramebuffer(m_pDevice->GetDevice(), m_frameBufferPBRHDR, nullptr); - vkDestroyImageView(m_pDevice->GetDevice(), m_depthBufferView, nullptr); - vkDestroyImageView(m_pDevice->GetDevice(), m_HDRMSAASRV, nullptr); + vkDestroyImageView(m_pDevice->GetDevice(), m_depthBufferDSV, nullptr); + vkDestroyImageView(m_pDevice->GetDevice(), m_HDRMSAARTV, nullptr); vkDestroyImageView(m_pDevice->GetDevice(), m_HDRSRV, nullptr); vkDestroyImageView(m_pDevice->GetDevice(), m_HDRUAV, nullptr); } @@ -335,7 +275,7 @@ void SPD_Renderer::OnDestroyWindowSizeDependentResources() // LoadScene // //-------------------------------------------------------------------------------------- -int SPD_Renderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) +int SPDRenderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) { // show loading progress // @@ -357,7 +297,7 @@ int SPD_Renderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) Profile p("m_pGltfLoader->Load"); m_pGLTFTexturesAndBuffers = new GLTFTexturesAndBuffers(); - m_pGLTFTexturesAndBuffers->OnCreate(m_pDevice, pGLTFCommon, &m_UploadHeap, &m_VidMemBufferPool, &m_ConstantBufferRing); + m_pGLTFTexturesAndBuffers->OnCreate(m_pDevice, pGLTFCommon, &m_uploadHeap, &m_vidMemBufferPool, &m_constantBufferRing); } else if (stage == 6) { @@ -372,19 +312,19 @@ int SPD_Renderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) 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_pGltfDepth = new GltfDepthPass(); + m_pGltfDepth->OnCreate( m_pDevice, - m_render_pass_shadow, - &m_UploadHeap, + m_renderPassShadow, + &m_uploadHeap, &m_resourceViewHeaps, - &m_ConstantBufferRing, - &m_VidMemBufferPool, + &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) @@ -392,25 +332,27 @@ int SPD_Renderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) Profile p("m_gltfPBR->OnCreate"); // same thing as above but for the PBR pass - m_gltfPBR = new GltfPbrPass(); - m_gltfPBR->OnCreate( + m_pGltfPBR = new GltfPbrPass(); + m_pGltfPBR->OnCreate( m_pDevice, - m_render_pass_HDR_MSAA, - &m_UploadHeap, + m_renderPassHDRMSAA, + &m_uploadHeap, &m_resourceViewHeaps, - &m_ConstantBufferRing, - &m_VidMemBufferPool, + &m_constantBufferRing, + &m_vidMemBufferPool, m_pGLTFTexturesAndBuffers, &m_skyDome, - m_shadowMapSRV, - true, - true, false, + m_shadowMapSRV, + true, // Exports ForwardPass + false, // Won't export Specular Roughness + false, // Won't export Diffuse Color + false, // Won't export normals VK_SAMPLE_COUNT_4_BIT ); #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) @@ -418,30 +360,30 @@ int SPD_Renderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) 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_pGltfBBox = new GltfBBoxPass(); + m_pGltfBBox->OnCreate( m_pDevice, - m_render_pass_HDR_MSAA, + m_renderPassHDRMSAA, &m_resourceViewHeaps, - &m_ConstantBufferRing, - &m_VidMemBufferPool, + &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()); + m_vidMemBufferPool.UploadData(m_uploadHeap.GetCommandList()); #endif } else if (stage == 10) { 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(); + m_vidMemBufferPool.FreeUploadHeap(); #endif // tell caller that we are done loading the map return -1; @@ -456,27 +398,29 @@ int SPD_Renderer::LoadScene(GLTFCommon *pGLTFCommon, int stage) // UnloadScene // //-------------------------------------------------------------------------------------- -void SPD_Renderer::UnloadScene() +void SPDRenderer::UnloadScene() { - if (m_gltfPBR) + m_pDevice->GPUFlush(); + + if (m_pGltfPBR) { - m_gltfPBR->OnDestroy(); - delete m_gltfPBR; - m_gltfPBR = NULL; + m_pGltfPBR->OnDestroy(); + delete m_pGltfPBR; + m_pGltfPBR = NULL; } - if (m_gltfDepth) + if (m_pGltfDepth) { - m_gltfDepth->OnDestroy(); - delete m_gltfDepth; - m_gltfDepth = NULL; + m_pGltfDepth->OnDestroy(); + delete m_pGltfDepth; + m_pGltfDepth = NULL; } - if (m_gltfBBox) + if (m_pGltfBBox) { - m_gltfBBox->OnDestroy(); - delete m_gltfBBox; - m_gltfBBox = NULL; + m_pGltfBBox->OnDestroy(); + delete m_pGltfBBox; + m_pGltfBBox = NULL; } if (m_pGLTFTexturesAndBuffers) @@ -492,15 +436,15 @@ void SPD_Renderer::UnloadScene() // OnRender // //-------------------------------------------------------------------------------------- -void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) +void SPDRenderer::OnRender(State *pState, SwapChain *pSwapChain) { // Let our resource managers do some house keeping // - m_ConstantBufferRing.OnBeginFrame(); + m_constantBufferRing.OnBeginFrame(); // command buffer calls // - VkCommandBuffer cmd_buf = m_CommandListRing.GetNewCommandList(); + VkCommandBuffer cmdBuf1 = m_commandListRing.GetNewCommandList(); { VkCommandBufferBeginInfo cmd_buf_info; @@ -508,17 +452,20 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) 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(cmd_buf, &cmd_buf_info); + VkResult res = vkBeginCommandBuffer(cmdBuf1, &cmd_buf_info); assert(res == VK_SUCCESS); } - m_GPUTimer.OnBeginFrame(cmd_buf, &m_TimeStamps); + m_GPUTimer.OnBeginFrame(cmdBuf1, &m_timeStamps); + + m_GPUTimer.GetTimeStampUser({ "time (s)", pState->time }); - // Sets the perFrame data (Camera and lights data), override as necessary and set them as constant buffers -------------- + // 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 @@ -552,9 +499,9 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) // Render to shadow map atlas for spot lights ------------------------------------------ // - if (m_gltfDepth && pPerFrame != NULL) + if (m_pGltfDepth && pPerFrame != NULL) { - SetPerfMarkerBegin(cmd_buf, "ShadowPass"); + SetPerfMarkerBegin(cmdBuf1, "ShadowPass"); VkClearValue depth_clear_values[1]; depth_clear_values[0].depthStencil.depth = 1.0f; @@ -564,8 +511,8 @@ void SPD_Renderer::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_shadow; - rp_begin.framebuffer = m_pFrameBuffer_shadow; + rp_begin.renderPass = m_renderPassShadow; + rp_begin.framebuffer = m_frameBufferShadow; rp_begin.renderArea.offset.x = 0; rp_begin.renderArea.offset.y = 0; rp_begin.renderArea.extent.width = m_shadowMap.GetWidth(); @@ -573,8 +520,8 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) rp_begin.clearValueCount = 1; rp_begin.pClearValues = depth_clear_values; - vkCmdBeginRenderPass(cmd_buf, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); - m_GPUTimer.GetTimeStamp(cmd_buf, "Clear Shadow Map"); + vkCmdBeginRenderPass(cmdBuf1, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); + m_GPUTimer.GetTimeStamp(cmdBuf1, "Clear Shadow Map"); } uint32_t shadowMapIndex = 0; @@ -588,27 +535,27 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) uint32_t viewportOffsetsY[4] = { 0, 0, 1, 1 }; uint32_t viewportWidth = m_shadowMap.GetWidth() / 2; uint32_t viewportHeight = m_shadowMap.GetHeight() / 2; - SetViewportAndScissor(cmd_buf, viewportOffsetsX[shadowMapIndex] * viewportWidth, viewportOffsetsY[shadowMapIndex] * viewportHeight, viewportWidth, viewportHeight); + SetViewportAndScissor(cmdBuf1, viewportOffsetsX[shadowMapIndex] * viewportWidth, viewportOffsetsY[shadowMapIndex] * viewportHeight, viewportWidth, viewportHeight); //set per frame constant buffer values - GltfDepthPass::per_frame *cbPerFrame = m_gltfDepth->SetPerFrameConstants(); + GltfDepthPass::per_frame *cbPerFrame = m_pGltfDepth->SetPerFrameConstants(); cbPerFrame->mViewProj = pPerFrame->lights[i].mLightViewProj; - m_gltfDepth->Draw(cmd_buf); + m_pGltfDepth->Draw(cmdBuf1); - m_GPUTimer.GetTimeStamp(cmd_buf, "Shadow maps"); + m_GPUTimer.GetTimeStamp(cmdBuf1, "Shadow maps"); shadowMapIndex++; } - vkCmdEndRenderPass(cmd_buf); + vkCmdEndRenderPass(cmdBuf1); - SetPerfMarkerEnd(cmd_buf); + SetPerfMarkerEnd(cmdBuf1); } // Render Scene to the MSAA HDR RT ------------------------------------------------ // { - SetPerfMarkerBegin(cmd_buf, "Color pass"); - m_GPUTimer.GetTimeStamp(cmd_buf, "before color RP"); + 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; @@ -620,8 +567,8 @@ void SPD_Renderer::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_HDR_MSAA; - rp_begin.framebuffer = m_pFrameBuffer_HDR_MSAA; + rp_begin.renderPass = m_renderPassHDRMSAA; + rp_begin.framebuffer = m_frameBufferHDRMSAA; rp_begin.renderArea.offset.x = 0; rp_begin.renderArea.offset.y = 0; rp_begin.renderArea.extent.width = m_Width; @@ -629,11 +576,11 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) rp_begin.clearValueCount = 2; rp_begin.pClearValues = clear_values; - vkCmdBeginRenderPass(cmd_buf, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); + vkCmdBeginRenderPass(cmdBuf1, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); - vkCmdSetScissor(cmd_buf, 0, 1, &m_scissor); - vkCmdSetViewport(cmd_buf, 0, 1, &m_viewport); - m_GPUTimer.GetTimeStamp(cmd_buf, "after color RP"); + vkCmdSetScissor(cmdBuf1, 0, 1, &m_scissor); + vkCmdSetViewport(cmdBuf1, 0, 1, &m_viewport); + m_GPUTimer.GetTimeStamp(cmdBuf1, "after color RP"); } if (pPerFrame != NULL) @@ -643,9 +590,9 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) if (pState->skyDomeType == 1) { XMMATRIX clipToView = XMMatrixInverse(NULL, pPerFrame->mCameraViewProj); - m_skyDome.Draw(cmd_buf, clipToView); + m_skyDome.Draw(cmdBuf1, clipToView); - m_GPUTimer.GetTimeStamp(cmd_buf, "Skydome cube"); + m_GPUTimer.GetTimeStamp(cmdBuf1, "Skydome cube"); } else if (pState->skyDomeType == 0) { @@ -658,28 +605,28 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) skyDomeConstants.mieDirectionalG = 0.8f; skyDomeConstants.luminance = 1.0f; skyDomeConstants.sun = false; - m_skyDomeProc.Draw(cmd_buf, skyDomeConstants); + m_skyDomeProc.Draw(cmdBuf1, skyDomeConstants); - m_GPUTimer.GetTimeStamp(cmd_buf, "Skydome Proc"); + m_GPUTimer.GetTimeStamp(cmdBuf1, "Skydome Proc"); } // Render scene to color buffer // - if (m_gltfBBox && pPerFrame != NULL) + if (m_pGltfBBox && pPerFrame != NULL) { - m_gltfPBR->Draw(cmd_buf); - m_GPUTimer.GetTimeStamp(cmd_buf, "Rendering Scene"); + m_pGltfPBR->Draw(cmdBuf1); + m_GPUTimer.GetTimeStamp(cmdBuf1, "Rendering Scene"); } // draw object's bounding boxes // - if (m_gltfBBox && pPerFrame != NULL) + if (m_pGltfBBox && pPerFrame != NULL) { if (pState->bDrawBoundingBoxes) { - m_gltfBBox->Draw(cmd_buf, pPerFrame->mCameraViewProj); + m_pGltfBBox->Draw(cmdBuf1, pPerFrame->mCameraViewProj); - m_GPUTimer.GetTimeStamp(cmd_buf, "Bounding Box"); + m_GPUTimer.GetTimeStamp(cmdBuf1, "Bounding Box"); } } @@ -687,7 +634,7 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) // if (pState->bDrawLightFrustum && pPerFrame != NULL) { - SetPerfMarkerBegin(cmd_buf, "light frustrum"); + SetPerfMarkerBegin(cmdBuf1, "light frustrum"); XMVECTOR vCenter = XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f); XMVECTOR vRadius = XMVectorSet(1.0f, 1.0f, 1.0f, 0.0f); @@ -696,24 +643,24 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) { XMMATRIX spotlightMatrix = XMMatrixInverse(NULL, pPerFrame->lights[i].mLightViewProj); XMMATRIX worldMatrix = spotlightMatrix * pPerFrame->mCameraViewProj; - m_wireframeBox.Draw(cmd_buf, &m_wireframe, worldMatrix, vCenter, vRadius, vColor); + m_wireframeBox.Draw(cmdBuf1, &m_wireframe, worldMatrix, vCenter, vRadius, vColor); } - m_GPUTimer.GetTimeStamp(cmd_buf, "Light's frustum"); + m_GPUTimer.GetTimeStamp(cmdBuf1, "Light's frustum"); - SetPerfMarkerEnd(cmd_buf); + SetPerfMarkerEnd(cmdBuf1); } } { - vkCmdEndRenderPass(cmd_buf); - SetPerfMarkerEnd(cmd_buf); + vkCmdEndRenderPass(cmdBuf1); + SetPerfMarkerEnd(cmdBuf1); } // Resolve MSAA ------------------------------------------------------------------------ // { - SetPerfMarkerBegin(cmd_buf, "resolve MSAA"); + SetPerfMarkerBegin(cmdBuf1, "resolve MSAA"); { VkImageMemoryBarrier barrier[2] = {}; barrier[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; @@ -746,7 +693,7 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) barrier[1].subresourceRange.layerCount = 1; barrier[1].image = m_HDRMSAA.Resource(); - vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 2, barrier); + vkCmdPipelineBarrier(cmdBuf1, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 2, barrier); } { @@ -762,7 +709,7 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) re.dstOffset.y = 0; re.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; re.dstSubresource.layerCount = 1; - vkCmdResolveImage(cmd_buf, m_HDRMSAA.Resource(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_HDR.Resource(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &re); + vkCmdResolveImage(cmdBuf1, m_HDRMSAA.Resource(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_HDR.Resource(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &re); } { @@ -773,6 +720,11 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) 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; // we need to read from it for the post-processing + // when we use load to fetch the data from the source texture, the source texture needs to be in general layout instead of shader read only + if (pState->downsampler == Downsampler::SPDCS && pState->spdLoad == SPDLoad::SPDLoad) + { + barrier[0].newLayout = VK_IMAGE_LAYOUT_GENERAL; // we load from a storage image in this case + } barrier[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier[0].subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; @@ -797,45 +749,63 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) barrier[1].subresourceRange.layerCount = 1; barrier[1].image = m_HDRMSAA.Resource(); - vkCmdPipelineBarrier(cmd_buf, 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); + 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(cmd_buf, "Resolve"); - SetPerfMarkerEnd(cmd_buf); + m_GPUTimer.GetTimeStamp(cmdBuf1, "Resolve"); + SetPerfMarkerEnd(cmdBuf1); } // Post proc--------------------------------------------------------------------------- // { - SetPerfMarkerBegin(cmd_buf, "post proc"); + SetPerfMarkerBegin(cmdBuf1, "post proc"); + VkImageMemoryBarrier barrier[1] = {}; switch (pState->downsampler) { case Downsampler::PS: - m_PSDownsampler.Draw(cmd_buf); - m_PSDownsampler.Gui(); - break; - case Downsampler::Multipass_CS: - m_CSDownsampler.Draw(cmd_buf); - m_CSDownsampler.Gui(); + m_PSDownsampler.Draw(cmdBuf1); + m_PSDownsampler.GUI(&pState->downsamplerImGUISlice); break; - case Downsampler::SPD_CS: - m_SPD_Versions.Dispatch(cmd_buf, pState->spdVersion, pState->spdPacked); - m_SPD_Versions.Gui(pState->spdVersion, pState->spdPacked); + case Downsampler::MultipassCS: + m_CSDownsampler.Draw(cmdBuf1); + m_CSDownsampler.GUI(&pState->downsamplerImGUISlice); break; - case Downsampler::SPD_CS_Linear_Sampler: - m_SPD_Versions.DispatchLinearSamplerVersion(cmd_buf, pState->spdVersion, pState->spdPacked); - m_SPD_Versions.GuiLinearSamplerVersion(pState->spdVersion, pState->spdPacked); + case Downsampler::SPDCS: + if (m_usingDescriptorIndexing) { + m_SPDVersions.Dispatch(cmdBuf1, pState->spdLoad, pState->spdWaveOps, pState->spdPacked); + m_SPDVersions.GUI(pState->spdLoad, pState->spdWaveOps, pState->spdPacked, &pState->downsamplerImGUISlice); + } + + if (pState->spdLoad == SPDLoad::SPDLoad) + { + 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_GENERAL; + 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(); + 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, 1, barrier); + } break; } - m_GPUTimer.GetTimeStamp(cmd_buf, "Downsampler"); + m_GPUTimer.GetTimeStamp(cmdBuf1, "Downsampler"); - SetPerfMarkerEnd(cmd_buf); + SetPerfMarkerEnd(cmdBuf1); } { - VkResult res = vkEndCommandBuffer(cmd_buf); + VkResult res = vkEndCommandBuffer(cmdBuf1); assert(res == VK_SUCCESS); VkSubmitInfo submit_info; @@ -845,7 +815,7 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) submit_info.pWaitSemaphores = NULL; submit_info.pWaitDstStageMask = NULL; submit_info.commandBufferCount = 1; - submit_info.pCommandBuffers = &cmd_buf; + submit_info.pCommandBuffers = &cmdBuf1; submit_info.signalSemaphoreCount = 0; submit_info.pSignalSemaphores = NULL; res = vkQueueSubmit(m_pDevice->GetGraphicsQueue(), 1, &submit_info, VK_NULL_HANDLE); @@ -858,9 +828,9 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) int imageIndex = pSwapChain->WaitForSwapChain(); - m_CommandListRing.OnBeginFrame(); + m_commandListRing.OnBeginFrame(); - VkCommandBuffer cmdBuf2 = m_CommandListRing.GetNewCommandList(); + VkCommandBuffer cmdBuf2 = m_commandListRing.GetNewCommandList(); { VkCommandBufferBeginInfo cmd_buf_info; @@ -902,7 +872,7 @@ void SPD_Renderer::OnRender(State *pState, SwapChain *pSwapChain) // Render HUD ------------------------------------------------------------------------ // { - m_ImGUI.Draw(cmdBuf2); + m_imGUI.Draw(cmdBuf2); m_GPUTimer.GetTimeStamp(cmdBuf2, "ImGUI Rendering"); } diff --git a/sample/src/VK/SPD_Renderer.h b/sample/src/VK/SPDRenderer.h similarity index 58% rename from sample/src/VK/SPD_Renderer.h rename to sample/src/VK/SPDRenderer.h index 4750d3d..2094934 100644 --- a/sample/src/VK/SPD_Renderer.h +++ b/sample/src/VK/SPDRenderer.h @@ -20,7 +20,7 @@ #include "CSDownsampler.h" #include "PSDownsampler.h" -#include "SPD_Versions.h" +#include "SPDVersions.h" static const int backBufferCount = 3; @@ -35,12 +35,11 @@ using namespace CAULDRON_VK; enum class Downsampler { PS, - Multipass_CS, - SPD_CS, - SPD_CS_Linear_Sampler, + MultipassCS, + SPDCS }; -class SPD_Renderer +class SPDRenderer { public: struct Spotlight @@ -52,27 +51,34 @@ class SPD_Renderer struct State { - float time; - Camera camera; + 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 bDrawBoundingBoxes; - uint32_t spotlightCount; - Spotlight spotlight[4]; - bool bDrawLightFrustum; + uint32_t spotlightCount; + Spotlight spotlight[4]; - Downsampler downsampler; - SPD_Version spdVersion; - SPD_Packed spdPacked; + bool isBenchmarking; + bool isValidationLayerEnabled; + + bool bDrawLightFrustum; + + Downsampler downsampler; + SPDLoad spdLoad; + SPDWaveOps spdWaveOps; + SPDPacked spdPacked; + + int downsamplerImGUISlice; }; - void OnCreate(Device *pDevice, SwapChain *pSwapChain); + void OnCreate(Device *pDevice, SwapChain *pSwapChain, bool usingDescriptorIndexing = false); void OnDestroy(); void OnCreateWindowSizeDependentResources(SwapChain *pSwapChain, uint32_t Width, uint32_t Height); @@ -81,33 +87,33 @@ class SPD_Renderer int LoadScene(GLTFCommon *pGLTFCommon, int stage = 0); void UnloadScene(); - const std::vector &GetTimingValues() { return m_TimeStamps; } + const std::vector &GetTimingValues() { return m_timeStamps; } void OnRender(State *pState, SwapChain *pSwapChain); private: - Device *m_pDevice; + Device *m_pDevice = nullptr; - uint32_t m_Width; - uint32_t m_Height; + uint32_t m_Width; + uint32_t m_Height; VkRect2D m_scissor; VkViewport m_viewport; // Initialize helper classes ResourceViewHeaps m_resourceViewHeaps; - UploadHeap m_UploadHeap; - DynamicBufferRing m_ConstantBufferRing; - StaticBufferPool m_VidMemBufferPool; - StaticBufferPool m_SysMemBufferPool; - CommandListRing m_CommandListRing; + UploadHeap m_uploadHeap; + DynamicBufferRing m_constantBufferRing; + StaticBufferPool m_vidMemBufferPool; + StaticBufferPool m_sysMemBufferPool; + CommandListRing m_commandListRing; GPUTimestamps m_GPUTimer; //gltf passes - GLTFTexturesAndBuffers *m_pGLTFTexturesAndBuffers; - GltfPbrPass *m_gltfPBR; - GltfDepthPass *m_gltfDepth; - GltfBBoxPass *m_gltfBBox; + GLTFTexturesAndBuffers *m_pGLTFTexturesAndBuffers = nullptr; + GltfPbrPass *m_pGltfPBR = nullptr; + GltfDepthPass *m_pGltfDepth = nullptr; + GltfBBoxPass *m_pGltfBBox = nullptr; // effects SkyDome m_skyDome; @@ -117,19 +123,19 @@ class SPD_Renderer // downsampling - m_HDR PSDownsampler m_PSDownsampler; CSDownsampler m_CSDownsampler; - SPD_Versions m_SPD_Versions; + SPDVersions m_SPDVersions; VkCommandPool m_CommandPool; VkCommandBuffer m_CommandBufferInit; // GUI - ImGUI m_ImGUI; + ImGUI m_imGUI; // Temporary render targets // depth buffer Texture m_depthBuffer; - VkImageView m_depthBufferView; + VkImageView m_depthBufferDSV; // shadowmaps Texture m_shadowMap; @@ -138,7 +144,7 @@ class SPD_Renderer // MSAA RT Texture m_HDRMSAA; - VkImageView m_HDRMSAASRV; + VkImageView m_HDRMSAARTV; // Resolved RT Texture m_HDR; @@ -149,15 +155,15 @@ class SPD_Renderer 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_renderPassShadow; + VkRenderPass m_renderPassHDRMSAA; + VkRenderPass m_renderPassPBRHDR; - VkFramebuffer m_pFrameBuffer_shadow; - VkFramebuffer m_pFrameBuffer_HDR_MSAA; - VkFramebuffer m_pFrameBuffer_PBR_HDR; + VkFramebuffer m_frameBufferShadow; + VkFramebuffer m_frameBufferHDRMSAA; + VkFramebuffer m_frameBufferPBRHDR; - std::vector m_TimeStamps; + std::vector m_timeStamps; - VkFormat m_Format; + bool m_usingDescriptorIndexing; }; diff --git a/sample/src/VK/SPDSample.cpp b/sample/src/VK/SPDSample.cpp new file mode 100644 index 0000000..fd9f7b2 --- /dev/null +++ b/sample/src/VK/SPDSample.cpp @@ -0,0 +1,561 @@ +// SPDSample +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include "stdafx.h" + +#include "SPDSample.h" +#include "base/ShaderCompilerCache.h" +#include "base/Instance.h" + +SPDSample::SPDSample(LPCSTR name) : FrameworkWindows(name) +{ + m_lastFrameTime = MillisecondsNow(); + m_time = 0; + m_bPlay = true; + + m_pGltfLoader = NULL; +} + +//-------------------------------------------------------------------------------------- +// +// OnParseCommandLine +// +//-------------------------------------------------------------------------------------- +void SPDSample::OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint32_t* pHeight, bool* pbFullScreen) +{ + // set some default values + *pWidth = 1920; + *pHeight = 1080; + *pbFullScreen = false; + m_state.isBenchmarking = true; + m_isCpuValidationLayerEnabled = false; + m_isGpuValidationLayerEnabled = false; + + //read globals + auto process = [&](json jData) + { + *pWidth = jData.value("width", *pWidth); + *pHeight = jData.value("height", *pHeight); + *pbFullScreen = jData.value("fullScreen", *pbFullScreen); + m_isCpuValidationLayerEnabled = jData.value("CpuValidationLayerEnabled", m_isCpuValidationLayerEnabled); + m_isGpuValidationLayerEnabled = jData.value("GpuValidationLayerEnabled", m_isGpuValidationLayerEnabled); + m_state.isBenchmarking = jData.value("benchmark", m_state.isBenchmarking); + m_state.downsampler = jData.value("downsampler", m_state.downsampler); + m_state.spdLoad = jData.value("spdLoad", m_state.spdLoad); + m_state.spdWaveOps = jData.value("spdWaveOps", m_state.spdWaveOps); + m_state.spdPacked = jData.value("spdPacked", m_state.spdPacked); + }; + + //read json globals from commandline + // + try + { + if (strlen(lpCmdLine) > 0) + { + auto j3 = json::parse(lpCmdLine); + process(j3); + } + } + catch (json::parse_error) + { + Trace("Error parsing commandline\n"); + exit(0); + } + + // read config file (and override values from commandline if so) + // + { + std::ifstream f("SpdSample.json"); + if (!f) + { + MessageBox(NULL, "Config file not found!\n", "Cauldron Panic!", MB_ICONERROR); + exit(0); + } + + try + { + f >> m_jsonConfigFile; + } + catch (json::parse_error) + { + MessageBox(NULL, "Error parsing GLTFSample.json!\n", "Cauldron Panic!", MB_ICONERROR); + exit(0); + } + } + + json globals = m_jsonConfigFile["globals"]; + process(globals); +} + + + +//-------------------------------------------------------------------------------------- +// +// OnCreate +// +//-------------------------------------------------------------------------------------- +void SPDSample::OnCreate(HWND hWnd) +{ + // Create Device + // + InstanceProperties ip; + ip.Init(); + m_device.SetEssentialInstanceExtensions(m_isCpuValidationLayerEnabled, m_isGpuValidationLayerEnabled, &ip); + + // Create Instance + VkInstance vulkanInstance; + VkPhysicalDevice physicalDevice; + CreateInstance("SpdSample", "Cauldron", &vulkanInstance, &physicalDevice, &ip); + + DeviceProperties dp; + dp.Init(physicalDevice); + m_device.SetEssentialDeviceExtensions(&dp); + + m_usingDescriptorIndexing = dp.AddDeviceExtensionName(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); + + VkPhysicalDeviceDescriptorIndexingFeatures descriptorIndexingFeatures = {}; + descriptorIndexingFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES; + descriptorIndexingFeatures.pNext = dp.GetNext(); + descriptorIndexingFeatures.descriptorBindingPartiallyBound = VK_TRUE; + + if (m_usingDescriptorIndexing) + { + dp.SetNewNext(&descriptorIndexingFeatures); + } + + // Create device + m_device.OnCreateEx(vulkanInstance, physicalDevice, hWnd, &dp); + + m_device.CreatePipelineCache(); + + //init the shader compiler + InitDirectXCompiler(); + InitShaderCompilerCache("ShaderLibVK", "ShaderLibVK\\ShaderCacheVK"); + CreateShaderCache(); + + // 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_pNode = new SPDRenderer(); + m_pNode->OnCreate(&m_device, &m_swapChain, m_usingDescriptorIndexing); + + // 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 = 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.spotlightCount = 1; + + m_state.spotlight[0].intensity = 10.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.downsamplerImGUISlice = 0; +} + +//-------------------------------------------------------------------------------------- +// +// OnDestroy +// +//-------------------------------------------------------------------------------------- +void SPDSample::OnDestroy() +{ + ImGUI_Shutdown(); + + m_device.GPUFlush(); + + // Fullscreen state should always be false before exiting the app. + m_swapChain.SetFullScreen(false); + + m_pNode->UnloadScene(); + m_pNode->OnDestroyWindowSizeDependentResources(); + m_pNode->OnDestroy(); + + delete m_pNode; + + m_swapChain.OnDestroyWindowSizeDependentResources(); + m_swapChain.OnDestroy(); + + //shut down the shader compiler + DestroyShaderCache(&m_device); + + if (m_pGltfLoader) + { + delete m_pGltfLoader; + m_pGltfLoader = NULL; + } + + m_device.DestroyPipelineCache(); + m_device.OnDestroy(); +} + +//-------------------------------------------------------------------------------------- +// +// OnEvent +// +//-------------------------------------------------------------------------------------- +bool SPDSample::OnEvent(MSG msg) +{ + if (ImGUI_WndProcHandler(msg.hwnd, msg.message, msg.wParam, msg.lParam)) + return true; + return true; +} + +//-------------------------------------------------------------------------------------- +// +// SetFullScreen +// +//-------------------------------------------------------------------------------------- +void SPDSample::SetFullScreen(bool fullscreen) +{ + m_device.GPUFlush(); + + m_swapChain.SetFullScreen(fullscreen); +} + +//-------------------------------------------------------------------------------------- +// +// OnResize +// +//-------------------------------------------------------------------------------------- +void SPDSample::OnResize(uint32_t width, uint32_t height) +{ + if (m_Width != width || m_Height != height) + { + // Flush GPU + // + m_device.GPUFlush(); + + // If resizing but no minimizing + // + if (m_Width > 0 && m_Height > 0) + { + if (m_pNode != NULL) + { + m_pNode->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, false); + if (m_pNode != NULL) + { + m_pNode->OnCreateWindowSizeDependentResources(&m_swapChain, m_Width, m_Height); + } + } + } + m_state.camera.SetFov(XM_PI / 4, m_Width, m_Height, 0.1f, 1000.0f); +} + +void SPDSample::BuildUI() +{ + ImGuiStyle& style = ImGui::GetStyle(); + style.FrameBorderSize = 1.0f; + + ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(250, 700), ImGuiCond_FirstUseEver); + + bool opened = true; + ImGui::Begin("Stats", &opened); + + if (ImGui::CollapsingHeader("Info", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::Text("Resolution : %ix%i", m_Width, m_Height); + } + + if (ImGui::CollapsingHeader("Downsampler", ImGuiTreeNodeFlags_DefaultOpen)) + { + if (m_usingDescriptorIndexing) { + // Downsample settings + const char* downsampleItemNames[] = + { + "PS", + "Multipass CS", + "SPD CS", + }; + ImGui::Combo("Downsampler Options", (int*)&m_state.downsampler, downsampleItemNames, _countof(downsampleItemNames)); + + // SPD Version + // Use load or linear sample to fetch data from source texture + const char* spdLoadItemNames[] = + { + "Load", + "Linear Sampler", + }; + ImGui::Combo("SPD Load / Linear Sampler", (int*)&m_state.spdLoad, spdLoadItemNames, _countof(spdLoadItemNames)); + + // if possible give choice of using wave operations + if (m_device.GetPhysicalDeviceSubgroupProperties().supportedOperations + & VK_SUBGROUP_FEATURE_QUAD_BIT) + { + const char* spdWaveOpsItemNames[] = + { + "No-WaveOps", + "WaveOps", + }; + ImGui::Combo("SPD No-WaveOps / WaveOps", (int*)&m_state.spdWaveOps, spdWaveOpsItemNames, _countof(spdWaveOpsItemNames)); + } + else { + const char* spdWaveOpsItemNames[] = + { + "No-WaveOps", + }; + ImGui::Combo("SPD No-WaveOps", (int*)&m_state.spdWaveOps, spdWaveOpsItemNames, _countof(spdWaveOpsItemNames)); + } + + // Non-Packed or Packed Version + const char* spdPackedItemNames[] = + { + "Non-Packed", + "Packed", + }; + ImGui::Combo("SPD Non-Packed / Packed Version", (int*)&m_state.spdPacked, spdPackedItemNames, _countof(spdPackedItemNames)); + } + else { + // Downsample settings + const char* downsampleItemNames[] = + { + "PS", + "Multipass CS" + }; + ImGui::Combo("Downsampler Options", (int*)&m_state.downsampler, downsampleItemNames, _countof(downsampleItemNames)); + } + } + + if (ImGui::CollapsingHeader("Lighting", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::SliderFloat("exposure", &m_state.exposure, 0.0f, 2.0f); + ImGui::SliderFloat("emmisive", &m_state.emmisiveFactor, 1.0f, 1000.0f, NULL, 1.0f); + ImGui::SliderFloat("iblFactor", &m_state.iblFactor, 0.0f, 2.0f); + } + + const char* tonemappers[] = { "Timothy", "DX11DSK", "Reinhard", "Uncharted2Tonemap", "ACES", "No tonemapper" }; + ImGui::Combo("tone mapper", &m_state.toneMapper, tonemappers, _countof(tonemappers)); + + const char* skyDomeType[] = { "Procedural Sky", "cubemap", "Simple clear" }; + ImGui::Combo("SkyDome", &m_state.skyDomeType, skyDomeType, _countof(skyDomeType)); + + const char* cameraControl[] = { "WASD", "Orbit" }; + static int cameraControlSelected = 1; + ImGui::Combo("Camera", &cameraControlSelected, cameraControl, _countof(cameraControl)); + + if (ImGui::CollapsingHeader("Profiler", ImGuiTreeNodeFlags_DefaultOpen)) + { + std::vector timeStamps = m_pNode->GetTimingValues(); + if (timeStamps.size() > 0) + { + for (uint32_t i = 1; i < timeStamps.size(); i++) + { + ImGui::Text("%-22s: %7.1f", timeStamps[i].m_label.c_str(), timeStamps[i].m_microseconds); + } + + //scrolling data and average computing + static float values[128]; + values[127] = timeStamps.back().m_microseconds; + for (uint32_t i = 0; i < 128 - 1; i++) { values[i] = values[i + 1]; } + ImGui::PlotLines("", values, 128, 0, "GPU frame time (us)", 0.0f, 30000.0f, ImVec2(0, 80)); + } + } + +#ifdef USE_VMA + if (ImGui::Button("Save VMA json")) + { + char* pJson; + vmaBuildStatsString(m_device.GetAllocator(), &pJson, VK_TRUE); + + static char filename[256]; + time_t now = time(NULL); + tm buf; + localtime_s(&buf, &now); + strftime(filename, sizeof(filename), "VMA_%Y%m%d_%H%M%S.json", &buf); + std::ofstream ofs(filename, std::ofstream::out); + ofs << pJson; + ofs.close(); + vmaFreeStatsString(m_device.GetAllocator(), pJson); + } +#endif + + ImGui::End(); + + // If the mouse was not used by the GUI then it's for the camera + // + ImGuiIO& io = ImGui::GetIO(); + if (io.WantCaptureMouse == false) + { + 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 (cameraControlSelected == 0) + { + // WASD + // + m_state.camera.UpdateCameraWASD(m_roll, m_pitch, io.KeysDown, io.DeltaTime); + } + else if (cameraControlSelected == 1) + { + // Orbiting + // + m_distance -= (float)io.MouseWheel / 3.0f; + m_distance = std::max(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); + } + } +} + +//-------------------------------------------------------------------------------------- +// +// OnRender, updates the state from the UI, animates, transforms and renders the scene +// +//-------------------------------------------------------------------------------------- +void SPDSample::OnRender() +{ + // Get timings + // + double timeNow = MillisecondsNow(); + float deltaTime = (float)(timeNow - m_lastFrameTime); + m_lastFrameTime = timeNow; + + // Build UI and set the scene state. Note that the rendering of the UI happens later. + // + ImGUI_UpdateIO(); + ImGui::NewFrame(); + + static int loadingStage = 0; + if (loadingStage >= 0) + { + // LoadScene needs to be called a number of times, the scene is not fully loaded until it returns -1 + // This is done so we can display a progress bar when the scene is loading + if (m_pGltfLoader == NULL) + { + m_pGltfLoader = new GLTFCommon(); + m_pGltfLoader->Load("..\\media\\DamagedHelmet\\glTF\\", "DamagedHelmet.gltf"); + loadingStage = 0; + + // set benchmarking state if enabled + // + json scene = m_jsonConfigFile["scenes"][0]; + + // set default camera + // + json camera = scene["camera"]; + XMVECTOR from = GetVector(GetElementJsonArray(camera, "defaultFrom", { 0.0, 0.0, 10.0 })); + XMVECTOR to = GetVector(GetElementJsonArray(camera, "defaultTo", { 0.0, 0.0, 0.0 })); + m_state.camera.LookAt(from, to); + m_roll = m_state.camera.GetYaw(); + m_pitch = m_state.camera.GetPitch(); + m_distance = m_state.camera.GetDistance(); + + // set benchmarking state if enabled + + if (m_state.isBenchmarking) + { + BenchmarkConfig(scene["BenchmarkSettings"], -1, m_pGltfLoader); + } + } + loadingStage = m_pNode->LoadScene(m_pGltfLoader, loadingStage); + if (loadingStage == 0) + { + m_time = 0; + m_loadingScene = false; + } + } + else if (m_pGltfLoader && m_state.isBenchmarking) + { + // benchmarking takes control of the time, and exits the app when the animation is done + std::vector timeStamps = m_pNode->GetTimingValues(); + + const std::string* pFilename; + m_time = BenchmarkLoop(timeStamps, &m_state.camera, &pFilename); + + BuildUI(); + } + else + { + BuildUI(); + } + + // Animate and transform the scene + // + if (m_pGltfLoader) + { + m_pGltfLoader->SetAnimationTime(0, m_time); + m_pGltfLoader->TransformScene(0, XMMatrixIdentity()); + } + + m_state.time = m_time; + + // Do Render frame using AFR + // + m_pNode->OnRender(&m_state, &m_swapChain); + + m_swapChain.Present(); +} + + +//-------------------------------------------------------------------------------------- +// +// WinMain +// +//-------------------------------------------------------------------------------------- +int WINAPI WinMain(HINSTANCE hInstance, + HINSTANCE hPrevInstance, + LPSTR lpCmdLine, + int nCmdShow) +{ + LPCSTR Name = "FFX SPD SampleVK v2.0"; + + // create new Vulkan sample + return RunFramework(hInstance, lpCmdLine, nCmdShow, new SPDSample(Name)); +} diff --git a/sample/src/DX12/SPD_Sample.h b/sample/src/VK/SPDSample.h similarity index 78% rename from sample/src/DX12/SPD_Sample.h rename to sample/src/VK/SPDSample.h index a72ede9..2ba385d 100644 --- a/sample/src/DX12/SPD_Sample.h +++ b/sample/src/VK/SPDSample.h @@ -18,7 +18,7 @@ // THE SOFTWARE. #pragma once -#include "SPD_Renderer.h" +#include "SPDRenderer.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. @@ -35,12 +35,14 @@ // - uses the SampleRenderer to update all the state to the GPU and do the rendering // -class SPD_Sample : public FrameworkWindows +class SPDSample : public FrameworkWindows { public: - SPD_Sample(LPCSTR name); + SPDSample(LPCSTR name); + void OnParseCommandLine(LPSTR lpCmdLine, uint32_t* pWidth, uint32_t* pHeight, bool* pbFullScreen); void OnCreate(HWND hWnd); void OnDestroy(); + void BuildUI(); void OnRender(); bool OnEvent(MSG msg); void OnResize(uint32_t Width, uint32_t Height); @@ -50,18 +52,25 @@ class SPD_Sample : public FrameworkWindows Device m_device; SwapChain m_swapChain; - GLTFCommon *m_pGltfLoader; + GLTFCommon *m_pGltfLoader = nullptr; + bool m_loadingScene = false; - SPD_Renderer *m_Node; - SPD_Renderer::State m_state; + SPDRenderer *m_pNode = nullptr; + SPDRenderer::State m_state; 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; + // json config file + json m_jsonConfigFile; + bool m_isCpuValidationLayerEnabled; + bool m_isGpuValidationLayerEnabled; + bool m_bPlay; -}; + + bool m_usingDescriptorIndexing; +}; \ No newline at end of file diff --git a/sample/src/VK/SPDVersions.cpp b/sample/src/VK/SPDVersions.cpp new file mode 100644 index 0000000..7d9b57e --- /dev/null +++ b/sample/src/VK/SPDVersions.cpp @@ -0,0 +1,204 @@ +// SPDSample +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include "stdafx.h" +#include "Base\DynamicBufferRing.h" +#include "Base\StaticBufferPool.h" +#include "Base\UploadHeap.h" +#include "Base\Texture.h" +#include "Base\Helper.h" +#include "SPDVersions.h" + + +namespace CAULDRON_VK +{ + void SPDVersions::OnCreate(Device *pDevice, UploadHeap *pUploadHeap, ResourceViewHeaps *pResourceViewHeaps) + { + m_pDevice = pDevice; + + // check if subgroup operations are supported, otherwise we need to fallback to the LDS only version + if (pDevice->GetPhysicalDeviceSubgroupProperties().supportedOperations + & VK_SUBGROUP_FEATURE_QUAD_BIT) + { + m_spd_WaveOps_NonPacked.OnCreate(pDevice, pUploadHeap, pResourceViewHeaps, SPDLoad::SPDLoad, SPDWaveOps::SPDWaveOps, SPDPacked::SPDNonPacked); + m_spd_WaveOps_Packed.OnCreate(pDevice, pUploadHeap, pResourceViewHeaps, SPDLoad::SPDLoad, SPDWaveOps::SPDWaveOps, SPDPacked::SPDPacked); + + m_spd_WaveOps_NonPacked_Linear_Sampler.OnCreate(pDevice, pUploadHeap, pResourceViewHeaps, SPDLoad::SPDLinearSampler, SPDWaveOps::SPDWaveOps, SPDPacked::SPDNonPacked); + m_spd_WaveOps_Packed_Linear_Sampler.OnCreate(pDevice, pUploadHeap, pResourceViewHeaps, SPDLoad::SPDLinearSampler, SPDWaveOps::SPDWaveOps, SPDPacked::SPDPacked); + } + + // fallback path + m_spd_No_WaveOps_NonPacked.OnCreate(pDevice, pUploadHeap, pResourceViewHeaps, SPDLoad::SPDLoad, SPDWaveOps::SPDNoWaveOps, SPDPacked::SPDNonPacked); + m_spd_No_WaveOps_Packed.OnCreate(pDevice, pUploadHeap, pResourceViewHeaps, SPDLoad::SPDLoad, SPDWaveOps::SPDNoWaveOps, SPDPacked::SPDPacked); + + m_spd_No_WaveOps_NonPacked_Linear_Sampler.OnCreate(pDevice, pUploadHeap, pResourceViewHeaps, SPDLoad::SPDLinearSampler, SPDWaveOps::SPDNoWaveOps, SPDPacked::SPDNonPacked); + m_spd_No_WaveOps_Packed_Linear_Sampler.OnCreate(pDevice, pUploadHeap, pResourceViewHeaps, SPDLoad::SPDLinearSampler, SPDWaveOps::SPDNoWaveOps, SPDPacked::SPDPacked); + } + + void SPDVersions::OnDestroy() + { + m_spd_No_WaveOps_NonPacked.OnDestroy(); + m_spd_No_WaveOps_Packed.OnDestroy(); + + m_spd_No_WaveOps_NonPacked_Linear_Sampler.OnDestroy(); + m_spd_No_WaveOps_Packed_Linear_Sampler.OnDestroy(); + + if (m_pDevice->GetPhysicalDeviceSubgroupProperties().supportedOperations + & VK_SUBGROUP_FEATURE_QUAD_BIT) + { + m_spd_WaveOps_NonPacked.OnDestroy(); + m_spd_WaveOps_Packed.OnDestroy(); + + m_spd_WaveOps_NonPacked_Linear_Sampler.OnDestroy(); + m_spd_WaveOps_Packed_Linear_Sampler.OnDestroy(); + } + } + + uint32_t SPDVersions::GetMaxMIPLevelCount(uint32_t Width, uint32_t Height) + { + int resolution = max(Width, Height); + return (static_cast(min(floor(log2(resolution)), 12))); + } + + void SPDVersions::Dispatch(VkCommandBuffer cmd_buf, SPDLoad spdLoad, SPDWaveOps spdWaveOps, SPDPacked spdPacked) + { + switch (spdLoad) + { + case SPDLoad::SPDLoad: + { + switch (spdWaveOps) + { + case SPDWaveOps::SPDWaveOps: + switch (spdPacked) + { + case SPDPacked::SPDNonPacked: + m_spd_WaveOps_NonPacked.Draw(cmd_buf); + break; + case SPDPacked::SPDPacked: + m_spd_WaveOps_Packed.Draw(cmd_buf); + break; + } + break; + case SPDWaveOps::SPDNoWaveOps: + switch (spdPacked) + { + case SPDPacked::SPDNonPacked: + m_spd_No_WaveOps_NonPacked.Draw(cmd_buf); + break; + case SPDPacked::SPDPacked: + m_spd_No_WaveOps_Packed.Draw(cmd_buf); + break; + } + } + break; + } + case SPDLoad::SPDLinearSampler: + { + switch (spdWaveOps) + { + case SPDWaveOps::SPDWaveOps: + switch (spdPacked) + { + case SPDPacked::SPDNonPacked: + m_spd_WaveOps_NonPacked_Linear_Sampler.Draw(cmd_buf); + break; + case SPDPacked::SPDPacked: + m_spd_WaveOps_Packed_Linear_Sampler.Draw(cmd_buf); + break; + } + break; + case SPDWaveOps::SPDNoWaveOps: + switch (spdPacked) + { + case SPDPacked::SPDNonPacked: + m_spd_No_WaveOps_NonPacked_Linear_Sampler.Draw(cmd_buf); + break; + case SPDPacked::SPDPacked: + m_spd_No_WaveOps_Packed_Linear_Sampler.Draw(cmd_buf); + break; + } + } + break; + } + } + } + + void SPDVersions::GUI(SPDLoad spdLoad, SPDWaveOps spdWaveOps, SPDPacked spdPacked, int* pSlice) + { + switch (spdLoad) + { + case SPDLoad::SPDLoad: + { + switch (spdWaveOps) + { + case SPDWaveOps::SPDWaveOps: + switch (spdPacked) + { + case SPDPacked::SPDNonPacked: + m_spd_WaveOps_NonPacked.GUI(pSlice); + break; + case SPDPacked::SPDPacked: + m_spd_WaveOps_Packed.GUI(pSlice); + break; + } + break; + case SPDWaveOps::SPDNoWaveOps: + switch (spdPacked) + { + case SPDPacked::SPDNonPacked: + m_spd_No_WaveOps_NonPacked.GUI(pSlice); + break; + case SPDPacked::SPDPacked: + m_spd_No_WaveOps_Packed.GUI(pSlice); + break; + } + } + break; + } + case SPDLoad::SPDLinearSampler: + { + switch (spdWaveOps) + { + case SPDWaveOps::SPDWaveOps: + switch (spdPacked) + { + case SPDPacked::SPDNonPacked: + m_spd_WaveOps_NonPacked_Linear_Sampler.GUI(pSlice); + break; + case SPDPacked::SPDPacked: + m_spd_WaveOps_Packed_Linear_Sampler.GUI(pSlice); + break; + } + break; + case SPDWaveOps::SPDNoWaveOps: + switch (spdPacked) + { + case SPDPacked::SPDNonPacked: + m_spd_No_WaveOps_NonPacked_Linear_Sampler.GUI(pSlice); + break; + case SPDPacked::SPDPacked: + m_spd_No_WaveOps_Packed_Linear_Sampler.GUI(pSlice); + break; + } + } + break; + } + } + } +} \ No newline at end of file diff --git a/sample/src/VK/SPDVersions.h b/sample/src/VK/SPDVersions.h new file mode 100644 index 0000000..b4c7e92 --- /dev/null +++ b/sample/src/VK/SPDVersions.h @@ -0,0 +1,55 @@ +// SPDSample +// +// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +#pragma once + +#include "PostProc/PostProcCS.h" +#include "PostProc/PostProcPS.h" +#include "Base/ResourceViewHeaps.h" + +#include "SPDCS.h" + +namespace CAULDRON_VK +{ + class SPDVersions + { + public: + void OnCreate(Device *pDevice, UploadHeap *pUploadHeap, ResourceViewHeaps *pResourceViewHeaps); + void OnDestroy(); + + void Dispatch(VkCommandBuffer cmd_buf, SPDLoad spdLoad, SPDWaveOps spdWaveOps, SPDPacked spdPacked); + void GUI(SPDLoad spdLoad, SPDWaveOps spdWaveOps, SPDPacked spdPacked, int *pSlice); + + private: + Device *m_pDevice = NULL; + + SPDCS m_spd_WaveOps_NonPacked; + SPDCS m_spd_No_WaveOps_NonPacked; + + SPDCS m_spd_WaveOps_Packed; + SPDCS m_spd_No_WaveOps_Packed; + + SPDCS m_spd_WaveOps_NonPacked_Linear_Sampler; + SPDCS m_spd_No_WaveOps_NonPacked_Linear_Sampler; + + SPDCS m_spd_WaveOps_Packed_Linear_Sampler; + SPDCS m_spd_No_WaveOps_Packed_Linear_Sampler; + + uint32_t GetMaxMIPLevelCount(uint32_t Width, uint32_t Height); + }; +} diff --git a/sample/src/VK/SPD_CS.cpp b/sample/src/VK/SPD_CS.cpp deleted file mode 100644 index 72351e0..0000000 --- a/sample/src/VK/SPD_CS.cpp +++ /dev/null @@ -1,353 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#include "stdafx.h" -#include "Base\Device.h" -#include "Base\ShaderCompilerHelper.h" -#include "Base\ExtDebugMarkers.h" -#include "Base\Imgui.h" - -#include "SPD_CS.h" - -namespace CAULDRON_VK -{ - void SPD_CS::OnCreate( - Device* pDevice, - ResourceViewHeaps *pResourceViewHeaps, - VkFormat outFormat, - bool fallback, - bool packed - ) - { - m_pDevice = pDevice; - m_pResourceViewHeaps = pResourceViewHeaps; - m_outFormat = outFormat; - - // create the descriptor set layout - // the shader needs - // source image: storage image (read-only) - // destination image: storage image - // global atomic counter: storage buffer - { - std::vector layoutBindings(3); - layoutBindings[0].binding = 0; - layoutBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - layoutBindings[0].descriptorCount = 1; - layoutBindings[0].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - layoutBindings[0].pImmutableSamplers = NULL; - - layoutBindings[1].binding = 1; - layoutBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - layoutBindings[1].descriptorCount = SPD_MAX_MIP_LEVELS; - layoutBindings[1].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - layoutBindings[1].pImmutableSamplers = NULL; - - layoutBindings[2].binding = 2; - layoutBindings[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - layoutBindings[2].descriptorCount = 1; - layoutBindings[2].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - layoutBindings[2].pImmutableSamplers = NULL; - - VkDescriptorSetLayoutCreateInfo descriptor_layout = {}; - descriptor_layout.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; - descriptor_layout.pNext = NULL; - descriptor_layout.bindingCount = (uint32_t)layoutBindings.size(); - descriptor_layout.pBindings = layoutBindings.data(); - - VkResult res = vkCreateDescriptorSetLayout(pDevice->GetDevice(), &descriptor_layout, NULL, &m_descriptorSetLayout); - assert(res == VK_SUCCESS); - } - - // Create global atomic counter - { - VkBufferCreateInfo bufferInfo = {}; - bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; - bufferInfo.flags = 0; - bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - bufferInfo.queueFamilyIndexCount = 0; - bufferInfo.pQueueFamilyIndices = NULL; - bufferInfo.size = sizeof(int) * 1; - bufferInfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; - - VmaAllocationCreateInfo bufferAllocCreateInfo = {}; - bufferAllocCreateInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; - bufferAllocCreateInfo.flags = VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT; - bufferAllocCreateInfo.pUserData = "SpdGlobalAtomicCounter"; - VmaAllocationInfo bufferAllocInfo = {}; - vmaCreateBuffer(m_pDevice->GetAllocator(), &bufferInfo, &bufferAllocCreateInfo, &m_globalCounter, - &m_globalCounterAllocation, &bufferAllocInfo); - } - - VkPipelineShaderStageCreateInfo computeShader; - DefineList defines; - - if (fallback) { - defines["SPD_NO_WAVE_OPERATIONS"] = std::to_string(1); - } - if (packed) { - defines["A_HALF"] = std::to_string(1); - defines["SPD_PACKED_ONLY"] = std::to_string(1); - } - - VkResult res = VKCompileFromFile(m_pDevice->GetDevice(), VK_SHADER_STAGE_COMPUTE_BIT, - "SPD_Integration.glsl", "main", &defines, &computeShader); - assert(res == VK_SUCCESS); - - // Create pipeline layout - // - VkPipelineLayoutCreateInfo pPipelineLayoutCreateInfo = {}; - pPipelineLayoutCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; - pPipelineLayoutCreateInfo.pNext = NULL; - - // push constants - VkPushConstantRange pushConstantRange = {}; - pushConstantRange.offset = 0; - pushConstantRange.size = sizeof(PushConstants); - pushConstantRange.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - pPipelineLayoutCreateInfo.pushConstantRangeCount = 1; - pPipelineLayoutCreateInfo.pPushConstantRanges = &pushConstantRange; - - pPipelineLayoutCreateInfo.setLayoutCount = 1; - pPipelineLayoutCreateInfo.pSetLayouts = &m_descriptorSetLayout; - - res = vkCreatePipelineLayout(pDevice->GetDevice(), &pPipelineLayoutCreateInfo, NULL, &m_pipelineLayout); - assert(res == VK_SUCCESS); - - // Create pipeline - // - VkComputePipelineCreateInfo pipeline = {}; - pipeline.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; - pipeline.pNext = NULL; - pipeline.flags = 0; - pipeline.layout = m_pipelineLayout; - pipeline.stage = computeShader; - pipeline.basePipelineHandle = VK_NULL_HANDLE; - pipeline.basePipelineIndex = 0; - - res = vkCreateComputePipelines(pDevice->GetDevice(), pDevice->GetPipelineCache(), 1, &pipeline, NULL, &m_pipeline); - assert(res == VK_SUCCESS); - - m_pResourceViewHeaps->AllocDescriptor(m_descriptorSetLayout, &m_descriptorSet); - } - - void SPD_CS::OnCreateWindowSizeDependentResources( - VkCommandBuffer cmd_buf, - uint32_t Width, - uint32_t Height, - Texture *pInput, - int mips - ) - { - m_Width = Width; - m_Height = Height; - m_mipCount = mips; - - VkImageCreateInfo image_info = {}; - image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; - image_info.pNext = NULL; - image_info.imageType = VK_IMAGE_TYPE_2D; - image_info.format = m_outFormat; - image_info.extent.width = m_Width >> 1; - image_info.extent.height = m_Height >> 1; - image_info.extent.depth = 1; - image_info.mipLevels = m_mipCount; - image_info.arrayLayers = 1; - image_info.samples = VK_SAMPLE_COUNT_1_BIT; - image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - image_info.queueFamilyIndexCount = 0; - image_info.pQueueFamilyIndices = NULL; - image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - image_info.usage = (VkImageUsageFlags)(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT); - image_info.flags = 0; - image_info.tiling = VK_IMAGE_TILING_OPTIMAL; - m_result.Init(m_pDevice, &image_info, "SpdDestinationMips"); - - // transition layout undefined to general layout? - VkImageMemoryBarrier imageMemoryBarrier = {}; - imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - imageMemoryBarrier.pNext = NULL; - imageMemoryBarrier.srcAccessMask = 0; - imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT; - imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; - imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - imageMemoryBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - imageMemoryBarrier.subresourceRange.baseMipLevel = 0; - imageMemoryBarrier.subresourceRange.levelCount = m_mipCount; - imageMemoryBarrier.subresourceRange.baseArrayLayer = 0; - imageMemoryBarrier.subresourceRange.layerCount = 1; - imageMemoryBarrier.image = m_result.Resource(); - - // transition general layout if detination image to shader read only for source image - vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier); - - // Create views for the mip chain - // - // source ----------- - // - pInput->CreateSRV(&m_SRV, 0); - - // Create and initialize the Descriptor Sets (all of them use the same Descriptor Layout) - // Create and initialize descriptor set for sampled image - VkDescriptorImageInfo desc_source_image = {}; - desc_source_image.sampler = VK_NULL_HANDLE; - desc_source_image.imageView = m_SRV; - desc_source_image.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - - std::vector writes(3); - writes[0] = {}; - writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[0].pNext = NULL; - writes[0].dstSet = m_descriptorSet; - writes[0].descriptorCount = 1; - writes[0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - writes[0].pImageInfo = &desc_source_image; - writes[0].dstBinding = 0; - writes[0].dstArrayElement = 0; - - // Create and initialize descriptor set for storage image - std::vector desc_storage_images(m_mipCount); - - for (int i = 0; i < m_mipCount; i++) - { - // destination ----------- - m_result.CreateRTV(&m_RTV[i], i); - - desc_storage_images[i] = {}; - desc_storage_images[i].sampler = VK_NULL_HANDLE; - desc_storage_images[i].imageView = m_RTV[i]; - desc_storage_images[i].imageLayout = VK_IMAGE_LAYOUT_GENERAL; - } - - writes[1] = {}; - writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[1].pNext = NULL; - writes[1].dstSet = m_descriptorSet; - writes[1].descriptorCount = m_mipCount; - writes[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - writes[1].pImageInfo = desc_storage_images.data(); - writes[1].dstBinding = 1; - writes[1].dstArrayElement = 0; - - VkDescriptorBufferInfo desc_buffer = {}; - desc_buffer.buffer = m_globalCounter; - desc_buffer.offset = 0; - desc_buffer.range = sizeof(int) * 1; - - writes[2] = {}; - writes[2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[2].pNext = NULL; - writes[2].dstSet = m_descriptorSet; - writes[2].descriptorCount = 1; - writes[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - writes[2].pBufferInfo = &desc_buffer; - writes[2].dstBinding = 2; - writes[2].dstArrayElement = 0; - - vkUpdateDescriptorSets(m_pDevice->GetDevice(), (uint32_t)writes.size(), writes.data(), 0, NULL); - } - - void SPD_CS::OnDestroyWindowSizeDependentResources() - { - vkDestroyImageView(m_pDevice->GetDevice(), m_SRV, NULL); - for (int i = 0; i < m_mipCount; i++) - { - vkDestroyImageView(m_pDevice->GetDevice(), m_RTV[i], NULL); - } - - m_result.OnDestroy(); - } - - void SPD_CS::OnDestroy() - { - - m_pResourceViewHeaps->FreeDescriptor(m_descriptorSet); - - vmaDestroyBuffer(m_pDevice->GetAllocator(), m_globalCounter, m_globalCounterAllocation); - - vkDestroyPipeline(m_pDevice->GetDevice(), m_pipeline, nullptr); - vkDestroyPipelineLayout(m_pDevice->GetDevice(), m_pipelineLayout, nullptr); - vkDestroyDescriptorSetLayout(m_pDevice->GetDevice(), m_descriptorSetLayout, NULL); - } - - void SPD_CS::Draw(VkCommandBuffer cmd_buf) - { - // downsample - // - - // initialize global atomic counter to 0 - vmaMapMemory(m_pDevice->GetAllocator(), m_globalCounterAllocation, (void**)&m_pCounter); - *m_pCounter = 0; - vmaUnmapMemory(m_pDevice->GetAllocator(), m_globalCounterAllocation); - - // transition general layout if detination image to shader read only for source image - vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 0, nullptr); - - SetPerfMarkerBegin(cmd_buf, "SPD_CS"); - - // Bind Pipeline - // - vkCmdBindPipeline(cmd_buf, VK_PIPELINE_BIND_POINT_COMPUTE, m_pipeline); - - // should be / 64 - uint32_t dispatchX = (((m_Width + 63) >> (6))); - uint32_t dispatchY = (((m_Height + 63) >> (6))); - uint32_t dispatchZ = 1; - - // single pass for storage buffer? - //uint32_t uniformOffsets[1] = { (uint32_t)constantBuffer.offset }; - vkCmdBindDescriptorSets(cmd_buf, VK_PIPELINE_BIND_POINT_COMPUTE, m_pipelineLayout, 0, 1, &m_descriptorSet, 0, nullptr); - - // Bind push constants - // - PushConstants data; - data.mips = m_mipCount; - data.numWorkGroups = dispatchX * dispatchY * dispatchZ; - vkCmdPushConstants(cmd_buf, m_pipelineLayout, - VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(PushConstants), (void*)&data); - - // Draw - // - vkCmdDispatch(cmd_buf, dispatchX, dispatchY, dispatchZ); - - // transition general layout if detination image to shader read only for source image - vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 0, nullptr); - - SetPerfMarkerEnd(cmd_buf); - } - - void SPD_CS::Gui() - { - bool opened = true; - ImGui::Begin("Downsample", &opened); - - ImGui::Image((ImTextureID)m_SRV, ImVec2(320, 180)); - - for (int i = 0; i < m_mipCount; i++) - { - ImGui::Image((ImTextureID)m_RTV[i], ImVec2(320, 180)); - } - - ImGui::End(); - } -} \ No newline at end of file diff --git a/sample/src/VK/SPD_CS.h b/sample/src/VK/SPD_CS.h deleted file mode 100644 index 6275f07..0000000 --- a/sample/src/VK/SPD_CS.h +++ /dev/null @@ -1,77 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -#pragma once - -#include "Base/StaticBufferPool.h" -#include "Base/Texture.h" -#include "Base/DynamicBufferRing.h" - -namespace CAULDRON_VK -{ -#define SPD_MAX_MIP_LEVELS 12 - - class SPD_CS - { - public: - void OnCreate(Device* pDevice, ResourceViewHeaps *pResourceViewHeaps, VkFormat outFormat, bool fallback, bool packed); - void OnDestroy(); - - void OnCreateWindowSizeDependentResources(VkCommandBuffer cmd_buf, uint32_t Width, uint32_t Height, Texture *pInput, int mips); - void OnDestroyWindowSizeDependentResources(); - - void Draw(VkCommandBuffer cmd_buf); - Texture *GetTexture() { return &m_result; } - VkImageView GetTextureView(int i) { return m_RTV[i]; } - void Gui(); - - struct PushConstants - { - int mips; - int numWorkGroups; - int padding[2]; - }; - - private: - Device *m_pDevice; - VkFormat m_outFormat; - - Texture m_result; - - VkImageView m_RTV[SPD_MAX_MIP_LEVELS]; // destinations (mips) - VkImageView m_SRV; // source - VkDescriptorSet m_descriptorSet; - - ResourceViewHeaps *m_pResourceViewHeaps; - DynamicBufferRing *m_pConstantBufferRing; - - uint32_t m_Width; - uint32_t m_Height; - int m_mipCount; - - VkDescriptorSetLayout m_descriptorSetLayout; - - VkPipelineLayout m_pipelineLayout; - VkPipeline m_pipeline; - - VkBuffer m_globalCounter; - VmaAllocation m_globalCounterAllocation; - - uint32_t* m_pCounter; - }; -} \ No newline at end of file diff --git a/sample/src/VK/SPD_CS_Linear_Sampler.cpp b/sample/src/VK/SPD_CS_Linear_Sampler.cpp deleted file mode 100644 index 0a51534..0000000 --- a/sample/src/VK/SPD_CS_Linear_Sampler.cpp +++ /dev/null @@ -1,397 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#include "stdafx.h" -#include "Base\Device.h" -#include "Base\ShaderCompilerHelper.h" -#include "Base\ExtDebugMarkers.h" -#include "Base\Imgui.h" - -#include "SPD_CS_Linear_Sampler.h" - -namespace CAULDRON_VK -{ - void SPD_CS_Linear_Sampler::OnCreate( - Device* pDevice, - ResourceViewHeaps *pResourceViewHeaps, - VkFormat outFormat, - bool fallback, - bool packed - ) - { - m_pDevice = pDevice; - m_pResourceViewHeaps = pResourceViewHeaps; - m_outFormat = outFormat; - - // create the descriptor set layout - // the shader needs - // source image: sampled image - // destination image: storage image - // global atomic counter: storage buffer - // sampler - { - std::vector layoutBindings(4); - layoutBindings[0].binding = 0; - layoutBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; - layoutBindings[0].descriptorCount = 1; - layoutBindings[0].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - layoutBindings[0].pImmutableSamplers = NULL; - - layoutBindings[1].binding = 1; - layoutBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - layoutBindings[1].descriptorCount = SPD_MAX_MIP_LEVELS; - layoutBindings[1].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - layoutBindings[1].pImmutableSamplers = NULL; - - layoutBindings[2].binding = 2; - layoutBindings[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - layoutBindings[2].descriptorCount = 1; - layoutBindings[2].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - layoutBindings[2].pImmutableSamplers = NULL; - - layoutBindings[3].binding = 3; - layoutBindings[3].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER; - layoutBindings[3].descriptorCount = 1; - layoutBindings[3].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - layoutBindings[3].pImmutableSamplers = NULL; - - VkDescriptorSetLayoutCreateInfo descriptor_layout = {}; - descriptor_layout.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; - descriptor_layout.pNext = NULL; - descriptor_layout.bindingCount = (uint32_t)layoutBindings.size(); - descriptor_layout.pBindings = layoutBindings.data(); - - VkResult res = vkCreateDescriptorSetLayout(pDevice->GetDevice(), &descriptor_layout, NULL, &m_descriptorSetLayout); - assert(res == VK_SUCCESS); - } - - // The sampler we want to use, needs to match the SPD Reduction function in the shader - // linear sampler: - // -> AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3){return (v0+v1+v2+v3)*0.25;} - // point sampler: - // -> AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3){return v3;} - { - VkSamplerCreateInfo info = {}; - info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; - info.magFilter = VK_FILTER_LINEAR; - info.minFilter = VK_FILTER_LINEAR; - info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; - info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - info.minLod = -1000; - info.maxLod = 1000; - info.maxAnisotropy = 1.0f; - VkResult res = vkCreateSampler(pDevice->GetDevice(), &info, NULL, &m_sampler); - assert(res == VK_SUCCESS); - } - - // Create global atomic counter - { - VkBufferCreateInfo bufferInfo = {}; - bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; - bufferInfo.flags = 0; - bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - bufferInfo.queueFamilyIndexCount = 0; - bufferInfo.pQueueFamilyIndices = NULL; - bufferInfo.size = sizeof(int) * 1; - bufferInfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; - - VmaAllocationCreateInfo bufferAllocCreateInfo = {}; - bufferAllocCreateInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; - bufferAllocCreateInfo.flags = VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT; - bufferAllocCreateInfo.pUserData = "SpdGlobalAtomicCounter"; - VmaAllocationInfo bufferAllocInfo = {}; - vmaCreateBuffer(m_pDevice->GetAllocator(), &bufferInfo, &bufferAllocCreateInfo, &m_globalCounter, - &m_globalCounterAllocation, &bufferAllocInfo); - } - - VkPipelineShaderStageCreateInfo computeShader; - DefineList defines; - - if (fallback) { - defines["SPD_NO_WAVE_OPERATIONS"] = std::to_string(1); - } - if (packed) { - defines["A_HALF"] = std::to_string(1); - defines["SPD_PACKED_ONLY"] = std::to_string(1); - } - - VkResult res = VKCompileFromFile(m_pDevice->GetDevice(), VK_SHADER_STAGE_COMPUTE_BIT, - "SPD_Integration_Linear_Sampler.glsl", "main", &defines, &computeShader); - assert(res == VK_SUCCESS); - - // Create pipeline layout - // - VkPipelineLayoutCreateInfo pPipelineLayoutCreateInfo = {}; - pPipelineLayoutCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; - pPipelineLayoutCreateInfo.pNext = NULL; - - // push constants - VkPushConstantRange pushConstantRange = {}; - pushConstantRange.offset = 0; - pushConstantRange.size = sizeof(PushConstants); - pushConstantRange.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - pPipelineLayoutCreateInfo.pushConstantRangeCount = 1; - pPipelineLayoutCreateInfo.pPushConstantRanges = &pushConstantRange; - - pPipelineLayoutCreateInfo.setLayoutCount = 1; - pPipelineLayoutCreateInfo.pSetLayouts = &m_descriptorSetLayout; - - res = vkCreatePipelineLayout(pDevice->GetDevice(), &pPipelineLayoutCreateInfo, NULL, &m_pipelineLayout); - assert(res == VK_SUCCESS); - - // Create pipeline - // - VkComputePipelineCreateInfo pipeline = {}; - pipeline.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; - pipeline.pNext = NULL; - pipeline.flags = 0; - pipeline.layout = m_pipelineLayout; - pipeline.stage = computeShader; - pipeline.basePipelineHandle = VK_NULL_HANDLE; - pipeline.basePipelineIndex = 0; - - res = vkCreateComputePipelines(pDevice->GetDevice(), pDevice->GetPipelineCache(), 1, &pipeline, NULL, &m_pipeline); - assert(res == VK_SUCCESS); - - m_pResourceViewHeaps->AllocDescriptor(m_descriptorSetLayout, &m_descriptorSet); - } - - void SPD_CS_Linear_Sampler::OnCreateWindowSizeDependentResources( - VkCommandBuffer cmd_buf, - uint32_t Width, - uint32_t Height, - Texture *pInput, - int mips - ) - { - m_Width = Width; - m_Height = Height; - m_mipCount = mips; - - VkImageCreateInfo image_info = {}; - image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; - image_info.pNext = NULL; - image_info.imageType = VK_IMAGE_TYPE_2D; - image_info.format = m_outFormat; - image_info.extent.width = m_Width >> 1; - image_info.extent.height = m_Height >> 1; - image_info.extent.depth = 1; - image_info.mipLevels = m_mipCount; - image_info.arrayLayers = 1; - image_info.samples = VK_SAMPLE_COUNT_1_BIT; - image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - image_info.queueFamilyIndexCount = 0; - image_info.pQueueFamilyIndices = NULL; - image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - image_info.usage = (VkImageUsageFlags)(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT); - image_info.flags = 0; - image_info.tiling = VK_IMAGE_TILING_OPTIMAL; - m_result.Init(m_pDevice, &image_info, "SpdDestinationMips"); - - // transition layout undefined to general layout? - VkImageMemoryBarrier imageMemoryBarrier = {}; - imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - imageMemoryBarrier.pNext = NULL; - imageMemoryBarrier.srcAccessMask = 0; - imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT; - imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; - imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - imageMemoryBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - imageMemoryBarrier.subresourceRange.baseMipLevel = 0; - imageMemoryBarrier.subresourceRange.levelCount = m_mipCount; - imageMemoryBarrier.subresourceRange.baseArrayLayer = 0; - imageMemoryBarrier.subresourceRange.layerCount = 1; - imageMemoryBarrier.image = m_result.Resource(); - - // transition general layout if detination image to shader read only for source image - vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier); - - // Create views for the mip chain - // - // source ----------- - // - pInput->CreateSRV(&m_SRV, 0); - - // Create and initialize the Descriptor Sets (all of them use the same Descriptor Layout) - // Create and initialize descriptor set for sampled image - VkDescriptorImageInfo desc_sampled_image = {}; - desc_sampled_image.sampler = VK_NULL_HANDLE; - desc_sampled_image.imageView = m_SRV; - desc_sampled_image.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - - std::vector writes(4); - writes[0] = {}; - writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[0].pNext = NULL; - writes[0].dstSet = m_descriptorSet; - writes[0].descriptorCount = 1; - writes[0].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; - writes[0].pImageInfo = &desc_sampled_image; - writes[0].dstBinding = 0; - writes[0].dstArrayElement = 0; - - // Create and initialize descriptor set for storage image - std::vector desc_storage_images(m_mipCount); - - for (int i = 0; i < m_mipCount; i++) - { - // destination ----------- - m_result.CreateRTV(&m_RTV[i], i); - - desc_storage_images[i] = {}; - desc_storage_images[i].sampler = VK_NULL_HANDLE; - desc_storage_images[i].imageView = m_RTV[i]; - desc_storage_images[i].imageLayout = VK_IMAGE_LAYOUT_GENERAL; - } - - writes[1] = {}; - writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[1].pNext = NULL; - writes[1].dstSet = m_descriptorSet; - writes[1].descriptorCount = m_mipCount; - writes[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - writes[1].pImageInfo = desc_storage_images.data(); - writes[1].dstBinding = 1; - writes[1].dstArrayElement = 0; - - VkDescriptorBufferInfo desc_buffer = {}; - desc_buffer.buffer = m_globalCounter; - desc_buffer.offset = 0; - desc_buffer.range = sizeof(int) * 1; - - writes[2] = {}; - writes[2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[2].pNext = NULL; - writes[2].dstSet = m_descriptorSet; - writes[2].descriptorCount = 1; - writes[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - writes[2].pBufferInfo = &desc_buffer; - writes[2].dstBinding = 2; - writes[2].dstArrayElement = 0; - - // Create and initialize descriptor set for sampler - VkDescriptorImageInfo desc_sampler = {}; - desc_sampler.sampler = m_sampler; - - writes[3] = {}; - writes[3].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[3].pNext = NULL; - writes[3].dstSet = m_descriptorSet; - writes[3].descriptorCount = 1; - writes[3].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER; - writes[3].pImageInfo = &desc_sampler; - writes[3].dstBinding = 3; - writes[3].dstArrayElement = 0; - - vkUpdateDescriptorSets(m_pDevice->GetDevice(), (uint32_t)writes.size(), writes.data(), 0, NULL); - } - - void SPD_CS_Linear_Sampler::OnDestroyWindowSizeDependentResources() - { - vkDestroyImageView(m_pDevice->GetDevice(), m_SRV, NULL); - for (int i = 0; i < m_mipCount; i++) - { - vkDestroyImageView(m_pDevice->GetDevice(), m_RTV[i], NULL); - } - - m_result.OnDestroy(); - } - - void SPD_CS_Linear_Sampler::OnDestroy() - { - - m_pResourceViewHeaps->FreeDescriptor(m_descriptorSet); - - vmaDestroyBuffer(m_pDevice->GetAllocator(), m_globalCounter, m_globalCounterAllocation); - - vkDestroyPipeline(m_pDevice->GetDevice(), m_pipeline, nullptr); - vkDestroyPipelineLayout(m_pDevice->GetDevice(), m_pipelineLayout, nullptr); - vkDestroyDescriptorSetLayout(m_pDevice->GetDevice(), m_descriptorSetLayout, NULL); - } - - void SPD_CS_Linear_Sampler::Draw(VkCommandBuffer cmd_buf) - { - // downsample - // - - // initialize global atomic counter to 0 - vmaMapMemory(m_pDevice->GetAllocator(), m_globalCounterAllocation, (void**)&m_pCounter); - *m_pCounter = 0; - vmaUnmapMemory(m_pDevice->GetAllocator(), m_globalCounterAllocation); - - // transition general layout if detination image to shader read only for source image - vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 0, nullptr); - - SetPerfMarkerBegin(cmd_buf, "SPD_CS_Linear_Sampler"); - - // Bind Pipeline - // - vkCmdBindPipeline(cmd_buf, VK_PIPELINE_BIND_POINT_COMPUTE, m_pipeline); - - // should be / 64 - uint32_t dispatchX = (((m_Width + 63) >> (6))); - uint32_t dispatchY = (((m_Height + 63) >> (6))); - uint32_t dispatchZ = 1; - - // single pass for storage buffer? - //uint32_t uniformOffsets[1] = { (uint32_t)constantBuffer.offset }; - vkCmdBindDescriptorSets(cmd_buf, VK_PIPELINE_BIND_POINT_COMPUTE, m_pipelineLayout, 0, 1, &m_descriptorSet, 0, nullptr); - - // Bind push constants - // - PushConstants data; - data.mips = m_mipCount; - data.numWorkGroups = dispatchX * dispatchY * dispatchZ; - data.invInputSize[0] = 1.0f / m_Width; - data.invInputSize[1] = 1.0f / m_Height; - vkCmdPushConstants(cmd_buf, m_pipelineLayout, - VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(PushConstants), (void*)&data); - - // Draw - // - vkCmdDispatch(cmd_buf, dispatchX, dispatchY, dispatchZ); - - // transition general layout if detination image to shader read only for source image - vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 0, nullptr); - - SetPerfMarkerEnd(cmd_buf); - } - - void SPD_CS_Linear_Sampler::Gui() - { - bool opened = true; - ImGui::Begin("Downsample", &opened); - - ImGui::Image((ImTextureID)m_SRV, ImVec2(320, 180)); - - for (int i = 0; i < m_mipCount; i++) - { - ImGui::Image((ImTextureID)m_RTV[i], ImVec2(320, 180)); - } - - ImGui::End(); - } -} \ No newline at end of file diff --git a/sample/src/VK/SPD_CS_Linear_Sampler.h b/sample/src/VK/SPD_CS_Linear_Sampler.h deleted file mode 100644 index fe6639d..0000000 --- a/sample/src/VK/SPD_CS_Linear_Sampler.h +++ /dev/null @@ -1,79 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -#pragma once - -#include "Base/StaticBufferPool.h" -#include "Base/Texture.h" -#include "Base/DynamicBufferRing.h" - -namespace CAULDRON_VK -{ -#define SPD_MAX_MIP_LEVELS 12 - - class SPD_CS_Linear_Sampler - { - public: - void OnCreate(Device* pDevice, ResourceViewHeaps *pResourceViewHeaps, VkFormat outFormat, bool fallback, bool packed); - void OnDestroy(); - - void OnCreateWindowSizeDependentResources(VkCommandBuffer cmd_buf, uint32_t Width, uint32_t Height, Texture *pInput, int mips); - void OnDestroyWindowSizeDependentResources(); - - void Draw(VkCommandBuffer cmd_buf); - Texture *GetTexture() { return &m_result; } - VkImageView GetTextureView(int i) { return m_RTV[i]; } - void Gui(); - - struct PushConstants - { - int mips; - int numWorkGroups; - float invInputSize[2]; - }; - - private: - Device *m_pDevice; - VkFormat m_outFormat; - - Texture m_result; - - VkImageView m_RTV[SPD_MAX_MIP_LEVELS]; // destinations (mips) - VkImageView m_SRV; // source - VkDescriptorSet m_descriptorSet; - - ResourceViewHeaps *m_pResourceViewHeaps; - DynamicBufferRing *m_pConstantBufferRing; - - uint32_t m_Width; - uint32_t m_Height; - int m_mipCount; - - VkDescriptorSetLayout m_descriptorSetLayout; - - VkPipelineLayout m_pipelineLayout; - VkPipeline m_pipeline; - - VkBuffer m_globalCounter; - VmaAllocation m_globalCounterAllocation; - - uint32_t* m_pCounter; - - VkSampler m_sampler; - }; -} \ No newline at end of file diff --git a/sample/src/VK/SPD_Integration.glsl b/sample/src/VK/SPD_Integration.glsl deleted file mode 100644 index 4090f1b..0000000 --- a/sample/src/VK/SPD_Integration.glsl +++ /dev/null @@ -1,124 +0,0 @@ -#version 450 -#extension GL_GOOGLE_include_directive : enable -#extension GL_ARB_separate_shader_objects : enable -#extension GL_ARB_shading_language_420pack : enable -#extension GL_ARB_compute_shader : enable -#extension GL_ARB_shader_group_vote : enable - -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; - -//-------------------------------------------------------------------------------------- -// Push Constants -//-------------------------------------------------------------------------------------- -layout(push_constant) uniform pushConstants { - uint mips; - uint numWorkGroups; -} spdConstants; - -//-------------------------------------------------------------------------------------- -// Texture definitions -//-------------------------------------------------------------------------------------- -layout(set=0, binding=0, rgba16f) uniform image2D imgSrc; -layout(set=0, binding=1, rgba16f) coherent uniform image2D imgDst[12]; - -//-------------------------------------------------------------------------------------- -// Buffer definitions - global atomic counter -//-------------------------------------------------------------------------------------- -layout(std430, binding=2) coherent buffer globalAtomicBuffer -{ - uint counter; -} globalAtomic; - -#define A_GPU -#define A_GLSL - -#include "ffx_a.h" - -shared AU1 spd_counter; - -// define fetch and store functions Non-Packed -#ifndef SPD_PACKED_ONLY -shared AF1 spd_intermediateR[16][16]; -shared AF1 spd_intermediateG[16][16]; -shared AF1 spd_intermediateB[16][16]; -shared AF1 spd_intermediateA[16][16]; -AF4 SpdLoadSourceImage(ASU2 p){return imageLoad(imgSrc, p);} -AF4 SpdLoad(ASU2 p){return imageLoad(imgDst[5],p);} -void SpdStore(ASU2 p, AF4 value, AU1 mip){imageStore(imgDst[mip], p, value);} -void SpdIncreaseAtomicCounter(){spd_counter = atomicAdd(globalAtomic.counter, 1);} -AU1 SpdGetAtomicCounter(){return spd_counter;} -AF4 SpdLoadIntermediate(AU1 x, AU1 y){ - return AF4( - spd_intermediateR[x][y], - spd_intermediateG[x][y], - spd_intermediateB[x][y], - spd_intermediateA[x][y]);} -void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value){ - spd_intermediateR[x][y] = value.x; - spd_intermediateG[x][y] = value.y; - spd_intermediateB[x][y] = value.z; - spd_intermediateA[x][y] = value.w;} -AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3){return (v0+v1+v2+v3)*0.25;} -#endif - -// define fetch and store functions Packed -#ifdef A_HALF -shared AH2 spd_intermediateRG[16][16]; -shared AH2 spd_intermediateBA[16][16]; -AH4 SpdLoadSourceImageH(ASU2 p){return AH4(imageLoad(imgSrc, p));} -AH4 SpdLoadH(ASU2 p){return AH4(imageLoad(imgDst[5],p));} -void SpdStoreH(ASU2 p, AH4 value, AU1 mip){imageStore(imgDst[mip], p, AF4(value));} -void SpdIncreaseAtomicCounter(){spd_counter = atomicAdd(globalAtomic.counter, 1);} -AU1 SpdGetAtomicCounter(){return spd_counter;} -AH4 SpdLoadIntermediateH(AU1 x, AU1 y){ - return AH4( - spd_intermediateRG[x][y].x, - spd_intermediateRG[x][y].y, - spd_intermediateBA[x][y].x, - spd_intermediateBA[x][y].y);} -void SpdStoreIntermediateH(AU1 x, AU1 y, AH4 value){ - spd_intermediateRG[x][y] = value.xy; - spd_intermediateBA[x][y] = value.zw;} -AH4 SpdReduce4H(AH4 v0, AH4 v1, AH4 v2, AH4 v3){return (v0+v1+v2+v3)*AH1(0.25f);} -#endif - -#include "ffx_spd.h" - -// Main function -//-------------------------------------------------------------------------------------- -//-------------------------------------------------------------------------------------- -void main() -{ -#ifndef A_HALF - SpdDownsample( - AU2(gl_WorkGroupID.xy), - AU1(gl_LocalInvocationIndex), - AU1(spdConstants.mips), - AU1(spdConstants.numWorkGroups)); -#else - SpdDownsampleH( - AU2(gl_WorkGroupID.xy), - AU1(gl_LocalInvocationIndex), - AU1(spdConstants.mips), - AU1(spdConstants.numWorkGroups)); -#endif -} \ No newline at end of file diff --git a/sample/src/VK/SPD_Integration.hlsl b/sample/src/VK/SPD_Integration.hlsl deleted file mode 100644 index 935dc5d..0000000 --- a/sample/src/VK/SPD_Integration.hlsl +++ /dev/null @@ -1,117 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -//-------------------------------------------------------------------------------------- -// Push Constants -//-------------------------------------------------------------------------------------- -[[vk::push_constant]] -cbuffer spdConstants { - uint mips; - uint numWorkGroups; -}; -//-------------------------------------------------------------------------------------- -// Texture definitions -//-------------------------------------------------------------------------------------- -[[vk::binding(0)]] Texture2D imgSrc :register(u0); -[[vk::binding(1)]] globallycoherent RWTexture2D imgDst[12] :register(u1); - -//-------------------------------------------------------------------------------------- -// Buffer definitions - global atomic counter -//-------------------------------------------------------------------------------------- -struct globalAtomicBuffer -{ - uint counter; -}; -[[vk::binding(2)]] globallycoherent RWStructuredBuffer globalAtomic; - -#define A_GPU -#define A_HLSL - -#include "ffx_a.h" - -groupshared AU1 spd_counter; - -// define fetch and store functions -#ifndef SPD_PACKED_ONLY -groupshared AF1 spd_intermediateR[16][16]; -groupshared AF1 spd_intermediateG[16][16]; -groupshared AF1 spd_intermediateB[16][16]; -groupshared AF1 spd_intermediateA[16][16]; -AF4 SpdLoadSourceImage(ASU2 tex){return imgSrc[tex];} -AF4 SpdLoad(ASU2 tex){return imgDst[5][tex];} -void SpdStore(ASU2 pix, AF4 outValue, AU1 index){imgDst[index][pix] = outValue;} -void SpdIncreaseAtomicCounter(){InterlockedAdd(globalAtomic[0].counter, 1, spd_counter);} -AU1 SpdGetAtomicCounter(){return spd_counter;} -AF4 SpdLoadIntermediate(AU1 x, AU1 y){ - return AF4( - spd_intermediateR[x][y], - spd_intermediateG[x][y], - spd_intermediateB[x][y], - spd_intermediateA[x][y]);} -void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value){ - spd_intermediateR[x][y] = value.x; - spd_intermediateG[x][y] = value.y; - spd_intermediateB[x][y] = value.z; - spd_intermediateA[x][y] = value.w;} -AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3){return (v0+v1+v2+v3)*0.25;} -#endif - -// define fetch and store functions Packed -#ifdef A_HALF -groupshared AH2 spd_intermediateRG[16][16]; -groupshared AH2 spd_intermediateBA[16][16]; -AH4 SpdLoadSourceImageH(ASU2 tex){return AH4(imgSrc[tex]);} -AH4 SpdLoadH(ASU2 p){return AH4(imgDst[5][p]);} -void SpdStoreH(ASU2 p, AH4 value, AU1 mip){imgDst[mip][p] = AF4(value);} -void SpdIncreaseAtomicCounter(){InterlockedAdd(globalAtomic[0].counter, 1, spd_counter);} -AU1 SpdGetAtomicCounter(){return spd_counter;} -AH4 SpdLoadIntermediateH(AU1 x, AU1 y){ - return AH4( - spd_intermediateRG[x][y].x, - spd_intermediateRG[x][y].y, - spd_intermediateBA[x][y].x, - spd_intermediateBA[x][y].y);} -void SpdStoreIntermediateH(AU1 x, AU1 y, AH4 value){ - spd_intermediateRG[x][y] = value.xy; - spd_intermediateBA[x][y] = value.zw;} -AH4 SpdReduce4H(AH4 v0, AH4 v1, AH4 v2, AH4 v3){return (v0+v1+v2+v3)*AH1(0.25);} -#endif - -#include "ffx_spd.h" - -// Main function -//-------------------------------------------------------------------------------------- -//-------------------------------------------------------------------------------------- -[numthreads(256,1,1)] -void main(uint3 WorkGroupId : SV_GroupID, uint LocalThreadIndex : SV_GroupIndex) -{ -#ifndef A_HALF - SpdDownsample( - AU2(WorkGroupId.xy), - AU1(LocalThreadIndex), - AU1(mips), - AU1(numWorkGroups)); -#else - SpdDownsampleH( - AU2(WorkGroupId.xy), - AU1(LocalThreadIndex), - AU1(mips), - AU1(numWorkGroups)); -#endif -} \ No newline at end of file diff --git a/sample/src/VK/SPD_Integration_Linear_Sampler.hlsl b/sample/src/VK/SPD_Integration_Linear_Sampler.hlsl deleted file mode 100644 index db9502b..0000000 --- a/sample/src/VK/SPD_Integration_Linear_Sampler.hlsl +++ /dev/null @@ -1,131 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -//-------------------------------------------------------------------------------------- -// Push Constants -//-------------------------------------------------------------------------------------- -[[vk::push_constant]] -cbuffer spdConstants { - uint mips; - uint numWorkGroups; - // [SAMPLER] - float2 invInputSize; -}; -//-------------------------------------------------------------------------------------- -// Texture definitions -//-------------------------------------------------------------------------------------- -[[vk::binding(0)]] Texture2D imgSrc :register(u0); -[[vk::binding(1)]] globallycoherent RWTexture2D imgDst[12] :register(u1); -// [SAMPLER] -[[vk::binding(3)]] SamplerState srcSampler :register(s0); - -//-------------------------------------------------------------------------------------- -// Buffer definitions - global atomic counter -//-------------------------------------------------------------------------------------- -struct globalAtomicBuffer -{ - uint counter; -}; -[[vk::binding(2)]] globallycoherent RWStructuredBuffer globalAtomic; - -#define A_GPU -#define A_HLSL - -#include "ffx_a.h" - -groupshared AU1 spd_counter; - -// define fetch and store functions -#ifndef SPD_PACKED_ONLY -groupshared AF1 spd_intermediateR[16][16]; -groupshared AF1 spd_intermediateG[16][16]; -groupshared AF1 spd_intermediateB[16][16]; -groupshared AF1 spd_intermediateA[16][16]; -//AF4 DSLoadSourceImage(ASU2 tex){return imgSrc[tex];} -//[SAMPLER] -AF4 SpdLoadSourceImage(ASU2 p){ - AF2 textureCoord = p * invInputSize + invInputSize; - return imgSrc.SampleLevel(srcSampler, textureCoord, 0); -} -AF4 SpdLoad(ASU2 tex){return imgDst[5][tex];} -void SpdStore(ASU2 pix, AF4 outValue, AU1 index){imgDst[index][pix] = outValue;} -void SpdIncreaseAtomicCounter(){InterlockedAdd(globalAtomic[0].counter, 1, spd_counter);} -AU1 SpdGetAtomicCounter(){return spd_counter;} -AF4 SpdLoadIntermediate(AU1 x, AU1 y){ - return AF4( - spd_intermediateR[x][y], - spd_intermediateG[x][y], - spd_intermediateB[x][y], - spd_intermediateA[x][y]);} -void SpdStoreIntermediate(AU1 x, AU1 y, AF4 value){ - spd_intermediateR[x][y] = value.x; - spd_intermediateG[x][y] = value.y; - spd_intermediateB[x][y] = value.z; - spd_intermediateA[x][y] = value.w;} -AF4 SpdReduce4(AF4 v0, AF4 v1, AF4 v2, AF4 v3){return (v0+v1+v2+v3)*0.25;} -#endif - -// define fetch and store functions Packed -#ifdef A_HALF -groupshared AH2 spd_intermediateRG[16][16]; -groupshared AH2 spd_intermediateBA[16][16]; -AH4 SpdLoadSourceImageH(ASU2 p){ - AF2 textureCoord = p * invInputSize + invInputSize; - return AH4(imgSrc.SampleLevel(srcSampler, textureCoord, 0)); -} -AH4 SpdLoadH(ASU2 p){return AH4(imgDst[5][p]);} -void SpdStoreH(ASU2 p, AH4 value, AU1 mip){imgDst[mip][p] = AF4(value);} -void SpdIncreaseAtomicCounter(){InterlockedAdd(globalAtomic[0].counter, 1, spd_counter);} -AU1 SpdGetAtomicCounter(){return spd_counter;} -AH4 SpdLoadIntermediateH(AU1 x, AU1 y){ - return AH4( - spd_intermediateRG[x][y].x, - spd_intermediateRG[x][y].y, - spd_intermediateBA[x][y].x, - spd_intermediateBA[x][y].y);} -void SpdStoreIntermediateH(AU1 x, AU1 y, AH4 value){ - spd_intermediateRG[x][y] = value.xy; - spd_intermediateBA[x][y] = value.zw;} -AH4 SpdReduce4H(AH4 v0, AH4 v1, AH4 v2, AH4 v3){return (v0+v1+v2+v3)*AH1(0.25);} -#endif - -#define SPD_LINEAR_SAMPLER - -#include "ffx_spd.h" - -// Main function -//-------------------------------------------------------------------------------------- -//-------------------------------------------------------------------------------------- -[numthreads(256,1,1)] -void main(uint3 WorkGroupId : SV_GroupID, uint LocalThreadIndex : SV_GroupIndex) -{ -#ifndef A_HALF - SpdDownsample( - AU2(WorkGroupId.xy), - AU1(LocalThreadIndex), - AU1(mips), - AU1(numWorkGroups)); -#else - SpdDownsampleH( - AU2(WorkGroupId.xy), - AU1(LocalThreadIndex), - AU1(mips), - AU1(numWorkGroups)); -#endif -} \ No newline at end of file diff --git a/sample/src/VK/SPD_Sample.cpp b/sample/src/VK/SPD_Sample.cpp deleted file mode 100644 index 070043c..0000000 --- a/sample/src/VK/SPD_Sample.cpp +++ /dev/null @@ -1,412 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#include "stdafx.h" - -#include "SPD_Sample.h" - -const bool VALIDATION_ENABLED = false; - -SPD_Sample::SPD_Sample(LPCSTR name) : FrameworkWindows(name) -{ - m_lastFrameTime = MillisecondsNow(); - m_time = 0; - m_bPlay = true; - - m_pGltfLoader = NULL; -} - -//-------------------------------------------------------------------------------------- -// -// OnCreate -// -//-------------------------------------------------------------------------------------- -void SPD_Sample::OnCreate(HWND hWnd) -{ - // Create Device - // - m_device.OnCreate("FFX_SPD_Sample", "Cauldron", VALIDATION_ENABLED, hWnd); - m_device.CreatePipelineCache(); - - //init the shader compiler - CreateShaderCache(); - - // 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 SPD_Renderer(); - 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 = 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.spotlightCount = 1; - - m_state.spotlight[0].intensity = 10.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.downsampler = Downsampler::SPD_CS; - m_state.spdVersion = SPD_Version::SPD_WaveOps; - m_state.spdPacked = SPD_Packed::SPD_Non_Packed; -} - -//-------------------------------------------------------------------------------------- -// -// OnDestroy -// -//-------------------------------------------------------------------------------------- -void SPD_Sample::OnDestroy() -{ - ImGUI_Shutdown(); - - m_device.GPUFlush(); - - // Fullscreen state should always be false before exiting the app. - m_swapChain.SetFullScreen(false); - - m_Node->UnloadScene(); - m_Node->OnDestroyWindowSizeDependentResources(); - m_Node->OnDestroy(); - - delete m_Node; - - m_swapChain.OnDestroyWindowSizeDependentResources(); - m_swapChain.OnDestroy(); - - //shut down the shader compiler - DestroyShaderCache(&m_device); - - if (m_pGltfLoader) - { - delete m_pGltfLoader; - m_pGltfLoader = NULL; - } - - m_device.DestroyPipelineCache(); - m_device.OnDestroy(); -} - -//-------------------------------------------------------------------------------------- -// -// OnEvent -// -//-------------------------------------------------------------------------------------- -bool SPD_Sample::OnEvent(MSG msg) -{ - if (ImGUI_WndProcHandler(msg.hwnd, msg.message, msg.wParam, msg.lParam)) - return true; - return true; -} - -//-------------------------------------------------------------------------------------- -// -// SetFullScreen -// -//-------------------------------------------------------------------------------------- -void SPD_Sample::SetFullScreen(bool fullscreen) -{ - m_device.GPUFlush(); - - m_swapChain.SetFullScreen(fullscreen); -} - -//-------------------------------------------------------------------------------------- -// -// OnResize -// -//-------------------------------------------------------------------------------------- -void SPD_Sample::OnResize(uint32_t width, uint32_t height) -{ - if (m_Width != width || m_Height != height) - { - // 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, false); - 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); -} - -//-------------------------------------------------------------------------------------- -// -// OnRender, updates the state from the UI, animates, transforms and renders the scene -// -//-------------------------------------------------------------------------------------- -void SPD_Sample::OnRender() -{ - // Get timings - // - double timeNow = MillisecondsNow(); - float deltaTime = (m_timeStep == 0.0f) ? (float)(timeNow - m_lastFrameTime) : m_timeStep; - m_lastFrameTime = timeNow; - - // Build UI and set the scene state. Note that the rendering of the UI happens later. - // - ImGUI_UpdateIO(); - ImGui::NewFrame(); - - static int loadingStage = 0; - if (loadingStage >= 0) - { - // LoadScene needs to be called a number of times, the scene is not fully loaded until it returns -1 - // This is done so we can display a progress bar when the scene is loading - if (m_pGltfLoader == NULL) - { - m_pGltfLoader = new GLTFCommon(); - m_pGltfLoader->Load("..\\media\\DamagedHelmet\\glTF\\", "DamagedHelmet.gltf"); - loadingStage = 0; - } - loadingStage = m_Node->LoadScene(m_pGltfLoader, loadingStage); - } - else - { - ImGuiStyle& style = ImGui::GetStyle(); - style.FrameBorderSize = 1.0f; - - bool opened = true; - ImGui::Begin("Stats", &opened); - - if (ImGui::CollapsingHeader("Info", ImGuiTreeNodeFlags_DefaultOpen)) - { - ImGui::Text("Resolution : %ix%i", m_Width, m_Height); - } - - if (ImGui::CollapsingHeader("Downsampler", ImGuiTreeNodeFlags_DefaultOpen)) - { - // Downsample settings - const char* downsampleItemNames[] = - { - "PS", - "Multipass CS", - "SPD CS", - "SPD CS Linear Sampler" - }; - ImGui::Combo("Downsampler Options", (int*)&m_state.downsampler, downsampleItemNames, _countof(downsampleItemNames)); - - // SPD Version - if (m_device.GetPhysicalDeviceSubgroupProperties().supportedOperations - & VK_SUBGROUP_FEATURE_QUAD_BIT) - { - const char* dsVersionItemNames[] = - { - "No-WaveOps", - "WaveOps", - }; - ImGui::Combo("SPD Version", (int*)&m_state.spdVersion, dsVersionItemNames, _countof(dsVersionItemNames)); - } - else { - const char* dsVersionItemNames[] = - { - "No-WaveOps", - }; - ImGui::Combo("SPD Version", (int*)&m_state.spdVersion, dsVersionItemNames, _countof(dsVersionItemNames)); - } - - // Non-Packed or Packed Version - const char* dsPackedNames[] = - { - "Non-Packed", - "Packed", - }; - ImGui::Combo("SPD Non-Packed / Packed Version", (int*)&m_state.spdPacked, dsPackedNames, _countof(dsPackedNames)); - } - - if (ImGui::CollapsingHeader("Lighting", ImGuiTreeNodeFlags_DefaultOpen)) - { - ImGui::SliderFloat("exposure", &m_state.exposure, 0.0f, 2.0f); - ImGui::SliderFloat("emmisive", &m_state.emmisiveFactor, 1.0f, 1000.0f, NULL,1.0f); - ImGui::SliderFloat("iblFactor", &m_state.iblFactor, 0.0f, 2.0f); - } - - const char * tonemappers[] = { "Timothy", "DX11DSK", "Reinhard", "Uncharted2Tonemap", "ACES", "No tonemapper" }; - ImGui::Combo("tone mapper", &m_state.toneMapper, tonemappers, _countof(tonemappers)); - - const char * skyDomeType[] = { "Procedural Sky", "cubemap", "Simple clear" }; - ImGui::Combo("SkyDome", &m_state.skyDomeType, skyDomeType, _countof(skyDomeType)); - - const char * cameraControl[] = { "WASD", "Orbit" }; - static int cameraControlSelected = 1; - ImGui::Combo("Camera", &cameraControlSelected, cameraControl, _countof(cameraControl)); - - if (ImGui::CollapsingHeader("Profiler", ImGuiTreeNodeFlags_DefaultOpen)) - { - std::vector timeStamps = m_Node->GetTimingValues(); - if (timeStamps.size() > 0) - { - for (uint32_t i = 1; i < timeStamps.size(); i++) - { - float DeltaTime = ((float)(timeStamps[i].m_microseconds - timeStamps[i - 1].m_microseconds)); - ImGui::Text("%-17s: %7.1f us", timeStamps[i].m_label.c_str(), DeltaTime); - } - - //scrolling data and average computing - static float values[128]; - values[127] = (float)(timeStamps.back().m_microseconds - timeStamps.front().m_microseconds); - float average = values[0]; - for (uint32_t i = 0; i < 128 - 1; i++) { values[i] = values[i + 1]; average += values[i]; } - average /= 128; - - ImGui::Text("%-17s: %7.1f us", "TotalGPUTime", average); - ImGui::PlotLines("", values, 128, 0, "", 0.0f, 30000.0f, ImVec2(0, 80)); - } - } - -#ifdef USE_VMA - if (ImGui::Button("Save VMA json")) - { - char *pJson; - vmaBuildStatsString(m_device.GetAllocator(), &pJson, VK_TRUE); - - static char filename[256]; - time_t now = time(NULL); - tm buf; - localtime_s(&buf, &now); - strftime(filename, sizeof(filename), "VMA_%Y%m%d_%H%M%S.json", &buf); - std::ofstream ofs(filename, std::ofstream::out); - ofs << pJson; - ofs.close(); - vmaFreeStatsString(m_device.GetAllocator(), pJson); - } -#endif - - ImGui::End(); - - // If the mouse was not used by the GUI then it's for the camera - // - ImGuiIO& io = ImGui::GetIO(); - if (io.WantCaptureMouse == false) - { - 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 (cameraControlSelected == 0) - { - // WASD - // - m_state.camera.UpdateCameraWASD(m_roll, m_pitch, io.KeysDown, io.DeltaTime); - } - else if (cameraControlSelected == 1) - { - // Orbiting - // - m_distance -= (float)io.MouseWheel / 3.0f; - m_distance = std::max(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); - } - } - } - - // Set animation time - // - 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()); - } - - m_state.time = m_time; - - // Do Render frame using AFR - // - m_Node->OnRender(&m_state, &m_swapChain); - - m_swapChain.Present(); -} - - -//-------------------------------------------------------------------------------------- -// -// WinMain -// -//-------------------------------------------------------------------------------------- -int WINAPI WinMain(HINSTANCE hInstance, - HINSTANCE hPrevInstance, - LPSTR lpCmdLine, - int nCmdShow) -{ - LPCSTR Name = "FFX SPD SampleVK v1.0"; - uint32_t Width = 1920; - uint32_t Height = 1080; - - // create new Vulkan sample - return RunFramework(hInstance, lpCmdLine, nCmdShow, Width, Height, new SPD_Sample(Name)); -} diff --git a/sample/src/VK/SPD_Versions.cpp b/sample/src/VK/SPD_Versions.cpp deleted file mode 100644 index b86eea9..0000000 --- a/sample/src/VK/SPD_Versions.cpp +++ /dev/null @@ -1,226 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#include "stdafx.h" -#include "Base\DynamicBufferRing.h" -#include "Base\StaticBufferPool.h" -#include "Base\UploadHeap.h" -#include "Base\Texture.h" -#include "Base\Helper.h" -#include "SPD_Versions.h" - - -namespace CAULDRON_VK -{ - void SPD_Versions::OnCreate(Device* pDevice, ResourceViewHeaps *pResourceViewHeaps, VkFormat outFormat) - { - m_pDevice = pDevice; - - // check if subgroup operations are supported, otherwise we need to fallback to the LDS only version - if (pDevice->GetPhysicalDeviceSubgroupProperties().supportedOperations - & VK_SUBGROUP_FEATURE_QUAD_BIT) - { - m_spd_WaveOps_NonPacked.OnCreate(pDevice, pResourceViewHeaps, outFormat, false, false); - m_spd_WaveOps_Packed.OnCreate(pDevice, pResourceViewHeaps, outFormat, false, true); - - m_spd_WaveOps_NonPacked_Linear_Sampler.OnCreate(pDevice, pResourceViewHeaps, outFormat, false, false); - m_spd_WaveOps_Packed_Linear_Sampler.OnCreate(pDevice, pResourceViewHeaps, outFormat, false, true); - } - - // fallback path - m_spd_No_WaveOps_NonPacked.OnCreate(pDevice, pResourceViewHeaps, outFormat, true, false); - m_spd_No_WaveOps_Packed.OnCreate(pDevice, pResourceViewHeaps, outFormat, true, true); - - m_spd_No_WaveOps_NonPacked_Linear_Sampler.OnCreate(pDevice, pResourceViewHeaps, outFormat, true, false); - m_spd_No_WaveOps_Packed_Linear_Sampler.OnCreate(pDevice, pResourceViewHeaps, outFormat, true, true); - } - - void SPD_Versions::OnDestroy() - { - m_spd_No_WaveOps_NonPacked.OnDestroy(); - m_spd_No_WaveOps_Packed.OnDestroy(); - - m_spd_No_WaveOps_NonPacked_Linear_Sampler.OnDestroy(); - m_spd_No_WaveOps_Packed_Linear_Sampler.OnDestroy(); - - if (m_pDevice->GetPhysicalDeviceSubgroupProperties().supportedOperations - & VK_SUBGROUP_FEATURE_QUAD_BIT) - { - m_spd_WaveOps_NonPacked.OnDestroy(); - m_spd_WaveOps_Packed.OnDestroy(); - - m_spd_WaveOps_NonPacked_Linear_Sampler.OnDestroy(); - m_spd_WaveOps_Packed_Linear_Sampler.OnDestroy(); - } - } - - uint32_t SPD_Versions::GetMaxMipLevelCount(uint32_t Width, uint32_t Height) - { - int resolution = max(Width, Height); - return (static_cast(min(1.0f + floor(log2(resolution)), 12)) - 1); - } - - void SPD_Versions::OnCreateWindowSizeDependentResources(VkCommandBuffer cmd_buf, uint32_t Width, uint32_t Height, Texture *pInput) - { - if (m_pDevice->GetPhysicalDeviceSubgroupProperties().supportedOperations - & VK_SUBGROUP_FEATURE_QUAD_BIT) - { - m_spd_WaveOps_NonPacked.OnCreateWindowSizeDependentResources(cmd_buf, Width, Height, pInput, GetMaxMipLevelCount(Width, Height)); - m_spd_WaveOps_Packed.OnCreateWindowSizeDependentResources(cmd_buf, Width, Height, pInput, GetMaxMipLevelCount(Width, Height)); - - m_spd_WaveOps_NonPacked_Linear_Sampler.OnCreateWindowSizeDependentResources(cmd_buf, Width, Height, pInput, GetMaxMipLevelCount(Width, Height)); - m_spd_WaveOps_Packed_Linear_Sampler.OnCreateWindowSizeDependentResources(cmd_buf, Width, Height, pInput, GetMaxMipLevelCount(Width, Height)); - } - m_spd_No_WaveOps_NonPacked.OnCreateWindowSizeDependentResources(cmd_buf, Width, Height, pInput, GetMaxMipLevelCount(Width, Height)); - m_spd_No_WaveOps_Packed.OnCreateWindowSizeDependentResources(cmd_buf, Width, Height, pInput, GetMaxMipLevelCount(Width, Height)); - - m_spd_No_WaveOps_NonPacked_Linear_Sampler.OnCreateWindowSizeDependentResources(cmd_buf, Width, Height, pInput, GetMaxMipLevelCount(Width, Height)); - m_spd_No_WaveOps_Packed_Linear_Sampler.OnCreateWindowSizeDependentResources(cmd_buf, Width, Height, pInput, GetMaxMipLevelCount(Width, Height)); - } - - void SPD_Versions::OnDestroyWindowSizeDependentResources() - { - if (m_pDevice->GetPhysicalDeviceSubgroupProperties().supportedOperations - & VK_SUBGROUP_FEATURE_QUAD_BIT) - { - m_spd_WaveOps_NonPacked.OnDestroyWindowSizeDependentResources(); - m_spd_WaveOps_Packed.OnDestroyWindowSizeDependentResources(); - - m_spd_WaveOps_NonPacked_Linear_Sampler.OnDestroyWindowSizeDependentResources(); - m_spd_WaveOps_Packed_Linear_Sampler.OnDestroyWindowSizeDependentResources(); - } - m_spd_No_WaveOps_NonPacked.OnDestroyWindowSizeDependentResources(); - m_spd_No_WaveOps_Packed.OnDestroyWindowSizeDependentResources(); - - m_spd_No_WaveOps_NonPacked_Linear_Sampler.OnDestroyWindowSizeDependentResources(); - m_spd_No_WaveOps_Packed_Linear_Sampler.OnDestroyWindowSizeDependentResources(); - } - - void SPD_Versions::Dispatch(VkCommandBuffer cmd_buf, SPD_Version dsVersion, SPD_Packed dsPacked) - { - switch (dsVersion) - { - case SPD_Version::SPD_WaveOps: - switch (dsPacked) - { - case SPD_Packed::SPD_Non_Packed: - m_spd_WaveOps_NonPacked.Draw(cmd_buf); - break; - case SPD_Packed::SPD_Packed: - m_spd_WaveOps_Packed.Draw(cmd_buf); - break; - } - break; - case SPD_Version::SPD_No_WaveOps: - switch (dsPacked) - { - case SPD_Packed::SPD_Non_Packed: - m_spd_No_WaveOps_NonPacked.Draw(cmd_buf); - break; - case SPD_Packed::SPD_Packed: - m_spd_No_WaveOps_Packed.Draw(cmd_buf); - break; - } - } - } - - void SPD_Versions::DispatchLinearSamplerVersion(VkCommandBuffer cmd_buf, SPD_Version dsVersion, SPD_Packed dsPacked) - { - switch (dsVersion) - { - case SPD_Version::SPD_WaveOps: - switch (dsPacked) - { - case SPD_Packed::SPD_Non_Packed: - m_spd_WaveOps_NonPacked_Linear_Sampler.Draw(cmd_buf); - break; - case SPD_Packed::SPD_Packed: - m_spd_WaveOps_Packed_Linear_Sampler.Draw(cmd_buf); - break; - } - break; - case SPD_Version::SPD_No_WaveOps: - switch (dsPacked) - { - case SPD_Packed::SPD_Non_Packed: - m_spd_No_WaveOps_NonPacked_Linear_Sampler.Draw(cmd_buf); - break; - case SPD_Packed::SPD_Packed: - m_spd_No_WaveOps_Packed_Linear_Sampler.Draw(cmd_buf); - break; - } - } - } - - void SPD_Versions::Gui(SPD_Version dsVersion, SPD_Packed dsPacked) - { - switch (dsVersion) - { - case SPD_Version::SPD_WaveOps: - switch (dsPacked) - { - case SPD_Packed::SPD_Non_Packed: - m_spd_WaveOps_NonPacked.Gui(); - break; - case SPD_Packed::SPD_Packed: - m_spd_WaveOps_Packed.Gui(); - break; - } - break; - case SPD_Version::SPD_No_WaveOps: - switch (dsPacked) - { - case SPD_Packed::SPD_Non_Packed: - m_spd_No_WaveOps_NonPacked.Gui(); - break; - case SPD_Packed::SPD_Packed: - m_spd_No_WaveOps_Packed.Gui(); - break; - } - } - } - - void SPD_Versions::GuiLinearSamplerVersion(SPD_Version dsVersion, SPD_Packed dsPacked) - { - switch (dsVersion) - { - case SPD_Version::SPD_WaveOps: - switch (dsPacked) - { - case SPD_Packed::SPD_Non_Packed: - m_spd_WaveOps_NonPacked_Linear_Sampler.Gui(); - break; - case SPD_Packed::SPD_Packed: - m_spd_WaveOps_Packed_Linear_Sampler.Gui(); - break; - } - break; - case SPD_Version::SPD_No_WaveOps: - switch (dsPacked) - { - case SPD_Packed::SPD_Non_Packed: - m_spd_No_WaveOps_NonPacked_Linear_Sampler.Gui(); - break; - case SPD_Packed::SPD_Packed: - m_spd_No_WaveOps_Packed_Linear_Sampler.Gui(); - break; - } - } - } -} \ No newline at end of file diff --git a/sample/src/VK/SPD_Versions.h b/sample/src/VK/SPD_Versions.h deleted file mode 100644 index 08ac4ab..0000000 --- a/sample/src/VK/SPD_Versions.h +++ /dev/null @@ -1,74 +0,0 @@ -// SPDSample -// -// Copyright (c) 2020 Advanced Micro Devices, Inc. All rights reserved. -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -#pragma once - -#include "PostProc/PostProcCS.h" -#include "PostProc/PostProcPS.h" -#include "Base/ResourceViewHeaps.h" - -#include "SPD_CS.h" -#include "SPD_CS_Linear_Sampler.h" - -namespace CAULDRON_VK -{ - enum class SPD_Version - { - SPD_No_WaveOps, - SPD_WaveOps, - }; - - enum class SPD_Packed - { - SPD_Non_Packed, - SPD_Packed, - }; - - class SPD_Versions - { - public: - void OnCreate(Device* pDevice, ResourceViewHeaps *pResourceViewHeaps, VkFormat outFormat); - void OnDestroy(); - - void OnCreateWindowSizeDependentResources(VkCommandBuffer cmd_buf, uint32_t Width, uint32_t Height, Texture *pInput); - void OnDestroyWindowSizeDependentResources(); - - void Dispatch(VkCommandBuffer cmd_buf, SPD_Version spdVersion, SPD_Packed spdPacked); - void Gui(SPD_Version spdVersion, SPD_Packed spdPacked); - - void DispatchLinearSamplerVersion(VkCommandBuffer cmd_buf, SPD_Version spdVersion, SPD_Packed spdPacked); - void GuiLinearSamplerVersion(SPD_Version spdVersion, SPD_Packed spdPacked); - - private: - Device* m_pDevice; - - SPD_CS m_spd_WaveOps_NonPacked; - SPD_CS m_spd_No_WaveOps_NonPacked; - - SPD_CS m_spd_WaveOps_Packed; - SPD_CS m_spd_No_WaveOps_Packed; - - SPD_CS_Linear_Sampler m_spd_WaveOps_NonPacked_Linear_Sampler; - SPD_CS_Linear_Sampler m_spd_No_WaveOps_NonPacked_Linear_Sampler; - - SPD_CS_Linear_Sampler m_spd_WaveOps_Packed_Linear_Sampler; - SPD_CS_Linear_Sampler m_spd_No_WaveOps_Packed_Linear_Sampler; - - uint32_t GetMaxMipLevelCount(uint32_t Width, uint32_t Height); - }; -}