mirage-engine 0.2.0

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! Where a game's parallel iterators run: on the workers the engine builds,
//! in every session one process starts.

use std::sync::{Arc, Mutex, PoisonError};
use std::thread::ThreadId;

use rayon::prelude::*;

use super::*;
use crate::platform::threads::Workers;

/// The values one chunk of the tick below adds up.
const CHUNK: usize = 1024;

/// What one tick's parallel iterator recorded: the threads its chunks ran
/// on, and how many workers the pool it reached holds.
#[derive(Default)]
struct RanOn {
    threads: Vec<ThreadId>,
    workers: usize,
}

/// What a test reads back of the ticks its session ran.
type Recorded = Arc<Mutex<RanOn>>;

/// A game whose tick adds a slice up over fixed chunks, recording what ran
/// each chunk.
struct Adding {
    values: Vec<f32>,
    total: f32,
    recorded: Recorded,
}

impl Adding {
    fn new(recorded: Recorded) -> Self {
        Self {
            values: (0..100_000).map(|at| (at as f32).sin()).collect(),
            total: 0.0,
            recorded,
        }
    }
}

impl Game for Adding {
    type Meshes = NoMeshes;
    type Sounds = NoSounds;
    type InputActions = NoInputActions;
    type Skyboxes = NoSkyboxes;
    type SurfaceStyles = NoSurfaceStyles;
    type PostEffects = NoPostEffects;

    fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {
        let recorded = Arc::clone(&self.recorded);
        self.total = self
            .values
            .par_chunks(CHUNK)
            .map(|chunk| {
                recorded
                    .lock()
                    .unwrap_or_else(PoisonError::into_inner)
                    .threads
                    .push(std::thread::current().id());
                chunk.iter().sum::<f32>()
            })
            .collect::<Vec<f32>>()
            .iter()
            .sum();
        recorded
            .lock()
            .unwrap_or_else(PoisonError::into_inner)
            .workers = rayon::current_num_threads();
    }

    fn frame(&mut self, _ctx: &mut FrameContext<'_, Self>) {}
}

#[test]
fn a_parallel_iterator_runs_on_the_engines_workers_in_every_session_of_one_process() {
    for title in ["headless workers", "headless workers again"] {
        let recorded = Recorded::default();
        let started = Session::new(raw(title), UVec2::splat(SIDE), |_ctx| {
            Ok(Adding::new(Arc::clone(&recorded)))
        });
        let Ok(mut session) = started else {
            eprintln!("skipped: this machine has no usable graphics adapter");
            return;
        };

        session.tick();

        let ran = recorded.lock().unwrap_or_else(PoisonError::into_inner);
        assert_eq!(
            ran.workers,
            Workers::here_again().count(),
            "{title} reached the pool the engine built, not one of rayon's own"
        );
        assert!(!ran.threads.is_empty(), "{title} ran its chunks somewhere");
        assert!(
            ran.threads
                .iter()
                .all(|ran| *ran != std::thread::current().id()),
            "{title} ran no chunk on the thread it runs the game on"
        );
    }
}