diff --git a/Cargo.lock b/Cargo.lock index 0d190f4f..0f1a715c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -434,6 +434,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -554,6 +573,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "filetime" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "windows-sys 0.48.0", +] + [[package]] name = "flate2" version = "1.0.26" @@ -627,6 +658,15 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "generator" version = "0.7.5" @@ -828,6 +868,26 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "instant" version = "0.1.12" @@ -917,6 +977,26 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1247,6 +1327,24 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5738a2795d57ea20abec2d6d76c6081186709c0024187cd5977265eda6598b51" +dependencies = [ + "bitflags 1.3.2", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "mio", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1393,6 +1491,7 @@ dependencies = [ "env_logger", "image", "log", + "notify", "oden-js", "pollster", "swc_common", diff --git a/Cargo.toml b/Cargo.toml index 17dfe5d6..25912249 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ bytemuck = { version = "1.13", features = ["derive"] } env_logger = "0.10" image = { version = "0.24", default-features = false, features = ["png"] } log = "0.4" +notify = "6" oden-js = { path = "oden-js" } pollster = "0.3" swc_common = "0.31.16" diff --git a/game/main.ts b/game/main.ts index 84282c67..5cbf622c 100644 --- a/game/main.ts +++ b/game/main.ts @@ -49,6 +49,20 @@ export function init() { load_assets(); } +export function suspend() { + return { clock }; +} + +export function resume(snapshot) { + if (snapshot) { + print("!Resuming!"); + const { clock: new_clock } = snapshot; + if (new_clock) { + clock = new_clock; + } + } +} + const friction = 0.6; let robo_vel = new_v2(0); let robo_pos = new_v2(10); diff --git a/oden-js/Cargo.toml b/oden-js/Cargo.toml index c9dcd5c5..a0a10e70 100644 --- a/oden-js/Cargo.toml +++ b/oden-js/Cargo.toml @@ -8,8 +8,9 @@ edition = "2021" [dependencies] anyhow = "1" bitflags = "1" -thiserror = "1" oden-js-sys = {path = "../oden-js-sys"} +thiserror = "1" + [dev-dependencies] assert_matches = "1.5.0" diff --git a/oden-js/src/context.rs b/oden-js/src/context.rs index 2004fb65..5eb6901a 100644 --- a/oden-js/src/context.rs +++ b/oden-js/src/context.rs @@ -396,6 +396,23 @@ impl ContextRef { pub fn process_all_jobs(&self) -> Result<()> { self.get_runtime().process_all_jobs() } + + /// Deserialize a value from bytes generated by `ValueRef::serialize()`. + /// + /// NOTE: The serialized value is only good for this exact version of + /// QuickJS- do *not* expect to be able to save it to disk and + /// re-load it. This is for more ephemeral usage: passing values + /// between threads, etc. + pub fn deserialize(&self, data: &[u8]) -> Result { + self.check_exception(unsafe { + sys::JS_ReadObject( + self.ctx, + data.as_ptr(), + data.len(), + sys::JS_READ_OBJ_REFERENCE as i32, + ) + }) + } } #[derive(Debug)] diff --git a/oden-js/src/value.rs b/oden-js/src/value.rs index c8a95def..cc73369f 100644 --- a/oden-js/src/value.rs +++ b/oden-js/src/value.rs @@ -337,6 +337,42 @@ impl ValueRef { } } } + + /// Serialize this value into a byte array. Consume the byte array with + /// `ContextRef::deserialize()`. + /// + /// NOTE: The serialized value is only good for this exact version of + /// QuickJS- do *not* expect to be able to save it to disk and + /// re-load it. This is for more ephemeral usage: passing values + /// between threads, etc. + /// + /// NOTE: In theory if we wanted to just serialize a message we could + /// avoid a memory copy; we copy here to avoid keeping the runtime + /// alive, which we would have to do in order to free the buffer + /// correctly. If we were willing to keep the runtime alive... + /// + /// TODO: We do not have support for SharedArrayBuffers here, which would + /// let us use this function to pass large buffers between Runtime + /// instances, like on different threads, without copying. What a + /// pity. + pub fn serialize(&self, ctx: &ContextRef) -> Result> { + unsafe { + let mut size = 0; + let data = sys::JS_WriteObject( + ctx.ctx, + &mut size, + self.val, + sys::JS_WRITE_OBJ_REFERENCE as i32, + ); + if data.is_null() { + Err(ctx.exception_error()) + } else { + let result = std::slice::from_raw_parts(data, size).to_vec(); + sys::js_free(ctx.ctx, data as *mut std::ffi::c_void); + Ok(result) + } + } + } } impl fmt::Debug for ValueRef { diff --git a/src/lib.rs b/src/lib.rs index 1dc698b6..4d96e084 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ use bytemuck; use std::collections::HashMap; use std::rc::Rc; -use std::sync::mpsc::Receiver; +use std::sync::mpsc::{channel, Receiver}; use std::time::Instant; use tracy_client::{frame_mark, set_thread_name, span}; use wgpu::util::DeviceExt; @@ -765,8 +765,11 @@ struct UIEvent { fn main_thread(event_loop: EventLoopProxy, state: State, reciever: Receiver) { let mut state = state; - let mut script = script::ScriptContext::new(); - script.init(); + + let (script_reload_send, script_reload_recv) = channel(); + + let mut script = script::ScriptContext::new(None, script_reload_send.clone()) + .expect("Unable to create initial script context"); const SPF: f64 = 1.0 / 60.0; loop { @@ -811,6 +814,22 @@ fn main_thread(event_loop: EventLoopProxy, state: State, reciever: Re } } + { + let _span = span!("check script reload"); + let mut reload_script = false; + while let Ok(_) = script_reload_recv.try_recv() { + reload_script = true; + } + if reload_script { + eprintln!("RELOADING SCRIPT"); + let suspend_state = script.suspend(); + match script::ScriptContext::new(suspend_state, script_reload_send.clone()) { + Ok(new_script) => script = new_script, + Err(e) => eprintln!("WARNING: Script reload aborted, load failure: {e:?}"), + }; + } + } + { let _span = span!("update"); script.update(); diff --git a/src/script.rs b/src/script.rs index 56086f5d..be3abd74 100644 --- a/src/script.rs +++ b/src/script.rs @@ -1,9 +1,11 @@ +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use oden_js::{ module::loader::{ModuleLoader, ModuleSource}, Context, ContextRef, Result, Runtime, Value, }; use std::ffi::OsStr; -use std::sync::mpsc::{channel, Receiver}; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::sync::{Arc, Mutex}; use std::time::Instant; use tracy_client::span; use winit::event::*; @@ -18,11 +20,19 @@ use graphics::GraphicsCommand; mod typescript; use typescript::transpile_to_javascript; -struct Loader {} +struct Loader { + watcher: Arc>, +} impl Loader { - pub fn new() -> Loader { - Loader {} + pub fn new(reload_trigger: Sender<()>) -> Loader { + let watcher = Arc::new(Mutex::new( + notify::recommended_watcher(move |_| { + let _ = reload_trigger.send(()); + }) + .expect("Unable to create watcher"), + )); + Loader { watcher } } } @@ -37,15 +47,17 @@ impl ModuleLoader for Loader { contents }; + let mut watcher = self.watcher.lock().unwrap(); + let _ = watcher.watch(&path, RecursiveMode::NonRecursive); Ok(ModuleSource::JavaScript(contents)) } } pub struct ScriptContext { context: Context, - init: Value, update: Value, draw: Value, + suspend: Value, state: Value, @@ -57,9 +69,9 @@ pub struct ScriptContext { } impl ScriptContext { - pub fn new() -> Self { + pub fn new(suspend_state: Option>, reload_trigger: Sender<()>) -> Result { let mut runtime = Runtime::new(); - runtime.set_module_loader(Loader::new()); + runtime.set_module_loader(Loader::new(reload_trigger)); let mut context = Context::new(runtime); context.add_intrinsic_bigfloat(); @@ -68,33 +80,31 @@ impl ScriptContext { let (gfx_send, gfx_receive) = channel(); - let gfx = graphics::GraphicsAPI::define(&context, gfx_send.clone()) - .expect("Graphics module should load without error"); - let _io = io::IoAPI::define(&context).expect("IO module should load without error"); - let time = time::TimeAPI::define(&context).expect("Time module should load without error"); - let input = - input::InputAPI::define(&context).expect("Input module should load without error"); + let gfx = graphics::GraphicsAPI::define(&context, gfx_send.clone())?; + let _io = io::IoAPI::define(&context)?; + let time = time::TimeAPI::define(&context)?; + let input = input::InputAPI::define(&context)?; - let module = context - .import_module("./main.ts", "") - .expect("Unable to load main"); + let module = context.import_module("./main.ts", "")?; - let init = module - .get_export(&context, "init") - .expect("Unable to fetch init"); - let update = module - .get_export(&context, "update") - .expect("Unable to fetch update"); - let draw = module - .get_export(&context, "draw") - .expect("Unable to fetch draw"); + let init = module.get_export(&context, "init")?; + let suspend = module.get_export(&context, "suspend")?; + let resume = module.get_export(&context, "resume")?; + let update = module.get_export(&context, "update")?; + let draw = module.get_export(&context, "draw")?; - let state = context.undefined(); + let mut state = init.call(&context, &[])?; + if let Some(buffer) = suspend_state { + if !resume.is_undefined() { + let serialized_state = context.deserialize(&buffer)?; + state = resume.call(&context, &[&serialized_state, &state])?; + } + } - ScriptContext { + Ok(ScriptContext { context, - init, + suspend, update, draw, @@ -105,6 +115,25 @@ impl ScriptContext { time, input, + }) + } + + /// Allow the script to save its state before we destroy it and re-create + /// it. + pub fn suspend(&self) -> Option> { + let _span = span!("script suspend"); + if !self.suspend.is_undefined() { + let suspend_state = self + .suspend + .call(&self.context, &[&self.state]) + .expect("Exception in suspend"); + Some( + suspend_state + .serialize(&self.context) + .expect("Unable to serialize state"), + ) + } else { + None } } @@ -112,15 +141,6 @@ impl ScriptContext { // We would want a bi-directional gate for frames to not let the // game thread go to fast probably? And to discard whole frames &c. - pub fn init(&mut self) { - let _span = span!("script init"); - - self.state = self - .init - .call(&self.context, &[]) - .expect("Exception in init"); - } - pub fn input(&mut self, event: &WindowEvent) -> bool { match event { WindowEvent::KeyboardInput { input, .. } => self.input.handle_keyboard_input(input), diff --git a/src/script/typescript.rs b/src/script/typescript.rs index 19bfc75b..dadeba6a 100644 --- a/src/script/typescript.rs +++ b/src/script/typescript.rs @@ -57,6 +57,20 @@ fn format_swc_diagnostic(source_map: &SourceMap, diagnostic: &Diagnostic) -> Str } } +fn diagnostics_to_parse_error<'a>( + name: &str, + source_map: &SourceMap, + diagnostics: impl Iterator, +) -> Error { + Error::ParseError( + name.into(), + diagnostics + .map(|d| format_swc_diagnostic(source_map, d)) + .collect::>() + .join("\n\n"), + ) +} + fn ensure_no_fatal_swc_diagnostics<'a>( name: &str, source_map: &SourceMap, @@ -66,13 +80,10 @@ fn ensure_no_fatal_swc_diagnostics<'a>( .filter(|d| is_fatal_swc_diagnostic(d)) .collect::>(); if !fatal_diagnostics.is_empty() { - Err(Error::ParseError( - name.into(), - fatal_diagnostics - .iter() - .map(|d| format_swc_diagnostic(source_map, d)) - .collect::>() - .join("\n\n"), + Err(diagnostics_to_parse_error( + name, + source_map, + fatal_diagnostics.into_iter(), )) } else { Ok(()) @@ -109,8 +120,12 @@ pub fn transpile_to_javascript(path: &str, input: String) -> Result { let module = parser .parse_module() - .map_err(|e| e.into_diagnostic(&handler).emit()) - .expect("failed to parse module."); + .map_err(|e| e.into_diagnostic(&handler)) + .map_err(|mut e| { + e.emit(); + let diagnostics = diagnostics_cell.borrow(); + diagnostics_to_parse_error(path, &cm, diagnostics.iter()) + })?; let globals = Globals::default(); GLOBALS.set(&globals, || {