1use core::time::Duration;
18
19use mirage_engine::prelude::*;
20
21const DEFAULT_INSTANCE_COUNT: u32 = 5_000;
23const MIN_INSTANCE_COUNT: u32 = 500;
25const MAX_INSTANCE_COUNT: u32 = 500_000;
26
27const DEFAULT_SEED_COUNT: u32 = 8;
29const MIN_SEED_COUNT: u32 = 1;
32const MAX_SEED_COUNT: u32 = 64;
33
34const FIELD_RADIUS: f32 = 400.0;
37const FIELD_INNER_RADIUS: f32 = 4.0;
39
40const ROCK_SIDES: u32 = 7;
42const ROCK_BASE_RADIUS: f32 = 0.5;
44const ROCK_HEIGHT: f32 = 1.4;
45const ROCK_RADIAL_DISPLACEMENT: f32 = 0.3;
48const ROCK_HEIGHT_DISPLACEMENT: f32 = 0.3;
49
50const ROCK_COLOR: Color = Color::rgb(0.42, 0.4, 0.38);
51const GROUND_COLOR: Color = Color::rgb(0.16, 0.17, 0.14);
52
53const SUN_DIRECTION: Vec3 = Vec3::new(-0.35, -1.0, -0.5);
54const SUN_COLOR: Color = Color::rgb(0.95, 0.92, 0.85);
55
56const SKY_ZENITH: Color = Color::rgb(0.25, 0.4, 0.65);
60const SKY_HORIZON: Color = Color::rgb(0.75, 0.72, 0.62);
61const SKY_NADIR: Color = Color::rgb(0.12, 0.12, 0.1);
62const SKY_LIGHT: f32 = 0.15;
63
64const MOVING_STRIDE: u32 = 5;
67const MOVING_SPEED: f32 = 0.6;
69
70const CAMERA_HEIGHT: f32 = 40.0;
75const CAMERA_ORBIT_RADIUS: f32 = 440.0;
76const CAMERA_FOV: f32 = 55.0;
77const CAMERA_ANGULAR_SPEED: f32 = 0.05;
78
79const MAX_IN_VIEW_SAMPLES: u32 = 50_000;
82
83const PAN_SPEED: f32 = 30.0;
85const WHEEL_STEP: f32 = 3.0;
87const MIN_CAMERA_HEIGHT: f32 = 2.0;
90const MAX_CAMERA_HEIGHT: f32 = 250.0;
91const LOOK_SENSITIVITY: f32 = core::f32::consts::FRAC_PI_2 / 1280.0;
93const PITCH_LIMIT: f32 = 1.5;
95
96const FRAME_TIME_SAMPLES: usize = 60;
98
99const PANEL_PADDING: i8 = 8;
101
102fn main() {
103 run(
104 Config::new("Mirage: stress preview").with_size(1280, 720),
105 StressPreview::init,
106 );
107}
108
109#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
112#[catalog(Self { seed: 0 }, Self { seed: 1 }, Self { seed: 2 })]
113struct Rock {
114 seed: u32,
115}
116
117impl Mesh for Rock {
118 fn build(&self, _: &Assets) -> MeshData {
119 build_rock(self.seed)
120 }
121}
122
123meshes! { enum Shape { Rock, Plane } }
126
127#[derive(Catalog, Clone, Copy, Debug, Eq, Hash, PartialEq)]
129enum Sky {
130 Day,
131}
132
133impl Skyboxes for Sky {
134 fn build(&self, _assets: &Assets) -> SkyboxData {
135 match self {
136 Self::Day => SkyboxData::gradient(SKY_ZENITH, SKY_HORIZON, SKY_NADIR).lit_by(SKY_LIGHT),
137 }
138 }
139}
140
141#[derive(InputAxis2Action, Clone, Copy, PartialEq)]
144enum Motion {
145 Pan,
146 Look,
147}
148
149impl InputAxis2Action for Motion {
150 fn bindings(&self) -> Vec<Axis2Binding> {
151 match self {
152 Self::Pan => vec![
153 Axis2Binding::from(ButtonAxis2 {
154 left: Key::A,
155 right: Key::D,
156 down: Key::S,
157 up: Key::W,
158 }),
159 Axis2Binding::from(ButtonAxis2 {
160 left: Key::Left,
161 right: Key::Right,
162 down: Key::Down,
163 up: Key::Up,
164 }),
165 ],
166 Self::Look => vec![Axis2Binding::pointer().scale(LOOK_SENSITIVITY)],
167 }
168 }
169}
170
171#[derive(InputButtonAction, Clone, Copy, PartialEq)]
173enum Drag {
174 Turn,
175}
176
177impl InputButtonAction for Drag {
178 fn bindings(&self) -> Vec<ButtonBinding> {
179 match self {
180 Self::Turn => vec![MouseButton::Left.into()],
181 }
182 }
183}
184
185#[derive(InputAxisAction, Clone, Copy, PartialEq)]
187enum Height {
188 Wheel,
189}
190
191impl InputAxisAction for Height {
192 fn bindings(&self) -> Vec<AxisBinding> {
193 match self {
194 Self::Wheel => vec![AxisBinding::from(WheelDelta::Up)],
195 }
196 }
197}
198
199struct Controls;
200
201impl InputActions for Controls {
202 type Button = Drag;
203 type Axis = Height;
204 type Axis2 = Motion;
205}
206
207struct FieldEntry {
211 seed: u32,
212 position: Vec3,
213 phase: f32,
214 moving: bool,
215}
216
217struct Settings {
220 instance_count: u32,
221 seed_count: u32,
222 sun_shadow: bool,
223 moving: bool,
224}
225
226impl Default for Settings {
227 fn default() -> Self {
228 Self {
229 instance_count: DEFAULT_INSTANCE_COUNT,
230 seed_count: DEFAULT_SEED_COUNT,
231 sun_shadow: true,
232 moving: true,
233 }
234 }
235}
236
237struct FrameTimer {
240 samples: [f32; FRAME_TIME_SAMPLES],
241 filled: usize,
242 next: usize,
243}
244
245impl FrameTimer {
246 fn new() -> Self {
247 Self {
248 samples: [0.0; FRAME_TIME_SAMPLES],
249 filled: 0,
250 next: 0,
251 }
252 }
253
254 fn record(&mut self, dt: Duration) {
255 self.samples[self.next] = dt.as_secs_f32() * 1000.0;
256 self.next = (self.next + 1) % self.samples.len();
257 self.filled = (self.filled + 1).min(self.samples.len());
258 }
259
260 fn average_ms(&self) -> f32 {
262 if self.filled == 0 {
263 return 0.0;
264 }
265 self.samples[..self.filled].iter().sum::<f32>() / self.filled as f32
266 }
267}
268
269struct Player {
272 eye: Vec3,
273 yaw: f32,
274 pitch: f32,
275}
276
277impl Player {
278 fn forward(&self) -> Vec3 {
280 Vec3::new(
281 -self.pitch.cos() * self.yaw.sin(),
282 self.pitch.sin(),
283 -self.pitch.cos() * self.yaw.cos(),
284 )
285 }
286
287 fn camera(&self) -> Camera {
288 Camera::new(
289 View::look_at(self.eye, self.eye + self.forward()),
290 Projection::perspective(CAMERA_FOV),
291 )
292 }
293}
294
295struct StressPreview {
296 settings: Settings,
297 applied_instance_count: u32,
298 applied_seed_count: u32,
299 field: Vec<FieldEntry>,
300 frame_times: FrameTimer,
301 player: Option<Player>,
304}
305
306impl StressPreview {
307 fn init(_ctx: &mut InitContext<'_, Self>) -> Result<Self, Error> {
308 let settings = Settings::default();
309 let field = build_field(settings.instance_count, settings.seed_count);
310 Ok(Self {
311 applied_instance_count: settings.instance_count,
312 applied_seed_count: settings.seed_count,
313 settings,
314 field,
315 frame_times: FrameTimer::new(),
316 player: None,
317 })
318 }
319
320 fn apply_settings(&mut self) {
323 if self.settings.instance_count == self.applied_instance_count
324 && self.settings.seed_count == self.applied_seed_count
325 {
326 return;
327 }
328 self.field = build_field(self.settings.instance_count, self.settings.seed_count);
329 self.applied_instance_count = self.settings.instance_count;
330 self.applied_seed_count = self.settings.seed_count;
331 }
332
333 fn orbit_eye(elapsed: f32) -> Vec3 {
336 let angle = elapsed * CAMERA_ANGULAR_SPEED;
337 Vec3::new(
338 angle.cos() * CAMERA_ORBIT_RADIUS,
339 CAMERA_HEIGHT,
340 angle.sin() * CAMERA_ORBIT_RADIUS,
341 )
342 }
343
344 fn camera(&self, elapsed: f32) -> Camera {
347 match &self.player {
348 Some(player) => player.camera(),
349 None => Camera::new(
350 View::look_at(Self::orbit_eye(elapsed), Vec3::ZERO),
351 Projection::perspective(CAMERA_FOV),
352 ),
353 }
354 }
355
356 fn handle_camera(&mut self, ctx: &mut FrameContext<'_, Self>, elapsed: f32) {
359 if ctx.ui_wants_pointer() || ctx.ui_wants_keyboard() {
360 return;
361 }
362 let pan = ctx.axis2(Motion::Pan);
363 let wheel = ctx.axis(Height::Wheel);
364 let look = if ctx.down(Drag::Turn) {
365 ctx.axis2(Motion::Look)
366 } else {
367 Vec2::ZERO
368 };
369 if pan == Vec2::ZERO && wheel == 0.0 && look == Vec2::ZERO {
370 return;
371 }
372
373 let player = self.player.get_or_insert_with(|| {
374 let eye = Self::orbit_eye(elapsed);
375 let forward = (Vec3::ZERO - eye).normalize();
376 Player {
377 eye,
378 yaw: (-forward.x).atan2(-forward.z),
379 pitch: forward.y.asin(),
380 }
381 });
382
383 player.yaw -= look.x;
384 player.pitch = (player.pitch + look.y).clamp(-PITCH_LIMIT, PITCH_LIMIT);
385
386 let forward = Vec3::new(-player.yaw.sin(), 0.0, -player.yaw.cos());
387 let right = Vec3::new(player.yaw.cos(), 0.0, -player.yaw.sin());
388 player.eye += (forward * pan.y + right * pan.x) * PAN_SPEED * ctx.dt().as_secs_f32();
389 player.eye.y =
390 (player.eye.y + wheel * WHEEL_STEP).clamp(MIN_CAMERA_HEIGHT, MAX_CAMERA_HEIGHT);
391 }
392
393 fn draw_ground(ctx: &mut FrameContext<'_, Self>) {
394 let side = (FIELD_RADIUS + FIELD_INNER_RADIUS) * 2.2;
395 ctx.draw(
396 Plane
397 .at(Transform::from_scale(Vec3::new(side, 1.0, side)))
398 .material(Material::lit(GROUND_COLOR).roughness(0.9)),
399 );
400 }
401
402 fn draw_field(&self, ctx: &mut FrameContext<'_, Self>, elapsed: f32) {
403 for entry in &self.field {
404 let yaw = if self.settings.moving && entry.moving {
405 entry.phase + elapsed * MOVING_SPEED
406 } else {
407 entry.phase
408 };
409 ctx.draw(
410 Rock { seed: entry.seed }.at(Transform::from_scale_rotation_translation(
411 Vec3::ONE,
412 Quat::from_rotation_y(yaw),
413 entry.position,
414 )),
415 );
416 }
417 }
418
419 fn count_in_view(&self, camera: &Camera, window_size: UVec2) -> (usize, bool) {
425 let stride = (self.field.len() as u32 / MAX_IN_VIEW_SAMPLES).max(1) as usize;
426 let tested = self.field.iter().step_by(stride);
427 let tested_count = tested.clone().count();
428 let in_view = tested
429 .filter(|entry| Self::in_view(camera, entry.position, window_size))
430 .count();
431 let estimate = in_view
432 .checked_mul(self.field.len())
433 .and_then(|scaled| scaled.checked_div(tested_count))
434 .unwrap_or(in_view);
435 (estimate, stride > 1)
436 }
437
438 fn in_view(camera: &Camera, position: Vec3, window_size: UVec2) -> bool {
441 camera.pixel_of(position, window_size).is_some_and(|pixel| {
442 pixel.x >= 0.0
443 && pixel.y >= 0.0
444 && pixel.x < window_size.x as f32
445 && pixel.y < window_size.y as f32
446 })
447 }
448
449 fn controls(&mut self, ctx: &mut FrameContext<'_, Self>, camera: &Camera) {
451 let submitted = self.field.len();
452 let seeds = self.applied_seed_count;
453 let average_ms = self.frame_times.average_ms();
454 let fps = if average_ms > 0.0 {
455 1000.0 / average_ms
456 } else {
457 0.0
458 };
459 let elapsed = ctx.elapsed().as_secs_f32();
460 let (in_view, sampled) = self.count_in_view(camera, ctx.window_size());
461
462 ctx.ui(|ui| {
463 egui::Frame::new()
464 .fill(egui::Color32::from_gray(24))
465 .inner_margin(PANEL_PADDING)
466 .corner_radius(f32::from(PANEL_PADDING))
467 .show(ui, |ui| {
468 ui.add(
469 egui::Slider::new(
470 &mut self.settings.instance_count,
471 MIN_INSTANCE_COUNT..=MAX_INSTANCE_COUNT,
472 )
473 .text("instance count"),
474 );
475 ui.add(
476 egui::Slider::new(
477 &mut self.settings.seed_count,
478 MIN_SEED_COUNT..=MAX_SEED_COUNT,
479 )
480 .text("distinct seeds"),
481 );
482 ui.checkbox(&mut self.settings.sun_shadow, "sun shadow");
483 ui.checkbox(&mut self.settings.moving, "moving fraction");
484 ui.separator();
485 ui.label(format!("instances submitted {submitted}"));
486 if sampled {
487 ui.label(format!("in view, sampled {in_view}"));
488 } else {
489 ui.label(format!("instances in view {in_view}"));
490 }
491 ui.label(format!("distinct seeds {seeds}"));
492 ui.label(format!("frame time {average_ms:.2}ms, {fps:.0} fps"));
493 ui.label(format!("elapsed {elapsed:.1}s"));
494 });
495 });
496 }
497}
498
499fn build_field(instance_count: u32, seed_count: u32) -> Vec<FieldEntry> {
504 (0..instance_count)
505 .map(|index| {
506 let angle = hash_unit(index, 0) * core::f32::consts::TAU;
507 let spread = hash_unit(index, 1).sqrt();
508 let distance = FIELD_INNER_RADIUS + spread * (FIELD_RADIUS - FIELD_INNER_RADIUS);
509 FieldEntry {
510 seed: index % seed_count,
511 position: Vec3::new(angle.cos() * distance, 0.0, angle.sin() * distance),
512 phase: hash_unit(index, 2) * core::f32::consts::TAU,
513 moving: index % MOVING_STRIDE == 0,
514 }
515 })
516 .collect()
517}
518
519fn build_rock(seed: u32) -> MeshData {
522 let height = ROCK_HEIGHT * (1.0 + hash_signed(seed, ROCK_SIDES) * ROCK_HEIGHT_DISPLACEMENT);
523 let apex = Vec3::Y * height;
524 let base: Vec<Vec3> = (0..ROCK_SIDES)
525 .map(|corner| {
526 let angle = core::f32::consts::TAU * corner as f32 / ROCK_SIDES as f32;
527 let radius =
528 ROCK_BASE_RADIUS * (1.0 + hash_signed(seed, corner) * ROCK_RADIAL_DISPLACEMENT);
529 Vec3::new(angle.cos() * radius, 0.0, angle.sin() * radius)
530 })
531 .collect();
532
533 let mut vertices = Vec::with_capacity(base.len() * 6);
534 let mut indices = Vec::with_capacity(base.len() * 6);
535 for corner in 0..base.len() {
536 let next = (corner + 1) % base.len();
537 push_face(&mut vertices, &mut indices, base[corner], apex, base[next]);
538 push_face(
539 &mut vertices,
540 &mut indices,
541 base[corner],
542 base[next],
543 Vec3::ZERO,
544 );
545 }
546
547 MeshData::new(vertices, indices).with_material(Material::lit(ROCK_COLOR))
548}
549
550fn push_face(vertices: &mut Vec<Vertex>, indices: &mut Vec<u32>, a: Vec3, b: Vec3, c: Vec3) {
554 let normal = (b - a).cross(c - a).normalize();
555 let uvs = [
556 Vec2::new(0.0, 1.0),
557 Vec2::new(0.5, 0.0),
558 Vec2::new(1.0, 1.0),
559 ];
560 let base = vertices.len() as u32;
561 for (point, uv) in [a, b, c].into_iter().zip(uvs) {
562 vertices.push(Vertex::new(point, normal, uv));
563 }
564 indices.extend([base, base + 1, base + 2]);
565}
566
567fn hash(seed: u32, salt: u32) -> u32 {
569 let mut x = seed ^ salt.wrapping_mul(0x9E37_79B9);
570 x ^= x >> 16;
571 x = x.wrapping_mul(0x7FEB_352D);
572 x ^= x >> 15;
573 x = x.wrapping_mul(0x846C_A68B);
574 x ^= x >> 16;
575 x
576}
577
578fn hash_unit(seed: u32, salt: u32) -> f32 {
580 hash(seed, salt) as f32 / u32::MAX as f32
581}
582
583fn hash_signed(seed: u32, salt: u32) -> f32 {
585 hash_unit(seed, salt) * 2.0 - 1.0
586}
587
588impl Game for StressPreview {
589 type Meshes = Shape;
590 type Sounds = NoSounds;
591 type InputActions = Controls;
592 type Skyboxes = Sky;
593 type SurfaceStyles = ();
594 type PostEffects = ();
595
596 fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
597
598 fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
599 self.apply_settings();
600 self.frame_times.record(ctx.dt());
601
602 let elapsed = ctx.elapsed().as_secs_f32();
603 self.handle_camera(ctx, elapsed);
604 let camera = self.camera(elapsed);
605 ctx.set_camera(camera);
606 ctx.set_skybox(Sky::Day);
607
608 let sun = Light::directional(SUN_DIRECTION, SUN_COLOR);
609 ctx.light(if self.settings.sun_shadow {
610 sun.shadow()
611 } else {
612 sun
613 });
614
615 Self::draw_ground(ctx);
616 self.draw_field(ctx, elapsed);
617 self.controls(ctx, &camera);
618 }
619}