Skip to content

Commit 07f57fb

Browse files
committed
[path_tracing] bug fixes
1 parent 156b5ae commit 07f57fb

9 files changed

Lines changed: 564 additions & 324 deletions

data/shaders/common_resources.hlsl

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,20 @@ Texture2D tex_perlin : register(t14);
5353
Texture3D tex3d_cloud_shape : register(t20); // 128^3 Perlin-Worley + Worley FBM
5454
Texture3D tex3d_cloud_detail : register(t21); // 32^3 high-frequency detail
5555
// restir reservoir textures (shared across path tracing, temporal, and spatial passes)
56+
// kept contiguous so a single loop can bind all six slots starting from tex_reservoir_prev0
57+
// the 6th slot carries the source primary g-buffer for the chosen path so the temporal and
58+
// spatial passes can evaluate the source brdf and reconnection jacobian without resampling
59+
// the current frame's g-buffer at a reprojected pixel (which is wrong on motion)
5660
Texture2D<float4> tex_reservoir_prev0 : register(t22);
5761
Texture2D<float4> tex_reservoir_prev1 : register(t23);
5862
Texture2D<float4> tex_reservoir_prev2 : register(t24);
5963
Texture2D<float4> tex_reservoir_prev3 : register(t25);
6064
Texture2D<float4> tex_reservoir_prev4 : register(t26);
65+
Texture2D<float4> tex_reservoir_prev5 : register(t27);
6166

6267
// wind field, baked once per frame, sampled by all wind-driven geometry
6368
// rg = flow vector (signed, [-1,1]), b = gust pressure (0..1), a = micro turbulence (0..1)
64-
Texture2D<float4> tex_wind_field : register(t27);
69+
Texture2D<float4> tex_wind_field : register(t29);
6570

6671
// geometry info buffer for ray tracing (per-blas-instance offsets)
6772
RWStructuredBuffer<GeometryInfo> geometry_infos : register(u20);
@@ -72,6 +77,7 @@ RWTexture2D<float4> tex_reservoir1 : register(u22);
7277
RWTexture2D<float4> tex_reservoir2 : register(u23);
7378
RWTexture2D<float4> tex_reservoir3 : register(u24);
7479
RWTexture2D<float4> tex_reservoir4 : register(u25);
80+
RWTexture2D<float4> tex_reservoir5 : register(u26);
7581

7682
// bindless arrays
7783
Texture2D material_textures[] : register(t15, space1);

data/shaders/restir_pt.hlsl

Lines changed: 109 additions & 52 deletions
Large diffs are not rendered by default.

