summaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorHenry <mail@henrygressmann.de>2026-05-02 15:40:47 +0200
committerHenry <mail@henrygressmann.de>2026-05-02 15:45:44 +0200
commit5ed8982ad2cb4b348046aa11c527ce6a51f02e0e (patch)
treec5427ac3ffdeedfa37b698eeb4f2dac09859b4a6 /examples
parent361b715de9ea6a859aaafe84d4bcd3d46ed70797 (diff)
feat: add doom example, fix import argument order
Signed-off-by: Henry <mail@henrygressmann.de>
Diffstat (limited to 'examples')
-rw-r--r--examples/doom/Cargo.toml21
-rw-r--r--examples/doom/README.md19
-rwxr-xr-xexamples/doom/build.sh55
-rw-r--r--examples/doom/guest/tinywasm_puredoom.c203
-rw-r--r--examples/doom/out/.gitkeep0
-rw-r--r--examples/doom/src/main.rs202
-rw-r--r--examples/doom/src/runtime.rs348
7 files changed, 848 insertions, 0 deletions
diff --git a/examples/doom/Cargo.toml b/examples/doom/Cargo.toml
new file mode 100644
index 0000000..c1d0af4
--- /dev/null
+++ b/examples/doom/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name="tinywasm-doom"
+publish=false
+edition.workspace=true
+rust-version.workspace=true
+
+[dependencies]
+eyre.workspace=true
+pretty_env_logger.workspace=true
+tinywasm={workspace=true, features=[
+ "std",
+ "parser",
+ "log",
+ "archive",
+ "canonicalize-nans",
+ "debug",
+ "parallel-parser",
+]}
+log.workspace=true
+softbuffer="0.4"
+winit="0.30"
diff --git a/examples/doom/README.md b/examples/doom/README.md
new file mode 100644
index 0000000..f3bc60e
--- /dev/null
+++ b/examples/doom/README.md
@@ -0,0 +1,19 @@
+# Doom Example
+
+This example builds a WebAssembly guest from [`PureDOOM`](https://github.com/Daivuk/PureDOOM) and runs it inside `tinywasm`. The host uses `winit` and `softbuffer` to present the framebuffer.
+
+## Prerequisites
+
+- `clang`
+- `git`
+- a Doom WAD that you provide yourself
+
+## Running the example
+
+```sh
+# download & build `PureDOOM`
+./examples/doom/build.sh
+
+# start doom with the WAD you want to use
+cargo run -p tinywasm-doom --release -- /path/to/DOOM1.WAD
+```
diff --git a/examples/doom/build.sh b/examples/doom/build.sh
new file mode 100755
index 0000000..a6b6c01
--- /dev/null
+++ b/examples/doom/build.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+cd "$(dirname "$0")"
+
+upstream_dir="upstream/PureDOOM"
+out_dir="out"
+wrapper_src="guest/tinywasm_puredoom.c"
+output="$out_dir/puredoom.wasm"
+
+if ! command -v git >/dev/null 2>&1; then
+ printf 'missing required tool: git\n' >&2
+ exit 1
+fi
+
+if ! command -v clang >/dev/null 2>&1; then
+ printf 'missing required tool: clang\n' >&2
+ exit 1
+fi
+
+mkdir -p "upstream" "$out_dir"
+
+if [[ ! -d "$upstream_dir/.git" ]]; then
+ git clone --depth 1 https://github.com/Daivuk/PureDOOM.git "$upstream_dir"
+else
+ git -C "$upstream_dir" pull --ff-only
+fi
+
+clang \
+ --target=wasm32-unknown-unknown \
+ -O2 \
+ -nostdlib \
+ -fno-builtin \
+ -w \
+ -Wl,--no-entry \
+ -Wl,--allow-undefined \
+ -Wl,--export=tinywasm_doom_init \
+ -Wl,--export=tinywasm_doom_update \
+ -Wl,--export=tinywasm_doom_framebuffer \
+ -Wl,--export=tinywasm_doom_sound_buffer \
+ -Wl,--export=tinywasm_doom_tick_midi \
+ -Wl,--export=tinywasm_doom_wad_path_buf \
+ -Wl,--export=tinywasm_doom_key_down \
+ -Wl,--export=tinywasm_doom_key_up \
+ -Wl,--export=memory \
+ -Wl,--export=__heap_base \
+ -Wl,--export=__data_end \
+ -Wl,--initial-memory=16777216 \
+ -Wl,--max-memory=268435456 \
+ -Wl,--stack-first \
+ -I"$upstream_dir" \
+ "$wrapper_src" \
+ -o "$output"
+
+printf 'built %s\n' "$output"
diff --git a/examples/doom/guest/tinywasm_puredoom.c b/examples/doom/guest/tinywasm_puredoom.c
new file mode 100644
index 0000000..1e3e6bd
--- /dev/null
+++ b/examples/doom/guest/tinywasm_puredoom.c
@@ -0,0 +1,203 @@
+#define DOOM_IMPLEMENTATION
+#include "PureDOOM.h"
+
+#if defined(__clang__)
+#define IMPORT(name) __attribute__((import_module("env"), import_name(name)))
+#else
+#define IMPORT(name)
+#endif
+
+extern int host_open(const char *filename, const char *mode) IMPORT("host_open");
+extern void host_close(int handle) IMPORT("host_close");
+extern int host_read(int handle, void *buf, int count) IMPORT("host_read");
+extern int host_write(int handle, const void *buf, int count) IMPORT("host_write");
+extern int host_seek(int handle, int offset, int origin) IMPORT("host_seek");
+extern int host_tell(int handle) IMPORT("host_tell");
+extern int host_eof(int handle) IMPORT("host_eof");
+extern void host_gettime(int *sec, int *usec) IMPORT("host_gettime");
+extern void host_exit(int code) IMPORT("host_exit");
+extern void host_print(const char *text) IMPORT("host_print");
+
+extern unsigned char __heap_base;
+
+static char g_wad_path[1024];
+static char g_home_dir[] = ".";
+static char g_program_name[] = "puredoom";
+static char g_iwad_flag[] = "-iwad";
+static char *g_argv[] = {g_program_name, g_iwad_flag, g_wad_path, 0};
+static unsigned int g_heap_ptr;
+
+static unsigned int align_up(unsigned int value, unsigned int alignment)
+{
+ return (value + alignment - 1u) & ~(alignment - 1u);
+}
+
+unsigned int strlen(const char *str)
+{
+ unsigned int len = 0;
+ while (str[len] != '\0')
+ ++len;
+ return len;
+}
+
+static int str_eq(const char *left, const char *right)
+{
+ unsigned int i = 0;
+ for (;;)
+ {
+ if (left[i] != right[i])
+ return 0;
+ if (left[i] == '\0')
+ return 1;
+ ++i;
+ }
+}
+
+static void *guest_malloc(int size)
+{
+ unsigned int alloc_size;
+ unsigned int end;
+ unsigned int capacity;
+ unsigned int extra_pages;
+ void *result;
+
+ if (size <= 0)
+ return 0;
+
+ if (g_heap_ptr == 0)
+ g_heap_ptr = align_up((unsigned int)(unsigned long)&__heap_base, 16u);
+
+ alloc_size = align_up((unsigned int)size, 16u);
+ end = g_heap_ptr + alloc_size;
+ capacity = __builtin_wasm_memory_size(0) * 65536u;
+
+ if (end > capacity)
+ {
+ extra_pages = (end - capacity + 65535u) / 65536u;
+ if (__builtin_wasm_memory_grow(0, extra_pages) == (unsigned int)-1)
+ return 0;
+ }
+
+ result = (void *)(unsigned long)g_heap_ptr;
+ g_heap_ptr = end;
+ return result;
+}
+
+static void guest_free(void *ptr)
+{
+ (void)ptr;
+}
+
+static void *guest_open(const char *filename, const char *mode)
+{
+ int handle = host_open(filename, mode);
+ if (handle < 0)
+ return 0;
+ return (void *)(unsigned long)(handle + 1);
+}
+
+static int guest_handle(void *handle)
+{
+ return (int)(unsigned long)handle - 1;
+}
+
+static void guest_close(void *handle)
+{
+ host_close(guest_handle(handle));
+}
+
+static int guest_read(void *handle, void *buf, int count)
+{
+ return host_read(guest_handle(handle), buf, count);
+}
+
+static int guest_write(void *handle, const void *buf, int count)
+{
+ return host_write(guest_handle(handle), buf, count);
+}
+
+static int guest_seek(void *handle, int offset, doom_seek_t origin)
+{
+ return host_seek(guest_handle(handle), offset, (int)origin);
+}
+
+static int guest_tell(void *handle)
+{
+ return host_tell(guest_handle(handle));
+}
+
+static int guest_eof(void *handle)
+{
+ return host_eof(guest_handle(handle));
+}
+
+static void guest_gettime(int *sec, int *usec)
+{
+ host_gettime(sec, usec);
+}
+
+static void guest_exit(int code)
+{
+ host_exit(code);
+}
+
+static char *guest_getenv(const char *var)
+{
+ if (str_eq(var, "HOME"))
+ return g_home_dir;
+ return 0;
+}
+
+unsigned int tinywasm_doom_wad_path_buf(void)
+{
+ return (unsigned int)(unsigned long)g_wad_path;
+}
+
+void tinywasm_doom_init(void)
+{
+ doom_set_print(host_print);
+ doom_set_malloc(guest_malloc, guest_free);
+ doom_set_file_io(guest_open, guest_close, guest_read, guest_write, guest_seek, guest_tell, guest_eof);
+ doom_set_gettime(guest_gettime);
+ doom_set_exit(guest_exit);
+ doom_set_getenv(guest_getenv);
+
+ doom_set_default_int("key_up", DOOM_KEY_W);
+ doom_set_default_int("key_down", DOOM_KEY_S);
+ doom_set_default_int("key_strafeleft", DOOM_KEY_A);
+ doom_set_default_int("key_straferight", DOOM_KEY_D);
+ doom_set_default_int("key_use", DOOM_KEY_E);
+ doom_set_resolution(320, 200);
+
+ doom_init(3, g_argv, DOOM_FLAG_HIDE_MOUSE_OPTIONS | DOOM_FLAG_HIDE_SOUND_OPTIONS | DOOM_FLAG_HIDE_MUSIC_OPTIONS);
+}
+
+void tinywasm_doom_update(void)
+{
+ doom_update();
+}
+
+unsigned int tinywasm_doom_framebuffer(void)
+{
+ return (unsigned int)(unsigned long)doom_get_framebuffer(4);
+}
+
+unsigned int tinywasm_doom_sound_buffer(void)
+{
+ return (unsigned int)(unsigned long)doom_get_sound_buffer();
+}
+
+unsigned long tinywasm_doom_tick_midi(void)
+{
+ return doom_tick_midi();
+}
+
+void tinywasm_doom_key_down(int key)
+{
+ doom_key_down((doom_key_t)key);
+}
+
+void tinywasm_doom_key_up(int key)
+{
+ doom_key_up((doom_key_t)key);
+}
diff --git a/examples/doom/out/.gitkeep b/examples/doom/out/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/examples/doom/out/.gitkeep
diff --git a/examples/doom/src/main.rs b/examples/doom/src/main.rs
new file mode 100644
index 0000000..095183d
--- /dev/null
+++ b/examples/doom/src/main.rs
@@ -0,0 +1,202 @@
+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)
+}
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<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
+}