diff --git a/game/main.ts b/game/main.ts index ce813849..802378b0 100644 --- a/game/main.ts +++ b/game/main.ts @@ -1,26 +1,48 @@ -import { cls, print, spr, use_texture } from "./graphics"; +import { cls, print, spr, use_texture, Texture } from "./graphics"; import { load_texture } from "./assets"; import { since_start } from "./time"; import { btn, Button } from "./input"; +// import { load_string } from "./io"; import { new_v2, vadd, vmul, vnorm } from "./vector"; +/// TODO: Support reload by saving and restoring state from init/update/restore. + /// A nice looping frame counter. let clock = 0; -let bot_sprite: number | undefined = undefined; +let bot_sprite: Texture | undefined = undefined; // Note zelda overworld is 16x8 screens // zelda screen is 16x11 tiles // from a feeling point of view this is sufficient, apparently :D -export function init() { - print("Hello world!"); +let loaded = false; +async function load_map(path: string) { + // print("Loading map:", path); + // const blob = await load_string(path); + // const map = JSON.parse(blob); + // print("Loaded map:", map); +} + +function load_assets() { // Start this load, but then... - load_texture("./bot.png").then((n) => { + let texture_load = load_texture("./bot.png").then((n) => { print("Bot loaded at", since_start()); bot_sprite = n; }); + + let map_load = load_map("./overworld.ldtk"); + + Promise.all([texture_load, map_load]).then(() => { + loaded = true; + print("All are loaded."); + }); +} + +export function init() { + print("Hello world!"); + load_assets(); } const friction = 0.6; @@ -80,6 +102,10 @@ const robo_info = { export function draw() { cls(0.1, 0.2, 0.3); + if (!loaded) { + return; + } + if (bot_sprite != undefined) { // ...it gets resolved here? use_texture(bot_sprite); diff --git a/oden-js/src/lib.rs b/oden-js/src/lib.rs index 6d6397c5..cf11d155 100644 --- a/oden-js/src/lib.rs +++ b/oden-js/src/lib.rs @@ -16,7 +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 promise::{DefaultRejectedPromiseTracker, Promise, RejectedPromiseTracker}; pub use runtime::Runtime; pub use value::{Value, ValueRef, ValueType}; @@ -77,6 +77,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: std::str::Utf8Error) -> Self { + Error::ConversionError(e.to_string()) + } +} + pub type Result = core::result::Result; pub type ValueResult = core::result::Result; diff --git a/oden-js/src/promise.rs b/oden-js/src/promise.rs index 8ee23b26..03cabee8 100644 --- a/oden-js/src/promise.rs +++ b/oden-js/src/promise.rs @@ -1,4 +1,4 @@ -use crate::{ContextRef, ValueResult}; +use crate::{ContextRef, ValueRef, ValueResult}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::mpsc::Sender; @@ -72,3 +72,53 @@ impl Drop for Promise { assert!(self.complete); } } + +pub trait RejectedPromiseTracker { + fn on_rejected_promise( + &self, + ctx: &ContextRef, + promise: &ValueRef, + reason: &ValueRef, + is_handled: bool, + ); +} + +impl RejectedPromiseTracker for T +where + T: Fn(&ContextRef, &ValueRef, &ValueRef, bool) -> (), +{ + fn on_rejected_promise( + &self, + ctx: &ContextRef, + promise: &ValueRef, + reason: &ValueRef, + is_handled: bool, + ) { + self(ctx, promise, reason, is_handled); + } +} + +pub struct DefaultRejectedPromiseTracker {} + +impl DefaultRejectedPromiseTracker { + pub fn new() -> Self { + DefaultRejectedPromiseTracker {} + } +} + +impl RejectedPromiseTracker for DefaultRejectedPromiseTracker { + fn on_rejected_promise( + &self, + ctx: &ContextRef, + _promise: &ValueRef, + reason: &ValueRef, + is_handled: bool, + ) { + if !is_handled { + let reason_str = reason.to_string(ctx).expect( + "Unhandled rejected promise: reason unknown: unable to convert reason to string", + ); + panic!("Unhandled rejected promise: {reason_str}"); + } + } +} diff --git a/oden-js/src/runtime.rs b/oden-js/src/runtime.rs index e349d955..5c1fb4ab 100644 --- a/oden-js/src/runtime.rs +++ b/oden-js/src/runtime.rs @@ -1,7 +1,8 @@ use crate::{ module::loader::{load_module, DefaultModuleLoader, ModuleLoader}, promise::{PromiseEvent, PromiseHandle}, - ContextRef, Promise, Result, Value, + ContextRef, DefaultRejectedPromiseTracker, Promise, RejectedPromiseTracker, Result, Value, + ValueRef, }; use oden_js_sys as sys; use std::cell::{RefCell, RefMut}; @@ -45,20 +46,19 @@ struct PrivateState { promise_send: Sender<(PromiseHandle, PromiseEvent)>, promise_recv: Receiver<(PromiseHandle, PromiseEvent)>, promise_table: HashMap, // ! + rejection_tracker: Arc>, } impl PrivateState { - pub fn new(loader: T) -> Box> - where - T: ModuleLoader + 'static, - { + pub fn new() -> Box> { let (send, recv) = channel(); Box::new(RefCell::new(PrivateState { refs: 1, - loader: Arc::new(Box::new(loader)), + loader: Arc::new(Box::new(DefaultModuleLoader::new())), promise_send: send, promise_recv: recv, promise_table: HashMap::new(), + rejection_tracker: Arc::new(Box::new(DefaultRejectedPromiseTracker::new())), })) } @@ -92,6 +92,28 @@ impl PrivateState { let context = ContextRef::from_raw(ctx); load_module(&context, path, &loader) } + + unsafe extern "C" fn promise_rejection_tracker( + ctx: *mut sys::JSContext, + promise: sys::JSValue, + reason: sys::JSValue, + is_handled: i32, + opaque: *mut std::os::raw::c_void, + ) { + let ctx = ContextRef::from_raw(ctx); + let promise = ValueRef::from_raw(promise); + let reason = ValueRef::from_raw(reason); + let is_handled = is_handled != 0; + let handler = unsafe { + let ptr = opaque as *const RefCell; + ptr.as_ref() + .expect("We already know this runtime is one of ours!") + .borrow() + .rejection_tracker + .clone() + }; + handler.on_rejected_promise(&ctx, &promise, &reason, is_handled); + } } #[derive(Debug)] @@ -101,16 +123,17 @@ pub struct Runtime { impl Runtime { pub fn new() -> Runtime { - Self::with_loader(DefaultModuleLoader::new()) - } - - pub fn with_loader(loader: TLoader) -> Runtime { - let state = PrivateState::new(loader); + let state = PrivateState::new(); let rt = unsafe { let rt = sys::JS_NewRuntime(); let state = Box::into_raw(state) as *mut _; sys::JS_SetRuntimeOpaque(rt, state); sys::JS_SetModuleLoaderFunc(rt, None, Some(PrivateState::module_loader), state); + sys::JS_SetHostPromiseRejectionTracker( + rt, + Some(PrivateState::promise_rejection_tracker), + state, + ); rt }; Runtime { rt } @@ -141,6 +164,26 @@ impl Runtime { } } + /// Set the handler for loading modules. By default this is an instance + /// of `DefaultModuleLoader`. + pub fn set_module_loader(&mut self, loader: T) + where + T: ModuleLoader + 'static, + { + let mut state = unsafe { PrivateState::from_rt_mut(self.rt) }; + state.loader = Arc::new(Box::new(loader)); + } + + /// Set a tracker to be notified whenever a promise is rejected. By + /// default, this is an instance of `DefaultRejectedPromiseHandler`. + pub fn set_rejected_promise_tracker(&mut self, tracker: T) + where + T: RejectedPromiseTracker + 'static, + { + let mut state = unsafe { PrivateState::from_rt_mut(self.rt) }; + state.rejection_tracker = Arc::new(Box::new(tracker)); + } + pub fn run_gc(&mut self) { unsafe { sys::JS_RunGC(self.rt); diff --git a/src/assets.ts b/src/assets.ts index fe1dd156..b375d450 100644 --- a/src/assets.ts +++ b/src/assets.ts @@ -1,7 +1,7 @@ import * as io from "./io.ts"; import * as gfx from "./graphics.ts"; -export async function load_texture(path: string): Promise { +export async function load_texture(path: string): Promise { const buffer = await io.load(path); return gfx.create_texture(buffer, path); } diff --git a/src/graphics.ts b/src/graphics.ts index 185413b2..06ce66f3 100644 --- a/src/graphics.ts +++ b/src/graphics.ts @@ -52,6 +52,16 @@ export function spr( core.spr(x, y, w, h, sx, sy, sw, sh); } +export class Texture { + #id: number; + constructor(id: number) { + this.#id = id; + } + id(): number { + return this.#id; + } +} + /** * Create a texture based on the loaded buffer. * @@ -61,15 +71,41 @@ export function spr( export function create_texture( buffer: ArrayBuffer, label: string | undefined = undefined -): number { - return core.create_texture(buffer, label); +): Texture { + const id = core.create_texture(buffer, label); + return new Texture(id); } /** * Set the specified texture as the current texture for calls to e.g. spr(). * - * @param id - The identifier of the texture to use. + * @param texture - The texture to use. */ -export function use_texture(id: number) { - core.use_texture(id); +export function use_texture(id: Texture) { + core.use_texture(id.id()); +} + +/** + * Create a texture that we can render to. + */ +export function create_writable_texture( + width: number, + height: number, + label: string | undefined = undefined +): Texture { + return new Texture(core.create_writable_texture(width, height, label)); +} + +/** + * Set the current render target to the screen. + */ +export function write_to_screen() { + core.write_to_screen(); +} + +/** + * Set the current render target to the specified texture. + */ +export function write_to_texture(texture: Texture) { + core.write_to_texture(texture.id()); } diff --git a/src/io.ts b/src/io.ts index e95fada6..7131bc94 100644 --- a/src/io.ts +++ b/src/io.ts @@ -6,6 +6,12 @@ import * as core from "io-core"; * @param path The path of the file to load. * @returns The contents of the file. */ -export function load(path: string): Promise { - return core.load(path); -} +export const load = core.load; + +/** + * Load the specified file into memory as a string. + * + * @param path The path of the file to load. + * @returns The contents of the file decoded from utf-8. + */ +export const load_string = core.load_string; diff --git a/src/lib.rs b/src/lib.rs index 47b3c23b..5f0493bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,16 @@ use bytemuck; use std::collections::HashMap; +use std::rc::Rc; use std::sync::mpsc::Receiver; use std::time::Instant; use tracy_client::{frame_mark, set_thread_name, span}; use wgpu::util::DeviceExt; -use winit::{event::*, event_loop::EventLoop, window::Window, window::WindowBuilder}; +use winit::{ + event::*, + event_loop::{EventLoopBuilder, EventLoopProxy}, + window::Window, + window::WindowBuilder, +}; mod script; use script::graphics::GraphicsCommand; @@ -67,6 +73,8 @@ struct State { sprite_bind_group_layout: wgpu::BindGroupLayout, sprite_textures: HashMap, + write_textures: HashMap, + screen_uniform: ScreenUniforms, screen_uniform_buffer: wgpu::Buffer, screen_uniform_bind_group: wgpu::BindGroup, @@ -274,6 +282,7 @@ impl State { max_vertices, sprite_bind_group_layout, sprite_textures: HashMap::new(), + write_textures: HashMap::new(), screen_uniform, screen_uniform_buffer, screen_uniform_bind_group, @@ -308,14 +317,19 @@ impl State { fn render(&mut self, commands: Vec) -> Result<(), wgpu::SurfaceError> { let _span = span!("context render"); let output = self.surface.get_current_texture()?; - let view = output - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); // Group the commands into passes. + let screen_view = Rc::new( + output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()), + ); + let mut last_view = screen_view.clone(); + struct Pass { color: Option<[f64; 4]>, commands: Vec, + target: Rc, } let mut passes = Vec::new(); for command in commands { @@ -323,7 +337,9 @@ impl State { GraphicsCommand::Clear(cc) => passes.push(Pass { color: Some(cc.color), commands: Vec::new(), + target: last_view.clone(), }), + GraphicsCommand::CreateTexture(ct) => { let texture = texture::Texture::from_image( &self.device, @@ -355,12 +371,93 @@ impl State { self.sprite_textures.insert(ct.id, sprite_bind_group); } + + GraphicsCommand::CreateWritableTexture { + id, + width, + height, + label, + } => { + let texture = texture::Texture::new_writable( + &self.device, + width, + height, + match &label { + Some(l) => Some(&l), + None => None, + }, + ); + let sprite_bind_group = + self.device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &self.sprite_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&texture.view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&texture.sampler), + }, + ], + label: match &label { + Some(l) => Some(&l), + None => None, + }, + }); + + self.sprite_textures.insert(id, sprite_bind_group); + self.write_textures.insert(id, texture); + } + + GraphicsCommand::WriteToTexture(id) => { + let texture = self.write_textures.get(&id).unwrap(); + last_view = Rc::new( + texture + .texture + .create_view(&wgpu::TextureViewDescriptor::default()), + ); + let new_pass = match passes.last_mut() { + Some(pass) => { + if pass.commands.is_empty() { + pass.target = last_view.clone(); + false + } else { + true + } + } + None => true, + }; + if new_pass { + passes.push(Pass { + color: None, + commands: vec![], + target: last_view.clone(), + }) + } + } + + GraphicsCommand::WriteToScreen => { + if !Rc::ptr_eq(&last_view, &screen_view) { + // If I have a pass already I need a new one. + last_view = screen_view.clone(); + if !passes.is_empty() { + passes.push(Pass { + color: None, + commands: vec![], + target: last_view.clone(), + }) + } + } + } + GraphicsCommand::EndFrame => (), other => match passes.last_mut() { Some(pass) => pass.commands.push(other), None => passes.push(Pass { color: None, commands: vec![other], + target: last_view.clone(), }), }, } @@ -383,7 +480,7 @@ impl State { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Render Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, + view: &pass.target, resolve_target: None, ops: wgpu::Operations { load: if let Some([r, g, b, a]) = pass.color { @@ -438,7 +535,11 @@ impl State { } GraphicsCommand::UseTexture(id) => texture_id = Some(id), + GraphicsCommand::CreateTexture(_) => (), // Already handled + GraphicsCommand::CreateWritableTexture { .. } => (), // Already handled + GraphicsCommand::WriteToTexture(_) => (), // Already handled + GraphicsCommand::WriteToScreen => (), // Already handled GraphicsCommand::Clear(_) => (), // Already handled GraphicsCommand::EndFrame => (), // Should never appear } @@ -472,15 +573,19 @@ impl State { } } +enum OdenEvent { + Close, +} + struct UIEvent { - winit: Event<'static, ()>, + winit: Event<'static, OdenEvent>, #[allow(unused)] time: Instant, } // TODO: flume? (https://docs.rs/flume/latest/flume/) -fn main_thread(state: State, reciever: Receiver) { +fn main_thread(event_loop: EventLoopProxy, state: State, reciever: Receiver) { let mut state = state; let mut script = script::ScriptContext::new(); script.init(); @@ -500,6 +605,10 @@ fn main_thread(state: State, reciever: Receiver) { } if window_id == state.window().id() => { if !script.input(event) { match event { + WindowEvent::CloseRequested => { + let _ = event_loop.send_event(OdenEvent::Close); + } + WindowEvent::CursorMoved { position, .. } => { state.mouse_x = position.x; state.mouse_y = position.y; @@ -557,27 +666,35 @@ pub async fn run() { set_thread_name!("ui thread"); env_logger::init(); - let event_loop = EventLoop::new(); + let event_loop = EventLoopBuilder::::with_user_event().build(); let window = WindowBuilder::new().build(&event_loop).unwrap(); + let event_loop_proxy = event_loop.create_proxy(); let state = State::new(window).await; let (sender, reciever) = std::sync::mpsc::channel(); std::thread::spawn(move || { set_thread_name!("game thread"); - main_thread(state, reciever); + main_thread(event_loop_proxy, state, reciever); }); event_loop.run(move |event, _, control_flow| { control_flow.set_wait(); - if let Some(e) = event.to_static() { - sender - .send(UIEvent { - winit: e, - time: Instant::now(), - }) - .unwrap(); + match event { + Event::UserEvent(OdenEvent::Close) => { + control_flow.set_exit(); + } + _ => { + if let Some(e) = event.to_static() { + sender + .send(UIEvent { + winit: e, + time: Instant::now(), + }) + .unwrap(); + } + } } }); } diff --git a/src/script.rs b/src/script.rs index 4d38a705..5d9b0c2e 100644 --- a/src/script.rs +++ b/src/script.rs @@ -56,7 +56,8 @@ pub struct ScriptContext { impl ScriptContext { pub fn new() -> Self { - let runtime = Runtime::with_loader(Loader::new()); + let mut runtime = Runtime::new(); + runtime.set_module_loader(Loader::new()); let mut context = Context::new(runtime); context.add_intrinsic_bigfloat(); diff --git a/src/script/graphics.rs b/src/script/graphics.rs index b3efe8d0..b55b5d93 100644 --- a/src/script/graphics.rs +++ b/src/script/graphics.rs @@ -40,7 +40,15 @@ pub enum GraphicsCommand { Print(PrintCommand), Sprite(SpriteCommand), CreateTexture(CreateTextureCommand), + CreateWritableTexture { + id: u32, + width: u32, + height: u32, + label: Option, + }, UseTexture(u32), + WriteToTexture(u32), + WriteToScreen, EndFrame, } @@ -109,6 +117,31 @@ impl GraphicsImpl { fn use_texture(&self, id: u32) { let _ = self.sender.send(GraphicsCommand::UseTexture(id)); } + + fn create_writable_texture( + &self, + width: u32, + height: u32, + label: Option, + ) -> Result { + let id = self.next_texture_id.fetch_add(1, Ordering::SeqCst); + let _ = self.sender.send(GraphicsCommand::CreateWritableTexture { + id, + width, + height, + label, + }); + + Ok(id) + } + + fn write_to_screen(&self) { + let _ = self.sender.send(GraphicsCommand::WriteToScreen); + } + + fn write_to_texture(&self, id: u32) { + let _ = self.sender.send(GraphicsCommand::WriteToTexture(id)); + } } pub struct GraphicsAPI { @@ -168,6 +201,32 @@ impl GraphicsAPI { )?, )?; } + { + let gfx = gfx.clone(); + builder.export( + "create_writable_texture", + ctx.new_fn( + move |_: &ContextRef, width: u32, height: u32, label: Option| { + gfx.create_writable_texture(width, height, label) + }, + )?, + )?; + } + { + let gfx = gfx.clone(); + builder.export( + "write_to_screen", + ctx.new_fn(move |_: &ContextRef| gfx.write_to_screen())?, + )?; + } + { + let gfx = gfx.clone(); + builder.export( + "write_to_texture", + ctx.new_fn(move |_: &ContextRef, id: u32| gfx.write_to_texture(id))?, + )?; + } + builder.build("graphics-core")?; Ok(GraphicsAPI { gfx }) } diff --git a/src/script/io.rs b/src/script/io.rs index d508c040..ba3521fc 100644 --- a/src/script/io.rs +++ b/src/script/io.rs @@ -82,6 +82,24 @@ impl IoImpl { Ok(value) } + + fn load_string(&self, context: &ContextRef, path: &str) -> ValueResult { + let (value, promise) = context.new_promise()?; + + let path = path.to_string(); + self.thread_pool.execute(Box::new(move || { + let path = path; + + let result = resolve_path(&path, &[]).and_then(|p| std::fs::read(p)); + promise.resolve(move |ctx: &ContextRef| { + let result = result?; + let string = std::str::from_utf8(&result)?; + ctx.new_string(string) + }); + })); + + Ok(value) + } } pub struct IoAPI {} @@ -97,6 +115,13 @@ impl IoAPI { ctx.new_fn(move |ctx: &ContextRef, p: String| io.load(ctx, &p))?, )?; } + { + let io = io.clone(); + builder.export( + "load_string", + ctx.new_fn(move |ctx: &ContextRef, p: String| io.load_string(ctx, &p))?, + )?; + } builder.build("io-core")?; Ok(IoAPI {}) } diff --git a/src/texture.rs b/src/texture.rs index eb1407e0..ae0e20ef 100644 --- a/src/texture.rs +++ b/src/texture.rs @@ -7,15 +7,46 @@ pub struct Texture { } impl Texture { - // pub fn from_bytes( - // device: &wgpu::Device, - // queue: &wgpu::Queue, - // bytes: &[u8], - // label: &str, - // ) -> Result { - // let img = image::load_from_memory(bytes)?; - // Ok(Self::from_image(device, queue, &img, Some(label))) - // } + pub fn new_writable( + device: &wgpu::Device, + width: u32, + height: u32, + label: Option<&str>, + ) -> Self { + let size = wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }; + + let texture = device.create_texture(&wgpu::TextureDescriptor { + label, + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + Self { + texture, + view, + sampler, + } + } pub fn from_image( device: &wgpu::Device, @@ -31,6 +62,7 @@ impl Texture { height: dimensions.1, depth_or_array_layers: 1, }; + let texture = device.create_texture(&wgpu::TextureDescriptor { label, size, diff --git a/types/graphics-core.d.ts b/types/graphics-core.d.ts index 3fde39fd..45cf9b2c 100644 --- a/types/graphics-core.d.ts +++ b/types/graphics-core.d.ts @@ -1,7 +1,9 @@ // These are the functions exposed by the native graphics module. // export function cls(r: number, g: number, b: number); + export function print(msg: string); + export function spr( x: number, y: number, @@ -12,8 +14,20 @@ export function spr( sw: number, sh: number ); + export function create_texture( buffer: ArrayBuffer, label: string | undefined ): number; + export function use_texture(id: number); + +export function create_writable_texture( + width: number, + height: number, + label: string | undefined +): number; + +export function write_to_screen(); + +export function write_to_texture(id: number); diff --git a/types/io-core.d.ts b/types/io-core.d.ts index e5114f02..283b70df 100644 --- a/types/io-core.d.ts +++ b/types/io-core.d.ts @@ -1,3 +1,5 @@ // These are the functions exposed by the native IO module. // export function load(path: string): Promise; + +export function load_string(path: string): Promise;