use std::sync::Arc;
use glam::Mat4;
use winit::window::Window;
use super::batch::{self, DrawList, PanelDraw, SpriteInstance};
use super::timer::GpuTimer;
use super::{FrameStatus, RenderStats, Screenshot};
use crate::color::Color;
pub(super) struct GpuTexture {
pub(super) bind_group: wgpu::BindGroup,
pub(super) texture: wgpu::Texture,
pub(super) nearest: bool,
#[cfg(feature = "3d")]
pub(super) mips: Option<bool>,
}
pub(crate) struct Renderer {
window: Arc<Window>,
instance: wgpu::Instance,
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
view_format: wgpu::TextureFormat,
pipeline: wgpu::RenderPipeline,
camera_buffer: wgpu::Buffer,
camera_bind_group: wgpu::BindGroup,
ui_camera_buffer: wgpu::Buffer,
ui_camera_bind_group: wgpu::BindGroup,
sprite_shader: wgpu::ShaderModule,
sprite_layout: wgpu::PipelineLayout,
camera_layout: wgpu::BindGroupLayout,
panel_pipeline: Option<wgpu::RenderPipeline>,
panel_cameras: Vec<(wgpu::Buffer, wgpu::BindGroup)>,
texture_layout: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
nearest_sampler: wgpu::Sampler,
textures: Vec<GpuTexture>,
instance_buffer: wgpu::Buffer,
instance_capacity: usize,
timer: Option<GpuTimer>,
draw_hidden: bool,
only_hidden: bool,
hidden_target: Option<wgpu::Texture>,
hidden_in_flight: std::collections::VecDeque<wgpu::SubmissionIndex>,
stats: RenderStats,
#[cfg(feature = "3d")]
three: Option<super::gpu3d::Renderer3D>,
#[cfg(feature = "3d")]
msaa: u32,
#[cfg(feature = "3d")]
tiled: bool,
#[cfg(feature = "3d")]
mips: Option<super::mips::MipMaker>,
#[cfg(feature = "3d")]
prepare_3d_ms: f32,
#[cfg(feature = "3d")]
collect_ms: f32,
display_wait_ms: f32,
screenshot: Option<Screenshot>,
#[cfg(feature = "inspector")]
overlay: Option<crate::inspector::Overlay>,
#[cfg(feature = "inspector")]
egui: Option<egui_wgpu::Renderer>,
#[cfg(feature = "inspector")]
egui_free: Vec<egui::TextureId>,
}
fn sprite_pipeline(
device: &wgpu::Device,
layout: &wgpu::PipelineLayout,
shader: &wgpu::ShaderModule,
format: wgpu::TextureFormat,
) -> wgpu::RenderPipeline {
let instance_attributes = wgpu::vertex_attr_array![
0 => Float32x2, 1 => Float32x2, 2 => Float16x2, 3 => Float32, 4 => Unorm8x4, 5 => Unorm16x4, ];
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("sprite pipeline"),
layout: Some(layout),
vertex: wgpu::VertexState {
module: shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: size_of::<SpriteInstance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &instance_attributes,
})],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
})
}
struct SpritePass<'a> {
viewport: [u32; 2],
scale: f32,
pipeline: &'a wgpu::RenderPipeline,
instances: &'a wgpu::Buffer,
textures: &'a [GpuTexture],
layers: &'a [(&'a wgpu::BindGroup, &'a [batch::Batch], u32)],
}
struct Capture {
texture: wgpu::Texture,
buffer: wgpu::Buffer,
width: u32,
height: u32,
padded_row: u32,
bgra: bool,
}
impl Capture {
fn new(device: &wgpu::Device, width: u32, height: u32, format: wgpu::TextureFormat) -> Self {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("screenshot"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let padded_row = (width * 4).next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("screenshot readback"),
size: u64::from(padded_row) * u64::from(height),
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bgra = matches!(
format,
wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
);
Self {
texture,
buffer,
width,
height,
padded_row,
bgra,
}
}
fn copy(&self, encoder: &mut wgpu::CommandEncoder) {
encoder.copy_texture_to_buffer(
self.texture.as_image_copy(),
wgpu::TexelCopyBufferInfo {
buffer: &self.buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(self.padded_row),
rows_per_image: Some(self.height),
},
},
wgpu::Extent3d {
width: self.width,
height: self.height,
depth_or_array_layers: 1,
},
);
}
fn read(self, device: &wgpu::Device) -> Screenshot {
self.buffer.map_async(wgpu::MapMode::Read, .., |_| {});
let _ = device.poll(wgpu::PollType::wait_indefinitely());
let mut rgba = Vec::with_capacity((self.width * self.height * 4) as usize);
if let Ok(view) = self.buffer.get_mapped_range(..) {
for row in view.chunks_exact(self.padded_row as usize) {
rgba.extend_from_slice(&row[..(self.width * 4) as usize]);
}
}
if self.bgra {
for px in rgba.as_chunks_mut::<4>().0 {
px.swap(0, 2);
}
}
Screenshot {
width: self.width,
height: self.height,
rgba,
}
}
}
const INITIAL_INSTANCE_CAPACITY: usize = 1024;
impl Renderer {
pub fn stats(&self) -> RenderStats {
self.stats
}
#[cfg(feature = "3d")]
pub fn mesh_costs(&self) -> Vec<(u32, usize, usize, usize, usize)> {
self.three.as_ref().map(|t| t.mesh_costs()).unwrap_or_default()
}
pub fn set_draw_hidden(&mut self, on: bool, always: bool) {
self.draw_hidden = on || always;
self.only_hidden = always;
}
fn hidden_view(&mut self) -> wgpu::TextureView {
let (width, height) = (self.config.width, self.config.height);
let fits = self
.hidden_target
.as_ref()
.is_some_and(|t| t.width() == width && t.height() == height);
if !fits {
self.hidden_target = Some(self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("offscreen frame"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: self.view_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
}));
}
self.hidden_target
.as_ref()
.map(|t| t.create_view(&wgpu::TextureViewDescriptor::default()))
.expect("made above")
}
pub fn gpu_passes(&self) -> &[crate::profiler::GpuPass] {
self.timer.as_ref().map_or(&[], |t| &t.passes)
}
pub fn gpu_frame(&self) -> Option<(&[crate::profiler::PassSpan], u64)> {
self.timer.as_ref().map(|t| (&t.last_frame[..], t.frames_read))
}
pub fn take_screenshot(&mut self) -> Option<Screenshot> {
self.screenshot.take()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn new(window: Arc<Window>, display: winit::event_loop::OwnedDisplayHandle, vsync: bool, msaa: u32) -> Self {
pollster::block_on(Self::new_async(window, display, vsync, msaa))
}
pub async fn new_async(
window: Arc<Window>,
display: winit::event_loop::OwnedDisplayHandle,
vsync: bool,
msaa: u32,
) -> Self {
let descriptor = wgpu::InstanceDescriptor::new_with_display_handle_from_env(Box::new(display));
#[cfg(target_arch = "wasm32")]
let instance = wgpu::util::new_instance_with_webgpu_detection(descriptor).await;
#[cfg(not(target_arch = "wasm32"))]
let instance = wgpu::Instance::new(descriptor);
let surface = instance
.create_surface(window.clone())
.unwrap_or_else(|e| panic!("ponko: could not create a GPU surface: {e}"));
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
..Default::default()
})
.await
.unwrap_or_else(|e| panic!("ponko: no compatible GPU found: {e}"));
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("ponko device"),
required_features: adapter.features()
& (wgpu::Features::TIMESTAMP_QUERY | wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES),
required_limits: if cfg!(target_arch = "wasm32") {
adapter.limits()
} else {
wgpu::Limits {
max_sampled_textures_per_shader_stage: adapter
.limits()
.max_sampled_textures_per_shader_stage
.clamp(16, 24),
..wgpu::Limits::default()
}
},
..Default::default()
})
.await
.unwrap_or_else(|e| panic!("ponko: could not open the GPU device: {e}"));
#[cfg(feature = "3d")]
let msaa = if super::gpu3d::multisamples(&adapter, msaa) {
msaa
} else {
eprintln!("ponko: this GPU can't do {msaa}x multisampling here; using 4x");
4
};
#[cfg(not(feature = "3d"))]
let _ = msaa;
#[cfg(feature = "3d")]
let tiled = adapter.get_info().backend == wgpu::Backend::Metal;
let size = window.inner_size();
let mut config = surface
.get_default_config(&adapter, size.width.max(1), size.height.max(1))
.expect("ponko: the GPU can't draw to this window");
let caps = surface.get_capabilities(&adapter);
if let Some(srgb) = caps.formats.iter().copied().find(|f| f.is_srgb()) {
config.format = srgb;
}
let view_format = config.format.add_srgb_suffix();
if view_format != config.format {
config.view_formats.push(view_format);
}
config.present_mode = if vsync {
wgpu::PresentMode::AutoVsync
} else {
wgpu::PresentMode::AutoNoVsync
};
config.desired_maximum_frame_latency = 2;
surface.configure(&device, &config);
let shader = device.create_shader_module(wgpu::include_wgsl!("sprite.wgsl"));
let camera_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("camera layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("sprite texture layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("sprite pipeline layout"),
bind_group_layouts: &[Some(&camera_layout), Some(&texture_layout)],
immediate_size: 0,
});
let pipeline = sprite_pipeline(&device, &layout, &shader, view_format);
let camera = |label| {
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: size_of::<Mat4>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(label),
layout: &camera_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer.as_entire_binding(),
}],
});
(buffer, bind_group)
};
let (camera_buffer, camera_bind_group) = camera("camera");
let (ui_camera_buffer, ui_camera_bind_group) = camera("ui camera");
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("sprite sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let nearest_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("pixel art sampler"),
..Default::default() });
let instance_buffer = Self::create_instance_buffer(&device, INITIAL_INSTANCE_CAPACITY);
let timer = GpuTimer::new(&device, &queue);
Self {
window,
instance,
surface,
device,
queue,
config,
view_format,
pipeline,
camera_buffer,
camera_bind_group,
ui_camera_buffer,
ui_camera_bind_group,
sprite_shader: shader,
sprite_layout: layout,
camera_layout,
panel_pipeline: None,
panel_cameras: Vec::new(),
texture_layout,
sampler,
nearest_sampler,
textures: Vec::new(),
instance_buffer,
instance_capacity: INITIAL_INSTANCE_CAPACITY,
timer,
draw_hidden: false,
only_hidden: false,
hidden_target: None,
hidden_in_flight: std::collections::VecDeque::with_capacity(4),
stats: RenderStats::default(),
#[cfg(feature = "3d")]
three: None,
#[cfg(feature = "3d")]
msaa,
#[cfg(feature = "3d")]
tiled,
#[cfg(feature = "3d")]
mips: None,
display_wait_ms: 0.0,
#[cfg(feature = "3d")]
prepare_3d_ms: 0.0,
#[cfg(feature = "3d")]
collect_ms: 0.0,
screenshot: None,
#[cfg(feature = "inspector")]
overlay: None,
#[cfg(feature = "inspector")]
egui: None,
#[cfg(feature = "inspector")]
egui_free: Vec::new(),
}
}
fn create_instance_buffer(device: &wgpu::Device, capacity: usize) -> wgpu::Buffer {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("sprite instances"),
size: (capacity * size_of::<SpriteInstance>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
#[cfg(feature = "3d")]
fn three(&mut self) -> &mut super::gpu3d::Renderer3D {
if self.three.is_none() {
let white = self.create_texture(1, 1, &[255; 4]);
let white_array = self.upload_texture_array(1, 1, &[&[255; 4]], false);
self.three = Some(super::gpu3d::Renderer3D::new(
&self.device,
&self.texture_layout,
self.view_format,
white,
&white_array,
self.msaa,
!self.tiled,
));
}
self.three.as_mut().expect("just made")
}
#[cfg(feature = "3d")]
pub fn create_material(&mut self, desc: &crate::three::GpuMaterialDesc) -> u32 {
self.material_mips(desc);
let device = self.device.clone();
self.three();
let three = self.three.as_mut().expect("just made");
three.create_material(&device, desc, &self.textures)
}
#[cfg(feature = "3d")]
pub fn update_material(&mut self, id: u32, desc: &crate::three::GpuMaterialDesc) {
self.material_mips(desc);
if let Some(three) = &mut self.three {
three.update_material(&self.device, &self.queue, id, desc, &self.textures);
}
}
#[cfg(feature = "3d")]
pub fn supports_decals(&self) -> bool {
super::gpu3d::supports_decals(&self.device)
}
#[cfg(feature = "3d")]
pub fn supports_gi(&self) -> bool {
super::gpu3d::supports_gi(&self.device)
}
#[cfg(feature = "3d")]
pub fn max_texture_array_size(&self) -> (u32, u32) {
let limits = self.device.limits();
(limits.max_texture_dimension_2d, limits.max_texture_array_layers)
}
#[cfg(feature = "3d")]
pub fn create_texture_array(&mut self, width: u32, height: u32, layers: &[&[u8]], linear: bool) -> u32 {
let texture = self.upload_texture_array(width, height, layers, linear);
self.three().add_texture_array(&texture)
}
#[cfg(feature = "3d")]
fn upload_texture_array(&mut self, width: u32, height: u32, layers: &[&[u8]], linear: bool) -> wgpu::Texture {
let size = wgpu::Extent3d {
width,
height,
depth_or_array_layers: layers.len() as u32,
};
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("3d texture array"),
size,
mip_level_count: super::mips::levels(width, height),
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: if linear {
wgpu::TextureFormat::Rgba8Unorm
} else {
wgpu::TextureFormat::Rgba8UnormSrgb
},
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
for (i, rgba) in layers.iter().enumerate() {
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: 0,
y: 0,
z: i as u32,
},
aspect: wgpu::TextureAspect::All,
},
rgba,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * width),
rows_per_image: Some(height),
},
wgpu::Extent3d {
depth_or_array_layers: 1,
..size
},
);
}
let mut encoder = self.device.create_command_encoder(&Default::default());
let mips = self
.mips
.get_or_insert_with(|| super::mips::MipMaker::new(&self.device));
mips.generate(&self.device, &mut encoder, &texture, linear);
self.queue.submit([encoder.finish()]);
texture
}
#[cfg(feature = "3d")]
fn material_mips(&mut self, desc: &crate::three::GpuMaterialDesc) {
let linear = [false, true, true, false, false, false];
for (map, linear) in desc.textures.iter().zip(linear) {
if let Some(id) = *map {
self.ensure_mips(id, linear);
}
}
}
#[cfg(feature = "3d")]
pub fn create_gpu_buffer(&mut self, id: u32, len: u32, data: &[crate::Vec4]) {
let device = self.device.clone();
self.three().create_gpu_buffer(&device, id, len, data);
}
#[cfg(feature = "3d")]
pub fn free_gpu_buffer(&mut self, id: u32) {
self.three().free_gpu_buffer(id);
}
#[cfg(feature = "3d")]
pub fn write_gpu_buffer(&mut self, id: u32, offset: u32, data: &[crate::Vec4]) {
let queue = self.queue.clone();
self.three().write_gpu_buffer(&queue, id, offset, data);
}
#[cfg(feature = "3d")]
pub fn set_instance_batch(&mut self, id: u32, batch: Option<&crate::three::instances::Batch>) {
let device = self.device.clone();
self.three().set_instance_batch(&device, id, batch);
}
#[cfg(feature = "3d")]
pub fn set_mesh_gpu_buffer(&mut self, mesh: u32, b: u32, count: u32) {
self.three().set_mesh_gpu_buffer(mesh, b, count);
}
#[cfg(feature = "3d")]
pub fn create_compute(&mut self, name: &str, source: &str) -> Result<u32, String> {
let device = self.device.clone();
self.three().create_compute(&device, name, source)
}
#[cfg(feature = "3d")]
pub fn replace_compute(&mut self, id: u32, name: &str, source: &str) -> Result<(), String> {
let device = self.device.clone();
self.three().replace_compute(&device, id, name, source)
}
#[cfg(feature = "3d")]
pub fn create_effect(&mut self, name: &str, source: &str) -> Result<u32, String> {
let device = self.device.clone();
self.three().create_effect(&device, name, source)
}
#[cfg(feature = "3d")]
pub fn replace_effect(&mut self, id: u32, name: &str, source: &str) -> Result<(), String> {
let device = self.device.clone();
self.three().replace_effect(&device, id, name, source)
}
#[cfg(feature = "3d")]
pub fn create_shader(&mut self, name: &str, source: &str) -> Result<u32, String> {
let device = self.device.clone();
self.three().create_shader(&device, name, source)
}
#[cfg(feature = "3d")]
pub fn replace_shader(&mut self, id: u32, name: &str, source: &str) -> Result<(), String> {
let device = self.device.clone();
self.three().replace_shader(&device, id, name, source)
}
#[cfg(feature = "3d")]
#[cfg(feature = "3d")]
pub fn create_mesh(
&mut self,
id: u32,
data: &crate::MeshData,
parts: &[(std::ops::Range<u32>, crate::Material)],
skinned: bool,
) {
let device = self.device.clone();
self.three().create_mesh_at(&device, id, data, parts, skinned);
}
#[cfg(feature = "3d")]
pub fn free_mesh(&mut self, id: u32) {
self.three().free_mesh(id);
}
#[cfg(feature = "3d")]
pub fn take_occlusion(&mut self) -> Option<crate::three::occlusion::OcclusionGrid> {
self.three.as_mut()?.take_occlusion()
}
#[cfg(feature = "3d")]
pub fn prepare_3d(&mut self, scene: &crate::three::Scene3D<'_>, collect_ms: f32) {
let start = web_time::Instant::now();
let size = (self.config.width, self.config.height);
let lut = scene.post.lut.filter(|t| {
let n = t.height();
(2..=64).contains(&n) && t.width() == n * n && (t.id as usize) < self.textures.len()
});
if let Some(three) = &mut self.three {
let lut = lut.map(|t| (t.id, t.height()));
let input = super::gpu3d::Prepare3D { scene, lut, size };
three.prepare(&self.device, &self.queue, &input);
}
self.prepare_3d_ms = collect_ms + start.elapsed().as_secs_f32() * 1000.0;
self.collect_ms = collect_ms;
}
pub fn display_wait_ms(&self) -> f32 {
self.display_wait_ms
}
pub fn max_texture_size(&self) -> u32 {
self.device.limits().max_texture_dimension_2d
}
#[cfg(feature = "3d")]
pub fn create_render_target(&mut self, width: u32, height: u32) -> u32 {
self.three();
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("render target"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: super::gpu3d::HDR_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let bind_group = self.sprite_bind_group(&texture, false);
let id = self.textures.len() as u32;
self.three().add_render_target(id, &texture);
self.textures.push(GpuTexture {
bind_group,
texture,
nearest: false,
mips: Some(false),
});
id
}
pub fn create_texture(&mut self, width: u32, height: u32, rgba: &[u8]) -> u32 {
debug_assert_eq!(rgba.len(), (width * height * 4) as usize);
let size = wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
};
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("sprite texture"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC,
view_formats: &[wgpu::TextureFormat::Rgba8Unorm],
});
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
rgba,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * width),
rows_per_image: Some(height),
},
size,
);
let bind_group = self.sprite_bind_group(&texture, false);
self.textures.push(GpuTexture {
bind_group,
texture,
nearest: false,
#[cfg(feature = "3d")]
mips: None,
});
(self.textures.len() - 1) as u32
}
fn sprite_bind_group(&self, texture: &wgpu::Texture, nearest: bool) -> wgpu::BindGroup {
let view = texture.create_view(&wgpu::TextureViewDescriptor {
mip_level_count: Some(1),
..Default::default()
});
self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("sprite texture"),
layout: &self.texture_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(if nearest {
&self.nearest_sampler
} else {
&self.sampler
}),
},
],
})
}
#[cfg(feature = "3d")]
fn ensure_mips(&mut self, id: u32, linear: bool) {
let Some(tex) = self.textures.get(id as usize) else {
return;
};
if tex.mips.is_some() {
return;
}
let (width, height) = (tex.texture.width(), tex.texture.height());
let levels = super::mips::levels(width, height);
if levels == 1 {
self.textures[id as usize].mips = Some(linear);
return;
}
let size = wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
};
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("material texture"),
size,
mip_level_count: levels,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[wgpu::TextureFormat::Rgba8Unorm],
});
let mut encoder = self.device.create_command_encoder(&Default::default());
encoder.copy_texture_to_texture(
self.textures[id as usize].texture.as_image_copy(),
texture.as_image_copy(),
size,
);
let mips = self
.mips
.get_or_insert_with(|| super::mips::MipMaker::new(&self.device));
mips.generate(&self.device, &mut encoder, &texture, linear);
self.queue.submit([encoder.finish()]);
let bind_group = self.sprite_bind_group(&texture, self.textures[id as usize].nearest);
let tex = &mut self.textures[id as usize];
tex.texture = texture;
tex.bind_group = bind_group;
tex.mips = Some(linear);
}
fn refresh_mips(&mut self, id: u32) {
#[cfg(feature = "3d")]
if let Some(linear) = self.textures[id as usize].mips
&& let Some(mips) = &self.mips
{
let mut encoder = self.device.create_command_encoder(&Default::default());
mips.generate(&self.device, &mut encoder, &self.textures[id as usize].texture, linear);
self.queue.submit([encoder.finish()]);
}
#[cfg(not(feature = "3d"))]
let _ = id;
}
pub fn update_texture(&mut self, id: u32, width: u32, height: u32, rgba: &[u8]) {
debug_assert_eq!(rgba.len(), (width * height * 4) as usize);
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &self.textures[id as usize].texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
rgba,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * width),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
self.refresh_mips(id);
}
pub fn set_filter(&mut self, id: u32, nearest: bool) {
let bind_group = self.sprite_bind_group(&self.textures[id as usize].texture, nearest);
let tex = &mut self.textures[id as usize];
tex.bind_group = bind_group;
tex.nearest = nearest;
}
pub fn update_texture_region(&mut self, id: u32, at: [u32; 2], size: [u32; 2], rgba: &[u8]) {
debug_assert_eq!(rgba.len(), (size[0] * size[1] * 4) as usize);
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &self.textures[id as usize].texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: at[0],
y: at[1],
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
rgba,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * size[0]),
rows_per_image: Some(size[1]),
},
wgpu::Extent3d {
width: size[0],
height: size[1],
depth_or_array_layers: 1,
},
);
self.refresh_mips(id);
}
pub fn resize(&mut self, width: u32, height: u32) {
if width == 0 || height == 0 {
return; }
self.config.width = width;
self.config.height = height;
self.surface.configure(&self.device, &self.config);
}
#[allow(clippy::too_many_arguments)]
pub fn render(
&mut self,
world: &mut DrawList,
ui: &mut DrawList,
panels: &mut [PanelDraw],
clear: Color,
world_view: Mat4,
ui_view: Mat4,
capture: bool,
) -> FrameStatus {
let acquire_start = web_time::Instant::now();
let acquired = if self.only_hidden {
wgpu::CurrentSurfaceTexture::Occluded
} else {
self.surface.get_current_texture()
};
self.display_wait_ms = acquire_start.elapsed().as_secs_f32() * 1000.0;
let (frame, status) = match acquired {
wgpu::CurrentSurfaceTexture::Success(t) => (Some(t), FrameStatus::Presented),
wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
self.surface.configure(&self.device, &self.config);
(Some(t), FrameStatus::Presented)
}
wgpu::CurrentSurfaceTexture::Occluded => (None, FrameStatus::Occluded),
wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Validation => {
(None, FrameStatus::Skipped)
}
wgpu::CurrentSurfaceTexture::Outdated => {
self.surface.configure(&self.device, &self.config);
(None, FrameStatus::Skipped)
}
wgpu::CurrentSurfaceTexture::Lost => {
self.surface = self
.instance
.create_surface(self.window.clone())
.unwrap_or_else(|e| panic!("ponko: lost the GPU surface: {e}"));
self.surface.configure(&self.device, &self.config);
(None, FrameStatus::Skipped)
}
};
if frame.is_none() && !capture && !self.draw_hidden {
return status;
}
let hidden = frame.is_none() && self.draw_hidden;
let status = if hidden { FrameStatus::Offscreen } else { status };
let mut hidden_view = hidden.then(|| self.hidden_view());
if hidden {
let wait = web_time::Instant::now();
while self.hidden_in_flight.len() >= 2 {
if let Some(oldest) = self.hidden_in_flight.pop_front() {
let _ = self.device.poll(wgpu::PollType::Wait {
submission_index: Some(oldest),
timeout: Some(std::time::Duration::from_secs(1)),
});
}
}
self.display_wait_ms += wait.elapsed().as_secs_f32() * 1000.0;
}
let cpu_start = web_time::Instant::now();
if let Some(timer) = &mut self.timer {
timer.collect(&self.device);
timer.begin_frame();
}
let (world, world_batches) = world.finish();
let (ui, ui_batches) = ui.finish();
let panels: Vec<_> = panels
.iter_mut()
.filter(|p| (p.texture as usize) < self.textures.len())
.map(|p| (p.texture, p.size, p.list.finish()))
.collect();
let panel_sprites: usize = panels.iter().map(|(_, _, (sprites, _))| sprites.len()).sum();
let total = world.len() + ui.len() + panel_sprites;
self.stats.sprites = total;
self.stats.draw_calls = world_batches.len()
+ ui_batches.len()
+ panels.iter().map(|(_, _, (_, batches))| batches.len()).sum::<usize>();
#[cfg(feature = "3d")]
if let Some(three) = &self.three {
self.stats.draw_calls += three.draw_calls();
let [(instances, triangles), (shadow_instances, shadow_triangles)] = three.counts();
self.stats.instances = instances;
self.stats.triangles = triangles;
self.stats.shadow_instances = shadow_instances;
self.stats.shadow_triangles = shadow_triangles;
self.stats.occluded = three.occluded;
self.stats.collect_ms = self.collect_ms;
}
if total > self.instance_capacity {
self.instance_capacity = total.next_power_of_two();
self.instance_buffer = Self::create_instance_buffer(&self.device, self.instance_capacity);
}
if !world.is_empty() {
self.queue
.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(world));
}
if !ui.is_empty() {
let offset = std::mem::size_of_val(world) as u64;
self.queue
.write_buffer(&self.instance_buffer, offset, bytemuck::cast_slice(ui));
}
let mut panel_base = (world.len() + ui.len()) as u32;
let mut panel_starts = Vec::with_capacity(panels.len());
for (i, (_, size, (sprites, _))) in panels.iter().enumerate() {
if !sprites.is_empty() {
let offset = u64::from(panel_base) * size_of::<SpriteInstance>() as u64;
self.queue
.write_buffer(&self.instance_buffer, offset, bytemuck::cast_slice(sprites));
}
if i == self.panel_cameras.len() {
self.panel_cameras.push(self.make_camera("ui panel camera"));
}
let view = crate::ctx::screen_projection(glam::Vec2::new(size[0] as f32, size[1] as f32));
self.queue
.write_buffer(&self.panel_cameras[i].0, 0, bytemuck::cast_slice(&view.to_cols_array()));
panel_starts.push(panel_base);
panel_base += sprites.len() as u32;
}
if !panels.is_empty() && self.panel_pipeline.is_none() {
#[cfg(feature = "3d")]
let format = super::gpu3d::HDR_FORMAT;
#[cfg(not(feature = "3d"))]
let format = self.view_format;
self.panel_pipeline = Some(sprite_pipeline(
&self.device,
&self.sprite_layout,
&self.sprite_shader,
format,
));
}
self.queue.write_buffer(
&self.camera_buffer,
0,
bytemuck::cast_slice(&world_view.to_cols_array()),
);
self.queue.write_buffer(
&self.ui_camera_buffer,
0,
bytemuck::cast_slice(&ui_view.to_cols_array()),
);
let layers = [
(&self.camera_bind_group, world_batches, 0),
(&self.ui_camera_bind_group, ui_batches, world.len() as u32),
];
let pass = SpritePass {
viewport: [self.config.width, self.config.height],
scale: self.window.scale_factor() as f32,
pipeline: &self.pipeline,
instances: &self.instance_buffer,
textures: &self.textures,
layers: &layers,
};
let panel_commands = self
.panel_pipeline
.as_ref()
.filter(|_| !panels.is_empty())
.map(|pipeline| {
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("ui panels"),
});
for (i, (texture, size, (_, batches))) in panels.iter().enumerate() {
let view = self.textures[*texture as usize]
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let layers = [(&self.panel_cameras[i].1, *batches, panel_starts[i])];
let pass = SpritePass {
viewport: *size,
scale: 1.0,
pipeline,
instances: &self.instance_buffer,
textures: &self.textures,
layers: &layers,
};
Self::sprite_pass(&mut encoder, &view, Some(Color::TRANSPARENT), None, &pass);
}
encoder.finish()
});
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("frame") });
let view = match &frame {
Some(frame) => Some(frame.texture.create_view(&wgpu::TextureViewDescriptor {
format: Some(self.view_format),
..Default::default()
})),
None => hidden_view.take(),
};
let mut buffers_3d = Vec::new();
let record_start = web_time::Instant::now();
if let Some(view) = &view {
let timer = self.timer.as_ref();
let sprite_clear = self.draw_3d(&mut buffers_3d, view, clear, timer);
Self::sprite_pass(
&mut encoder,
view,
sprite_clear,
timer.and_then(|t| t.pass("sprites")),
&pass,
);
}
let capture = capture.then(|| {
let target = Capture::new(&self.device, self.config.width, self.config.height, self.view_format);
let target_view = target.texture.create_view(&wgpu::TextureViewDescriptor::default());
let sprite_clear = self.draw_3d(&mut buffers_3d, &target_view, clear, None);
Self::sprite_pass(&mut encoder, &target_view, sprite_clear, None, &pass);
target.copy(&mut encoder);
target
});
#[cfg(feature = "inspector")]
let overlay_commands = match &view {
Some(view) => self.draw_overlay(&mut encoder, view),
None => Vec::new(),
};
#[cfg(not(feature = "inspector"))]
let overlay_commands: [wgpu::CommandBuffer; 0] = [];
if let Some(timer) = &mut self.timer {
timer.resolve(&mut encoder);
}
let finished = encoder.finish();
self.stats.record_ms = record_start.elapsed().as_secs_f32() * 1000.0;
let submit_start = web_time::Instant::now();
let submitted = self.queue.submit(
panel_commands
.into_iter()
.chain(overlay_commands)
.chain(buffers_3d)
.chain([finished]),
);
self.stats.submit_ms = submit_start.elapsed().as_secs_f32() * 1000.0;
if hidden {
self.hidden_in_flight.push_back(submitted);
}
#[cfg(feature = "3d")]
if let Some(three) = &mut self.three {
three.submitted();
}
if let Some(timer) = &mut self.timer {
timer.submitted();
self.stats.gpu_ms = timer.smoothed_ms;
}
self.stats.cpu_ms = cpu_start.elapsed().as_secs_f32() * 1000.0;
#[cfg(feature = "3d")]
{
self.stats.cpu_ms += std::mem::take(&mut self.prepare_3d_ms);
}
if let Some(frame) = frame {
self.window.pre_present_notify();
self.queue.present(frame);
}
if let Some(capture) = capture {
self.screenshot = Some(capture.read(&self.device));
}
status
}
#[cfg(feature = "inspector")]
pub fn set_overlay(&mut self, mut overlay: crate::inspector::Overlay) {
if let Some(older) = self.overlay.take() {
overlay.merge_older(older);
}
self.overlay = Some(overlay);
}
#[cfg(feature = "inspector")]
fn draw_overlay(
&mut self,
encoder: &mut wgpu::CommandEncoder,
view: &wgpu::TextureView,
) -> Vec<wgpu::CommandBuffer> {
let Some(overlay) = self.overlay.take() else {
return Vec::new();
};
let renderer = self.egui.get_or_insert_with(|| {
let options = egui_wgpu::RendererOptions {
msaa_samples: 1,
depth_stencil_format: None,
dithering: true,
predictable_texture_filtering: false,
};
egui_wgpu::Renderer::new(&self.device, self.view_format, options)
});
for id in self.egui_free.drain(..) {
renderer.free_texture(&id);
}
for (id, deltas) in &overlay.textures.set {
for delta in deltas {
renderer.update_texture(&self.device, &self.queue, *id, delta);
}
}
self.egui_free.extend(overlay.textures.free.iter().copied());
let screen = egui_wgpu::ScreenDescriptor {
size_in_pixels: [self.config.width, self.config.height],
pixels_per_point: overlay.pixels_per_point,
};
let commands = renderer.update_buffers(&self.device, &self.queue, encoder, &overlay.primitives, &screen);
let mut pass = encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("inspector"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
})
.forget_lifetime();
renderer.render(&mut pass, &overlay.primitives, &screen);
commands
}
#[cfg_attr(not(feature = "3d"), allow(clippy::unused_self))]
fn draw_3d(
&self,
buffers: &mut Vec<wgpu::CommandBuffer>,
view: &wgpu::TextureView,
clear: Color,
timer: Option<&GpuTimer>,
) -> Option<Color> {
#[cfg(feature = "3d")]
if let Some(three) = self.three.as_ref().filter(|t| t.has_draws()) {
buffers.extend(three.draw(&self.device, view, clear.to_linear_wgpu(), &self.textures, timer));
return None;
}
let _ = (buffers, view, timer);
Some(clear)
}
fn make_camera(&self, label: &str) -> (wgpu::Buffer, wgpu::BindGroup) {
let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: size_of::<Mat4>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(label),
layout: &self.camera_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer.as_entire_binding(),
}],
});
(buffer, bind_group)
}
fn sprite_pass(
encoder: &mut wgpu::CommandEncoder,
view: &wgpu::TextureView,
clear: Option<Color>,
timestamp_writes: Option<wgpu::RenderPassTimestampWrites<'_>>,
p: &SpritePass<'_>,
) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("sprites"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: clear.map_or(wgpu::LoadOp::Load, |c| wgpu::LoadOp::Clear(c.to_linear_wgpu())),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes,
occlusion_query_set: None,
multiview_mask: None,
});
if p.layers.iter().all(|(_, batches, _)| batches.is_empty()) {
return;
}
pass.set_pipeline(p.pipeline);
pass.set_vertex_buffer(0, p.instances.slice(..));
for (camera, batches, base) in p.layers {
pass.set_bind_group(0, *camera, &[]);
for batch in *batches {
let [width, height] = p.viewport;
let (x, y, w, h) = batch.clip.map_or((0, 0, width, height), |r| {
let x = (r.min.x * p.scale).floor().clamp(0.0, width as f32) as u32;
let y = (r.min.y * p.scale).floor().clamp(0.0, height as f32) as u32;
let right = (r.max.x * p.scale).ceil().clamp(x as f32, width as f32) as u32;
let bottom = (r.max.y * p.scale).ceil().clamp(y as f32, height as f32) as u32;
(x, y, right - x, bottom - y)
});
if w == 0 || h == 0 {
continue;
}
pass.set_scissor_rect(x, y, w, h);
pass.set_bind_group(1, &p.textures[batch.texture as usize].bind_group, &[]);
pass.draw(0..6, base + batch.start..base + batch.end);
}
}
}
}