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/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/lib.rs b/src/lib.rs index 8a9f2448..5f0493bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ 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}; @@ -72,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, @@ -279,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, @@ -313,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 { @@ -328,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, @@ -360,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(), }), }, } @@ -388,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 { @@ -443,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 } 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/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);