Skip to content

Commit 0500409

Browse files
Solari: Prevent world cache cells from keeping each other alive infinitely (#21904)
# Objective - Prevent world cache cells from querying each other and keeping each other alive infinitely - Now when you leave an area of your game, you're no longer paying performance cost for the out-of-view cells. - Also fix a long-standing off-by-1 bug in the world cache compaction code ## Solution - Instead of always resetting cell lifetimes to the max lifetime during a cell query, for world cache update, we now set it to the max of the current cell lifetime and it's existing lifetime (max prevents a lower-lifetime cell overwriting a higher-lifetime cell). - Credit to IsaacSM and @NthTensor for the idea! ## Testing Video showing the current number of live world cache cells, before and after this PR. https://github.com/user-attachments/assets/7639c62c-9bdb-41d7-aebb-b2494c03c042 --------- Co-authored-by: Alice Cecile <[email protected]>
1 parent c5023b7 commit 0500409

File tree

7 files changed

+39
-23
lines changed

7 files changed

+39
-23
lines changed

crates/bevy_solari/src/realtime/node.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ impl FromWorld for SolariLightingNode {
498498
"sample_radiance",
499499
load_embedded_asset!(world, "world_cache_update.wgsl"),
500500
None,
501-
vec![],
501+
vec!["WORLD_CACHE_QUERY_ATOMIC_MAX_LIFETIME".into()],
502502
),
503503
blend_new_world_cache_samples_pipeline: create_pipeline(
504504
"solari_lighting_blend_new_world_cache_samples_pipeline",

crates/bevy_solari/src/realtime/prepare.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ pub fn prepare_solari_lighting_resources(
204204

205205
let world_cache_active_cells_new_radiance =
206206
render_device.create_buffer(&BufferDescriptor {
207-
label: Some("solari_lighting_world_cache_active_cells_new_irradiance"),
207+
label: Some("solari_lighting_world_cache_active_cells_new_radiance"),
208208
size: WORLD_CACHE_SIZE * size_of::<[f32; 4]>() as u64,
209209
usage: BufferUsages::STORAGE,
210210
mapped_at_creation: false,

crates/bevy_solari/src/realtime/restir_gi.wgsl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
#import bevy_solari::gbuffer_utils::{gpixel_resolve, pixel_dissimilar, permute_pixel}
1010
#import bevy_solari::sampling::{sample_random_light, trace_point_visibility}
1111
#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, RAY_T_MIN, RAY_T_MAX}
12-
#import bevy_solari::world_cache::query_world_cache
12+
#import bevy_solari::world_cache::{query_world_cache, WORLD_CACHE_CELL_LIFETIME}
1313

1414
@group(1) @binding(0) var view_output: texture_storage_2d<rgba16float, read_write>;
1515
@group(1) @binding(5) var<storage, read_write> gi_reservoirs_a: array<Reservoir>;
@@ -105,7 +105,7 @@ fn generate_initial_reservoir(world_position: vec3<f32>, world_normal: vec3<f32>
105105
reservoir.radiance = direct_lighting.radiance;
106106
reservoir.unbiased_contribution_weight = direct_lighting.inverse_pdf * uniform_hemisphere_inverse_pdf();
107107
#else
108-
reservoir.radiance = query_world_cache(sample_point.world_position, sample_point.geometric_world_normal, view.world_position, rng);
108+
reservoir.radiance = query_world_cache(sample_point.world_position, sample_point.geometric_world_normal, view.world_position, WORLD_CACHE_CELL_LIFETIME, rng);
109109
reservoir.unbiased_contribution_weight = uniform_hemisphere_inverse_pdf();
110110
#endif
111111

crates/bevy_solari/src/realtime/specular_gi.wgsl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#import bevy_solari::gbuffer_utils::gpixel_resolve
66
#import bevy_solari::sampling::{sample_ggx_vndf, ggx_vndf_pdf}
77
#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, RAY_T_MIN, RAY_T_MAX}
8-
#import bevy_solari::world_cache::query_world_cache
8+
#import bevy_solari::world_cache::{query_world_cache, WORLD_CACHE_CELL_LIFETIME}
99

1010
@group(1) @binding(0) var view_output: texture_storage_2d<rgba16float, read_write>;
1111
@group(1) @binding(5) var<storage, read_write> gi_reservoirs_a: array<Reservoir>;
@@ -61,7 +61,7 @@ fn specular_gi(@builtin(global_invocation_id) global_id: vec3<u32>) {
6161
textureStore(view_output, global_id.xy, pixel_color);
6262

6363
#ifdef VISUALIZE_WORLD_CACHE
64-
textureStore(view_output, global_id.xy, vec4(query_world_cache(surface.world_position, surface.world_normal, view.world_position, &rng) * view.exposure, 1.0));
64+
textureStore(view_output, global_id.xy, vec4(query_world_cache(surface.world_position, surface.world_normal, view.world_position, WORLD_CACHE_CELL_LIFETIME, &rng) * view.exposure, 1.0));
6565
#endif
6666
}
6767

@@ -80,7 +80,7 @@ fn trace_glossy_path(initial_ray_origin: vec3<f32>, initial_wi: vec3<f32>, rng:
8080

8181
// Add world cache contribution
8282
let diffuse_brdf = ray_hit.material.base_color / PI;
83-
radiance += throughput * diffuse_brdf * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position, rng);
83+
radiance += throughput * diffuse_brdf * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position, WORLD_CACHE_CELL_LIFETIME, rng);
8484

8585
// Surface is very rough, terminate path in the world cache
8686
if ray_hit.material.roughness > 0.1 && i != 0u { break; }

crates/bevy_solari/src/realtime/world_cache_compact.wgsl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,14 @@ fn compact_world_cache_write_active_cells(
5959
@builtin(local_invocation_index) thread_index: u32,
6060
) {
6161
let compacted_index = world_cache_a[cell_id.x] + world_cache_b[workgroup_id.x];
62-
if world_cache_life[cell_id.x] != 0u {
62+
let cell_active = world_cache_life[cell_id.x] != 0u;
63+
64+
if cell_active {
6365
world_cache_active_cell_indices[compacted_index] = cell_id.x;
6466
}
6567

6668
if thread_index == 1023u && workgroup_id.x == 1023u {
67-
world_cache_active_cells_count = compacted_index + 1u; // TODO: This is 1 even when there are zero active entries in the cache
69+
world_cache_active_cells_count = compacted_index + u32(cell_active);
6870
world_cache_active_cells_dispatch = vec3((world_cache_active_cells_count + 63u) / 64u, 1u, 1u);
6971
}
7072
}

crates/bevy_solari/src/realtime/world_cache_query.wgsl

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@
55

66
/// How responsive the world cache is to changes in lighting (higher is less responsive, lower is more responsive)
77
const WORLD_CACHE_MAX_TEMPORAL_SAMPLES: f32 = 10.0;
8+
/// How many direct light samples each cell takes when updating each frame
9+
const WORLD_CACHE_DIRECT_LIGHT_SAMPLE_COUNT: u32 = 32u;
10+
/// Maximum amount of distance to trace GI rays between two cache cells
11+
const WORLD_CACHE_MAX_GI_RAY_DISTANCE: f32 = 50.0;
12+
813
/// Maximum amount of frames a cell can live for without being queried
9-
const WORLD_CACHE_CELL_LIFETIME: u32 = 4u;
14+
const WORLD_CACHE_CELL_LIFETIME: u32 = 30u;
1015
/// Maximum amount of attempts to find a cache entry after a hash collision
1116
const WORLD_CACHE_MAX_SEARCH_STEPS: u32 = 3u;
1217

13-
/// The size of a cache cell at the lowest LOD in meters
18+
/// Size of a cache cell at the lowest LOD in meters
1419
const WORLD_CACHE_POSITION_BASE_CELL_SIZE: f32 = 0.25;
1520
/// How fast the world cache transitions between LODs as a function of distance to the camera
1621
const WORLD_CACHE_POSITION_LOD_SCALE: f32 = 8.0;
@@ -40,7 +45,7 @@ struct WorldCacheGeometryData {
4045
@group(1) @binding(22) var<storage, read_write> world_cache_active_cells_count: u32;
4146

4247
#ifndef WORLD_CACHE_NON_ATOMIC_LIFE_BUFFER
43-
fn query_world_cache(world_position: vec3<f32>, world_normal: vec3<f32>, view_position: vec3<f32>, rng: ptr<function, u32>) -> vec3<f32> {
48+
fn query_world_cache(world_position: vec3<f32>, world_normal: vec3<f32>, view_position: vec3<f32>, cell_lifetime: u32, rng: ptr<function, u32>) -> vec3<f32> {
4449
let cell_size = get_cell_size(world_position, view_position);
4550

4651
// https://tomclabault.github.io/blog/2025/regir, jitter_world_position_tangent_plane
@@ -55,13 +60,21 @@ fn query_world_cache(world_position: vec3<f32>, world_normal: vec3<f32>, view_po
5560

5661
for (var i = 0u; i < WORLD_CACHE_MAX_SEARCH_STEPS; i++) {
5762
let existing_checksum = atomicCompareExchangeWeak(&world_cache_checksums[key], WORLD_CACHE_EMPTY_CELL, checksum).old_value;
63+
64+
// Cell already exists or is empty - reset lifetime
65+
if existing_checksum == checksum || existing_checksum == WORLD_CACHE_EMPTY_CELL {
66+
#ifndef WORLD_CACHE_QUERY_ATOMIC_MAX_LIFETIME
67+
atomicStore(&world_cache_life[key], cell_lifetime);
68+
#else
69+
atomicMax(&world_cache_life[key], cell_lifetime);
70+
#endif
71+
}
72+
5873
if existing_checksum == checksum {
59-
// Cache entry already exists - get radiance and reset cell lifetime
60-
atomicStore(&world_cache_life[key], WORLD_CACHE_CELL_LIFETIME);
74+
// Cache entry already exists - get radiance
6175
return world_cache_radiance[key].rgb;
6276
} else if existing_checksum == WORLD_CACHE_EMPTY_CELL {
63-
// Cell is empty - reset cell lifetime so that it starts getting updated next frame
64-
atomicStore(&world_cache_life[key], WORLD_CACHE_CELL_LIFETIME);
77+
// Cell is empty - initialize it
6578
world_cache_geometry_data[key].world_position = jittered_position;
6679
world_cache_geometry_data[key].world_normal = world_normal;
6780
return vec3(0.0);

crates/bevy_solari/src/realtime/world_cache_update.wgsl

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, RAY_T_MIN}
77
#import bevy_solari::world_cache::{
88
WORLD_CACHE_MAX_TEMPORAL_SAMPLES,
9+
WORLD_CACHE_DIRECT_LIGHT_SAMPLE_COUNT,
10+
WORLD_CACHE_MAX_GI_RAY_DISTANCE,
911
query_world_cache,
1012
world_cache_active_cells_count,
1113
world_cache_active_cell_indices,
14+
world_cache_life,
1215
world_cache_geometry_data,
1316
world_cache_radiance,
1417
world_cache_active_cells_new_radiance,
@@ -19,9 +22,6 @@
1922
struct PushConstants { frame_index: u32, reset: u32 }
2023
var<push_constant> constants: PushConstants;
2124

22-
const DIRECT_LIGHT_SAMPLE_COUNT: u32 = 32u;
23-
const MAX_GI_RAY_DISTANCE: f32 = 4.0;
24-
2525
@compute @workgroup_size(64, 1, 1)
2626
fn sample_radiance(@builtin(workgroup_id) workgroup_id: vec3<u32>, @builtin(global_invocation_id) active_cell_id: vec3<u32>) {
2727
if active_cell_id.x < world_cache_active_cells_count {
@@ -35,10 +35,11 @@ fn sample_radiance(@builtin(workgroup_id) workgroup_id: vec3<u32>, @builtin(glob
3535

3636
#ifndef NO_MULTIBOUNCE
3737
let ray_direction = sample_cosine_hemisphere(geometry_data.world_normal, &rng);
38-
let ray_hit = trace_ray(geometry_data.world_position, ray_direction, RAY_T_MIN, MAX_GI_RAY_DISTANCE, RAY_FLAG_NONE);
38+
let ray_hit = trace_ray(geometry_data.world_position, ray_direction, RAY_T_MIN, WORLD_CACHE_MAX_GI_RAY_DISTANCE, RAY_FLAG_NONE);
3939
if ray_hit.kind != RAY_QUERY_INTERSECTION_NONE {
4040
let ray_hit = resolve_ray_hit_full(ray_hit);
41-
new_radiance += ray_hit.material.base_color * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position, &rng);
41+
let cell_life = atomicLoad(&world_cache_life[cell_index]);
42+
new_radiance += ray_hit.material.base_color * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position, cell_life, &rng);
4243
}
4344
#endif
4445

@@ -69,8 +70,8 @@ fn sample_random_light_ris(world_position: vec3<f32>, world_normal: vec3<f32>, w
6970
var selected_sample_radiance = vec3(0.0);
7071
var selected_sample_target_function = 0.0;
7172
var selected_sample_world_position = vec4(0.0);
72-
let mis_weight = 1.0 / f32(DIRECT_LIGHT_SAMPLE_COUNT);
73-
for (var i = 0u; i < DIRECT_LIGHT_SAMPLE_COUNT; i++) {
73+
let mis_weight = 1.0 / f32(WORLD_CACHE_DIRECT_LIGHT_SAMPLE_COUNT);
74+
for (var i = 0u; i < WORLD_CACHE_DIRECT_LIGHT_SAMPLE_COUNT; i++) {
7475
let tile_sample = light_tile_start + rand_range_u(1024u, rng);
7576
let resolved_light_sample = unpack_resolved_light_sample(light_tile_resolved_samples[tile_sample], view.exposure);
7677
let light_contribution = calculate_resolved_light_contribution(resolved_light_sample, world_position, world_normal);

0 commit comments

Comments
 (0)