From f9648d88cd1e67654815ff969207351fcb2b6de7 Mon Sep 17 00:00:00 2001 From: John Doty Date: Mon, 7 Aug 2023 10:05:24 -0700 Subject: [PATCH] [oden][game] Draw the world This involved basically a giant rewrite of the renderer because I now need to share the vertex buffer across textures and it is *a lot* let me tell you. There's like a vertical seam which I don't understand yet. --- game/bot.aseprite | Bin 4251 -> 3993 bytes game/level.ts | 141 ++++++++ game/main.ts | 26 +- game/overworld.aseprite | Bin 0 -> 1068 bytes game/overworld.ldtk | 612 ++++++++++++++++---------------- game/overworld.png | Bin 0 -> 543 bytes src/lib.rs | 752 +++++++++++++++++++++++++--------------- 7 files changed, 936 insertions(+), 595 deletions(-) create mode 100644 game/level.ts create mode 100644 game/overworld.aseprite create mode 100644 game/overworld.png diff --git a/game/bot.aseprite b/game/bot.aseprite index 31b78edc88e293202658c4f099723d50d95ed4f4..de16c1e10481c8370c6cc115abd01826101bd3b1 100644 GIT binary patch delta 81 zcmbQOI8&Z|CO-qigQct!*((`;ZETcbVvk~AV31H?o@gk=#=_v4l9M`lEt9wmBLfRi b90_nTgeT@?E4bvB=S>c0joYlm?7{;8{@@Ww delta 391 zcmbO!KU)Q&Hw%`y)9g)y4z$*R3y~jt$^;7o1h2RzF+1?cMb| z^CM<0e_Z*m>F@e?Icqq~`?rc%tj&`v-+9mO+TV}$O641(?u0ITA{HI7|McBnWzDmx z+2uQr-g-BG=c)Je^vrkOJ$%(C^X`oG7e!)AF5L<$l)lUMIXyOaef5rOHOhCUeYZTd zwQm2!bx-#g?Rw(#ea?A{@UuS2i(+R4n=Z9`_O|({<-iEs*(NMawGfGQJwau^Je6JQPls`Ga>bgt5N$-<#XFs Yt$nlI{CsHM+vryhKkdB#bW_ej0K?I!8~^|S diff --git a/game/level.ts b/game/level.ts new file mode 100644 index 00000000..63d8f168 --- /dev/null +++ b/game/level.ts @@ -0,0 +1,141 @@ +// NOTE: It's super not clear how much of this should be in rust vs +// javascript. Dealing with the level code is really nice in javascript +// but there's a bunch of weird stuff that really feels lower level. +import { load_texture } from "./assets"; +import { Texture, print, spr, use_texture } from "./graphics"; +import { load_string } from "./io"; + +// TODO: Use io-ts? YIKES. + +export type Tile = { + px: [number, number]; + src: [number, number]; + f: number; + t: number; + a: number; +}; + +export type TileLayer = { + type: "tile"; + texture: Texture; + grid_size: number; + offset: [number, number]; + tiles: Tile[]; +}; + +export type Layer = TileLayer; + +export type Level = { + world_x: number; + world_y: number; + width: number; + height: number; + layers: Layer[]; +}; + +export type TileSet = { id: number; texture: Texture }; + +export type World = { levels: Level[]; tilesets: Map }; + +async function load_tileset(def: { + uid: number; + relPath: string; +}): Promise { + let relPath = def.relPath as string; + if (relPath.endsWith(".aseprite")) { + // Whoops let's load the export instead? + relPath = relPath.substring(0, relPath.length - 8) + "png"; + } + let texture = await load_texture(relPath); + + print("Loaded tileset", def.uid, "from", relPath, "as ID", texture.id()); + return { id: def.uid, texture }; +} + +function load_level( + tile_sets: Map, + def: { + worldX: number; + worldY: number; + pxWid: number; + pxHei: number; + layerInstances: { + __gridSize: number; + __pxTotalOffsetX: number; + __pxTotalOffsetY: number; + __tilesetDefUid: number; + gridTiles: { + px: [number, number]; + src: [number, number]; + f: number; + t: number; + a: number; + }[]; + }[]; + } +): Level { + return { + world_x: def.worldX, + world_y: def.worldY, + width: def.pxWid, + height: def.pxHei, + layers: def.layerInstances.map((li) => { + const tileset = tile_sets.get(li.__tilesetDefUid); + if (!tileset) { + throw new Error("Unable to find texture!!! " + li.__tilesetDefUid); + } + + return { + type: "tile", + texture: tileset.texture, + grid_size: li.__gridSize, + offset: [li.__pxTotalOffsetX, li.__pxTotalOffsetY], + tiles: li.gridTiles, + }; + }), + }; +} + +export async function load_world(path: string): Promise { + print("Loading map:", path); + const blob = await load_string(path); + const map = JSON.parse(blob); + + const tilesets = new Map(); + let loaded_tilesets = await Promise.all( + map.defs.tilesets + .filter((def: any) => def.relPath != null) + .map((def: any) => load_tileset(def)) + ); + for (const ts of loaded_tilesets) { + tilesets.set(ts.id, ts); + } + + const levels = map.levels.map((l: any) => load_level(tilesets, l)); + return { levels, tilesets }; +} + +export function draw_level( + level: Level, + offset_x: number = 0, + offset_y: number = 0 +) { + for (const layer of level.layers) { + use_texture(layer.texture); + + let [ofx, ofy] = layer.offset; + ofx += offset_x; + ofy += offset_y; + for (const tile of layer.tiles) { + // TODO: Flip and whatnot. + spr( + tile.px[0] + ofx, + tile.px[1] + ofy, + layer.grid_size, + layer.grid_size, + tile.src[0], + tile.src[1] + ); + } + } +} diff --git a/game/main.ts b/game/main.ts index 802378b0..84282c67 100644 --- a/game/main.ts +++ b/game/main.ts @@ -2,8 +2,8 @@ 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"; +import { load_world, World, Level, draw_level } from "./level"; /// TODO: Support reload by saving and restoring state from init/update/restore. @@ -11,6 +11,8 @@ import { new_v2, vadd, vmul, vnorm } from "./vector"; let clock = 0; let bot_sprite: Texture | undefined = undefined; +let world: World | undefined = undefined; +let level: Level | undefined = undefined; // Note zelda overworld is 16x8 screens // zelda screen is 16x11 tiles @@ -18,13 +20,6 @@ let bot_sprite: Texture | undefined = undefined; 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... let texture_load = load_texture("./bot.png").then((n) => { @@ -32,7 +27,16 @@ function load_assets() { bot_sprite = n; }); - let map_load = load_map("./overworld.ldtk"); + let map_load = load_world("./overworld.ldtk").then((w) => { + print("World loaded at", since_start()); + world = w; + + // Assume we start at 0,0 + level = world.levels.find((l) => l.world_x == 0 && l.world_y == 0); + if (!level) { + throw new Error("UNABLE TO FIND LEVEL AT 0,0: CANNOT START"); + } + }); Promise.all([texture_load, map_load]).then(() => { loaded = true; @@ -106,6 +110,10 @@ export function draw() { return; } + if (level != undefined) { + draw_level(level, 0, 0); + } + if (bot_sprite != undefined) { // ...it gets resolved here? use_texture(bot_sprite); diff --git a/game/overworld.aseprite b/game/overworld.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..b7804aaab5b3764b2d43e1839ca99df1c14770ef GIT binary patch literal 1068 zcmcJPYe-X30LRZv8@*=lC}Q1OrG=0Vv&=aqh?NU9rJ0tcPOVK+be1~HW%g7;B2lN- z%ttTOjEJR@5=+QT!z^_!oMpAd$F&DO+UoY7z5LX-&f)wxoOAB||IRtw>tstvZ#j#c zK&1$a5E6;@w_waVNamvck5Vy>kgp%vlc{V3EJkBflaW*6y3t=sQNKo3OqrSNGK8$6 zT)}OQ0x;U$1puRX79M%*`kg;0uXhGF<-~$w*#K8H$gu%i>$y|tU=!E@^x+P1SVI}UkcBBU;Rr$4K@DDzf)RAAE(`+;C?Fr< zNJcC&5r{NIt*|ChIPO0M3WQB|hNna&P%i(T=4+s6NBw-1vHAUq6VOgB8p?k#$Hry$ z>80mqc6cw`Ug7=F)zwMo=HxAAm5^;0FMamonBI()brvf)=DYOl>xQpKZjCFA-}cpU zxb1634bA$*;pcs#MBzZWnt3rCb9g9kxmQWx=qqmZtGND;yn+aNVp!XweL0pUbXz&5 z**_jb$6fH#XD}*tOigg#O^e95NL~{!@A?$_RPVX@X~;-+wS!MWS(TdCQ>(~*!xQRi zJj41I(8*N~jU7C(x#bFN)BEi9lHH1R9`v}YL$#(Va0^d1q!8;<9+@oRDU~-S+WBA3 zoi!LjRCMy5*g@YurCrTTi*%MnniY{C7%W_7F)wNq&~{CNz|PLrkyfu-`LVLWjh?-i zRf)1_N&B>3<8?=&Wb_3m-K5eKC>gDCdQR&o*1gX^-o&*o3m!W@FxhFjV~*EnG&J}W zIn{YD^Ei^5^u2AjG!aFTKieFr8G;YuCqUsCaPzR zLer$s3+@{xy{a>WxLq(`ZnR0(0{I0~05iCdQ458$~$N_>Sc6a9DqUSK@|s$;uod z9aGjje#<^yCVoEu`~A@Nr*C~fe}DFP^Xss40k&U`ZTWot_VQ-Ans;A!MjSDAu(~yU zyScS{?VSq#^7o77tKNOJ%uH+8@%eBmhohFst$n@aK#`Yo86Uk~x%I3=@KtvG{^?g= zz1BKydSSik0idxz=S+&-d?DoP!q3H~6@nI1W9OQxO=!Qi{QlNoyH#eF{_?tI+2PW;t{AGL}ZyO*!8yS_K}>&MUEZdR3D?dLsq z_-^&*lcBaHu6M3o{2zFK>sJ|Rrd?M, - window: Window, - render_pipeline: wgpu::RenderPipeline, - - vertex_buffer: wgpu::Buffer, +#[derive(Debug)] +struct VertexBuffer { + buffer: wgpu::Buffer, + vec: Vec, max_vertices: usize, - - 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, - - // Garbage - mouse_x: f64, - mouse_y: f64, } -// TUTORIAL FOR BABIES LIKE ME: https://sotrh.github.io/learn-wgpu/beginner/tutorial2-surface/ +#[derive(Clone, Debug)] +struct VertexBufferHandle { + index: usize, + capacity: usize, +} -impl State { - // Creating some of the wgpu types requires async code +struct WindowAndDevice { + pub window: Window, + // pub instance: wgpu::Instance, + pub surface: wgpu::Surface, + pub adapter: wgpu::Adapter, + pub device: wgpu::Device, + pub queue: wgpu::Queue, +} + +impl WindowAndDevice { async fn new(window: Window) -> Self { - let size = window.inner_size(); - // The instance is a handle to our GPU // Backends::all => Vulkan + Metal + DX12 + Browser WebGPU let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { @@ -131,7 +122,52 @@ impl State { .await .unwrap(); - let surface_caps = surface.get_capabilities(&adapter); + WindowAndDevice { + window, + surface, + adapter, + device, + queue, + } + } +} + +struct State { + surface: wgpu::Surface, + device: wgpu::Device, + queue: wgpu::Queue, + config: wgpu::SurfaceConfiguration, + size: winit::dpi::PhysicalSize, + window: Window, + render_pipeline: wgpu::RenderPipeline, + + vertex_buffers: Vec, + next_free_vertex_buffer: usize, + + 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, + + // Garbage + mouse_x: f64, + mouse_y: f64, +} + +// TUTORIAL FOR BABIES LIKE ME: https://sotrh.github.io/learn-wgpu/beginner/tutorial2-surface/ + +impl State { + // Creating some of the wgpu types requires async code + fn new(hardware: WindowAndDevice) -> Self { + let size = hardware.window.inner_size(); + + let device = hardware.device; + + let surface_caps = hardware.surface.get_capabilities(&hardware.adapter); // Shader code in this tutorial assumes an sRGB surface // texture. Using a different one will result all the colors coming @@ -152,7 +188,7 @@ impl State { alpha_mode: surface_caps.alpha_modes[0], view_formats: vec![], }; - surface.configure(&device, &config); + hardware.surface.configure(&device, &config); let sprite_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { @@ -260,26 +296,17 @@ impl State { multiview: None, }); - let max_vertices: usize = 4096; - let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Vertex Buffer"), - size: (max_vertices * std::mem::size_of::()) - .try_into() - .unwrap(), - mapped_at_creation: false, - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - }); - Self { - window, - surface, + window: hardware.window, + surface: hardware.surface, device, - queue, + queue: hardware.queue, config, size, render_pipeline: sprite_pipeline, - vertex_buffer, - max_vertices, + vertex_buffers: Vec::new(), + next_free_vertex_buffer: 0, + sprite_bind_group_layout, sprite_textures: HashMap::new(), write_textures: HashMap::new(), @@ -316,260 +343,430 @@ impl State { fn render(&mut self, commands: Vec) -> Result<(), wgpu::SurfaceError> { let _span = span!("context render"); - let output = self.surface.get_current_texture()?; - // Group the commands into passes. + // Reset vertex buffers. + self.next_free_vertex_buffer = 0; + + let mut builder = FrameBuilder::new(self)?; + for command in commands { + builder.handle_command(command); + } + FrameBuilder::complete(builder); + + Ok(()) + } + + fn create_texture(&mut self, id: u32, image: image::DynamicImage, label: Option) { + let texture = texture::Texture::from_image( + &self.device, + &self.queue, + &image, + 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); + } + + fn create_writeable_texture( + &mut self, + id: u32, + width: u32, + height: u32, + label: Option, + ) { + 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); + } + + fn new_vertex_buffer(&mut self) -> VertexBufferHandle { + if self.next_free_vertex_buffer >= self.vertex_buffers.len() { + let max_vertices: usize = 4096; + let buffer = self.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Vertex Buffer"), + size: (max_vertices * std::mem::size_of::()) + .try_into() + .unwrap(), + mapped_at_creation: false, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); + self.vertex_buffers.push(VertexBuffer { + buffer, + max_vertices, + vec: Vec::with_capacity(max_vertices), + }); + } + + let index = self.next_free_vertex_buffer; + self.next_free_vertex_buffer += 1; + let vb = &mut self.vertex_buffers[index]; + vb.vec.clear(); + VertexBufferHandle { + index, + capacity: vb.max_vertices, + } + } + + fn get_vertex_buffer(&self, handle: &VertexBufferHandle) -> &VertexBuffer { + &self.vertex_buffers[handle.index] + } + + fn get_vertex_buffer_mut(&mut self, handle: &VertexBufferHandle) -> &mut VertexBuffer { + &mut self.vertex_buffers[handle.index] + } + + fn copy_vertex_buffers(&mut self) { + for i in 0..self.next_free_vertex_buffer { + let vb = &self.vertex_buffers[i]; + self.queue + .write_buffer(&vb.buffer, 0, bytemuck::cast_slice(&vb.vec)); + } + } +} + +#[derive(Debug)] +struct DrawCall { + texture_id: Option, + vertex_buffer: VertexBufferHandle, + draw_start: u32, + draw_end: u32, +} + +impl DrawCall { + pub fn new(vertex_buffer: VertexBufferHandle, draw_start: u32) -> Self { + DrawCall { + texture_id: None, + vertex_buffer, + draw_start, + draw_end: draw_start, + } + } + + pub fn new_at_buffer_tail(&self) -> Self { + DrawCall::new(self.vertex_buffer.clone(), self.draw_end) + } + + pub fn switch_textures(&self, id: u32) -> DrawCall { + let mut next = self.new_at_buffer_tail(); + next.texture_id = Some(id); + next + } + + pub fn empty(&self) -> bool { + self.draw_start == self.draw_end + } + + pub fn allocate_capacity(&mut self, capacity: u32) -> Option { + if self.vertex_buffer.capacity >= (self.draw_end + capacity) as usize { + self.draw_end += capacity; + Some(self.vertex_buffer.clone()) + } else { + None + } + } + + // pub fn draw<'a>(&'a self, pass: &'a mut wgpu::RenderPass, state: &'a State) { + // if self.draw_start == self.draw_end { + // return; + // } + + // let texture_id = match self.texture_id { + // Some(id) => id, + // None => return, + // }; + + // let bind_group = state.sprite_textures.get(&texture_id).unwrap(); + // pass.set_bind_group(0, bind_group, &[]); + + // let vb = self.vertex_buffer.borrow(); + // pass.set_bind_group(1, &state.screen_uniform_bind_group, &[]); + // pass.set_vertex_buffer(0, vb.buffer.slice(..)); + // pass.draw(self.draw_start..self.draw_end, 0..1); + // } +} + +struct FrameBuilder<'a> { + state: &'a mut State, + screen_view: Rc, + last_view: Rc, + encoder: wgpu::CommandEncoder, + output: wgpu::SurfaceTexture, + + target: Rc, + color: Option<[f64; 4]>, + draw_calls: Vec, +} + +impl<'a> FrameBuilder<'a> { + fn new(state: &'a mut State) -> Result { + let encoder = state + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Render Encoder"), + }); + + let output = state.surface.get_current_texture()?; + let screen_view = Rc::new( output .texture .create_view(&wgpu::TextureViewDescriptor::default()), ); - let mut last_view = screen_view.clone(); + let last_view = screen_view.clone(); - struct Pass { - color: Option<[f64; 4]>, - commands: Vec, - target: Rc, + Ok(FrameBuilder { + state, + screen_view, + last_view: last_view.clone(), + encoder, + output, + + target: last_view, + color: None, + draw_calls: Vec::new(), + }) + } + + fn complete(builder: FrameBuilder<'a>) { + let mut builder = builder; + builder.flush(); + + // At this point the state needs to copy the vertex buffers we know about. + builder.state.copy_vertex_buffers(); + + // Submit will accept anything that implements IntoIter + builder + .state + .queue + .submit(std::iter::once(builder.encoder.finish())); + + builder.output.present(); + } + + fn handle_command(&mut self, command: GraphicsCommand) { + match command { + // ================================================================ + // Pass commands + // ================================================================ + // NOTE: You would expect a "change target" command to be + // followed nearly immediately by a clear command and we + // wind up starting an extra pass in that case which sucks. + GraphicsCommand::Clear(cc) => self.start_pass(Some(cc.color), self.last_view.clone()), + GraphicsCommand::WriteToTexture(id) => { + let texture = self.state.write_textures.get(&id).unwrap(); + self.last_view = Rc::new( + texture + .texture + .create_view(&wgpu::TextureViewDescriptor::default()), + ); + self.start_pass(None, self.last_view.clone()); + } + GraphicsCommand::WriteToScreen => { + if !Rc::ptr_eq(&self.last_view, &self.screen_view) { + self.last_view = self.screen_view.clone(); + self.start_pass(None, self.last_view.clone()); + } + } + GraphicsCommand::Print(pc) => println!("{}", pc.text), + GraphicsCommand::Sprite(sc) => self.push_sprite(sc), + GraphicsCommand::UseTexture(id) => self.use_texture(id), + GraphicsCommand::EndFrame => self.flush(), + + // ================================================================ + // Resource commands + // ================================================================ + GraphicsCommand::CreateTexture(ct) => { + self.state.create_texture(ct.id, ct.image, ct.label) + } + GraphicsCommand::CreateWritableTexture { + id, + width, + height, + label, + } => self + .state + .create_writeable_texture(id, width, height, label), } - let mut passes = Vec::new(); - for command in commands { - match command { - 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, - &self.queue, - &ct.image, - match &ct.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 &ct.label { - Some(l) => Some(&l), - None => None, - }, - }); + fn start_pass(&mut self, color: Option<[f64; 4]>, target: Rc) { + self.flush(); + self.color = color; + self.target = target; + } - 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(), - }) + fn use_texture(&mut self, texture_id: u32) { + match self.draw_calls.last_mut() { + Some(call) => match call.texture_id { + Some(id) => { + if id == texture_id { + return; + } else if call.empty() { + call.texture_id = Some(texture_id); + } else { + let next = call.switch_textures(texture_id); + self.draw_calls.push(next); + return; } } + None => { + call.texture_id = Some(texture_id); + } + }, + None => { + let mut call = DrawCall::new(self.state.new_vertex_buffer(), 0); + call.texture_id = Some(texture_id); + self.draw_calls.push(call); + } + } + } - 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(), + fn get_vertex_buffer(&mut self, required_capacity: u32) -> &mut VertexBuffer { + match self.draw_calls.last_mut() { + Some(call) => match call.allocate_capacity(required_capacity) { + Some(vb) => return self.state.get_vertex_buffer_mut(&vb), + None => {} + }, + None => {} + }; + + let mut call = DrawCall::new(self.state.new_vertex_buffer(), 0); + let vb = call.allocate_capacity(required_capacity).unwrap(); + self.draw_calls.push(call); + self.state.get_vertex_buffer_mut(&vb) + } + + fn push_sprite(&mut self, sc: script::graphics::SpriteCommand) { + let vertex_buffer = self.get_vertex_buffer(6); + + vertex_buffer.vec.push(Vertex { + position: [sc.x, sc.y, 0.0], + tex_coords: [sc.u, sc.v], + }); + vertex_buffer.vec.push(Vertex { + position: [sc.x, sc.y + sc.h, 0.0], + tex_coords: [sc.u, sc.v + sc.sh], + }); + vertex_buffer.vec.push(Vertex { + position: [sc.x + sc.w, sc.y, 0.0], + tex_coords: [sc.u + sc.sw, sc.v], + }); + + vertex_buffer.vec.push(Vertex { + position: [sc.x, sc.y + sc.h, 0.0], + tex_coords: [sc.u, sc.v + sc.sh], + }); + vertex_buffer.vec.push(Vertex { + position: [sc.x + sc.w, sc.y + sc.h, 0.0], + tex_coords: [sc.u + sc.sw, sc.v + sc.sh], + }); + vertex_buffer.vec.push(Vertex { + position: [sc.x + sc.w, sc.y, 0.0], + tex_coords: [sc.u + sc.sw, sc.v], + }); + } + + fn flush(&mut self) { + let first_call = match self.draw_calls.last() { + Some(call) => call.new_at_buffer_tail(), + None => DrawCall::new(self.state.new_vertex_buffer(), 0), + }; + + if self.draw_calls.len() > 0 { + let mut pass = self.encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &self.target, + resolve_target: None, + ops: wgpu::Operations { + load: if let Some([r, g, b, a]) = self.color { + wgpu::LoadOp::Clear(wgpu::Color { + r, //0.1, + g, //0.2, + b, + a, }) - } - } - } - - 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(), - }), - }, - } - } - - let mut vertices = Vec::new(); - for pass in passes { - // TODO: It would be great if we could use multiple passes in a - // single encoder but right now because of the dyanmic - // nature of vertices we can't, I think? Because - // queue.write_buffer doesn't actually happen until we call - // submit... - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Render Encoder"), - }); - - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Render Pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &pass.target, - resolve_target: None, - ops: wgpu::Operations { - load: if let Some([r, g, b, a]) = pass.color { - wgpu::LoadOp::Clear(wgpu::Color { - r, //0.1, - g, //0.2, - b, - a, - }) - } else { - wgpu::LoadOp::Load - }, - store: true, + } else { + wgpu::LoadOp::Load }, - })], - depth_stencil_attachment: None, - }); + store: true, + }, + })], + depth_stencil_attachment: None, + }); - let mut texture_id = None; - vertices.clear(); - for command in pass.commands { - match command { - GraphicsCommand::Print(pc) => { - println!("{}", pc.text); - } - GraphicsCommand::Sprite(sc) => { - vertices.push(Vertex { - position: [sc.x, sc.y, 0.0], - tex_coords: [sc.u, sc.v], - }); - vertices.push(Vertex { - position: [sc.x, sc.y + sc.h, 0.0], - tex_coords: [sc.u, sc.v + sc.sh], - }); - vertices.push(Vertex { - position: [sc.x + sc.w, sc.y, 0.0], - tex_coords: [sc.u + sc.sw, sc.v], - }); - - vertices.push(Vertex { - position: [sc.x, sc.y + sc.h, 0.0], - tex_coords: [sc.u, sc.v + sc.sh], - }); - vertices.push(Vertex { - position: [sc.x + sc.w, sc.y + sc.h, 0.0], - tex_coords: [sc.u + sc.sw, sc.v + sc.sh], - }); - vertices.push(Vertex { - position: [sc.x + sc.w, sc.y, 0.0], - tex_coords: [sc.u + sc.sw, sc.v], - }); - } - - 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 - } + pass.set_pipeline(&self.state.render_pipeline); + for call in &self.draw_calls { + if call.draw_start == call.draw_end { + continue; } - if let Some(id) = texture_id { - assert!(vertices.len() < self.max_vertices); // ! - self.queue.write_buffer( - &self.vertex_buffer, - 0, - bytemuck::cast_slice(&vertices), - ); - render_pass.set_pipeline(&self.render_pipeline); + let texture_id = match call.texture_id { + Some(id) => id, + None => return, + }; - let bind_group = self.sprite_textures.get(&id).unwrap(); - render_pass.set_bind_group(0, bind_group, &[]); + let bind_group = self.state.sprite_textures.get(&texture_id).unwrap(); + pass.set_bind_group(0, bind_group, &[]); - render_pass.set_bind_group(1, &self.screen_uniform_bind_group, &[]); - render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); - render_pass.draw(0..(vertices.len() as u32), 0..1); - } + let vb = self.state.get_vertex_buffer(&call.vertex_buffer); + pass.set_bind_group(1, &self.state.screen_uniform_bind_group, &[]); + pass.set_vertex_buffer(0, vb.buffer.slice(..)); + pass.draw(call.draw_start..call.draw_end, 0..1); + + // call.draw(&mut pass, &self.state); } - - // Submit will accept anything that implements IntoIter - self.queue.submit(std::iter::once(encoder.finish())); } - output.present(); - - Ok(()) + self.color = None; + self.draw_calls.clear(); + self.draw_calls.push(first_call); } } @@ -670,11 +867,12 @@ pub async fn run() { let window = WindowBuilder::new().build(&event_loop).unwrap(); let event_loop_proxy = event_loop.create_proxy(); - let state = State::new(window).await; + let hardware = WindowAndDevice::new(window).await; let (sender, reciever) = std::sync::mpsc::channel(); std::thread::spawn(move || { set_thread_name!("game thread"); + let state = State::new(hardware); main_thread(event_loop_proxy, state, reciever); });