data/shaders/restir_pt_debug.hlsl

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
Copyright(c) 2015-2026 Panos Karabelas
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
8+
copies of the Software, and to permit persons to whom the Software is furnished
9+
to do so, subject to the following conditions :
10+
11+
The above copyright notice and this permission notice shall be included in
12+
all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR
17+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
//= INCLUDES ===================
23+
#include "common.hlsl"
24+
#include "restir_reservoir.hlsl"
25+
//==============================
26+
27+
// debug visualization for restir state
28+
// reads the denoised gi (rgb = color, alpha = variance from the svgf temporal pass), the previous frame reservoirs (M, W, confidence), and produces a viridis heatmap of the requested quantity
29+
// the result is written to tex_uav in place of the gi color so the composite remultiplies by albedo as usual, producing a tinted but readable overlay
30+
//
31+
// debug_mode values match the r.restir_pt_debug_mode cvar list in RenderOptions:
32+
// 1 = confidence (alpha of reservoir.tex4.w, mapped 0..1)
33+
// 2 = reservoir M (0..RESTIR_M_CAP)
34+
// 3 = reservoir W (log scaled since W has high dynamic range)
35+
// 4 = reuse ratio (placeholder, shows confidence for now)
36+
// 5 = temporal rejection (placeholder, shows 1 - confidence)
37+
// 6 = variance (alpha of the denoised gi, log scaled, the svgf per pixel luminance variance)
38+
39+
// viridis colormap, approximated via a small polynomial fit, returns a perceptually uniform color from a [0,1] input
40+
float3 viridis(float t)
41+
{
42+
t = saturate(t);
43+
float3 c0 = float3(0.2777f, 0.0054f, 0.3340f);
44+
float3 c1 = float3(0.1056f, 1.4046f, 1.3845f);
45+
float3 c2 = float3(-0.3308f, 0.2148f, 0.0950f);
46+
float3 c3 = float3(-4.6342f, -5.7991f, -19.3324f);
47+
float3 c4 = float3(6.2289f, 14.1799f, 56.6905f);
48+
float3 c5 = float3(4.7763f, -13.7451f, -65.3530f);
49+
float3 c6 = float3(-5.4354f, 4.6458f, 26.3124f);
50+
return saturate(c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * c6))))));
51+
}
52+
53+
[numthreads(THREAD_GROUP_COUNT_X, THREAD_GROUP_COUNT_Y, 1)]
54+
void main_cs(uint3 dispatch_id : SV_DispatchThreadID)
55+
{
56+
uint2 pixel = dispatch_id.xy;
57+
uint resolution_x, resolution_y;
58+
tex_uav.GetDimensions(resolution_x, resolution_y);
59+
uint2 resolution = uint2(resolution_x, resolution_y);
60+
61+
if (pixel.x >= resolution.x || pixel.y >= resolution.y)
62+
{
63+
return;
64+
}
65+
66+
uint mode = uint(buffer_frame.restir_pt_debug_mode);
67+
if (mode == 0)
68+
{
69+
return;
70+
}
71+
72+
// sample what we need based on the mode
73+
// reservoir packing matches pack_reservoir / unpack_reservoir in restir_reservoir.hlsl
74+
float visualization_t = 0.0f;
75+
[branch] switch (mode)
76+
{
77+
case 1: // confidence (high f16 of tex4.w)
78+
{
79+
uint age_conf = asuint(tex_reservoir_prev4[pixel].w);
80+
float confidence = saturate(f16tof32(age_conf >> 16u));
81+
visualization_t = confidence;
82+
break;
83+
}
84+
case 2: // reservoir M, normalized by RESTIR_M_CAP for [0,1]
85+
{
86+
float M = tex_reservoir_prev2[pixel].w;
87+
visualization_t = saturate(M / float(RESTIR_M_CAP));
88+
break;
89+
}
90+
case 3: // reservoir W, log scaled since W spans many decades
91+
{
92+
float W = tex_reservoir_prev3[pixel].x;
93+
visualization_t = saturate(log2(W + 1.0f) / 8.0f);
94+
break;
95+
}
96+
case 4: // reuse ratio placeholder, shows confidence (proxy for how much we have reused this reservoir)
97+
{
98+
uint age_conf = asuint(tex_reservoir_prev4[pixel].w);
99+
float confidence = saturate(f16tof32(age_conf >> 16u));
100+
visualization_t = confidence;
101+
break;
102+
}
103+
case 5: // temporal rejection placeholder, shows 1 - confidence so freshly rejected pixels are hot
104+
{
105+
uint age_conf = asuint(tex_reservoir_prev4[pixel].w);
106+
float confidence = saturate(f16tof32(age_conf >> 16u));
107+
visualization_t = saturate(1.0f - confidence);
108+
break;
109+
}
110+
case 6: // variance (alpha of the denoised gi, log scaled because per pixel variance spans several decades on disocclusion edges)
111+
{
112+
float variance = max(tex_uav[pixel].a, 0.0f);
113+
visualization_t = saturate(log2(variance + 1.0f) / 4.0f);
114+
break;
115+
}
116+
}
117+
118+
// composition adds light_gi directly when restir is enabled, no albedo remultiply is done
119+
// so we write the viridis color straight into the gi slot, the confidence stored in alpha
120+
// is preserved so downstream bilateral upsampling still has a sane weight
121+
float3 heatmap = viridis(visualization_t);
122+
float alpha_in = tex_uav[pixel].a;
123+
tex_uav[pixel] = float4(heatmap, alpha_in);
124+
}

data/shaders/restir_pt_spatial.hlsl

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ void main_cs(uint3 dispatch_id : SV_DispatchThreadID)
145145
tex_reservoir_prev1[pixel],
146146
tex_reservoir_prev2[pixel],
147147
tex_reservoir_prev3[pixel],
148-
tex_reservoir_prev4[pixel]
148+
tex_reservoir_prev4[pixel],
149+
tex_reservoir_prev5[pixel]
149150
);
150151

