summaryrefslogtreecommitdiff
path: root/examples/doom/src/runtime.rs
blob: 980dc21ceecbd929cf65617c63109df2caec96cc (plain)
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::fs::{File, OpenOptions, create_dir_all};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::time::Instant;

use eyre::Result;
use tinywasm::{FuncContext, HostFunction, Imports, ModuleInstance, Store};

const IMPORT_MODULE: &str = "env";
pub const SCREEN_WIDTH: usize = 320;
pub const SCREEN_HEIGHT: usize = 200;

pub struct Runtime {
    store: Store,
    update: tinywasm::FunctionTyped<(), ()>,
    framebuffer: tinywasm::FunctionTyped<(), i32>,
    key_down: tinywasm::FunctionTyped<i32, ()>,
    key_up: tinywasm::FunctionTyped<i32, ()>,
    memory: tinywasm::Memory,
    framebuffer_bytes: Vec<u8>,
    pub host_state: Rc<RefCell<HostState>>,
}

impl Runtime {
    pub fn new(wad_path: PathBuf, guest_path: PathBuf) -> Result<Self> {
        let module = tinywasm::parse_file(&guest_path)?;
        let mut store = Store::default();
        let host_state = Rc::new(RefCell::new(HostState::new(wad_path)));
        let imports = build_imports(&mut store, host_state.clone());
        let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?;

        let wad_path_buf = instance.func::<(), i32>(&store, "tinywasm_doom_wad_path_buf")?;
        let init = instance.func::<(), ()>(&store, "tinywasm_doom_init")?;
        let update = instance.func::<(), ()>(&store, "tinywasm_doom_update")?;
        let framebuffer = instance.func::<(), i32>(&store, "tinywasm_doom_framebuffer")?;
        let key_down = instance.func::<i32, ()>(&store, "tinywasm_doom_key_down")?;
        let key_up = instance.func::<i32, ()>(&store, "tinywasm_doom_key_up")?;
        let memory = instance.memory("memory")?;

        let buf_ptr = wad_path_buf.call(&mut store, ())? as usize;
        let wad_path_string = host_state.borrow().wad_path.to_string_lossy().into_owned();
        memory.write_cstring_bytes(&mut store, buf_ptr, &wad_path_string)?;
        init.call(&mut store, ())?;

        let width = SCREEN_WIDTH;
        let height = SCREEN_HEIGHT;

        Ok(Self {
            store,
            update,
            framebuffer,
            key_down,
            key_up,
            memory,
            framebuffer_bytes: vec![0; width * height * 4],
            host_state,
        })
    }

    pub fn tick(&mut self) -> Result<()> {
        self.update.call(&mut self.store, ())?;
        Ok(())
    }

    pub fn write_framebuffer(&mut self, dst: &mut [u32]) -> Result<()> {
        let ptr = self.framebuffer.call(&mut self.store, ())? as usize;
        self.memory.read_exact(&self.store, ptr, &mut self.framebuffer_bytes)?;
        for (index, pixel) in dst.iter_mut().enumerate() {
            let byte_index = index * 4;
            let chunk = &self.framebuffer_bytes[byte_index..byte_index + 4];
            *pixel = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32;
        }
        Ok(())
    }

    pub fn key_down(&mut self, key: i32) -> Result<()> {
        self.key_down.call(&mut self.store, key)?;
        Ok(())
    }

    pub fn key_up(&mut self, key: i32) -> Result<()> {
        self.key_up.call(&mut self.store, key)?;
        Ok(())
    }
}

pub struct HostState {
    pub wad_path: PathBuf,
    runtime_dir: PathBuf,
    start: Instant,
    files: BTreeMap<i32, File>,
    next_file: i32,
    pub exit_code: Option<i32>,
}

impl HostState {
    fn new(wad_path: PathBuf) -> Self {
        let runtime_dir = PathBuf::from("examples/doom/out/runtime");
        let _ = create_dir_all(&runtime_dir);
        Self { wad_path, runtime_dir, start: Instant::now(), files: BTreeMap::new(), next_file: 3, exit_code: None }
    }

