int_world::{decode, decode_deltas, decode_planes} render through world/1 and
return a Result; int_world::{load, World} hold the set lowered and loaded once,
with step, step_delta, step_action and render for the live path, which replace
the per-row step functions of a parsed model. int_world::legacy is test-only
(legacy_int). wai-web's WorldModel loads its set once and renders through it;
its methods throw on a refusal.
Callers moved: tests int_world, int_world2, int_world_shared,
int_kernels_vectors; the int_world_decode example; the decode bench's world
row. tests/int_world_memory.rs (its own binary; both fast-kernel steps): a
clip, a step and a frame hold no more than the direct renderer's.
Text: integer-operators' status note, §13.5 (before the first frame: the
plan, T giving the shape back, the informative codes), §15.3 items 2 and 3;
SPEC §8's planned breaks; CHANGELOG.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
108 lines
6.2 KiB
Rust
108 lines
6.2 KiB
Rust
//! What `wai.world.model`'s integer renderer holds, through the integer operator set,
|
|
//! against the direct renderer it replaced (`int_world::legacy`, feature `legacy_int`),
|
|
//! measured with a global allocator that tracks the bytes live and their peak.
|
|
//!
|
|
//! The renderer declares no cost: it has no payload (`extensions/integer-operators.md`
|
|
//! §13.5, informative). So what it holds is held to the direct renderer: with the set
|
|
//! loaded beforehand (`int_world::load`, once per clip or per session, as the browser's
|
|
//! `WorldModel` holds it), a clip through the operator set holds at least its frames and
|
|
//! no more than the direct renderer holds for the same clip, both on the reference
|
|
//! kernels; and so does each step and each frame of the live path.
|
|
//!
|
|
//! Its own test binary with a single test, so that no other test's allocations land in
|
|
//! the window it measures.
|
|
#![cfg(all(feature = "world_int", feature = "legacy_int"))]
|
|
|
|
use std::alloc::{GlobalAlloc, Layout, System};
|
|
use std::path::PathBuf;
|
|
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
|
|
|
|
use serde_json::Value;
|
|
use wai::int_hyper_synth::IntModel;
|
|
use wai::int_kernels::{with_policy, Policy};
|
|
use wai::int_world::{self, legacy, ACTIONS};
|
|
|
|
/// The system allocator, tracking the bytes live and the most live at once.
|
|
struct Live;
|
|
static CUR: AtomicUsize = AtomicUsize::new(0);
|
|
static PEAK: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
unsafe impl GlobalAlloc for Live {
|
|
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
|
|
PEAK.fetch_max(CUR.fetch_add(l.size(), Relaxed) + l.size(), Relaxed);
|
|
unsafe { System.alloc(l) }
|
|
}
|
|
unsafe fn alloc_zeroed(&self, l: Layout) -> *mut u8 {
|
|
PEAK.fetch_max(CUR.fetch_add(l.size(), Relaxed) + l.size(), Relaxed);
|
|
unsafe { System.alloc_zeroed(l) }
|
|
}
|
|
unsafe fn realloc(&self, p: *mut u8, l: Layout, new: usize) -> *mut u8 {
|
|
if new >= l.size() {
|
|
PEAK.fetch_max(CUR.fetch_add(new - l.size(), Relaxed) + (new - l.size()), Relaxed);
|
|
} else {
|
|
CUR.fetch_sub(l.size() - new, Relaxed);
|
|
}
|
|
unsafe { System.realloc(p, l, new) }
|
|
}
|
|
unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
|
|
CUR.fetch_sub(l.size(), Relaxed);
|
|
unsafe { System.dealloc(p, l) }
|
|
}
|
|
}
|
|
|
|
#[global_allocator]
|
|
static ALLOC: Live = Live;
|
|
|
|
/// The most bytes `f` holds at once beyond what was live when it started, and its result
|
|
/// (still held when the measure is taken).
|
|
fn peak_during<R>(f: impl FnOnce() -> R) -> (R, u64) {
|
|
let base = CUR.load(Relaxed);
|
|
PEAK.store(base, Relaxed);
|
|
let r = f();
|
|
(r, (PEAK.load(Relaxed) - base) as u64)
|
|
}
|
|
|
|
fn read(rel: &str) -> Vec<u8> {
|
|
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/vectors").join(rel);
|
|
std::fs::read(&p).unwrap_or_else(|e| panic!("{}: {e}", p.display()))
|
|
}
|
|
|
|
#[test]
|
|
fn the_renderer_holds_no_more_than_the_direct_renderer() {
|
|
for (d, js, t, g) in [("int_world", "world.json", "world_T.bin", "world_gs.bin"), ("int_world2", "world2.json", "world2_T.bin", "world2_gs.bin")] {
|
|
let j: Value = serde_json::from_slice(&read(&format!("{d}/{js}"))).unwrap();
|
|
let (c, lat, afrac) = (j["c"].as_u64().unwrap() as usize, j["lat"].as_u64().unwrap() as usize, j["afrac"].as_u64().unwrap() as u32);
|
|
let z0: Vec<i64> = j["z0"].as_array().unwrap().iter().map(|v| v.as_i64().unwrap()).collect();
|
|
let mv = |a: &Value| ACTIONS[a.as_u64().unwrap() as usize];
|
|
let rows: Vec<Vec<i64>> = match j.get("pairs") {
|
|
Some(p) => p.as_array().unwrap().iter().map(|p| {
|
|
let ((xa, ya), (xb, yb)) = (mv(&p[0]), mv(&p[1]));
|
|
[xa, ya, xb, yb].iter().map(|v| v << afrac).collect()
|
|
}).collect(),
|
|
None => j["actions"].as_array().unwrap().iter().map(|a| {
|
|
let (x, y) = mv(a);
|
|
vec![x << afrac, y << afrac, 0]
|
|
}).collect(),
|
|
};
|
|
let (tm, gm) = (IntModel::from_bytes(&read(&format!("{d}/{t}"))).unwrap(), IntModel::from_bytes(&read(&format!("{d}/{g}"))).unwrap());
|
|
// A clip: the direct renderer, then the renderer with its set loaded beforehand.
|
|
let (want, direct) = with_policy(Policy::reference(), || peak_during(|| legacy::decode_planes(&tm, &gm, &z0, &rows, c, lat)));
|
|
let (world, set) = peak_during(|| int_world::load(&tm, &gm, c).unwrap());
|
|
let (got, held) = with_policy(Policy::reference(), || peak_during(|| world.decode_planes(&z0, &rows, lat).unwrap()));
|
|
assert!(got.frames == want.frames, "{d}: the frames through the lowering are the direct renderer's");
|
|
let frames: u64 = got.frames.iter().map(|f| f.len() as u64).sum();
|
|
assert!(held >= frames && held <= direct, "{d}, set loaded: held {held}; the direct renderer held {direct}; the frames are {frames}");
|
|
// The whole call, which loads the set first.
|
|
let (r, whole) = with_policy(Policy::reference(), || peak_during(|| int_world::decode_planes(&tm, &gm, &z0, &rows, c, lat)));
|
|
drop(r.unwrap());
|
|
assert!(whole <= set + direct, "{d}: the call that loads the set held {whole}, over the set loaded ({set}) and the direct renderer's {direct}");
|
|
// The live path: one transition, then one frame, against the direct renderer's.
|
|
let (zd, step_direct) = with_policy(Policy::reference(), || peak_during(|| legacy::step_planes(&tm, &z0, &rows[0], c, lat)));
|
|
let (zo, step_held) = with_policy(Policy::reference(), || peak_during(|| world.step(&z0, &rows[0], lat).unwrap()));
|
|
assert!(zo == zd && step_held <= step_direct, "{d}: one step held {step_held}; the direct step held {step_direct}");
|
|
let (fd, frame_direct) = with_policy(Policy::reference(), || peak_during(|| legacy::decode_planes(&tm, &gm, &zd, &[], c, lat).frames));
|
|
let (fo, frame_held) = with_policy(Policy::reference(), || peak_during(|| world.render(&zo, lat).unwrap().0));
|
|
assert!(fo == fd[0] && frame_held <= frame_direct, "{d}: one frame held {frame_held}; the direct frame held {frame_direct}");
|
|
println!("{d}: a clip of {} frames, set loaded beforehand, held {held}; the direct renderer held {direct}; loading the set held {set}; the call that loads it held {whole}; one step held {step_held} (direct {step_direct}); one frame {frame_held} (direct {frame_direct})", got.frames.len());
|
|
}
|
|
}
|