Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support for Multisampled Anti-Aliasing (#213)
closes #198 ### Changes This PR introduces a `sample_count: u32` to `TextureDesc` as well as a `multisample_state: MultisampleState` to the `RenderPipelineDesc`. Also added `alpha_to_coverage` while at it. ```rust #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct MultisampleState { pub sample_count: u32, pub sample_mask: u64, pub alpha_to_coverage: bool, } impl Default for MultisampleState { fn default() -> Self { Self { sample_count: 1, sample_mask: !0, alpha_to_coverage: false, } } } ``` Together with the existing `FinishOp::ResolveTo(TextureView)`, a multisampled renderpass can now be described and executed ### Implementations <details> <summary>Vulkan</summary> The rendering attachment needs to be told to resolve ```rust if let crate::FinishOp::ResolveTo(resolve_view) = rt.finish_op { vk_info = vk_info .resolve_image_view(resolve_view.raw) .resolve_image_layout(vk::ImageLayout::GENERAL) .resolve_mode(vk::ResolveModeFlags::AVERAGE); } ``` The store_op is currently always set to `DONT_CARE` as in many cases for msaa rendering the resolved texture is the only one required. Would be nice to be able to specify this behaviour in blade as well though: ```rust vk_info.store_op = match rt.finish_op { crate::FinishOp::ResolveTo(..) => { /* TODO: DONT_CARE is most optimal in many cases where the msaa texture itself is never read afterwards but only the resolved, but how can the user specify this in blade? https://docs.vulkan.org/samples/latest/samples/performance/msaa/README.html#_best_practice_summary */ // vk::AttachmentStoreOp::STORE vk::AttachmentStoreOp::DONT_CARE } }; ``` For the texture creation, a `SampleCountFlags` is constructed for the `vk::ImageCreateInfo`: ```rust /// in vulkan/resource.rs:268 samples: vk::SampleCountFlags::from_raw(desc.sample_count), ``` and for the pipeline, I looked at how wgpu does it and came to the following code ```rust let vk_sample_mask = [ desc.multisample_state.sample_mask as u32, (desc.multisample_state.sample_mask >> 32) as u32, ]; let vk_multisample = vk::PipelineMultisampleStateCreateInfo::default() .rasterization_samples(vk::SampleCountFlags::from_raw( desc.multisample_state.sample_count, )) .alpha_to_coverage_enable(desc.multisample_state.alpha_to_coverage) .sample_mask(&vk_sample_mask); ``` </details> <details> <summary>Metal</summary> The metal backend already had support for the `ResolveTo` finishop so no changes needed for the renderpipeline. The creation of the renderpipeline required description though: ```rust descriptor.set_raster_sample_count(desc.multisample_state.sample_count); descriptor.set_alpha_to_coverage_enabled(desc.multisample_state.alpha_to_coverage); ``` For the texture, only specifying the sample_count was required ```rust /// in metal/resource.rs:205 descriptor.set_sample_count(desc.sample_count as u64); ``` </details> <details> <summary>OpenGL ES</summary> This was the hardest one as I am very unfamiliar with msaa in opengl, but in summary the texture needs to be created with `renderbuffer_storage_multisample` if it is a rendertarget and with a `glow::TEXTURE_2D_MULTISAMPLE` target type if it is a normal texture. The sample count is then set with `tex_storage_2d_multisample`, but there is now no-longer a way to specify `mip_count` so don't know if that is even possible. ```rust /* TODO(ErikWDev): How to set mip count and sample count? Not possible in gles? */ gl.tex_storage_2d_multisample( target, desc.sample_count as i32, format_desc.internal, desc.size.width as i32, desc.size.height as i32, true, ); ``` For the rendering, I use `blit_framebuffer` to blit the msaa texture onto the resolve target which required turning the renderbuffers into FBO:s. Currently, the FBO:s are created and deleted ad-hoc but could be saved as done in wgpu-hal. See #198 ```rust /// in gles/command.rs:814 Self::BlitFramebuffer { from, to } => { /* TODO(ErikWDev): Validate */ let target_from = match from.inner { super::TextureInner::Renderbuffer { raw } => raw, _ => panic!("Unsupported resolve between non-renderbuffers"), }; let target_to = match to.inner { super::TextureInner::Renderbuffer { raw } => raw, _ => panic!("Unsupported resolve between non-renderbuffers"), }; let framebuf_from = gl.create_framebuffer().unwrap(); let framebuf_to = gl.create_framebuffer().unwrap(); gl.bind_framebuffer(glow::READ_FRAMEBUFFER, Some(framebuf_from)); gl.framebuffer_renderbuffer( glow::READ_FRAMEBUFFER, glow::COLOR_ATTACHMENT0, // NOTE: Assuming color attachment glow::RENDERBUFFER, Some(target_from), ); gl.bind_framebuffer(glow::DRAW_FRAMEBUFFER, Some(framebuf_to)); gl.framebuffer_renderbuffer( glow::DRAW_FRAMEBUFFER, glow::COLOR_ATTACHMENT0, // NOTE: Assuming color attachment glow::RENDERBUFFER, Some(target_to), ); assert_eq!( gl.check_framebuffer_status(glow::DRAW_FRAMEBUFFER), glow::FRAMEBUFFER_COMPLETE, "DRAW_FRAMEBUFFER is not complete" ); gl.blit_framebuffer( 0, 0, from.target_size[0] as _, from.target_size[1] as _, 0, 0, to.target_size[0] as _, to.target_size[1] as _, glow::COLOR_BUFFER_BIT, // NOTE: Assuming color glow::NEAREST, ); gl.bind_framebuffer(glow::READ_FRAMEBUFFER, None); gl.bind_framebuffer(glow::DRAW_FRAMEBUFFER, None); gl.delete_framebuffer(framebuf_from); gl.delete_framebuffer(framebuf_to); } ``` </details> ### Testing The vulkan implementation has been validated on an AMD RX 6600 on debian, an integrated intel GPU on Fedora as well as a GTX 1050 on Windows and it all seems to work (the particle sample). After inspection in renderdoc the result is as expected. The metal implementation has been tested on a Mac Mini M4 and works. However, particle example seems to break after the sample count is adjusted in runtime for some reason. The Opengl ES implementation has **not been tested**!!! ### Particle Example I changed the particle example to now utilize msaa which requires it to keep a msaa texture with the desired `sample_count` available and recreated upon resizing as well as a FinishOp::ResolveTo to resolve the msaa texture to the acquired frame texture as such: ```rust if let mut pass = self.command_encoder.render( "draw", gpu::RenderTargetSet { colors: &[gpu::RenderTarget { view: self.msaa_view, init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack), finish_op: gpu::FinishOp::ResolveTo(frame.texture_view()), }], depth_stencil: None, }, ) { self.particle_system .draw(&mut pass, screen_desc.physical_size); self.gui_painter .paint(&mut pass, gui_primitives, screen_desc, &self.context); } ``` ### Egui Furthermore, the `blade_egui` renderer also requires information about the multisample state since it has a pipeline that now needs information about the texture it's going to render to so it's initializer now takes a sample_count as well: ```rust pub fn new( info: blade_graphics::SurfaceInfo, context: &blade_graphics::Context, sample_count: u32, ) -> GuiPainter { // ... } ``` --- Let me know what needs changing or if testing fails somewhere. Especially the Opengl ES implementation as I didn't know how to run using it
- Loading branch information