Galactica/crates/render/src/gpustate.rs

756 lines
21 KiB
Rust
Raw Normal View History

2024-02-03 11:24:17 -08:00
use std::{iter, rc::Rc};
2024-02-02 22:32:03 -08:00
2024-01-10 17:53:27 -08:00
use anyhow::Result;
2024-01-27 15:45:37 -08:00
use bytemuck;
2024-01-10 17:53:27 -08:00
use galactica_content::Content;
2024-02-03 11:24:17 -08:00
use galactica_system::data::ShipState;
use galactica_util::to_radians;
use glyphon::{FontSystem, Resolution, SwashCache, TextAtlas, TextRenderer};
use nalgebra::{Point2, Point3};
2024-01-27 15:45:37 -08:00
use wgpu;
use winit;
2024-01-10 17:53:27 -08:00
use crate::{
2024-02-03 11:24:17 -08:00
globaluniform::{GlobalDataContent, GlobalUniform, ObjectData},
2024-01-27 15:45:37 -08:00
pipeline::PipelineBuilder,
shaderprocessor::preprocess_shader,
starfield::Starfield,
texturearray::TextureArray,
2024-02-03 11:24:17 -08:00
ui::UiManager,
vertexbuffer::{consts::SPRITE_INDICES, types::ObjectInstance},
RenderInput, RenderState, VertexBuffers,
2024-01-10 17:53:27 -08:00
};
2024-01-27 15:45:37 -08:00
/// A high-level GPU wrapper. Reads game state (via RenderInput), produces pretty pictures.
pub struct GPUState {
pub(crate) device: wgpu::Device,
pub(crate) config: wgpu::SurfaceConfiguration,
pub(crate) surface: wgpu::Surface,
pub(crate) object_pipeline: wgpu::RenderPipeline,
pub(crate) starfield_pipeline: wgpu::RenderPipeline,
pub(crate) ui_pipeline: wgpu::RenderPipeline,
pub(crate) radialbar_pipeline: wgpu::RenderPipeline,
pub(crate) starfield: Starfield,
pub(crate) texture_array: TextureArray,
pub(crate) state: RenderState,
pub(crate) ui: UiManager,
}
2024-01-10 17:53:27 -08:00
impl GPUState {
/// Make a new GPUState that draws on `window`
2024-02-03 07:33:10 -08:00
pub async fn new(window: winit::window::Window, ct: Rc<Content>) -> Result<Self> {
2024-01-10 17:53:27 -08:00
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(),
..Default::default()
});
let surface = unsafe { instance.create_surface(&window) }.unwrap();
// Basic setup
let device;
let queue;
let config;
{
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.unwrap();
(device, queue) = adapter
2024-01-27 15:45:37 -08:00
.request_device(
&wgpu::DeviceDescriptor {
// TODO: remove nonuniform sampled textures
features: wgpu::Features::TEXTURE_BINDING_ARRAY | wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
// We may need limits if we compile for wasm
limits: wgpu::Limits::default(),
label: Some("gpu device"),
},
None,
)
.await
.unwrap();
2024-01-10 17:53:27 -08:00
// Assume sRGB
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps
.formats
.iter()
.copied()
.filter(|f| f.is_srgb())
.filter(|f| f.has_stencil_aspect())
.next()
.unwrap_or(surface_caps.formats[0]);
config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
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![],
};
surface.configure(&device, &config);
}
2024-02-02 22:32:03 -08:00
let vertex_buffers = VertexBuffers::new(&device, &ct);
2024-01-10 17:53:27 -08:00
// Load uniforms
let global_uniform = GlobalUniform::new(&device);
2024-02-02 22:32:03 -08:00
let texture_array = TextureArray::new(&device, &queue, &ct)?;
2024-01-10 17:53:27 -08:00
// Make sure these match the indices in each shader
let bind_group_layouts = &[
&texture_array.bind_group_layout,
&global_uniform.bind_group_layout,
];
// Text renderer
let mut text_atlas = TextAtlas::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb);
2024-01-10 22:44:22 -08:00
let mut text_font_system = FontSystem::new_with_locale_and_db(
"en-US".to_string(),
glyphon::fontdb::Database::new(),
);
let conf = ct.get_config();
for font in &conf.font_files {
text_font_system.db_mut().load_font_file(font)?;
}
// TODO: nice error if no family with this name is found
text_font_system
.db_mut()
.set_sans_serif_family(conf.font_sans.clone());
text_font_system
.db_mut()
.set_serif_family(conf.font_serif.clone());
text_font_system
.db_mut()
.set_monospace_family(conf.font_mono.clone());
//text_font_system
// .db_mut()
// .set_cursive_family(conf.font_cursive.clone());
//text_font_system
// .db_mut()
// .set_fantasy_family(conf.font_fantasy.clone());
2024-01-10 17:53:27 -08:00
let text_cache = SwashCache::new();
let text_renderer = TextRenderer::new(
&mut text_atlas,
&device,
wgpu::MultisampleState::default(),
None,
);
// Create render pipelines
let object_pipeline = PipelineBuilder::new("object", &device)
.set_shader(&preprocess_shader(
&include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/shaders/",
"object.wgsl"
)),
&global_uniform,
1,
))
.set_format(config.format)
.set_triangle(true)
2024-01-17 10:17:18 -08:00
.set_vertex_buffer(vertex_buffers.get_object())
2024-01-10 17:53:27 -08:00
.set_bind_group_layouts(bind_group_layouts)
.build();
let starfield_pipeline = PipelineBuilder::new("starfield", &device)
.set_shader(&preprocess_shader(
&include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/shaders/",
"starfield.wgsl"
)),
&global_uniform,
1,
))
.set_format(config.format)
.set_triangle(true)
2024-01-17 10:17:18 -08:00
.set_vertex_buffer(vertex_buffers.get_starfield())
2024-01-10 17:53:27 -08:00
.set_bind_group_layouts(bind_group_layouts)
.build();
let ui_pipeline = PipelineBuilder::new("ui", &device)
.set_shader(&preprocess_shader(
&include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/", "ui.wgsl")),
&global_uniform,
1,
))
.set_format(config.format)
.set_triangle(true)
2024-01-17 10:17:18 -08:00
.set_vertex_buffer(vertex_buffers.get_ui())
2024-01-10 17:53:27 -08:00
.set_bind_group_layouts(bind_group_layouts)
.build();
let radialbar_pipeline = PipelineBuilder::new("radialbar", &device)
.set_shader(&preprocess_shader(
&include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/shaders/",
"radialbar.wgsl"
)),
&global_uniform,
1,
))
.set_format(config.format)
.set_triangle(true)
2024-01-17 10:17:18 -08:00
.set_vertex_buffer(vertex_buffers.get_radialbar())
2024-01-10 17:53:27 -08:00
.set_bind_group_layouts(bind_group_layouts)
.build();
let mut starfield = Starfield::new();
2024-02-02 22:32:03 -08:00
starfield.regenerate(&ct);
2024-01-10 17:53:27 -08:00
2024-02-03 07:47:19 -08:00
let mut state = RenderState {
queue,
window,
window_size,
window_aspect,
global_uniform,
vertex_buffers,
text_atlas,
text_cache,
text_font_system,
text_renderer,
};
2024-01-10 18:53:19 -08:00
return Ok(Self {
2024-02-03 07:47:19 -08:00
ui: UiManager::new(ct, &mut state),
2024-01-10 17:53:27 -08:00
device,
config,
surface,
starfield,
texture_array,
object_pipeline,
starfield_pipeline,
ui_pipeline,
radialbar_pipeline,
2024-02-03 07:47:19 -08:00
state,
2024-01-10 17:53:27 -08:00
});
}
}
2024-01-27 15:45:37 -08:00
impl GPUState {
/// Get the window we are attached to
pub fn window(&self) -> &winit::window::Window {
&self.state.window
}
/// Update window size.
/// This should be called whenever our window is resized.
pub fn resize(&mut self, ct: &Content) {
let new_size = self.state.window.inner_size();
if new_size.width > 0 && new_size.height > 0 {
self.state.window_size = new_size;
self.state.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);
}
self.starfield.update_buffer(ct, &mut self.state);
}
/// Initialize the rendering engine
pub fn init(&mut self, ct: &Content) {
// Update global values
self.state.queue.write_buffer(
&self.state.global_uniform.atlas_buffer,
0,
bytemuck::cast_slice(&[self.texture_array.texture_atlas]),
);
self.starfield.update_buffer(ct, &mut self.state);
}
/// Main render function. Draws sprites on a window.
pub fn render(&mut self, input: RenderInput) -> Result<(), wgpu::SurfaceError> {
2024-02-03 11:24:17 -08:00
let input = Rc::new(input);
2024-01-27 15:45:37 -08:00
// Update global values
self.state.queue.write_buffer(
&self.state.global_uniform.data_buffer,
0,
bytemuck::cast_slice(&[GlobalDataContent {
camera_position_x: input.camera_pos.x,
camera_position_y: input.camera_pos.y,
camera_zoom: input.camera_zoom,
camera_zoom_min: input.ct.get_config().zoom_min,
camera_zoom_max: input.ct.get_config().zoom_max,
window_size_w: self.state.window_size.width as f32,
window_size_h: self.state.window_size.height as f32,
window_scale: self.state.window.scale_factor() as f32,
window_aspect: self.state.window_aspect,
starfield_sprite: input.ct.get_config().starfield_texture.into(),
starfield_tile_size: input.ct.get_config().starfield_size,
starfield_size_min: input.ct.get_config().starfield_min_size,
starfield_size_max: input.ct.get_config().starfield_max_size,
}]),
);
self.state.frame_reset();
2024-02-03 11:24:17 -08:00
let output = self.surface.get_current_texture()?;
let view = output.texture.create_view(&Default::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,
});
if self.ui.get_config().show_phys {
// Create sprite instances
// Game coordinates (relative to camera) of ne and sw corners of screen.
// Used to skip off-screen sprites.
let clip_ne = Point2::new(-self.state.window_aspect, 1.0) * input.camera_zoom;
let clip_sw = Point2::new(self.state.window_aspect, -1.0) * input.camera_zoom;
// Order matters, it determines what is drawn on top.
// The order inside ships and projectiles doesn't matter,
// but ships should always be under projectiles.
self.push_system(&input, (clip_ne, clip_sw));
self.push_ships(&input, (clip_ne, clip_sw));
self.push_projectiles(&input, (clip_ne, clip_sw));
self.push_effects(&input, (clip_ne, clip_sw));
2024-01-27 15:45:37 -08:00
}
2024-02-03 11:24:17 -08:00
self.ui.draw(input.clone(), &mut self.state).unwrap();
// These should match the indices in each shader,
// and should each have a corresponding bind group layout.
render_pass.set_bind_group(0, &self.texture_array.bind_group, &[]);
render_pass.set_bind_group(1, &self.state.global_uniform.bind_group, &[]);
if self.ui.get_config().show_starfield {
// Starfield pipeline
self.state
.vertex_buffers
.get_starfield()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.starfield_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..self.state.get_starfield_counter(),
);
}
if self.ui.get_config().show_phys {
// Sprite pipeline
self.state
.vertex_buffers
.get_object()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.object_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..self.state.get_object_counter(),
);
}
// Ui pipeline
self.state
.vertex_buffers
.get_ui()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.ui_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..self.state.get_ui_counter(),
);
// Radial progress bars
// TODO: do we need to do this every time?
self.state
.vertex_buffers
.get_radialbar()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.radialbar_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..self.state.get_radialbar_counter(),
);
let textareas = self.ui.get_textareas(&input, &self.state);
self.state
.text_renderer
.prepare(
&self.device,
&self.state.queue,
&mut self.state.text_font_system,
&mut self.state.text_atlas,
Resolution {
width: self.state.window_size.width,
height: self.state.window_size.height,
},
textareas,
&mut self.state.text_cache,
)
.unwrap();
self.state
.text_renderer
.render(&self.state.text_atlas, &mut render_pass)
.unwrap();
// begin_render_pass borrows encoder mutably,
// so we need to drop it before calling finish.
drop(render_pass);
self.state.queue.submit(iter::once(encoder.finish()));
output.present();
2024-01-27 15:45:37 -08:00
return Ok(());
}
}
2024-02-03 11:24:17 -08:00
impl GPUState {
fn push_ships(
&mut self,
input: &RenderInput,
// NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>),
) {
for s in input.phys_img.iter_ships() {
let ship_pos;
let ship_ang;
let ship_cnt;
match s.ship.get_data().get_state() {
ShipState::Dead | ShipState::Landed { .. } => continue,
ShipState::Collapsing { .. } | ShipState::Flying { .. } => {
let r = &s.rigidbody;
let pos = *r.translation();
ship_pos = Point3::new(pos.x, pos.y, 1.0);
let ship_rot = r.rotation();
ship_ang = ship_rot.angle();
ship_cnt = input.ct.get_ship(s.ship.get_data().get_content());
}
ShipState::UnLanding { current_z, .. } | ShipState::Landing { current_z, .. } => {
let r = &s.rigidbody;
let pos = *r.translation();
ship_pos = Point3::new(pos.x, pos.y, *current_z);
let ship_rot = r.rotation();
ship_ang = ship_rot.angle();
ship_cnt = input.ct.get_ship(s.ship.get_data().get_content());
}
}
// Position adjusted for parallax
// TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for ships
let pos: Point2<f32> =
(Point2::new(ship_pos.x, ship_pos.y) - input.camera_pos) / ship_pos.z;
// Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// We take the maximum to account for rotated sprites.
let m =
(ship_cnt.size / ship_pos.z) * input.ct.get_sprite(ship_cnt.sprite).aspect.max(1.0);
// Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m
|| pos.y > screen_clip.0.y + m
|| pos.x > screen_clip.1.x + m
|| pos.y < screen_clip.1.y - m
{
continue;
}
let idx = self.state.get_object_counter();
// Write this object's location data
self.state.queue.write_buffer(
&self.state.global_uniform.object_buffer,
ObjectData::SIZE * idx as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: ship_pos.x,
ypos: ship_pos.y,
zpos: ship_pos.z,
angle: ship_ang,
size: ship_cnt.size,
parent: 0,
is_child: 0,
_padding: Default::default(),
}]),
);
// Push this object's instance
let anim_state = s.ship.get_anim_state();
self.state.push_object_buffer(ObjectInstance {
texture_index: anim_state.texture_index(),
texture_fade: anim_state.fade,
object_index: idx as u32,
color: [1.0, 1.0, 1.0, 1.0],
});
if {
let is_flying = match s.ship.get_data().get_state() {
ShipState::Flying { .. }
| ShipState::UnLanding { .. }
| ShipState::Landing { .. } => true,
_ => false,
};
is_flying
} {
for (engine_point, anim) in s.ship.iter_engine_anim() {
self.state.queue.write_buffer(
&self.state.global_uniform.object_buffer,
ObjectData::SIZE * self.state.get_object_counter() as u64,
bytemuck::cast_slice(&[ObjectData {
// Note that we adjust the y-coordinate for half-height,
// not the x-coordinate, even though our ships point east
// at 0 degrees. This is because this is placed pre-rotation,
// and the parent rotation adjustment in our object shader
// automatically accounts for this.
xpos: engine_point.pos.x,
ypos: engine_point.pos.y - engine_point.size / 2.0,
zpos: 1.0,
// We still need an adjustment here, though,
// since engine sprites point north (with exhaust towards the south)
angle: to_radians(90.0),
size: engine_point.size,
parent: idx as u32,
is_child: 1,
_padding: Default::default(),
}]),
);
let anim_state = anim.get_texture_idx();
self.state.push_object_buffer(ObjectInstance {
texture_index: anim_state.texture_index(),
texture_fade: anim_state.fade,
object_index: self.state.get_object_counter() as u32,
color: [1.0, 1.0, 1.0, 1.0],
});
}
}
}
}
fn push_projectiles(
&mut self,
input: &RenderInput,
// NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>),
) {
for p in input.phys_img.iter_projectiles() {
let r = &p.rigidbody;
let proj_pos = *r.translation();
let proj_rot = r.rotation();
let proj_ang = proj_rot.angle();
let proj_cnt = &p.projectile.content; // TODO: don't clone this?
// Position adjusted for parallax
// TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for projectiles
let pos = (proj_pos - input.camera_pos) / 1.0;
// Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// We take the maximum to account for rotated sprites.
let m = (proj_cnt.size / 1.0) * input.ct.get_sprite(proj_cnt.sprite).aspect.max(1.0);
// Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m
|| pos.y > screen_clip.0.y + m
|| pos.x > screen_clip.1.x + m
|| pos.y < screen_clip.1.y - m
{
continue;
}
let idx = self.state.get_object_counter();
// Write this object's location data
self.state.queue.write_buffer(
&self.state.global_uniform.object_buffer,
ObjectData::SIZE * idx as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: proj_pos.x,
ypos: proj_pos.y,
zpos: 1.0,
angle: proj_ang,
size: 0f32.max(proj_cnt.size + p.projectile.size_rng),
parent: 0,
is_child: 0,
_padding: Default::default(),
}]),
);
let anim_state = p.projectile.get_anim_state();
self.state.push_object_buffer(ObjectInstance {
texture_index: anim_state.texture_index(),
texture_fade: anim_state.fade,
object_index: idx as u32,
color: [1.0, 1.0, 1.0, 1.0],
});
}
}
fn push_system(
&mut self,
input: &RenderInput,
// NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>),
) {
let system = input.ct.get_system(input.current_system);
for o in &system.objects {
// Position adjusted for parallax
let pos: Point2<f32> = (Point2::new(o.pos.x, o.pos.y) - input.camera_pos) / o.pos.z;
// Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// We take the maximum to account for rotated sprites.
let m = (o.size / o.pos.z) * input.ct.get_sprite(o.sprite).aspect.max(1.0);
// Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m
|| pos.y > screen_clip.0.y + m
|| pos.x > screen_clip.1.x + m
|| pos.y < screen_clip.1.y - m
{
continue;
}
let idx = self.state.get_object_counter();
// Write this object's location data
self.state.queue.write_buffer(
&self.state.global_uniform.object_buffer,
ObjectData::SIZE * idx as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: o.pos.x,
ypos: o.pos.y,
zpos: o.pos.z,
angle: o.angle,
size: o.size,
parent: 0,
is_child: 0,
_padding: Default::default(),
}]),
);
let sprite = input.ct.get_sprite(o.sprite);
let texture_a = sprite.get_first_frame(); // ANIMATE
// Push this object's instance
self.state.push_object_buffer(ObjectInstance {
texture_index: [texture_a, texture_a],
texture_fade: 1.0,
object_index: idx as u32,
color: [1.0, 1.0, 1.0, 1.0],
});
}
}
fn push_effects(
&mut self,
input: &RenderInput,
// NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>),
) {
for p in input.phys_img.iter_effects() {
let r = &p.rigidbody;
let pos = *r.translation();
let rot = r.rotation();
let ang = rot.angle();
// Position adjusted for parallax
// TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for projectiles
let adjusted_pos = (pos - input.camera_pos) / 1.0;
// Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// We take the maximum to account for rotated sprites.
let m = (p.effect.size / 1.0)
* input
.ct
.get_sprite(p.effect.anim.get_sprite())
.aspect
.max(1.0);
// Don't draw sprites that are off the screen
if adjusted_pos.x < screen_clip.0.x - m
|| adjusted_pos.y > screen_clip.0.y + m
|| adjusted_pos.x > screen_clip.1.x + m
|| adjusted_pos.y < screen_clip.1.y - m
{
continue;
}
let idx = self.state.get_object_counter();
// Write this object's location data
self.state.queue.write_buffer(
&self.state.global_uniform.object_buffer,
ObjectData::SIZE * idx as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: pos.x,
ypos: pos.y,
zpos: 1.0,
angle: ang,
size: p.effect.size,
parent: 0,
is_child: 0,
_padding: Default::default(),
}]),
);
let anim_state = p.effect.anim.get_texture_idx();
self.state.push_object_buffer(ObjectInstance {
texture_index: anim_state.texture_index(),
texture_fade: anim_state.fade,
object_index: idx as u32,
color: [1.0, 1.0, 1.0, p.get_fade()],
});
}
}
}