tirbofish/dropbear · diff
feature: mipmaps (fixes #77) Unverified
@@ -140,7 +140,16 @@ impl AssetRegistry { return Arc::clone(existing.value()); } - let texture = Texture::from_bytes_verbose(&graphics.device, &graphics.queue, &rgba, Some((1, 1)), None, None, None); + let label = format!("Solid texture [{}, {}, {}, {}]", rgba[0], rgba[1], rgba[2], rgba[3]); + + let texture = Texture::from_bytes_verbose_mipmapped( + graphics, + &rgba, + Some((1, 1)), + None, + None, + Some(label.as_str()) + ); let texture = Arc::new(texture); match self.solid_textures.entry(key) { @@ -1,3 +1,4 @@ +use std::ops::{Deref, DerefMut}; use crate::{BindGroupLayouts, texture}; use crate::{ State, @@ -11,12 +12,9 @@ use std::sync::Arc; use wgpu::*; use winit::window::Window; -pub const NO_TEXTURE: &[u8] = include_bytes!("../../../resources/textures/no-texture.png"); +use crate::mipmap::MipMapper; -pub struct FrameGraphicsContext<'a> { - pub view: TextureView, - pub encoder: &'a mut CommandEncoder, -} +pub const NO_TEXTURE: &[u8] = include_bytes!("../../../resources/textures/no-texture.png"); pub struct SharedGraphicsContext { pub device: Arc<Device>, @@ -32,6 +30,7 @@ pub struct SharedGraphicsContext { pub texture_id: Arc<TextureId>, pub future_queue: Arc<FutureQueue>, pub supports_storage: bool, + pub mipmapper: Arc<MipMapper>, } impl SharedGraphicsContext { @@ -73,6 +72,7 @@ impl SharedGraphicsContext { surface: state.surface.clone(), surface_format: state.surface_format, supports_storage: state.supports_storage, + mipmapper: state.mipmapper.clone(), } } } @@ -187,3 +187,49 @@ impl InstanceRaw { } } } + + +/// A wrapper to the [wgpu::CommandEncoder] +pub struct CommandEncoder { + inner: wgpu::CommandEncoder, +} + +impl Deref for CommandEncoder { + type Target = wgpu::CommandEncoder; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for CommandEncoder { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl CommandEncoder { + /// Creates a new instance of a command encoder. + pub fn new(graphics: Arc<SharedGraphicsContext>, label: Option<&str>) -> Self { + Self { + inner: graphics.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }), + } + } + + /// Submits the command encoder for execution. + /// + /// Panics if an unwinding error is caught, or just returns the error as normal. + pub fn submit(self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()> { + let command_buffer = self.inner.finish(); + + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + graphics.queue.submit(std::iter::once(command_buffer)); + })) { + Ok(_) => {Ok(())} + Err(_) => { + log::error!("Failed to submit command buffer, device may be lost"); + return Err(anyhow::anyhow!("Command buffer submission failed")); + } + } + } +} @@ -17,6 +17,7 @@ pub mod shader; pub mod utils; pub mod texture; pub mod pipelines; +pub mod mipmap; pub static WGPU_BACKEND: OnceLock<String> = OnceLock::new(); pub const PHYSICS_STEP_RATE: u32 = 120; @@ -50,10 +51,11 @@ use winit::{ }; use crate::camera::Camera; -use crate::graphics::{FrameGraphicsContext, SharedGraphicsContext}; +use crate::graphics::{CommandEncoder, SharedGraphicsContext}; use crate::lighting::{Light}; use crate::texture::Texture; use crate::{egui_renderer::EguiRenderer}; +use crate::mipmap::MipMapper; pub use dropbear_future_queue as future; pub use gilrs; @@ -91,6 +93,7 @@ pub struct State { pub viewport_texture: Texture, pub texture_id: Arc<TextureId>, pub future_queue: Arc<FutureQueue>, + pub mipmapper: Arc<MipMapper>, physics_accumulator: Duration, @@ -233,6 +236,8 @@ Hardware: let viewport_texture = Texture::viewport(&config, &device, Some("viewport texture")); + let mipmapper = Arc::new(MipMapper::new(&device)); + let mut egui_renderer = Arc::new(Mutex::new(EguiRenderer::new( &device, config.format, @@ -335,6 +340,7 @@ Hardware: viewport_texture, texture_id: Arc::new(texture_id), future_queue, + mipmapper, instance, physics_accumulator: Duration::ZERO, scene_manager: scene::Manager::new(), @@ -427,17 +433,32 @@ Hardware: .texture .create_view(&wgpu::TextureViewDescriptor::default()); - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Render Encoder"), - }); - - let frame_ctx = FrameGraphicsContext { - view: view.clone(), - encoder: &mut encoder, - }; + { // ensures clearing of the encoder is done correctly. + let mut encoder = CommandEncoder::new(graphics.clone(), Some("surface clear render encoder")); + + { + let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("surface clear pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + } + if let Err(e) = encoder.submit(graphics.clone()) { + log_once::error_once!("{}", e); + } + } + self.egui_renderer.lock().begin_frame(&self.window); let mut scene_manager = std::mem::replace(&mut self.scene_manager, scene::Manager::new()); @@ -459,7 +480,7 @@ Hardware: } let commands = scene_manager.update(previous_dt, graphics.clone(), event_loop); - scene_manager.render(graphics.clone(), frame_ctx); + scene_manager.render(graphics.clone()); commands }; @@ -467,6 +488,12 @@ Hardware: self.scene_manager = scene_manager; + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("egui render encoder"), + }); + self.egui_renderer.lock().end_frame_and_draw( &self.device, &self.queue, @@ -0,0 +1,390 @@ +use crate::texture::Texture; + +pub struct MipMapper { + blit_mipmap: wgpu::RenderPipeline, + blit_sampler: wgpu::Sampler, + compute_pipeline: wgpu::ComputePipeline, + storage_texture_layout: wgpu::BindGroupLayout, + pub enabled: bool, +} + +impl MipMapper { + pub fn new(device: &wgpu::Device) -> Self { + let blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("mipmap blit shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("pipelines/shaders/blit.wgsl").into()), + }); + + // Keep this SRGB so we can render directly into the SRGB textures we create for materials. + let blit_format = Texture::TEXTURE_FORMAT; + let blit_mipmap = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("mipmap blit render pipeline"), + layout: None, + vertex: wgpu::VertexState { + module: &blit_shader, + entry_point: None, + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &blit_shader, + entry_point: None, + compilation_options: Default::default(), + targets: &[ + Some(wgpu::ColorTargetState { + format: blit_format, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + }) + ], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Cw, // change if issues + cull_mode: Some(wgpu::Face::Back), + strip_index_format: None, + polygon_mode: wgpu::PolygonMode::Fill, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + cache: None, + multiview: None, + }); + + let storage_texture_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Mipmapper::texture_layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::StorageTexture { + access: wgpu::StorageTextureAccess::ReadOnly, + format: wgpu::TextureFormat::Rgba8Unorm, + view_dimension: wgpu::TextureViewDimension::D2, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::StorageTexture { + access: wgpu::StorageTextureAccess::WriteOnly, + format: wgpu::TextureFormat::Rgba8Unorm, + view_dimension: wgpu::TextureViewDimension::D2, + }, + count: None, + }, + ], + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[&storage_texture_layout], + push_constant_ranges: &[], + }); + let compute_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("mipmap compute shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("pipelines/shaders/mipmap.wgsl").into()), + }); + let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { + label: Some("Mipmapper"), + layout: Some(&pipeline_layout), + module: &compute_shader, + entry_point: Some("compute_mipmap"), + compilation_options: Default::default(), + cache: None, + }); + + let blit_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + min_filter: wgpu::FilterMode::Linear, + mag_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + + Self { + storage_texture_layout, + compute_pipeline, + blit_mipmap, + blit_sampler, + enabled: true, + } + } + + pub fn blit_mipmaps( + &self, + device: &wgpu::Device, + queue: &wgpu::Queue, + texture: &Texture, + ) -> anyhow::Result<()> { + let texture = &texture.texture; + + match texture.format() { + wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => {}, + _ => anyhow::bail!("Unsupported format {:?}", texture.format()), + } + + if texture.mip_level_count() == 1 { + return Ok(()); + } + + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("blit mipmap encoder"), + }); + + // We need to render to this texture, so if the supplied texture + // isn't setup for rendering, we need to create a temporary one. + let (mut src_view, maybe_temp) = if texture.usage().contains(wgpu::TextureUsages::RENDER_ATTACHMENT) { + ( + texture.create_view(&wgpu::TextureViewDescriptor { + base_mip_level: 0, + mip_level_count: Some(1), + ..Default::default() + }), + None, + ) + } else { + // Create a temporary texture that can be rendered to since the + // supplied texture can't be rendered to. It will be basically + // identical to the original apart from the usage field and removing + // sRGB from the format if it's present. + let temp = device.create_texture(&wgpu::TextureDescriptor { + label: Some("Mipmapper::blit_mipmaps::temp"), + size: texture.size(), + mip_level_count: texture.mip_level_count(), + sample_count: texture.sample_count(), + dimension: texture.dimension(), + format: texture.format(), + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + + encoder.copy_texture_to_texture( + texture.as_image_copy(), + temp.as_image_copy(), + temp.size(), + ); + + ( + temp.create_view(&wgpu::TextureViewDescriptor { + mip_level_count: Some(1), + ..Default::default() + }), + Some(temp), + ) + }; + + for mip in 1..texture.mip_level_count() { + let dst_view = src_view + .texture() + .create_view(&wgpu::TextureViewDescriptor { + // What mip we want to render to + base_mip_level: mip, + // Like src_view we need to ignore other mips + mip_level_count: Some(1), + ..Default::default() + }); + + let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &self.blit_mipmap.get_bind_group_layout(0), + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&src_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&self.blit_sampler), + }, + ], + }); + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &dst_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, + }); + pass.set_pipeline(&self.blit_mipmap); + pass.set_bind_group(0, &texture_bind_group, &[]); + pass.draw(0..3, 0..1); + + // Make sure that we use the mip we just generated for the + // next iteration. + src_view = dst_view; + } + + // If we created a temporary texture, now we need to copy it back + // into the original. + if let Some(temp) = maybe_temp { + let mut size = temp.size(); + for mip_level in 0..temp.mip_level_count() { + encoder.copy_texture_to_texture( + wgpu::TexelCopyTextureInfo { + mip_level, + ..temp.as_image_copy() + }, + wgpu::TexelCopyTextureInfo { + mip_level, + ..texture.as_image_copy() + }, + size, + ); + + // Each mipmap is half the size of the original, + // so we need to half the copy size as well. + size.width /= 2; + size.height /= 2; + } + } + + // Submit directly (this is a standalone utility). + let command_buffer = encoder.finish(); + queue.submit(std::iter::once(command_buffer)); + + Ok(()) + } + + pub fn compute_mipmaps( + &self, + device: &wgpu::Device, + queue: &wgpu::Queue, + texture: &Texture, + ) -> anyhow::Result<()> { + let texture = &texture.texture; + + match texture.format() { + wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => {} + _ => anyhow::bail!("Unsupported format {:?}", texture.format()), + } + + if texture.mip_level_count() == 1 { + return Ok(()); + } + + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("compute mipmap encoder"), + }); + + // Create temp texture if supplied texture isn't setup for use + // as a storage texture + let (mut src_view, maybe_temp) = if texture + .usage() + .contains(wgpu::TextureUsages::STORAGE_BINDING) + { + ( + texture.create_view(&wgpu::TextureViewDescriptor { + mip_level_count: Some(1), + ..Default::default() + }), + None, + ) + } else { + let temp = device.create_texture(&wgpu::TextureDescriptor { + label: Some("Mipmapper::compute_mipmaps::temp"), + size: texture.size(), + mip_level_count: texture.mip_level_count(), + sample_count: texture.sample_count(), + dimension: texture.dimension(), + format: texture.format().remove_srgb_suffix(), + usage: wgpu::TextureUsages::STORAGE_BINDING + | wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + + encoder.copy_texture_to_texture( + texture.as_image_copy(), + temp.as_image_copy(), + temp.size(), + ); + + ( + temp.create_view(&wgpu::TextureViewDescriptor { + mip_level_count: Some(1), + ..Default::default() + }), + Some(temp), + ) + }; + + let dispatch_x = texture.width().div_ceil(16); + let dispatch_y = texture.height().div_ceil(16); + + { + let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default()); + pass.set_pipeline(&self.compute_pipeline); + for mip in 1..texture.mip_level_count() { + let dst_view = src_view + .texture() + .create_view(&wgpu::TextureViewDescriptor { + base_mip_level: mip, + mip_level_count: Some(1), + ..Default::default() + }); + let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &self.storage_texture_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&src_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&dst_view), + }, + ], + }); + pass.set_bind_group(0, &texture_bind_group, &[]); + pass.dispatch_workgroups(dispatch_x, dispatch_y, 1); + + src_view = dst_view; + } + } + + if let Some(temp) = maybe_temp { + let mut size = temp.size(); + for mip_level in 0..temp.mip_level_count() { + encoder.copy_texture_to_texture( + wgpu::TexelCopyTextureInfo { + mip_level, + ..temp.as_image_copy() + }, + wgpu::TexelCopyTextureInfo { + mip_level, + ..texture.as_image_copy() + }, + size, + ); + + // Each mipmap is half the size of the original + size.width /= 2; + size.height /= 2; + } + } + + let command_buffer = encoder.finish(); + queue.submit(std::iter::once(command_buffer)); + + Ok(()) + } +} @@ -250,28 +250,32 @@ impl Material { normal: &Texture, name: &str, ) -> BindGroup { - graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &graphics.layouts.texture_bind_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&diffuse.view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&diffuse.sampler), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::TextureView(&normal.view), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: wgpu::BindingResource::Sampler(&normal.sampler), - }, - ], - label: Some(name), - }) + graphics. + device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some(format!("{} mipmapped compute bind group", name).as_str()), + layout: &graphics.layouts.texture_bind_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &diffuse.view, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&diffuse.sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::TextureView(&normal.view), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::Sampler(&normal.sampler), + }, + ], + }) } pub fn set_tint(&mut self, graphics: &SharedGraphicsContext, tint: [f32; 4]) { @@ -616,13 +620,27 @@ impl Model { let start = Instant::now(); let diffuse_texture = if let Some((rgba_data, dimensions)) = processed_diffuse { - Texture::from_bytes_verbose(&graphics.device, &graphics.queue, &rgba_data, Some(dimensions), None, None, None) + Texture::from_bytes_verbose_mipmapped( + graphics.clone(), + &rgba_data, + Some(dimensions), + None, + None, + Some(material_name.as_str()) + ) } else { (*grey_texture).clone() }; let normal_texture = if let Some((rgba_data, dimensions)) = processed_normal { - Texture::from_bytes_verbose(&graphics.device, &graphics.queue, &rgba_data, Some(dimensions), None, None, None) + Texture::from_bytes_verbose_mipmapped( + graphics.clone(), + &rgba_data, + Some(dimensions), + None, + None, + Some(material_name.as_str()) + ) } else { (*flat_normal_texture).clone() }; @@ -0,0 +1,28 @@ +struct VertexOutput { + @builtin(position) clip_position: vec4<f32>, + @location(0) uv: vec2<f32>, +} + +@group(0) @binding(0) +var tex: texture_2d<f32>; +@group(0) @binding(1) +var tex_sampler: sampler; + +@vertex +fn vs_main( + @builtin(vertex_index) in_vertex_index: u32, +) -> VertexOutput { + var out: VertexOutput; + // Create fullscreen triangle + let x = f32((in_vertex_index << 1u) & 2u); + let y = f32(in_vertex_index & 2u); + out.clip_position = vec4<f32>(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0); + out.uv = vec2<f32>(x, 1.0 - y); + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { + let s = textureSample(tex, tex_sampler, in.uv); + return s; +} @@ -0,0 +1,31 @@ +@group(0) +@binding(0) +var src: texture_storage_2d<rgba8unorm, read>; +@group(0) +@binding(1) +var dst: texture_storage_2d<rgba8unorm, write>; + +@compute +@workgroup_size(16, 16, 1) +fn compute_mipmap( + @builtin(global_invocation_id) gid: vec3<u32>, +) { + let dstPos = gid.xy; + let srcPos = gid.xy * 2; + + let dim = textureDimensions(src); + + if (dstPos.x >= dim.x || dstPos.y >= dim.y) { + return; + } + + let t00 = textureLoad(src, srcPos); + let t01 = textureLoad(src, srcPos + vec2(0, 1)); + let t10 = textureLoad(src, srcPos + vec2(1, 0)); + let t11 = textureLoad(src, srcPos + vec2(1, 1)); + + // A simple linear average of 4 adjacent pixels + let t = (t00 + t01 + t10 + t11) * 0.25; + + textureStore(dst, dstPos, t); +} @@ -5,7 +5,7 @@ use winit::event_loop::ActiveEventLoop; use winit::window::WindowId; -use crate::{WindowData, graphics::{FrameGraphicsContext, SharedGraphicsContext}, input}; +use crate::{WindowData, graphics::{SharedGraphicsContext}, input}; use parking_lot::RwLock; use std::{collections::HashMap, rc::Rc, sync::Arc}; @@ -13,7 +13,7 @@ pub trait Scene { fn load(&mut self, graphics: Arc<SharedGraphicsContext>); fn physics_update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>); fn update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>); - fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, frame_ctx: FrameGraphicsContext<'a>); + fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>); fn exit(&mut self, event_loop: &ActiveEventLoop); /// By far a mess of a trait however it works. /// @@ -160,11 +160,11 @@ impl Manager { } } - pub fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, frame_ctx: FrameGraphicsContext<'a>) { + pub fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) { if let Some(scene_name) = &self.current_scene && let Some(scene) = self.scenes.get_mut(scene_name) { - scene.write().render(graphics.clone(), frame_ctx) + scene.write().render(graphics.clone()) } } @@ -4,9 +4,9 @@ use image::GenericImageView; use serde::{Deserialize, Serialize}; use crate::graphics::SharedGraphicsContext; +use crate::utils::ToPotentialString; - -/// As defined in `shaders.wgsl` as +/// As defined in `shaders.wgsl` as /// ``` /// @group(0) @binding(0) /// var t_diffuse: texture_2d<f32>; @@ -63,6 +63,7 @@ pub const TEXTURE_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = #[derive(Clone)] /// Describes a texture, like an image of some sort. Can be a normal texture on a model or a viewport or depth texture. pub struct Texture { + pub label: Option<String>, pub texture: wgpu::Texture, pub sampler: wgpu::Sampler, pub size: wgpu::Extent3d, @@ -116,7 +117,8 @@ impl Texture { texture, sampler, size, - view + view, + label: label.to_potential_string(), } } @@ -150,6 +152,7 @@ impl Texture { let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); Self { + label: label.to_potential_string(), texture, sampler, size, @@ -161,37 +164,61 @@ impl Texture { pub async fn from_file( graphics: Arc<SharedGraphicsContext>, path: &PathBuf, + label: Option<&str>, ) -> anyhow::Result<Self> { let data = fs::read(path)?; - Ok(Self::from_bytes(graphics.clone(), &data)) + Ok(Self::from_bytes(graphics.clone(), &data, label)) } /// Loads the texture from bytes. /// /// If you want more customisability in the texture being generated, you can use [Self::from_bytes_verbose] - pub fn from_bytes(graphics: Arc<SharedGraphicsContext>, bytes: &[u8]) -> Self { - Self::from_bytes_verbose( - &graphics.device, - &graphics.queue, - bytes, - None, - None, + pub fn from_bytes(graphics: Arc<SharedGraphicsContext>, bytes: &[u8], label: Option<&str>) -> Self { + Self::from_bytes_verbose_mipmapped(graphics, bytes, None, None, None, label) + } + + /// Loads the texture from bytes and generates mipmaps on the GPU. + /// + /// This is the recommended constructor for any sampled texture used for rendering. + pub fn from_bytes_verbose_mipmapped( + graphics: Arc<SharedGraphicsContext>, + bytes: &[u8], + dimensions: Option<(u32, u32)>, + view_descriptor: Option<wgpu::TextureViewDescriptor>, + sampler: Option<wgpu::SamplerDescriptor>, + label: Option<&str>, + ) -> Self { + let texture = Self::from_bytes_verbose( + graphics.clone(), + bytes, + dimensions, None, - None - ) + view_descriptor, + sampler, + label, + ); + + if let Err(err) = graphics + .mipmapper + .compute_mipmaps(&graphics.device, &graphics.queue, &texture) + { + log_once::warn_once!("Failed to generate mipmaps: {}", err); + } + + texture } /// Loads the texture from bytes, with options for more arguments. /// /// Requires more arguments. For a simpler usage, you should use [Self::from_bytes] pub fn from_bytes_verbose( - device: &wgpu::Device, - queue: &wgpu::Queue, + graphics: Arc<SharedGraphicsContext>, bytes: &[u8], dimensions: Option<(u32, u32)>, - texture_descriptor: Option<wgpu::TextureDescriptor>, + _texture_descriptor: Option<wgpu::TextureDescriptor>, view_descriptor: Option<wgpu::TextureViewDescriptor>, sampler: Option<wgpu::SamplerDescriptor>, + label: Option<&str>, ) -> Self { let (diffuse_rgba, dimensions) = match image::load_from_memory(bytes) { Ok(image) => { @@ -208,7 +235,8 @@ impl Texture { (bytes.to_vec(), dims) } else { log::error!( - "Texture decode failed ({:?}); expected {} bytes for raw RGBA ({}x{}), got {}. Falling back.", + "Texture [{:?}] decode failed ({:?}); expected {} bytes for raw RGBA ({}x{}), got {}. Falling back.", + label, err, expected_len, dims.0, @@ -219,40 +247,44 @@ impl Texture { } } else { log::error!( - "Texture decode failed ({:?}) and no dimensions were provided; falling back to 1x1 magenta.", + "Texture [{:?}] decode failed ({:?}) and no dimensions were provided; falling back to 1x1 magenta.", + label, err ); (vec![255, 0, 255, 255], (1, 1)) } } }; + let size = wgpu::Extent3d { width: dimensions.0, height: dimensions.1, depth_or_array_layers: 1, }; - let desc = texture_descriptor.unwrap_or_else(|| - wgpu::TextureDescriptor { - label: Some("diffuse_texture"), - size: size, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: Texture::TEXTURE_FORMAT, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - } - ); + let mip_level_count = size.width.min(size.height).ilog2() + 1; + log::debug!("Mip level count [{:?}]: {}", label, mip_level_count); - let texture = device.create_texture(&desc); + let texture = graphics.device.create_texture(&wgpu::TextureDescriptor { + label: Some(format!("{:?} diffuse blit texture", label).as_str()), + size, + mip_level_count, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: Texture::TEXTURE_FORMAT, + usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); let unpadded_bytes_per_row = 4 * size.width; let padded_bytes_per_row = (unpadded_bytes_per_row + wgpu::COPY_BYTES_PER_ROW_ALIGNMENT - 1) & !(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT - 1); debug_assert!(diffuse_rgba.len() >= (unpadded_bytes_per_row * size.height) as usize); if padded_bytes_per_row == unpadded_bytes_per_row { - queue.write_texture( + graphics.queue.write_texture( wgpu::TexelCopyTextureInfo { texture: &texture, mip_level: 0, @@ -278,7 +310,7 @@ impl Texture { .copy_from_slice(&diffuse_rgba[src_start..src_start + src_stride]); } - queue.write_texture( + graphics.queue.write_texture( wgpu::TexelCopyTextureInfo { texture: &texture, mip_level: 0, @@ -304,16 +336,17 @@ impl Texture { address_mode_w: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Linear, ..Default::default() } }; - let sampler = device.create_sampler(&sampler_desc); + let sampler = graphics.device.create_sampler(&sampler_desc); let view = texture.create_view(&view_descriptor.unwrap_or_default()); Self { + label: label.to_potential_string(), texture, sampler, size, @@ -328,7 +361,7 @@ impl Texture { address_mode_w: wrap.into(), mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Linear, ..Default::default() } } @@ -263,3 +263,20 @@ macro_rules! resource { ::dropbear_engine::utils::ResourceReference::from_euca_uri($path).expect("Invalid euca URI") }; } + +/// Helper trait for converting `Option<T: ToString>` to [`Option<String>`] without looking into its contents. +pub trait ToPotentialString { + /// Converts an [`Option<T>`], where [`T`] can be converted to a [`String`], into an [`Option<String>`]. + fn to_potential_string(&self) -> Option<String>; +} + +impl<T> ToPotentialString for Option<T> +where T: ToString +{ + fn to_potential_string(&self) -> Option<String> { + match self { + None => None, + Some(v) => Some(v.to_string()) + } + } +} @@ -266,14 +266,13 @@ impl SceneConfig { if let Some(reference) = &custom.diffuse_texture { if let Ok(path) = reference.resolve() { if let Ok(bytes) = std::fs::read(&path) { - let diffuse = Texture::from_bytes_verbose( - &graphics.device, - &graphics.queue, + let diffuse = Texture::from_bytes_verbose_mipmapped( + graphics.clone(), bytes.as_slice(), None, None, - None, Some(Texture::sampler_from_wrap(custom.wrap_mode)), + Some(material.name.as_str()) ); let flat_normal = (*ASSET_REGISTRY .solid_texture_rgba8( @@ -297,13 +297,12 @@ pub mod jni { if let Err(e) = super::shared::switch_to_scene_async(command_buffer, scene_loader, scene_id as u64) { eprintln!("Failed to switch scene async: {}", e); - // Check if it's a premature scene switch error if let crate::scripting::native::DropbearNativeError::PrematureSceneSwitch = e { let _ = env.throw_new("com/dropbear/exception/PrematureSceneSwitchException", - "Cannot switch to scene before it has finished loading"); + "Cannot switch to scene before it has finished loading"); } else { let _ = env.throw_new("java/lang/RuntimeException", - format!("Failed to switch scene async: {:?}", e)); + format!("Failed to switch scene async: {:?}", e)); } } } @@ -324,7 +323,7 @@ pub mod jni { Err(e) => { eprintln!("Failed to create Progress object: {:?}", e); let _ = env.throw_new("java/lang/RuntimeException", - format!("Failed to create Progress object: {:?}", e)); + format!("Failed to create Progress object: {:?}", e)); std::ptr::null_mut() } } @@ -332,7 +331,7 @@ pub mod jni { Err(e) => { eprintln!("Failed to get scene load progress: {}", e); let _ = env.throw_new("java/lang/RuntimeException", - format!("Failed to get scene load progress: {:?}", e)); + format!("Failed to get scene load progress: {:?}", e)); std::ptr::null_mut() } } @@ -352,7 +351,7 @@ pub mod jni { Err(e) => { eprintln!("Failed to get scene load status: {}", e); let _ = env.throw_new("java/lang/RuntimeException", - format!("Failed to get scene load status: {:?}", e)); + format!("Failed to get scene load status: {:?}", e)); -1 as jint } } @@ -1,6 +1,5 @@ //! Used for displaying the Help->About window in the editor. -use dropbear_engine::graphics::FrameGraphicsContext; use egui::{CentralPanel}; use gilrs::{Button, GamepadId}; use winit::dpi::PhysicalPosition; @@ -76,7 +75,7 @@ impl Scene for AboutWindow { self.window = Some(graphics.window.id()); } - fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: FrameGraphicsContext<'a>) { + fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { } fn exit(&mut self, _event_loop: &ActiveEventLoop) {} @@ -45,7 +45,7 @@ impl Scene for DebugWindow { self.window = Some(graphics.window.id()); } - fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: dropbear_engine::graphics::FrameGraphicsContext<'a>) { + fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { } fn exit(&mut self, _event_loop: &ActiveEventLoop) {} @@ -57,6 +57,7 @@ use wgpu::{Color, Extent3d}; use winit::window::{CursorGrabMode, WindowAttributes}; use winit::{keyboard::KeyCode, window::Window}; use winit::dpi::PhysicalSize; +use dropbear_engine::mipmap::MipMapper; use dropbear_engine::pipelines::DropbearShaderPipeline; use dropbear_engine::pipelines::shader::MainRenderPipeline; use dropbear_engine::pipelines::GlobalsUniform; @@ -84,6 +85,7 @@ pub struct Editor { pub main_render_pipeline: Option<MainRenderPipeline>, pub shader_globals: Option<GlobalsUniform>, pub collider_wireframe_pipeline: Option<ColliderWireframePipeline>, + pub mipmapper: Option<MipMapper>, pub active_camera: Arc<Mutex<Option<Entity>>>, @@ -248,6 +250,7 @@ impl Editor { instance_buffer_cache: HashMap::new(), collider_wireframe_geometry_cache: HashMap::new(), collider_instance_buffer: None, + mipmapper: None, }) } @@ -1323,6 +1326,8 @@ impl Editor { self.light_cube_pipeline = Some(LightCubePipeline::new(graphics.clone())); self.shader_globals = Some(GlobalsUniform::new(graphics.clone(), Some("editor shader globals"))); self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone())); + // Mipmaps are generated by the engine during texture creation; keep this optional field unused for now. + self.mipmapper = None; self.texture_id = Some((*graphics.texture_id).clone()); self.window = Some(graphics.window.clone()); @@ -7,7 +7,7 @@ use super::*; use crate::signal::SignalController; use crate::spawn::PendingSpawnController; use dropbear_engine::asset::{PointerKind, ASSET_REGISTRY}; -use dropbear_engine::graphics::{FrameGraphicsContext, InstanceRaw}; +use dropbear_engine::graphics::{CommandEncoder, InstanceRaw}; use dropbear_engine::model::MODEL_CACHE; use dropbear_engine::{ entity::{EntityTransform, MeshRenderer, Transform}, @@ -346,7 +346,7 @@ impl Scene for Editor { self.input_state.mouse_delta = None; } - fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, frame_ctx: FrameGraphicsContext<'a>) { + fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) { self.size = graphics.viewport_texture.size; self.texture_id = Some(*graphics.texture_id.clone()); self.window = Some(graphics.window.clone()); @@ -361,23 +361,7 @@ impl Scene for Editor { self.color = clear_color; eucalyptus_core::logging::render(&graphics.get_egui_context()); - { - let _ = frame_ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("editor surface clear pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &frame_ctx.view, - depth_slice: None, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(clear_color), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - occlusion_query_set: None, - timestamp_writes: None, - }); - } + let mut encoder = CommandEncoder::new(graphics.clone(), Some("editor viewport encoder")); let cam = { let c = self.active_camera.lock(); @@ -403,6 +387,37 @@ impl Scene for Editor { }; log_once::debug_once!("Pipeline ready"); + { // ensures clearing of the encoder is done correctly. + let mut encoder = CommandEncoder::new(graphics.clone(), Some("viewport clear render encoder")); + + { + let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("viewport clear pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &graphics.viewport_texture.view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 100.0 / 255.0, + g: 149.0 / 255.0, + b: 237.0 / 255.0, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + } + + if let Err(e) = encoder.submit(graphics.clone()) { + log_once::error_once!("{}", e); + } + } + let lights = { let mut lights = Vec::new(); let mut query = self.world.query::<(&Light, &LightComponent)>(); @@ -484,7 +499,7 @@ impl Scene for Editor { } { - let mut render_pass = frame_ctx.encoder + let mut render_pass = encoder .begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("light cube render pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -521,6 +536,9 @@ impl Scene for Editor { } } } + + + if let Some(lcp) = &self.light_cube_pipeline { for (model, instance_buffer, instance_count) in prepared_models { let globals_bind_group = &self @@ -529,7 +547,7 @@ impl Scene for Editor { .expect("Shader globals not initialised") .bind_group; - let mut render_pass = frame_ctx.encoder + let mut render_pass = encoder .begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("model render pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -579,7 +597,7 @@ impl Scene for Editor { if show_hitboxes { if let Some(collider_pipeline) = &self.collider_wireframe_pipeline { - let mut render_pass = frame_ctx.encoder + let mut render_pass = encoder .begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("model render pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -699,6 +717,9 @@ impl Scene for Editor { } } } + if let Err(e) = encoder.submit(graphics.clone()) { + log_once::error_once!("{}", e); + } } } @@ -213,7 +213,7 @@ impl Scene for EditorSettingsWindow { self.window = Some(graphics.window.id()); } - fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: dropbear_engine::graphics::FrameGraphicsContext<'a>) { + fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { } fn exit(&mut self, _event_loop: &ActiveEventLoop) { @@ -183,7 +183,7 @@ impl Scene for ProjectSettingsWindow { self.window = Some(graphics.window.id()); } - fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: dropbear_engine::graphics::FrameGraphicsContext<'a>) { + fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { } fn exit(&mut self, _event_loop: &ActiveEventLoop) { @@ -1,5 +1,5 @@ use anyhow::{Context, anyhow}; -use dropbear_engine::{DropbearWindowBuilder, future::{FutureHandle, FutureQueue}, graphics::FrameGraphicsContext, input::{Controller, Keyboard, Mouse}, scene::{Scene, SceneCommand}}; +use dropbear_engine::{DropbearWindowBuilder, future::{FutureHandle, FutureQueue}, input::{Controller, Keyboard, Mouse}, scene::{Scene, SceneCommand}}; use egui::{self, FontId, Frame, RichText}; use egui_toast::{ToastOptions, Toasts}; use eucalyptus_core::config::ProjectConfig; @@ -243,7 +243,7 @@ impl Scene for MainMenu { fn update(&mut self, _dt: f32, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {} - fn render<'a>(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: FrameGraphicsContext<'a>) { + fn render<'a>(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { #[allow(clippy::collapsible_if)] if let Some(handle) = self.project_creation_handle.as_ref() { if let Some(result) = graphics @@ -800,14 +800,13 @@ impl SignalController for Editor { let path = resolve_editor_path(&uri); if let Ok(bytes) = std::fs::read(&path) { - let diffuse = Texture::from_bytes_verbose( - &graphics.device, - &graphics.queue, - &bytes, - None, + let diffuse = Texture::from_bytes_verbose_mipmapped( + graphics.clone(), + &bytes, None, None, - Some(Texture::sampler_from_wrap(wrap_mode)) + Some(Texture::sampler_from_wrap(wrap_mode)), + Some(mat_name.as_str()) ); let flat_normal = (*dropbear_engine::asset::ASSET_REGISTRY .solid_texture_rgba8( @@ -856,14 +855,13 @@ impl SignalController for Editor { } }; - let diffuse = Texture::from_bytes_verbose( - &graphics.device, - &graphics.queue, - &bytes, - None, + let diffuse = Texture::from_bytes_verbose_mipmapped( + graphics.clone(), + &bytes, None, None, - Some(Texture::sampler_from_wrap(wrap_mode.clone())) + Some(Texture::sampler_from_wrap(wrap_mode.clone())), + Some(target_material) ); let flat_normal = (*dropbear_engine::asset::ASSET_REGISTRY .solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255])) @@ -911,14 +909,13 @@ impl SignalController for Editor { let path = resolve_editor_path(&uri); if let Ok(bytes) = std::fs::read(&path) { - let diffuse = Texture::from_bytes_verbose( - &graphics.device, - &graphics.queue, - &bytes, - None, + let diffuse = Texture::from_bytes_verbose_mipmapped( + graphics.clone(), + &bytes, None, None, - Some(Texture::sampler_from_wrap(wrap_mode.clone())) + Some(Texture::sampler_from_wrap(wrap_mode.clone())), + Some(target_material) ); material.diffuse_texture = diffuse; material.bind_group = Material::create_bind_group( @@ -393,14 +393,13 @@ async fn load_renderer_from_serialized( if let Ok(path) = reference.resolve() { match std::fs::read(&path) { Ok(bytes) => { - let diffuse = Texture::from_bytes_verbose( - &graphics.device, - &graphics.queue, - &bytes, - None, + let diffuse = Texture::from_bytes_verbose_mipmapped( + graphics.clone(), + &bytes, None, None, - Some(Texture::sampler_from_wrap(custom.wrap_mode)) + Some(Texture::sampler_from_wrap(custom.wrap_mode)), + Some(material.name.as_str()), ); let flat_normal = (*ASSET_REGISTRY .solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255])) @@ -1,7 +1,6 @@ use std::{collections::VecDeque, time::Instant}; use dropbear_engine::WGPU_BACKEND; -use dropbear_engine::graphics::FrameGraphicsContext; use egui::{Color32, Context, RichText, Ui}; use egui_plot::{Legend, Line, Plot, PlotPoints}; use dropbear_engine::scene::Scene; @@ -336,7 +335,7 @@ impl Scene for NerdStats { fn update(&mut self, dt: f32, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { self.record_stats(dt, self.entity_count); } - fn render<'a>(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: FrameGraphicsContext<'a>) { + fn render<'a>(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { self.show_window(&graphics.get_egui_context()); } fn exit(&mut self, _event_loop: &ActiveEventLoop) {} @@ -2,19 +2,20 @@ use std::sync::Arc; use std::collections::HashMap; use dropbear_engine::pipelines::DropbearShaderPipeline; +use dropbear_engine::graphics::CommandEncoder; use eucalyptus_core::egui::CentralPanel; use eucalyptus_core::physics::collider::ColliderGroup; use eucalyptus_core::physics::collider::ColliderShapeKey; use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw; use glam::{DMat4, DQuat, DVec3, Quat}; use hecs::Entity; -use wgpu::Color; +use wgpu::{Color}; use wgpu::util::DeviceExt; use winit::event_loop::ActiveEventLoop; use dropbear_engine::camera::Camera; use dropbear_engine::buffer::ResizableBuffer; use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; -use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext, FrameGraphicsContext}; +use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext}; use dropbear_engine::lighting::{Light, LightComponent}; use dropbear_engine::lighting::MAX_LIGHTS; use dropbear_engine::model::{DrawLight, DrawModel, ModelId, MODEL_CACHE}; @@ -456,7 +457,16 @@ impl Scene for PlayMode { self.input_state.mouse_delta = None; } - fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, frame_ctx: FrameGraphicsContext<'a>) { + fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) { + let clear_color = Color { + r: 100.0 / 255.0, + g: 149.0 / 255.0, + b: 237.0 / 255.0, + a: 1.0, + }; + + let mut encoder = CommandEncoder::new(graphics.clone(), Some("runtime viewport encoder")); + let Some(active_camera) = self.active_camera else { return; }; @@ -476,12 +486,36 @@ impl Scene for PlayMode { }; log_once::debug_once!("Pipeline ready"); - let clear_color = Color { - r: 100.0 / 255.0, - g: 149.0 / 255.0, - b: 237.0 / 255.0, - a: 1.0, - }; + { // ensures clearing of the encoder is done correctly. + let mut encoder = dropbear_engine::graphics::CommandEncoder::new(graphics.clone(), Some("viewport clear render encoder")); + + { + let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("viewport clear pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &graphics.viewport_texture.view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 100.0 / 255.0, + g: 149.0 / 255.0, + b: 237.0 / 255.0, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + } + + if let Err(e) = encoder.submit(graphics.clone()) { + log_once::error_once!("{}", e); + } + } let lights = { let mut lights = Vec::new(); @@ -492,15 +526,15 @@ impl Scene for PlayMode { lights }; - if let Some(globals) = &mut self.shader_globals { - let enabled_count = lights - .iter() - .filter(|(_, comp)| comp.enabled) - .take(MAX_LIGHTS) - .count() as u32; - globals.set_num_lights(enabled_count); - globals.write(&graphics.queue); - } + if let Some(globals) = &mut self.shader_globals { + let enabled_count = lights + .iter() + .filter(|(_, comp)| comp.enabled) + .take(MAX_LIGHTS) + .count() as u32; + globals.set_num_lights(enabled_count); + globals.write(&graphics.queue); + } let renderers = { let mut renderers = Vec::new(); @@ -564,25 +598,7 @@ impl Scene for PlayMode { } { - let _ = frame_ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("runtime surface clear pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &frame_ctx.view, - depth_slice: None, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(clear_color), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - occlusion_query_set: None, - timestamp_writes: None, - }); - } - - { - let mut render_pass = frame_ctx.encoder + let mut render_pass = encoder .begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("light cube render pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -620,53 +636,51 @@ impl Scene for PlayMode { } } - for (model, instance_buffer, instance_count) in prepared_models { - let light_bind_group = self - .light_cube_pipeline - .as_ref() - .expect("Light cube pipeline not initialised") - .bind_group(); - - let globals_bind_group = &self - .shader_globals - .as_ref() - .expect("Shader globals not initialised") - .bind_group; - - let mut render_pass = frame_ctx.encoder - .begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("model render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &graphics.viewport_texture.view, - depth_slice: None, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &graphics.depth_texture.view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, + // model rendering + if let Some(lcp) = &self.light_cube_pipeline { + for (model, instance_buffer, instance_count) in prepared_models { + let globals_bind_group = &self + .shader_globals + .as_ref() + .expect("Shader globals not initialised") + .bind_group; + + let mut render_pass = encoder + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("model render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &graphics.viewport_texture.view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &graphics.depth_texture.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, }), - stencil_ops: None, - }), - occlusion_query_set: None, - timestamp_writes: None, - }); - render_pass.set_pipeline(pipeline.pipeline()); - render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); - render_pass.set_bind_group(4, globals_bind_group, &[]); - render_pass.draw_model_instanced( - &model, - 0..instance_count, - &camera.bind_group, - light_bind_group, - ); + occlusion_query_set: None, + timestamp_writes: None, + }); + render_pass.set_pipeline(pipeline.pipeline()); + render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); + render_pass.set_bind_group(4, globals_bind_group, &[]); + render_pass.draw_model_instanced( + &model, + 0..instance_count, + &camera.bind_group, + lcp.bind_group(), + ); + } } + // collider pipeline { let show_hitboxes = self .current_scene @@ -682,7 +696,7 @@ impl Scene for PlayMode { if show_hitboxes { if let Some(collider_pipeline) = &self.collider_wireframe_pipeline { - let mut render_pass = frame_ctx.encoder + let mut render_pass = encoder .begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("model render pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -802,6 +816,9 @@ impl Scene for PlayMode { } } } + if let Err(e) = encoder.submit(graphics.clone()) { + log_once::error_once!("{}", e); + } } }