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/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..c0188976 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,7 +765,10 @@ struct UIEvent { fn main_thread(event_loop: EventLoopProxy, state: State, reciever: Receiver) { let mut state = state; - let mut script = script::ScriptContext::new(); + + let (script_reload_send, script_reload_recv) = channel(); + + let mut script = script::ScriptContext::new(script_reload_send.clone()); script.init(); const SPF: f64 = 1.0 / 60.0; @@ -811,6 +814,23 @@ 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(); + script = script::ScriptContext::new(script_reload_send.clone()); + script.init(); + if let Some(s) = suspend_state { + script.resume(&s); + } + } + } + { let _span = span!("update"); script.update(); diff --git a/src/script.rs b/src/script.rs index 56086f5d..fe837177 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,6 +47,8 @@ impl ModuleLoader for Loader { contents }; + let mut watcher = self.watcher.lock().unwrap(); + let _ = watcher.watch(&path, RecursiveMode::NonRecursive); Ok(ModuleSource::JavaScript(contents)) } } @@ -46,6 +58,8 @@ pub struct ScriptContext { init: Value, update: Value, draw: Value, + suspend: Value, + resume: Value, state: Value, @@ -57,9 +71,9 @@ pub struct ScriptContext { } impl ScriptContext { - pub fn new() -> Self { + pub fn new(reload_trigger: Sender<()>) -> Self { 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(); @@ -82,6 +96,12 @@ impl ScriptContext { let init = module .get_export(&context, "init") .expect("Unable to fetch init"); + let suspend = module + .get_export(&context, "suspend") + .expect("Unable to fetch suspend"); + let resume = module + .get_export(&context, "resume") + .expect("Unable to fetch suspend"); let update = module .get_export(&context, "update") .expect("Unable to fetch update"); @@ -95,6 +115,8 @@ impl ScriptContext { context, init, + suspend, + resume, update, draw, @@ -108,6 +130,42 @@ impl ScriptContext { } } + /// 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 + } + } + + /// Allow the script to restore its state after being destroyed and + /// re-created. + pub fn resume(&mut self, state: &[u8]) { + let _span = span!("script resume"); + if !self.resume.is_undefined() { + let suspend_state = self + .context + .deserialize(state) + .expect("Unable to deserialize state"); + let prev_state = self.state.clone(); + self.state = self + .resume + .call(&self.context, &[&suspend_state, &prev_state]) + .expect("Exception in resume"); + } + } + // TODO: The script could really be on a background thread you know. // 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.