1use core::time::Duration;
18
19use mirage_engine::prelude::*;
20use mirage_engine::rayon::prelude::*;
21
22const DEFAULT_INSTANCE_COUNT: u32 = 5_000;
24const MIN_INSTANCE_COUNT: u32 = 500;
26const MAX_INSTANCE_COUNT: u32 = 500_000;
27
28const DEFAULT_SEED_COUNT: u32 = 8;
30const MIN_SEED_COUNT: u32 = 1;
33const MAX_SEED_COUNT: u32 = 64;
34
35const FIELD_RADIUS: f32 = 400.0;
38const FIELD_INNER_RADIUS: f32 = 4.0;
40
41const ROCK_SIDES: u32 = 7;
43const ROCK_BASE_RADIUS: f32 = 0.5;
45const ROCK_HEIGHT: f32 = 1.4;
46const 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
57const 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
65const MOVING_STRIDE: u32 = 5;
68const MOVING_SPEED: f32 = 0.6;
70
71const 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
80const MAX_IN_VIEW_SAMPLES: u32 = 50_000;
83
84const PAN_SPEED: f32 = 30.0;
86const WHEEL_STEP: f32 = 3.0;
88const MIN_CAMERA_HEIGHT: f32 = 2.0;
91const MAX_CAMERA_HEIGHT: f32 = 250.0;
92const LOOK_SENSITIVITY: f32 = core::f32::consts::FRAC_PI_2 / 1280.0;
94const PITCH_LIMIT: f32 = 1.5;
96
97const FRAME_TIME_SAMPLES: usize = 60;
99
100const 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#[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
124meshes! { enum Shape { Rock, Plane } }
127
128#[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#[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#[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#[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
208struct FieldEntry {
212 seed: u32,
213 position: Vec3,
214 phase: f32,
215 moving: bool,
216}
217
218struct 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
238struct 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 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
270struct Player {
273 eye: Vec3,
274 yaw: f32,
275 pitch: f32,
276}
277
278impl Player {
279 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 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 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 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 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 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 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 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 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
503fn 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
523fn 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
554fn 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
571fn 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
582fn hash_unit(seed: u32, salt: u32) -> f32 {
584 hash(seed, salt) as f32 / u32::MAX as f32
585}
586
587fn 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}