    fn resolve_path(&self, path: &str) -> PathBuf {
        let candidate = Path::new(path);
        if candidate.is_absolute() { candidate.to_path_buf() } else { self.runtime_dir.join(candidate) }
    }

    fn should_redirect_to_wad(&self, requested: &str) -> bool {
        let Some(file_name) = Path::new(requested).file_name().and_then(|name| name.to_str()) else {
            return false;
        };

        let requested = file_name.to_ascii_lowercase();
        let provided = self.wad_path.file_name().and_then(|name| name.to_str()).map(|name| name.to_ascii_lowercase());

        match provided.as_deref() {
            Some("doom1.wad") => requested == "doom1.wad",
            Some("doom.wad") => requested == "doom.wad",
            Some("doomu.wad") => requested == "doomu.wad",
            Some("doom2.wad") => requested == "doom2.wad",
            Some("doom2f.wad") => requested == "doom2f.wad",
            Some("plutonia.wad") => requested == "plutonia.wad",
            Some("tnt.wad") => requested == "tnt.wad",
            Some(provided_name) => requested == provided_name,
            None => false,
        }
    }

    fn open_mode_options(mode: &str) -> OpenOptions {
        let mut options = OpenOptions::new();
        let plus = mode.as_bytes().contains(&b'+');

        match mode.as_bytes().first().copied() {
            Some(b'r') => {
                options.read(true);
                if plus {
                    options.write(true);
                }
            }
            Some(b'w') => {
                options.write(true).create(true).truncate(true);
                if plus {
                    options.read(true);
                }
            }
            Some(b'a') => {
                options.write(true).create(true).append(true);
                if plus {
                    options.read(true);
                }
            }
            _ => {
                options.read(true);
            }
        }

        options
    }
}

