use core::num::NonZeroU32;
use core::time::Duration;
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError, TryLockError};
use rayon::ThreadPoolBuilder;
use winit::event::{DeviceEvent, ElementState, WindowEvent};
use crate::assets::Assets;
use crate::context::{Engine, Recording, Run, Simulating};
use crate::gpu::Gpu;
use crate::input::{Controls, Cursor, Devices, Queries};
use crate::math::UVec2;
use crate::platform::{Init, Instant, PointerHold, Store, hardware_threads, spawn_worker};
use crate::renderer::Renderer;
use crate::renderer::draw_list::DrawList;
use crate::renderer::mesh_cache::{HandedMeshes, MeshCatalog};
use crate::renderer::skybox::{SkyCatalog, SkyId};
use crate::save::Saved;
use crate::skybox::Resident;
use crate::sound::{MixRate, Played, SoundOutput, Sounding};
use crate::time::{Clock, FrameTime};
use crate::ui::{Building, Changes, Layer, Layered, Painted, Painter};
use crate::{Camera, Config, Error, Game, InitContext};
pub(crate) struct Stated {
pub(crate) window_size: UVec2,
pub(crate) sound_unlocked: bool,
}
impl Stated {
fn starting(window_size: UVec2) -> Self {
Self {
window_size,
sound_unlocked: false,
}
}
}
pub(crate) struct Sample {
pub(crate) stated: Stated,
pub(crate) controls: Controls,
pub(crate) ui_input: UiInput,
}
#[derive(Default)]
pub(crate) struct UiInput {
#[cfg(feature = "ui")]
raw: egui::RawInput,
}
impl UiInput {
#[cfg(feature = "ui")]
pub(crate) fn of(raw: egui::RawInput) -> Self {
Self { raw }
}
#[cfg(feature = "ui")]
pub(crate) fn raw(self) -> egui::RawInput {
self.raw
}
}
pub(crate) struct HandedFrame {
pub(crate) draws: DrawList,
pub(crate) ui: Painted,
pub(crate) cursor: Cursor,
pub(crate) closing: bool,
}
pub(crate) struct Commands {
pub(crate) meshes: HandedMeshes,
pub(crate) skies: Vec<(SkyId, Resident)>,
pub(crate) ui: Changes,
pub(crate) sound: Played,
pub(crate) bindings: Option<String>,
pub(crate) saves: Option<String>,
}
pub(crate) enum Queued {
Frame(Box<Commands>),
Failed(Error),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Standing {
Refused,
#[cfg(any(feature = "offscreen", test))]
Taken,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct Workers {
count: usize,
standing: Standing,
}
impl Standing {
fn reported(self) -> Result<(), Error> {
match self {
Self::Refused => Err(Error::msg(
"a pool for parallel work stood before the run started: the engine builds the pool, so a game builds none",
)),
#[cfg(any(feature = "offscreen", test))]
Self::Taken => {
log::debug!("mirage-engine runs parallel work on the pool an earlier start built");
Ok(())
}
}
}
}
impl Workers {
const MOST: usize = 16;
const OWN_THREADS: usize = 2;
pub(crate) fn here() -> Self {
Self {
count: Self::beside_the_run(hardware_threads()),
standing: Standing::Refused,
}
}
#[cfg(any(feature = "offscreen", test))]
pub(crate) fn here_again() -> Self {
Self {
count: Self::beside_the_run(hardware_threads()),
standing: Standing::Taken,
}
}
pub(crate) fn build_pool(self) -> Result<(), Error> {
if rayon::current_thread_index().is_some() {
return Err(Error::msg(
"mirage-engine starts no game thread on a worker of a pool: parallel work there reaches that pool and never the engine's",
));
}
let built = ThreadPoolBuilder::new()
.num_threads(self.count())
.spawn_handler(spawn_worker)
.build_global();
match built {
Ok(()) => {
log::info!(
"mirage-engine runs parallel work on {} workers",
self.count()
);
Ok(())
}
Err(standing) if std::error::Error::source(&standing).is_none() => {
self.standing.reported()
}
Err(error) => Err(Error::msg(format!(
"mirage-engine started no worker for parallel work: {error}"
))),
}
}
pub(crate) fn count(self) -> usize {
self.count
}
fn beside_the_run(hardware_threads: usize) -> usize {
hardware_threads
.saturating_sub(Self::OWN_THREADS)
.clamp(1, Self::MOST)
}
}
pub(crate) struct Kept {
pub(crate) bindings: Option<String>,
pub(crate) saves: Option<String>,
pub(crate) window_size: UVec2,
pub(crate) mix_rate: Option<MixRate>,
pub(crate) workers: Workers,
}
pub(crate) struct Starting<G: Game> {
pub(crate) config: Config,
pub(crate) files: Vec<(String, Vec<u8>)>,
pub(crate) kept: Kept,
pub(crate) init: Init<G>,
}
struct Crossing {
pending: Mutex<Pending>,
handed: Condvar,
back: Mutex<Back>,
}
struct Pending {
sample: Option<Sample>,
running: bool,
}
#[derive(Default)]
struct Back {
frame: Option<HandedFrame>,
queued: Vec<Queued>,
}
pub(crate) struct DisplayEnd(Arc<Crossing>);
impl DisplayEnd {
pub(crate) fn paired() -> (Self, GameEnd) {
let crossing = Arc::new(Crossing {
pending: Mutex::new(Pending {
sample: None,
running: true,
}),
handed: Condvar::new(),
back: Mutex::default(),
});
(Self(Arc::clone(&crossing)), GameEnd(crossing))
}
pub(crate) fn hand(&self, close: impl FnOnce() -> Sample) {
let Some(mut pending) = tried(&self.0.pending) else {
return;
};
if pending.sample.is_some() {
return;
}
pending.sample = Some(close());
self.0.handed.notify_one();
}
pub(crate) fn frame(&self) -> Option<HandedFrame> {
tried(&self.0.back)?.frame.take()
}
pub(crate) fn queued(&self) -> Vec<Queued> {
tried(&self.0.back)
.map(|mut back| core::mem::take(&mut back.queued))
.unwrap_or_default()
}
}
impl Drop for DisplayEnd {
fn drop(&mut self) {
let mut pending = loop {
if let Some(pending) = tried(&self.0.pending) {
break pending;
}
core::hint::spin_loop();
};
pending.running = false;
self.0.handed.notify_one();
}
}
fn tried<T>(lock: &Mutex<T>) -> Option<MutexGuard<'_, T>> {
match lock.try_lock() {
Ok(state) => Some(state),
Err(TryLockError::Poisoned(poisoned)) => Some(poisoned.into_inner()),
Err(TryLockError::WouldBlock) => None,
}
}
pub(crate) struct GameEnd(Arc<Crossing>);
impl GameEnd {
pub(crate) fn sample(&self) -> Option<Sample> {
let mut pending = self.pending();
while pending.running && pending.sample.is_none() {
pending = self
.0
.handed
.wait(pending)
.unwrap_or_else(PoisonError::into_inner);
}
pending.sample.take()
}
pub(crate) fn hand(&self, frame: HandedFrame, commands: Commands) {
let mut back = self.back();
back.queued.push(Queued::Frame(Box::new(commands)));
back.frame = Some(frame);
}
pub(crate) fn fail(&self, error: Error) {
self.back().queued.push(Queued::Failed(error));
}
fn back(&self) -> MutexGuard<'_, Back> {
self.0.back.lock().unwrap_or_else(PoisonError::into_inner)
}
fn pending(&self) -> MutexGuard<'_, Pending> {
self.0
.pending
.lock()
.unwrap_or_else(PoisonError::into_inner)
}
}
pub(crate) struct Paced<G: Game> {
game: GameThread<G>,
clock: Clock,
}
impl<G: Game> Paced<G> {
pub(crate) fn started(end: &GameEnd, starting: Starting<G>) -> Option<Self> {
let Starting {
config,
files,
kept,
init,
} = starting;
match GameThread::start(config, files, kept, init) {
Ok(game) => Some(Self {
game,
clock: Clock::new(Instant::now()),
}),
Err(error) => {
end.fail(error);
None
}
}
}
pub(crate) fn run(&mut self, end: &GameEnd) {
while let Some(sample) = end.sample() {
let dt = self.game.start_ticks();
let owed = self.clock.frame_at(Instant::now(), dt);
let Sample {
stated,
controls,
ui_input,
} = sample;
self.game.take(stated, Some(controls));
if let Some(ticks) = NonZeroU32::new(owed) {
self.game.ticks(ticks, dt, self.clock.elapsed());
}
let time = self.clock.frame_time(self.game.tick_interval());
let (handed, commands) = self.game.frame(time, ui_input);
let closing = handed.closing;
end.hand(handed, commands);
if closing {
return;
}
}
}
}
pub(crate) struct GameThread<G: Game> {
game: G,
config: Config,
meshes: MeshCatalog<G::Meshes>,
skies: SkyCatalog<G::Skyboxes>,
sounding: Sounding<G::Sounds>,
queries: Queries,
saves: Saved,
ui: Layer,
draws: DrawList,
run: Run,
last_camera: Camera,
stated: Stated,
}
impl<G: Game> GameThread<G> {
pub(crate) fn start(
config: Config,
files: Vec<(String, Vec<u8>)>,
kept: Kept,
init: impl FnOnce(&mut InitContext<'_, G>) -> Result<G, Error>,
) -> Result<Self, Error> {
kept.workers.build_pool()?;
let assets = Arc::new(Assets::load(files)?);
let mut meshes = MeshCatalog::new(Arc::clone(&assets), config.mesh_memory());
let mut skies = SkyCatalog::new(Arc::clone(&assets));
let mut sounding = Sounding::new(Arc::clone(&assets), kept.mix_rate);
let queries = Queries::new::<G::InputActions>(kept.bindings.as_deref());
let saves = Saved::new(kept.saves.as_deref());
let ui = Layer::new();
let mut unresolved = sounding.build_catalog();
unresolved.record(meshes.build_catalog());
unresolved.record(skies.build_catalog());
if let Some(error) = unresolved.error() {
return Err(error);
}
let game = init(&mut InitContext::new(
Engine::new(kept.window_size, &config, Camera::default(), false),
&mut meshes,
&mut sounding,
&saves,
&assets,
&ui,
))?;
Ok(Self {
game,
run: Run::new(config.tick_interval()),
stated: Stated::starting(kept.window_size),
config,
meshes,
skies,
sounding,
queries,
saves,
ui,
draws: DrawList::new(),
last_camera: Camera::default(),
})
}
pub(crate) fn take(&mut self, stated: Stated, controls: Option<Controls>) {
self.stated = stated;
if let Some(controls) = controls {
self.queries.take(controls);
}
}
pub(crate) fn start_ticks(&mut self) -> Duration {
self.run.start_ticks()
}
pub(crate) fn tick_interval(&self) -> Duration {
self.run.tick_interval()
}
pub(crate) fn ticks(&mut self, count: NonZeroU32, dt: Duration, elapsed: Duration) {
let Self {
game,
config,
meshes,
sounding,
queries,
saves,
ui,
run,
last_camera,
stated,
..
} = self;
queries.ticks(count, |ticking| {
let simulating = Simulating {
audio: sounding,
saves,
run,
};
game.tick(&mut simulating.tick_context(
Engine::new(
stated.window_size,
config,
*last_camera,
stated.sound_unlocked,
),
meshes,
ticking,
dt,
elapsed,
ui.claims(),
));
});
}
pub(crate) fn frame(&mut self, time: FrameTime, ui_input: UiInput) -> (HandedFrame, Commands) {
let Self {
game,
config,
meshes,
skies,
sounding,
queries,
saves,
ui,
draws,
run,
last_camera,
stated,
} = self;
let window_size = stated.window_size;
let sound_unlocked = stated.sound_unlocked;
let build = |layer: Building<'_>| {
let recording = Recording {
draws,
meshes,
skies,
input: queries,
simulating: Simulating {
audio: sounding,
saves,
run,
},
};
let engine = Engine::new(window_size, config, *last_camera, sound_unlocked);
game.frame(&mut recording.frame_context(engine, time, layer));
};
let layered = ui.frame(ui_input, build);
let Layered {
painted,
changes,
cursor,
} = layered;
let draws = draws.take();
*last_camera = draws.camera();
let handed = HandedFrame {
draws,
ui: painted,
cursor,
closing: run.closing(),
};
let commands = Commands {
meshes: meshes.end_frame(),
skies: skies.end_frame(),
ui: changes,
sound: sounding.flush(last_camera.view()),
bindings: queries.flush(),
saves: saves.flush(),
};
(handed, commands)
}
#[cfg(feature = "offscreen")]
pub(crate) fn closing(&self) -> bool {
self.run.closing()
}
#[cfg(feature = "offscreen")]
pub(crate) fn game(&self) -> &G {
&self.game
}
#[cfg(feature = "offscreen")]
pub(crate) fn game_mut(&mut self) -> &mut G {
&mut self.game
}
}
pub(crate) struct DisplayThread {
pub(crate) gpu: Gpu,
devices: Devices,
renderer: Renderer,
painter: Painter,
output: SoundOutput,
hold: PointerHold,
bindings: Store,
saves: Store,
drawn: DrawList,
started: Instant,
}
impl DisplayThread {
pub(crate) fn new(
gpu: Gpu,
devices: Devices,
renderer: Renderer,
painter: Painter,
output: SoundOutput,
bindings: Store,
saves: Store,
) -> Self {
Self {
gpu,
devices,
renderer,
painter,
output,
hold: PointerHold::released(),
bindings,
saves,
drawn: DrawList::new(),
started: Instant::now(),
}
}
pub(crate) fn see(&mut self, event: &WindowEvent) {
self.painter.fold(event);
self.devices.see(event);
if is_gesture(event) {
self.output.unlock();
self.hold.see_gesture(&self.gpu.window());
}
if matches!(event, WindowEvent::Focused(_)) {
self.hold.see_focus_change();
}
}
pub(crate) fn see_device(&mut self, event: &DeviceEvent) {
self.devices.see_device(event);
}
pub(crate) fn sample(&mut self) -> Sample {
Sample {
stated: Stated {
window_size: self.gpu.physical_size(),
sound_unlocked: self.output.unlocked(),
},
controls: self.devices.sample(self.started.elapsed()),
ui_input: self.painter.take_input(),
}
}
pub(crate) fn take(&mut self, queued: Vec<Queued>) -> Option<Error> {
queued.into_iter().find_map(|one| match one {
Queued::Frame(commands) => {
self.commanded(*commands);
None
}
Queued::Failed(error) => Some(error),
})
}
pub(crate) fn keep(&mut self, frame: HandedFrame) -> bool {
let HandedFrame {
draws,
ui,
cursor,
closing,
} = frame;
let held = self.hold.set(&self.gpu.window(), cursor.holds_pointer());
self.devices.hold_pointer(held);
self.painter.keep(ui, cursor);
self.drawn = draws;
closing
}
pub(crate) fn render(&mut self) {
self.renderer
.render(&mut self.gpu, &self.drawn, &mut self.painter);
}
fn commanded(&mut self, commands: Commands) {
let Commands {
meshes,
skies,
ui,
sound,
bindings,
saves,
} = commands;
self.renderer.take(
self.gpu.device(),
self.gpu.queue(),
meshes,
skies,
ui,
&mut self.painter,
);
self.output.play(sound);
if let Some(text) = bindings {
self.bindings.write(&text);
}
if let Some(text) = saves {
self.saves.write(&text);
}
}
}
fn is_gesture(event: &WindowEvent) -> bool {
matches!(
event,
WindowEvent::KeyboardInput { .. }
| WindowEvent::Touch(_)
| WindowEvent::MouseInput {
state: ElementState::Pressed,
..
}
)
}
#[cfg(test)]
mod tests {
use rayon::prelude::*;
use super::*;
use crate::{
FrameContext, NoInputActions, NoMeshes, NoPostEffects, NoSkyboxes, NoSounds,
NoSurfaceStyles, TickContext,
};
const SIZE: UVec2 = UVec2::new(320, 200);
const CHUNK: usize = 1024;
type Ran = Arc<Mutex<Vec<std::thread::ThreadId>>>;
struct Probe {
frames: u32,
closes_at: u32,
ran: Ran,
}
impl Game for Probe {
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>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
self.frames += 1;
self.ran
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(std::thread::current().id());
if self.frames == self.closes_at {
ctx.close();
}
}
}
fn starting(closes_at: u32) -> (Starting<Probe>, Ran) {
let ran = Ran::default();
let probe = Arc::clone(&ran);
let starting = Starting {
config: Config::new("threads"),
files: Vec::new(),
kept: Kept {
bindings: None,
saves: None,
window_size: SIZE,
mix_rate: None,
workers: Workers::here_again(),
},
init: Box::new(move |_ctx| {
Ok(Probe {
frames: 0,
closes_at,
ran: probe,
})
}),
};
(starting, ran)
}
fn sample() -> Sample {
Sample {
stated: Stated::starting(SIZE),
controls: Controls::default(),
ui_input: UiInput::default(),
}
}
fn game_thread(closes_at: u32) -> GameThread<Probe> {
let (
Starting {
config,
files,
kept,
init,
},
_,
) = starting(closes_at);
GameThread::start(config, files, kept, init).expect("a game that names no asset starts")
}
#[test]
fn a_sample_the_game_thread_has_not_taken_is_never_closed_over() {
let (end, game) = DisplayEnd::paired();
let closed = core::cell::Cell::new(0);
let close = || {
closed.set(closed.get() + 1);
sample()
};
end.hand(close);
end.hand(close);
assert_eq!(
closed.get(),
1,
"the events since fold into the one waiting"
);
game.sample().expect("the sample the display thread handed");
end.hand(close);
assert_eq!(closed.get(), 2, "and the next is closed once it is taken");
}
#[test]
fn the_display_thread_returns_while_the_game_thread_holds_the_crossing() {
let (end, game) = DisplayEnd::paired();
let (took, taken) = std::sync::mpsc::channel();
let (release, released) = std::sync::mpsc::channel();
let (report, reported) = std::sync::mpsc::channel();
let holder = std::thread::spawn(move || {
let pending = game.pending();
let back = game.back();
took.send(()).expect("the test reads that both are held");
released.recv().expect("the test lets both go");
drop((pending, back));
game
});
taken.recv().expect("the game thread holds the crossing");
let display = std::thread::spawn(move || {
let mut closed = 0;
end.hand(|| {
closed += 1;
sample()
});
report
.send((closed, end.frame().is_some(), end.queued().len()))
.expect("the test reads what the display thread took");
end
});
let took = reported.recv_timeout(Duration::from_secs(5));
let (closed, framed, queued) =
took.expect("the display thread returns while the game thread holds the crossing");
assert_eq!(closed, 0, "no sample is closed over a slot it cannot hand");
assert!(!framed, "no frame is taken out of a hand being made");
assert_eq!(queued, 0, "and nothing is taken out of the queue");
release.send(()).expect("the game thread lets go");
let game = holder.join().expect("the thread holding the crossing");
let end = display.join().expect("the thread reading it");
end.hand(sample);
assert!(
game.sample().is_some(),
"and the hand after it lands, so no sample is lost"
);
}
#[test]
fn a_frame_the_slot_replaces_loses_nothing_the_display_thread_acts_on() {
let (end, game) = DisplayEnd::paired();
let mut thread = game_thread(2);
for _ in 0..2 {
let Sample {
stated,
controls,
ui_input,
} = sample();
thread.take(stated, Some(controls));
let (frame, commands) = thread.frame(FrameTime::default(), ui_input);
game.hand(frame, commands);
}
assert!(
end.frame().expect("a frame to draw").closing,
"the newest frame is the one the display thread draws"
);
assert_eq!(
end.queued().len(),
2,
"and what both frames command is queued, neither of them dropped"
);
}
#[test]
fn the_frame_of_the_sample_handed_runs_on_the_game_thread() {
let (end, game) = DisplayEnd::paired();
let (starting, ran) = starting(1);
let thread = std::thread::spawn(move || {
let Some(mut paced) = Paced::started(&game, starting) else {
return;
};
paced.run(&game);
});
end.hand(sample);
thread.join().expect("the game thread ends with the run");
assert!(
end.frame().expect("the frame it ran").closing,
"the frame that closed the run is drawn"
);
assert_eq!(end.queued().len(), 1, "beside what it commands");
match ran
.lock()
.unwrap_or_else(PoisonError::into_inner)
.as_slice()
{
[frame] => assert_ne!(
*frame,
std::thread::current().id(),
"and no frame ran where the display thread's own events run"
),
frames => panic!("one sample runs one frame, not {}", frames.len()),
}
}
#[test]
fn the_error_the_init_closure_failed_with_reaches_the_display_thread() {
let (end, game) = DisplayEnd::paired();
let failing = Starting::<Probe> {
init: Box::new(|_ctx| Err(Error::msg("no game today"))),
..starting(1).0
};
let thread = std::thread::spawn(move || {
assert!(
Paced::started(&game, failing).is_none(),
"no game thread runs where the init closure failed"
);
});
thread.join().expect("the game thread ends with the error");
match end.queued().as_slice() {
[Queued::Failed(error)] => assert_eq!(error.to_string(), "no game today"),
_ => panic!("the error is the whole of what the display thread is handed"),
}
assert!(end.frame().is_none(), "and no frame was ever handed");
}
#[test]
fn the_worker_count_leaves_the_engine_its_two_threads_and_stops_at_the_cap() {
assert_eq!(Workers::beside_the_run(8), 6);
assert_eq!(
Workers::beside_the_run(64),
Workers::MOST,
"however many a machine states"
);
assert_eq!(
Workers::beside_the_run(2),
1,
"and a machine with nothing to spare still runs one"
);
assert_eq!(Workers::beside_the_run(0), 1);
}
#[test]
fn a_game_thread_that_is_already_a_worker_of_a_pool_is_refused_at_startup() {
let pool = ThreadPoolBuilder::new()
.num_threads(2)
.build()
.expect("a pool of this test's own");
let refused = pool
.install(|| Workers::here_again().build_pool())
.expect_err("a game thread on a worker of a pool");
assert!(
refused.to_string().contains("worker of a pool"),
"{refused}"
);
}
#[test]
fn a_pool_the_game_built_before_the_run_stops_a_start_behind_a_window() {
Workers::here_again()
.build_pool()
.expect("the pool a game builds for itself, which a start of many builds here");
let refused = Workers::here()
.build_pool()
.expect_err("a pool that stood before the run started");
assert!(
refused.to_string().contains("stood before the run"),
"{refused}"
);
}
#[test]
fn a_chunked_fold_reads_the_same_bits_at_any_worker_count() {
let values: Vec<f32> = (0..100_000).map(|at| (at as f32).sin() * 1e3).collect();
let folded = |workers| {
let pool = ThreadPoolBuilder::new()
.num_threads(workers)
.build()
.expect("a pool of this test's own");
pool.install(|| {
values
.par_chunks(CHUNK)
.map(|chunk| chunk.iter().sum::<f32>())
.collect::<Vec<f32>>()
.iter()
.sum::<f32>()
})
};
assert_eq!(
folded(1).to_bits(),
folded(Workers::MOST).to_bits(),
"the chunks hold the same values at either count, and the fold adds them up in order"
);
}
#[test]
fn everything_that_crosses_between_the_two_threads_is_plain_data() {
fn crosses<T: Send>() {}
crosses::<Sample>();
crosses::<HandedFrame>();
crosses::<Queued>();
crosses::<Starting<Probe>>();
crosses::<GameEnd>();
}
}