Skip to main content

stress_preview/
stress-preview.rs

1//! Many rock draws, values of one mesh type keyed by a seed and batched by
2//! it, scattered from the field's center out to its own edge. A panel
3//! raises or lowers the load: how many instances the field draws, how many
4//! distinct seed values among them, the sun's own shadow, and whether a
5//! control turns a fraction of the field each frame, and reports what a
6//! frame costs alongside how many of its instances lie in the camera's
7//! view.
8//!
9//! `cargo run --example stress-preview`. The camera starts outside the
10//! field's edge looking across it toward the horizon, and orbits the
11//! field's center on its own until `WASD`, the arrows, the wheel or a drag
12//! first move it, at which point the orbit stops for good: `WASD` or the
13//! arrows pan across the field, the wheel raises or lowers the camera, and
14//! holding the left mouse button and dragging turns it, so turning away
15//! from the field faces open ground and turning back faces it again.
16
17use core::time::Duration;
18
19use mirage_engine::prelude::*;
20use mirage_engine::rayon::prelude::*;
21
22/// Instance count the field starts at.
23const DEFAULT_INSTANCE_COUNT: u32 = 5_000;
24/// Least and most instances the slider allows.
25const MIN_INSTANCE_COUNT: u32 = 500;
26const MAX_INSTANCE_COUNT: u32 = 500_000;
27
28/// Distinct seed values the field starts drawing.
29const DEFAULT_SEED_COUNT: u32 = 8;
30/// Least and most seed values the slider allows, which sets how many
31/// batches the field draws in.
32const MIN_SEED_COUNT: u32 = 1;
33const MAX_SEED_COUNT: u32 = 64;
34
35/// Meters from the center the field scatters over, out to the horizon
36/// under the camera's orbit.
37const FIELD_RADIUS: f32 = 400.0;
38/// Nearest a rock is placed to the center, so the middle stays clear.
39const FIELD_INNER_RADIUS: f32 = 4.0;
40
41/// One rock's side count.
42const ROCK_SIDES: u32 = 7;
43/// Its base radius and height, in meters, before either is displaced.
44const ROCK_BASE_RADIUS: f32 = 0.5;
45const ROCK_HEIGHT: f32 = 1.4;
46/// How far each base corner, and the whole rock's height, are displaced by
47/// an integer-hash of its seed, as a fraction of the base value.
48const ROCK_RADIAL_DISPLACEMENT: f32 = 0.3;
49const ROCK_HEIGHT_DISPLACEMENT: f32 = 0.3;
50
51const ROCK_COLOR: Color = Color::rgb(0.42, 0.4, 0.38);
52const GROUND_COLOR: Color = Color::rgb(0.16, 0.17, 0.14);
53
54const SUN_DIRECTION: Vec3 = Vec3::new(-0.35, -1.0, -0.5);
55const SUN_COLOR: Color = Color::rgb(0.95, 0.92, 0.85);
56
57/// The sky's own zenith, horizon and nadir colors, and the fraction of its
58/// own light it lands on top of the sun's, held low so the sun's shadows
59/// still read.
60const SKY_ZENITH: Color = Color::rgb(0.25, 0.4, 0.65);
61const SKY_HORIZON: Color = Color::rgb(0.75, 0.72, 0.62);
62const SKY_NADIR: Color = Color::rgb(0.12, 0.12, 0.1);
63const SKY_LIGHT: f32 = 0.15;
64
65/// One instance in every `MOVING_STRIDE` is the fraction a control turns
66/// each frame.
67const MOVING_STRIDE: u32 = 5;
68/// Radians a moving rock turns per second.
69const MOVING_SPEED: f32 = 0.6;
70
71/// The camera's height above the ground and its distance from the center,
72/// placed outside the field's own edge so its shallow angle down to the
73/// center keeps the horizon in frame; its vertical field of view in
74/// degrees, and the radians it turns per second.
75const CAMERA_HEIGHT: f32 = 40.0;
76const CAMERA_ORBIT_RADIUS: f32 = 440.0;
77const CAMERA_FOV: f32 = 55.0;
78const CAMERA_ANGULAR_SPEED: f32 = 0.05;
79
80/// Items the camera's view test samples at most, stepping over the field
81/// instead of a test per item where it holds more.
82const MAX_IN_VIEW_SAMPLES: u32 = 50_000;
83
84/// Meters a player's pan covers per second.
85const PAN_SPEED: f32 = 30.0;
86/// Meters one wheel step raises or lowers the camera.
87const WHEEL_STEP: f32 = 3.0;
88/// The camera's least and most height above the ground once the player
89/// holds it.
90const MIN_CAMERA_HEIGHT: f32 = 2.0;
91const MAX_CAMERA_HEIGHT: f32 = 250.0;
92/// Radians a drag turns the view by, per physical pixel it crosses.
93const LOOK_SENSITIVITY: f32 = core::f32::consts::FRAC_PI_2 / 1280.0;
94/// How far short of straight up or down the pitch may turn, in radians.
95const PITCH_LIMIT: f32 = 1.5;
96
97/// Frames the average frame time is taken over.
98const FRAME_TIME_SAMPLES: usize = 60;
99
100/// The control panel's padding, in points.
101const PANEL_PADDING: i8 = 8;
102
103fn main() {
104    run(
105        Config::new("Mirage: stress preview").with_size(1280, 720),
106        StressPreview::init,
107    );
108}
109
110/// One generated rock: a cone of [`ROCK_SIDES`] sides, whose base corners
111/// and height are each displaced by an integer-hash of `seed`.
112#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
113#[catalog(Self { seed: 0 }, Self { seed: 1 }, Self { seed: 2 })]
114struct Rock {
115    seed: u32,
116}
117
118impl Mesh for Rock {
119    fn build(&self, _: &Assets) -> MeshData {
120        build_rock(self.seed)
121    }
122}
123
124// Everything this game draws: the generated rock field and the ground
125// plane under it.
126meshes! { enum Shape { Rock, Plane } }
127
128/// The one sky this game draws, a gradient set each frame.
129#[derive(Catalog, Clone, Copy, Debug, Eq, Hash, PartialEq)]
130enum Sky {
131    Day,
132}
133
134impl Skyboxes for Sky {
135    fn build(&self, _assets: &Assets) -> SkyboxData {
136        match self {
137            Self::Day => SkyboxData::gradient(SKY_ZENITH, SKY_HORIZON, SKY_NADIR).lit_by(SKY_LIGHT),
138        }
139    }
140}
141
142/// `WASD`/arrows pan the camera across the field; the pointer's own motion
143/// turns it, read only while [`Drag::Turn`] is held.
144#[derive(InputAxis2Action, Clone, Copy, PartialEq)]
145enum Motion {
146    Pan,
147    Look,
148}
149
150impl InputAxis2Action for Motion {
151    fn bindings(&self) -> Vec<Axis2Binding> {
152        match self {
153            Self::Pan => vec![
154                Axis2Binding::from(ButtonAxis2 {
155                    left: Key::A,
156                    right: Key::D,
157                    down: Key::S,
158                    up: Key::W,
159                }),
160                Axis2Binding::from(ButtonAxis2 {
161                    left: Key::Left,
162                    right: Key::Right,
163                    down: Key::Down,
164                    up: Key::Up,
165                }),
166            ],
167            Self::Look => vec![Axis2Binding::pointer().scale(LOOK_SENSITIVITY)],
168        }
169    }
170}
171
172/// Held, turns the camera by the pointer's own motion while dragging.
173#[derive(InputButtonAction, Clone, Copy, PartialEq)]
174enum Drag {
175    Turn,
176}
177
178impl InputButtonAction for Drag {
179    fn bindings(&self) -> Vec<ButtonBinding> {
180        match self {
181            Self::Turn => vec![MouseButton::Left.into()],
182        }
183    }
184}
185
186/// How far the wheel moved this frame, which raises or lowers the camera.
187#[derive(InputAxisAction, Clone, Copy, PartialEq)]
188enum Height {
189    Wheel,
190}
191
192impl InputAxisAction for Height {
193    fn bindings(&self) -> Vec<AxisBinding> {
194        match self {
195            Self::Wheel => vec![AxisBinding::from(WheelDelta::Up)],
196        }
197    }
198}
199
200struct Controls;
201
202impl InputActions for Controls {
203    type Button = Drag;
204    type Axis = Height;
205    type Axis2 = Motion;
206}
207
208/// One entry of the field: which rock it draws, where it stands, the
209/// turn it starts at, and whether it is one the moving control turns
210/// further each frame.
211struct FieldEntry {
212    seed: u32,
213    position: Vec3,
214    phase: f32,
215    moving: bool,
216}
217
218/// The load a player sets: how many instances the field draws, how many
219/// distinct seed values among them, and the two controls beside them.
220struct Settings {
221    instance_count: u32,
222    seed_count: u32,
223    sun_shadow: bool,
224    moving: bool,
225}
226
227impl Default for Settings {
228    fn default() -> Self {
229        Self {
230            instance_count: DEFAULT_INSTANCE_COUNT,
231            seed_count: DEFAULT_SEED_COUNT,
232            sun_shadow: true,
233            moving: true,
234        }
235    }
236}
237
238/// Frame time averaged over the last [`FRAME_TIME_SAMPLES`] frames, in
239/// milliseconds.
240struct FrameTimer {
241    samples: [f32; FRAME_TIME_SAMPLES],
242    filled: usize,
243    next: usize,
244}
245
246impl FrameTimer {
247    fn new() -> Self {
248        Self {
249            samples: [0.0; FRAME_TIME_SAMPLES],
250            filled: 0,
251            next: 0,
252        }
253    }
254
255    fn record(&mut self, dt: Duration) {
256        self.samples[self.next] = dt.as_secs_f32() * 1000.0;
257        self.next = (self.next + 1) % self.samples.len();
258        self.filled = (self.filled + 1).min(self.samples.len());
259    }
260
261    /// The average over every sample kept so far, `0.0` before the first.
262    fn average_ms(&self) -> f32 {
263        if self.filled == 0 {
264            return 0.0;
265        }
266        self.samples[..self.filled].iter().sum::<f32>() / self.filled as f32
267    }
268}
269
270/// The camera once the player has taken it over: an `eye` turned by `yaw`
271/// (around the world's own up) and `pitch` (up or down).
272struct Player {
273    eye: Vec3,
274    yaw: f32,
275    pitch: f32,
276}
277
278impl Player {
279    /// This camera's forward direction, from `yaw` and `pitch`.
280    fn forward(&self) -> Vec3 {
281        Vec3::new(
282            -self.pitch.cos() * self.yaw.sin(),
283            self.pitch.sin(),
284            -self.pitch.cos() * self.yaw.cos(),
285        )
286    }
287
288    fn camera(&self) -> Camera {
289        Camera::new(
290            View::look_at(self.eye, self.eye + self.forward()),
291            Projection::perspective(CAMERA_FOV),
292        )
293    }
294}
295
296struct StressPreview {
297    settings: Settings,
298    applied_instance_count: u32,
299    applied_seed_count: u32,
300    field: Vec<FieldEntry>,
301    frame_times: FrameTimer,
302    /// Set at the instant a pan, a wheel step or a drag first moves the
303    /// camera; from then on the orbit never runs again.
304    player: Option<Player>,
305}
306
307impl StressPreview {
308    fn init(_ctx: &mut InitContext<'_, Self>) -> Result<Self, Error> {
309        let settings = Settings::default();
310        let field = build_field(settings.instance_count, settings.seed_count);
311        Ok(Self {
312            applied_instance_count: settings.instance_count,
313            applied_seed_count: settings.seed_count,
314            settings,
315            field,
316            frame_times: FrameTimer::new(),
317            player: None,
318        })
319    }
320
321    /// Rebuilds the field where the instance count or the seed count
322    /// changed since the last frame.
323    fn apply_settings(&mut self) {
324        if self.settings.instance_count == self.applied_instance_count
325            && self.settings.seed_count == self.applied_seed_count
326        {
327            return;
328        }
329        self.field = build_field(self.settings.instance_count, self.settings.seed_count);
330        self.applied_instance_count = self.settings.instance_count;
331        self.applied_seed_count = self.settings.seed_count;
332    }
333
334    /// The camera's place along the orbit at `elapsed`, before the player
335    /// takes it over.
336    fn orbit_eye(elapsed: f32) -> Vec3 {
337        let angle = elapsed * CAMERA_ANGULAR_SPEED;
338        Vec3::new(
339            angle.cos() * CAMERA_ORBIT_RADIUS,
340            CAMERA_HEIGHT,
341            angle.sin() * CAMERA_ORBIT_RADIUS,
342        )
343    }
344
345    /// The frame's camera: the orbit at `elapsed`, or the player's own
346    /// place once they have taken over.
347    fn camera(&self, elapsed: f32) -> Camera {
348        match &self.player {
349            Some(player) => player.camera(),
350            None => Camera::new(
351                View::look_at(Self::orbit_eye(elapsed), Vec3::ZERO),
352                Projection::perspective(CAMERA_FOV),
353            ),
354        }
355    }
356
357    /// Reads the pan, wheel and drag controls, taking the camera over from
358    /// the orbit at the first frame any of them moves it.
359    fn handle_camera(&mut self, ctx: &mut FrameContext<'_, Self>, elapsed: f32) {
360        if ctx.ui_wants_pointer() || ctx.ui_wants_keyboard() {
361            return;
362        }
363        let pan = ctx.axis2(Motion::Pan);
364        let wheel = ctx.axis(Height::Wheel);
365        let look = if ctx.down(Drag::Turn) {
366            ctx.axis2(Motion::Look)
367        } else {
368            Vec2::ZERO
369        };
370        if pan == Vec2::ZERO && wheel == 0.0 && look == Vec2::ZERO {
371            return;
372        }
373
374        let player = self.player.get_or_insert_with(|| {
375            let eye = Self::orbit_eye(elapsed);
376            let forward = (Vec3::ZERO - eye).normalize();
377            Player {
378                eye,
379                yaw: (-forward.x).atan2(-forward.z),
380                pitch: forward.y.asin(),
381            }
382        });
383
384        player.yaw -= look.x;
385        player.pitch = (player.pitch + look.y).clamp(-PITCH_LIMIT, PITCH_LIMIT);
386
387        let forward = Vec3::new(-player.yaw.sin(), 0.0, -player.yaw.cos());
388        let right = Vec3::new(player.yaw.cos(), 0.0, -player.yaw.sin());
389        player.eye += (forward * pan.y + right * pan.x) * PAN_SPEED * ctx.dt().as_secs_f32();
390        player.eye.y =
391            (player.eye.y + wheel * WHEEL_STEP).clamp(MIN_CAMERA_HEIGHT, MAX_CAMERA_HEIGHT);
392    }
393
394    fn draw_ground(ctx: &mut FrameContext<'_, Self>) {
395        let side = (FIELD_RADIUS + FIELD_INNER_RADIUS) * 2.2;
396        ctx.draw(
397            Plane
398                .at(Transform::from_scale(Vec3::new(side, 1.0, side)))
399                .material(Material::lit(GROUND_COLOR).roughness(0.9)),
400        );
401    }
402
403    fn draw_field(&self, ctx: &mut FrameContext<'_, Self>, elapsed: f32) {
404        for entry in &self.field {
405            let yaw = if self.settings.moving && entry.moving {
406                entry.phase + elapsed * MOVING_SPEED
407            } else {
408                entry.phase
409            };
410            ctx.draw(
411                Rock { seed: entry.seed }.at(Transform::from_scale_rotation_translation(
412                    Vec3::ONE,
413                    Quat::from_rotation_y(yaw),
414                    entry.position,
415                )),
416            );
417        }
418    }
419
420    /// How many field items lie in the camera's view at `window_size`:
421    /// every item where the field holds at most [`MAX_IN_VIEW_SAMPLES`],
422    /// otherwise one item stepped at a time and the count scaled back up
423    /// to the whole field; `true` in the second place where the count
424    /// came from such a step.
425    ///
426    /// Each item is tested on the engine's own workers: a parallel iterator
427    /// reaches them with nothing configured for it.
428    fn count_in_view(&self, camera: &Camera, window_size: UVec2) -> (usize, bool) {
429        let stride = (self.field.len() as u32 / MAX_IN_VIEW_SAMPLES).max(1) as usize;
430        let tested = self.field.par_iter().step_by(stride);
431        let tested_count = self.field.len().div_ceil(stride);
432        let in_view = tested
433            .filter(|entry| Self::in_view(camera, entry.position, window_size))
434            .count();
435        let estimate = in_view
436            .checked_mul(self.field.len())
437            .and_then(|scaled| scaled.checked_div(tested_count))
438            .unwrap_or(in_view);
439        (estimate, stride > 1)
440    }
441
442    /// Whether `position` draws inside `window_size`, the frame's own
443    /// bound of what the camera's view holds.
444    fn in_view(camera: &Camera, position: Vec3, window_size: UVec2) -> bool {
445        camera.pixel_of(position, window_size).is_some_and(|pixel| {
446            pixel.x >= 0.0
447                && pixel.y >= 0.0
448                && pixel.x < window_size.x as f32
449                && pixel.y < window_size.y as f32
450        })
451    }
452
453    /// The load controls, and this frame's own cost, reported below them.
454    fn controls(&mut self, ctx: &mut FrameContext<'_, Self>, camera: &Camera) {
455        let submitted = self.field.len();
456        let seeds = self.applied_seed_count;
457        let average_ms = self.frame_times.average_ms();
458        let fps = if average_ms > 0.0 {
459            1000.0 / average_ms
460        } else {
461            0.0
462        };
463        let elapsed = ctx.elapsed().as_secs_f32();
464        let (in_view, sampled) = self.count_in_view(camera, ctx.window_size());
465
466        ctx.ui(|ui| {
467            egui::Frame::new()
468                .fill(egui::Color32::from_gray(24))
469                .inner_margin(PANEL_PADDING)
470                .corner_radius(f32::from(PANEL_PADDING))
471                .show(ui, |ui| {
472                    ui.add(
473                        egui::Slider::new(
474                            &mut self.settings.instance_count,
475                            MIN_INSTANCE_COUNT..=MAX_INSTANCE_COUNT,
476                        )
477                        .text("instance count"),
478                    );
479                    ui.add(
480                        egui::Slider::new(
481                            &mut self.settings.seed_count,
482                            MIN_SEED_COUNT..=MAX_SEED_COUNT,
483                        )
484                        .text("distinct seeds"),
485                    );
486                    ui.checkbox(&mut self.settings.sun_shadow, "sun shadow");
487                    ui.checkbox(&mut self.settings.moving, "moving fraction");
488                    ui.separator();
489                    ui.label(format!("instances submitted {submitted}"));
490                    if sampled {
491                        ui.label(format!("in view, sampled {in_view}"));
492                    } else {
493                        ui.label(format!("instances in view {in_view}"));
494                    }
495                    ui.label(format!("distinct seeds {seeds}"));
496                    ui.label(format!("frame time {average_ms:.2}ms, {fps:.0} fps"));
497                    ui.label(format!("elapsed {elapsed:.1}s"));
498                });
499        });
500    }
501}
502
503/// `instance_count` field values, each drawing one of `seed_count`
504/// distinct seed values in a cycle, and scattered from
505/// [`FIELD_INNER_RADIUS`] out to [`FIELD_RADIUS`]; each built from an
506/// integer-hash of its own index.
507fn build_field(instance_count: u32, seed_count: u32) -> Vec<FieldEntry> {
508    (0..instance_count)
509        .map(|index| {
510            let angle = hash_unit(index, 0) * core::f32::consts::TAU;
511            let spread = hash_unit(index, 1).sqrt();
512            let distance = FIELD_INNER_RADIUS + spread * (FIELD_RADIUS - FIELD_INNER_RADIUS);
513            FieldEntry {
514                seed: index % seed_count,
515                position: Vec3::new(angle.cos() * distance, 0.0, angle.sin() * distance),
516                phase: hash_unit(index, 2) * core::f32::consts::TAU,
517                moving: index % MOVING_STRIDE == 0,
518            }
519        })
520        .collect()
521}
522
523/// A rock built from `seed`: a cone of [`ROCK_SIDES`] sides, each base
524/// corner and the apex height displaced by an integer-hash of `seed`.
525fn build_rock(seed: u32) -> MeshData {
526    let height = ROCK_HEIGHT * (1.0 + hash_signed(seed, ROCK_SIDES) * ROCK_HEIGHT_DISPLACEMENT);
527    let apex = Vec3::Y * height;
528    let base: Vec<Vec3> = (0..ROCK_SIDES)
529        .map(|corner| {
530            let angle = core::f32::consts::TAU * corner as f32 / ROCK_SIDES as f32;
531            let radius =
532                ROCK_BASE_RADIUS * (1.0 + hash_signed(seed, corner) * ROCK_RADIAL_DISPLACEMENT);
533            Vec3::new(angle.cos() * radius, 0.0, angle.sin() * radius)
534        })
535        .collect();
536
537    let mut vertices = Vec::with_capacity(base.len() * 6);
538    let mut indices = Vec::with_capacity(base.len() * 6);
539    for corner in 0..base.len() {
540        let next = (corner + 1) % base.len();
541        push_face(&mut vertices, &mut indices, base[corner], apex, base[next]);
542        push_face(
543            &mut vertices,
544            &mut indices,
545            base[corner],
546            base[next],
547            Vec3::ZERO,
548        );
549    }
550
551    MeshData::new(vertices, indices).with_material(Material::lit(ROCK_COLOR))
552}
553
554/// One triangle, shaded flat, over `a`, `b`, `c`, in the order that faces
555/// outward: counter-clockwise as seen from the side its own normal points
556/// to.
557fn push_face(vertices: &mut Vec<Vertex>, indices: &mut Vec<u32>, a: Vec3, b: Vec3, c: Vec3) {
558    let normal = (b - a).cross(c - a).normalize();
559    let uvs = [
560        Vec2::new(0.0, 1.0),
561        Vec2::new(0.5, 0.0),
562        Vec2::new(1.0, 1.0),
563    ];
564    let base = vertices.len() as u32;
565    for (point, uv) in [a, b, c].into_iter().zip(uvs) {
566        vertices.push(Vertex::new(point, normal, uv));
567    }
568    indices.extend([base, base + 1, base + 2]);
569}
570
571/// An integer-hash of `seed` and `salt`.
572fn hash(seed: u32, salt: u32) -> u32 {
573    let mut x = seed ^ salt.wrapping_mul(0x9E37_79B9);
574    x ^= x >> 16;
575    x = x.wrapping_mul(0x7FEB_352D);
576    x ^= x >> 15;
577    x = x.wrapping_mul(0x846C_A68B);
578    x ^= x >> 16;
579    x
580}
581
582/// `hash`, scaled to `0.0..1.0`.
583fn hash_unit(seed: u32, salt: u32) -> f32 {
584    hash(seed, salt) as f32 / u32::MAX as f32
585}
586
587/// `hash`, scaled to `-1.0..1.0`.
588fn hash_signed(seed: u32, salt: u32) -> f32 {
589    hash_unit(seed, salt) * 2.0 - 1.0
590}
591
592impl Game for StressPreview {
593    type Meshes = Shape;
594    type Sounds = NoSounds;
595    type InputActions = Controls;
596    type Skyboxes = Sky;
597    type SurfaceStyles = NoSurfaceStyles;
598    type PostEffects = NoPostEffects;
599
600    fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
601
602    fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
603        self.apply_settings();
604        self.frame_times.record(ctx.dt());
605
606        let elapsed = ctx.elapsed().as_secs_f32();
607        self.handle_camera(ctx, elapsed);
608        let camera = self.camera(elapsed);
609        ctx.set_camera(camera);
610        ctx.set_skybox(Sky::Day);
611
612        let sun = Light::directional(SUN_DIRECTION, SUN_COLOR);
613        ctx.light(if self.settings.sun_shadow {
614            sun.shadow()
615        } else {
616            sun
617        });
618
619        Self::draw_ground(ctx);
620        self.draw_field(ctx, elapsed);
621        self.controls(ctx, &camera);
622    }
623}