summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--examples/snake.rs47
-rw-r--r--src/file.rs86
-rw-r--r--src/lib.rs1
-rw-r--r--src/main.rs28
-rw-r--r--src/scene.rs66
-rw-r--r--src/script.rs94
-rw-r--r--src/script/wasm.rs121
-rw-r--r--src/time.rs45
8 files changed, 401 insertions, 87 deletions
diff --git a/examples/snake.rs b/examples/snake.rs
index b284dba..49ea212 100644
--- a/examples/snake.rs
+++ b/examples/snake.rs
@@ -1,4 +1,7 @@
-use std::time::{Duration, SystemTime};
+use std::{
+ io::BufWriter,
+ time::{Duration, SystemTime},
+};
use bytemuck::{Pod, Zeroable, bytes_of, from_bytes_mut};
use crossterm::{event::KeyCode, style::ContentStyle};
@@ -8,6 +11,7 @@ use tortuise::{
input::InputManager,
scene::{ResourceRequirements, Scene, switch_scenes},
script::{ScriptContext, ScriptDeclaration, ScriptManager, WasmRunner},
+ time::TimeContext,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Pod, Zeroable)]
@@ -106,38 +110,39 @@ fn main() {
first_scene: 0,
scenes: Box::from([Scene {
name: Box::from("main"),
+ preload_scenes: Box::from([]),
scripts: ResourceRequirements {
required: Box::from([0, 1]),
preload: Box::from([]),
},
}]),
+ scripts: Box::from([
+ ScriptDeclaration::Static {
+ start: None,
+ update: Some(exit_on_escape),
+ end: None,
+ },
+ ScriptDeclaration::Static {
+ start: Some(init_scene),
+ update: Some(render),
+ end: None,
+ },
+ ]),
};
- 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 files = FileManager::default();
let mut input = InputManager::new();
+ let wasm_runner = WasmRunner::new();
+ let mut scripts = ScriptManager::new(&manifest.scripts);
+ let mut time = TimeContext::new();
+
let mut context = ScriptContext {
buffer: &mut buffer,
input: &input,
+ time: &time,
};
-
- let scripts = [
- ScriptDeclaration::Static {
- start: None,
- update: Some(exit_on_escape),
- end: None,
- },
- ScriptDeclaration::Static {
- start: Some(init_scene),
- update: Some(render),
- end: None,
- },
- ];
-
- let mut files = FileManager::default();
- let mut scripts = ScriptManager::new(&scripts);
- let wasm_runner = WasmRunner::new();
-
switch_scenes(
&manifest.scenes,
&mut scripts,
@@ -158,9 +163,11 @@ fn main() {
let mut context = ScriptContext {
buffer,
input: &input,
+ time: &time,
};
scripts.run_scripts(&mut context, &wasm_runner, &mut files);
input.reset_next_frame();
+ time.next_frame();
}
})
.unwrap();
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())
}
diff --git a/src/lib.rs b/src/lib.rs
index 3d76b0d..9c3162a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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()
+ }
+}