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 <noreply@anthropic.com>
This commit is contained in:
neptune
2026-10-06 16:27:41 -04:00
parent 96e9f701d5
commit 078ed7fbda
6 changed files with 47 additions and 58 deletions
+7 -11
View File
@@ -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,
+4
View File
@@ -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],
+1 -1
View File
@@ -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,
+1
View File
@@ -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,
+14 -27
View File
@@ -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(
+20 -19
View File
@@ -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,