Minor cleanup

master
Mark 2023-12-23 14:07:12 -08:00
parent 5345355909
commit 6e59d95752
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
5 changed files with 127 additions and 137 deletions

View File

@ -174,10 +174,10 @@ pub async fn run() -> Result<()> {
Event::RedrawRequested(window_id) if window_id == gpu.window().id() => { Event::RedrawRequested(window_id) if window_id == gpu.window().id() => {
gpu.update(); gpu.update();
game.update(); game.update();
match gpu.render(&game.sprites(), game.camera) { match gpu.render(&game) {
Ok(_) => {} Ok(_) => {}
// Reconfigure the surface if lost // 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 // The system is out of memory, we should probably quit
Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit, Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit,
// All other errors (Outdated, Timeout) should be resolved by the next frame // All other errors (Outdated, Timeout) should be resolved by the next frame

View File

@ -1,11 +1,11 @@
use anyhow::Result; use anyhow::Result;
use bytemuck; use bytemuck;
use cgmath::{EuclideanSpace, Point2}; use cgmath::{EuclideanSpace, Point2, Vector2};
use std::{iter, rc::Rc}; use std::{iter, rc::Rc};
use wgpu; use wgpu;
use winit::{self, window::Window}; use winit::{self, dpi::PhysicalSize, window::Window};
use crate::{Camera, Sprite}; use crate::Game;
use super::{ use super::{
pipeline::PipelineBuilder, pipeline::PipelineBuilder,
@ -13,7 +13,7 @@ use super::{
util::Transform, util::Transform,
vertexbuffer::{ vertexbuffer::{
data::{SPRITE_INDICES, SPRITE_VERTICES}, data::{SPRITE_INDICES, SPRITE_VERTICES},
types::{PlainVertex, SpriteInstance, StarInstance, TexturedVertex}, types::{PlainVertex, SpriteInstance, StarfieldInstance, TexturedVertex},
VertexBuffer, VertexBuffer,
}, },
}; };
@ -25,7 +25,8 @@ pub struct GPUState {
queue: wgpu::Queue, queue: wgpu::Queue,
pub window: Window, pub window: Window,
pub size: winit::dpi::PhysicalSize<u32>, pub window_size: winit::dpi::PhysicalSize<u32>,
window_aspect: f32,
sprite_pipeline: wgpu::RenderPipeline, sprite_pipeline: wgpu::RenderPipeline,
starfield_pipeline: wgpu::RenderPipeline, starfield_pipeline: wgpu::RenderPipeline,
@ -42,11 +43,12 @@ struct VertexBuffers {
impl GPUState { impl GPUState {
// We can draw at most this many sprites on the screen. // We can draw at most this many sprites on the screen.
// TODO: compile-time option // TODO: compile-time option
pub const SPRITE_LIMIT: u64 = 100; pub const SPRITE_INSTANCE_LIMIT: u64 = 100;
pub const STAR_LIMIT: u64 = 100; pub const STARFIELD_INSTANCE_LIMIT: u64 = 100;
pub async fn new(window: Window) -> Result<Self> { pub async fn new(window: Window) -> Result<Self> {
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 { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(), backends: wgpu::Backends::all(),
@ -97,8 +99,8 @@ impl GPUState {
config = wgpu::SurfaceConfiguration { config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT, usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format, format: surface_format,
width: size.width, width: window_size.width,
height: size.height, height: window_size.height,
present_mode: surface_caps.present_modes[0], present_mode: surface_caps.present_modes[0],
alpha_mode: surface_caps.alpha_modes[0], alpha_mode: surface_caps.alpha_modes[0],
view_formats: vec![], view_formats: vec![],
@ -113,15 +115,15 @@ impl GPUState {
&device, &device,
Some(SPRITE_VERTICES), Some(SPRITE_VERTICES),
Some(SPRITE_INDICES), Some(SPRITE_INDICES),
Self::SPRITE_LIMIT, Self::SPRITE_INSTANCE_LIMIT,
)), )),
starfield: Rc::new(VertexBuffer::new::<PlainVertex, StarInstance>( starfield: Rc::new(VertexBuffer::new::<PlainVertex, StarfieldInstance>(
"starfield", "starfield",
&device, &device,
None, None,
None, None,
Self::STAR_LIMIT, Self::STARFIELD_INSTANCE_LIMIT,
)), )),
}; };
@ -159,7 +161,8 @@ impl GPUState {
queue, queue,
window, window,
size, window_size,
window_aspect,
sprite_pipeline, sprite_pipeline,
starfield_pipeline, starfield_pipeline,
@ -173,9 +176,10 @@ impl GPUState {
&self.window &self.window
} }
pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) { pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
if new_size.width > 0 && new_size.height > 0 { 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.width = new_size.width;
self.config.height = new_size.height; self.config.height = new_size.height;
self.surface.configure(&self.device, &self.config); self.surface.configure(&self.device, &self.config);
@ -184,56 +188,20 @@ impl GPUState {
pub fn update(&mut self) {} pub fn update(&mut self) {}
pub fn render( /// Make a SpriteInstance for each of the game's visible sprites.
&mut self, /// Will panic if SPRITE_INSTANCE_LIMIT is exceeded.
sprites: &Vec<Sprite>, ///
camera: Camera, /// This is only called inside self.render()
) -> Result<(), wgpu::SurfaceError> { fn make_sprite_instances(&self, game: &Game) -> Vec<SpriteInstance> {
let output = self.surface.get_current_texture()?; let mut instances: Vec<SpriteInstance> = Vec::new();
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;
// Game coordinates (relative to camera) of ne and sw corners of screen. // Game coordinates (relative to camera) of ne and sw corners of screen.
// Used to skip off-screen sprites. // Used to skip off-screen sprites.
let clip_ne = 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)) * camera.zoom; let clip_sw = Point2::from((1.0, -1.0)) * game.camera.zoom;
let mut instances: Vec<SpriteInstance> = Vec::new(); for s in game.sprites() {
let pos = s.post_parallax_position(game.camera) - game.camera.pos.to_vec();
for s in sprites {
let pos = s.post_parallax_position(camera) - camera.pos.to_vec();
let texture = self.texture_array.get_texture(&s.name[..]); let texture = self.texture_array.get_texture(&s.name[..]);
// Game dimensions of this sprite post-scale. // 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 // We can't use screen_pos to exclude off-screen sprites because
// it can't account for height and width. // it can't account for height and width.
let scale = height / camera.zoom; let scale = height / game.camera.zoom;
let screen_pos: Point2<f32> = pos / camera.zoom; let screen_pos: Point2<f32> = pos / game.camera.zoom;
instances.push(SpriteInstance { instances.push(SpriteInstance {
transform: Transform { transform: Transform {
pos: screen_pos, pos: screen_pos,
aspect: texture.aspect, sprite_aspect: texture.aspect,
screen_aspect, window_aspect: self.window_aspect,
rotate: s.angle, rotate: s.angle,
scale, scale,
} }
@ -279,46 +247,101 @@ impl GPUState {
} }
// Enforce sprite limit // 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. // TODO: no panic, handle this better.
panic!("Sprite limit exceeded!") unreachable!("Sprite limit exceeded!")
} }
// Write new sprite data to buffer return instances;
self.queue.write_buffer( }
&self.vertex_buffers.sprite.instances,
0,
bytemuck::cast_slice(&instances),
);
render_pass.set_bind_group(0, &self.texture_array.bind_group, &[]); /// Make a StarfieldInstance for each star that needs to be drawn.
/// Will panic if STARFIELD_INSTANCE_LIMIT is exceeded.
let nstances: Vec<StarInstance> = (-10..10) ///
.map(|x| StarInstance { /// This is only called inside self.render()
transform: cgmath::Matrix4::from_translation(cgmath::Vector3 { fn make_starfield_instances(&self, _game: &Game) -> Vec<StarfieldInstance> {
let instances: Vec<StarfieldInstance> = (-10..10)
.map(|x| StarfieldInstance {
position: Vector2 {
x: x as f32 / 10.0, x: x as f32 / 10.0,
y: 0.0, y: 0.0,
z: 0.0, }
})
.into(), .into(),
}) })
.collect(); .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.queue.write_buffer(
&self.vertex_buffers.starfield.instances, &self.vertex_buffers.starfield.instances,
0, 0,
bytemuck::cast_slice(&nstances), bytemuck::cast_slice(&starfield_instances),
); );
render_pass.set_bind_group(0, &self.texture_array.bind_group, &[]);
// Starfield pipeline // Starfield pipeline
self.vertex_buffers.starfield.set_in_pass(&mut render_pass); self.vertex_buffers.starfield.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.starfield_pipeline); 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 // Sprite pipeline
self.vertex_buffers.sprite.set_in_pass(&mut render_pass); self.vertex_buffers.sprite.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.sprite_pipeline); 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() // begin_render_pass borrows encoder mutably, so we can't call finish()
// without dropping this variable. // without dropping this variable.

View File

@ -1,9 +1,6 @@
// Vertex shader // Vertex shader
struct InstanceInput { struct InstanceInput {
@location(2) transform_matrix_0: vec4<f32>, @location(2) position: vec2<f32>,
@location(3) transform_matrix_1: vec4<f32>,
@location(4) transform_matrix_2: vec4<f32>,
@location(5) transform_matrix_3: vec4<f32>,
}; };
struct VertexOutput { struct VertexOutput {
@ -14,19 +11,10 @@ struct VertexOutput {
fn vertex_shader_main( fn vertex_shader_main(
instance: InstanceInput, instance: InstanceInput,
) -> VertexOutput { ) -> VertexOutput {
let transform_matrix = mat4x4<f32>(
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. // 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! // We need one instance per star, and that's it!
let pos = vec4<f32>(0.0, 0.0, 0.0, 1.0);
var out: VertexOutput; var out: VertexOutput;
out.clip_position = transform_matrix * pos; out.clip_position = vec4<f32>(instance.position, 0.0, 1.0);
return out; return out;
} }

View File

@ -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. // to put it in the right place, at the right angle, with the right scale.
pub struct Transform { pub struct Transform {
pub pos: Point2<f32>, // position on screen pub pos: Point2<f32>, // position on screen
pub screen_aspect: f32, // width / height. Screen aspect ratio. pub window_aspect: f32, // width / height. Screen aspect ratio.
pub aspect: f32, // width / height. Sprite 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 scale: f32, // if scale = 1, this sprite will be as tall as the screen.
pub rotate: Deg<f32>, // Around this object's center, in degrees measured ccw from vertical pub rotate: Deg<f32>, // 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 // We apply the provided scale here as well as a minor optimization
let sprite_aspect_and_scale = 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 // Apply rotation
let rotate = Matrix4::from_angle_z(self.rotate); let rotate = Matrix4::from_angle_z(self.rotate);
// Apply screen aspect ratio, again preserving height. // Apply screen aspect ratio, again preserving height.
// This must be done AFTER rotation... think about it! // 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. // After finishing all op, translate.
// This must be done last, all other operations // This must be done last, all other operations

View File

@ -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)] #[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct StarInstance { pub struct StarfieldInstance {
// All transformations we need to put this star in its place // All transformations we need to put this dot in its place
pub transform: [[f32; 4]; 4], // 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> { fn layout() -> wgpu::VertexBufferLayout<'static> {
wgpu::VertexBufferLayout { wgpu::VertexBufferLayout {
array_stride: Self::SIZE, 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, step_mode: wgpu::VertexStepMode::Instance,
attributes: &[ attributes: &[wgpu::VertexAttribute {
// A mat4 takes up 4 Texturedvertex slots as it is technically 4 vec4s. We need to define a slot offset: 0,
// for each vec4. We'll have to reassemble the mat4 in the shader. shader_location: 2,
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x2,
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,
},
],
} }
} }
} }
@ -113,7 +92,7 @@ impl BufferObject for SpriteInstance {
fn layout() -> wgpu::VertexBufferLayout<'static> { fn layout() -> wgpu::VertexBufferLayout<'static> {
wgpu::VertexBufferLayout { wgpu::VertexBufferLayout {
array_stride: Self::SIZE, 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 // This means that our shaders will only change to use the next
// instance when the shader starts processing a new instance // instance when the shader starts processing a new instance
step_mode: wgpu::VertexStepMode::Instance, step_mode: wgpu::VertexStepMode::Instance,