bevy/crates/bevy_anti_aliasing/src/smaa/mod.rs
Chris Russell f7e112a3c9
Let query items borrow from query state to avoid needing to clone (#15396)
# Objective

Improve the performance of `FilteredEntity(Ref|Mut)` and
`Entity(Ref|Mut)Except`.

`FilteredEntityRef` needs an `Access<ComponentId>` to determine what
components it can access. There is one stored in the query state, but
query items cannot borrow from the state, so it has to `clone()` the
access for each row. Cloning the access involves memory allocations and
can be expensive.


## Solution

Let query items borrow from their query state.  

Add an `'s` lifetime to `WorldQuery::Item` and `WorldQuery::Fetch`,
similar to the one in `SystemParam`, and provide `&'s Self::State` to
the fetch so that it can borrow from the state.

Unfortunately, there are a few cases where we currently return query
items from temporary query states: the sorted iteration methods create a
temporary state to query the sort keys, and the
`EntityRef::components<Q>()` methods create a temporary state for their
query.

To allow these to continue to work with most `QueryData`
implementations, introduce a new subtrait `ReleaseStateQueryData` that
converts a `QueryItem<'w, 's>` to `QueryItem<'w, 'static>`, and is
implemented for everything except `FilteredEntity(Ref|Mut)` and
`Entity(Ref|Mut)Except`.

`#[derive(QueryData)]` will generate `ReleaseStateQueryData`
implementations that apply when all of the subqueries implement
`ReleaseStateQueryData`.

This PR does not actually change the implementation of
`FilteredEntity(Ref|Mut)` or `Entity(Ref|Mut)Except`! That will be done
as a follow-up PR so that the changes are easier to review. I have
pushed the changes as chescock/bevy#5.

## Testing

I ran performance traces of many_foxes, both against main and against
chescock/bevy#5, both including #15282. These changes do appear to make
generalized animation a bit faster:

(Red is main, yellow is chescock/bevy#5)

![image](https://github.com/user-attachments/assets/de900117-0c6a-431d-ab62-c013834f97a9)


## Migration Guide

The `WorldQuery::Item` and `WorldQuery::Fetch` associated types and the
`QueryItem` and `ROQueryItem` type aliases now have an additional
lifetime parameter corresponding to the `'s` lifetime in `Query`. Manual
implementations of `WorldQuery` will need to update the method
signatures to include the new lifetimes. Other uses of the types will
need to be updated to include a lifetime parameter, although it can
usually be passed as `'_`. In particular, `ROQueryItem` is used when
implementing `RenderCommand`.

Before: 

```rust
fn render<'w>(
    item: &P,
    view: ROQueryItem<'w, Self::ViewQuery>,
    entity: Option<ROQueryItem<'w, Self::ItemQuery>>,
    param: SystemParamItem<'w, '_, Self::Param>,
    pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult;
```

After: 

```rust
fn render<'w>(
    item: &P,
    view: ROQueryItem<'w, '_, Self::ViewQuery>,
    entity: Option<ROQueryItem<'w, '_, Self::ItemQuery>>,
    param: SystemParamItem<'w, '_, Self::Param>,
    pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult;
```

---

Methods on `QueryState` that take `&mut self` may now result in
conflicting borrows if the query items capture the lifetime of the
mutable reference. This affects `get()`, `iter()`, and others. To fix
the errors, first call `QueryState::update_archetypes()`, and then
replace a call `state.foo(world, param)` with
`state.query_manual(world).foo_inner(param)`. Alternately, you may be
able to restructure the code to call `state.query(world)` once and then
make multiple calls using the `Query`.

Before:
```rust
let mut state: QueryState<_, _> = ...;
let d1 = state.get(world, e1);
let d2 = state.get(world, e2); // Error: cannot borrow `state` as mutable more than once at a time
println!("{d1:?}");
println!("{d2:?}");
```

After: 
```rust
let mut state: QueryState<_, _> = ...;

state.update_archetypes(world);
let d1 = state.get_manual(world, e1);
let d2 = state.get_manual(world, e2);
// OR
state.update_archetypes(world);
let d1 = state.query(world).get_inner(e1);
let d2 = state.query(world).get_inner(e2);
// OR
let query = state.query(world);
let d1 = query.get_inner(e1);
let d1 = query.get_inner(e2);

println!("{d1:?}");
println!("{d2:?}");
```
2025-06-16 21:05:41 +00:00

1092 lines
41 KiB
Rust

//! Subpixel morphological antialiasing (SMAA).
//!
//! [SMAA] is a 2011 antialiasing technique that takes an aliased image and
//! smooths out the *jaggies*, making edges smoother. It's been used in numerous
//! games and has become a staple postprocessing technique. Compared to MSAA,
//! SMAA has the advantage of compatibility with deferred rendering and
//! reduction of GPU memory bandwidth. Compared to FXAA, SMAA has the advantage
//! of improved quality, but the disadvantage of reduced performance. Compared
//! to TAA, SMAA has the advantage of stability and lack of *ghosting*
//! artifacts, but has the disadvantage of not supporting temporal accumulation,
//! which have made SMAA less popular when advanced photorealistic rendering
//! features are used in recent years.
//!
//! To use SMAA, add [`Smaa`] to a [`bevy_render::camera::Camera`]. In a
//! pinch, you can simply use the default settings (via the [`Default`] trait)
//! for a high-quality, high-performance appearance. When using SMAA, you will
//! likely want set [`bevy_render::view::Msaa`] to [`bevy_render::view::Msaa::Off`]
//! for every camera using SMAA.
//!
//! Those who have used SMAA in other engines should be aware that Bevy doesn't
//! yet support the following more advanced features of SMAA:
//!
//! * The temporal variant.
//!
//! * Depth- and chroma-based edge detection.
//!
//! * Predicated thresholding.
//!
//! * Compatibility with SSAA and MSAA.
//!
//! [SMAA]: https://www.iryoku.com/smaa/
use bevy_app::{App, Plugin};
#[cfg(feature = "smaa_luts")]
use bevy_asset::load_internal_binary_asset;
use bevy_asset::{embedded_asset, load_embedded_asset, weak_handle, Handle};
#[cfg(not(feature = "smaa_luts"))]
use bevy_core_pipeline::tonemapping::lut_placeholder;
use bevy_core_pipeline::{
core_2d::graph::{Core2d, Node2d},
core_3d::graph::{Core3d, Node3d},
};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::Entity,
query::{QueryItem, With},
reflect::ReflectComponent,
resource::Resource,
schedule::IntoScheduleConfigs as _,
system::{lifetimeless::Read, Commands, Query, Res, ResMut},
world::{FromWorld, World},
};
use bevy_image::{BevyDefault, Image};
use bevy_math::{vec4, Vec4};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
camera::ExtractedCamera,
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_asset::RenderAssets,
render_graph::{
NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner,
},
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
AddressMode, BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries,
CachedRenderPipelineId, ColorTargetState, ColorWrites, CompareFunction, DepthStencilState,
DynamicUniformBuffer, Extent3d, FilterMode, FragmentState, LoadOp, MultisampleState,
Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment,
RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipeline,
RenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, Shader, ShaderDefVal,
ShaderStages, ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines,
StencilFaceState, StencilOperation, StencilState, StoreOp, TextureDescriptor,
TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureView,
VertexState,
},
renderer::{RenderContext, RenderDevice, RenderQueue},
texture::{CachedTexture, GpuImage, TextureCache},
view::{ExtractedView, ViewTarget},
Render, RenderApp, RenderSystems,
};
use bevy_utils::prelude::default;
/// The handle of the area LUT, a KTX2 format texture that SMAA uses internally.
const SMAA_AREA_LUT_TEXTURE_HANDLE: Handle<Image> =
weak_handle!("569c4d67-c7fa-4958-b1af-0836023603c0");
/// The handle of the search LUT, a KTX2 format texture that SMAA uses internally.
const SMAA_SEARCH_LUT_TEXTURE_HANDLE: Handle<Image> =
weak_handle!("43b97515-252e-4c8a-b9af-f2fc528a1c27");
/// Adds support for subpixel morphological antialiasing, or SMAA.
pub struct SmaaPlugin;
/// A component for enabling Subpixel Morphological Anti-Aliasing (SMAA)
/// for a [`bevy_render::camera::Camera`].
#[derive(Clone, Copy, Default, Component, Reflect, ExtractComponent)]
#[reflect(Component, Default, Clone)]
#[doc(alias = "SubpixelMorphologicalAntiAliasing")]
pub struct Smaa {
/// A predefined set of SMAA parameters: i.e. a quality level.
///
/// Generally, you can leave this at its default level.
pub preset: SmaaPreset,
}
/// A preset quality level for SMAA.
///
/// Higher values are slower but result in a higher-quality image.
///
/// The default value is *high*.
#[derive(Clone, Copy, Reflect, Default, PartialEq, Eq, Hash)]
#[reflect(Default, Clone, PartialEq, Hash)]
pub enum SmaaPreset {
/// Four search steps; no diagonal or corner detection.
Low,
/// Eight search steps; no diagonal or corner detection.
Medium,
/// Sixteen search steps, 8 diagonal search steps, and corner detection.
///
/// This is the default.
#[default]
High,
/// Thirty-two search steps, 8 diagonal search steps, and corner detection.
Ultra,
}
/// A render world resource that holds all render pipeline data needed for SMAA.
///
/// There are three separate passes, so we need three separate pipelines.
#[derive(Resource)]
pub struct SmaaPipelines {
/// Pass 1: Edge detection.
edge_detection: SmaaEdgeDetectionPipeline,
/// Pass 2: Blending weight calculation.
blending_weight_calculation: SmaaBlendingWeightCalculationPipeline,
/// Pass 3: Neighborhood blending.
neighborhood_blending: SmaaNeighborhoodBlendingPipeline,
}
/// The pipeline data for phase 1 of SMAA: edge detection.
struct SmaaEdgeDetectionPipeline {
/// The bind group layout common to all passes.
postprocess_bind_group_layout: BindGroupLayout,
/// The bind group layout for data specific to this pass.
edge_detection_bind_group_layout: BindGroupLayout,
/// The shader asset handle.
shader: Handle<Shader>,
}
/// The pipeline data for phase 2 of SMAA: blending weight calculation.
struct SmaaBlendingWeightCalculationPipeline {
/// The bind group layout common to all passes.
postprocess_bind_group_layout: BindGroupLayout,
/// The bind group layout for data specific to this pass.
blending_weight_calculation_bind_group_layout: BindGroupLayout,
/// The shader asset handle.
shader: Handle<Shader>,
}
/// The pipeline data for phase 3 of SMAA: neighborhood blending.
struct SmaaNeighborhoodBlendingPipeline {
/// The bind group layout common to all passes.
postprocess_bind_group_layout: BindGroupLayout,
/// The bind group layout for data specific to this pass.
neighborhood_blending_bind_group_layout: BindGroupLayout,
/// The shader asset handle.
shader: Handle<Shader>,
}
/// A unique identifier for a set of SMAA pipelines.
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct SmaaNeighborhoodBlendingPipelineKey {
/// The format of the framebuffer.
texture_format: TextureFormat,
/// The quality preset.
preset: SmaaPreset,
}
/// A render world component that holds the pipeline IDs for the SMAA passes.
///
/// There are three separate SMAA passes, each with a different shader and bind
/// group layout, so we need three pipeline IDs.
#[derive(Component)]
pub struct ViewSmaaPipelines {
/// The pipeline ID for edge detection (phase 1).
edge_detection_pipeline_id: CachedRenderPipelineId,
/// The pipeline ID for blending weight calculation (phase 2).
blending_weight_calculation_pipeline_id: CachedRenderPipelineId,
/// The pipeline ID for neighborhood blending (phase 3).
neighborhood_blending_pipeline_id: CachedRenderPipelineId,
}
/// The render graph node that performs subpixel morphological antialiasing
/// (SMAA).
#[derive(Default)]
pub struct SmaaNode;
/// Values supplied to the GPU for SMAA.
///
/// Currently, this just contains the render target metrics and values derived
/// from them. These could be computed by the shader itself, but the original
/// SMAA HLSL code supplied them in a uniform, so we do the same for
/// consistency.
#[derive(Clone, Copy, ShaderType)]
pub struct SmaaInfoUniform {
/// Information about the width and height of the framebuffer.
///
/// * *x*: The reciprocal pixel width of the framebuffer.
///
/// * *y*: The reciprocal pixel height of the framebuffer.
///
/// * *z*: The pixel width of the framebuffer.
///
/// * *w*: The pixel height of the framebuffer.
pub rt_metrics: Vec4,
}
/// A render world component that stores the offset of each [`SmaaInfoUniform`]
/// within the [`SmaaInfoUniformBuffer`] for each view.
#[derive(Clone, Copy, Deref, DerefMut, Component)]
pub struct SmaaInfoUniformOffset(pub u32);
/// The GPU buffer that holds all [`SmaaInfoUniform`]s for all views.
///
/// This is a resource stored in the render world.
#[derive(Resource, Default, Deref, DerefMut)]
pub struct SmaaInfoUniformBuffer(pub DynamicUniformBuffer<SmaaInfoUniform>);
/// A render world component that holds the intermediate textures necessary to
/// perform SMAA.
///
/// This is stored on each view that has enabled SMAA.
#[derive(Component)]
pub struct SmaaTextures {
/// The two-channel texture that stores the output from the first pass (edge
/// detection).
///
/// The second pass (blending weight calculation) reads this texture to do
/// its work.
pub edge_detection_color_texture: CachedTexture,
/// The 8-bit stencil texture that records which pixels the first pass
/// touched, so that the second pass doesn't have to examine other pixels.
///
/// Each texel will contain a 0 if the first pass didn't touch the
/// corresponding pixel or a 1 if the first pass did touch that pixel.
pub edge_detection_stencil_texture: CachedTexture,
/// A four-channel RGBA texture that stores the output from the second pass
/// (blending weight calculation).
///
/// The final pass (neighborhood blending) reads this texture to do its
/// work.
pub blend_texture: CachedTexture,
}
/// A render world component that stores the bind groups necessary to perform
/// SMAA.
///
/// This is stored on each view.
#[derive(Component)]
pub struct SmaaBindGroups {
/// The bind group for the first pass (edge detection).
pub edge_detection_bind_group: BindGroup,
/// The bind group for the second pass (blending weight calculation).
pub blending_weight_calculation_bind_group: BindGroup,
/// The bind group for the final pass (neighborhood blending).
pub neighborhood_blending_bind_group: BindGroup,
}
/// Stores the specialized render pipelines for SMAA.
///
/// Because SMAA uses three passes, we need three separate render pipeline
/// stores.
#[derive(Resource, Default)]
pub struct SmaaSpecializedRenderPipelines {
/// Specialized render pipelines for the first phase (edge detection).
edge_detection: SpecializedRenderPipelines<SmaaEdgeDetectionPipeline>,
/// Specialized render pipelines for the second phase (blending weight
/// calculation).
blending_weight_calculation: SpecializedRenderPipelines<SmaaBlendingWeightCalculationPipeline>,
/// Specialized render pipelines for the third phase (neighborhood
/// blending).
neighborhood_blending: SpecializedRenderPipelines<SmaaNeighborhoodBlendingPipeline>,
}
impl Plugin for SmaaPlugin {
fn build(&self, app: &mut App) {
// Load the shader.
embedded_asset!(app, "smaa.wgsl");
// Load the two lookup textures. These are compressed textures in KTX2
// format.
#[cfg(feature = "smaa_luts")]
load_internal_binary_asset!(
app,
SMAA_AREA_LUT_TEXTURE_HANDLE,
"SMAAAreaLUT.ktx2",
|bytes, _: String| Image::from_buffer(
bytes,
bevy_image::ImageType::Format(bevy_image::ImageFormat::Ktx2),
bevy_image::CompressedImageFormats::NONE,
false,
bevy_image::ImageSampler::Default,
bevy_asset::RenderAssetUsages::RENDER_WORLD,
)
.expect("Failed to load SMAA area LUT")
);
#[cfg(feature = "smaa_luts")]
load_internal_binary_asset!(
app,
SMAA_SEARCH_LUT_TEXTURE_HANDLE,
"SMAASearchLUT.ktx2",
|bytes, _: String| Image::from_buffer(
bytes,
bevy_image::ImageType::Format(bevy_image::ImageFormat::Ktx2),
bevy_image::CompressedImageFormats::NONE,
false,
bevy_image::ImageSampler::Default,
bevy_asset::RenderAssetUsages::RENDER_WORLD,
)
.expect("Failed to load SMAA search LUT")
);
#[cfg(not(feature = "smaa_luts"))]
app.world_mut()
.resource_mut::<bevy_asset::Assets<Image>>()
.insert(SMAA_AREA_LUT_TEXTURE_HANDLE.id(), lut_placeholder());
#[cfg(not(feature = "smaa_luts"))]
app.world_mut()
.resource_mut::<bevy_asset::Assets<Image>>()
.insert(SMAA_SEARCH_LUT_TEXTURE_HANDLE.id(), lut_placeholder());
app.add_plugins(ExtractComponentPlugin::<Smaa>::default())
.register_type::<Smaa>();
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SmaaSpecializedRenderPipelines>()
.init_resource::<SmaaInfoUniformBuffer>()
.add_systems(
Render,
(
prepare_smaa_pipelines.in_set(RenderSystems::Prepare),
prepare_smaa_uniforms.in_set(RenderSystems::PrepareResources),
prepare_smaa_textures.in_set(RenderSystems::PrepareResources),
prepare_smaa_bind_groups.in_set(RenderSystems::PrepareBindGroups),
),
)
.add_render_graph_node::<ViewNodeRunner<SmaaNode>>(Core3d, Node3d::Smaa)
.add_render_graph_edges(
Core3d,
(
Node3d::Tonemapping,
Node3d::Smaa,
Node3d::EndMainPassPostProcessing,
),
)
.add_render_graph_node::<ViewNodeRunner<SmaaNode>>(Core2d, Node2d::Smaa)
.add_render_graph_edges(
Core2d,
(
Node2d::Tonemapping,
Node2d::Smaa,
Node2d::EndMainPassPostProcessing,
),
);
}
fn finish(&self, app: &mut App) {
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<SmaaPipelines>();
}
}
}
impl FromWorld for SmaaPipelines {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
// Create the postprocess bind group layout (all passes, bind group 0).
let postprocess_bind_group_layout = render_device.create_bind_group_layout(
"SMAA postprocess bind group layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
uniform_buffer::<SmaaInfoUniform>(true)
.visibility(ShaderStages::VERTEX_FRAGMENT),
),
),
);
// Create the edge detection bind group layout (pass 1, bind group 1).
let edge_detection_bind_group_layout = render_device.create_bind_group_layout(
"SMAA edge detection bind group layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(sampler(SamplerBindingType::Filtering),),
),
);
// Create the blending weight calculation bind group layout (pass 2, bind group 1).
let blending_weight_calculation_bind_group_layout = render_device.create_bind_group_layout(
"SMAA blending weight calculation bind group layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }), // edges texture
sampler(SamplerBindingType::Filtering), // edges sampler
texture_2d(TextureSampleType::Float { filterable: true }), // search texture
texture_2d(TextureSampleType::Float { filterable: true }), // area texture
),
),
);
// Create the neighborhood blending bind group layout (pass 3, bind group 1).
let neighborhood_blending_bind_group_layout = render_device.create_bind_group_layout(
"SMAA neighborhood blending bind group layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
),
),
);
let shader = load_embedded_asset!(world, "smaa.wgsl");
SmaaPipelines {
edge_detection: SmaaEdgeDetectionPipeline {
postprocess_bind_group_layout: postprocess_bind_group_layout.clone(),
edge_detection_bind_group_layout,
shader: shader.clone(),
},
blending_weight_calculation: SmaaBlendingWeightCalculationPipeline {
postprocess_bind_group_layout: postprocess_bind_group_layout.clone(),
blending_weight_calculation_bind_group_layout,
shader: shader.clone(),
},
neighborhood_blending: SmaaNeighborhoodBlendingPipeline {
postprocess_bind_group_layout,
neighborhood_blending_bind_group_layout,
shader,
},
}
}
}
// Phase 1: edge detection.
impl SpecializedRenderPipeline for SmaaEdgeDetectionPipeline {
type Key = SmaaPreset;
fn specialize(&self, preset: Self::Key) -> RenderPipelineDescriptor {
let shader_defs = vec!["SMAA_EDGE_DETECTION".into(), preset.shader_def()];
// We mark the pixels that we touched with a 1 so that the blending
// weight calculation (phase 2) will only consider those. This reduces
// the overhead of phase 2 considerably.
let stencil_face_state = StencilFaceState {
compare: CompareFunction::Always,
fail_op: StencilOperation::Replace,
depth_fail_op: StencilOperation::Replace,
pass_op: StencilOperation::Replace,
};
RenderPipelineDescriptor {
label: Some("SMAA edge detection".into()),
layout: vec![
self.postprocess_bind_group_layout.clone(),
self.edge_detection_bind_group_layout.clone(),
],
vertex: VertexState {
shader: self.shader.clone(),
shader_defs: shader_defs.clone(),
entry_point: "edge_detection_vertex_main".into(),
buffers: vec![],
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
shader_defs,
entry_point: "luma_edge_detection_fragment_main".into(),
targets: vec![Some(ColorTargetState {
format: TextureFormat::Rg8Unorm,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
push_constant_ranges: vec![],
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: TextureFormat::Stencil8,
depth_write_enabled: false,
depth_compare: CompareFunction::Always,
stencil: StencilState {
front: stencil_face_state,
back: stencil_face_state,
read_mask: 1,
write_mask: 1,
},
bias: default(),
}),
multisample: MultisampleState::default(),
zero_initialize_workgroup_memory: false,
}
}
}
// Phase 2: blending weight calculation.
impl SpecializedRenderPipeline for SmaaBlendingWeightCalculationPipeline {
type Key = SmaaPreset;
fn specialize(&self, preset: Self::Key) -> RenderPipelineDescriptor {
let shader_defs = vec![
"SMAA_BLENDING_WEIGHT_CALCULATION".into(),
preset.shader_def(),
];
// Only consider the pixels that were touched in phase 1.
let stencil_face_state = StencilFaceState {
compare: CompareFunction::Equal,
fail_op: StencilOperation::Keep,
depth_fail_op: StencilOperation::Keep,
pass_op: StencilOperation::Keep,
};
RenderPipelineDescriptor {
label: Some("SMAA blending weight calculation".into()),
layout: vec![
self.postprocess_bind_group_layout.clone(),
self.blending_weight_calculation_bind_group_layout.clone(),
],
vertex: VertexState {
shader: self.shader.clone(),
shader_defs: shader_defs.clone(),
entry_point: "blending_weight_calculation_vertex_main".into(),
buffers: vec![],
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
shader_defs,
entry_point: "blending_weight_calculation_fragment_main".into(),
targets: vec![Some(ColorTargetState {
format: TextureFormat::Rgba8Unorm,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
push_constant_ranges: vec![],
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: TextureFormat::Stencil8,
depth_write_enabled: false,
depth_compare: CompareFunction::Always,
stencil: StencilState {
front: stencil_face_state,
back: stencil_face_state,
read_mask: 1,
write_mask: 1,
},
bias: default(),
}),
multisample: MultisampleState::default(),
zero_initialize_workgroup_memory: false,
}
}
}
impl SpecializedRenderPipeline for SmaaNeighborhoodBlendingPipeline {
type Key = SmaaNeighborhoodBlendingPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let shader_defs = vec!["SMAA_NEIGHBORHOOD_BLENDING".into(), key.preset.shader_def()];
RenderPipelineDescriptor {
label: Some("SMAA neighborhood blending".into()),
layout: vec![
self.postprocess_bind_group_layout.clone(),
self.neighborhood_blending_bind_group_layout.clone(),
],
vertex: VertexState {
shader: self.shader.clone(),
shader_defs: shader_defs.clone(),
entry_point: "neighborhood_blending_vertex_main".into(),
buffers: vec![],
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
shader_defs,
entry_point: "neighborhood_blending_fragment_main".into(),
targets: vec![Some(ColorTargetState {
format: key.texture_format,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
push_constant_ranges: vec![],
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
zero_initialize_workgroup_memory: false,
}
}
}
/// A system, part of the render app, that specializes the three pipelines
/// needed for SMAA according to each view's SMAA settings.
fn prepare_smaa_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut specialized_render_pipelines: ResMut<SmaaSpecializedRenderPipelines>,
smaa_pipelines: Res<SmaaPipelines>,
view_targets: Query<(Entity, &ExtractedView, &Smaa)>,
) {
for (entity, view, smaa) in &view_targets {
let edge_detection_pipeline_id = specialized_render_pipelines.edge_detection.specialize(
&pipeline_cache,
&smaa_pipelines.edge_detection,
smaa.preset,
);
let blending_weight_calculation_pipeline_id = specialized_render_pipelines
.blending_weight_calculation
.specialize(
&pipeline_cache,
&smaa_pipelines.blending_weight_calculation,
smaa.preset,
);
let neighborhood_blending_pipeline_id = specialized_render_pipelines
.neighborhood_blending
.specialize(
&pipeline_cache,
&smaa_pipelines.neighborhood_blending,
SmaaNeighborhoodBlendingPipelineKey {
texture_format: if view.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
preset: smaa.preset,
},
);
commands.entity(entity).insert(ViewSmaaPipelines {
edge_detection_pipeline_id,
blending_weight_calculation_pipeline_id,
neighborhood_blending_pipeline_id,
});
}
}
/// A system, part of the render app, that builds the [`SmaaInfoUniform`] data
/// for each view with SMAA enabled and writes the resulting data to GPU memory.
fn prepare_smaa_uniforms(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
view_targets: Query<(Entity, &ExtractedView), With<Smaa>>,
mut smaa_info_buffer: ResMut<SmaaInfoUniformBuffer>,
) {
smaa_info_buffer.clear();
for (entity, view) in &view_targets {
let offset = smaa_info_buffer.push(&SmaaInfoUniform {
rt_metrics: vec4(
1.0 / view.viewport.z as f32,
1.0 / view.viewport.w as f32,
view.viewport.z as f32,
view.viewport.w as f32,
),
});
commands
.entity(entity)
.insert(SmaaInfoUniformOffset(offset));
}
smaa_info_buffer.write_buffer(&render_device, &render_queue);
}
/// A system, part of the render app, that builds the intermediate textures for
/// each view with SMAA enabled.
///
/// Phase 1 (edge detection) needs a two-channel RG texture and an 8-bit stencil
/// texture; phase 2 (blend weight calculation) needs a four-channel RGBA
/// texture.
fn prepare_smaa_textures(
mut commands: Commands,
render_device: Res<RenderDevice>,
mut texture_cache: ResMut<TextureCache>,
view_targets: Query<(Entity, &ExtractedCamera), (With<ExtractedView>, With<Smaa>)>,
) {
for (entity, camera) in &view_targets {
let Some(texture_size) = camera.physical_target_size else {
continue;
};
let texture_size = Extent3d {
width: texture_size.x,
height: texture_size.y,
depth_or_array_layers: 1,
};
// Create the two-channel RG texture for phase 1 (edge detection).
let edge_detection_color_texture = texture_cache.get(
&render_device,
TextureDescriptor {
label: Some("SMAA edge detection color texture"),
size: texture_size,
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rg8Unorm,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
},
);
// Create the stencil texture for phase 1 (edge detection).
let edge_detection_stencil_texture = texture_cache.get(
&render_device,
TextureDescriptor {
label: Some("SMAA edge detection stencil texture"),
size: texture_size,
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Stencil8,
usage: TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
},
);
// Create the four-channel RGBA texture for phase 2 (blending weight
// calculation).
let blend_texture = texture_cache.get(
&render_device,
TextureDescriptor {
label: Some("SMAA blend texture"),
size: texture_size,
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
},
);
commands.entity(entity).insert(SmaaTextures {
edge_detection_color_texture,
edge_detection_stencil_texture,
blend_texture,
});
}
}
/// A system, part of the render app, that builds the SMAA bind groups for each
/// view with SMAA enabled.
fn prepare_smaa_bind_groups(
mut commands: Commands,
render_device: Res<RenderDevice>,
smaa_pipelines: Res<SmaaPipelines>,
images: Res<RenderAssets<GpuImage>>,
view_targets: Query<(Entity, &SmaaTextures), (With<ExtractedView>, With<Smaa>)>,
) {
// Fetch the two lookup textures. These are bundled in this library.
let (Some(search_texture), Some(area_texture)) = (
images.get(&SMAA_SEARCH_LUT_TEXTURE_HANDLE),
images.get(&SMAA_AREA_LUT_TEXTURE_HANDLE),
) else {
return;
};
for (entity, smaa_textures) in &view_targets {
// We use the same sampler settings for all textures, so we can build
// only one and reuse it.
let sampler = render_device.create_sampler(&SamplerDescriptor {
label: Some("SMAA sampler"),
address_mode_u: AddressMode::ClampToEdge,
address_mode_v: AddressMode::ClampToEdge,
address_mode_w: AddressMode::ClampToEdge,
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
..default()
});
commands.entity(entity).insert(SmaaBindGroups {
edge_detection_bind_group: render_device.create_bind_group(
Some("SMAA edge detection bind group"),
&smaa_pipelines
.edge_detection
.edge_detection_bind_group_layout,
&BindGroupEntries::sequential((&sampler,)),
),
blending_weight_calculation_bind_group: render_device.create_bind_group(
Some("SMAA blending weight calculation bind group"),
&smaa_pipelines
.blending_weight_calculation
.blending_weight_calculation_bind_group_layout,
&BindGroupEntries::sequential((
&smaa_textures.edge_detection_color_texture.default_view,
&sampler,
&search_texture.texture_view,
&area_texture.texture_view,
)),
),
neighborhood_blending_bind_group: render_device.create_bind_group(
Some("SMAA neighborhood blending bind group"),
&smaa_pipelines
.neighborhood_blending
.neighborhood_blending_bind_group_layout,
&BindGroupEntries::sequential((
&smaa_textures.blend_texture.default_view,
&sampler,
)),
),
});
}
}
impl ViewNode for SmaaNode {
type ViewQuery = (
Read<ViewTarget>,
Read<ViewSmaaPipelines>,
Read<SmaaInfoUniformOffset>,
Read<SmaaTextures>,
Read<SmaaBindGroups>,
);
fn run<'w>(
&self,
_: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(
view_target,
view_pipelines,
view_smaa_uniform_offset,
smaa_textures,
view_smaa_bind_groups,
): QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let pipeline_cache = world.resource::<PipelineCache>();
let smaa_pipelines = world.resource::<SmaaPipelines>();
let smaa_info_uniform_buffer = world.resource::<SmaaInfoUniformBuffer>();
// Fetch the render pipelines.
let (
Some(edge_detection_pipeline),
Some(blending_weight_calculation_pipeline),
Some(neighborhood_blending_pipeline),
) = (
pipeline_cache.get_render_pipeline(view_pipelines.edge_detection_pipeline_id),
pipeline_cache
.get_render_pipeline(view_pipelines.blending_weight_calculation_pipeline_id),
pipeline_cache.get_render_pipeline(view_pipelines.neighborhood_blending_pipeline_id),
)
else {
return Ok(());
};
// Fetch the framebuffer textures.
let postprocess = view_target.post_process_write();
let (source, destination) = (postprocess.source, postprocess.destination);
// Stage 1: Edge detection pass.
perform_edge_detection(
render_context,
smaa_pipelines,
smaa_textures,
view_smaa_bind_groups,
smaa_info_uniform_buffer,
view_smaa_uniform_offset,
edge_detection_pipeline,
source,
);
// Stage 2: Blending weight calculation pass.
perform_blending_weight_calculation(
render_context,
smaa_pipelines,
smaa_textures,
view_smaa_bind_groups,
smaa_info_uniform_buffer,
view_smaa_uniform_offset,
blending_weight_calculation_pipeline,
source,
);
// Stage 3: Neighborhood blending pass.
perform_neighborhood_blending(
render_context,
smaa_pipelines,
view_smaa_bind_groups,
smaa_info_uniform_buffer,
view_smaa_uniform_offset,
neighborhood_blending_pipeline,
source,
destination,
);
Ok(())
}
}
/// Performs edge detection (phase 1).
///
/// This runs as part of the [`SmaaNode`]. It reads from the source texture and
/// writes to the two-channel RG edges texture. Additionally, it ensures that
/// all pixels it didn't touch are stenciled out so that phase 2 won't have to
/// examine them.
fn perform_edge_detection(
render_context: &mut RenderContext,
smaa_pipelines: &SmaaPipelines,
smaa_textures: &SmaaTextures,
view_smaa_bind_groups: &SmaaBindGroups,
smaa_info_uniform_buffer: &SmaaInfoUniformBuffer,
view_smaa_uniform_offset: &SmaaInfoUniformOffset,
edge_detection_pipeline: &RenderPipeline,
source: &TextureView,
) {
// Create the edge detection bind group.
let postprocess_bind_group = render_context.render_device().create_bind_group(
None,
&smaa_pipelines.edge_detection.postprocess_bind_group_layout,
&BindGroupEntries::sequential((source, &**smaa_info_uniform_buffer)),
);
// Create the edge detection pass descriptor.
let pass_descriptor = RenderPassDescriptor {
label: Some("SMAA edge detection pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: &smaa_textures.edge_detection_color_texture.default_view,
resolve_target: None,
ops: default(),
})],
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &smaa_textures.edge_detection_stencil_texture.default_view,
depth_ops: None,
stencil_ops: Some(Operations {
load: LoadOp::Clear(0),
store: StoreOp::Store,
}),
}),
timestamp_writes: None,
occlusion_query_set: None,
};
// Run the actual render pass.
let mut render_pass = render_context
.command_encoder()
.begin_render_pass(&pass_descriptor);
render_pass.set_pipeline(edge_detection_pipeline);
render_pass.set_bind_group(0, &postprocess_bind_group, &[**view_smaa_uniform_offset]);
render_pass.set_bind_group(1, &view_smaa_bind_groups.edge_detection_bind_group, &[]);
render_pass.set_stencil_reference(1);
render_pass.draw(0..3, 0..1);
}
/// Performs blending weight calculation (phase 2).
///
/// This runs as part of the [`SmaaNode`]. It reads the edges texture and writes
/// to the blend weight texture, using the stencil buffer to avoid processing
/// pixels it doesn't need to examine.
fn perform_blending_weight_calculation(
render_context: &mut RenderContext,
smaa_pipelines: &SmaaPipelines,
smaa_textures: &SmaaTextures,
view_smaa_bind_groups: &SmaaBindGroups,
smaa_info_uniform_buffer: &SmaaInfoUniformBuffer,
view_smaa_uniform_offset: &SmaaInfoUniformOffset,
blending_weight_calculation_pipeline: &RenderPipeline,
source: &TextureView,
) {
// Create the blending weight calculation bind group.
let postprocess_bind_group = render_context.render_device().create_bind_group(
None,
&smaa_pipelines
.blending_weight_calculation
.postprocess_bind_group_layout,
&BindGroupEntries::sequential((source, &**smaa_info_uniform_buffer)),
);
// Create the blending weight calculation pass descriptor.
let pass_descriptor = RenderPassDescriptor {
label: Some("SMAA blending weight calculation pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: &smaa_textures.blend_texture.default_view,
resolve_target: None,
ops: default(),
})],
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &smaa_textures.edge_detection_stencil_texture.default_view,
depth_ops: None,
stencil_ops: Some(Operations {
load: LoadOp::Load,
store: StoreOp::Discard,
}),
}),
timestamp_writes: None,
occlusion_query_set: None,
};
// Run the actual render pass.
let mut render_pass = render_context
.command_encoder()
.begin_render_pass(&pass_descriptor);
render_pass.set_pipeline(blending_weight_calculation_pipeline);
render_pass.set_bind_group(0, &postprocess_bind_group, &[**view_smaa_uniform_offset]);
render_pass.set_bind_group(
1,
&view_smaa_bind_groups.blending_weight_calculation_bind_group,
&[],
);
render_pass.set_stencil_reference(1);
render_pass.draw(0..3, 0..1);
}
/// Performs blending weight calculation (phase 3).
///
/// This runs as part of the [`SmaaNode`]. It reads from the blend weight
/// texture. It's the only phase that writes to the postprocessing destination.
fn perform_neighborhood_blending(
render_context: &mut RenderContext,
smaa_pipelines: &SmaaPipelines,
view_smaa_bind_groups: &SmaaBindGroups,
smaa_info_uniform_buffer: &SmaaInfoUniformBuffer,
view_smaa_uniform_offset: &SmaaInfoUniformOffset,
neighborhood_blending_pipeline: &RenderPipeline,
source: &TextureView,
destination: &TextureView,
) {
let postprocess_bind_group = render_context.render_device().create_bind_group(
None,
&smaa_pipelines
.neighborhood_blending
.postprocess_bind_group_layout,
&BindGroupEntries::sequential((source, &**smaa_info_uniform_buffer)),
);
let pass_descriptor = RenderPassDescriptor {
label: Some("SMAA neighborhood blending pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: destination,
resolve_target: None,
ops: default(),
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
};
let mut neighborhood_blending_render_pass = render_context
.command_encoder()
.begin_render_pass(&pass_descriptor);
neighborhood_blending_render_pass.set_pipeline(neighborhood_blending_pipeline);
neighborhood_blending_render_pass.set_bind_group(
0,
&postprocess_bind_group,
&[**view_smaa_uniform_offset],
);
neighborhood_blending_render_pass.set_bind_group(
1,
&view_smaa_bind_groups.neighborhood_blending_bind_group,
&[],
);
neighborhood_blending_render_pass.draw(0..3, 0..1);
}
impl SmaaPreset {
/// Returns the `#define` in the shader corresponding to this quality
/// preset.
fn shader_def(&self) -> ShaderDefVal {
match *self {
SmaaPreset::Low => "SMAA_PRESET_LOW".into(),
SmaaPreset::Medium => "SMAA_PRESET_MEDIUM".into(),
SmaaPreset::High => "SMAA_PRESET_HIGH".into(),
SmaaPreset::Ultra => "SMAA_PRESET_ULTRA".into(),
}
}
}