From 5ed8982ad2cb4b348046aa11c527ce6a51f02e0e Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 2 May 2026 15:40:47 +0200 Subject: feat: add doom example, fix import argument order Signed-off-by: Henry --- examples/doom/src/runtime.rs | 348 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 examples/doom/src/runtime.rs (limited to 'examples/doom/src/runtime.rs') diff --git a/examples/doom/src/runtime.rs b/examples/doom/src/runtime.rs new file mode 100644 index 0000000..980dc21 --- /dev/null +++ b/examples/doom/src/runtime.rs @@ -0,0 +1,348 @@ +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, + key_up: tinywasm::FunctionTyped, + memory: tinywasm::Memory, + framebuffer_bytes: Vec, + pub host_state: Rc>, +} + +impl Runtime { + pub fn new(wad_path: PathBuf, guest_path: PathBuf) -> Result { + 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::(&store, "tinywasm_doom_key_down")?; + let key_up = instance.func::(&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, + next_file: i32, + pub exit_code: Option, +} + +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>) -> 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 +} -- cgit v1.3.1