Unravel Engine C++ Reference
Loading...
Searching...
No Matches
ssil_pass.cpp
Go to the documentation of this file.
1#include "ssil_pass.h"
2#include <algorithm>
6#include <graphics/graphics.h>
8#include <graphics/texture.h>
9
10namespace unravel
11{
12
14{
15 auto& am = ctx.get_cached<asset_manager>();
16
17 auto vs_clip_quad = am.get_asset<gfx::shader>("engine:/data/shaders/vs_clip_quad.sc");
18
19 auto fs_ssil_trace = am.get_asset<gfx::shader>("engine:/data/shaders/ssil/fs_ssil_trace.sc");
20 auto fs_ssil_temporal = am.get_asset<gfx::shader>("engine:/data/shaders/ssil/fs_ssil_temporal_resolve.sc");
21 auto cs_ssil_denoise = am.get_asset<gfx::shader>("engine:/data/shaders/ssil/cs_ssil_spatial_denoise.sc");
22 auto cs_ssil_downsample = am.get_asset<gfx::shader>("engine:/data/shaders/ssil/cs_ssil_downsample.sc");
23 auto fs_ssil_upsample = am.get_asset<gfx::shader>("engine:/data/shaders/ssil/fs_ssil_upsample.sc");
24
25 trace_program_.program = std::make_unique<gpu_program>(vs_clip_quad, fs_ssil_trace);
26 trace_program_.cache_uniforms();
27
28 temporal_program_.program = std::make_unique<gpu_program>(vs_clip_quad, fs_ssil_temporal);
29 temporal_program_.cache_uniforms();
30
31 denoise_program_.program = std::make_unique<gpu_program>(cs_ssil_denoise);
32 denoise_program_.cache_uniforms();
33
34 downsample_program_.program = std::make_unique<gpu_program>(cs_ssil_downsample);
35 downsample_program_.cache_uniforms();
36
37 upsample_program_.program = std::make_unique<gpu_program>(vs_clip_quad, fs_ssil_upsample);
38 upsample_program_.cache_uniforms();
39
40 // The upsample and downsample programs are optional: SSIL still works at full res (and
41 // falls back to a hardware-bilinear consume at reduced res / all-full-res denoise) if
42 // they fail to build.
43 return trace_program_.is_valid() && temporal_program_.is_valid() && denoise_program_.is_valid();
44}
45
46auto ssil_pass::create_or_update_ssil_fb(gfx::render_view& rview,
47 const std::string& name,
48 const gfx::frame_buffer::ptr& reference,
50 uint64_t extra_flags) -> gfx::frame_buffer::ptr
51{
52 const auto target_size = compute_trace_size(reference->get_size(), res);
53
54 auto& tex = rview.tex_get_or_emplace(name);
55 if(gfx::needs_recreate(tex, target_size))
56 {
57 tex.reset();
58 tex = std::make_shared<gfx::texture>(target_size.width, target_size.height, false, 1,
59 gfx::texture_format::RGBA16F,
60 BGFX_TEXTURE_RT | BGFX_SAMPLER_U_CLAMP |
61 BGFX_SAMPLER_V_CLAMP | extra_flags);
62 }
63
64 auto& fbo = rview.fbo_get_or_emplace(name);
65 if(gfx::needs_recreate(fbo, target_size))
66 {
67 fbo.reset();
68 fbo = std::make_shared<gfx::frame_buffer>();
69 fbo->populate({tex});
70 }
71
72 return fbo;
73}
74
75auto ssil_pass::create_or_update_ssil_tex(gfx::render_view& rview,
76 const std::string& name,
77 const gfx::frame_buffer::ptr& reference,
79 uint64_t extra_flags) -> gfx::texture::ptr
80{
81 const auto target_size = compute_trace_size(reference->get_size(), res);
82
83 auto& tex = rview.tex_get_or_emplace(name);
84 if(gfx::needs_recreate(tex, target_size))
85 {
86 tex.reset();
87 tex = std::make_shared<gfx::texture>(target_size.width, target_size.height, false, 1,
88 gfx::texture_format::RGBA16F,
89 BGFX_TEXTURE_RT | BGFX_TEXTURE_BLIT_DST |
90 BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP | extra_flags);
91 }
92
93 return tex;
94}
95
96auto ssil_pass::create_or_update_ssil_fb_mrt(gfx::render_view& rview,
97 const std::string& fbo_name,
98 const std::string& color_tex_name,
99 const std::string& moments_tex_name,
100 const gfx::frame_buffer::ptr& reference,
102 uint64_t extra_flags) -> gfx::frame_buffer::ptr
103{
104 const auto target_size = compute_trace_size(reference->get_size(), res);
105
106 auto color_tex = create_or_update_ssil_tex(rview, color_tex_name, reference, res, extra_flags);
107 auto moments_tex = create_or_update_ssil_tex(rview, moments_tex_name, reference, res, extra_flags);
108
109 auto& fbo = rview.fbo_get_or_emplace(fbo_name);
110 if(gfx::needs_recreate(fbo, target_size))
111 {
112 fbo.reset();
113 fbo = std::make_shared<gfx::frame_buffer>();
114 fbo->populate({color_tex, moments_tex});
115 }
116
117 return fbo;
118}
119
121{
122 if(!params.g_buffer || !trace_program_.is_valid())
123 {
124 return nullptr;
125 }
126
128 auto ssil_curr_fb = run_trace(rview, params);
129 if(!ssil_curr_fb)
130 {
132 return nullptr;
133 }
134
135 auto result_fb = ssil_curr_fb;
136
137 bool moments_valid = false;
138 gfx::texture::ptr moments_tex_for_denoise;
139 if(params.settings.enable_temporal_accumulation && temporal_program_.is_valid())
140 {
141 // Use a SEPARATE output variable -- aliasing `result_fb` as both the input
142 // (`ssil_input`) and the output (`out_result_fb`) caused the temporal pass to
143 // silently rebind `s_ssil_curr` to the empty history framebuffer when it wrote
144 // back to its out parameter, making the running mean converge to zero (black).
145 gfx::frame_buffer::ptr temporal_result_fb;
146 moments_valid = run_temporal_resolve(rview,
147 result_fb,
148 params.g_buffer,
149 params.prev_depth,
150 params.cam,
151 params.settings,
152 temporal_result_fb,
153 moments_tex_for_denoise);
154 result_fb = temporal_result_fb;
155 }
156 else
157 {
158 release_history_resources(rview);
159 }
160
161 if(params.settings.enable_spatial_denoise && denoise_program_.is_valid())
162 {
163 // The temporal pass produces per-pixel luminance moments (E[L], E[L^2]); the
164 // denoiser uses them for true spatiotemporal variance. Only valid when the
165 // resolve shader actually ran this frame.
166 auto moments_tex = moments_valid ? moments_tex_for_denoise : gfx::texture::ptr{};
167 result_fb = run_spatial_denoise(rview, result_fb, params.g_buffer, moments_tex, params.cam, params.settings);
168 }
169 else
170 {
171 release_denoise_resources(rview);
172 }
173
174 // Joint-bilateral upsample to full res when the trace ran below full res so the
175 // indirect-lighting consumer samples a 1:1 full-res buffer instead of bleeding a
176 // reduced-res buffer across depth/normal edges with hardware bilinear. When the
177 // mixed-resolution denoiser also ran, run_spatial_denoise returned its half-of-trace
178 // result directly (skipping the intermediate trace-res upsample) and this single
179 // pass reconstructs all the way to full -- one less bilateral resample, sharper.
180 const bool reduced_res = params.settings.resolution != trace_resolution::full;
181 if(reduced_res && upsample_program_.is_valid())
182 {
183 result_fb = run_upsample(rview, result_fb, params.g_buffer, params.cam, params.settings);
184 }
185 else
186 {
187 rview.fbo_remove("SSIL_UPSAMPLED");
188 rview.tex_remove("SSIL_UPSAMPLED");
189 }
191
192 return result_fb->get_texture();
193}
194
195auto ssil_pass::run_trace(gfx::render_view& rview, const run_params& params) -> gfx::frame_buffer::ptr
196{
197 APP_SCOPE_PERF("Rendering/SSIL/Trace Pass");
198
199 auto ssil_curr_fb = create_or_update_ssil_fb(rview, "SSIL_CURR", params.g_buffer, params.settings.resolution);
200
201 gfx::render_pass pass("Trace Pass");
202 pass.bind(ssil_curr_fb.get());
203 pass.set_view_proj(params.cam->get_view(), params.cam->get_projection());
204
205 trace_program_.program->begin();
206
207 gfx::set_texture(trace_program_.s_color, 0, params.direct_lighting);
208 gfx::set_texture(trace_program_.s_normal, 1, params.g_buffer->get_texture(1));
209 gfx::set_texture(trace_program_.s_hiz, 2, params.hiz_buffer);
210 gfx::set_texture(trace_program_.s_emissive, 3, params.g_buffer->get_texture(2));
211 gfx::set_texture(trace_program_.s_albedo, 4, params.g_buffer->get_texture(0));
212
213 bool multi_bounce_active = params.settings.enable_multi_bounce && params.prev_ssil && trace_program_.s_prev_ssil;
214 if(multi_bounce_active)
215 {
216 gfx::set_texture(trace_program_.s_prev_ssil, 5, params.prev_ssil);
217 }
218
219 // Environment SH for the per-ray miss fallback. Null on the first frame (the SH is
220 // computed later in the indirect pass and persists for the next frame), so bind black
221 // and signal the shader to disable the fallback (env_intensity = 0) until it exists.
222 const bool env_fallback_active = static_cast<bool>(params.irradiance_sh);
223 gfx::set_texture(trace_program_.s_irradiance, 6,
224 env_fallback_active ? params.irradiance_sh : default_textures::get().black_texture());
225
226 float ssil_params[4] = {
227 float(params.settings.max_steps),
228 float(params.settings.max_rays),
229 params.settings.depth_tolerance,
230 params.settings.brightness};
231 gfx::set_uniform(trace_program_.u_ssil_params, ssil_params);
232
233 float multi_bounce_val = multi_bounce_active ? params.settings.multi_bounce_intensity : 0.0f;
234 // The seed is fed to InterleavedGradientNoise as the temporal axis (via
235 // Hammersley16_IGN) -- it drives per-pixel scramble decorrelation across
236 // frames so the temporal accumulator averages over many distinct Monte
237 // Carlo samples instead of revisiting the same pattern forever. Using the
238 // raw render frame (rather than render_frame % max_accum_frames) keeps
239 // every frame's scramble unique within the accumulator's window. Wrap to
240 // 16 bits to keep the float-precision domain ample.
241 float ssil_params2[4] = {
242 params.settings.max_distance,
243 float(gfx::get_render_frame() & 0xFFFFu),
244 multi_bounce_val,
245 env_fallback_active ? 1.0f : 0.0f};
246 gfx::set_uniform(trace_program_.u_ssil_params2, ssil_params2);
247
248 float ssil_params3[4] = {params.settings.thickness, 0.0f, 0.0f, 0.0f};
249 gfx::set_uniform(trace_program_.u_ssil_params3, ssil_params3);
250
251 // u_ssil_resolution: xy = full G-buffer size, zw = PER-AXIS (full / trace) scale.
252 // Per-axis is required: at odd full-res W with even full-res H (e.g. 1233 x 900)
253 // the X and Y ratios disagree (2.00162 vs 2.0) and applying a scalar across both
254 // axes shifts the bottom-row gbuffer fetch ~0.7 pixels off, producing an out-of-
255 // frustum view-space ray origin and a visible noise band at the viewport bottom.
256 // SSIL hid this under its smooth hemisphere kernel; SSR exposed it sharply.
257 const auto gbuf_sz = params.g_buffer->get_size();
258 const auto trace_sz = ssil_curr_fb->get_size();
259 const float ssil_resolution[4] = {static_cast<float>(gbuf_sz.width),
260 static_cast<float>(gbuf_sz.height),
261 static_cast<float>(gbuf_sz.width) / static_cast<float>(trace_sz.width),
262 static_cast<float>(gbuf_sz.height) / static_cast<float>(trace_sz.height)};
263 gfx::set_uniform(trace_program_.u_ssil_resolution, ssil_resolution);
264
266 if(topology == 0)
267 {
268 topology = gfx::clip_quad(1.0f);
269 }
270 gfx::set_state(topology | BGFX_STATE_DEPTH_TEST_NEVER | BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A);
271 gfx::submit(pass.id, trace_program_.program->native_handle());
272
273 gfx::set_state(BGFX_STATE_DEFAULT);
274 trace_program_.program->end();
275 gfx::discard();
276
277 return ssil_curr_fb;
278}
279
280auto ssil_pass::run_spatial_denoise(gfx::render_view& rview,
281 const gfx::frame_buffer::ptr& ssil_curr,
282 const gfx::frame_buffer::ptr& g_buffer,
283 const gfx::texture::ptr& moments,
284 const camera* cam,
285 const ssil_settings& settings) -> gfx::frame_buffer::ptr
286{
287 APP_SCOPE_PERF("Rendering/SSIL/Spatial Denoise Pass");
288
289 const int num_passes = std::clamp(settings.spatial_denoise.passes, 1, 5);
290 const int max_step = std::max(settings.spatial_denoise.max_step, 1);
291 const bool has_moments = static_cast<bool>(moments);
292 // Fall back to the colour buffer for the moments sampler when temporal moments are
293 // unavailable; u_denoise_params2.x = 0 makes the shader ignore it (spatial only).
294 auto moments_tex = has_moments ? moments : ssil_curr->get_texture();
295
296 const float depth_sigma = settings.spatial_denoise.depth_sigma;
297 const float normal_power = settings.spatial_denoise.normal_power;
298 const float luma_sigma = settings.spatial_denoise.luma_sigma;
299
300 // Mixed resolution: keep `full_passes` narrow passes at the trace resolution (preserving
301 // local detail + feeding a clean edge-aware signal into the downsample), then run the
302 // remaining wide passes at HALF that resolution where their large dilation is cache-
303 // coherent and ~4x cheaper, and finally bilateral-upsample back to trace res (which
304 // restores sharp silhouettes). Falls back to all-full-res if the helper programs are
305 // unavailable or there are no passes to push down.
306 int full_passes = std::clamp(settings.spatial_denoise.full_res_passes, 0, num_passes);
307 if(has_moments)
308 {
309 // The temporal resolve writes moments at the trace resolution. Run at least one
310 // trace-resolution pass so the denoiser can consume that stable variance before
311 // the mixed half-res tier falls back to propagated spatial variance.
312 full_passes = std::max(full_passes, 1);
313 }
314 const bool mixed = downsample_program_.is_valid() && upsample_program_.is_valid() &&
315 (num_passes - full_passes) > 0;
316 if(!mixed)
317 {
318 full_passes = num_passes;
319 }
320
321 // ssil_curr already carries the trace resolution; the full-res tier buffers match it 1:1.
322 auto fb_a = create_or_update_ssil_fb(rview, "SSIL_DENOISED_A", ssil_curr, trace_resolution::full, BGFX_TEXTURE_COMPUTE_WRITE);
323 auto fb_b = create_or_update_ssil_fb(rview, "SSIL_DENOISED_B", ssil_curr, trace_resolution::full, BGFX_TEXTURE_COMPUTE_WRITE);
324 auto sz = fb_a->get_size();
325 uint32_t gx = (sz.width + 7) / 8;
326 uint32_t gy = (sz.height + 7) / 8;
327
328 // Variance ping-pong (SVGF). Single-channel R16F, compute-written and sampled. The
329 // first pass integrates variance in-shader; each subsequent pass refilters the prior
330 // pass's variance with the kernel weights squared, so the luminance sigma converges.
331 auto make_variance_tex = [&](const std::string& name, const usize32_t& size) -> gfx::texture::ptr
332 {
333 auto& tex = rview.tex_get_or_emplace(name);
334 if(gfx::needs_recreate(tex, size))
335 {
336 tex.reset();
337 tex = std::make_shared<gfx::texture>(size.width, size.height, false, 1, gfx::texture_format::R16F,
338 BGFX_TEXTURE_COMPUTE_WRITE | BGFX_SAMPLER_U_CLAMP |
339 BGFX_SAMPLER_V_CLAMP);
340 }
341 return tex;
342 };
343 auto var_a = make_variance_tex("SSIL_VARIANCE_A", sz);
344 auto var_b = make_variance_tex("SSIL_VARIANCE_B", sz);
345
346 // Single a-trous dispatch. The shader is resolution-agnostic (derives every position from
347 // its output image size vs the full-res G-buffer), so the same call drives both tiers.
348 auto run_atrous = [&](const gfx::texture::ptr& in_tex,
349 const gfx::frame_buffer::ptr& out_fb,
350 const gfx::texture::ptr& v_src,
351 const gfx::texture::ptr& v_dst,
352 uint32_t dgx,
353 uint32_t dgy,
354 int step,
355 bool first_pass,
356 bool use_moments,
357 int kernel_radius,
358 const std::string& label) -> void
359 {
360 gfx::render_pass pass(label.c_str());
361 // Bind the camera transforms so the plane-distance edge-stop can reconstruct view-
362 // space positions (computeViewSpacePosition -> u_invProj) and rotate the centre
363 // normal into view space (u_view).
364 pass.set_view_proj(cam->get_view(), cam->get_projection());
365
366 denoise_program_.program->begin();
367
368 gfx::set_texture(denoise_program_.s_ssil_input, 0, in_tex);
369 gfx::set_image(1, out_fb->get_texture()->native_handle(), 0, bgfx::Access::Write);
370 gfx::set_texture(denoise_program_.s_normal, 2, g_buffer->get_texture(1));
371 gfx::set_texture(denoise_program_.s_depth, 3, g_buffer->get_texture(4));
372 gfx::set_texture(denoise_program_.s_ssil_moments, 4, moments_tex);
373 gfx::set_texture(denoise_program_.s_ssil_variance, 5, v_src);
374 gfx::set_image(6, v_dst->native_handle(), 0, bgfx::Access::Write);
375
376 float denoise_params[4] = {float(step), depth_sigma, normal_power, luma_sigma};
377 gfx::set_uniform(denoise_program_.u_denoise_params, denoise_params);
378
379 // .w = kernel radius (2 => 5x5 full-res tier, 1 => 3x3 wide half-res tier).
380 float denoise_params2[4] = {use_moments ? 1.0f : 0.0f, first_pass ? 1.0f : 0.0f, 0.0f,
381 float(kernel_radius)};
382 gfx::set_uniform(denoise_program_.u_denoise_params2, denoise_params2);
383
384 gfx::dispatch(pass.id, denoise_program_.program->native_handle(), dgx, dgy, 1);
385
386 denoise_program_.program->end();
387 };
388
389 // --- Full-resolution tier ---
390 // Seed the variance read slot to the buffer NOT written on pass 0 so a texture is never
391 // bound as both sampler and write-image in one dispatch (pass 0 integrates variance in-
392 // shader and does not sample it anyway).
393 auto src_tex = ssil_curr->get_texture();
394 gfx::frame_buffer::ptr dst_fb = fb_a;
395 gfx::texture::ptr var_src = var_b;
396 gfx::texture::ptr var_dst = var_a;
397
398 // Trace-pixel doubling chain (1, 2, 4, 8, ...). Both tiers express their dilation in
399 // their own pixel space, so the half-res tier converts the same trace-pixel step into
400 // its half-res step. This keeps the wavelet pyramid contiguous across the tier change;
401 // computing the half-tier step from scratch as `1 << (j + 1)` introduced a one-octave
402 // gap when full_passes == 1 (full ended at step 1, half started at step 4 in trace
403 // pixels, skipping step 2) -- visible as an SVGF "missing band" artefact.
404 const int half_divisor = static_cast<int>(get_divisor(trace_resolution::half));
405 for(int i = 0; i < full_passes; ++i)
406 {
407 const int step = std::min(1 << i, max_step);
408 // Full-res tier preserves local detail -> full 5x5 (radius 2) kernel.
409 run_atrous(src_tex, dst_fb, var_src, var_dst, gx, gy, step, i == 0, has_moments, 2,
410 fmt::format("Spatial Denoise/Full Pass {}", i));
411
412 src_tex = dst_fb->get_texture();
413 dst_fb = (dst_fb == fb_a) ? fb_b : fb_a;
414 var_src = var_dst;
415 var_dst = (var_dst == var_a) ? var_b : var_a;
416 }
417
418 if(!mixed)
419 {
420 // src_tex holds the final colour; its framebuffer is the one NOT pointed at by dst_fb.
421 return (dst_fb == fb_a) ? fb_b : fb_a;
422 }
423
424 // After the full tier, `src_tex` holds the latest result and `dst_fb` is the free full-res
425 // framebuffer (used below as the upsample target). If full_passes == 0, `src_tex` is the
426 // raw trace buffer and both full-res buffers are free.
427
428 // --- Half-resolution wide tier ---
429 auto half_a = create_or_update_ssil_fb(rview, "SSIL_DENOISED_HALF_A", ssil_curr, trace_resolution::half, BGFX_TEXTURE_COMPUTE_WRITE);
430 auto half_b = create_or_update_ssil_fb(rview, "SSIL_DENOISED_HALF_B", ssil_curr, trace_resolution::half, BGFX_TEXTURE_COMPUTE_WRITE);
431 auto half_sz = half_a->get_size();
432 uint32_t hgx = (half_sz.width + 7) / 8;
433 uint32_t hgy = (half_sz.height + 7) / 8;
434 auto var_ha = make_variance_tex("SSIL_VARIANCE_HALF_A", half_sz);
435 auto var_hb = make_variance_tex("SSIL_VARIANCE_HALF_B", half_sz);
436 const bool has_downsampled_variance = full_passes > 0;
437
438 // Geometry-aware downsample of the full-res tier result into the half-res input.
439 // When a full-res tier ran, carry its variance with the colour so the wide half-res
440 // passes keep the temporal/spatial variance guidance instead of recomputing it.
441 {
442 gfx::render_pass ds_pass("Spatial Denoise/Downsample Pass");
443 ds_pass.set_view_proj(cam->get_view(), cam->get_projection());
444
445 downsample_program_.program->begin();
446 gfx::set_texture(downsample_program_.s_ssil_input, 0, src_tex);
447 gfx::set_image(1, half_a->get_texture()->native_handle(), 0, bgfx::Access::Write);
448 gfx::set_texture(downsample_program_.s_normal, 2, g_buffer->get_texture(1));
449 gfx::set_texture(downsample_program_.s_depth, 3, g_buffer->get_texture(4));
450 gfx::set_texture(downsample_program_.s_ssil_variance, 4, var_src);
451 gfx::set_image(5, var_ha->native_handle(), 0, bgfx::Access::Write);
452
453 float ds_params[4] = {depth_sigma, normal_power, has_downsampled_variance ? 1.0f : 0.0f, 0.0f};
454 gfx::set_uniform(downsample_program_.u_downsample_params, ds_params);
455
456 gfx::dispatch(ds_pass.id, downsample_program_.program->native_handle(), hgx, hgy, 1);
457 downsample_program_.program->end();
458 }
459
460 const int half_passes = num_passes - full_passes;
461 auto half_src = half_a->get_texture();
462 gfx::frame_buffer::ptr half_dst = half_b;
463 gfx::texture::ptr hvar_src = has_downsampled_variance ? var_ha : var_hb;
464 gfx::texture::ptr hvar_dst = has_downsampled_variance ? var_hb : var_ha;
465
466 for(int j = 0; j < half_passes; ++j)
467 {
468 // If the downsample carried full-res variance, propagate it; otherwise the first
469 // half-res pass computes a fresh spatial estimate.
470 const bool first_half_pass = !has_downsampled_variance && j == 0;
471 // Continue the trace-pixel power-of-two chain across the tier boundary, then
472 // express it in the half-res tier's own pixel space. With full_passes = 1 this
473 // gives trace_step (1, 2, 4, ...) -> half_step (1, 1, 2, 4, ...) -- no missing
474 // octave -- where the old `1 << (j + 1)` started at half_step 2 and skipped a
475 // band of the wavelet pyramid.
476 const int trace_step = 1 << (full_passes + j);
477 const int capped_trace_step = std::min(trace_step, max_step);
478 const int half_step = std::max(1, capped_trace_step / half_divisor);
479 // The wide tier uses the narrow 3x3 (radius 1) kernel -- indirect diffuse is low-
480 // frequency, so the outer ring adds little.
481 run_atrous(half_src, half_dst, hvar_src, hvar_dst, hgx, hgy, half_step, first_half_pass, false, 1,
482 fmt::format("Spatial Denoise/Half Pass {}", j));
483
484 half_src = half_dst->get_texture();
485 half_dst = (half_dst == half_a) ? half_b : half_a;
486 hvar_src = hvar_dst;
487 hvar_dst = (hvar_dst == var_ha) ? var_hb : var_ha;
488 }
489 auto half_result_fb = (half_dst == half_a) ? half_b : half_a;
490
491 // --- Bilateral upsample half-res wide result ---
492 // Two cases:
493 //
494 // 1. trace == full: there is no outer upsample (run() skips it because reduced_res is
495 // false), so we MUST do the half->trace(=full) reconstruction here. Render into
496 // the free full-res framebuffer (`dst_fb`); silhouettes are reconstructed sharply
497 // because the upsample rejects cross-edge taps using the full-res G-buffer.
498 //
499 // 2. trace != full: run()'s outer upsample WILL run unconditionally, and it can
500 // reconstruct directly from half-of-trace to full in a single bilateral pass.
501 // Skipping the intermediate trace-res reconstruction here saves a whole pass per
502 // frame (no allocation, no bilateral resample, no full-bandwidth write) and the
503 // fused output is sharper because we don't introduce an intermediate softening
504 // bilateral resample.
505 if(settings.resolution != trace_resolution::full)
506 {
507 return half_result_fb;
508 }
509
510 auto out_fb = dst_fb;
511 {
512 gfx::render_pass up_pass("Spatial Denoise/Upsample To Trace Resolution Pass");
513 up_pass.bind(out_fb.get());
514 up_pass.set_view_proj(cam->get_view(), cam->get_projection());
515
516 upsample_program_.program->begin();
517 gfx::set_texture(upsample_program_.s_ssil_input, 0, half_result_fb->get_texture());
518 gfx::set_texture(upsample_program_.s_normal, 1, g_buffer->get_texture(1));
519 gfx::set_texture(upsample_program_.s_depth, 2, g_buffer->get_texture(4));
520
521 // Target dim = the framebuffer we render INTO (trace res). The shader uses it to
522 // size its reconstruction footprint; without an explicit target the shader would
523 // infer from s_depth (full-res G-buffer) and over-widen the kernel at reduced
524 // trace resolutions.
525 const auto out_sz = out_fb->get_size();
526 float upsample_params[4] = {depth_sigma, normal_power, static_cast<float>(out_sz.width),
527 static_cast<float>(out_sz.height)};
528 gfx::set_uniform(upsample_program_.u_upsample_params, upsample_params);
529
530 auto topology = gfx::clip_quad(1.0f);
531 gfx::set_state(topology | BGFX_STATE_DEPTH_TEST_NEVER | BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A);
532 gfx::submit(up_pass.id, upsample_program_.program->native_handle());
533
534 gfx::set_state(BGFX_STATE_DEFAULT);
535 upsample_program_.program->end();
536 gfx::discard();
537 }
538
539 return out_fb;
540}
541
542auto ssil_pass::run_temporal_resolve(gfx::render_view& rview,
543 // ssil_input is taken by VALUE (shared_ptr copy) on
544 // purpose: a previous version took it by const ref and
545 // aliased the same caller variable as out_result_fb,
546 // so writing to the out param mutated the input and
547 // bound s_ssil_curr to the empty history fb instead
548 // of the trace output -- the running mean converged
549 // to zero (black SSIL). Passing by value gives us a
550 // local copy immune to caller mutations.
551 gfx::frame_buffer::ptr ssil_input,
552 const gfx::frame_buffer::ptr& g_buffer,
553 const gfx::texture::ptr& prev_depth,
554 const camera* cam,
555 const ssil_settings& settings,
556 gfx::frame_buffer::ptr& out_result_fb,
557 gfx::texture::ptr& out_moments_tex) -> bool
558{
559 APP_SCOPE_PERF("Rendering/SSIL/Temporal Resolve Pass");
560
561 // Ping-pong history. Two persistent MRT framebuffers (colour + moments) alternate
562 // read/write each frame, so the temporal shader writes DIRECTLY into next frame's
563 // history texture -- no blit. The previous "TEMP + 2 blits" pattern did two full-
564 // bandwidth copies per frame (colour + moments) for no semantic reason; this saves
565 // ~10 MB of bandwidth/frame at 1080p and ~40 MB at 4K. Parity comes from the bgfx
566 // render frame counter (monotonic across frames, stable within a frame).
567 const uint32_t frame = gfx::get_render_frame();
568 const bool read_is_a = (frame & 1u) == 0u;
569
570 const char* read_color_name = read_is_a ? "SSIL_HISTORY_A_COLOR" : "SSIL_HISTORY_B_COLOR";
571 const char* read_moments_name = read_is_a ? "SSIL_HISTORY_A_MOMENTS" : "SSIL_HISTORY_B_MOMENTS";
572 const char* write_fb_name = read_is_a ? "SSIL_HISTORY_B_FB" : "SSIL_HISTORY_A_FB";
573 const char* write_color_name = read_is_a ? "SSIL_HISTORY_B_COLOR" : "SSIL_HISTORY_A_COLOR";
574 const char* write_moments_name = read_is_a ? "SSIL_HISTORY_B_MOMENTS" : "SSIL_HISTORY_A_MOMENTS";
575
576 // Detect "history was just allocated" by capturing the READ texture pointers BEFORE
577 // create_or_update may replace them. If either differs after the call, we have no
578 // valid history this frame and must seed (init mode). Both textures are tracked so
579 // either-only recreation (e.g. resize that only hits one mip path) still triggers.
580 auto old_read_color = rview.tex_safe_get(read_color_name);
581 auto old_read_moments = rview.tex_safe_get(read_moments_name);
582 auto read_color = create_or_update_ssil_tex(rview, read_color_name, ssil_input, trace_resolution::full);
583 auto read_moments = create_or_update_ssil_tex(rview, read_moments_name, ssil_input, trace_resolution::full);
584 const bool history_just_allocated =
585 (read_color != old_read_color) || (read_moments != old_read_moments);
586
587 auto write_fb = create_or_update_ssil_fb_mrt(rview, write_fb_name, write_color_name, write_moments_name,
588 ssil_input, trace_resolution::full);
589 auto write_moments = rview.tex_safe_get(write_moments_name);
590
591 // The temporal result is plumbed back to the caller via the out parameters: no longer
592 // aliased under "SSIL_HISTORY_TEMP" / "SSIL_MOMENTS_TEMP" because no external pass
593 // looks those up directly (the indirect-lighting consumer samples "SSIL" via the
594 // texture ptr returned from run()).
595 out_result_fb = write_fb;
596 out_moments_tex = write_moments;
597
598 const auto gbuf_sz = g_buffer->get_size();
599 const auto temporal_sz = ssil_input->get_size();
600 const float temporal_resolution[4] = {
601 static_cast<float>(gbuf_sz.width),
602 static_cast<float>(gbuf_sz.height),
603 static_cast<float>(gbuf_sz.width) / static_cast<float>(temporal_sz.width),
604 static_cast<float>(gbuf_sz.height) / static_cast<float>(temporal_sz.height)};
605
606 const float temporal_params2[4] = {settings.temporal.normal_dot_threshold, 0.0f, 0.0f, 0.0f};
607
608 auto bind_common = [&](float enable_temporal,
609 const gfx::texture::ptr& prev_depth_tex)
610 {
611 gfx::set_texture(temporal_program_.s_ssil_curr, 0, ssil_input->get_texture());
612 gfx::set_texture(temporal_program_.s_ssil_history, 1, read_color);
613 gfx::set_texture(temporal_program_.s_depth, 2, g_buffer->get_texture(4));
614 gfx::set_texture(temporal_program_.s_prev_depth, 3, prev_depth_tex);
615 gfx::set_texture(temporal_program_.s_ssil_moments_history, 4, read_moments);
616 gfx::set_texture(temporal_program_.s_normal, 5, g_buffer->get_texture(1));
617
618 float temporal_params[4] = {enable_temporal,
619 settings.temporal.history_strength,
620 settings.temporal.depth_threshold,
621 float(settings.temporal.max_accum_frames)};
622 gfx::set_uniform(temporal_program_.u_temporal_params, temporal_params);
623 gfx::set_uniform(temporal_program_.u_temporal_params2, temporal_params2);
624 gfx::set_uniform(temporal_program_.u_temporal_resolution, temporal_resolution);
625
626 auto prev_vp = cam->get_prev_view_projection();
627 gfx::set_uniform(temporal_program_.u_prev_view_proj, prev_vp.get_matrix());
628 };
629
630 // Init mode: history was just allocated -- RGBA16F contains undefined data (possibly
631 // NaN). Run the temporal shader with `u_enable_temporal = 0`, which takes the early
632 // path that writes colour = curr and seeds the moments texture with valid (L, L^2)
633 // values. The shader writes DIRECTLY into the write fb -- no blit needed because the
634 // ping-pong picks this fb up as the READ next frame automatically.
635 if(history_just_allocated || !prev_depth)
636 {
637 gfx::render_pass init_pass("Temporal Init Pass");
638 init_pass.bind(write_fb.get());
639 init_pass.set_view_proj(cam->get_view(), cam->get_projection());
640
641 temporal_program_.program->begin();
642 bind_common(0.0f, g_buffer->get_texture(4));
643
644 auto topology = gfx::clip_quad(1.0f);
645 gfx::set_state(topology | BGFX_STATE_DEPTH_TEST_NEVER | BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A);
646 gfx::submit(init_pass.id, temporal_program_.program->native_handle());
647
648 gfx::set_state(BGFX_STATE_DEFAULT);
649 temporal_program_.program->end();
650 gfx::discard();
651 return false;
652 }
653
654 gfx::render_pass pass("Temporal Resolve Pass");
655 pass.bind(write_fb.get());
656 pass.set_view_proj(cam->get_view(), cam->get_projection());
657
658 temporal_program_.program->begin();
659 bind_common(settings.enable_temporal_accumulation ? 1.0f : 0.0f, prev_depth);
660
661 auto topology = gfx::clip_quad(1.0f);
662 gfx::set_state(topology | BGFX_STATE_DEPTH_TEST_NEVER | BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A);
663 gfx::submit(pass.id, temporal_program_.program->native_handle());
664
665 gfx::set_state(BGFX_STATE_DEFAULT);
666 temporal_program_.program->end();
667 gfx::discard();
668
669 return true;
670}
671
672auto ssil_pass::run_upsample(gfx::render_view& rview,
673 const gfx::frame_buffer::ptr& ssil_input,
674 const gfx::frame_buffer::ptr& g_buffer,
675 const camera* cam,
676 const ssil_settings& settings) -> gfx::frame_buffer::ptr
677{
678 APP_SCOPE_PERF("Rendering/SSIL/Upsample Pass");
679
680 // Output matches the full G-buffer resolution (reference = g_buffer, res = full).
681 auto out_fb = create_or_update_ssil_fb(rview, "SSIL_UPSAMPLED", g_buffer, trace_resolution::full);
682
683 gfx::render_pass pass("Output/Upsample To Full Resolution Pass");
684 pass.bind(out_fb.get());
685 // Required so the shader's computeViewSpacePosition (u_invProj/u_view) is valid
686 // for the linear-depth edge-stopping weight.
687 pass.set_view_proj(cam->get_view(), cam->get_projection());
688
689 upsample_program_.program->begin();
690
691 gfx::set_texture(upsample_program_.s_ssil_input, 0, ssil_input->get_texture());
692 gfx::set_texture(upsample_program_.s_normal, 1, g_buffer->get_texture(1));
693 gfx::set_texture(upsample_program_.s_depth, 2, g_buffer->get_texture(4));
694
695 // Reuse the spatial-denoise edge-stopping sigmas for upsample tap rejection. Target
696 // dim = full G-buffer dim (the output framebuffer we render INTO); see the in-shader
697 // u_target_dim docs for why the shader cannot infer it from s_depth.
698 const auto out_sz = out_fb->get_size();
699 float upsample_params[4] = {
700 settings.spatial_denoise.depth_sigma,
701 settings.spatial_denoise.normal_power,
702 static_cast<float>(out_sz.width),
703 static_cast<float>(out_sz.height)};
704 gfx::set_uniform(upsample_program_.u_upsample_params, upsample_params);
705
706 auto topology = gfx::clip_quad(1.0f);
707 gfx::set_state(topology | BGFX_STATE_DEPTH_TEST_NEVER | BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A);
708 gfx::submit(pass.id, upsample_program_.program->native_handle());
709
710 gfx::set_state(BGFX_STATE_DEFAULT);
711 upsample_program_.program->end();
712 gfx::discard();
713
714 return out_fb;
715}
716
717void ssil_pass::release_history_resources(gfx::render_view& rview)
718{
719 rview.fbo_remove("SSIL_HISTORY_A_FB");
720 rview.fbo_remove("SSIL_HISTORY_B_FB");
721 rview.tex_remove("SSIL_HISTORY_A_COLOR");
722 rview.tex_remove("SSIL_HISTORY_A_MOMENTS");
723 rview.tex_remove("SSIL_HISTORY_B_COLOR");
724 rview.tex_remove("SSIL_HISTORY_B_MOMENTS");
725}
726
727void ssil_pass::release_denoise_resources(gfx::render_view& rview)
728{
729 rview.fbo_remove("SSIL_DENOISED_A");
730 rview.tex_remove("SSIL_DENOISED_A");
731 rview.fbo_remove("SSIL_DENOISED_B");
732 rview.tex_remove("SSIL_DENOISED_B");
733 rview.tex_remove("SSIL_VARIANCE_A");
734 rview.tex_remove("SSIL_VARIANCE_B");
735 rview.fbo_remove("SSIL_DENOISED_HALF_A");
736 rview.tex_remove("SSIL_DENOISED_HALF_A");
737 rview.fbo_remove("SSIL_DENOISED_HALF_B");
738 rview.tex_remove("SSIL_DENOISED_HALF_B");
739 rview.tex_remove("SSIL_VARIANCE_HALF_A");
740 rview.tex_remove("SSIL_VARIANCE_HALF_B");
741}
742
744{
745 rview.fbo_remove("SSIL_CURR");
746 rview.tex_remove("SSIL_CURR");
747 release_denoise_resources(rview);
748 release_history_resources(rview);
749 rview.fbo_remove("SSIL_UPSAMPLED");
750 rview.tex_remove("SSIL_UPSAMPLED");
751}
752
753} // namespace unravel
void tex_remove(const hpp::string_view &id)
void fbo_remove(const hpp::string_view &id)
Manages assets, including loading, unloading, and storage.
auto get_asset(const std::string &key, load_flags flags=load_flags::standard, load_mode mode=load_mode::immediate) -> asset_handle< T >
Gets an asset by its key.
static auto get() -> default_textures &
auto run(gfx::render_view &rview, const run_params &params) -> gfx::texture::ptr
void release_resources(gfx::render_view &rview)
Releases all GPU resources owned by this pass from the render_view.
auto init(rtti::context &ctx) -> bool
Definition ssil_pass.cpp:13
uint32_t frame
Definition graphics.cpp:23
std::string name
Definition hub.cpp:33
void dispatch(view_id _id, program_handle _handle, uint32_t _numX, uint32_t _numY, uint32_t _numZ)
void submit(view_id _id, program_handle _handle, int32_t _depth, bool _preserveState)
void set_state(uint64_t _state, uint32_t _rgba)
Definition graphics.cpp:937
auto clip_fullscreen_triangle(float depth, float width, float height) -> uint64_t
auto needs_recreate(const gfx::frame_buffer::ptr &fbo, const usize32_t &size) -> bool
bgfx::Topology topology
Definition graphics.h:37
void discard(uint8_t _flags)
void set_image(uint8_t _stage, texture_handle _handle, uint8_t _mip, access _access, texture_format _format)
void set_uniform(uniform_handle _handle, const void *_value, uint16_t _num)
Definition graphics.cpp:977
uint32_t get_render_frame()
auto clip_quad(float depth, float width, float height) -> uint64_t
void set_texture(uint8_t _stage, uniform_handle _sampler, texture_handle _handle, uint32_t _flags)
auto get_divisor(trace_resolution res) -> uint32_t
Returns the integer divisor backing the enum value (never zero).
auto compute_trace_size(const usize32_t &ref, trace_resolution res) -> usize32_t
Computes the trace-target size from a full-resolution reference, clamped to 1x1 min.
#define APP_SCOPE_PERF(name_literal)
Create a scoped performance timer that records to the timeline profiler. Only accepts string literals...
Definition profiler.h:675
static void pop_scope()
static void push_scope(const char *name)
T width
Definition basetypes.hpp:55
T height
Definition basetypes.hpp:56
float size