fn build_imports(store: &mut Store, state: Rc<RefCell<HostState>>) -> Imports {
    let mut imports = Imports::new();

    {
        let state = state.clone();
        imports.define(
            IMPORT_MODULE,
            "host_open",
            HostFunction::from(store, move |ctx: FuncContext<'_>, (filename_ptr, mode_ptr): (i32, i32)| {
                let memory = ctx.memory("memory")?;
                let filename = memory.read_cstring_until_null(ctx.store(), filename_ptr as usize, 1024)?;
                let mode = memory.read_cstring_until_null(ctx.store(), mode_ptr as usize, 16)?;
                let filename = filename.to_string_lossy();
                let mode = mode.to_string_lossy();
                let mut state = state.borrow_mut();
                let path = if filename == state.wad_path.to_string_lossy() || state.should_redirect_to_wad(&filename) {
                    state.wad_path.clone()
                } else {
                    state.resolve_path(&filename)
                };

                if path.is_dir() {
                    log::debug!("guest open rejected directory: path={} mode={}", path.display(), mode);
                    return Ok(-1);
                }

                let file = match HostState::open_mode_options(&mode).open(&path) {
                    Ok(file) => file,
                    Err(err) => {
                        log::debug!("guest open failed: path={} mode={} err={err}", path.display(), mode);
                        return Ok(-1);
                    }
                };

                let handle = state.next_file;
                state.next_file += 1;
                state.files.insert(handle, file);
                Ok(handle)
            }),
        );
    }

    {
        let state = state.clone();
        imports.define(
            IMPORT_MODULE,
            "host_close",
            HostFunction::from(store, move |_ctx: FuncContext<'_>, handle: i32| {
                state.borrow_mut().files.remove(&handle);
                Ok(())
            }),
        );
    }

    {
        let state = state.clone();
        imports.define(
            IMPORT_MODULE,
            "host_read",
            HostFunction::from(store, move |mut ctx: FuncContext<'_>, (handle, buf_ptr, count): (i32, i32, i32)| {
                let mut state = state.borrow_mut();
                let Some(file) = state.files.get_mut(&handle) else {
                    return Ok(0);
                };
                let mut buffer = vec![0; count.max(0) as usize];
                let read = file.read(&mut buffer).map_err(|err| tinywasm::Error::Other(err.to_string()))?;
                ctx.memory("memory")?.copy_from_slice(ctx.store_mut(), buf_ptr as usize, &buffer[..read])?;
                Ok(read as i32)
            }),
        );
    }

    {
        let state = state.clone();
        imports.define(
            IMPORT_MODULE,
            "host_write",
            HostFunction::from(store, move |ctx: FuncContext<'_>, (handle, buf_ptr, count): (i32, i32, i32)| {
                let data = ctx.memory("memory")?.read_vec(ctx.store(), buf_ptr as usize, count.max(0) as usize)?;
                let mut state = state.borrow_mut();
                let Some(file) = state.files.get_mut(&handle) else {
                    return Ok(-1);
                };
                let written = file.write(&data).map_err(|err| tinywasm::Error::Other(err.to_string()))?;
                Ok(written as i32)
            }),
        );
    }

    {
        let state = state.clone();
        imports.define(
            IMPORT_MODULE,
            "host_seek",
            HostFunction::from(store, move |_ctx: FuncContext<'_>, (handle, offset, origin): (i32, i32, i32)| {
                let seek_from = match origin {
                    0 => SeekFrom::Start(offset.max(0) as u64),
                    1 => SeekFrom::Current(offset as i64),
                    2 => SeekFrom::End(offset as i64),
                    _ => return Err(tinywasm::Error::Other(format!("invalid seek origin: {origin}"))),
                };
                let mut state = state.borrow_mut();
                let Some(file) = state.files.get_mut(&handle) else {
                    return Ok(-1);
                };
                let pos = file.seek(seek_from).map_err(|err| tinywasm::Error::Other(err.to_string()))?;
                Ok(pos.min(i32::MAX as u64) as i32)
            }),
        );
    }

    {
        let state = state.clone();
        imports.define(
            IMPORT_MODULE,
            "host_tell",
            HostFunction::from(store, move |_ctx: FuncContext<'_>, handle: i32| {
                let mut state = state.borrow_mut();
                let Some(file) = state.files.get_mut(&handle) else {
                    return Ok(-1);
                };
                let pos = file.stream_position().map_err(|err| tinywasm::Error::Other(err.to_string()))?;
                Ok(pos.min(i32::MAX as u64) as i32)
            }),
        );
    }

    {
        let state = state.clone();
        imports.define(
            IMPORT_MODULE,
            "host_eof",
            HostFunction::from(store, move |_ctx: FuncContext<'_>, handle: i32| {
                let mut state = state.borrow_mut();
                let Some(file) = state.files.get_mut(&handle) else {
                    return Ok(1);
                };
                let pos = file.stream_position().map_err(|err| tinywasm::Error::Other(err.to_string()))?;
                let len = file.metadata().map_err(|err| tinywasm::Error::Other(err.to_string()))?.len();
                Ok((pos >= len) as i32)
            }),
        );
    }

    {
        let state = state.clone();
        imports.define(
            IMPORT_MODULE,
            "host_gettime",
            HostFunction::from(store, move |mut ctx: FuncContext<'_>, (sec_ptr, usec_ptr): (i32, i32)| {
                let elapsed = state.borrow().start.elapsed();
                let sec = elapsed.as_secs().min(i32::MAX as u64) as i32;
                let usec = elapsed.subsec_micros() as i32;
                let memory = ctx.memory("memory")?;
                memory.copy_from_slice(ctx.store_mut(), sec_ptr as usize, &sec.to_le_bytes())?;
                memory.copy_from_slice(ctx.store_mut(), usec_ptr as usize, &usec.to_le_bytes())?;
                Ok(())
            }),
        );
    }

    {
        let state = state.clone();
        imports.define(
            IMPORT_MODULE,
            "host_exit",
            HostFunction::from(store, move |_ctx: FuncContext<'_>, code: i32| {
                state.borrow_mut().exit_code = Some(code);
                Ok(())
            }),
        );
    }

    imports.define(
        IMPORT_MODULE,
        "host_print",
        HostFunction::from(store, move |ctx: FuncContext<'_>, ptr: i32| {
            let text = ctx.memory("memory")?.read_cstring_until_null(ctx.store(), ptr as usize, 4096)?;
            log::info!("guest: {}", text.to_string_lossy());
            Ok(())
        }),
    );

    imports
}