use core::time::Duration;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use winit::application::ApplicationHandler;
use winit::event::{DeviceEvent, DeviceId, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::window::{Window, WindowId};
use crate::gpu::Gpu;
use crate::input::{Devices, Pads, WheelRate};
use crate::math::UVec2;
use crate::renderer::Renderer;
use crate::sound::{Output, SoundOutput};
use crate::ui::Painter;
use crate::{Config, Error, Game, InitContext};
use threads::{DisplayEnd, DisplayThread, Kept, Starting, Workers};
#[cfg(not(target_arch = "wasm32"))]
use native as sys;
#[cfg(target_arch = "wasm32")]
use web as sys;
pub use sys::Instant;
pub(crate) use sys::{AFTER_LOSS, AFTER_OUT_OF_MEMORY, PLATFORM, hardware_threads, spawn_worker};
#[cfg(feature = "offscreen")]
pub(crate) use sys::install_diagnostics;
pub(crate) use sys::PointerHold;
pub(crate) const WHEEL_RATE: WheelRate = PLATFORM.wheel_rate();
pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum Platform {
Desktop,
Browser,
}
impl Platform {
pub(crate) const fn wheel_rate(self) -> WheelRate {
match self {
Self::Desktop => WheelRate::DESKTOP,
Self::Browser => WheelRate::BROWSER,
}
}
}
pub(crate) fn double_click_interval(config: &Config) -> Duration {
config
.double_click_interval()
.or_else(sys::double_click_interval)
.unwrap_or(DOUBLE_CLICK_INTERVAL)
}
#[cfg(test)]
const GARBAGE: [char; 8] = [' ', '\n', '\\', 's', '#', '\u{0}', '\u{7f}', 'é'];
pub(crate) struct Store(Option<String>);
impl Store {
pub(crate) fn bindings(title: Option<&str>) -> Self {
Self(title.map(|title| format!("{}-bindings", named(title))))
}
pub(crate) fn saves(title: Option<&str>) -> Self {
Self(title.map(|title| format!("{}-saves", named(title))))
}
pub(crate) fn read(&self) -> Option<String> {
sys::store_read(self.0.as_deref()?)
}
pub(crate) fn write(&self, text: &str) {
let Some(title) = self.0.as_deref() else {
return;
};
sys::store_write(title, text);
}
}
#[cfg(test)]
pub(crate) fn manglings(kept: &str) -> Vec<String> {
let letters: Vec<char> = kept.chars().collect();
let lines: Vec<&str> = kept.lines().collect();
let mut out = Vec::with_capacity((letters.len() + 1) * (GARBAGE.len() + 1) + lines.len());
for at in 0..=letters.len() {
out.push(letters[..at].iter().collect());
out.extend(GARBAGE.iter().map(|&dropped| {
let mut with = letters.clone();
with.insert(at, dropped);
with.iter().collect()
}));
}
out.extend((0..lines.len()).map(|at| {
let mut twice = lines.clone();
twice.insert(at, lines[at]);
twice.join("\n")
}));
out
}
fn named(title: &str) -> String {
let plain: String = title
.chars()
.map(|letter| match letter.is_ascii_alphanumeric() {
true => letter.to_ascii_lowercase(),
false => '-',
})
.collect();
match plain.trim_matches('-') {
"" => "game".to_owned(),
trimmed => trimmed.to_owned(),
}
}
pub(crate) type Init<G> = Box<dyn FnOnce(&mut InitContext<'_, G>) -> Result<G, Error> + Send>;
pub(crate) fn run<G: Game>(
config: Config,
init: impl FnOnce(&mut InitContext<'_, G>) -> Result<G, Error> + Send + 'static,
) {
sys::run_app(App::<G>::new(config, Box::new(init)));
}
fn event_loop() -> Option<EventLoop<()>> {
sys::install_diagnostics();
match EventLoop::new() {
Ok(event_loop) => {
event_loop.set_control_flow(ControlFlow::Wait);
Some(event_loop)
}
Err(error) => {
sys::report_fatal(&Error::msg(format!("no event loop: {error}")));
None
}
}
}
struct Booted {
gpu: Gpu,
files: Vec<(String, Vec<u8>)>,
renderer: Renderer,
}
type Handed = Rc<RefCell<Option<Result<Booted, Error>>>>;
#[derive(Clone, Default)]
struct BootHandoff(Handed);
impl BootHandoff {
fn publish(&self, booted: Result<Booted, Error>) {
*self.0.borrow_mut() = Some(booted);
}
fn collect(&self) -> Option<Result<Booted, Error>> {
self.0.borrow_mut().take()
}
}
pub(crate) async fn read_sources(sources: &[String]) -> Result<Vec<(String, Vec<u8>)>, Error> {
let mut files = Vec::with_capacity(sources.len());
for source in sources {
files.push((source.clone(), sys::read(source).await?));
}
Ok(files)
}
async fn boot<G: Game>(
instance: wgpu::Instance,
window: Arc<Window>,
config: Config,
) -> Result<Booted, Error> {
let gpu = Gpu::new(instance, window, sys::adapter_facts().await).await?;
let files = read_sources(config.asset_sources()).await?;
let renderer = Renderer::new(
gpu.device(),
gpu.queue(),
gpu.target_format(),
&config,
crate::surface_style::Declarations::of::<G::SurfaceStyles>(),
crate::post_effect::Declarations::of::<G::PostEffects>(),
)
.await?;
Ok(Booted {
gpu,
files,
renderer,
})
}
enum Stage {
Unstarted,
Booting,
Running(Box<Running>),
Ended,
}
struct Running {
game: sys::GameThreadHandle,
display: DisplayThread,
end: DisplayEnd,
}
struct App<G: Game> {
config: Config,
handoff: BootHandoff,
stage: Stage,
init: Option<Init<G>>,
}
impl<G: Game> App<G> {
fn new(config: Config, init: Init<G>) -> Self {
Self {
config,
handoff: BootHandoff::default(),
stage: Stage::Unstarted,
init: Some(init),
}
}
fn boot(&mut self, event_loop: &ActiveEventLoop) {
let attributes = match sys::window_attributes(&self.config) {
Ok(attributes) => attributes,
Err(error) => return end_run(event_loop, &error),
};
let window = match event_loop.create_window(attributes) {
Ok(window) => Arc::new(window),
Err(error) => {
return end_run(event_loop, &Error::msg(format!("no window: {error}")));
}
};
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_with_display_handle(
Box::new(event_loop.owned_display_handle()),
));
self.stage = Stage::Booting;
let handoff = self.handoff.clone();
let config = self.config.clone();
sys::spawn(async move {
handoff.publish(boot::<G>(instance, Arc::clone(&window), config).await);
wake_event_loop(&window);
});
}
fn start_game_if_booted(&mut self, event_loop: &ActiveEventLoop) {
let Some(booted) = self.handoff.collect() else {
return;
};
let Some(init) = self.init.take() else {
return;
};
let Booted {
gpu,
files,
renderer,
} = match booted {
Ok(booted) => booted,
Err(error) => return end_run(event_loop, &error),
};
let output = SoundOutput::new(Output::open());
let bindings = Store::bindings(Some(self.config.title()));
let saves = Store::saves(Some(self.config.title()));
let starting = Starting {
config: self.config.clone(),
files,
kept: Kept {
bindings: bindings.read(),
saves: saves.read(),
window_size: gpu.physical_size(),
mix_rate: output.rate(),
workers: Workers::here(),
},
init,
};
let painter = Painter::windowed(gpu.device(), gpu.overlay_format(), gpu.window());
let (end, game_end) = DisplayEnd::paired();
let game = match sys::start_game::<G>(game_end, starting) {
Ok(game) => game,
Err(error) => return end_run(event_loop, &error),
};
self.stage = Stage::Running(Box::new(Running {
game,
display: DisplayThread::new(
gpu,
Devices::new(Pads::open(), double_click_interval(&self.config)),
renderer,
painter,
output,
bindings,
saves,
),
end,
}));
}
}
impl<G: Game> ApplicationHandler for App<G> {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if matches!(self.stage, Stage::Unstarted) {
self.boot(event_loop);
self.start_game_if_booted(event_loop);
}
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
_window: WindowId,
event: WindowEvent,
) {
self.start_game_if_booted(event_loop);
let Stage::Running(running) = &mut self.stage else {
return;
};
let Running { game, display, end } = running.as_mut();
display.see(&event);
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(size) => {
display.gpu.resize(UVec2::new(size.width, size.height));
display.gpu.request_frame();
}
WindowEvent::RedrawRequested => {
if let Some(fault) = display.gpu.fault() {
log::error!("{fault}");
return end_run(event_loop, &Error::msg(fault.told(display.gpu.adapter())));
}
if let Some(error) = game.failed().or_else(|| display.take(end.queued())) {
return end_run(event_loop, &error);
}
let closing = end.frame().is_some_and(|frame| display.keep(frame));
display.render();
if closing {
sys::close(&display.gpu.window(), &self.config);
event_loop.exit();
return;
}
display.gpu.request_frame();
end.hand(|| display.sample());
}
_ => {}
}
}
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
if let Stage::Running(running) = core::mem::replace(&mut self.stage, Stage::Ended) {
let Running { game, end, .. } = *running;
game.ended(end);
}
}
fn device_event(
&mut self,
_event_loop: &ActiveEventLoop,
_device: DeviceId,
event: DeviceEvent,
) {
let Stage::Running(running) = &mut self.stage else {
return;
};
running.display.see_device(&event);
}
}
fn wake_event_loop(window: &Window) {
window.request_redraw();
}
fn end_run(event_loop: &ActiveEventLoop, error: &Error) {
sys::report_fatal(error);
event_loop.exit();
}
#[cfg(not(target_arch = "wasm32"))]
mod native;
pub(crate) mod threads;
#[cfg(target_arch = "wasm32")]
mod web;
#[cfg(test)]
mod tests {
use super::{Store, named};
#[test]
fn a_title_becomes_a_name_a_file_system_and_a_page_both_take() {
assert_eq!(named("Mirage Breakout"), "mirage-breakout");
assert_eq!(named("../../etc/passwd"), "etc-passwd");
assert_eq!(named(" "), "game", "and there is always a name");
}
#[test]
fn the_two_kinds_of_kept_text_never_name_one_place() {
assert_eq!(
Store::bindings(Some("Mirage Breakout")).0.as_deref(),
Some("mirage-breakout-bindings")
);
assert_eq!(
Store::saves(Some("Mirage Breakout")).0.as_deref(),
Some("mirage-breakout-saves")
);
assert_ne!(
Store::bindings(Some("Escape Saves")).0,
Store::saves(Some("Escape")).0,
"whatever a title ends in"
);
assert_eq!(Store::saves(None).0, None, "and a titleless run keeps none");
}
}