151152
if (!is_reservoir_valid(center))
@@ -224,7 +225,8 @@ void main_cs(uint3 dispatch_id : SV_DispatchThreadID)
224225
tex_reservoir_prev1[neighbor_pixel],
225226
tex_reservoir_prev2[neighbor_pixel],
226227
tex_reservoir_prev3[neighbor_pixel],
227-
tex_reservoir_prev4[neighbor_pixel]
228+
tex_reservoir_prev4[neighbor_pixel],
229+
tex_reservoir_prev5[neighbor_pixel]
228230
);
229231

230232
if (!is_reservoir_valid(neighbor) || neighbor.M <= 0.0f || neighbor.W <= 0.0f)
@@ -360,28 +362,32 @@ void main_cs(uint3 dispatch_id : SV_DispatchThreadID)
360362
combined.confidence = saturate(max(center_confidence, merged_confidence));
361363
combined.age = center.age;
362364

363-
float4 t0, t1, t2, t3, t4;
364-
pack_reservoir(combined, t0, t1, t2, t3, t4);
365+
// re-stamp source primary g-buffer onto the chosen sample, the spatial combine may have
366+
// copied a neighbor's reservoir into combined.sample so the src_* fields may belong to
367+
// that neighbor's pixel, downstream passes always want the current pixel's primary as the
368+
// source for shifts originating from this pixel
369+
combined.sample.src_pos = pos_ws;
370+
combined.sample.src_normal = normal_ws;
371+
combined.sample.src_albedo = albedo;
372+
combined.sample.src_roughness = roughness;
373+
combined.sample.src_metallic = metallic;
374+
375+
float4 t0, t1, t2, t3, t4, t5;
376+
pack_reservoir(combined, t0, t1, t2, t3, t4, t5);
365377
tex_reservoir0[pixel] = t0;
366378
tex_reservoir1[pixel] = t1;
367379
tex_reservoir2[pixel] = t2;
368380
tex_reservoir3[pixel] = t3;
369381
tex_reservoir4[pixel] = t4;
382+
tex_reservoir5[pixel] = t5;
370383

371384
float3 gi = shade_reservoir_path(combined, pos_ws, normal_ws, view_dir, albedo, roughness, metallic);
372385
if (any(isnan(gi)) || any(isinf(gi)))
373386
gi = float3(0, 0, 0);
374387

375-
// primary direct from all analytical lights with ray-traced visibility
376-
// light.hlsl skips analytical lights entirely when restir_pt is enabled, so this is the only
377-
// path that adds direct contribution from the sun, area, point, and spot lights to the gi buffer
378-
// ibl / sky / emissive geometry remain handled by light_image_based.hlsl and indirect bounces
379-
uint direct_seed = create_seed_for_pass(pixel, buffer_frame.frame, 6 + spatial_pass_index);
380-
float3 geometric_normal = normal_ws;
381-
float3 direct = direct_lighting_at_primary_analytical(
382-
pos_ws, normal_ws, geometric_normal, view_dir, albedo, roughness, metallic, direct_seed);
383-
if (any(isnan(direct)) || any(isinf(direct)))
384-
direct = float3(0, 0, 0);
385-
386-
tex_uav[pixel] = float4(gi + direct, saturate(combined.confidence));
388+
// analytical direct lighting is no longer added here, the initial ris pass already streams
389+
// light nee samples into the reservoir alongside brdf samples, so all primary direct +
390+
// indirect contributions live inside the reservoir's chosen sample and are shaded above by
391+
// shade_reservoir_path, doing both would double count the sun and the area lights
392+
tex_uav[pixel] = float4(gi, saturate(combined.confidence));
387393
}

data/shaders/restir_pt_temporal.hlsl

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ void main_cs(uint3 dispatch_id : SV_DispatchThreadID)
123123
tex_reservoir1[pixel],
124124
tex_reservoir2[pixel],
125125
tex_reservoir3[pixel],
126-
tex_reservoir4[pixel]
126+
tex_reservoir4[pixel],
127+
tex_reservoir5[pixel]
127128
);
128129

