diff --git a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/D3D12RaytracingRealTimeDenoisedAmbientOcclusion.vcxproj b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/D3D12RaytracingRealTimeDenoisedAmbientOcclusion.vcxproj
index 23a8bf9f5..9cd236ede 100644
--- a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/D3D12RaytracingRealTimeDenoisedAmbientOcclusion.vcxproj
+++ b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/D3D12RaytracingRealTimeDenoisedAmbientOcclusion.vcxproj
@@ -422,6 +422,58 @@
+
+ Compute
+ Compute
+ 6.3
+ 6.3
+ Compute
+ Compute
+ 6.3
+ 6.3
+ Compute
+ Compute
+ 6.3
+ 6.3
+ $(IntDir)CompiledShaders\%(Filename).hlsl.h
+ $(IntDir)CompiledShaders\%(Filename).hlsl.h
+ $(IntDir)CompiledShaders\%(Filename).hlsl.h
+ $(IntDir)CompiledShaders\%(Filename).hlsl.h
+ $(IntDir)CompiledShaders\%(Filename).hlsl.h
+ $(IntDir)CompiledShaders\%(Filename).hlsl.h
+ g_p%(Filename)
+ g_p%(Filename)
+ g_p%(Filename)
+ g_p%(Filename)
+ g_p%(Filename)
+ g_p%(Filename)
+
+
+ Compute
+ Compute
+ 6.3
+ 6.3
+ Compute
+ Compute
+ 6.3
+ 6.3
+ Compute
+ Compute
+ 6.3
+ 6.3
+ $(IntDir)CompiledShaders\%(Filename).hlsl.h
+ $(IntDir)CompiledShaders\%(Filename).hlsl.h
+ $(IntDir)CompiledShaders\%(Filename).hlsl.h
+ $(IntDir)CompiledShaders\%(Filename).hlsl.h
+ $(IntDir)CompiledShaders\%(Filename).hlsl.h
+ $(IntDir)CompiledShaders\%(Filename).hlsl.h
+ g_p%(Filename)
+ g_p%(Filename)
+ g_p%(Filename)
+ g_p%(Filename)
+ g_p%(Filename)
+ g_p%(Filename)
+
Compute
Compute
diff --git a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/D3D12RaytracingRealTimeDenoisedAmbientOcclusion.vcxproj.filters b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/D3D12RaytracingRealTimeDenoisedAmbientOcclusion.vcxproj.filters
index 1a083e1d3..4dbae87d4 100644
--- a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/D3D12RaytracingRealTimeDenoisedAmbientOcclusion.vcxproj.filters
+++ b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/D3D12RaytracingRealTimeDenoisedAmbientOcclusion.vcxproj.filters
@@ -329,6 +329,12 @@
Shaders\RTAO\Denoising\Filtering
+
+ Shaders\RTAO\Denoising\Filtering
+
+
+ Shaders\RTAO\Denoising\Filtering
+
Shaders\RTAO\Denoising\Filtering
diff --git a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Denoiser.cpp b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Denoiser.cpp
index 3abb07eed..32b2dcc75 100644
--- a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Denoiser.cpp
+++ b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Denoiser.cpp
@@ -18,6 +18,7 @@
#include "Denoiser.h"
#include "D3D12RaytracingRealTimeDenoisedAmbientOcclusion.h"
#include "Composition.h"
+#include "DirectXRaytracingHelper.h"
using namespace std;
using namespace DX;
@@ -67,6 +68,16 @@ namespace Denoiser_Args
const WCHAR* Modes[RTAOGpuKernels::AtrousWaveletTransformCrossBilateralFilter::FilterType::Count] = { L"3x3", L"5x5" };
EnumVar Mode(L"Render/AO/Denoising/Fullscreen blur/Kernel", RTAOGpuKernels::AtrousWaveletTransformCrossBilateralFilter::FilterType::EdgeStoppingGaussian3x3, RTAOGpuKernels::AtrousWaveletTransformCrossBilateralFilter::FilterType::Count, Modes);
+ // Row-exchange implementation used by the mean/variance and disocclusion-blur compute shaders:
+ // - Groupshared: portable, correct on any device (default).
+ // - Wave intrinsics: faster, but relies on a wave width >= 16 AND threads being packed to
+ // lanes in row-major SV_GroupIndex order (not guaranteed by HLSL). This option is limited
+ // to Groupshared only on devices that can't run the wave path (see
+ // Denoiser::CreateAuxilaryDeviceResources).
+ namespace WaveFilterPath { enum Enum { Groupshared = 0, WaveIntrinsics, Count }; }
+ const WCHAR* WaveFilterPathNames[WaveFilterPath::Count] = { L"Groupshared (portable)", L"Wave intrinsics (fast)" };
+ EnumVar FilterRowExchange(L"Render/AO/Denoising/Filter row exchange", WaveFilterPath::Groupshared, WaveFilterPath::Count, WaveFilterPathNames);
+
}
@@ -105,6 +116,14 @@ void Denoiser::CreateAuxilaryDeviceResources()
m_atrousWaveletTransformFilter.Initialize(device, Sample::FrameCount);
m_calculateMeanVarianceKernel.Initialize(device, Sample::FrameCount);
m_disocclusionBlurKernel.Initialize(device, Sample::FrameCount, c_MaxNumDisocllusionBlurPasses);
+
+ // If the device can't run the wave-intrinsic filter path (needs wave ops + wave width >= 16),
+ // restrict the "Filter row exchange" UI option to the portable groupshared path only.
+ if (!SupportsWaveIntrinsicDenoiserFilterPath(device))
+ {
+ Denoiser_Args::FilterRowExchange.SetValue(Denoiser_Args::WaveFilterPath::Groupshared);
+ Denoiser_Args::FilterRowExchange.SetListLength(1);
+ }
}
@@ -291,7 +310,8 @@ void Denoiser::TemporalSupersamplingBlendWithCurrentFrame(RTAO& rtao)
m_localMeanVarianceResources[AOVarianceResource::Raw].gpuDescriptorWriteAccess,
Denoiser_Args::Variance_BilateralFilterKernelWidth,
isCheckerboardSamplingEnabled,
- checkerboardLoadEvenPixels);
+ checkerboardLoadEvenPixels,
+ Denoiser_Args::FilterRowExchange == Denoiser_Args::WaveFilterPath::WaveIntrinsics);
// Interpolate the variance for the inactive cells from the valid checherkboard cells.
if (isCheckerboardSamplingEnabled)
@@ -421,7 +441,8 @@ void Denoiser::BlurDisocclusions(Pathtracer& pathtracer)
m_cbvSrvUavHeap->GetHeap(),
GBufferResources[GBufferResource::Depth].gpuDescriptorReadAccess,
m_disocclusionBlurStrength.gpuDescriptorReadAccess,
- inOutResource);
+ inOutResource,
+ Denoiser_Args::FilterRowExchange == Denoiser_Args::WaveFilterPath::WaveIntrinsics);
filterStep *= 2;
}
diff --git a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/RTAOGpuKernels.cpp b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/RTAOGpuKernels.cpp
index 1e8e18f71..418acfeed 100644
--- a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/RTAOGpuKernels.cpp
+++ b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/RTAOGpuKernels.cpp
@@ -24,6 +24,8 @@
#include "CompiledShaders\CountingSort_SortRays_64x128rayGroupCS.hlsl.h"
#include "CompiledShaders\AORayGenCS.hlsl.h"
#include "CompiledShaders\DisocclusionBlur3x3CS.hlsl.h"
+#include "CompiledShaders\CalculateMeanVarianceCS_WaveReadLaneAt.hlsl.h"
+#include "CompiledShaders\DisocclusionBlur3x3CS_WaveReadLaneAt.hlsl.h"
#include "CompiledShaders\FillInCheckerboard_CrossBox4TapFilterCS.hlsl.h"
using namespace std;
@@ -180,6 +182,19 @@ namespace RTAOGpuKernels
m_pipelineStateObject->SetName(L"Pipeline state object: DisocclusionBilateralFilter");
}
+ // Optionally create the wave-intrinsic fast-path pipeline state (see Run()); only used on
+ // devices with wave ops and a wave width >= 16, otherwise the groupshared default is used.
+ m_waveReadLanePathSupported = SupportsWaveIntrinsicDenoiserFilterPath(device);
+ if (m_waveReadLanePathSupported)
+ {
+ D3D12_COMPUTE_PIPELINE_STATE_DESC descComputePSO = {};
+ descComputePSO.pRootSignature = m_rootSignature.Get();
+ descComputePSO.CS = CD3DX12_SHADER_BYTECODE(static_cast(g_pDisocclusionBlur3x3CS_WaveReadLaneAt), ARRAYSIZE(g_pDisocclusionBlur3x3CS_WaveReadLaneAt));
+
+ ThrowIfFailed(device->CreateComputePipelineState(&descComputePSO, IID_PPV_ARGS(&m_pipelineStateObjectWaveReadLane)));
+ m_pipelineStateObjectWaveReadLane->SetName(L"Pipeline state object: DisocclusionBilateralFilter (wave read-lane)");
+ }
+
// Create shader resources
{
m_CB.Create(device, frameCount * numCallsPerFrame, L"Constant Buffer: DisocclusionBilateralFilter");
@@ -194,7 +209,8 @@ namespace RTAOGpuKernels
ID3D12DescriptorHeap* descriptorHeap,
D3D12_GPU_DESCRIPTOR_HANDLE inputDepthResourceHandle,
D3D12_GPU_DESCRIPTOR_HANDLE inputBlurStrengthResourceHandle,
- GpuResource* inputOutputResource)
+ GpuResource* inputOutputResource,
+ bool useWaveReadLanePath)
{
using namespace RootSignature::DisocclusionBilateralFilter;
using namespace DefaultComputeShaderParams;
@@ -222,7 +238,8 @@ namespace RTAOGpuKernels
commandList->SetComputeRootDescriptorTable(Slot::Debug1, debugResources[0].gpuDescriptorWriteAccess);
commandList->SetComputeRootDescriptorTable(Slot::Debug2, debugResources[1].gpuDescriptorWriteAccess);
- commandList->SetPipelineState(m_pipelineStateObject.Get());
+ const bool runWaveReadLanePath = useWaveReadLanePath && m_waveReadLanePathSupported && m_pipelineStateObjectWaveReadLane;
+ commandList->SetPipelineState((runWaveReadLanePath ? m_pipelineStateObjectWaveReadLane : m_pipelineStateObject).Get());
}
// Account for interleaved Group execution
@@ -447,6 +464,20 @@ namespace RTAOGpuKernels
m_pipelineStateObject->SetName(L"Pipeline state object: CalculateMeanVariance");
}
+ // Optionally create the wave-intrinsic fast-path pipeline state. It is only selected at
+ // run time on devices that report support (wave ops + wave width >= 16); otherwise the
+ // portable groupshared pipeline state above is always used (see Run()).
+ m_waveReadLanePathSupported = SupportsWaveIntrinsicDenoiserFilterPath(device);
+ if (m_waveReadLanePathSupported)
+ {
+ D3D12_COMPUTE_PIPELINE_STATE_DESC descComputePSO = {};
+ descComputePSO.pRootSignature = m_rootSignature.Get();
+ descComputePSO.CS = CD3DX12_SHADER_BYTECODE(static_cast(g_pCalculateMeanVarianceCS_WaveReadLaneAt), ARRAYSIZE(g_pCalculateMeanVarianceCS_WaveReadLaneAt));
+
+ ThrowIfFailed(device->CreateComputePipelineState(&descComputePSO, IID_PPV_ARGS(&m_pipelineStateObjectWaveReadLane)));
+ m_pipelineStateObjectWaveReadLane->SetName(L"Pipeline state object: CalculateMeanVariance (wave read-lane)");
+ }
+
// Create shader resources.
{
m_CB.Create(device, frameCount * numCallsPerFrame, L"Constant Buffer: CalculateMeanVariance");
@@ -463,7 +494,8 @@ namespace RTAOGpuKernels
D3D12_GPU_DESCRIPTOR_HANDLE outputMeanVarianceResourceHandle,
UINT kernelWidth,
bool doCheckerboardSampling,
- bool checkerboardLoadEvenPixels)
+ bool checkerboardLoadEvenPixels,
+ bool useWaveReadLanePath)
{
using namespace RootSignature::CalculateMeanVariance;
using namespace DefaultComputeShaderParams;
@@ -476,7 +508,8 @@ namespace RTAOGpuKernels
{
commandList->SetDescriptorHeaps(1, &descriptorHeap);
commandList->SetComputeRootSignature(m_rootSignature.Get());
- commandList->SetPipelineState(m_pipelineStateObject.Get());
+ const bool runWaveReadLanePath = useWaveReadLanePath && m_waveReadLanePathSupported && m_pipelineStateObjectWaveReadLane;
+ commandList->SetPipelineState((runWaveReadLanePath ? m_pipelineStateObjectWaveReadLane : m_pipelineStateObject).Get());
commandList->SetComputeRootDescriptorTable(Slot::Input, inputValuesResourceHandle);
commandList->SetComputeRootDescriptorTable(Slot::OutputMeanVariance, outputMeanVarianceResourceHandle);
@@ -1088,4 +1121,4 @@ namespace RTAOGpuKernels
XMUINT2 groupSize(CeilDivide(width, ThreadGroup::Width), CeilDivide(height, ThreadGroup::Height));
commandList->Dispatch(groupSize.x, groupSize.y, 1);
}
-}
\ No newline at end of file
+}
diff --git a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/RTAOGpuKernels.h b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/RTAOGpuKernels.h
index f4e8af26a..a8eb9e096 100644
--- a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/RTAOGpuKernels.h
+++ b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/RTAOGpuKernels.h
@@ -84,11 +84,14 @@ namespace RTAOGpuKernels
ID3D12DescriptorHeap* descriptorHeap,
D3D12_GPU_DESCRIPTOR_HANDLE inputDepthResourceHandle,
D3D12_GPU_DESCRIPTOR_HANDLE inputBlurStrengthResourceHandle,
- GpuResource* inputOutputResource);
+ GpuResource* inputOutputResource,
+ bool useWaveReadLanePath = false);
private:
ComPtr m_rootSignature;
- ComPtr m_pipelineStateObject;
+ ComPtr m_pipelineStateObject; // portable groupshared path (default)
+ ComPtr m_pipelineStateObjectWaveReadLane; // optional wave-intrinsic fast path
+ bool m_waveReadLanePathSupported = false;
ConstantBuffer m_CB;
UINT m_CBinstanceID = 0;
@@ -171,11 +174,14 @@ namespace RTAOGpuKernels
D3D12_GPU_DESCRIPTOR_HANDLE outputMeanVarianceResourceHandle,
UINT kernelWidth,
bool doCheckerboardSampling = false,
- bool checkerboardLoadEvenPixels = false);
+ bool checkerboardLoadEvenPixels = false,
+ bool useWaveReadLanePath = false);
private:
ComPtr m_rootSignature;
- ComPtr m_pipelineStateObject;
+ ComPtr m_pipelineStateObject; // portable groupshared path (default)
+ ComPtr m_pipelineStateObjectWaveReadLane; // optional wave-intrinsic fast path
+ bool m_waveReadLanePathSupported = false;
ConstantBuffer m_CB;
UINT m_CBinstanceID = 0;
};
diff --git a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/CalculateMeanVarianceCS.hlsl b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/CalculateMeanVarianceCS.hlsl
index 9172f88b5..c3a082ab9 100644
--- a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/CalculateMeanVarianceCS.hlsl
+++ b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/CalculateMeanVarianceCS.hlsl
@@ -9,8 +9,11 @@
//
//*********************************************************
-// Desc: Calculate Local Mean and Variance via a separable kernel and using wave intrinsics.
-// Requirements:
+// Desc: Calculate Local Mean and Variance via a separable kernel. The per-row aggregation is
+// exchanged either through groupshared memory (default, portable) or, on devices with a
+// wave width >= 16, across wave lanes (compile with RTAO_WAVE_READ_LANE_PATH; the host
+// selects the permutation at runtime, see RTAOGpuKernels::CalculateMeanVariance).
+// Requirements (wave-intrinsic permutation only):
// - Wave lane size 16 or higher.
// - WaveReadLaneAt() with any to any to wave read lane support.
// Supports:
@@ -19,8 +22,6 @@
// Active pixel is a pixel on the checkerboard pattern and has a valid /
// generated value for it. The kernel is stretched in y direction
// to sample only from active pixels.
-// Performance:
-// - 4K, 2080Ti, 9x9 kernel: 0.37ms (separable) -> 0.305 ms (separable + wave intrinsics)
#define HLSL
#include "RaytracingHlslCompat.h"
@@ -50,6 +51,12 @@ int2 GetActivePixelIndex(int2 pixel)
? pixel + int2(0, 1)
: pixel;
}
+#ifndef RTAO_WAVE_READ_LANE_PATH
+// Portable (non-wave) path: cache of loaded input values so each output lane can read its
+// kernel-window neighbours via groupshared memory instead of wave-lane reads.
+groupshared float ValueCache[16][16]; // [row][column] loaded input values.
+#endif
+
// Load up to 16x16 pixels and filter them horizontally.
// The output is cached in Shared Memory and contains NumRows x 8 results.
void FilterHorizontally(in uint2 Gid, in uint GI)
@@ -63,6 +70,12 @@ void FilterHorizontally(in uint2 Gid, in uint GI)
uint2 GTid4x16_row0 = uint2(GI % 16, GI / 16);
const int2 KernelBasePixel = (Gid * GroupDim - int(cb.kernelRadius)) * int2(1, cb.pixelStepY);
const uint NumRowsToLoadPerThread = 4;
+
+#ifdef RTAO_WAVE_READ_LANE_PATH
+ // ---- Fast path: exchange each row's values across lanes with wave-lane reads. ----
+ // Valid only when waves have >= 16 lanes AND threads are packed to lanes in row-major
+ // SV_GroupIndex order. Neither is guaranteed by HLSL; the host selects this permutation
+ // at runtime only when WaveLaneCountMin >= 16 (otherwise the LDS path below is used).
const uint Row_BaseWaveLaneIndex = (WaveGetLaneIndex() / 16) * 16;
[unroll]
@@ -144,6 +157,58 @@ void FilterHorizontally(in uint2 Gid, in uint GI)
}
}
}
+#else
+ // ---- Portable path: exchange each row's values through groupshared memory. ----
+ // Independent of wave size and of any lane<->SV_GroupIndex ordering assumption, so it is
+ // correct on every device (used whenever the fast wave path above is not selected).
+
+ // Phase 1: cooperatively load up to 16x16 input values into shared memory.
+ [unroll]
+ for (uint i = 0; i < NumRowsToLoadPerThread; i++)
+ {
+ uint2 GTid4x16 = GTid4x16_row0 + uint2(0, i * 4);
+ if (GTid4x16.y < NumValuesToLoadPerRowOrColumn)
+ {
+ int2 pixel = GetActivePixelIndex(KernelBasePixel + GTid4x16 * int2(1, cb.pixelStepY));
+ float value = RTAO::InvalidAOCoefficientValue;
+ if (GTid4x16.x < NumValuesToLoadPerRowOrColumn && IsWithinBounds(pixel, cb.textureDim))
+ {
+ value = g_inValue[pixel];
+ }
+ ValueCache[GTid4x16.y][GTid4x16.x] = value;
+ }
+ }
+ GroupMemoryBarrierWithGroupSync();
+
+ // Phase 2: accumulate the whole kernel width for the first GroupDim (8) columns of each row.
+ [unroll]
+ for (uint j = 0; j < NumRowsToLoadPerThread; j++)
+ {
+ uint2 GTid4x16 = GTid4x16_row0 + uint2(0, j * 4);
+ if (GTid4x16.y >= NumValuesToLoadPerRowOrColumn || GTid4x16.x >= GroupDim.x)
+ {
+ continue;
+ }
+
+ float valueSum = 0;
+ float squaredValueSum = 0;
+ uint numValues = 0;
+
+ for (uint c = 0; c < cb.kernelWidth; c++)
+ {
+ float cValue = ValueCache[GTid4x16.y][GTid4x16.x + c];
+ if (cValue != RTAO::InvalidAOCoefficientValue)
+ {
+ valueSum += cValue;
+ squaredValueSum += cValue * cValue;
+ numValues++;
+ }
+ }
+
+ PackedRowResultCache[GTid4x16.y][GTid4x16.x] = Float2ToHalf(float2(valueSum, squaredValueSum));
+ NumValuesCache[GTid4x16.y][GTid4x16.x] = numValues;
+ }
+#endif
}
void FilterVertically(uint2 DTid, in uint2 GTid)
diff --git a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/CalculateMeanVarianceCS_WaveReadLaneAt.hlsl b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/CalculateMeanVarianceCS_WaveReadLaneAt.hlsl
new file mode 100644
index 000000000..82dd0af6b
--- /dev/null
+++ b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/CalculateMeanVarianceCS_WaveReadLaneAt.hlsl
@@ -0,0 +1,19 @@
+//*********************************************************
+//
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
+// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
+// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
+// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
+//
+//*********************************************************
+
+// Fast-path permutation of CalculateMeanVarianceCS that exchanges each row's values across
+// wave lanes (WaveReadLaneAt) instead of through groupshared memory. It assumes waves of >= 16
+// lanes AND that threads are packed to lanes in row-major SV_GroupIndex order, neither of which
+// is guaranteed by HLSL. The host (RTAOGpuKernels::CalculateMeanVariance) only selects this
+// permutation when the device reports WaveLaneCountMin >= 16; otherwise the portable default
+// (CalculateMeanVarianceCS.hlsl, groupshared exchange) is used.
+#define RTAO_WAVE_READ_LANE_PATH
+#include "CalculateMeanVarianceCS.hlsl"
diff --git a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/DepthAwareSeparableGaussianFilter3x3CS_AnyToAnyWaveReadLaneAt.hlsl b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/DepthAwareSeparableGaussianFilter3x3CS_AnyToAnyWaveReadLaneAt.hlsl
index de2813391..73aef2be5 100644
--- a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/DepthAwareSeparableGaussianFilter3x3CS_AnyToAnyWaveReadLaneAt.hlsl
+++ b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/DepthAwareSeparableGaussianFilter3x3CS_AnyToAnyWaveReadLaneAt.hlsl
@@ -47,6 +47,12 @@ static const uint NumValuesToLoadPerRowOrColumn =
groupshared uint PackedValueDepthCache[NumValuesToLoadPerRowOrColumn][8]; // 16bit float value, depth.
groupshared float FilteredResultCache[NumValuesToLoadPerRowOrColumn][8]; // 32 bit float filteredValue.
+#ifndef RTAO_WAVE_READ_LANE_PATH
+// Portable (non-wave) path: cache of loaded (value, depth) so each output lane can read its
+// kernel-window neighbours via groupshared memory instead of wave-lane reads.
+groupshared float2 ValueDepthCache[NumValuesToLoadPerRowOrColumn][NumValuesToLoadPerRowOrColumn];
+#endif
+
// Find a DTID with steps in between the group threads and groups interleaved to cover all pixels.
uint2 GetPixelIndex(in uint2 Gid, in uint2 GTid)
@@ -71,6 +77,11 @@ void FilterHorizontally(in uint2 Gid, in uint GI)
uint2 GTid4x16_row0 = uint2(GI % 16, GI / 16);
int2 GroupKernelBasePixel = GetPixelIndex(Gid, 0) - int(FilterKernel::Radius * cb.step);
const uint NumRowsToLoadPerThread = 4;
+
+#ifdef RTAO_WAVE_READ_LANE_PATH
+ // ---- Fast path: exchange each row's (value, depth) across lanes with wave-lane reads. ----
+ // Valid only when waves have >= 16 lanes AND threads are packed to lanes in row-major
+ // SV_GroupIndex order; the host selects this permutation only when WaveLaneCountMin >= 16.
const uint Row_BaseWaveLaneIndex = (WaveGetLaneIndex() / 16) * 16;
[unroll]
@@ -189,6 +200,100 @@ void FilterHorizontally(in uint2 Gid, in uint GI)
}
}
}
+#else
+ // ---- Portable path: exchange each row's (value, depth) through groupshared memory. ----
+ // Independent of wave size and of any lane<->SV_GroupIndex ordering assumption.
+
+ // Phase 1: cooperatively load values/depths into shared memory and cache kernel centers.
+ [unroll]
+ for (uint i = 0; i < NumRowsToLoadPerThread; i++)
+ {
+ uint2 GTid4x16 = GTid4x16_row0 + uint2(0, i * 4);
+ if (GTid4x16.y < NumValuesToLoadPerRowOrColumn && GTid4x16.x < NumValuesToLoadPerRowOrColumn)
+ {
+ int2 pixel = GroupKernelBasePixel + GTid4x16 * cb.step;
+ float value = RTAO::InvalidAOCoefficientValue;
+ float depth = 0;
+ if (IsWithinBounds(pixel, cb.textureDim))
+ {
+ value = g_inOutValue[pixel];
+ depth = g_inDepth[pixel];
+ }
+ ValueDepthCache[GTid4x16.y][GTid4x16.x] = float2(value, depth);
+
+ // Cache the kernel center values for the vertical pass.
+ if (IsInRange(GTid4x16.x, FilterKernel::Radius, FilterKernel::Radius + GroupDim.x - 1))
+ {
+ PackedValueDepthCache[GTid4x16.y][GTid4x16.x - FilterKernel::Radius] = Float2ToHalf(float2(value, depth));
+ }
+ }
+ }
+ GroupMemoryBarrierWithGroupSync();
+
+ // Phase 2: depth-aware gaussian accumulation for the first GroupDim (8) columns of each row.
+ [unroll]
+ for (uint j = 0; j < NumRowsToLoadPerThread; j++)
+ {
+ uint2 GTid4x16 = GTid4x16_row0 + uint2(0, j * 4);
+ if (GTid4x16.y >= NumValuesToLoadPerRowOrColumn || GTid4x16.x >= GroupDim.x)
+ {
+ continue;
+ }
+
+ // Kernel center values.
+ float2 kc = ValueDepthCache[GTid4x16.y][GTid4x16.x + FilterKernel::Radius];
+ float kcValue = kc.x;
+ float kcDepth = kc.y;
+
+ float weightedValueSum = 0;
+ float weightSum = 0;
+ float gaussianWeightedValueSum = 0;
+ float gaussianWeightedSum = 0;
+
+ // Kernel center contribution.
+ if (kcValue != RTAO::InvalidAOCoefficientValue && kcDepth != 0)
+ {
+ float w_h = FilterKernel::Kernel1D[FilterKernel::Radius];
+ gaussianWeightedValueSum = w_h * kcValue;
+ gaussianWeightedSum = w_h;
+ weightedValueSum = gaussianWeightedValueSum;
+ weightSum = w_h;
+ }
+
+ // Remaining kernel cells.
+ for (uint k = 0; k < FilterKernel::Width; k++)
+ {
+ if (k == FilterKernel::Radius)
+ {
+ continue; // center already accumulated
+ }
+
+ float2 cvd = ValueDepthCache[GTid4x16.y][GTid4x16.x + k];
+ float cValue = cvd.x;
+ float cDepth = cvd.y;
+
+ if (cValue != RTAO::InvalidAOCoefficientValue && kcDepth != 0 && cDepth != 0)
+ {
+ float w_h = FilterKernel::Kernel1D[k];
+
+ // Simple depth test with tolerance growing as the kernel radius increases.
+ float depthThreshold = 0.05 + cb.step * 0.001 * abs(int(FilterKernel::Radius) - int(k));
+ float w_d = abs(kcDepth - cDepth) <= depthThreshold * kcDepth;
+ float w = w_h * w_d;
+
+ weightedValueSum += w * cValue;
+ weightSum += w;
+ gaussianWeightedValueSum += w_h * cValue;
+ gaussianWeightedSum += w_h;
+ }
+ }
+
+ float gaussianFilteredValue = gaussianWeightedSum > 1e-6 ? gaussianWeightedValueSum / gaussianWeightedSum : RTAO::InvalidAOCoefficientValue;
+ float filteredValue = weightSum > 1e-6 ? weightedValueSum / weightSum : gaussianFilteredValue;
+
+ FilteredResultCache[GTid4x16.y][GTid4x16.x] = filteredValue;
+ }
+#endif
}
void FilterVertically(uint2 DTid, in uint2 GTid, in float blurStrength)
diff --git a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/DisocclusionBlur3x3CS.hlsl b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/DisocclusionBlur3x3CS.hlsl
index 8159f320f..c51dabc78 100644
--- a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/DisocclusionBlur3x3CS.hlsl
+++ b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/DisocclusionBlur3x3CS.hlsl
@@ -17,7 +17,7 @@
// is more relaxed than the one used in AtrousWaveletTransform filter.
// It still however does a relaxed depth test to prevent blending surfaces too far apart.
// Supports up to 9x9 kernels.
-// Requirements:
+// Requirements (wave-intrinsic permutation only; the default groupshared permutation has none):
// - Wave lane size 16 or higher.
// - WaveReadLaneAt() with any to any to wave read lane support.
@@ -46,6 +46,12 @@ static const uint NumValuesToLoadPerRowOrColumn =
groupshared uint PackedValueDepthCache[NumValuesToLoadPerRowOrColumn][8]; // 16bit float value, depth.
groupshared float FilteredResultCache[NumValuesToLoadPerRowOrColumn][8]; // 32 bit float filteredValue.
+#ifndef RTAO_WAVE_READ_LANE_PATH
+// Portable (non-wave) path: cache of loaded (value, depth) so each output lane can read its
+// kernel-window neighbours via groupshared memory instead of wave-lane reads.
+groupshared float2 ValueDepthCache[NumValuesToLoadPerRowOrColumn][NumValuesToLoadPerRowOrColumn];
+#endif
+
// Find a DTID with steps in between the group threads and groups interleaved to cover all pixels.
uint2 GetPixelIndex(in uint2 Gid, in uint2 GTid)
@@ -70,6 +76,11 @@ void FilterHorizontally(in uint2 Gid, in uint GI)
uint2 GTid4x16_row0 = uint2(GI % 16, GI / 16);
int2 GroupKernelBasePixel = GetPixelIndex(Gid, 0) - int(FilterKernel::Radius * cb.step);
const uint NumRowsToLoadPerThread = 4;
+
+#ifdef RTAO_WAVE_READ_LANE_PATH
+ // ---- Fast path: exchange each row's (value, depth) across lanes with wave-lane reads. ----
+ // Valid only when waves have >= 16 lanes AND threads are packed to lanes in row-major
+ // SV_GroupIndex order; the host selects this permutation only when WaveLaneCountMin >= 16.
const uint Row_BaseWaveLaneIndex = (WaveGetLaneIndex() / 16) * 16;
[unroll]
@@ -188,6 +199,100 @@ void FilterHorizontally(in uint2 Gid, in uint GI)
}
}
}
+#else
+ // ---- Portable path: exchange each row's (value, depth) through groupshared memory. ----
+ // Independent of wave size and of any lane<->SV_GroupIndex ordering assumption.
+
+ // Phase 1: cooperatively load values/depths into shared memory and cache kernel centers.
+ [unroll]
+ for (uint i = 0; i < NumRowsToLoadPerThread; i++)
+ {
+ uint2 GTid4x16 = GTid4x16_row0 + uint2(0, i * 4);
+ if (GTid4x16.y < NumValuesToLoadPerRowOrColumn && GTid4x16.x < NumValuesToLoadPerRowOrColumn)
+ {
+ int2 pixel = GroupKernelBasePixel + GTid4x16 * cb.step;
+ float value = RTAO::InvalidAOCoefficientValue;
+ float depth = 0;
+ if (IsWithinBounds(pixel, cb.textureDim))
+ {
+ value = g_inOutValue[pixel];
+ depth = g_inDepth[pixel];
+ }
+ ValueDepthCache[GTid4x16.y][GTid4x16.x] = float2(value, depth);
+
+ // Cache the kernel center values for the vertical pass.
+ if (IsInRange(GTid4x16.x, FilterKernel::Radius, FilterKernel::Radius + GroupDim.x - 1))
+ {
+ PackedValueDepthCache[GTid4x16.y][GTid4x16.x - FilterKernel::Radius] = Float2ToHalf(float2(value, depth));
+ }
+ }
+ }
+ GroupMemoryBarrierWithGroupSync();
+
+ // Phase 2: depth-aware gaussian accumulation for the first GroupDim (8) columns of each row.
+ [unroll]
+ for (uint j = 0; j < NumRowsToLoadPerThread; j++)
+ {
+ uint2 GTid4x16 = GTid4x16_row0 + uint2(0, j * 4);
+ if (GTid4x16.y >= NumValuesToLoadPerRowOrColumn || GTid4x16.x >= GroupDim.x)
+ {
+ continue;
+ }
+
+ // Kernel center values.
+ float2 kc = ValueDepthCache[GTid4x16.y][GTid4x16.x + FilterKernel::Radius];
+ float kcValue = kc.x;
+ float kcDepth = kc.y;
+
+ float weightedValueSum = 0;
+ float weightSum = 0;
+ float gaussianWeightedValueSum = 0;
+ float gaussianWeightedSum = 0;
+
+ // Kernel center contribution.
+ if (kcValue != RTAO::InvalidAOCoefficientValue && kcDepth != HitDistanceOnMiss)
+ {
+ float w_h = FilterKernel::Kernel1D[FilterKernel::Radius];
+ gaussianWeightedValueSum = w_h * kcValue;
+ gaussianWeightedSum = w_h;
+ weightedValueSum = gaussianWeightedValueSum;
+ weightSum = w_h;
+ }
+
+ // Remaining kernel cells.
+ for (uint k = 0; k < FilterKernel::Width; k++)
+ {
+ if (k == FilterKernel::Radius)
+ {
+ continue; // center already accumulated
+ }
+
+ float2 cvd = ValueDepthCache[GTid4x16.y][GTid4x16.x + k];
+ float cValue = cvd.x;
+ float cDepth = cvd.y;
+
+ if (cValue != RTAO::InvalidAOCoefficientValue && kcDepth != HitDistanceOnMiss && cDepth != HitDistanceOnMiss)
+ {
+ float w_h = FilterKernel::Kernel1D[k];
+
+ // Simple depth test with tolerance growing as the kernel radius increases.
+ float depthThreshold = 0.05 + cb.step * 0.001 * abs(int(FilterKernel::Radius) - int(k));
+ float w_d = abs(kcDepth - cDepth) <= depthThreshold * kcDepth;
+ float w = w_h * w_d;
+
+ weightedValueSum += w * cValue;
+ weightSum += w;
+ gaussianWeightedValueSum += w_h * cValue;
+ gaussianWeightedSum += w_h;
+ }
+ }
+
+ float gaussianFilteredValue = gaussianWeightedSum > 1e-6 ? gaussianWeightedValueSum / gaussianWeightedSum : RTAO::InvalidAOCoefficientValue;
+ float filteredValue = weightSum > 1e-6 ? weightedValueSum / weightSum : gaussianFilteredValue;
+
+ FilteredResultCache[GTid4x16.y][GTid4x16.x] = filteredValue;
+ }
+#endif
}
void FilterVertically(uint2 DTid, in uint2 GTid, in float blurStrength)
diff --git a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/DisocclusionBlur3x3CS_WaveReadLaneAt.hlsl b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/DisocclusionBlur3x3CS_WaveReadLaneAt.hlsl
new file mode 100644
index 000000000..f3d737333
--- /dev/null
+++ b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/Denoising/DisocclusionBlur3x3CS_WaveReadLaneAt.hlsl
@@ -0,0 +1,19 @@
+//*********************************************************
+//
+// Copyright (c) Microsoft. All rights reserved.
+// This code is licensed under the MIT License (MIT).
+// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
+// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
+// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
+// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
+//
+//*********************************************************
+
+// Fast-path permutation of DisocclusionBlur3x3CS that exchanges each row's (value, depth) across
+// wave lanes (WaveReadLaneAt) instead of through groupshared memory. It assumes waves of >= 16
+// lanes AND that threads are packed to lanes in row-major SV_GroupIndex order, neither of which
+// is guaranteed by HLSL. The host (RTAOGpuKernels::DisocclusionBilateralFilter) only selects this
+// permutation when the device reports WaveLaneCountMin >= 16; otherwise the portable default
+// (DisocclusionBlur3x3CS.hlsl, groupshared exchange) is used.
+#define RTAO_WAVE_READ_LANE_PATH
+#include "DisocclusionBlur3x3CS.hlsl"
diff --git a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/SampleCore/DirectXRaytracingHelper.h b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/SampleCore/DirectXRaytracingHelper.h
index 5d1c2c1de..00a1ca038 100644
--- a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/SampleCore/DirectXRaytracingHelper.h
+++ b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/SampleCore/DirectXRaytracingHelper.h
@@ -261,4 +261,18 @@ inline bool IsDirectXRaytracingSupported(IDXGIAdapter1* adapter)
return SUCCEEDED(D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&testDevice)))
&& SUCCEEDED(testDevice->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS5, &featureSupportData, sizeof(featureSupportData)))
&& featureSupportData.RaytracingTier != D3D12_RAYTRACING_TIER_NOT_SUPPORTED;
+}
+
+// Returns whether the device can run the wave-intrinsic denoiser filter permutations.
+// Those permutations exchange a row of values/depths across wave lanes and require wave ops
+// plus a wave width of at least 16 lanes. They additionally assume threads map to lanes in
+// row-major SV_GroupIndex order, which HLSL does not guarantee but which holds on the
+// wave >= 16 GPUs the fast path targets; the portable groupshared permutation is used
+// otherwise and is the safe default.
+inline bool SupportsWaveIntrinsicDenoiserFilterPath(ID3D12Device* device)
+{
+ D3D12_FEATURE_DATA_D3D12_OPTIONS1 options1 = {};
+ return SUCCEEDED(device->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS1, &options1, sizeof(options1)))
+ && options1.WaveOps
+ && options1.WaveLaneCountMin >= 16;
}
\ No newline at end of file
diff --git a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/readme.md b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/readme.md
index eafd4affb..5e0f0e4cd 100644
--- a/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/readme.md
+++ b/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/readme.md
@@ -5,7 +5,7 @@
[[YouTube video preview and explanation of the sample (watch at 4K@60)]](https://www.youtube.com/watch?v=3EdE38iRn2A)
-This sample presents a combination of established denoising techniques to bring raytraced Ambient Occlusion (AO) to within acceptable frame budget of AAA games on current and previous generation of DirectX Raytracing capable GPUs. With 60 frames-per-second (FPS) being the standard for modern games, a rendering budget for effects such as AO can be as low as 1.5 ms per frame in modern game engines. The sample employs spatio-temporal accumulation and denoising of raytraced AO rays that are cast at 1 (or 0.5) sample rays per pixel (spp). The denoiser is targetted at and optimized for 60+ FPS scenarios. It denoises a 1080p 1 spp AO at less than 0.8 ms on RTX 2080 Ti. In practice, that translates to a 4K framebuffer target as AO is often generated and denoised at quarter resolution and then bilaterally upsampled. The denoiser implementation provided here allows developers to have a starting point that they can experiment with and expand upon. See the 2nd part of this readme below for details on the denoiser implementation.
+This sample presents a combination of established denoising techniques to bring raytraced Ambient Occlusion (AO) to within acceptable frame budget of AAA games on current and previous generation of DirectX Raytracing capable GPUs. With 60 frames-per-second (FPS) being the standard for modern games, a rendering budget for effects such as AO can be as low as 1.5 ms per frame in modern game engines. The sample employs spatio-temporal accumulation and denoising of raytraced AO rays that are cast at 1 (or 0.5) sample rays per pixel (spp). The denoiser is targetted at and optimized for 60+ FPS scenarios. AO is often generated and denoised at quarter resolution and then bilaterally upsampled, so a 1080p AO denoise maps to a 4K framebuffer target. The denoiser implementation provided here allows developers to have a starting point that they can experiment with and expand upon. See the 2nd part of this readme below for details on the denoiser implementation.
In addition, this DXR sample shows an implementation of:
* A specular pathtracer with physically-based shading (PBR)
@@ -83,7 +83,7 @@ AO is a cheap approximation for global illumination that is more appropriate for
While indirect lighting phenomena can be physically modeled via Global Illumination (GI) models, a full fledged GI solution can be very expensive and impractical for games on today's commodity graphics processing unit hardware (GPU). Adding the cheaper AO to the games on top has become popular way of complementing the simpler GI models and achieving a more plausible look. Game engines have done this mostly either by prebaking it into textures, and thus, limiting it to static object layouts or via screen-space approximations, which can suffer because of limited geometry data available in screen space. Some limitations due to screen-space approximation include creating false dark halos, lack of occlusion due to geometry not being visible on screen, unstable occlusion around screen borders and generally limitation to only occlusions from objects close to the target. Raytraced AO, in contrast can avoid all these issues by evaluating AO in a more natural way to solve the problem.
-The physically-based approach to estimating AO is via Monte Carlo (random) sampling of the hemisphere around a normal of a point on a surface and testing for visibility by tracing the casted rays. Rays that hit any objects increase the occlusion value, inversely lowering the ambient lighting term making surfaces appear darker. Generating AO in this way can be very noisy requiring large number of rays per pixel to be cast to reach visually pleasing results. This number can be in hundreds of rays per pixel. In the scene used in the sample, rendering such ground truth AO at 256 spp takes 170 ms at 1080p on 2080 Ti. Such high spp is not practical for a game on today's HW. This sample instead implements a real-time denoiser of 1 spp raytraced Ambient Occlusion targeted at 60+ FPS apps. Denoising is a critical part to reach real-time raytraced effects on current-gen hardware.
+The physically-based approach to estimating AO is via Monte Carlo (random) sampling of the hemisphere around a normal of a point on a surface and testing for visibility by tracing the casted rays. Rays that hit any objects increase the occlusion value, inversely lowering the ambient lighting term making surfaces appear darker. Generating AO in this way can be very noisy requiring large number of rays per pixel to be cast to reach visually pleasing results. This number can be in hundreds of rays per pixel. In the scene used in the sample, rendering such ground truth AO at 256 spp is far too expensive for real-time. Such high spp is not practical for a game on today's HW. This sample instead implements a real-time denoiser of 1 spp raytraced Ambient Occlusion targeted at 60+ FPS apps. Denoising is a critical part to reach real-time raytraced effects on current-gen hardware.
## Input to AO raytracer
The sample implements a specular physically-based (PBR) pathtracer to calculate per-pixel color. It casts radiance and shadow rays as the rays bounce around the scene. AO rays are raytraced and denoised in a separate pass. This can improve GPU's performance by running a single type of rays at a time. It also provides an opportunity to manually pre-sort the rays. The denoiser supports a single AO value per pixel and since it depends on per-pixel normal, depth and motion vector data, it implicly requires each pixel to represent only a single surface hit point. This is not always the case as the pathracer can hit multiple surfaces for a single camera ray. To address this, the pathtracer uses a heuristic to pick a single surface hit. It selects a hit position with the highest perceived material luminance. That luminance depends on the surface's material albedo scaled by the radiance's ray radiance contribution. All other surfaces are lit with a constant ambient term when a radiance ray hits a surface. This generally works well as switching between selected surfaces across frames (i.e. due to the radiance ray incidence angle changing and thus fresnel ratio changing) is rare and is not very noticeable in the final composited image.
@@ -106,7 +106,7 @@ Random samples for sampling of a hemisphere with AO rays are generated with a co
* NxN sample set distribution is only supported for 1 spp. On 2+ spp settings, it will use 1x1 sample set distribution. This is trivial to extend should you need it.
## Ray sorting
-Calculating AO means sampling a hemisphere, and thus AO rays are divergent by design. This severely affects the ray tracing performance, especially on Pascal architecture. We found the neighboring AO rays can be efficiently ray sorted by their ray direction to provide a boost of 10-40% of original runtime raytracing cost. The sample implements a compute based pre-sort pass for each 64x128 pixels and then dispatches the sorted rays with 1D DispatchRays(). On Turing, the ray sorted RTAO path is mostly as fast as non-ray sorted RTAO path due to the ray sorting overhead and because of the fact, the raytracing performance on Turing is better. The ray sorting is done by hashing the rays and sorting them similar to *Costa et al. 2014, Ray Reordering Techniques for GPU Ray-Cast Ambient Occlusion*. In contrast to the paper, we found hashing the rays by 8bit encoded octahedral ray direction working well and no impact for extending the hash by ray origin depth. But likely there can be cases where the ray origin depth would make a difference, such as in cases of cluttered geometry (looking at a dense grass field from a sharp angle). The ray sorting shader performance directly depends on the hash bit length and, thus, using only 8 bit ray direction hash instead of a hash that also includes depth quantization speeds the ray sorting shader up.
+Calculating AO means sampling a hemisphere, and thus AO rays are divergent by design. This severely affects the ray tracing performance, especially on Pascal architecture. We found the neighboring AO rays can be efficiently ray sorted by their ray direction to provide a meaningful reduction in runtime raytracing cost. The sample implements a compute based pre-sort pass for each 64x128 pixels and then dispatches the sorted rays with 1D DispatchRays(). On Turing, the ray sorted RTAO path is mostly as fast as non-ray sorted RTAO path due to the ray sorting overhead and because of the fact, the raytracing performance on Turing is better. The ray sorting is done by hashing the rays and sorting them similar to *Costa et al. 2014, Ray Reordering Techniques for GPU Ray-Cast Ambient Occlusion*. In contrast to the paper, we found hashing the rays by 8bit encoded octahedral ray direction working well and no impact for extending the hash by ray origin depth. But likely there can be cases where the ray origin depth would make a difference, such as in cases of cluttered geometry (looking at a dense grass field from a sharp angle). The ray sorting shader performance directly depends on the hash bit length and, thus, using only 8 bit ray direction hash instead of a hash that also includes depth quantization speeds the ray sorting shader up.