Skip to main content

bunnymark_ecs/
bunnymark_ecs.rs

1//! Bunnymark on the ECS: the same bouncing crabs as `bunnymark`, stored as
2//! entities and drawn by the built-in `draw_sprites` system. Compare the two
3//! to see what the ECS costs.
4//!
5//!   `cargo run --release --example bunnymark_ecs`
6//!   `CRABS=100000 cargo run --release --example bunnymark_ecs`
7//!   `CRABS=1000000 SCALE=0.25 cargo run --release --example bunnymark_ecs` (smaller crabs: fewer pixels for the GPU to fill)
8//!
9//! - Hold left mouse: add 1,000 crabs per frame
10//! - Escape: quit
11//!
12//! Vsync is off. `SECONDS=10` quits after that long (for scripted runs).
13//! Movement runs once per frame (not on the fixed tick) to match `bunnymark`.
14
15use ponko::*;
16
17struct Vel(Vec2);
18
19struct Rng(u64);
20impl Rng {
21    fn next_f32(&mut self) -> f32 {
22        self.0 ^= self.0 << 13;
23        self.0 ^= self.0 >> 7;
24        self.0 ^= self.0 << 17;
25        (self.0 >> 40) as f32 / (1u64 << 24) as f32
26    }
27}
28
29struct Bench {
30    crab: Texture,
31    rng: Rng,
32    report_at: f64,
33    frames: u32,
34    worst_ms: f32,
35    frame_start: Instant,
36    quit_after: f64,
37    scale: f32,
38}
39
40fn env_number(name: &str, default: f64) -> f64 {
41    std::env::var(name).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
42}
43
44fn spawn(ctx: &mut Ctx, n: usize, at: Vec2) {
45    let b = ctx.world.resource_mut::<Bench>();
46    let crab = b.crab;
47    let mut batch = Vec::with_capacity(n);
48    for _ in 0..n {
49        let r = &mut b.rng;
50        let vel = vec2(r.next_f32() * 400.0 - 200.0, r.next_f32() * 400.0 - 300.0);
51        let color = Color::rgb(
52            0.6 + r.next_f32() * 0.4,
53            0.6 + r.next_f32() * 0.4,
54            0.6 + r.next_f32() * 0.4,
55        );
56        batch.push((
57            Transform2D {
58                scale: Vec2::splat(b.scale),
59                ..Transform2D::at(at)
60            },
61            Sprite::new(crab).with_color(color),
62            Vel(vel),
63        ));
64    }
65    ctx.world.spawn_batch(batch);
66}
67
68fn bounce(ctx: &mut Ctx) {
69    let dt = ctx.dt();
70    let bounds = ctx.window_size();
71    let gravity = 600.0;
72    ctx.world.query::<(&mut Transform2D, &mut Vel)>().for_each(|(t, v)| {
73        let (p, v) = (&mut t.position, &mut v.0);
74        v.y += gravity * dt;
75        *p += *v * dt;
76        if p.x < 0.0 {
77            p.x = 0.0;
78            v.x = v.x.abs();
79        }
80        if p.x > bounds.x {
81            p.x = bounds.x;
82            v.x = -v.x.abs();
83        }
84        if p.y > bounds.y {
85            p.y = bounds.y;
86            v.y = -v.y.abs() * 0.85;
87        }
88        if p.y < 0.0 {
89            p.y = 0.0;
90            v.y = 0.0;
91        }
92    });
93}
94
95fn report(ctx: &mut Ctx) {
96    if ctx.mouse_down(MouseButton::Left) {
97        let at = ctx.mouse_pos();
98        spawn(ctx, 1000, at);
99    }
100    // "update + draw calls" = every system except this one, as measured by the
101    // engine (smoothed averages; draw_sprites hasn't run yet this frame).
102    let systems_ms: f32 = ctx
103        .system_timings()
104        .iter()
105        .filter(|t| !t.name.ends_with("report"))
106        .map(|t| t.avg_ms)
107        .sum();
108    let (time, count, render) = (ctx.time(), ctx.world.len(), ctx.render_stats());
109    let b = ctx.world.resource_mut::<Bench>();
110    let now = Instant::now();
111    let frame_ms = now.duration_since(b.frame_start).as_secs_f32() * 1000.0;
112    b.frame_start = now;
113    b.worst_ms = b.worst_ms.max(frame_ms);
114    b.frames += 1;
115    if time >= b.report_at {
116        println!(
117            "crabs: {:>8}  fps: {:>6.1}  avg frame: {:>6.2} ms  worst: {:>6.2} ms  update+draw calls: {:>6.2} ms  render cpu: {:>5.2} ms  gpu: {}",
118            count,
119            b.frames,
120            1000.0 / b.frames as f64,
121            b.worst_ms,
122            systems_ms,
123            render.cpu_ms,
124            render.gpu_ms.map_or("n/a".into(), |g| format!("{g:.2} ms")),
125        );
126        b.report_at += 1.0;
127        b.frames = 0;
128        b.worst_ms = 0.0;
129    }
130    let quit_after = b.quit_after;
131    if ctx.key_pressed(Key::Escape) || (quit_after > 0.0 && time >= quit_after) {
132        ctx.quit();
133    }
134}
135
136fn main() {
137    let start_count = env_number("CRABS", 10_000.0) as usize;
138    let quit_after = env_number("SECONDS", 0.0);
139
140    Game::new("Crabby Engine: bunnymark (ECS)", 1280, 720)
141        .vsync(false)
142        .system(bounce)
143        .system(report)
144        .run(move |ctx| {
145            let crab = ctx.load_texture("crab.png");
146            ctx.world.insert_resource(Bench {
147                crab,
148                rng: Rng(0x9E37_79B9_7F4A_7C15),
149                report_at: 1.0,
150                frames: 0,
151                worst_ms: 0.0,
152                frame_start: Instant::now(),
153                quit_after,
154                scale: env_number("SCALE", 1.0) as f32,
155            });
156            spawn(ctx, start_count, vec2(20.0, 20.0));
157        });
158}