Compare commits

..

4 commits

Author SHA1 Message Date
043a3ee183 [game] Do some amount of snapshot save and restore
This is super jankety and I feel the need for organization but FOR NOW
let's hack away.
2023-08-19 18:40:39 -07:00
e32643486d [oden] Tolerate bad scripts on hot reload
When the script changes from under us it might be bugged for some
reason; just let that be for now, ignore the load, and hopefully the
engineer will fix it, eventually.
2023-08-19 18:39:42 -07:00
d79b891b7b [oden] Tolerate parse errors better 2023-08-19 18:39:16 -07:00
a850c3cc58 [oden] Hot-reload script files 2023-08-19 16:54:50 -07:00
9 changed files with 272 additions and 50 deletions

99
Cargo.lock generated
View file

@ -434,6 +434,25 @@ dependencies = [
"cfg-if", "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]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@ -554,6 +573,18 @@ dependencies = [
"simd-adler32", "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]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.26" version = "1.0.26"
@ -627,6 +658,15 @@ dependencies = [
"syn 2.0.18", "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]] [[package]]
name = "generator" name = "generator"
version = "0.7.5" version = "0.7.5"
@ -828,6 +868,26 @@ dependencies = [
"hashbrown", "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]] [[package]]
name = "instant" name = "instant"
version = "0.1.12" version = "0.1.12"
@ -917,6 +977,26 @@ dependencies = [
"pkg-config", "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]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@ -1247,6 +1327,24 @@ dependencies = [
"minimal-lexical", "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]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.46.0" version = "0.46.0"
@ -1393,6 +1491,7 @@ dependencies = [
"env_logger", "env_logger",
"image", "image",
"log", "log",
"notify",
"oden-js", "oden-js",
"pollster", "pollster",
"swc_common", "swc_common",

View file

@ -14,6 +14,7 @@ bytemuck = { version = "1.13", features = ["derive"] }
env_logger = "0.10" env_logger = "0.10"
image = { version = "0.24", default-features = false, features = ["png"] } image = { version = "0.24", default-features = false, features = ["png"] }
log = "0.4" log = "0.4"
notify = "6"
oden-js = { path = "oden-js" } oden-js = { path = "oden-js" }
pollster = "0.3" pollster = "0.3"
swc_common = "0.31.16" swc_common = "0.31.16"

View file

@ -49,6 +49,20 @@ export function init() {
load_assets(); 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; const friction = 0.6;
let robo_vel = new_v2(0); let robo_vel = new_v2(0);
let robo_pos = new_v2(10); let robo_pos = new_v2(10);

View file

@ -8,8 +8,9 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
bitflags = "1" bitflags = "1"
thiserror = "1"
oden-js-sys = {path = "../oden-js-sys"} oden-js-sys = {path = "../oden-js-sys"}
thiserror = "1"
[dev-dependencies] [dev-dependencies]
assert_matches = "1.5.0" assert_matches = "1.5.0"

View file

@ -396,6 +396,23 @@ impl ContextRef {
pub fn process_all_jobs(&self) -> Result<()> { pub fn process_all_jobs(&self) -> Result<()> {
self.get_runtime().process_all_jobs() 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<Value> {
self.check_exception(unsafe {
sys::JS_ReadObject(
self.ctx,
data.as_ptr(),
data.len(),
sys::JS_READ_OBJ_REFERENCE as i32,
)
})
}
} }
#[derive(Debug)] #[derive(Debug)]

View file

@ -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<Vec<u8>> {
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 { impl fmt::Debug for ValueRef {

View file

@ -1,7 +1,7 @@
use bytemuck; use bytemuck;
use std::collections::HashMap; use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;
use std::sync::mpsc::Receiver; use std::sync::mpsc::{channel, Receiver};
use std::time::Instant; use std::time::Instant;
use tracy_client::{frame_mark, set_thread_name, span}; use tracy_client::{frame_mark, set_thread_name, span};
use wgpu::util::DeviceExt; use wgpu::util::DeviceExt;
@ -765,8 +765,11 @@ struct UIEvent {
fn main_thread(event_loop: EventLoopProxy<OdenEvent>, state: State, reciever: Receiver<UIEvent>) { fn main_thread(event_loop: EventLoopProxy<OdenEvent>, state: State, reciever: Receiver<UIEvent>) {
let mut state = state; 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; const SPF: f64 = 1.0 / 60.0;
loop { loop {
@ -811,6 +814,22 @@ fn main_thread(event_loop: EventLoopProxy<OdenEvent>, 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"); let _span = span!("update");
script.update(); script.update();

View file

@ -1,9 +1,11 @@
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use oden_js::{ use oden_js::{
module::loader::{ModuleLoader, ModuleSource}, module::loader::{ModuleLoader, ModuleSource},
Context, ContextRef, Result, Runtime, Value, Context, ContextRef, Result, Runtime, Value,
}; };
use std::ffi::OsStr; 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 std::time::Instant;
use tracy_client::span; use tracy_client::span;
use winit::event::*; use winit::event::*;
@ -18,11 +20,19 @@ use graphics::GraphicsCommand;
mod typescript; mod typescript;
use typescript::transpile_to_javascript; use typescript::transpile_to_javascript;
struct Loader {} struct Loader {
watcher: Arc<Mutex<RecommendedWatcher>>,
}
impl Loader { impl Loader {
pub fn new() -> Loader { pub fn new(reload_trigger: Sender<()>) -> Loader {
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 contents
}; };
let mut watcher = self.watcher.lock().unwrap();
let _ = watcher.watch(&path, RecursiveMode::NonRecursive);
Ok(ModuleSource::JavaScript(contents)) Ok(ModuleSource::JavaScript(contents))
} }
} }
pub struct ScriptContext { pub struct ScriptContext {
context: Context, context: Context,
init: Value,
update: Value, update: Value,
draw: Value, draw: Value,
suspend: Value,
state: Value, state: Value,
@ -57,9 +69,9 @@ pub struct ScriptContext {
} }
impl ScriptContext { impl ScriptContext {
pub fn new() -> Self { pub fn new(suspend_state: Option<Vec<u8>>, reload_trigger: Sender<()>) -> Result<Self> {
let mut runtime = Runtime::new(); 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); let mut context = Context::new(runtime);
context.add_intrinsic_bigfloat(); context.add_intrinsic_bigfloat();
@ -68,33 +80,31 @@ impl ScriptContext {
let (gfx_send, gfx_receive) = channel(); let (gfx_send, gfx_receive) = channel();
let gfx = graphics::GraphicsAPI::define(&context, gfx_send.clone()) let gfx = graphics::GraphicsAPI::define(&context, gfx_send.clone())?;
.expect("Graphics module should load without error"); let _io = io::IoAPI::define(&context)?;
let _io = io::IoAPI::define(&context).expect("IO module should load without error"); let time = time::TimeAPI::define(&context)?;
let time = time::TimeAPI::define(&context).expect("Time module should load without error"); let input = input::InputAPI::define(&context)?;
let input =
input::InputAPI::define(&context).expect("Input module should load without error");
let module = context let module = context.import_module("./main.ts", "")?;
.import_module("./main.ts", "")
.expect("Unable to load main");
let init = module let init = module.get_export(&context, "init")?;
.get_export(&context, "init") let suspend = module.get_export(&context, "suspend")?;
.expect("Unable to fetch init"); let resume = module.get_export(&context, "resume")?;
let update = module let update = module.get_export(&context, "update")?;
.get_export(&context, "update") let draw = module.get_export(&context, "draw")?;
.expect("Unable to fetch update");
let draw = module
.get_export(&context, "draw")
.expect("Unable to fetch 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, context,
init, suspend,
update, update,
draw, draw,
@ -105,6 +115,25 @@ impl ScriptContext {
time, time,
input, input,
})
}
/// Allow the script to save its state before we destroy it and re-create
/// it.
pub fn suspend(&self) -> Option<Vec<u8>> {
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 // 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. // 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 { pub fn input(&mut self, event: &WindowEvent) -> bool {
match event { match event {
WindowEvent::KeyboardInput { input, .. } => self.input.handle_keyboard_input(input), WindowEvent::KeyboardInput { input, .. } => self.input.handle_keyboard_input(input),

View file

@ -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<Item = &'a Diagnostic>,
) -> Error {
Error::ParseError(
name.into(),
diagnostics
.map(|d| format_swc_diagnostic(source_map, d))
.collect::<Vec<_>>()
.join("\n\n"),
)
}
fn ensure_no_fatal_swc_diagnostics<'a>( fn ensure_no_fatal_swc_diagnostics<'a>(
name: &str, name: &str,
source_map: &SourceMap, source_map: &SourceMap,
@ -66,13 +80,10 @@ fn ensure_no_fatal_swc_diagnostics<'a>(
.filter(|d| is_fatal_swc_diagnostic(d)) .filter(|d| is_fatal_swc_diagnostic(d))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if !fatal_diagnostics.is_empty() { if !fatal_diagnostics.is_empty() {
Err(Error::ParseError( Err(diagnostics_to_parse_error(
name.into(), name,
fatal_diagnostics source_map,
.iter() fatal_diagnostics.into_iter(),
.map(|d| format_swc_diagnostic(source_map, d))
.collect::<Vec<_>>()
.join("\n\n"),
)) ))
} else { } else {
Ok(()) Ok(())
@ -109,8 +120,12 @@ pub fn transpile_to_javascript(path: &str, input: String) -> Result<String> {
let module = parser let module = parser
.parse_module() .parse_module()
.map_err(|e| e.into_diagnostic(&handler).emit()) .map_err(|e| e.into_diagnostic(&handler))
.expect("failed to parse module."); .map_err(|mut e| {
e.emit();
let diagnostics = diagnostics_cell.borrow();
diagnostics_to_parse_error(path, &cm, diagnostics.iter())
})?;
let globals = Globals::default(); let globals = Globals::default();
GLOBALS.set(&globals, || { GLOBALS.set(&globals, || {