diff --git a/oden-js/src/context.rs b/oden-js/src/context.rs index ae994378..b11b82df 100644 --- a/oden-js/src/context.rs +++ b/oden-js/src/context.rs @@ -1,6 +1,6 @@ use crate::{ - callback::new_fn, conversion::RustFunction, module::Module, Atom, ClassID, Error, Result, - Runtime, Value, ValueRef, ValueResult, + callback::new_fn, conversion::RustFunction, module::Module, Atom, ClassID, Error, Promise, + Result, Runtime, Value, ValueRef, ValueResult, }; use bitflags::bitflags; use oden_js_sys as sys; @@ -34,6 +34,7 @@ pub struct ContextRef { pub(crate) ctx: *mut sys::JSContext, } +// TODO: Should all these require mutability to enforce single-threadedness? impl ContextRef { pub(crate) fn from_raw(ctx: *mut sys::JSContext) -> Self { ContextRef { ctx } @@ -277,6 +278,64 @@ impl ContextRef { Value::from_raw(v, self) } + /// Construct a new promise. + pub fn new_promise(&self) -> Result { + unsafe { + let mut resolving_funcs: [sys::JSValue; 2] = + [sys::JS_MakeUndefined(), sys::JS_MakeUndefined()]; + let val = + sys::JS_NewPromiseCapability(self.ctx, &mut resolving_funcs as *mut sys::JSValue); + + if sys::JS_ValueGetTag(val) == sys::JS_TAG_EXCEPTION { + Err(self.exception_error()) + } else { + Ok(Promise::new( + Value::from_raw(val, self), + Value::from_raw(resolving_funcs[0], self), + Value::from_raw(resolving_funcs[1], self), + )) + } + } + } + + /// Construct a new exception object, suitable for throwing. + pub fn new_error(&self, message: &str) -> Value { + let e = match self.new_string(message) { + Ok(e) => e, + Err(_) => match self.new_string("INTERNAL ERROR: Embedded NUL in message") { + Ok(e) => e, + + // Faulting this hard is inexcusable. + Err(_) => return self.exception(), + }, + }; + unsafe { + let err = Value::from_raw(sys::JS_NewError(self.ctx), self); + + // NOTE: Throughout this function we work at the lower-level + // error handling stuff because the errors are easier to + // manage. (We know how it can fail!) + if sys::JS_ValueGetTag(err.val) == sys::JS_TAG_EXCEPTION { + // GIVE UP; This is out of memory anyway things probably + // went wrong because of that. We'll return *that* + // exception. + return self.exception(); + } + + sys::JS_DupValue(self.ctx, e.val); // SetProperty takes ownership. + let prop = CString::new("message").unwrap(); + if sys::JS_SetPropertyStr(self.ctx, err.val, prop.as_ptr(), e.val) == -1 { + // As before, we're just going to take the exception from + // the context, and drop the one we were trying to create + // on the floor. + return self.exception(); + } + + // We put the message in, we can return the value. + err + } + } + /// Fetch the global object for the context. pub fn global_object(&self) -> ValueResult { self.check_exception(unsafe { sys::JS_GetGlobalObject(self.ctx) }) @@ -315,6 +374,23 @@ impl ContextRef { Error::Exception(exc, desc, stack) } + + /// Process all pending async jobs. This includes all promise resolutions. + pub fn process_all_jobs(&self) -> Result<()> { + // TODO: SAFETY + // This is unsafe because multiple contexts can be sharing the same runtime and cause + // a race condition on the underlying runtime. + loop { + let mut ctx1: *mut sys::JSContext = std::ptr::null_mut(); + let err = unsafe { sys::JS_ExecutePendingJob(sys::JS_GetRuntime(self.ctx), &mut ctx1) }; + if err == 0 { + break; + } else if err < 0 { + return Err(ContextRef::from_raw(ctx1).exception_error()); + } + } + Ok(()) + } } #[derive(Debug)] diff --git a/oden-js/src/lib.rs b/oden-js/src/lib.rs index 3246ecbc..1784abb6 100644 --- a/oden-js/src/lib.rs +++ b/oden-js/src/lib.rs @@ -8,6 +8,7 @@ mod class; mod context; mod conversion; pub mod module; +mod promise; mod runtime; mod value; @@ -15,6 +16,7 @@ pub use atom::{Atom, AtomRef}; pub use class::{Class, ClassID}; pub use context::{Context, ContextRef, EvalFlags}; pub use conversion::*; +pub use promise::Promise; pub use runtime::Runtime; pub use value::{Value, ValueRef, ValueType}; @@ -49,6 +51,20 @@ pub enum Error { ParseError(String, String), } +impl Error { + // Convert the error into an exception-type object which can be + // thrown. This is *different* from try_into_value which just propagates + // the error. + pub fn to_js_error(&self, context: &ContextRef) -> Value { + if let Error::Exception(e, _, _) = self { + e.clone() + } else { + let message = self.to_string(); + context.new_error(&message) + } + } +} + impl From for Error { fn from(_: NulError) -> Self { Error::UnexpectedNul @@ -75,35 +91,9 @@ pub(crate) fn throw_error(context: &ContextRef, error: Error) -> sys::JSValue { } pub(crate) fn throw_string(context: &ContextRef, message: String) -> sys::JSValue { - let ctx = context.ctx; - match context.new_string(&message) { - Ok(e) => unsafe { - // Because context.new_string yields an owned Value, and will - // clean it up on the way out, we need to explicitly DupValue a - // reference for the `Throw` to own. - let err = sys::JS_NewError(ctx); - if sys::JS_ValueGetTag(err) == sys::JS_TAG_EXCEPTION { - // GIVE UP; this is out of memory anyway things probably went - // wrong because of that. - return err; - } - - sys::JS_DupValue(ctx, e.val); // SetProperty takes ownership. - let prop = CString::new("message").unwrap(); - if sys::JS_SetPropertyStr(ctx, err, prop.as_ptr(), e.val) == -1 { - // Also an out of memory but we need to free the error object - // on our way out. - sys::JS_FreeValue(ctx, err); - return sys::JS_MakeException(); // JS_EXCEPTION - } - - sys::JS_Throw(ctx, err) - }, - Err(_) => unsafe { - sys::JS_Throw( - ctx, - sys::JS_NewString(ctx, "Errors within errors: embedded nulls in the description of the error that occurred".as_bytes().as_ptr() as *const i8), - ) - }, + let err = context.new_error(&message); + unsafe { + sys::JS_DupValue(context.ctx, err.val); + sys::JS_Throw(context.ctx, err.val) } } diff --git a/oden-js/src/promise.rs b/oden-js/src/promise.rs new file mode 100644 index 00000000..152085b2 --- /dev/null +++ b/oden-js/src/promise.rs @@ -0,0 +1,34 @@ +use crate::{ContextRef, Value, ValueRef}; + +#[derive(Debug, Clone)] +pub struct Promise { + pub object: Value, + pub resolve_fn: Value, + pub reject_fn: Value, +} + +impl Promise { + pub(crate) fn new(object: Value, resolve_fn: Value, reject_fn: Value) -> Self { + Promise { + object, + resolve_fn, + reject_fn, + } + } + + pub fn dup(&self, ctx: &ContextRef) -> Self { + Promise { + object: self.object.dup(ctx), + resolve_fn: self.resolve_fn.dup(ctx), + reject_fn: self.reject_fn.dup(ctx), + } + } + + pub fn resolve(self, context: &ContextRef, value: &ValueRef) { + let _ = self.resolve_fn.call(context, &[value]); + } + + pub fn reject(self, context: &ContextRef, value: &ValueRef) { + let _ = self.reject_fn.call(context, &[value]); + } +} diff --git a/oden-js/src/value.rs b/oden-js/src/value.rs index 58a42134..dfc8d65c 100644 --- a/oden-js/src/value.rs +++ b/oden-js/src/value.rs @@ -310,14 +310,16 @@ impl ValueRef { Ok(result) } - pub fn call(&self, ctx: &ContextRef) -> Result { + pub fn call(&self, ctx: &ContextRef, args: &[&ValueRef]) -> Result { + // TODO: There *must* be a way to avoid this allocation. + let mut args: Vec = args.iter().map(|v| v.val).collect(); unsafe { ctx.check_exception(sys::JS_Call( ctx.ctx, self.val, sys::JS_MakeUndefined(), - 0, - std::ptr::null_mut(), + args.len() as i32, + args.as_mut_ptr(), )) } } @@ -345,9 +347,16 @@ impl Value { /// the runtime of the specified context, if not the context itself. This /// function makes no attempt to validate this. pub(crate) fn from_raw(val: sys::JSValue, ctx: &ContextRef) -> Self { + Value::from_raw_rt( + val, + Runtime::from_raw(unsafe { sys::JS_GetRuntime(ctx.ctx) }), + ) + } + + pub(crate) fn from_raw_rt(val: sys::JSValue, rt: Runtime) -> Self { Value { value: ValueRef::from_raw(val), - rt: Runtime::from_raw(unsafe { sys::JS_GetRuntime(ctx.ctx) }), + rt, } } @@ -394,6 +403,15 @@ impl fmt::Debug for Value { } } +impl Clone for Value { + fn clone(&self) -> Self { + unsafe { + sys::JS_DupValueRT(self.rt.rt, self.val); + } + Value::from_raw_rt(self.val, self.rt.clone()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/lib.rs b/src/lib.rs index f44bc109..8577050a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -178,22 +178,6 @@ impl State { label: Some("sprite_bind_group_layout"), }); - // TODO: DELETE THIS - // let sprite_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - // layout: &sprite_bind_group_layout, - // entries: &[ - // wgpu::BindGroupEntry { - // binding: 0, - // resource: wgpu::BindingResource::TextureView(&diffuse_texture.view), - // }, - // wgpu::BindGroupEntry { - // binding: 1, - // resource: wgpu::BindingResource::Sampler(&diffuse_texture.sampler), - // }, - // ], - // label: Some("diffuse_bind_group"), - // }); - let screen_uniform = ScreenUniforms::new(size.width, size.height); let screen_uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Screen Uniform Buffer"), @@ -205,7 +189,7 @@ impl State { device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { entries: &[wgpu::BindGroupLayoutEntry { binding: 0, - visibility: wgpu::ShaderStages::VERTEX, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, @@ -225,28 +209,28 @@ impl State { label: Some("camera_bind_group"), }); - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Shader"), - source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), + let sprite_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Sprite Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("sprite_shader.wgsl").into()), }); - let render_pipeline_layout = + let sprite_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Render Pipeline Layout"), + label: Some("Sprite Pipeline Layout"), bind_group_layouts: &[&sprite_bind_group_layout, &screen_uniform_bind_group_layout], push_constant_ranges: &[], }); - let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Render Pipeline"), - layout: Some(&render_pipeline_layout), + let sprite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Sprite Pipeline"), + layout: Some(&sprite_pipeline_layout), vertex: wgpu::VertexState { - module: &shader, + module: &sprite_shader, entry_point: "vs_main", buffers: &[Vertex::desc()], }, fragment: Some(wgpu::FragmentState { - module: &shader, + module: &sprite_shader, entry_point: "fs_main", targets: &[Some(wgpu::ColorTargetState { format: config.format, @@ -292,7 +276,7 @@ impl State { queue, config, size, - render_pipeline, + render_pipeline: sprite_pipeline, vertex_buffer, max_vertices, sprite_bind_group_layout, @@ -529,7 +513,7 @@ pub async fn run() { let mut state = State::new(window).await; - let context = script::ScriptContext::new(); + let mut context = script::ScriptContext::new(); context.init(); event_loop.run(move |event, _, control_flow| { diff --git a/src/script.rs b/src/script.rs index d1bb9828..496b6a5d 100644 --- a/src/script.rs +++ b/src/script.rs @@ -1,9 +1,11 @@ use oden_js::{ module::loader::{ModuleLoader, ModuleSource}, - Context, ContextRef, Result, Runtime, Value, + Context, ContextRef, Promise, Result, Runtime, Value, ValueResult, }; +use std::collections::HashMap; use std::ffi::OsStr; use std::path::Path; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::mpsc::{channel, Receiver}; pub mod graphics; @@ -37,6 +39,21 @@ impl ModuleLoader for Loader { } } +#[derive(Eq, PartialEq, Hash, Debug, Clone, Copy)] +pub struct PromiseHandle(u64); + +impl PromiseHandle { + pub fn new() -> Self { + static NEXT_ID: AtomicU64 = AtomicU64::new(0); + PromiseHandle(NEXT_ID.fetch_add(1, Ordering::SeqCst)) + } +} + +pub enum ScriptEvent { + AddPromise(PromiseHandle, Promise), + CompletePromise(PromiseHandle, Box ValueResult>), +} + pub struct ScriptContext { context: Context, init: Value, @@ -46,6 +63,9 @@ pub struct ScriptContext { gfx: graphics::GraphicsAPI, _assets: assets::AssetsAPI, gfx_receive: Receiver, + + script_receive: Receiver, + promises: HashMap, } impl ScriptContext { @@ -58,6 +78,7 @@ impl ScriptContext { context.add_intrinsic_operators(); let (gfx_send, gfx_receive) = channel(); + let (script_send, script_receive) = channel(); let gfx = graphics::GraphicsAPI::define(&context, gfx_send.clone()) .expect("Graphics module should load without error"); @@ -89,6 +110,9 @@ impl ScriptContext { gfx_receive, _assets: assets, + + script_receive, + promises: HashMap::new(), } } @@ -96,18 +120,52 @@ 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(&self) { - self.init.call(&self.context).expect("Exception in init"); + pub fn init(&mut self) { + self.init + .call(&self.context, &[]) + .expect("Exception in init"); } - pub fn update(&self) { + pub fn update(&mut self) { + // Handle any promises that have completed before calling update. + while let Ok(event) = self.script_receive.try_recv() { + match event { + // TODO: Capture debugging information. + ScriptEvent::AddPromise(handle, promise) => { + self.promises.insert(handle, promise); + } + ScriptEvent::CompletePromise(handle, value_producer) => { + if let Some(promise) = self.promises.remove(&handle) { + let result = value_producer(&self.context); + match result { + Ok(v) => { + promise.resolve(&self.context, &v); + } + Err(e) => { + let error = e.to_js_error(&self.context); + promise.reject(&self.context, &error); + } + } + } + } + } + } + + // Tell the runtime to process all pending "jobs". + self.context + .process_all_jobs() + .expect("Error processing async jobs"); + + // Now run the update function. self.update - .call(&self.context) + .call(&self.context, &[]) .expect("Exception in update"); } - pub fn render(&self) -> Vec { - self.draw.call(&self.context).expect("Exception in draw"); + pub fn render(&mut self) -> Vec { + self.draw + .call(&self.context, &[]) + .expect("Exception in draw"); self.gfx.end_frame(); let mut commands = Vec::new(); diff --git a/src/shader.wgsl b/src/shader.wgsl index 06cfa782..a1ab6a39 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -48,13 +48,30 @@ const RES = vec2f(320.0, 240.0); // The logical resolution of the screen. return out; } -// Fragment shader +// Fragment shader.... @group(0) @binding(0) var t_diffuse : texture_2d; @group(0) @binding(1) var s_diffuse : sampler; @fragment fn fs_main(in : VertexOutput)->@location(0) vec4 { + // The "screen" is centered in the window, so anything outside of the + // screen borders should be black. But *where are they*? + let RES_AR = RES.x / RES.y; // The aspect ratio of the logical screen. + let screen_ar = screen.resolution.x / screen.resolution.y; + var black_mod = 1.0; + if (screen_ar > RES_AR) { + // Wider than tall, bars are on the left and right. + let active_width = screen.resolution.y * RES_AR; + let half_delta = (screen.resolution.x - active_width) / 2.0; + if (in.clip_position.x < half_delta || + in.clip_position.x > half_delta + active_width) { + black_mod = 0.0; + } + } else { + // Taller than wide, bars are on top and bottom. + } + let dims = vec2f(textureDimensions(t_diffuse)); - return textureSample(t_diffuse, s_diffuse, in.tex_coords / dims); + return black_mod * textureSample(t_diffuse, s_diffuse, in.tex_coords / dims); }