diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/file.rs | 86 | ||||
| -rw-r--r-- | src/lib.rs | 1 | ||||
| -rw-r--r-- | src/main.rs | 28 | ||||
| -rw-r--r-- | src/scene.rs | 66 | ||||
| -rw-r--r-- | src/script.rs | 94 | ||||
| -rw-r--r-- | src/script/wasm.rs | 121 | ||||
| -rw-r--r-- | src/time.rs | 45 |
7 files changed, 374 insertions, 67 deletions
diff --git a/src/file.rs b/src/file.rs index 2963879..20fdd68 100644 --- a/src/file.rs +++ b/src/file.rs @@ -4,7 +4,10 @@ use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use smol::{Task, block_on, unblock}; -use crate::scene::{Scene, scene_id_by_name}; +use crate::{ + scene::{ResourceRequirements, Scene, scene_id_by_name}, + script::ScriptDeclaration, +}; type FileTask = Task<std::io::Result<FileData>>; @@ -13,18 +16,39 @@ pub struct FileManager { cache: FxHashMap<Box<Path>, FileData>, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct GameManifest { pub name: Box<str>, pub first_scene: usize, pub scenes: Box<[Scene]>, + pub scripts: Box<[ScriptDeclaration]>, } #[derive(Debug, Serialize, Deserialize)] struct TomlGameManifest { name: Box<str>, first_scene: Box<str>, - scene: Box<[Scene]>, + #[serde(default)] + scene: Box<[TomlScene]>, + #[serde(default)] + script: Box<[TomlScript]>, +} + +#[derive(Debug, Serialize, Deserialize)] +struct TomlScene { + name: Box<str>, + #[serde(default)] + preload_scenes: Box<[Box<str>]>, + #[serde(default)] + scripts: Box<[Box<str>]>, + #[serde(default)] + preload_scripts: Box<[Box<str>]>, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "kebab-case")] +enum TomlScript { + Wasm { name: Box<str>, path: Box<Path> }, } pub enum FileData { @@ -33,22 +57,50 @@ pub enum FileData { } fn normalize_game_manifest(manifest: TomlGameManifest) -> std::io::Result<GameManifest> { - let first_scene = scene_id_by_name(&manifest.scene, &manifest.first_scene).ok_or( + let mut scripts = Vec::with_capacity(manifest.script.len()); + let script_map: FxHashMap<Box<str>, usize> = manifest + .script + .into_iter() + .map(|script| match script { + TomlScript::Wasm { name, path } => { + scripts.push(ScriptDeclaration::Wasm { path }); + (name, scripts.len() - 1) + } + }) + .collect(); + let scenes: Box<[Scene]> = manifest + .scene + .into_iter() + .map(|scene| Scene { + name: scene.name, + preload_scenes: Box::from([]), + scripts: ResourceRequirements { + required: scene + .scripts + .iter() + .map(|s| *script_map.get(s).unwrap()) + .collect(), + preload: scene + .preload_scripts + .iter() + .map(|s| *script_map.get(s).unwrap()) + .collect(), + }, + }) + .collect(); + let first_scene = scene_id_by_name(&scenes, &manifest.first_scene).ok_or( std::io::Error::other("Invalid first scene: scene not found"), )?; Ok(GameManifest { name: manifest.name, first_scene, - scenes: manifest.scene, + scenes, + scripts: scripts.into_boxed_slice(), }) } fn compile_data(path: &Path, data: &[u8], compiler: &wasmi::Engine) -> std::io::Result<FileData> { - if path == "manifest.postcard" { - return Ok(FileData::GameManifest( - postcard::from_bytes(data).map_err(std::io::Error::other)?, - )); - } else if path == "manifest.toml" { + if path == "manifest.toml" { return Ok(FileData::GameManifest(normalize_game_manifest( toml::from_slice(data).map_err(std::io::Error::other)?, )?)); @@ -96,12 +148,14 @@ impl FileManager { path: Box<Path>, wasm_compiler: &wasmi::Engine, ) -> std::io::Result<&FileData> { - let data = compile_data( - path.as_ref(), - &std::fs::read(path.as_ref()).map(Vec::into_boxed_slice)?, - wasm_compiler, - )?; - self.cache.entry(path.clone()).insert_entry(data); + if !self.cache.contains_key(&path) { + let data = compile_data( + path.as_ref(), + &std::fs::read(path.as_ref()).map(Vec::into_boxed_slice)?, + wasm_compiler, + )?; + self.cache.entry(path.clone()).insert_entry(data); + } Ok(self.cache.get(&path).unwrap()) } @@ -3,3 +3,4 @@ pub mod file; pub mod input; pub mod scene; pub mod script; +pub mod time; diff --git a/src/main.rs b/src/main.rs index bd42e18..d39b0f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::time::SystemTime; +use std::{io::BufWriter, time::SystemTime}; use crossterm::{event::KeyCode, style::ContentStyle}; use mimalloc::MiMalloc; @@ -7,7 +7,8 @@ use tortuise::{ file::FileManager, input::InputManager, scene::switch_scenes, - script::{ScriptContext, ScriptDeclaration, ScriptManager, WasmRunner}, + script::{ScriptContext, ScriptManager, WasmRunner}, + time::TimeContext, }; #[global_allocator] @@ -63,28 +64,17 @@ fn main() { .unwrap_manifest() .clone(); - let stdout = Box::new(std::io::stdout().lock()); + let stdout = Box::new(BufWriter::new(std::io::stdout().lock())); let mut buffer = Buffer::new(stdout).unwrap(); let mut input = InputManager::new(); + let mut time = TimeContext::new(); + let mut scripts = ScriptManager::new(&manifest.scripts); let mut context = ScriptContext { buffer: &mut buffer, input: &input, + time: &time, }; - let scripts = [ - ScriptDeclaration::Static { - start: Some(init_first_frame), - update: Some(render_frame_rate), - end: None, - }, - ScriptDeclaration::Static { - start: None, - update: Some(exit_on_escape), - end: None, - }, - ]; - let mut scripts = ScriptManager::new(&scripts); - switch_scenes( &manifest.scenes, &mut scripts, @@ -105,13 +95,15 @@ fn main() { } } Event::Render => { + buffer.cells.clear(); let mut context = ScriptContext { buffer, input: &input, + time: &time, }; scripts.run_scripts(&mut context, &wasm_runner, &mut files); input.reset_next_frame(); - puffin::GlobalProfiler::lock().new_frame(); + time.next_frame(); } }) .unwrap(); diff --git a/src/scene.rs b/src/scene.rs index 34ff877..28a4e6a 100644 --- a/src/scene.rs +++ b/src/scene.rs @@ -9,6 +9,7 @@ use crate::{ #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Scene { pub name: Box<str>, + pub preload_scenes: Box<[usize]>, pub scripts: ResourceRequirements, } @@ -28,6 +29,34 @@ pub fn scene_id_by_name(scenes: &[Scene], name: &str) -> Option<usize> { None } +pub fn preload_scene( + scenes: &[Scene], + scripts: &mut ScriptManager, + files: &mut FileManager, + wasm_runner: &WasmRunner, + id: usize, +) { + let scene = &scenes[id]; + for script in &scene.scripts.required { + scripts.preload_script(*script, files, wasm_runner); + } +} + +pub fn is_scene_loaded( + scenes: &[Scene], + scripts: &mut ScriptManager, + files: &mut FileManager, + id: usize, +) -> bool { + let scene = &scenes[id]; + scene + .scripts + .required + .iter() + .cloned() + .all(|script_id| scripts.is_script_loaded(script_id, files)) +} + pub fn switch_scenes( scenes: &[Scene], scripts: &mut ScriptManager, @@ -36,13 +65,36 @@ pub fn switch_scenes( wasm_runner: &WasmRunner, new: usize, ) { - let active_scripts = FxHashSet::from_iter(scripts.active_scripts.iter().cloned()); - let scripts_to_load = &scenes[new].scripts.required; - for script in scripts_to_load { - if !active_scripts.contains(script) { - scripts.activate_script(*script, context, wasm_runner, files); - } else { - scripts.reset_script(*script, context, wasm_runner, files); + let active_scripts = scripts + .active_scripts + .iter() + .cloned() + .collect::<Box<[usize]>>(); + for script_id in &active_scripts { + scripts.deactivate_script(*script_id, context, wasm_runner, files); + } + + let mut needed_scripts = FxHashSet::default(); + let scene = &scenes[new]; + for script_id in &scene.scripts.required { + needed_scripts.insert(*script_id); + scripts.activate_script(*script_id, context, wasm_runner, files); + } + for script_id in &scene.scripts.preload { + needed_scripts.insert(*script_id); + scripts.preload_script(*script_id, files, wasm_runner); + } + for scene_to_preload in &scene.preload_scenes { + let scene_to_preload = &scenes[*scene_to_preload]; + for script_id in &scene_to_preload.scripts.required { + needed_scripts.insert(*script_id); + scripts.preload_script(*script_id, files, wasm_runner); } } + + let active_scripts: FxHashSet<usize> = active_scripts.iter().cloned().collect(); + let unneeded_scripts = active_scripts.difference(&needed_scripts); + for script_id in unneeded_scripts { + scripts.unload_script(*script_id, files); + } } diff --git a/src/script.rs b/src/script.rs index 861f665..2de50f5 100644 --- a/src/script.rs +++ b/src/script.rs @@ -2,7 +2,9 @@ use std::path::Path; use rustc_hash::FxHashMap; -use crate::{buffer::Buffer, file::FileManager, input::InputManager}; +use crate::{buffer::Buffer, file::FileManager, input::InputManager, time::TimeContext}; + +mod wasm; pub struct ScriptManager<'a> { scripts: &'a [ScriptDeclaration], @@ -12,16 +14,18 @@ pub struct ScriptManager<'a> { pub struct WasmRunner { pub engine: wasmi::Engine, - pub linker: wasmi::Linker<&'static ScriptContext<'static>>, + pub linker: wasmi::Linker<&'static mut ScriptContext<'static>>, } pub struct ScriptContext<'a> { pub buffer: &'a mut Buffer, pub input: &'a InputManager, + pub time: &'a TimeContext, } type StaticScriptFunction = Option<fn(&mut ScriptContext<'_>, &mut Box<[u8]>)>; +#[derive(Debug, Clone)] pub enum ScriptDeclaration { Static { start: StaticScriptFunction, @@ -53,7 +57,12 @@ impl Default for WasmRunner { impl WasmRunner { pub fn new() -> Self { let engine = wasmi::Engine::default(); - let linker = wasmi::Linker::new(&engine); + let linker = unsafe { + std::mem::transmute::< + wasmi::Linker<&mut ScriptContext<'_>>, + wasmi::Linker<&mut ScriptContext<'_>>, + >(wasm::linker(&engine).unwrap()) + }; Self { engine, linker } } @@ -69,14 +78,26 @@ impl WasmRunner { ) -> Result<(), wasmi::Error> { // safety: the store won't be kept around after this function ends. Only the // lifetimes are being transmuted - let mut store = wasmi::Store::new(&self.engine, unsafe { - std::mem::transmute::<&ScriptContext<'_>, &ScriptContext<'_>>(context) - }); - let instance = self.linker.instantiate_and_start(&mut store, module)?; - let function = instance.get_func(&store, function_name).ok_or_else(|| { - wasmi::Error::new(format!("No function named {function_name} was exported")) - })?; - function.call(&mut store, &[], &mut []) + let mut store = { + puffin::profile_scope!("create store"); + wasmi::Store::new(&self.engine, unsafe { + std::mem::transmute::<&mut ScriptContext<'_>, &mut ScriptContext<'_>>(context) + }) + }; + let instance = { + puffin::profile_scope!("instantiate"); + self.linker.instantiate_and_start(&mut store, module)? + }; + let function = { + puffin::profile_scope!("get function"); + instance.get_func(&store, function_name).ok_or_else(|| { + wasmi::Error::new(format!("No function named {function_name} was exported")) + })? + }; + { + puffin::profile_scope!("call function"); + function.call(&mut store, &[], &mut []) + } } } @@ -89,6 +110,35 @@ impl<'a> ScriptManager<'a> { } } + pub fn preload_script( + &self, + script_id: usize, + files: &mut FileManager, + wasm_runner: &WasmRunner, + ) { + let script = &self.scripts[script_id]; + match script { + ScriptDeclaration::Static { .. } => (), + ScriptDeclaration::Wasm { path } => files.start_loading_file(path, &wasm_runner.engine), + } + } + + pub fn is_script_loaded(&self, script_id: usize, files: &mut FileManager) -> bool { + let script = &self.scripts[script_id]; + match script { + ScriptDeclaration::Static { .. } => true, + ScriptDeclaration::Wasm { path } => files.is_file_loaded(path), + } + } + + pub fn unload_script(&self, script_id: usize, files: &mut FileManager) { + let script = &self.scripts[script_id]; + match script { + ScriptDeclaration::Static { .. } => (), + ScriptDeclaration::Wasm { path } => files.unload_file(path), + } + } + pub fn activate_script( &mut self, script_id: usize, @@ -177,24 +227,16 @@ impl<'a> ScriptManager<'a> { } } ScriptDeclaration::Wasm { path } => { - let module = files - .load_file(path.clone(), &wasm_runner.engine) - .unwrap() - .unwrap_wasm_module(); + let module = { + puffin::profile_scope!("load module"); + files + .load_file(path.clone(), &wasm_runner.engine) + .unwrap() + .unwrap_wasm_module() + }; wasm_runner.run_function(module, context, "update"); } } } } - - pub fn reset_script( - &mut self, - script_id: usize, - context: &mut ScriptContext<'_>, - wasm_runner: &WasmRunner, - files: &mut FileManager, - ) { - self.activate_script(script_id, context, wasm_runner, files); - self.deactivate_script(script_id, context, wasm_runner, files); - } } diff --git a/src/script/wasm.rs b/src/script/wasm.rs new file mode 100644 index 0000000..96190f6 --- /dev/null +++ b/src/script/wasm.rs @@ -0,0 +1,121 @@ +use std::time::UNIX_EPOCH; + +use crossterm::{event::KeyCode, style::ContentStyle}; +use wasmi::{Caller, Engine, Linker}; + +use crate::{buffer::Cell, script::ScriptContext}; + +fn add_cell(mut caller: Caller<&mut ScriptContext>, character: u32, location: u32, z: u32) { + caller.data_mut().buffer.cells.push(Cell { + character: char::from_u32(character).unwrap_or_default(), + style: ContentStyle::new(), + x: (location >> 16) as u16, + y: (location & 0xffff) as u16, + z: z as u8, + }); +} + +fn window_size(caller: Caller<&mut ScriptContext>) -> u32 { + let window_size = &caller.data().buffer.window_size; + ((window_size.rows as u32) << 16) | (window_size.columns as u32) +} + +fn is_key_pressed_this_frame(caller: Caller<&mut ScriptContext>, key_code: u32) -> u32 { + const BACKSPACE: u32 = 1 << 24; + const ENTER: u32 = 2 << 24; + const LEFT: u32 = 3 << 24; + const RIGHT: u32 = 4 << 24; + const UP: u32 = 5 << 24; + const DOWN: u32 = 6 << 24; + const HOME: u32 = 7 << 24; + const END: u32 = 8 << 24; + const PAGE_UP: u32 = 9 << 24; + const PAGE_DOWN: u32 = 10 << 24; + const TAB: u32 = 11 << 24; + const BACK_TAB: u32 = 12 << 24; + const DELETE: u32 = 13 << 24; + const INSERT: u32 = 14 << 24; + const F: u32 = 15 << 24; + const CHAR: u32 = 16 << 24; + const ESC: u32 = 17 << 24; + let key_code = match key_code { + 0 => KeyCode::Null, + BACKSPACE => KeyCode::Backspace, + ENTER => KeyCode::Enter, + LEFT => KeyCode::Left, + RIGHT => KeyCode::Right, + UP => KeyCode::Up, + DOWN => KeyCode::Down, + HOME => KeyCode::Home, + END => KeyCode::End, + PAGE_UP => KeyCode::PageUp, + PAGE_DOWN => KeyCode::PageDown, + TAB => KeyCode::Tab, + BACK_TAB => KeyCode::BackTab, + DELETE => KeyCode::Delete, + INSERT => KeyCode::Insert, + F..CHAR => KeyCode::F((key_code & 0xff) as u8), + CHAR..ESC => KeyCode::Char(char::from_u32(key_code & 0xffffff).unwrap_or_default()), + ESC => KeyCode::Esc, + _ => KeyCode::Null, + }; + + if caller.data().input.is_key_pressed_this_frame(key_code) { + 1 + } else { + 0 + } +} + +fn exit(mut caller: Caller<&mut ScriptContext>) { + let _ = caller.data_mut().buffer.finish(); + std::process::exit(0); +} + +fn random_u64() -> u64 { + getrandom::u64().unwrap_or(4) +} + +fn system_now(caller: Caller<&mut ScriptContext>) -> u64 { + caller + .data() + .time + .system_now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as u64 +} + +fn monotonic_now(caller: Caller<&mut ScriptContext>) -> u64 { + caller.data().time.monotonic_now().as_nanos() as u64 +} + +fn delta_time(caller: Caller<&mut ScriptContext>) -> u64 { + caller.data().time.delta_time().as_nanos() as u64 +} + +pub fn linker<'ctx>( + engine: &Engine, +) -> Result<wasmi::Linker<&'ctx mut ScriptContext<'ctx>>, wasmi::Error> { + macro_rules! add_fns { + ($linker: expr, [$($func: ident),*]) => { + $($linker.func_wrap("tortuise", stringify!($func), $func)?;)* + }; + } + + let mut linker = Linker::new(engine); + add_fns!( + linker, + [ + add_cell, + window_size, + is_key_pressed_this_frame, + exit, + random_u64, + system_now, + monotonic_now, + delta_time + ] + ); + Ok(linker) +} diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..5978b57 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,45 @@ +use std::time::{Duration, Instant, SystemTime}; + +pub struct TimeContext { + start_time: Instant, + last_frame: Instant, + current_frame: Instant, + system_time: SystemTime, +} + +impl TimeContext { + pub fn new() -> Self { + let now = Instant::now(); + Self { + start_time: now, + last_frame: now, + current_frame: now, + system_time: SystemTime::now(), + } + } + + pub fn next_frame(&mut self) { + puffin::profile_scope!("update time"); + self.last_frame = self.current_frame; + self.current_frame = Instant::now(); + self.system_time = SystemTime::now() + } + + pub fn delta_time(&self) -> Duration { + self.current_frame - self.last_frame + } + + pub fn monotonic_now(&self) -> Duration { + self.current_frame - self.start_time + } + + pub fn system_now(&self) -> SystemTime { + self.system_time + } +} + +impl Default for TimeContext { + fn default() -> Self { + Self::new() + } +} |
