summaryrefslogtreecommitdiff
path: root/examples/doom/src/main.rs
blob: 095183dcc0902715c3519550b01ca13122d1f588 (plain)
mod runtime;

use std::num::NonZeroU32;
use std::path::{Path, PathBuf};
use std::rc::Rc;

use eyre::{ContextCompat, Result, bail, eyre};
use runtime::{Runtime, SCREEN_HEIGHT, SCREEN_WIDTH};
use softbuffer::{Context as SoftbufferContext, Surface};
use winit::application::ApplicationHandler;
use winit::dpi::{LogicalSize, Size};
use winit::event::{ElementState, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{Window, WindowAttributes, WindowLevel};

const GUEST_MODULE: &str = "examples/doom/out/puredoom.wasm";
const DEFAULT_TITLE: &str = "tinywasm doom";

fn main() -> Result<()> {
    pretty_env_logger::init();

    let wad_path = std::env::args().nth(1).map(PathBuf::from).context("usage: cargo run -p tinywasm-doom -- <wad>")?;
    if !wad_path.exists() {
        bail!("WAD not found: {}", wad_path.display());
    }

    let guest_path = Path::new(GUEST_MODULE);
    if !guest_path.exists() {
        bail!("guest module missing: {}. Run ./examples/doom/build.sh first", guest_path.display());
    }

    let event_loop = EventLoop::new()?;
    let mut app = DoomApp::new(wad_path, guest_path.to_path_buf())?;
    event_loop.run_app(&mut app)?;
    Ok(())
}

struct DoomApp {
    runtime: Runtime,
    window: Option<Rc<Window>>,
    softbuffer_context: Option<SoftbufferContext<Rc<Window>>>,
    softbuffer_surface: Option<Surface<Rc<Window>, Rc<Window>>>,
}

impl DoomApp {
    fn new(wad_path: PathBuf, guest_path: PathBuf) -> Result<Self> {
        Ok(Self {
            runtime: Runtime::new(wad_path, guest_path)?,
            window: None,
            softbuffer_context: None,
            softbuffer_surface: None,
        })
    }

    fn present(&mut self) -> Result<()> {
        let Some(surface) = self.softbuffer_surface.as_mut() else {
            return Ok(());
        };

        surface
            .resize(NonZeroU32::new(SCREEN_WIDTH as u32).unwrap(), NonZeroU32::new(SCREEN_HEIGHT as u32).unwrap())
            .map_err(|err| eyre!(err.to_string()))?;
        let mut buffer = surface.buffer_mut().map_err(|err| eyre!(err.to_string()))?;
        self.runtime.write_framebuffer(&mut buffer)?;
        buffer.present().map_err(|err| eyre!(err.to_string()))?;
        Ok(())
    }
}

impl ApplicationHandler for DoomApp {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        event_loop.set_control_flow(ControlFlow::Poll);

        let size = LogicalSize::new((SCREEN_WIDTH * 2) as f64, (SCREEN_HEIGHT * 2) as f64);

        let attributes = WindowAttributes::default()
            .with_title(DEFAULT_TITLE)
            .with_inner_size(Size::Logical(size))
            .with_resizable(false)
            .with_window_level(if cfg!(target_os = "linux") { WindowLevel::AlwaysOnTop } else { WindowLevel::Normal });

        let window = Rc::new(event_loop.create_window(attributes).expect("create window"));
        let context = SoftbufferContext::new(window.clone()).expect("create softbuffer context");
        let surface = Surface::new(&context, window.clone()).expect("create softbuffer surface");

        self.softbuffer_context = Some(context);
        self.softbuffer_surface = Some(surface);
        self.window = Some(window.clone());
        window.set_cursor_visible(false);
        window.request_redraw();
    }

    fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: winit::window::WindowId, event: WindowEvent) {
        match event {
            WindowEvent::CloseRequested => event_loop.exit(),
            WindowEvent::RedrawRequested => {
                if let Err(err) = self.present() {
                    log::error!("present failed: {err:?}");
                    event_loop.exit();
                }
            }
            WindowEvent::KeyboardInput { event, .. } => {
                if let Some(key) = doom_key(&event.physical_key) {
                    let result = if matches!(event.state, ElementState::Pressed) {
                        self.runtime.key_down(i32::from(key))
                    } else {
                        self.runtime.key_up(i32::from(key))
                    };

                    if let Err(err) = result {
                        log::error!("keyboard event failed: {err:?}");
                        event_loop.exit();
                    }
                }
            }
            _ => {}
        }
    }

    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
        if let Err(err) = self.runtime.tick() {
            log::error!("doom tick failed: {err:?}");
            event_loop.exit();
            return;
        }

        if let Some(window) = &self.window {
            window.request_redraw();
        }

        if self.runtime.host_state.borrow().exit_code.is_some() {
            event_loop.exit();
        }
    }
}

