Add MSAA to new renderer (#3042)

Adds support for MSAA to the new renderer. This is done using the new [pipeline specialization](#3031) support to specialize on sample count. This is an alternative implementation to #2541 that cuts out the need for complicated render graph edge management by moving the relevant target information into View entities. This reuses @superdump's clever MSAA bitflag range code from #2541.

Note that wgpu currently only supports 1 or 4 samples due to those being the values supported by WebGPU. However they do plan on exposing ways to [enable/query for natively supported sample counts](https://github.com/gfx-rs/wgpu/issues/1832). When this happens we should integrate
This commit is contained in:
Carter Anderson 2021-10-29 05:37:43 +00:00
parent 7d932ac1d8
commit c5af1335eb
8 changed files with 213 additions and 123 deletions

View file

@ -24,7 +24,7 @@ use bevy_render2::{
render_resource::*,
renderer::RenderDevice,
texture::{Image, TextureCache},
view::ExtractedView,
view::{ExtractedView, Msaa, ViewDepthTexture},
RenderApp, RenderStage, RenderWorld,
};
@ -53,7 +53,6 @@ pub mod draw_2d_graph {
pub const NAME: &str = "draw_2d";
pub mod input {
pub const VIEW_ENTITY: &str = "view_entity";
pub const RENDER_TARGET: &str = "render_target";
}
pub mod node {
pub const MAIN_PASS: &str = "main_pass";
@ -64,8 +63,6 @@ pub mod draw_3d_graph {
pub const NAME: &str = "draw_3d";
pub mod input {
pub const VIEW_ENTITY: &str = "view_entity";
pub const RENDER_TARGET: &str = "render_target";
pub const DEPTH: &str = "depth";
}
pub mod node {
pub const MAIN_PASS: &str = "main_pass";
@ -95,10 +92,10 @@ impl Plugin for CorePipelinePlugin {
let mut draw_2d_graph = RenderGraph::default();
draw_2d_graph.add_node(draw_2d_graph::node::MAIN_PASS, pass_node_2d);
let input_node_id = draw_2d_graph.set_input(vec![
SlotInfo::new(draw_2d_graph::input::VIEW_ENTITY, SlotType::Entity),
SlotInfo::new(draw_2d_graph::input::RENDER_TARGET, SlotType::TextureView),
]);
let input_node_id = draw_2d_graph.set_input(vec![SlotInfo::new(
draw_2d_graph::input::VIEW_ENTITY,
SlotType::Entity,
)]);
draw_2d_graph
.add_slot_edge(
input_node_id,
@ -107,23 +104,14 @@ impl Plugin for CorePipelinePlugin {
MainPass2dNode::IN_VIEW,
)
.unwrap();
draw_2d_graph
.add_slot_edge(
input_node_id,
draw_2d_graph::input::RENDER_TARGET,
draw_2d_graph::node::MAIN_PASS,
MainPass2dNode::IN_COLOR_ATTACHMENT,
)
.unwrap();
graph.add_sub_graph(draw_2d_graph::NAME, draw_2d_graph);
let mut draw_3d_graph = RenderGraph::default();
draw_3d_graph.add_node(draw_3d_graph::node::MAIN_PASS, pass_node_3d);
let input_node_id = draw_3d_graph.set_input(vec![
SlotInfo::new(draw_3d_graph::input::VIEW_ENTITY, SlotType::Entity),
SlotInfo::new(draw_3d_graph::input::RENDER_TARGET, SlotType::TextureView),
SlotInfo::new(draw_3d_graph::input::DEPTH, SlotType::TextureView),
]);
let input_node_id = draw_3d_graph.set_input(vec![SlotInfo::new(
draw_3d_graph::input::VIEW_ENTITY,
SlotType::Entity,
)]);
draw_3d_graph
.add_slot_edge(
input_node_id,
@ -132,22 +120,6 @@ impl Plugin for CorePipelinePlugin {
MainPass3dNode::IN_VIEW,
)
.unwrap();
draw_3d_graph
.add_slot_edge(
input_node_id,
draw_3d_graph::input::RENDER_TARGET,
draw_3d_graph::node::MAIN_PASS,
MainPass3dNode::IN_COLOR_ATTACHMENT,
)
.unwrap();
draw_3d_graph
.add_slot_edge(
input_node_id,
draw_3d_graph::input::DEPTH,
draw_3d_graph::node::MAIN_PASS,
MainPass3dNode::IN_DEPTH,
)
.unwrap();
graph.add_sub_graph(draw_3d_graph::NAME, draw_3d_graph);
graph.add_node(node::MAIN_PASS_DEPENDENCIES, EmptyNode);
@ -235,11 +207,6 @@ impl RenderCommand<Transparent2d> for SetItemPipeline {
}
}
pub struct ViewDepthTexture {
pub texture: Texture,
pub view: TextureView,
}
pub fn extract_clear_color(clear_color: Res<ClearColor>, mut render_world: ResMut<RenderWorld>) {
// If the clear color has changed
if clear_color.is_changed() {
@ -271,10 +238,11 @@ pub fn extract_core_pipeline_camera_phases(
pub fn prepare_core_views_system(
mut commands: Commands,
mut texture_cache: ResMut<TextureCache>,
msaa: Res<Msaa>,
render_device: Res<RenderDevice>,
views: Query<(Entity, &ExtractedView), With<RenderPhase<Transparent3d>>>,
views_3d: Query<(Entity, &ExtractedView), With<RenderPhase<Transparent3d>>>,
) {
for (entity, view) in views.iter() {
for (entity, view) in views_3d.iter() {
let cached_texture = texture_cache.get(
&render_device,
TextureDescriptor {
@ -285,7 +253,7 @@ pub fn prepare_core_views_system(
height: view.height as u32,
},
mip_level_count: 1,
sample_count: 1,
sample_count: msaa.samples,
dimension: TextureDimension::D2,
format: TextureFormat::Depth32Float, /* PERF: vulkan docs recommend using 24
* bit depth for better performance */

View file

@ -5,15 +5,15 @@ use bevy_render2::{
render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass},
render_resource::{LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor},
renderer::RenderContext,
view::ExtractedView,
view::{ExtractedView, ViewTarget},
};
pub struct MainPass2dNode {
query: QueryState<&'static RenderPhase<Transparent2d>, With<ExtractedView>>,
query:
QueryState<(&'static RenderPhase<Transparent2d>, &'static ViewTarget), With<ExtractedView>>,
}
impl MainPass2dNode {
pub const IN_COLOR_ATTACHMENT: &'static str = "color_attachment";
pub const IN_VIEW: &'static str = "view";
pub fn new(world: &mut World) -> Self {
@ -25,10 +25,7 @@ impl MainPass2dNode {
impl Node for MainPass2dNode {
fn input(&self) -> Vec<SlotInfo> {
vec![
SlotInfo::new(MainPass2dNode::IN_COLOR_ATTACHMENT, SlotType::TextureView),
SlotInfo::new(MainPass2dNode::IN_VIEW, SlotType::Entity),
]
vec![SlotInfo::new(MainPass2dNode::IN_VIEW, SlotType::Entity)]
}
fn update(&mut self, world: &mut World) {
@ -41,12 +38,16 @@ impl Node for MainPass2dNode {
render_context: &mut RenderContext,
world: &World,
) -> Result<(), NodeRunError> {
let color_attachment_texture = graph.get_input_texture(Self::IN_COLOR_ATTACHMENT)?;
let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
let (transparent_phase, target) = self
.query
.get_manual(world, view_entity)
.expect("view entity should exist");
let clear_color = world.get_resource::<ClearColor>().unwrap();
let pass_descriptor = RenderPassDescriptor {
label: Some("main_pass_2d"),
color_attachments: &[RenderPassColorAttachment {
view: color_attachment_texture,
view: &target.view,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(clear_color.0.into()),
@ -56,16 +57,10 @@ impl Node for MainPass2dNode {
depth_stencil_attachment: None,
};
let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
let draw_functions = world
.get_resource::<DrawFunctions<Transparent2d>>()
.unwrap();
let transparent_phase = self
.query
.get_manual(world, view_entity)
.expect("view entity should exist");
let render_pass = render_context
.command_encoder
.begin_render_pass(&pass_descriptor);

View file

@ -8,16 +8,21 @@ use bevy_render2::{
RenderPassDescriptor,
},
renderer::RenderContext,
view::ExtractedView,
view::{ExtractedView, ViewDepthTexture, ViewTarget},
};
pub struct MainPass3dNode {
query: QueryState<&'static RenderPhase<Transparent3d>, With<ExtractedView>>,
query: QueryState<
(
&'static RenderPhase<Transparent3d>,
&'static ViewTarget,
&'static ViewDepthTexture,
),
With<ExtractedView>,
>,
}
impl MainPass3dNode {
pub const IN_COLOR_ATTACHMENT: &'static str = "color_attachment";
pub const IN_DEPTH: &'static str = "depth";
pub const IN_VIEW: &'static str = "view";
pub fn new(world: &mut World) -> Self {
@ -29,11 +34,7 @@ impl MainPass3dNode {
impl Node for MainPass3dNode {
fn input(&self) -> Vec<SlotInfo> {
vec![
SlotInfo::new(MainPass3dNode::IN_COLOR_ATTACHMENT, SlotType::TextureView),
SlotInfo::new(MainPass3dNode::IN_DEPTH, SlotType::TextureView),
SlotInfo::new(MainPass3dNode::IN_VIEW, SlotType::Entity),
]
vec![SlotInfo::new(MainPass3dNode::IN_VIEW, SlotType::Entity)]
}
fn update(&mut self, world: &mut World) {
@ -46,21 +47,32 @@ impl Node for MainPass3dNode {
render_context: &mut RenderContext,
world: &World,
) -> Result<(), NodeRunError> {
let color_attachment_texture = graph.get_input_texture(Self::IN_COLOR_ATTACHMENT)?;
let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
let (transparent_phase, target, depth) = self
.query
.get_manual(world, view_entity)
.expect("view entity should exist");
let clear_color = world.get_resource::<ClearColor>().unwrap();
let depth_texture = graph.get_input_texture(Self::IN_DEPTH)?;
let pass_descriptor = RenderPassDescriptor {
label: Some("main_pass_3d"),
color_attachments: &[RenderPassColorAttachment {
view: color_attachment_texture,
resolve_target: None,
view: if let Some(sampled_target) = &target.sampled_target {
sampled_target
} else {
&target.view
},
resolve_target: if target.sampled_target.is_some() {
Some(&target.view)
} else {
None
},
ops: Operations {
load: LoadOp::Clear(clear_color.0.into()),
store: true,
},
}],
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: depth_texture,
view: &depth.view,
depth_ops: Some(Operations {
load: LoadOp::Clear(0.0),
store: true,
@ -69,14 +81,9 @@ impl Node for MainPass3dNode {
}),
};
let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
let draw_functions = world
.get_resource::<DrawFunctions<Transparent3d>>()
.unwrap();
let transparent_phase = self
.query
.get_manual(world, view_entity)
.expect("view entity should exist");
let render_pass = render_context
.command_encoder

View file

@ -1,10 +1,8 @@
use crate::ViewDepthTexture;
use bevy_ecs::world::World;
use bevy_render2::{
camera::{CameraPlugin, ExtractedCamera, ExtractedCameraNames},
camera::{CameraPlugin, ExtractedCameraNames},
render_graph::{Node, NodeRunError, RenderGraphContext, SlotValue},
renderer::RenderContext,
view::ExtractedWindows,
};
pub struct MainPassDriverNode;
@ -17,41 +15,17 @@ impl Node for MainPassDriverNode {
world: &World,
) -> Result<(), NodeRunError> {
let extracted_cameras = world.get_resource::<ExtractedCameraNames>().unwrap();
let extracted_windows = world.get_resource::<ExtractedWindows>().unwrap();
if let Some(camera_2d) = extracted_cameras.entities.get(CameraPlugin::CAMERA_2D) {
let extracted_camera = world.entity(*camera_2d).get::<ExtractedCamera>().unwrap();
let extracted_window = extracted_windows.get(&extracted_camera.window_id).unwrap();
let swap_chain_texture = extracted_window
.swap_chain_texture
.as_ref()
.unwrap()
.clone();
graph.run_sub_graph(
crate::draw_2d_graph::NAME,
vec![
SlotValue::Entity(*camera_2d),
SlotValue::TextureView(swap_chain_texture),
],
vec![SlotValue::Entity(*camera_2d)],
)?;
}
if let Some(camera_3d) = extracted_cameras.entities.get(CameraPlugin::CAMERA_3D) {
let extracted_camera = world.entity(*camera_3d).get::<ExtractedCamera>().unwrap();
let depth_texture = world.entity(*camera_3d).get::<ViewDepthTexture>().unwrap();
let extracted_window = extracted_windows.get(&extracted_camera.window_id).unwrap();
let swap_chain_texture = extracted_window
.swap_chain_texture
.as_ref()
.unwrap()
.clone();
graph.run_sub_graph(
crate::draw_3d_graph::NAME,
vec![
SlotValue::Entity(*camera_3d),
SlotValue::TextureView(swap_chain_texture),
SlotValue::TextureView(depth_texture.view.clone()),
],
vec![SlotValue::Entity(*camera_3d)],
)?;
}

View file

@ -21,7 +21,7 @@ use bevy_render2::{
render_resource::*,
renderer::{RenderDevice, RenderQueue},
texture::{BevyDefault, GpuImage, Image, TextureFormatPixelInfo},
view::{ExtractedView, ViewUniformOffset, ViewUniforms},
view::{ExtractedView, Msaa, ViewUniformOffset, ViewUniforms},
};
use bevy_transform::components::GlobalTransform;
use crevice::std140::AsStd140;
@ -369,16 +369,34 @@ impl FromWorld for PbrPipeline {
}
}
// TODO: add actual specialization key: MSAA, normal maps, shadeless, etc
bitflags::bitflags! {
#[repr(transparent)]
pub struct PbrPipelineKey: u32 { }
// NOTE: Apparently quadro drivers support up to 64x MSAA.
/// MSAA uses the highest 6 bits for the MSAA sample count - 1 to support up to 64x MSAA.
pub struct PbrPipelineKey: u32 {
const NONE = 0;
const MSAA_RESERVED_BITS = PbrPipelineKey::MSAA_MASK_BITS << PbrPipelineKey::MSAA_SHIFT_BITS;
}
}
impl PbrPipelineKey {
const MSAA_MASK_BITS: u32 = 0b111111;
const MSAA_SHIFT_BITS: u32 = 32 - 6;
pub fn from_msaa_samples(msaa_samples: u32) -> Self {
let msaa_bits = ((msaa_samples - 1) & Self::MSAA_MASK_BITS) << Self::MSAA_SHIFT_BITS;
PbrPipelineKey::from_bits(msaa_bits).unwrap()
}
pub fn msaa_samples(&self) -> u32 {
((self.bits >> Self::MSAA_SHIFT_BITS) & Self::MSAA_MASK_BITS) + 1
}
}
impl SpecializedPipeline for PbrPipeline {
type Key = PbrPipelineKey;
fn specialize(&self, _key: Self::Key) -> RenderPipelineDescriptor {
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
vertex: VertexState {
shader: PBR_SHADER_HANDLE.typed::<Shader>(),
@ -461,7 +479,7 @@ impl SpecializedPipeline for PbrPipeline {
},
}),
multisample: MultisampleState {
count: 1,
count: key.msaa_samples(),
mask: !0,
alpha_to_coverage_enabled: false,
},
@ -508,6 +526,7 @@ pub fn queue_meshes(
mut pipelines: ResMut<SpecializedPipelines<PbrPipeline>>,
mut pipeline_cache: ResMut<RenderPipelineCache>,
light_meta: Res<LightMeta>,
msaa: Res<Msaa>,
view_uniforms: Res<ViewUniforms>,
render_materials: Res<RenderAssets<StandardMaterial>>,
standard_material_meshes: Query<
@ -525,6 +544,7 @@ pub fn queue_meshes(
view_uniforms.uniforms.binding(),
light_meta.view_gpu_lights.binding(),
) {
let msaa_key = PbrPipelineKey::from_msaa_samples(msaa.samples);
for (entity, view, view_lights, mut transparent_phase) in views.iter_mut() {
let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor {
entries: &[
@ -580,8 +600,8 @@ pub fn queue_meshes(
continue;
}
let key = PbrPipelineKey::empty();
let pipeline_id = pipelines.specialize(&mut pipeline_cache, &pbr_pipeline, key);
let pipeline_id =
pipelines.specialize(&mut pipeline_cache, &pbr_pipeline, msaa_key);
// NOTE: row 2 of the view matrix dotted with column 3 of the model matrix
// gives the z component of translation of the mesh in view space
let mesh_z = view_row_2.dot(mesh_uniform.transform.col(3));
@ -692,3 +712,14 @@ impl RenderCommand<Transparent3d> for DrawMesh {
}
}
}
#[cfg(test)]
mod tests {
use super::PbrPipelineKey;
#[test]
fn pbr_key_msaa_samples() {
for i in 1..=64 {
assert_eq!(PbrPipelineKey::from_msaa_samples(i).msaa_samples(), i);
}
}
}

View file

@ -5,7 +5,10 @@ use bevy_utils::tracing::{info, info_span};
pub use graph_runner::*;
pub use render_device::*;
use crate::{render_graph::RenderGraph, view::ExtractedWindows};
use crate::{
render_graph::RenderGraph,
view::{ExtractedWindows, ViewTarget},
};
use bevy_ecs::prelude::*;
use std::sync::Arc;
use wgpu::{CommandEncoder, DeviceDescriptor, Instance, Queue, RequestAdapterOptions};
@ -27,6 +30,17 @@ pub fn render_system(world: &mut World) {
{
let span = info_span!("present_frames");
let _guard = span.enter();
// Remove ViewTarget components to ensure swap chain TextureViews are dropped.
// If all TextureViews aren't dropped before present, acquiring the next swap chain texture will fail.
let view_entities = world
.query_filtered::<Entity, With<ViewTarget>>()
.iter(world)
.collect::<Vec<_>>();
for view_entity in view_entities {
world.entity_mut(view_entity).remove::<ViewTarget>();
}
let mut windows = world.get_resource_mut::<ExtractedWindows>().unwrap();
for window in windows.values_mut() {
if let Some(texture_view) = window.swap_chain_texture.take() {

View file

@ -1,10 +1,13 @@
pub mod window;
use wgpu::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages};
pub use window::*;
use crate::{
render_resource::DynamicUniformVec,
camera::{ExtractedCamera, ExtractedCameraNames},
render_resource::{DynamicUniformVec, Texture, TextureView},
renderer::{RenderDevice, RenderQueue},
texture::{BevyDefault, TextureCache},
RenderApp, RenderStage,
};
use bevy_app::{App, Plugin};
@ -17,12 +20,38 @@ pub struct ViewPlugin;
impl Plugin for ViewPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<Msaa>();
app.sub_app(RenderApp)
.init_resource::<ViewUniforms>()
.add_system_to_stage(RenderStage::Prepare, prepare_views);
.add_system_to_stage(RenderStage::Extract, extract_msaa)
.add_system_to_stage(RenderStage::Prepare, prepare_view_uniforms)
.add_system_to_stage(
RenderStage::Prepare,
prepare_view_targets.after(WindowSystem::Prepare),
);
}
}
#[derive(Clone)]
pub struct Msaa {
/// The number of samples to run for Multi-Sample Anti-Aliasing. Higher numbers result in
/// smoother edges. Note that WGPU currently only supports 1 or 4 samples.
/// Ultimately we plan on supporting whatever is natively supported on a given device.
/// Check out this issue for more info: https://github.com/gfx-rs/wgpu/issues/1832
pub samples: u32,
}
impl Default for Msaa {
fn default() -> Self {
Self { samples: 4 }
}
}
pub fn extract_msaa(mut commands: Commands, msaa: Res<Msaa>) {
// NOTE: windows.is_changed() handles cases where a window was resized
commands.insert_resource(msaa.clone());
}
pub struct ExtractedView {
pub projection: Mat4,
pub transform: GlobalTransform,
@ -46,17 +75,27 @@ pub struct ViewUniformOffset {
pub offset: u32,
}
fn prepare_views(
pub struct ViewTarget {
pub view: TextureView,
pub sampled_target: Option<TextureView>,
}
pub struct ViewDepthTexture {
pub texture: Texture,
pub view: TextureView,
}
fn prepare_view_uniforms(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut view_uniforms: ResMut<ViewUniforms>,
mut extracted_views: Query<(Entity, &ExtractedView)>,
mut views: Query<(Entity, &ExtractedView)>,
) {
view_uniforms
.uniforms
.reserve_and_clear(extracted_views.iter_mut().len(), &render_device);
for (entity, camera) in extracted_views.iter() {
.reserve_and_clear(views.iter_mut().len(), &render_device);
for (entity, camera) in views.iter() {
let projection = camera.projection;
let view_uniforms = ViewUniformOffset {
offset: view_uniforms.uniforms.push(ViewUniform {
@ -71,3 +110,57 @@ fn prepare_views(
view_uniforms.uniforms.write_buffer(&render_queue);
}
fn prepare_view_targets(
mut commands: Commands,
camera_names: Res<ExtractedCameraNames>,
windows: Res<ExtractedWindows>,
msaa: Res<Msaa>,
render_device: Res<RenderDevice>,
mut texture_cache: ResMut<TextureCache>,
cameras: Query<&ExtractedCamera>,
) {
for entity in camera_names.entities.values().copied() {
let camera = if let Ok(camera) = cameras.get(entity) {
camera
} else {
continue;
};
let window = if let Some(window) = windows.get(&camera.window_id) {
window
} else {
continue;
};
let swap_chain_texture = if let Some(texture) = &window.swap_chain_texture {
texture
} else {
continue;
};
let sampled_target = if msaa.samples > 1 {
let sampled_texture = texture_cache.get(
&render_device,
TextureDescriptor {
label: Some("sampled_color_attachment_texture"),
size: Extent3d {
width: window.physical_width,
height: window.physical_height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: msaa.samples,
dimension: TextureDimension::D2,
format: TextureFormat::bevy_default(),
usage: TextureUsages::RENDER_ATTACHMENT,
},
);
Some(sampled_texture.default_view.clone())
} else {
None
};
commands.entity(entity).insert(ViewTarget {
view: swap_chain_texture.clone(),
sampled_target,
});
}
}

View file

@ -17,6 +17,11 @@ pub struct NonSendMarker;
pub struct WindowRenderPlugin;
#[derive(SystemLabel, Debug, Clone, PartialEq, Eq, Hash)]
pub enum WindowSystem {
Prepare,
}
impl Plugin for WindowRenderPlugin {
fn build(&self, app: &mut App) {
app.sub_app(RenderApp)
@ -24,7 +29,10 @@ impl Plugin for WindowRenderPlugin {
.init_resource::<WindowSurfaces>()
.init_resource::<NonSendMarker>()
.add_system_to_stage(RenderStage::Extract, extract_windows)
.add_system_to_stage(RenderStage::Prepare, prepare_windows);
.add_system_to_stage(
RenderStage::Prepare,
prepare_windows.label(WindowSystem::Prepare),
);
}
}