GPU: the keyed Birth Rate test tells birth timing from birth count and checks the GPU ran (#1373)

The two histories now bear the same 180 particles by 0.9 s at different
steps (3/s from 0 to 0.5 s, and from 0.25 to 0.75 s). So a checkpoint
check by birth count alone fails the test, not only no check. A
test-only dispatch counter asserts each GPU state really came from the
GPU rather than the CPU fallback.
This commit is contained in:
tylerjenningsw
2026-10-13 21:59:03 -04:00
parent f22ce3d839
commit d04f88750e
2 changed files with 14 additions and 6 deletions
+6
View File
@@ -121,6 +121,10 @@ fn uniforms(req: &SimRequest, n: u32, s0: u64, prev_n: u32) -> Vec<u8> {
out
}
/// Simulations dispatched on the GPU (tests check the GPU path really ran).
#[cfg(test)]
pub(crate) static DISPATCHES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// Simulate `req` (see [`effectcraft_effects::psim::ParticleSim`]).
///
/// With deferred readbacks (a browser worker) the state's readback is keyed by the request
@@ -202,6 +206,8 @@ pub(crate) fn simulate(g: &GpuContext, req: &SimRequest) -> Option<Vec<SimPartic
}
enc.copy_buffer_to_buffer(&next, 0, &read, 0, size);
g.queue.submit([enc.finish()]);
#[cfg(test)]
DISPATCHES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if let (Some(d), Some(k)) = (&deferred, key) {
let row = usize::try_from(size).ok()?;
let done = d.start(k, n, 1, [0.0; 2], 1.0);
+8 -6
View File
@@ -122,23 +122,25 @@ fn keyed_state(p: &Params, t: f64, host: &Keyed) -> Vec<SimParticle> {
/// #1373: with a keyed Birth Rate the GPU is given the births of each moment, as the CPU
/// simulates them: a burst lives on after the rate drops to 0, and a GPU checkpoint of one
/// history is not resumed for another (the burst's start moved, so the same ids are born at
/// other steps; same system key).
/// history is not resumed for another with the same system key, even when both have born as
/// many particles (180 by 0.9 s here) at other steps.
#[test]
fn particle_world_with_a_keyed_birth_rate_matches_the_cpu() {
let Some(g) = gpu() else { return };
fn long(t: f64) -> f64 {
if t < 0.5 { 4.0 } else { 0.0 }
fn early(t: f64) -> f64 {
if t < 0.5 { 3.0 } else { 0.0 }
}
fn late(t: f64) -> f64 {
if (0.2..0.5).contains(&t) { 4.0 } else { 0.0 }
if (0.25..0.75).contains(&t) { 3.0 } else { 0.0 }
}
let p = params("ec.sim.ccparticleworld", &[("birthRate", n(0.0)), ("longevity", n(1.2)), ("extras/randomSeed", n(21.0))]);
for (rate, times) in [(long as fn(f64) -> f64, [0.4, 0.9, 1.2]), (late, [0.9, 1.2, 0.6])] {
for (rate, times) in [(early as fn(f64) -> f64, [0.4, 0.9, 1.2]), (late, [0.9, 1.2, 0.6])] {
let (cpu_host, gpu_host) = (Keyed { gpu: None, rate }, Keyed { gpu: Some(g), rate });
for t in times {
let cpu = keyed_state(&p, t, &cpu_host);
let before = crate::particles::DISPATCHES.load(std::sync::atomic::Ordering::Relaxed);
let gpu = keyed_state(&p, t, &gpu_host);
assert!(crate::particles::DISPATCHES.load(std::sync::atomic::Ordering::Relaxed) > before, "t {t}: simulated on the GPU");
agree(&format!("keyed birth rate t {t}"), &cpu, &gpu, 1e-4, cpu.len() / 100 + 1);
}
}