use std::{
ops::{Index, Range},
path::PathBuf,
sync::Arc,
time::{Duration, Instant},
};
use glam::{vec2, UVec2, Vec2};
use log::{error, info, warn};
use rustc_hash::FxHashMap;
use wgpu::util::DeviceExt;
use winit::{
application::ApplicationHandler,
dpi::PhysicalSize,
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow},
window::{Window, WindowAttributes, WindowId},
};
use crate::{
input::InputState,
render::{
bind_group::AsBindGroup, register_builtin_assets, render2d::SpriteMaterial, viewport,
BufferItem, Buffers, Camera, DynamicBuffer, Mesh, PipelineSet, TextureSet, View,
PIPELINE_FILL, PIPELINE_VIEWPORT, TEXTURE_EMPTY,
},
Context, PittoreApp,
};
pub struct PittoreBackend {
app: Box<dyn PittoreApp>,
window_attribs: WindowAttributes,
render_state: Option<RenderState>,
render_commands: Vec<RenderCommand>,
input_state: InputState,
next_time_begin_frame: Instant,
desired_draw_interval: Duration,
ran_update_since_last_redraw: bool,
}
impl PittoreBackend {
pub fn new<A: PittoreApp + 'static>(app: A, window_attribs: WindowAttributes) -> Self {
PittoreBackend {
app: Box::new(app),
window_attribs,
render_state: None,
render_commands: Vec::new(),
input_state: InputState::default(),
next_time_begin_frame: Instant::now(),
desired_draw_interval: Duration::from_secs_f64(1.0 / 60.0),
ran_update_since_last_redraw: false,
}
}
fn update(&mut self) {
self.input_state.update();
if let Some(ref mut render_state) = &mut self.render_state {
self.render_commands.clear();
render_state.meshes.clear_temporary();
let size = render_state.resolution();
let view_buffer = render_state.buffers.get_mut::<View>().unwrap();
view_buffer.append_temporary(vec![Camera::default_2d().view(size)]);
let mut ctx = Context {
render_state,
render_commands: &mut self.render_commands,
input_state: &self.input_state,
};
self.app.update(&mut ctx);
render_state.window.request_redraw();
}
}
fn render(&mut self) {
let render_state = self.render_state.as_mut().unwrap();
render_state
.buffers
.write_all(&render_state.device, &render_state.queue);
let view_bind_group = View::as_bind_group(&View::default(), render_state);
for (id, pipeline) in &render_state.pipelines.persistent {
let bind_group = pipeline.pipeline_bind_group.as_bind_group(render_state);
render_state
.pipeline_bind_group_cache
.insert(*id, bind_group);
}
for (id, material) in &render_state.materials.persistent {
let bind_group = material.as_bind_group(render_state);
render_state
.material_bind_group_cache
.insert(*id, bind_group);
}
let mut encoder = render_state
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &render_state.viewport.msaa_texture.texture_view,
resolve_target: Some(&render_state.viewport.viewport_texture.texture_view),
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(render_state.background_color),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
let active_pipeline_id = PIPELINE_FILL;
let mut active_texture_id = TEXTURE_EMPTY;
let mut view_offset = 0;
let pipeline = &render_state.pipelines[active_pipeline_id];
let pipeline_bind_group = &render_state.pipeline_bind_group_cache[active_pipeline_id];
let empty_texture_bind_group = &render_state.material_bind_group_cache[TEXTURE_EMPTY];
render_pass.set_pipeline(&pipeline.pipeline);
render_pass.set_bind_group(0, Some(&view_bind_group), &[0]);
render_pass.set_bind_group(1, Some(pipeline_bind_group), &[]);
render_pass.set_bind_group(2, Some(empty_texture_bind_group), &[]);
render_pass.set_index_buffer(
render_state.buffers.get::<u32>().unwrap().slice(..),
wgpu::IndexFormat::Uint32,
);
for command in &self.render_commands {
match command {
RenderCommand::SetCamera => {
view_offset += 1;
render_pass.set_bind_group(
0,
Some(&view_bind_group),
&[view_offset * size_of::<View>() as u32],
);
}
RenderCommand::Draw {
instance_range,
material_handle,
mesh_handle,
} => {
if let Handle::Persistent(id) = material_handle {
if *id != active_texture_id {
if let Some(bind_group) =
render_state.material_bind_group_cache.get(*id)
{
render_pass.set_bind_group(2, Some(bind_group), &[]);
active_texture_id = *id;
}
}
}
let mesh = &render_state.meshes[*mesh_handle];
render_pass.draw_indexed(
mesh.index_range.clone(),
mesh.base_vertex,
instance_range.clone(),
);
}
}
}
}
let surface_texture = render_state.surface.get_current_texture().unwrap();
let surface_view = surface_texture
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &surface_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
let pipeline = &render_state.pipelines[PIPELINE_VIEWPORT];
render_pass.set_pipeline(&pipeline.pipeline);
render_pass.set_bind_group(
0,
Some(&viewport::PipelineBindGroup.as_bind_group(render_state)),
&[],
);
render_pass.draw(0..6, 0..1);
}
if render_state.capture_target.is_some() {
render_state.viewport.copy_to_buffer(&mut encoder);
}
render_state.queue.submit(std::iter::once(encoder.finish()));
surface_texture.present();
if let Some(capture_target) = render_state.capture_target.take() {
render_state.viewport.save_capture(capture_target);
}
}
fn resize(&mut self, new_size: PhysicalSize<u32>) {
let new_size = UVec2::new(new_size.width, new_size.height);
let render_state = self.render_state.as_mut().unwrap();
render_state.surface_config.width = new_size.x;
render_state.surface_config.height = new_size.y;
render_state
.surface
.configure(&render_state.device, &render_state.surface_config);
let viewport = &mut render_state.viewport;
*viewport = ViewportState::new(
&render_state.device,
&render_state.surface_config,
viewport.fixed_viewport_size,
);
self.app.resize(new_size);
}
}
impl ApplicationHandler for PittoreBackend {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let window = event_loop
.create_window(self.window_attribs.clone())
.unwrap();
let mut render_state = pollster::block_on(RenderState::new(window));
register_builtin_assets(&mut render_state);
self.app.init(&mut Context {
render_commands: &mut self.render_commands,
render_state: &mut render_state,
input_state: &self.input_state,
});
self.render_state = Some(render_state);
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
if let Some(ref render_state) = self.render_state {
if render_state.window.id() == window_id {
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(physical_size) => self.resize(physical_size),
WindowEvent::RedrawRequested => {
self.render();
self.ran_update_since_last_redraw = false;
}
WindowEvent::KeyboardInput { .. }
| WindowEvent::CursorEntered { .. }
| WindowEvent::CursorLeft { .. }
| WindowEvent::CursorMoved { .. }
| WindowEvent::MouseInput { .. }
| WindowEvent::MouseWheel { .. } => self.input_state.push_event(event),
_ => {}
}
}
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
if self.render_state.is_some() {
let time = Instant::now();
if !self.ran_update_since_last_redraw {
self.update();
self.render_state.as_ref().unwrap().window.request_redraw();
self.next_time_begin_frame = time + self.desired_draw_interval;
self.ran_update_since_last_redraw = true;
}
event_loop.set_control_flow(ControlFlow::Poll);
}
}
}
pub struct RenderState {
pub shaders: Assets<wgpu::ShaderModule>,
pub pipelines: Assets<PipelineSet>,
pub pipeline_bind_group_cache: Assets<wgpu::BindGroup>,
pub buffers: Buffers,
pub meshes: Assets<MeshBufferRange>,
pub textures: Assets<TextureSet>,
pub default_sampler: wgpu::Sampler,
pub materials: Assets<Box<dyn AsBindGroup>>,
pub material_bind_group_cache: Assets<wgpu::BindGroup>,
pub surface: wgpu::Surface<'static>,
pub surface_config: wgpu::SurfaceConfiguration,
pub surface_format: wgpu::TextureFormat,
pub background_color: wgpu::Color,
pub viewport: ViewportState,
pub capture_target: Option<PathBuf>,
pub device: wgpu::Device,
pub queue: wgpu::Queue,
pub window: Arc<Window>,
}
impl RenderState {
pub async fn new(window: Window) -> Self {
let surface_size = window.inner_size();
let window = Arc::new(window);
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
#[cfg(not(target_arch = "wasm32"))]
backends: wgpu::Backends::PRIMARY,
#[cfg(target_arch = "wasm32")]
backends: wgpu::Backends::GL,
..Default::default()
});
let surface = instance.create_surface(window.clone()).unwrap();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.unwrap();
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
required_limits: if cfg!(target_arch = "wasm32") {
wgpu::Limits::downlevel_webgl2_defaults()
} else {
wgpu::Limits::default()
},
..Default::default()
},
None,
)
.await
.unwrap();
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps
.formats
.iter()
.find(|f| f.is_srgb())
.copied()
.unwrap_or(surface_caps.formats[0]);
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
format: surface_format,
width: surface_size.width,
height: surface_size.height,
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: surface_caps.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 1,
};
surface.configure(&device, &surface_config);
let viewport = ViewportState::new(&device, &surface_config, None);
let default_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
mag_filter: wgpu::FilterMode::Linear,
..Default::default()
});
Self {
shaders: Default::default(),
pipelines: Default::default(),
pipeline_bind_group_cache: Default::default(),
buffers: Default::default(),
meshes: Default::default(),
textures: Default::default(),
default_sampler,
materials: Default::default(),
material_bind_group_cache: Default::default(),
surface,
surface_config,
surface_format,
background_color: wgpu::Color::BLACK,
viewport,
capture_target: None,
device,
queue,
window,
}
}
pub fn init_buffer<V: BufferItem>(&mut self, usage: wgpu::BufferUsages) {
self.buffers
.insert(DynamicBuffer::new::<V>(&self.device, usage));
}
pub fn register_shader(
&mut self,
shader_id: u32,
shader_module_descriptor: wgpu::ShaderModuleDescriptor,
) {
let shader = self.device.create_shader_module(shader_module_descriptor);
self.shaders.insert(shader_id, shader);
}
pub fn register_mesh<V: BufferItem>(&mut self, mesh_id: u32, mesh: Mesh<V>) {
let base_vertex = self
.buffers
.get_mut::<V>()
.unwrap()
.append(mesh.vertices)
.start as i32;
let index_range = self.buffers.get_mut::<u32>().unwrap().append(mesh.indices);
let mesh_range = MeshBufferRange {
base_vertex,
index_range: index_range.start..index_range.end,
};
if self.meshes.insert(mesh_id, mesh_range).is_some() {
warn!("duplicated mesh id {mesh_id}");
}
}
pub fn register_mesh_temporary<V: BufferItem>(&mut self, mesh: Mesh<V>) -> Handle {
let base_vertex = self
.buffers
.get_mut::<V>()
.unwrap()
.append_temporary(mesh.vertices)
.start as i32;
let index_range = self
.buffers
.get_mut::<u32>()
.unwrap()
.append_temporary(mesh.indices);
let mesh_range = MeshBufferRange {
base_vertex,
index_range: index_range.start..index_range.end,
};
self.meshes.push_temporary(mesh_range)
}
pub fn register_texture(&mut self, texture_id: u32, image: image::DynamicImage) {
let texture_set = TextureSet::new(&self, image);
if self.textures.insert(texture_id, texture_set).is_some() {
warn!("duplicated texture id {texture_id}");
}
let material = SpriteMaterial { texture_id };
if self
.materials
.insert(texture_id, Box::new(material))
.is_some()
{
warn!("duplicated material id {texture_id}");
}
}
pub fn resolution(&self) -> Vec2 {
self.viewport.viewport_texture.size()
}
}
pub struct ViewportState {
pub fixed_viewport_size: Option<wgpu::Extent3d>,
pub viewport_texture: TextureSet,
pub msaa_texture: TextureSet,
pub format: wgpu::TextureFormat,
pub scale_buffer: wgpu::Buffer,
pub capture_buffer: Arc<wgpu::Buffer>,
pub padded_bytes_per_row: u32,
}
impl ViewportState {
pub const TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
pub fn new(
device: &wgpu::Device,
surface_config: &wgpu::SurfaceConfiguration,
fixed_viewport_size: Option<wgpu::Extent3d>,
) -> Self {
let surface_size = wgpu::Extent3d {
width: surface_config.width,
height: surface_config.height,
depth_or_array_layers: 1,
};
let size = fixed_viewport_size.unwrap_or(surface_size);
let aspect = {
let scale = vec2(size.width as f32, size.height as f32)
/ vec2(surface_size.width as f32, surface_size.height as f32);
if scale.y > scale.x {
Vec2::new(scale.x / scale.y, 1.0)
} else {
Vec2::new(1.0, scale.y / scale.x)
}
};
let format = Self::TEXTURE_FORMAT;
let viewport_texture =
TextureSet::from_texture(device.create_texture(&wgpu::TextureDescriptor {
label: Some("viewport_texture"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
}));
let msaa_texture =
TextureSet::from_texture(device.create_texture(&wgpu::TextureDescriptor {
label: Some("msaa_texture"),
size,
mip_level_count: 1,
sample_count: 4,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
}));
let scale_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("viewport_scale_buffer"),
contents: bytemuck::cast_slice(&[aspect]),
usage: wgpu::BufferUsages::UNIFORM,
});
let block_size = format.block_copy_size(None).unwrap();
let padded_bytes_per_row =
((size.width * block_size - 1) / wgpu::COPY_BYTES_PER_ROW_ALIGNMENT + 1)
* wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let capture_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("viewport_capture_buffer"),
size: (padded_bytes_per_row * size.height) as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
ViewportState {
fixed_viewport_size,
viewport_texture,
msaa_texture,
format,
scale_buffer,
capture_buffer: Arc::new(capture_buffer),
padded_bytes_per_row,
}
}
pub fn copy_to_buffer(&mut self, encoder: &mut wgpu::CommandEncoder) {
encoder.copy_texture_to_buffer(
wgpu::ImageCopyTexture {
texture: &self.viewport_texture.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::ImageCopyBuffer {
buffer: &self.capture_buffer,
layout: wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(self.padded_bytes_per_row),
rows_per_image: None,
},
},
self.viewport_texture.texture.size(),
);
}
pub fn save_capture(&self, path: PathBuf) {
let capture_buffer = self.capture_buffer.clone();
let size = self.viewport_texture.texture.size();
let padded_bytes_per_row = self.padded_bytes_per_row;
self.capture_buffer
.slice(..)
.map_async(wgpu::MapMode::Read, move |result| {
if result.is_ok() {
let buf = capture_buffer.slice(..).get_mapped_range();
let mut data = Vec::with_capacity((size.width * size.height) as usize * 4);
for y in 0..size.height as usize {
let start = y * padded_bytes_per_row as usize;
let end = start + size.width as usize * 4;
data.extend_from_slice(&buf[start..end]);
}
drop(buf);
capture_buffer.unmap();
std::thread::spawn(move || {
let image =
image::RgbaImage::from_raw(size.width, size.height, data).unwrap();
match image.save(&path) {
Ok(_) => info!("Screen capture saved at {}", path.display()),
Err(_) => error!("Failed to save screen capture at {}", path.display()),
};
});
}
});
}
}
pub enum RenderCommand {
SetCamera,
Draw {
material_handle: Handle,
mesh_handle: Handle,
instance_range: Range<u32>,
},
}
#[derive(Debug, Clone)]
pub struct Assets<T> {
pub persistent: FxHashMap<u32, T>,
pub temporary: Vec<T>,
}
impl<T> Default for Assets<T> {
fn default() -> Self {
Assets::new()
}
}
impl<T> Assets<T> {
pub fn new() -> Self {
Assets {
persistent: Default::default(),
temporary: Default::default(),
}
}
pub fn get(&self, handle: impl Into<Handle>) -> Option<&T> {
match handle.into() {
Handle::Persistent(id) => self.persistent.get(&id),
Handle::Temporary(id) => self.temporary.get(id),
}
}
pub fn insert(&mut self, id: u32, value: T) -> Option<T> {
self.persistent.insert(id, value)
}
pub fn push_temporary(&mut self, value: T) -> Handle {
self.temporary.push(value);
Handle::Temporary(self.temporary.len() - 1)
}
pub fn remove(&mut self, handle: impl Into<Handle>) -> Option<T> {
match handle.into() {
Handle::Persistent(id) => self.persistent.remove(&id),
_ => None,
}
}
pub fn clear_temporary(&mut self) {
self.temporary.clear();
}
}
impl<T, I: Into<Handle>> Index<I> for Assets<T> {
type Output = T;
fn index(&self, handle: I) -> &Self::Output {
match handle.into() {
Handle::Persistent(id) => &self.persistent[&id],
Handle::Temporary(id) => &self.temporary[id],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Handle {
Persistent(u32),
Temporary(usize),
}
impl From<u32> for Handle {
fn from(value: u32) -> Self {
Handle::Persistent(value)
}
}
#[derive(Debug, Clone)]
pub struct MeshBufferRange {
pub base_vertex: i32,
pub index_range: Range<u32>,
}