From e1773c34babe53804ac23c85e01a4175b3411e52 Mon Sep 17 00:00:00 2001 From: Baplar Date: Sat, 30 May 2026 20:42:50 +0200 Subject: [PATCH 1/3] Do not parallelize building chunk offsets It turns out that the amount of effort needed to parallelize this operation and then transpose the Vec> is more costly in time than just looping sequentially, even (especially?) with large numbers of iterations. --- .../clustered_light/froxel_cpu/parallel.rs | 58 +++++++------------ 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/crates/renderide/src/passes/clustered_light/froxel_cpu/parallel.rs b/crates/renderide/src/passes/clustered_light/froxel_cpu/parallel.rs index d0bfd7a8b..6cd53f07a 100644 --- a/crates/renderide/src/passes/clustered_light/froxel_cpu/parallel.rs +++ b/crates/renderide/src/passes/clustered_light/froxel_cpu/parallel.rs @@ -1,3 +1,4 @@ +use std::num::Saturating; use std::sync::atomic::AtomicU32; use rayon::prelude::*; @@ -38,7 +39,7 @@ pub(super) fn build_parallel( let chunks = count_parallel_light_chunks(&inputs); let (counts, stats) = merge_parallel_chunk_counts(&chunks, total_clusters); let (ranges, total_indices) = prefix_counts_to_ranges(&counts)?; - let chunk_offsets = build_parallel_chunk_offsets(&chunks, &ranges, total_clusters); + let chunk_offsets = build_chunk_offsets(&chunks, &ranges, total_clusters); let indices = write_parallel_light_chunks(&inputs, &chunk_offsets, total_indices); Some(CpuClusterAssignments { @@ -103,14 +104,17 @@ fn merge_parallel_chunk_counts( chunks: &[CpuFroxelCountChunk], total_clusters: usize, ) -> (Vec, CpuFroxelStats) { + profiling::scope!("clustered_light::merge_parallel_chunk_counts"); let counts = if should_parallelize_cpu_froxel_prefix(total_clusters) { (0..total_clusters) .into_par_iter() .with_min_len(CPU_FROXEL_PREFIX_CHUNK_SIZE) .map(|cluster_id| { - chunks.iter().fold(0u32, |total, chunk| { - total.saturating_add(chunk.counts[cluster_id]) - }) + chunks + .iter() + .map(|chunk| Saturating(chunk.counts[cluster_id])) + .sum::>() + .0 }) .collect() } else { @@ -130,48 +134,28 @@ fn merge_parallel_chunk_counts( (counts, stats) } -fn build_parallel_chunk_offsets( +fn build_chunk_offsets( chunks: &[CpuFroxelCountChunk], ranges: &[[u32; 2]], total_clusters: usize, ) -> Vec> { + profiling::scope!("clustered_light::build_chunk_offsets"); let chunk_count = chunks.len(); - if should_parallelize_cpu_froxel_prefix(total_clusters) && chunk_count >= 2 { - let per_cluster_offsets = (0..total_clusters) - .into_par_iter() - .with_min_len(CPU_FROXEL_PREFIX_CHUNK_SIZE) - .map(|cluster_id| { - let mut next = ranges[cluster_id][0]; - chunks - .iter() - .map(|chunk| { - let offset = next; - next = next.saturating_add(chunk.counts[cluster_id]); - offset - }) - .collect::>() - }) - .collect::>(); - let mut chunk_offsets = (0..chunk_count) - .map(|_| vec![0u32; total_clusters]) - .collect::>(); - for (cluster_id, offsets) in per_cluster_offsets.into_iter().enumerate() { - for (chunk_idx, offset) in offsets.into_iter().enumerate() { - chunk_offsets[chunk_idx][cluster_id] = offset; - } - } - return chunk_offsets; - } - + // The amount of memory allocations/deallocations necessary + // to properly parallelize computations and then transpose the Vec> + // is not worth the amount of time spent iterating here let mut chunk_offsets = (0..chunk_count) .map(|_| vec![0u32; total_clusters]) .collect::>(); for cluster_id in 0..total_clusters { - let mut next = ranges[cluster_id][0]; - for (chunk_idx, chunk) in chunks.iter().enumerate() { - chunk_offsets[chunk_idx][cluster_id] = next; - next = next.saturating_add(chunk.counts[cluster_id]); - } + chunks + .iter() + .map(|chunk| chunk.counts[cluster_id]) + .enumerate() + .fold(ranges[cluster_id][0], |offset, (chunk_idx, chunk)| { + chunk_offsets[chunk_idx][cluster_id] = offset; + offset.saturating_add(chunk) + }); } chunk_offsets } From a7f15614386dbc82a53efeed2f401226c4325c54 Mon Sep 17 00:00:00 2001 From: Baplar Date: Sat, 30 May 2026 20:44:23 +0200 Subject: [PATCH 2/3] Add one level of parallelization to eye froxel spheres creation This allows us to do parallelize the 16 depth slices, which do not have dependencies on each other. We also do a bit of intermediate values caching as a cherry on top. --- .../clustered_light/froxel_cpu/bounds.rs | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/crates/renderide/src/passes/clustered_light/froxel_cpu/bounds.rs b/crates/renderide/src/passes/clustered_light/froxel_cpu/bounds.rs index 354c4ca89..b5aada9e7 100644 --- a/crates/renderide/src/passes/clustered_light/froxel_cpu/bounds.rs +++ b/crates/renderide/src/passes/clustered_light/froxel_cpu/bounds.rs @@ -1,4 +1,5 @@ use glam::{Mat4, Vec2, Vec3, Vec4}; +use rayon::prelude::*; use crate::gpu::GpuLight; use crate::world_mesh::cluster::{ClusterFrameParams, TILE_SIZE, sanitize_cluster_clip_planes}; @@ -190,6 +191,7 @@ pub(super) fn build_eye_froxel_spheres( return Some(Vec::new()); } + profiling::scope!("clustered_light::build_eye_froxel_spheres"); let mut all_spheres = Vec::with_capacity(eye_params.len()); for (params, &layout) in eye_params.iter().zip(layouts.iter()) { all_spheres.push(froxel_bounding_spheres(*params, layout)?); @@ -218,16 +220,43 @@ fn froxel_bounding_spheres( return None; } - let cluster_count = layout.cluster_count()?; - let mut spheres = Vec::with_capacity(cluster_count); - for z in 0..layout.cluster_count_z { - for y in 0..layout.cluster_count_y { - for x in 0..layout.cluster_count_x { - spheres.push(cluster_aabb(params, inv_proj, layout, x, y, z)?.bounding_sphere()); + (0..layout.cluster_count_z) + .into_par_iter() + .map(|z| { + let depth_bounds = cluster_z_depth_bounds( + z, + layout.cluster_count_z, + params.near_clip, + params.far_clip, + ); + ClusterDepthParams { + cluster_z: z, + near_depth: depth_bounds.0, + far_depth: depth_bounds.1, } - } - } - Some(spheres) + }) + .flat_map_iter(|depth_params| { + (0..layout.cluster_count_y) + .into_iter() + .map(move |y| (y, depth_params)) + }) + .flat_map_iter(|(y, depth_params)| { + (0..layout.cluster_count_x) + .into_iter() + .map(move |x| (x, y, depth_params)) + }) + .map(|(x, y, depth_params)| { + cluster_aabb(params, inv_proj, layout, x, y, depth_params).map(|c| c.bounding_sphere()) + }) + .collect() +} + +/// Cached logarithmic clustered-depth bounds for one Z slice. +#[derive(Clone, Copy, Debug)] +struct ClusterDepthParams { + cluster_z: u32, + near_depth: f32, + far_depth: f32, } /// Computes the view-space AABB for one froxel. @@ -237,14 +266,13 @@ fn cluster_aabb( layout: FroxelLayout, cluster_x: u32, cluster_y: u32, - cluster_z: u32, + depth_params: ClusterDepthParams, ) -> Option { - let (near_depth, far_depth) = cluster_z_depth_bounds( + let ClusterDepthParams { cluster_z, - layout.cluster_count_z, - params.near_clip, - params.far_clip, - ); + near_depth, + far_depth, + } = depth_params; let tile_near = -near_depth; let tile_far = -far_depth; From 19a9e569509f06d6e819e9b9d34b2f898669d945 Mon Sep 17 00:00:00 2001 From: Baplar Date: Sat, 30 May 2026 21:24:15 +0200 Subject: [PATCH 3/3] Clippy --- .../src/passes/clustered_light/froxel_cpu/bounds.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/renderide/src/passes/clustered_light/froxel_cpu/bounds.rs b/crates/renderide/src/passes/clustered_light/froxel_cpu/bounds.rs index b5aada9e7..5ebb29ba2 100644 --- a/crates/renderide/src/passes/clustered_light/froxel_cpu/bounds.rs +++ b/crates/renderide/src/passes/clustered_light/froxel_cpu/bounds.rs @@ -235,15 +235,9 @@ fn froxel_bounding_spheres( far_depth: depth_bounds.1, } }) - .flat_map_iter(|depth_params| { - (0..layout.cluster_count_y) - .into_iter() - .map(move |y| (y, depth_params)) - }) + .flat_map_iter(|depth_params| (0..layout.cluster_count_y).map(move |y| (y, depth_params))) .flat_map_iter(|(y, depth_params)| { - (0..layout.cluster_count_x) - .into_iter() - .map(move |x| (x, y, depth_params)) + (0..layout.cluster_count_x).map(move |x| (x, y, depth_params)) }) .map(|(x, y, depth_params)| { cluster_aabb(params, inv_proj, layout, x, y, depth_params).map(|c| c.bounding_sphere())