From 078ed7fbda192f1569aaa4a8ce60e8459eee5239 Mon Sep 17 00:00:00 2001 From: neptune Date: Tue, 6 Oct 2026 16:27:41 -0400 Subject: [PATCH] Stop rehashing FX element random draws every frame Each FX random draw is a BLAKE3 hash. Velocity graph walks hashed the same three per-element draws for every sample they touched, and the element seed was rehashed several times per element per frame across update, sort and draw. Take the velocity draws once per walk and keep the element seed on the slot, set at spawn from the same inputs, so every value is unchanged. 12 bots on mp_rust: fx_update 0.57 -> 0.25 ms per frame. Co-Authored-By: Claude Opus 5.5 --- crates/fx/src/draw.rs | 18 ++++++--------- crates/fx/src/elem.rs | 4 ++++ crates/fx/src/sort.rs | 2 +- crates/fx/src/spawn.rs | 1 + crates/fx/src/update.rs | 41 ++++++++++++---------------------- crates/fx_iw4/src/integrate.rs | 39 ++++++++++++++++---------------- 6 files changed, 47 insertions(+), 58 deletions(-) diff --git a/crates/fx/src/draw.rs b/crates/fx/src/draw.rs index 0a6bb68..8d557a7 100644 --- a/crates/fx/src/draw.rs +++ b/crates/fx/src/draw.rs @@ -1,6 +1,6 @@ use fx_iw4::{ FxElemType, FxRandomChannel, FxTrailEmittedVert, FxTrailSegmentDrawState, FxTrailVertex, - draw_elem_handler_present, elem_norm_time, elem_random_seed, sample_life_span_msec, sample_u16, + draw_elem_handler_present, elem_norm_time, sample_life_span_msec, sample_u16, spark_fountain_cluster_draw_allows, spark_fountain_slot_for_handle, trail_compute_u, trail_emit_index_quad, trail_emit_segment_verts, trail_uncompress_basis, vec3_length_sq, vec3_normalize, @@ -375,8 +375,7 @@ fn draw_one_elem( }; let age = host.msec_now.wrapping_sub(elem.msec_begin); let norm = elem_norm_time(age, elem.life_span_msec); - let elem_random_seed = - elem_random_seed(effect.random_seed, elem.sequence, elem.msec_begin); + let elem_random_seed = elem.random_seed; let origin = crate::spark::spark_elem_world_origin( elem.origin, elem.flags, @@ -423,7 +422,7 @@ fn draw_one_elem( }; let age = host.msec_now.wrapping_sub(elem.msec_begin); let norm = elem_norm_time(age, elem.life_span_msec); - let seed = elem_random_seed(effect.random_seed, elem.sequence, elem.msec_begin); + let seed = elem.random_seed; let Some(size1) = on_spark_size1(FxSparkDrawQuery { def_name: effect.def_name.as_str(), catalog_index: effect.catalog_index, @@ -468,8 +467,7 @@ fn draw_one_elem( }; let age = host.msec_now.wrapping_sub(elem.msec_begin); let norm = elem_norm_time(age, elem.life_span_msec); - let elem_random_seed = - elem_random_seed(effect.random_seed, elem.sequence, elem.msec_begin); + let elem_random_seed = elem.random_seed; let origin = crate::spark::spark_elem_world_origin( elem.origin, elem.flags, @@ -534,8 +532,7 @@ fn draw_one_elem( }; let age = host.msec_now.wrapping_sub(elem.msec_begin); let norm = elem_norm_time(age, elem.life_span_msec); - let elem_random_seed = - elem_random_seed(effect.random_seed, elem.sequence, elem.msec_begin); + let elem_random_seed = elem.random_seed; let origin = crate::spark::spark_elem_world_origin( elem.origin, elem.flags, @@ -574,8 +571,7 @@ fn draw_one_elem( }; let age = host.msec_now.wrapping_sub(elem.msec_begin); let norm = elem_norm_time(age, elem.life_span_msec); - let elem_random_seed = - elem_random_seed(effect.random_seed, elem.sequence, elem.msec_begin); + let elem_random_seed = elem.random_seed; let origin = crate::spark::spark_elem_world_origin( elem.origin, elem.flags, @@ -626,7 +622,7 @@ fn draw_one_elem( }; let age = host.msec_now.wrapping_sub(elem.msec_begin); let norm = elem_norm_time(age, elem.life_span_msec); - let elem_random_seed = elem_random_seed(effect.random_seed, elem.sequence, elem.msec_begin); + let elem_random_seed = elem.random_seed; let origin = crate::spark::spark_elem_world_origin( elem.origin, elem.flags, diff --git a/crates/fx/src/elem.rs b/crates/fx/src/elem.rs index ee7908a..07d2eff 100644 --- a/crates/fx/src/elem.rs +++ b/crates/fx/src/elem.rs @@ -22,6 +22,9 @@ pub struct FxElemSlot { pub msec_begin: i32, pub life_span_msec: i32, + /// `elem_random_seed` of the owning effect, sequence and `msec_begin`, + /// none of which change while the element lives. + pub random_seed: u64, pub base_vel: [f32; 3], pub origin: [f32; 3], @@ -54,6 +57,7 @@ impl Default for FxElemSlot { prev_elem_handle: FX_ELEM_HANDLE_NONE, msec_begin: 0, life_span_msec: 0, + random_seed: 0, base_vel: [0.0; 3], origin: [0.0; 3], spawn_origin: [[0.0; 2]; 3], diff --git a/crates/fx/src/sort.rs b/crates/fx/src/sort.rs index 2ad728e..c758087 100644 --- a/crates/fx/src/sort.rs +++ b/crates/fx/src/sort.rs @@ -145,7 +145,7 @@ fn sort_distance( } let elem = host.elems.get(elem_slot)?; let effect = host.effect_at(effect_slot)?; - let seed = fx_iw4::elem_random_seed(effect.random_seed, elem.sequence, elem.msec_begin); + let seed = elem.random_seed; let world = spark_elem_world_origin( elem.origin, elem.flags, diff --git a/crates/fx/src/spawn.rs b/crates/fx/src/spawn.rs index 1deb76b..3816866 100644 --- a/crates/fx/src/spawn.rs +++ b/crates/fx/src/spawn.rs @@ -476,6 +476,7 @@ fn spawn_elem( prev_elem_handle: FX_ELEM_HANDLE_NONE, msec_begin, life_span_msec: life, + random_seed: life_idx, base_vel: [0.0; 3], origin, spawn_origin: elem_def.spawn_origin, diff --git a/crates/fx/src/update.rs b/crates/fx/src/update.rs index 421a094..2497a5f 100644 --- a/crates/fx/src/update.rs +++ b/crates/fx/src/update.rs @@ -2,8 +2,8 @@ use fx_iw4::{ FX_ELEM_TYPE_SPARK_CLOUD, FX_ELEM_TYPE_SPARK_FOUNTAIN, FX_STATUS_HAS_PENDING_LOOP_ELEMS, FX_STATUS_REF_COUNT_MASK_IW4, FxOrientFrame, FxUpdateEffectBolt, axis_to_quat, begin_iterating_over_effects_exclusive, bolt_compose_orientation, bolt_mark_lost, - elem_norm_time, elem_random_seed, elem_uses_collision, end_iterating_over_effects, - end_iterating_runs_gc, get_orientation, unit_quat_to_axis, update_effect_bolt, vector_vectors, + elem_norm_time, elem_uses_collision, end_iterating_over_effects, end_iterating_runs_gc, + get_orientation, unit_quat_to_axis, update_effect_bolt, vector_vectors, }; use std::collections::HashMap; @@ -384,22 +384,20 @@ fn pending_collide_for_elem( } let def_index = elem.def_index; let base_vel = elem.base_vel; - let sequence = elem.sequence; + let elem_seed = elem.random_seed; let origin = elem.origin; let msec_begin = elem.msec_begin; let life_msec = elem.life_span_msec.max(1); let flags = elem.flags; - let (def_name, catalog_index, effect_seed, now, alt) = match host.effect_at(effect_slot) { + let (def_name, catalog_index, now, alt) = match host.effect_at(effect_slot) { Some(e) => ( slot_def_name(e), e.catalog_index, - e.random_seed, e.frame_now(), e.frame_when_played(), ), None => return None, }; - let elem_seed = elem_random_seed(effect_seed, sequence, msec_begin); let spawn = host .elems .get(slot) @@ -614,22 +612,20 @@ fn update_element( let msec_begin = elem.msec_begin; let life_msec = elem.life_span_msec.max(1); let base_vel = elem.base_vel; - let sequence = elem.sequence; + let elem_seed = elem.random_seed; let origin = elem.origin; let elem_type = elem.elem_type; let spark_handle = elem.spark_cloud_handle; let at_rest_fraction = elem.at_rest_fraction; - let (def_name, catalog_index, effect_seed, now, alt) = match host.effect_at(effect_slot) { + let (def_name, catalog_index, now, alt) = match host.effect_at(effect_slot) { Some(e) => ( slot_def_name(e), e.catalog_index, - e.random_seed, e.frame_now(), e.frame_when_played(), ), None => return true, }; - let elem_seed = elem_random_seed(effect_seed, sequence, msec_begin); let spawn = host .elems .get(slot) @@ -666,7 +662,7 @@ fn update_element( life_ms, dt_sec, base_vel, - elem_random_seed: elem_random_seed(effect_seed, sequence, msec_begin), + elem_random_seed: elem_seed, origin, prev_msec, msec_now, @@ -694,7 +690,6 @@ fn update_element( } } let emit_residual = host.elems.get(slot).map(|e| e.emit_residual).unwrap_or(0); - let elem_seed = elem_random_seed(effect_seed, sequence, msec_begin); if let Some(sched) = on_emit(FxEmitQuery { def_name: def_name.as_str(), catalog_index, @@ -797,25 +792,17 @@ fn update_element( def_index, age_msec: msec_now.saturating_sub(msec_begin).max(0), life_msec, - elem_random_seed: elem_random_seed(effect_seed, sequence, msec_begin), + elem_random_seed: elem_seed, norm_time: elem_norm_time(msec_now.saturating_sub(msec_begin).max(0), life_msec), }) { let (origin_now, at_rest_now, spawn, seed) = match host.elems.get(slot) { - Some(e) => { - let seed = elem_random_seed(effect_seed, sequence, msec_begin); - ( - e.origin, - e.at_rest_fraction, - Some(e.orient_spawn_params(seed)), - seed, - ) - } - None => ( - origin, - at_rest_fraction, - None, - elem_random_seed(effect_seed, sequence, msec_begin), + Some(e) => ( + e.origin, + e.at_rest_fraction, + Some(e.orient_spawn_params(elem_seed)), + elem_seed, ), + None => (origin, at_rest_fraction, None, elem_seed), }; let world = spark_elem_world_origin(origin_now, flags, &now, &alt, spawn); let axis = spark_elem_axis( diff --git a/crates/fx_iw4/src/integrate.rs b/crates/fx_iw4/src/integrate.rs index d5d188f..7f2cb7e 100644 --- a/crates/fx_iw4/src/integrate.rs +++ b/crates/fx_iw4/src/integrate.rs @@ -7,22 +7,22 @@ pub struct FxElemVec3Range { } impl FxElemVec3Range { - fn sampled(self, seed: u64) -> [f32; 3] { - core::array::from_fn(|axis| { - self.base[axis] - + self.amplitude[axis] - * crate::random::sample_f32( - seed, - [ - crate::random::FxRandomChannel::VelocityX, - crate::random::FxRandomChannel::VelocityY, - crate::random::FxRandomChannel::VelocityZ, - ][axis], - ) - }) + fn sampled(self, random: [f32; 3]) -> [f32; 3] { + core::array::from_fn(|axis| self.base[axis] + self.amplitude[axis] * random[axis]) } } +/// Every sample of an element's velocity graph shares these three draws, and +/// each one is a hash, so a graph walk takes them once. +fn velocity_random(seed: u64) -> [f32; 3] { + use crate::random::{FxRandomChannel, sample_f32}; + [ + sample_f32(seed, FxRandomChannel::VelocityX), + sample_f32(seed, FxRandomChannel::VelocityY), + sample_f32(seed, FxRandomChannel::VelocityZ), + ] +} + pub fn integrate_velocity_graph( samples: &[FxElemVec3Range], start_age01: f32, @@ -41,6 +41,7 @@ pub fn integrate_velocity_graph( let segment_count = samples.len() - 1; let segment_span_ms = life_ms * segment_count as f32; + let random = velocity_random(seed); let mut delta = [0.0f32; 3]; let first = (libm::floorf(start * segment_count as f32) as usize).min(segment_count - 1); let end_excl = @@ -53,8 +54,8 @@ pub fn integrate_velocity_graph( if hi <= lo { continue; } - let v0 = sample_lerp(samples, lo, seed); - let v1 = sample_lerp(samples, hi, seed); + let v0 = sample_lerp(samples, lo, random); + let v1 = sample_lerp(samples, hi, random); let w = 0.5 * (hi - lo) * segment_span_ms; delta[0] += (v0[0] + v1[0]) * w; delta[1] += (v0[1] + v1[1]) * w; @@ -65,7 +66,7 @@ pub fn integrate_velocity_graph( #[inline] pub fn sample_vel_graph_at_age(samples: &[FxElemVec3Range], age01: f32, seed: u64) -> [f32; 3] { - sample_lerp(samples, age01, seed) + sample_lerp(samples, age01, velocity_random(seed)) } fn clamp01(x: f32) -> f32 { @@ -78,15 +79,15 @@ fn clamp01(x: f32) -> f32 { } } -fn sample_lerp(samples: &[FxElemVec3Range], age01: f32, seed: u64) -> [f32; 3] { +fn sample_lerp(samples: &[FxElemVec3Range], age01: f32, random: [f32; 3]) -> [f32; 3] { let last = samples.len() - 1; let t = clamp01(age01); let seg = t * last as f32; let i = (libm::floorf(seg) as usize).min(last); let j = (i + 1).min(last); let f = if i == j { 0.0 } else { seg - i as f32 }; - let a = samples[i].sampled(seed); - let b = samples[j].sampled(seed); + let a = samples[i].sampled(random); + let b = samples[j].sampled(random); [ a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f,