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() => {
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

View File

@ -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<u32>,
pub window_size: winit::dpi::PhysicalSize<u32>,
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<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 {
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::<PlainVertex, StarInstance>(
starfield: Rc::new(VertexBuffer::new::<PlainVertex, StarfieldInstance>(
"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<u32>) {
pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
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<Sprite>,
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<SpriteInstance> {
let mut instances: Vec<SpriteInstance> = 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<SpriteInstance> = 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<f32> = pos / camera.zoom;
let scale = height / game.camera.zoom;
let screen_pos: Point2<f32> = 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<StarInstance> = (-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<StarfieldInstance> {
let instances: Vec<StarfieldInstance> = (-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.

View File

@ -1,9 +1,6 @@
// Vertex shader
struct InstanceInput {
@location(2) transform_matrix_0: vec4<f32>,
@location(3) transform_matrix_1: vec4<f32>,
@location(4) transform_matrix_2: vec4<f32>,
@location(5) transform_matrix_3: vec4<f32>,
@location(2) position: vec2<f32>,
};
struct VertexOutput {
@ -14,19 +11,10 @@ struct VertexOutput {
fn vertex_shader_main(
instance: InstanceInput,
) -> 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.
// 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;
out.clip_position = transform_matrix * pos;
out.clip_position = vec4<f32>(instance.position, 0.0, 1.0);
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.
pub struct Transform {
pub pos: Point2<f32>, // 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<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
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

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)]
#[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 {
attributes: &[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,
},
],
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,