fn doom_key(key: &PhysicalKey) -> Option<u8> {
    let code = match key {
        PhysicalKey::Code(KeyCode::Enter) => 13,
        PhysicalKey::Code(KeyCode::Escape) => 27,
        PhysicalKey::Code(KeyCode::ArrowLeft) => 0xac,
        PhysicalKey::Code(KeyCode::ArrowRight) => 0xae,
        PhysicalKey::Code(KeyCode::ArrowUp) => 0xad,
        PhysicalKey::Code(KeyCode::ArrowDown) => 0xaf,
        PhysicalKey::Code(KeyCode::ControlLeft | KeyCode::ControlRight) => 0x80 + 0x1d,
        PhysicalKey::Code(KeyCode::ShiftLeft | KeyCode::ShiftRight) => 0xb6,
        PhysicalKey::Code(KeyCode::AltLeft | KeyCode::AltRight) => 0xb8,
        PhysicalKey::Code(KeyCode::Space) => b' ',
        PhysicalKey::Code(KeyCode::F2) => 0x80 + 0x3c,
        PhysicalKey::Code(KeyCode::F3) => 0x80 + 0x3d,
        PhysicalKey::Code(KeyCode::F4) => 0x80 + 0x3e,
        PhysicalKey::Code(KeyCode::F5) => 0x80 + 0x3f,
        PhysicalKey::Code(KeyCode::F6) => 0x80 + 0x40,
        PhysicalKey::Code(KeyCode::F7) => 0x80 + 0x41,
        PhysicalKey::Code(KeyCode::F8) => 0x80 + 0x42,
        PhysicalKey::Code(KeyCode::F9) => 0x80 + 0x43,
        PhysicalKey::Code(KeyCode::F10) => 0x80 + 0x44,
        PhysicalKey::Code(KeyCode::F11) => 0x80 + 0x57,
        PhysicalKey::Code(KeyCode::Equal) => b'=',
        PhysicalKey::Code(KeyCode::Minus) => b'-',
        PhysicalKey::Code(KeyCode::KeyA) => b'a',
        PhysicalKey::Code(KeyCode::KeyB) => b'b',
        PhysicalKey::Code(KeyCode::KeyC) => b'c',
        PhysicalKey::Code(KeyCode::KeyD) => b'd',
        PhysicalKey::Code(KeyCode::KeyE) => b'e',
        PhysicalKey::Code(KeyCode::KeyF) => b'f',
        PhysicalKey::Code(KeyCode::KeyG) => b'g',
        PhysicalKey::Code(KeyCode::KeyH) => b'h',
        PhysicalKey::Code(KeyCode::KeyI) => b'i',
        PhysicalKey::Code(KeyCode::KeyJ) => b'j',
        PhysicalKey::Code(KeyCode::KeyK) => b'k',
        PhysicalKey::Code(KeyCode::KeyL) => b'l',
        PhysicalKey::Code(KeyCode::KeyM) => b'm',
        PhysicalKey::Code(KeyCode::KeyN) => b'n',
        PhysicalKey::Code(KeyCode::KeyO) => b'o',
        PhysicalKey::Code(KeyCode::KeyP) => b'p',
        PhysicalKey::Code(KeyCode::KeyQ) => b'q',
        PhysicalKey::Code(KeyCode::KeyR) => b'r',
        PhysicalKey::Code(KeyCode::KeyS) => b's',
        PhysicalKey::Code(KeyCode::KeyT) => b't',
        PhysicalKey::Code(KeyCode::KeyU) => b'u',
        PhysicalKey::Code(KeyCode::KeyV) => b'v',
        PhysicalKey::Code(KeyCode::KeyW) => b'w',
        PhysicalKey::Code(KeyCode::KeyX) => b'x',
        PhysicalKey::Code(KeyCode::KeyY) => b'y',
        PhysicalKey::Code(KeyCode::KeyZ) => b'z',
        PhysicalKey::Code(KeyCode::Digit0) => b'0',
        PhysicalKey::Code(KeyCode::Digit1) => b'1',
        PhysicalKey::Code(KeyCode::Digit2) => b'2',
        PhysicalKey::Code(KeyCode::Digit3) => b'3',
        PhysicalKey::Code(KeyCode::Digit4) => b'4',
        PhysicalKey::Code(KeyCode::Digit5) => b'5',
        PhysicalKey::Code(KeyCode::Digit6) => b'6',
        PhysicalKey::Code(KeyCode::Digit7) => b'7',
        PhysicalKey::Code(KeyCode::Digit8) => b'8',
        PhysicalKey::Code(KeyCode::Digit9) => b'9',
        _ => return None,
    };

    Some(code)
}