129130
if (!is_reservoir_valid(current))
@@ -165,27 +166,23 @@ void main_cs(uint3 dispatch_id : SV_DispatchThreadID)
165166
tex_reservoir_prev1[prev_pixel],
166167
tex_reservoir_prev2[prev_pixel],
167168
tex_reservoir_prev3[prev_pixel],
168-
tex_reservoir_prev4[prev_pixel]
169+
tex_reservoir_prev4[prev_pixel],
170+
tex_reservoir_prev5[prev_pixel]
169171
);
170172

171173
if (is_reservoir_valid(temporal) && temporal.M > 0.0f && temporal.W > 0.0f)
172174
{
173-
// reconstruct the source primary world position from the reprojected pixel so
174-
// the reconnection jacobian correctly recovers the area-measure change between
175-
// the previous primary (where the reservoir was built) and the current primary.
176-
// for static geometry this is exact, for moving geometry the disocclusion gate
177-
// above already rejects large depth deltas so the residual error stays bounded
178-
float3 src_primary_pos = get_position(prev_uv);
179-
180-
// approximate src primary material with current g-buffer at prev_uv; for static
181-
// surfaces this is exact, and the disocclusion gate above already rejected
182-
// mismatched surfaces so the residual error stays bounded
183-
float4 src_material = tex_material.SampleLevel(GET_SAMPLER(sampler_point_clamp), prev_uv, 0);
184-
float3 src_albedo = saturate(tex_albedo.SampleLevel(GET_SAMPLER(sampler_point_clamp), prev_uv, 0).rgb);
185-
float src_roughness = max(src_material.r, 0.04f);
186-
float src_metallic = src_material.g;
187-
float3 src_normal_ws = get_normal(prev_uv);
188-
float3 src_view_dir = normalize(get_camera_position() - src_primary_pos);
175+
// use the stored source primary g-buffer that was captured at the time the
176+
// reservoir was generated, this is the actual previous-frame primary surface
177+
// and is correct even for moving objects, sampling the current g-buffer at
178+
// prev_uv was wrong on motion and caused ghosting / inflated reconnection
179+
// jacobians on dynamic scenes
180+
float3 src_primary_pos = temporal.sample.src_pos;
181+
float3 src_normal_ws = temporal.sample.src_normal;
182+
float3 src_albedo = temporal.sample.src_albedo;
183+
float src_roughness = max(temporal.sample.src_roughness, 0.04f);
184+
float src_metallic = temporal.sample.src_metallic;
185+
float3 src_view_dir = normalize(get_camera_position() - src_primary_pos);
189186

190187
ShiftResult shift_t_to_c = try_hybrid_shift(
191188
temporal.sample,
@@ -310,13 +307,23 @@ void main_cs(uint3 dispatch_id : SV_DispatchThreadID)
310307
combined.age = have_temporal ? (temporal.age + 1.0f) : 0.0f;
311308
combined.confidence = saturate(max(current.confidence, have_temporal ? temporal.confidence * temporal_confidence : 0.0f));
312309

313-
float4 t0, t1, t2, t3, t4;
314-
pack_reservoir(combined, t0, t1, t2, t3, t4);
310+
// re-stamp source primary g-buffer onto the chosen sample, the temporal combine may have
311+
// copied a reservoir whose chosen sample came from this pixel (canonical) or from the
312+
// previous frame, either way the source primary for downstream shifts is the current pixel
313+
combined.sample.src_pos = pos_ws;
314+
combined.sample.src_normal = normal_ws;
315+
combined.sample.src_albedo = albedo;
316+
combined.sample.src_roughness = roughness;
317+
combined.sample.src_metallic = metallic;
318+
319+
float4 t0, t1, t2, t3, t4, t5;
320+
pack_reservoir(combined, t0, t1, t2, t3, t4, t5);
315321
tex_reservoir0[pixel] = t0;
316322
tex_reservoir1[pixel] = t1;
317323
tex_reservoir2[pixel] = t2;
318324
tex_reservoir3[pixel] = t3;
319325
tex_reservoir4[pixel] = t4;
326+
tex_reservoir5[pixel] = t5;
320327

321328
float3 gi = shade_reservoir_path(combined, pos_ws, normal_ws, view_dir, albedo, roughness, metallic);
322329
if (any(isnan(gi)) || any(isinf(gi)))

0 commit comments

Comments
 (0)