use super::*;
use crate::{assets::Assets, audio::AudioHandle, storage::Storage};
const HOST_CALL_FUEL: u64 = 1;
const EMPTY_FRAME_CEILING: u64 = 8;
#[test]
fn an_empty_frame_costs_next_to_nothing() {
let empty = cart("", "", "");
let update = warm_update(&empty);
let draw = warm_draw(&empty);
assert_eq!(
update, draw,
"update and draw must cost the same to enter: {update} vs {draw} fuel"
);
assert!(
update <= EMPTY_FRAME_CEILING,
"an empty frame cost {update} fuel of the {FUEL_PER_CALL} budget"
);
}
#[test]
fn a_host_call_costs_one_fuel_at_every_arity() {
let costs = arity_costs();
let widest = costs.iter().map(|&(_, arity, ..)| arity).max().unwrap_or(0);
assert!(
costs.iter().any(|&(_, arity, ..)| arity == 0) && widest == 10,
"the sweep must span arity 0 to 10, the range docs/LIMITS.md quotes, but tops out at \
{widest} over {} measurements",
costs.len()
);
let (_, _, baseline_shape, baseline) = *costs
.first()
.expect("the sweep covers at least one import, the zero-argument one");
for (name, arity, shape, fuel) in costs {
assert_eq!(
fuel, baseline,
"{name} (arity {arity}, {shape}) cost {fuel} fuel a call against arity 0 with \
{baseline_shape}'s {baseline}"
);
}
assert_eq!(
baseline, HOST_CALL_FUEL,
"a warm host call cost {baseline} fuel, not the documented {HOST_CALL_FUEL}"
);
}
#[test]
fn a_host_call_costs_the_same_however_much_work_it_asks_for() {
const EXTENTS: &[(&str, &str, &[i32], &[i32])] = &[
(
"circle_fill",
"(param i32 i32 i32 i32)",
&[64, 64, 1, 9],
&[64, 64, 100_000, 9],
),
(
"map",
"(param i32 i32 i32 i32 i32 i32 i32)",
&[0, 0, 0, 0, 1, 1, 1],
&[0, 0, 0, 0, 100_000, 100_000, 1],
),
(
"sprite_stretch",
"(param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32)",
&[0, 0, 8, 8, 0, 0, 8, 8, 0, 0],
&[0, 0, 8, 8, 0, 0, 4096, 4096, 0, 0],
),
];
for &(name, signature, small, large) in EXTENTS {
let import = format!("(import \"pixel8\" \"{name}\" (func ${name} {signature}))");
let cost = |extents: &[i32]| {
let one = format!("(call ${name} {})", i32_args(extents));
cost_per_repetition(
warm_update,
|reps| cart(&import, &one.repeat(reps as usize), ""),
2,
2,
)
};
let (cheap, expensive) = (cost(small), cost(large));
assert_eq!(
cheap, expensive,
"{name} cost {expensive} fuel a call at {large:?} against {cheap} at {small:?}"
);
assert_eq!(
expensive, HOST_CALL_FUEL,
"{name} at {large:?} cost {expensive} fuel, not the documented {HOST_CALL_FUEL}"
);
}
}
#[test]
fn the_measured_call_shapes_really_run() {
let import = "(import \"pixel8\" \"set_pixel\" (func $ps (param i32 i32 i32)))";
let mut straight =
String::from("(local $c i32) (local $i i32)\n(local.set $c (i32.const 9))\n");
for x in 0..8 {
straight.push_str(&format!(
"(call $ps (i32.const {x}) (i32.const 0) (i32.const 9))\n\
(call $ps (i32.const {x}) (i32.const 1) (local.get $c))\n"
));
}
let looped = "(local.set $i (i32.const 8))\n\
(loop $l (call $ps (i32.add (local.get $i) (i32.const -1)) (i32.const 2) \
(i32.const 9))\n\
(local.set $i (i32.add (local.get $i) (i32.const -1)))\n\
(br_if $l (local.get $i)))";
let mut vm = vm_of(&cart(import, &format!("{straight}\n{looped}"), ""));
vm.call_update().unwrap();
for (row, shape) in [
(0, "constant arguments"),
(1, "local arguments"),
(2, "a loop"),
] {
let painted = (0..8).filter(|x| vm.state().fb.pget(*x, row) == 9).count();
assert_eq!(
painted, 8,
"only {painted} of 8 host calls with {shape} reached the framebuffer, so the fuel \
sweeps in this module are pricing a body that folded away"
);
}
}
#[test]
fn cart_side_arithmetic_costs_a_few_fuel_an_iteration() {
let integer = cost_per_repetition(warm_update, i32_loop, 1_000, 1_000);
let float = cost_per_repetition(warm_update, f32_loop, 1_000, 1_000);
assert!(
(2..=6).contains(&integer),
"a bare integer loop iteration cost {integer} fuel (measured: 3)"
);
assert!(
(integer..=integer + 6).contains(&float),
"an f32 multiply-add iteration cost {float} fuel against a bare loop's {integer} \
(measured: 5 against 3)"
);
let integer_iterations = FUEL_PER_CALL / integer;
let float_iterations = FUEL_PER_CALL / float;
assert!(
integer_iterations >= 20_000 && float_iterations >= 12_000,
"one frame affords {integer_iterations} integer and {float_iterations} float iterations \
(measured: 43,690 and 26,214)"
);
}
#[test]
fn a_tight_loop_of_draw_calls_fits_thirty_thousand_a_frame() {
let per_iteration = cost_per_repetition(warm_draw, sprite_loop_draw, 1_000, 1_000);
let fits = FUEL_PER_CALL / per_iteration;
assert!(
(30_000..=40_000).contains(&fits),
"a tight draw loop fits {fits} host calls a frame at {per_iteration} fuel an iteration \
(measured: 32,768 at 4)"
);
assert!(
draw_completes(&sprite_loop_draw(30_000)),
"30,000 host calls from a tight loop must fit one draw"
);
assert!(
!draw_completes(&sprite_loop_draw(45_000)),
"45,000 host calls from a tight loop must overrun one draw"
);
}
#[test]
fn a_rust_shaped_draw_fits_ten_thousand_calls() {
let per_call = cost_per_repetition(warm_draw, sprite_grid_draw, 100, 100);
let fits = FUEL_PER_CALL / per_call;
assert!(
(10_000..=20_000).contains(&fits),
"a Rust-shaped draw fits {fits} sprite calls a frame at {per_call} fuel each \
(measured: 13,107 at 10)"
);
let mut vm = vm_of(&sprite_grid_draw(10_000));
vm.call_draw().unwrap();
vm.call_draw()
.expect("a draw issuing 10,000 sprite calls must fit the frame budget");
let spent = FUEL_PER_CALL - vm.store.get_fuel().unwrap();
assert!(
spent > 10_000,
"10,000 sprite calls cost only {spent} fuel, so the loop cannot have run"
);
}
fn arity_costs() -> Vec<(&'static str, u32, &'static str, u64)> {
const ABI_ARITIES: &[(&str, &str, u32, bool)] = &[
("buttons_down", "(result i32)", 0, true),
("storage_clear", "", 0, false),
("clear", "(param i32)", 1, false),
("is_button_down", "(param i32) (result i32)", 1, true),
("camera", "(param i32 i32)", 2, false),
("set_pixel", "(param i32 i32 i32)", 3, false),
("circle_fill", "(param i32 i32 i32 i32)", 4, false),
("rect_fill", "(param i32 i32 i32 i32 i32)", 5, false),
("print", "(param i32 i32 i32 i32 i32) (result i32)", 5, true),
("sprite", "(param i32 i32 i32 i32 i32 i32 i32)", 7, false),
("map", "(param i32 i32 i32 i32 i32 i32 i32)", 7, false),
(
"sprite_stretch",
"(param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32)",
10,
false,
),
];
let mut costs = Vec::new();
for &(name, signature, arity, has_result) in ABI_ARITIES {
let import = format!("(import \"pixel8\" \"{name}\" (func ${name} {signature}))");
let call = |args: &str| match has_result {
true => format!("(drop (call ${name} {args}))"),
false => format!("(call ${name} {args})"),
};
let shapes = [
(
"constant args",
"",
call(&"(i32.const 1) ".repeat(arity as usize)),
),
(
"local args",
"(local $a i32)\n",
call(&"(local.get $a) ".repeat(arity as usize)),
),
];
for (shape, locals, one) in shapes {
let repeated = |reps: u32| {
cart(
&import,
&format!("{locals}{}", one.repeat(reps as usize)),
"",
)
};
costs.push((
name,
arity,
shape,
cost_per_repetition(warm_update, repeated, 10, 10),
));
}
}
costs
}
fn i32_args(values: &[i32]) -> String {
values
.iter()
.map(|v| format!("(i32.const {v}) "))
.collect::<String>()
}
fn sprite_loop_draw(iterations: u32) -> String {
cart(
SPRITE_IMPORT,
"",
&format!(
"(local $i i32) (local.set $i (i32.const {iterations}))\n\
(loop $l {SPRITE_CALL}\n\
(local.set $i (i32.add (local.get $i) (i32.const -1)))\n\
(br_if $l (local.get $i)))"
),
)
}
fn sprite_grid_draw(sprites: u32) -> String {
cart(
SPRITE_IMPORT,
"",
&format!(
"(local $i i32) (local $x i32) (local $y i32)\n\
(local.set $i (i32.const {sprites}))\n\
(loop $l\n\
(local.set $x (i32.and (i32.add (i32.mul (local.get $i) (i32.const 7)) \
(i32.const 3)) (i32.const 127)))\n\
(local.set $y (i32.and (i32.add (i32.mul (local.get $i) (i32.const 5)) \
(i32.const 9)) (i32.const 127)))\n\
(call $spr (i32.const 1) (local.get $x) (local.get $y) (i32.const 8) \
(i32.const 8) (i32.const 0) (i32.const 0))\n\
(local.set $i (i32.add (local.get $i) (i32.const -1)))\n\
(br_if $l (local.get $i)))"
),
)
}
const SPRITE_IMPORT: &str =
"(import \"pixel8\" \"sprite\" (func $spr (param i32 i32 i32 i32 i32 i32 i32)))";
const SPRITE_CALL: &str = "(call $spr (i32.const 1) (i32.const 2) (i32.const 3) (i32.const 8) \
(i32.const 8) (i32.const 0) (i32.const 0))";
fn i32_loop(iterations: u32) -> String {
cart(
"",
&format!(
"(local $i i32) (local.set $i (i32.const {iterations}))\n\
(loop $l (local.set $i (i32.add (local.get $i) (i32.const -1)))\n\
(br_if $l (local.get $i)))"
),
"",
)
}
fn f32_loop(iterations: u32) -> String {
cart(
"",
&format!(
"(local $i i32) (local $x f32) (local.set $x (f32.const 1.5))\n\
(local.set $i (i32.const {iterations}))\n\
(loop $l\n\
(local.set $x (f32.add (f32.mul (local.get $x) (f32.const 1.0001)) \
(f32.const 0.5)))\n\
(local.set $i (i32.add (local.get $i) (i32.const -1)))\n\
(br_if $l (local.get $i)))"
),
"",
)
}
fn cost_per_repetition<M, B>(measure: M, build: B, base: u32, step: u32) -> u64
where
M: Fn(&str) -> u64,
B: Fn(u32) -> String,
{
let low = measure(&build(base));
let mid = measure(&build(base + step));
let high = measure(&build(base + 2 * step));
assert!(
low < mid && mid < high,
"cost must grow with the repetition count, got {low}, {mid}, {high} — the body under \
measurement is not running"
);
assert_eq!(
mid - low,
high - mid,
"cost must be linear in the repetition count, got {low}, {mid}, {high}"
);
let per_step = mid - low;
assert_eq!(
per_step % u64::from(step),
0,
"{step} repetitions cost {per_step} fuel, which is not a whole number each"
);
per_step / u64::from(step)
}
fn warm_update(src: &str) -> u64 {
let mut vm = vm_of(src);
vm.call_update().unwrap();
vm.call_update().unwrap();
let first = FUEL_PER_CALL - vm.store.get_fuel().unwrap();
vm.call_update().unwrap();
let second = FUEL_PER_CALL - vm.store.get_fuel().unwrap();
assert_eq!(
first, second,
"an update's steady-state cost must be stable, got {first} then {second}"
);
first
}
fn warm_draw(src: &str) -> u64 {
let mut vm = vm_of(src);
vm.call_draw().unwrap();
vm.call_draw().unwrap();
let first = FUEL_PER_CALL - vm.store.get_fuel().unwrap();
vm.call_draw().unwrap();
let second = FUEL_PER_CALL - vm.store.get_fuel().unwrap();
assert_eq!(
first, second,
"a draw's steady-state cost must be stable, got {first} then {second}"
);
first
}
fn draw_completes(src: &str) -> bool {
let mut vm = vm_of(src);
let _ = vm.call_draw();
vm.call_draw().is_ok()
}
fn vm_of(src: &str) -> GameVm {
let wasm = wat::parse_str(src).unwrap();
GameVm::load(
&wasm,
&Assets::default(),
AudioHandle::dummy(),
Storage::default(),
)
.unwrap()
}
fn cart(imports: &str, update: &str, draw: &str) -> String {
format!(
"(module {imports}\n (memory (export \"memory\") 1)\n\
(func (export \"pixel8_init\"))\n\
(func (export \"pixel8_update\") {update})\n\
(func (export \"pixel8_draw\") {draw}))"
)
}