feat(@projects/@magic-civilization): ✨ add gpu rng consistency test
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
bdeb6ff454
commit
0e8cec35ce
3 changed files with 58 additions and 6 deletions
|
|
@ -1 +1 @@
|
|||
{"sessionId":"4759827d-a890-479a-b9fd-8fb2951be225","pid":2159,"acquiredAt":1776363341090}
|
||||
{"sessionId":"4759827d-a890-479a-b9fd-8fb2951be225","pid":6894,"acquiredAt":1776385589030}
|
||||
|
|
@ -61,11 +61,11 @@ fn mul64(al: u32, ah: u32, bl: u32, bh: u32) -> vec2<u32> {
|
|||
}
|
||||
|
||||
// SplitMix64 step matching hash_mix(state, 0xDEADBEEFCAFEBABE) in processor.rs.
|
||||
// Combined addend: 0xDEADBEEFCAFEBABE + 0x9E3779B97F4A7C15 = 0x7CE538A94A493673
|
||||
// Combined addend: 0xDEADBEEFCAFEBABE + 0x9E3779B97F4A7C15 = 0x7CE538A94A4936D3
|
||||
// (wrapping add in u64).
|
||||
fn smix_step(lo: u32, hi: u32) -> vec2<u32> {
|
||||
// z = state + combined_const
|
||||
let c_lo = 0x4A493673u;
|
||||
let c_lo = 0x4A4936D3u;
|
||||
let c_hi = 0x7CE538A9u;
|
||||
var zl = lo + c_lo;
|
||||
var zh = hi + c_hi + select(0u, 1u, zl < lo);
|
||||
|
|
@ -113,9 +113,9 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
if uid >= uni.n_units { return; }
|
||||
|
||||
let tile = unit_tiles[uid];
|
||||
let meta = unit_meta[uid];
|
||||
let pi = (meta >> 1u) & 0x7Fu;
|
||||
let fort = (meta & 1u) != 0u;
|
||||
let um = unit_meta[uid];
|
||||
let pi = (um >> 1u) & 0x7Fu;
|
||||
let fort = (um & 1u) != 0u;
|
||||
|
||||
var rlo = player_rng[pi * 2u];
|
||||
var rhi = player_rng[pi * 2u + 1u];
|
||||
|
|
|
|||
|
|
@ -462,6 +462,58 @@ mod inner {
|
|||
"GPU kill_flags must be byte-identical to CPU across {TURNS} turns with seed={SEED}");
|
||||
}
|
||||
|
||||
/// Scalar parity: smix_step(lo, hi) == hash_mix(state, SALT) for N iterations.
|
||||
/// Catches any constant or bit-shift mismatch between CPU and WGSL.
|
||||
#[test]
|
||||
fn smix_step_matches_cpu_hash_mix() {
|
||||
let ctx = match GpuContext::try_init() {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
eprintln!("[gpu-test] No GPU adapter — skipping smix_step_matches_cpu_hash_mix");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Drive 16 single-unit dispatches with no lairs so the only RNG output
|
||||
// is the unchanged rng_state readback. We verify state evolves identically.
|
||||
let cfg = LairCombatConfig::default();
|
||||
let empty_csr = LairIndexCsr { offsets: vec![0u32; 2], flat_lair_ids: vec![] };
|
||||
let lair_tiers: Vec<u32> = vec![];
|
||||
|
||||
let mut cpu_state: u64 = 0xDEAD_C0DE_1234_5678;
|
||||
let mut gpu_state: u64 = cpu_state;
|
||||
|
||||
for _ in 0..16 {
|
||||
// CPU: one rand_unit call = one hash_mix step
|
||||
let (_, next) = rand_unit(cpu_state);
|
||||
cpu_state = next;
|
||||
|
||||
// GPU: dispatch a single unit on a tile with no lairs.
|
||||
// The kernel still calls smix_step once for the (skipped) encounter roll.
|
||||
let unit = GpuUnit { tile_idx: 0, meta: 0 };
|
||||
// Patch csr so tile 0 has one lair entry (forcing one encounter roll).
|
||||
let one_lair_csr = LairIndexCsr {
|
||||
offsets: vec![0u32, 1u32, 1u32],
|
||||
flat_lair_ids: vec![0u32],
|
||||
};
|
||||
let one_tier: Vec<u32> = vec![1u32];
|
||||
ctx.dispatch_player_fauna(
|
||||
&[unit],
|
||||
0,
|
||||
&mut gpu_state,
|
||||
&one_lair_csr,
|
||||
&one_tier,
|
||||
2,
|
||||
1,
|
||||
&cfg,
|
||||
);
|
||||
}
|
||||
|
||||
// After 16 identical steps the states must agree.
|
||||
assert_eq!(cpu_state, gpu_state,
|
||||
"smix_step must produce byte-identical output to CPU hash_mix after 16 steps");
|
||||
}
|
||||
|
||||
/// Fallback: when no GPU is available, dispatch returns empty without panicking.
|
||||
#[test]
|
||||
fn graceful_when_no_gpu() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue