From 6e59d957521fa43ca5c31dcc5fd7c53b71daaba6 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 23 Dec 2023 14:07:12 -0800 Subject: [PATCH] Minor cleanup --- src/main.rs | 4 +- src/render/gpustate.rs | 191 +++++++++++++++++------------- src/render/shaders/starfield.wgsl | 16 +-- src/render/util.rs | 8 +- src/render/vertexbuffer/types.rs | 45 ++----- 5 files changed, 127 insertions(+), 137 deletions(-) diff --git a/src/main.rs b/src/main.rs index d87bde5..2368d1a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -174,10 +174,10 @@ pub async fn run() -> Result<()> { Event::RedrawRequested(window_id) if window_id == gpu.window().id() => { gpu.update(); game.update(); - match gpu.render(&game.sprites(), game.camera) { + match gpu.render(&game) { Ok(_) => {} // Reconfigure the surface if lost - Err(wgpu::SurfaceError::Lost) => gpu.resize(gpu.size), + Err(wgpu::SurfaceError::Lost) => gpu.resize(gpu.window_size), // The system is out of memory, we should probably quit Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit, // All other errors (Outdated, Timeout) should be resolved by the next frame diff --git a/src/render/gpustate.rs b/src/render/gpustate.rs index ddec6bd..f3eeb88 100644 --- a/src/render/gpustate.rs +++ b/src/render/gpustate.rs @@ -1,11 +1,11 @@ use anyhow::Result; use bytemuck; -use cgmath::{EuclideanSpace, Point2}; +use cgmath::{EuclideanSpace, Point2, Vector2}; use std::{iter, rc::Rc}; use wgpu; -use winit::{self, window::Window}; +use winit::{self, dpi::PhysicalSize, window::Window}; -use crate::{Camera, Sprite}; +use crate::Game; use super::{ pipeline::PipelineBuilder, @@ -13,7 +13,7 @@ use super::{ util::Transform, vertexbuffer::{ data::{SPRITE_INDICES, SPRITE_VERTICES}, - types::{PlainVertex, SpriteInstance, StarInstance, TexturedVertex}, + types::{PlainVertex, SpriteInstance, StarfieldInstance, TexturedVertex}, VertexBuffer, }, }; @@ -25,7 +25,8 @@ pub struct GPUState { queue: wgpu::Queue, pub window: Window, - pub size: winit::dpi::PhysicalSize, + pub window_size: winit::dpi::PhysicalSize, + window_aspect: f32, sprite_pipeline: wgpu::RenderPipeline, starfield_pipeline: wgpu::RenderPipeline, @@ -42,11 +43,12 @@ struct VertexBuffers { impl GPUState { // We can draw at most this many sprites on the screen. // TODO: compile-time option - pub const SPRITE_LIMIT: u64 = 100; - pub const STAR_LIMIT: u64 = 100; + pub const SPRITE_INSTANCE_LIMIT: u64 = 100; + pub const STARFIELD_INSTANCE_LIMIT: u64 = 100; pub async fn new(window: Window) -> Result { - let size = window.inner_size(); + let window_size = window.inner_size(); + let window_aspect = window_size.width as f32 / window_size.height as f32; let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: wgpu::Backends::all(), @@ -97,8 +99,8 @@ impl GPUState { config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: surface_format, - width: size.width, - height: size.height, + width: window_size.width, + height: window_size.height, present_mode: surface_caps.present_modes[0], alpha_mode: surface_caps.alpha_modes[0], view_formats: vec![], @@ -113,15 +115,15 @@ impl GPUState { &device, Some(SPRITE_VERTICES), Some(SPRITE_INDICES), - Self::SPRITE_LIMIT, + Self::SPRITE_INSTANCE_LIMIT, )), - starfield: Rc::new(VertexBuffer::new::( + starfield: Rc::new(VertexBuffer::new::( "starfield", &device, None, None, - Self::STAR_LIMIT, + Self::STARFIELD_INSTANCE_LIMIT, )), }; @@ -159,7 +161,8 @@ impl GPUState { queue, window, - size, + window_size, + window_aspect, sprite_pipeline, starfield_pipeline, @@ -173,9 +176,10 @@ impl GPUState { &self.window } - pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize) { + pub fn resize(&mut self, new_size: PhysicalSize) { if new_size.width > 0 && new_size.height > 0 { - self.size = new_size; + self.window_size = new_size; + self.window_aspect = new_size.width as f32 / new_size.height as f32; self.config.width = new_size.width; self.config.height = new_size.height; self.surface.configure(&self.device, &self.config); @@ -184,56 +188,20 @@ impl GPUState { pub fn update(&mut self) {} - pub fn render( - &mut self, - sprites: &Vec, - camera: Camera, - ) -> Result<(), wgpu::SurfaceError> { - let output = self.surface.get_current_texture()?; - let view = output - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("sprite render encoder"), - }); - - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("sprite render pass"), - - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 1.0, - }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - occlusion_query_set: None, - timestamp_writes: None, - }); - - // Correct for screen aspect ratio - // (it may not be square!) - let screen_aspect = self.size.width as f32 / self.size.height as f32; + /// Make a SpriteInstance for each of the game's visible sprites. + /// Will panic if SPRITE_INSTANCE_LIMIT is exceeded. + /// + /// This is only called inside self.render() + fn make_sprite_instances(&self, game: &Game) -> Vec { + let mut instances: Vec = Vec::new(); // Game coordinates (relative to camera) of ne and sw corners of screen. // Used to skip off-screen sprites. - let clip_ne = Point2::from((-1.0, 1.0)) * camera.zoom; - let clip_sw = Point2::from((1.0, -1.0)) * camera.zoom; + let clip_ne = Point2::from((-1.0, 1.0)) * game.camera.zoom; + let clip_sw = Point2::from((1.0, -1.0)) * game.camera.zoom; - let mut instances: Vec = Vec::new(); - - for s in sprites { - let pos = s.post_parallax_position(camera) - camera.pos.to_vec(); + for s in game.sprites() { + let pos = s.post_parallax_position(game.camera) - game.camera.pos.to_vec(); let texture = self.texture_array.get_texture(&s.name[..]); // Game dimensions of this sprite post-scale. @@ -261,14 +229,14 @@ impl GPUState { // // We can't use screen_pos to exclude off-screen sprites because // it can't account for height and width. - let scale = height / camera.zoom; - let screen_pos: Point2 = pos / camera.zoom; + let scale = height / game.camera.zoom; + let screen_pos: Point2 = pos / game.camera.zoom; instances.push(SpriteInstance { transform: Transform { pos: screen_pos, - aspect: texture.aspect, - screen_aspect, + sprite_aspect: texture.aspect, + window_aspect: self.window_aspect, rotate: s.angle, scale, } @@ -279,46 +247,101 @@ impl GPUState { } // Enforce sprite limit - if sprites.len() as u64 > Self::SPRITE_LIMIT { + if instances.len() as u64 > Self::SPRITE_INSTANCE_LIMIT { // TODO: no panic, handle this better. - panic!("Sprite limit exceeded!") + unreachable!("Sprite limit exceeded!") } - // Write new sprite data to buffer - self.queue.write_buffer( - &self.vertex_buffers.sprite.instances, - 0, - bytemuck::cast_slice(&instances), - ); + return instances; + } - render_pass.set_bind_group(0, &self.texture_array.bind_group, &[]); - - let nstances: Vec = (-10..10) - .map(|x| StarInstance { - transform: cgmath::Matrix4::from_translation(cgmath::Vector3 { + /// Make a StarfieldInstance for each star that needs to be drawn. + /// Will panic if STARFIELD_INSTANCE_LIMIT is exceeded. + /// + /// This is only called inside self.render() + fn make_starfield_instances(&self, _game: &Game) -> Vec { + let instances: Vec = (-10..10) + .map(|x| StarfieldInstance { + position: Vector2 { x: x as f32 / 10.0, y: 0.0, - z: 0.0, - }) + } .into(), }) .collect(); + // Enforce starfield limit + if instances.len() as u64 > Self::STARFIELD_INSTANCE_LIMIT { + // TODO: no panic, handle this better. + unreachable!("Starfield limit exceeded!") + } + return instances; + } + + pub fn render(&mut self, game: &Game) -> Result<(), wgpu::SurfaceError> { + let output = self.surface.get_current_texture()?; + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + 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: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + + // Create sprite instances + let sprite_instances = self.make_sprite_instances(game); + self.queue.write_buffer( + &self.vertex_buffers.sprite.instances, + 0, + bytemuck::cast_slice(&sprite_instances), + ); + + // Create starfield instances + let starfield_instances = self.make_starfield_instances(game); self.queue.write_buffer( &self.vertex_buffers.starfield.instances, 0, - bytemuck::cast_slice(&nstances), + bytemuck::cast_slice(&starfield_instances), ); + render_pass.set_bind_group(0, &self.texture_array.bind_group, &[]); + // Starfield pipeline self.vertex_buffers.starfield.set_in_pass(&mut render_pass); render_pass.set_pipeline(&self.starfield_pipeline); - render_pass.draw(0..1, 0..nstances.len() as _); + render_pass.draw(0..1, 0..starfield_instances.len() as _); // Sprite pipeline self.vertex_buffers.sprite.set_in_pass(&mut render_pass); render_pass.set_pipeline(&self.sprite_pipeline); - render_pass.draw_indexed(0..SPRITE_INDICES.len() as u32, 0, 0..instances.len() as _); + render_pass.draw_indexed( + 0..SPRITE_INDICES.len() as u32, + 0, + 0..sprite_instances.len() as _, + ); // begin_render_pass borrows encoder mutably, so we can't call finish() // without dropping this variable. diff --git a/src/render/shaders/starfield.wgsl b/src/render/shaders/starfield.wgsl index b8c3337..c63e44c 100644 --- a/src/render/shaders/starfield.wgsl +++ b/src/render/shaders/starfield.wgsl @@ -1,9 +1,6 @@ // Vertex shader struct InstanceInput { - @location(2) transform_matrix_0: vec4, - @location(3) transform_matrix_1: vec4, - @location(4) transform_matrix_2: vec4, - @location(5) transform_matrix_3: vec4, + @location(2) position: vec2, }; struct VertexOutput { @@ -14,19 +11,10 @@ struct VertexOutput { fn vertex_shader_main( instance: InstanceInput, ) -> VertexOutput { - let transform_matrix = mat4x4( - instance.transform_matrix_0, - instance.transform_matrix_1, - instance.transform_matrix_2, - instance.transform_matrix_3, - ); - // Stars consist of only one vertex, so we don't need to pass a buffer into this shader. // We need one instance per star, and that's it! - let pos = vec4(0.0, 0.0, 0.0, 1.0); - var out: VertexOutput; - out.clip_position = transform_matrix * pos; + out.clip_position = vec4(instance.position, 0.0, 1.0); return out; } diff --git a/src/render/util.rs b/src/render/util.rs index 19748bd..73f9b84 100644 --- a/src/render/util.rs +++ b/src/render/util.rs @@ -8,8 +8,8 @@ use super::OPENGL_TO_WGPU_MATRIX; // to put it in the right place, at the right angle, with the right scale. pub struct Transform { pub pos: Point2, // position on screen - pub screen_aspect: f32, // width / height. Screen aspect ratio. - pub aspect: f32, // width / height. Sprite aspect ratio. + pub window_aspect: f32, // width / height. Screen aspect ratio. + pub sprite_aspect: f32, // width / height. Sprite aspect ratio. pub scale: f32, // if scale = 1, this sprite will be as tall as the screen. pub rotate: Deg, // Around this object's center, in degrees measured ccw from vertical } @@ -26,14 +26,14 @@ impl Transform { // // We apply the provided scale here as well as a minor optimization let sprite_aspect_and_scale = - Matrix4::from_nonuniform_scale(self.aspect * self.scale, self.scale, 1.0); + Matrix4::from_nonuniform_scale(self.sprite_aspect * self.scale, self.scale, 1.0); // Apply rotation let rotate = Matrix4::from_angle_z(self.rotate); // Apply screen aspect ratio, again preserving height. // This must be done AFTER rotation... think about it! - let screen_aspect = Matrix4::from_nonuniform_scale(1.0 / self.screen_aspect, 1.0, 1.0); + let screen_aspect = Matrix4::from_nonuniform_scale(1.0 / self.window_aspect, 1.0, 1.0); // After finishing all op, translate. // This must be done last, all other operations diff --git a/src/render/vertexbuffer/types.rs b/src/render/vertexbuffer/types.rs index 553648a..a57e158 100644 --- a/src/render/vertexbuffer/types.rs +++ b/src/render/vertexbuffer/types.rs @@ -53,46 +53,25 @@ impl BufferObject for PlainVertex { } } -// Represents a star instance in WGSL +// Represents a point in the starfield instance in WGSL #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub struct StarInstance { - // All transformations we need to put this star in its place - pub transform: [[f32; 4]; 4], +pub struct StarfieldInstance { + // All transformations we need to put this dot in its place + // TODO: we don't need that much data + pub position: [f32; 2], } -impl BufferObject for StarInstance { +impl BufferObject for StarfieldInstance { fn layout() -> wgpu::VertexBufferLayout<'static> { wgpu::VertexBufferLayout { array_stride: Self::SIZE, - // We need to switch from using a step mode of TexturedVertex to Instance - // This means that our shaders will only change to use the next - // instance when the shader starts processing a new instance step_mode: wgpu::VertexStepMode::Instance, - attributes: &[ - // A mat4 takes up 4 Texturedvertex slots as it is technically 4 vec4s. We need to define a slot - // for each vec4. We'll have to reassemble the mat4 in the shader. - wgpu::VertexAttribute { - offset: 0, - shader_location: 2, - format: wgpu::VertexFormat::Float32x4, - }, - wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress, - shader_location: 3, - format: wgpu::VertexFormat::Float32x4, - }, - wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress, - shader_location: 4, - format: wgpu::VertexFormat::Float32x4, - }, - wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress, - shader_location: 5, - format: wgpu::VertexFormat::Float32x4, - }, - ], + attributes: &[wgpu::VertexAttribute { + offset: 0, + shader_location: 2, + format: wgpu::VertexFormat::Float32x2, + }], } } } @@ -113,7 +92,7 @@ impl BufferObject for SpriteInstance { fn layout() -> wgpu::VertexBufferLayout<'static> { wgpu::VertexBufferLayout { array_stride: Self::SIZE, - // We need to switch from using a step mode of TexturedVertex to Instance + // Switch to a step mode of Vertex to Instance. // This means that our shaders will only change to use the next // instance when the shader starts processing a new instance step_mode: wgpu::VertexStepMode::Instance,