pub mod backdrop;
pub mod clip;
pub mod mips;
pub mod store;
pub mod transform3d;
pub use store::*;
use std::ops::Range;
use bevy::asset::{AssetServer, Handle};
use bevy::camera::{Camera, Camera2d, Camera3d};
use bevy::ecs::system::SystemParamItem;
use bevy::ecs::system::lifetimeless::SRes;
use bevy::math::{FloatOrd, Mat4, UVec4};
use bevy::mesh::VertexBufferLayout;
use bevy::platform::collections::HashMap;
use bevy::prelude::*;
use bevy::render::Extract;
use bevy::render::camera::CameraMainPassTextureFormats;
use bevy::render::render_phase::{
DrawFunctions, PhaseItem, PhaseItemExtraIndex, RenderCommand, RenderCommandResult,
SetItemPipeline, TrackedRenderPass, ViewSortedRenderPhases,
};
use bevy::render::render_resource::binding_types::{sampler, texture_2d, uniform_buffer};
use bevy::render::render_resource::*;
use bevy::render::renderer::{RenderContext, RenderDevice, RenderQueue, ViewQuery};
use bevy::render::sync_world::{MainEntity, RenderEntity, TemporaryRenderEntity};
use bevy::render::view::{ExtractedView, RetainedViewEntity, ViewUniform};
use bevy::shader::Shader;
use bevy::shader::ShaderCacheError;
use bevy::ui::{ComputedNode, ComputedUiTargetCamera};
use bevy::ui_render::{SetUiViewBindGroup, TransparentUi, stack_z_offsets};
use super::{LayerCaptureRect, LayerGroupAlpha, LayerMembership, PromotedLayer};
use crate::filters::{MAX_FILTER_PARAM_VECS, ResolvedFilterChain};
const UI_CAMERA_FAR: f32 = 1000.0;
const UI_CAMERA_TRANSFORM_OFFSET: f32 = -0.1;
const UI_LAYER_CAPTURE_SUBVIEW: u32 = 2;
const MAX_LAYER_DEPTH: usize = 64;
const STUCK_GATE_HANG_FRAMES: u32 = 600;
pub struct ExtractedFilterPass {
pub shader: Handle<Shader>,
pub params: [Vec4; MAX_FILTER_PARAM_VECS],
}
pub struct ExtractedChain {
pub passes: Vec<ExtractedFilterPass>,
pub version: u32,
pub always_dirty: bool,
}
fn extract_chain(chain: Option<&ResolvedFilterChain>) -> Option<ExtractedChain> {
chain
.filter(|chain| !chain.passes.is_empty())
.map(|chain| ExtractedChain {
passes: chain
.passes
.iter()
.map(|pass| {
debug_assert!(
pass.params.len() <= MAX_FILTER_PARAM_VECS,
"filter pass packs {} vec4s, over MAX_FILTER_PARAM_VECS",
pass.params.len()
);
let mut params = [Vec4::ZERO; MAX_FILTER_PARAM_VECS];
for (slot, value) in params.iter_mut().zip(&pass.params) {
*slot = *value;
}
ExtractedFilterPass {
shader: pass.shader.clone(),
params,
}
})
.collect(),
version: chain.version,
always_dirty: chain.always_dirty,
})
}
pub struct ExtractedLayer {
pub main_entity: MainEntity,
pub view_entity: Entity,
pub retained: RetainedViewEntity,
pub quad_entity: Entity,
pub min: Vec2,
pub size: UVec2,
pub quad_clip: Option<bevy::math::Rect>,
pub alpha: f32,
pub target_format: TextureFormat,
pub needs_capture: bool,
pub chain: Option<ExtractedChain>,
pub backdrop_chain: Option<ExtractedChain>,
pub backdrop_quad_entity: Option<Entity>,
pub outset: u32,
pub corner_radius: [f32; 4],
pub transform3d: Option<Mat4>,
pub wants_mips: bool,
}
#[derive(Resource, Default)]
pub struct ExtractedUiLayers {
pub layers: Vec<ExtractedLayer>,
pub membership: HashMap<MainEntity, usize>,
pub enclosing: Vec<Option<usize>>,
pub stock_view: Option<RetainedViewEntity>,
pub camera_render_entity: Option<Entity>,
pub capture_order: Vec<usize>,
}
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub fn extract_ui_layers(
mut commands: Commands,
mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
mut extracted: ResMut<ExtractedUiLayers>,
layers: Extract<
Query<(
Entity,
&LayerCaptureRect,
&LayerGroupAlpha,
&ComputedUiTargetCamera,
Option<&ResolvedFilterChain>,
Option<&crate::filters::ResolvedBackdropChain>,
Option<&crate::layer::transform3d::LayerTransform3dMatrix>,
&PromotedLayer,
Option<&ComputedNode>,
)>,
>,
membership: Extract<Res<LayerMembership>>,
repaints: Extract<Res<super::LayerRepaintState>>,
clips: Extract<Res<crate::layer::clip::LayerClips>>,
cameras: Extract<Query<(RenderEntity, &Camera), Or<(With<Camera2d>, With<Camera3d>)>>>,
main_pass_formats: Res<CameraMainPassTextureFormats>,
store: Res<LayerTextureStore>,
) {
extracted.layers.clear();
extracted.membership.clear();
extracted.enclosing.clear();
extracted.capture_order.clear();
extracted.stock_view = None;
extracted.camera_render_entity = None;
if layers.is_empty() {
return;
}
let mut layer_index: HashMap<Entity, usize> = HashMap::default();
for (
root,
rect,
alpha,
target_camera,
filter_chain,
backdrop,
transform3d,
promoted,
computed,
) in layers.iter()
{
let Some(camera_main) = target_camera.get() else {
continue;
};
let Ok((camera_render, camera)) = cameras.get(camera_main) else {
continue;
};
if !camera.is_active {
continue;
}
let Some(target_format) = main_pass_formats.get(&camera_render).copied() else {
continue;
};
if extracted.stock_view.is_none() {
extracted.stock_view = Some(RetainedViewEntity::new(
camera_main.into(),
None,
1,
));
extracted.camera_render_entity = Some(camera_render);
}
let (min, size) = (rect.min, rect.size);
let projection = Mat4::orthographic_rh(
min.x,
min.x + size.x as f32,
min.y + size.y as f32,
min.y,
0.0,
UI_CAMERA_FAR,
);
let retained =
RetainedViewEntity::new(MainEntity::from(root), None, UI_LAYER_CAPTURE_SUBVIEW);
let view_entity = commands
.spawn((
ExtractedView {
retained_view_entity: retained,
clip_from_view: projection,
world_from_view: GlobalTransform::from_xyz(
0.0,
0.0,
UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET,
),
clip_from_world: None,
target_format,
viewport: UVec4::new(0, 0, size.x, size.y),
color_grading: Default::default(),
invert_culling: false,
},
TemporaryRenderEntity,
))
.id();
let quad_entity = commands.spawn(TemporaryRenderEntity).id();
phases.prepare_for_new_frame(retained);
let wants_mips = promoted.reasons.0 & crate::layer::PromotionReasons::TRANSFORM3D != 0;
let cached_ok = store
.slots
.get(&MainEntity::from(root))
.is_some_and(|slot| {
slot.content_valid
&& slot.size == size
&& slot.format == target_format
&& slot.mips.is_some() == wants_mips
});
let needs_capture = !cached_ok || repaints.dirty.contains(&root);
let chain = extract_chain(filter_chain);
let backdrop_chain = extract_chain(backdrop.map(|b| &b.0));
let backdrop_quad_entity =
(backdrop_chain.is_some()).then(|| commands.spawn(TemporaryRenderEntity).id());
layer_index.insert(root, extracted.layers.len());
extracted.layers.push(ExtractedLayer {
main_entity: MainEntity::from(root),
view_entity,
retained,
quad_entity,
min,
size,
quad_clip: clips.quads.get(&root).copied().flatten(),
alpha: alpha.0.clamp(0.0, 1.0),
target_format,
needs_capture,
chain,
backdrop_chain,
backdrop_quad_entity,
outset: rect.outset,
corner_radius: computed.map_or([0.0; 4], |c| c.border_radius.into()),
transform3d: transform3d.filter(|m| !m.identity).map(|m| m.model),
wants_mips,
});
}
let live: Vec<RetainedViewEntity> = extracted.layers.iter().map(|l| l.retained).collect();
phases.retain(|retained, _| {
retained.subview_index != UI_LAYER_CAPTURE_SUBVIEW || live.contains(retained)
});
for (node, layer_root) in membership.node_to_layer.iter() {
if let Some(&idx) = layer_index.get(layer_root) {
extracted.membership.insert(MainEntity::from(*node), idx);
}
}
extracted.enclosing = extracted
.layers
.iter()
.map(|layer| {
membership
.enclosing
.get(&layer.main_entity.id())
.copied()
.flatten()
.and_then(|e| layer_index.get(&e).copied())
})
.collect();
let extracted = &mut *extracted;
for i in 0..extracted.layers.len() {
if extracted.layers[i].needs_capture {
let layers = &mut extracted.layers;
walk_enclosing(i, &extracted.enclosing, |outer| {
if layers[outer].needs_capture {
return false; }
layers[outer].needs_capture = true;
true
});
}
}
for i in 0..extracted.layers.len() {
if extracted.layers[i].backdrop_chain.is_some() {
let layers = &mut extracted.layers;
walk_enclosing(i, &extracted.enclosing, |outer| {
if layers[outer].needs_capture {
return false; }
layers[outer].needs_capture = true;
true
});
}
}
let enclosing = extracted.enclosing.clone();
let depth_of = |mut idx: usize| {
let mut depth = 0usize;
while let Some(outer) = enclosing[idx] {
depth += 1;
idx = outer;
if depth > MAX_LAYER_DEPTH {
break; }
}
depth
};
let mut order: Vec<usize> = (0..extracted.layers.len()).collect();
order.sort_by_key(|&i| std::cmp::Reverse(depth_of(i)));
extracted.capture_order = order;
}
pub fn redistribute_ui_layers(
extracted: Res<ExtractedUiLayers>,
mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
draw_functions: Res<DrawFunctions<TransparentUi>>,
composite_pipeline: Option<Res<LayerCompositePipeline>>,
mut specialized: ResMut<SpecializedRenderPipelines<LayerCompositePipeline>>,
pipeline_cache: Res<PipelineCache>,
) {
if extracted.layers.is_empty() {
return;
}
let Some(stock_view) = extracted.stock_view else {
return;
};
let Some(composite_pipeline) = composite_pipeline else {
return;
};
let mut stolen: Vec<(usize, (Entity, MainEntity), TransparentUi)> = Vec::new();
let mut quad_sort_keys: Vec<Option<FloatOrd>> = vec![None; extracted.layers.len()];
{
let Some(stock_phase) = phases.get_mut(&stock_view) else {
return;
};
let taken = std::mem::take(&mut stock_phase.items);
for (key, item) in taken {
let Some(&idx) = extracted.membership.get(&item.main_entity()) else {
stock_phase.items.insert(key, item);
continue;
};
let best = &mut quad_sort_keys[idx];
if best.is_none() || item.sort_key < best.unwrap() {
*best = Some(item.sort_key);
}
stolen.push((idx, key, item));
}
}
for (idx, _key, item) in stolen {
if !extracted.layers[idx].needs_capture {
continue;
}
if let Some(phase) = phases.get_mut(&extracted.layers[idx].retained) {
phase.add_transient(item);
}
}
if std::env::var("BEVY_REACT_LAYER_SPIKE_MODE").as_deref() == Ok("steal") {
return;
}
let draw_function = draw_functions.read().id::<DrawLayerComposite>();
for (idx, layer) in extracted.layers.iter().enumerate() {
let Some(sort_key) = quad_sort_keys[idx] else {
continue;
};
let pipeline = specialized.specialize(
&pipeline_cache,
&composite_pipeline,
LayerCompositePipelineKey {
target_format: layer.target_format,
},
);
let target = match extracted.enclosing[idx] {
Some(outer) => {
if !extracted.layers[outer].needs_capture {
debug_assert!(
!layer.needs_capture,
"inner layer re-captures but its enclosing layer is cached"
);
continue;
}
extracted.layers[outer].retained
}
None => stock_view,
};
if let Some(phase) = phases.get_mut(&target) {
if let Some(backdrop_quad_entity) = layer.backdrop_quad_entity {
phase.add_transient(TransparentUi {
sort_key: FloatOrd(sort_key.0 - backdrop::BACKDROP_UNDERLAY_EPSILON),
entity: (backdrop_quad_entity, layer.main_entity),
pipeline,
draw_function,
batch_range: 0..0,
extra_index: PhaseItemExtraIndex::None,
index: idx,
indexed: false,
});
}
phase.add_transient(TransparentUi {
sort_key: FloatOrd(sort_key.0 + stack_z_offsets::BACKGROUND_COLOR),
entity: (layer.quad_entity, layer.main_entity),
pipeline,
draw_function,
batch_range: 0..0,
extra_index: PhaseItemExtraIndex::None,
index: idx,
indexed: false,
});
}
}
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct LayerCompositeVertex {
pub position: [f32; 3],
pub uv: [f32; 2],
pub alpha: f32,
}
#[derive(Resource)]
pub struct LayerCompositeMeta {
pub vertices: RawBufferVec<LayerCompositeVertex>,
pub atlas_bind_groups: Vec<BindGroup>,
}
impl Default for LayerCompositeMeta {
fn default() -> Self {
Self {
vertices: RawBufferVec::new(BufferUsages::VERTEX),
atlas_bind_groups: Vec::new(),
}
}
}
#[derive(Component)]
pub struct LayerCompositeBatch {
pub range: Range<u32>,
pub atlas: usize,
pub uniform_offset: u32,
}
const EDGE_AA_INFLATE_PX: f32 = 1.0;
fn inflated_transform_quad(min: Vec2, size: UVec2, inset: f32) -> clip::ClippedQuad {
let size = size.as_vec2().max(Vec2::ONE);
let uv_inset = inset / size;
clip::ClippedQuad {
pos_min: min - inset,
pos_max: min + size + inset,
uv_min: -uv_inset,
uv_max: Vec2::ONE + uv_inset,
}
}
#[allow(clippy::too_many_arguments)]
pub fn prepare_layer_composites(
mut commands: Commands,
extracted: Res<ExtractedUiLayers>,
mut store: ResMut<LayerTextureStore>,
pipeline: Option<Res<LayerCompositePipeline>>,
pipeline_cache: Res<PipelineCache>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut meta: ResMut<LayerCompositeMeta>,
mut uniforms_meta: ResMut<transform3d::CompositeUniformsMeta>,
filter_meta: Res<LayerFilterMeta>,
backdrop_meta: Res<backdrop::BackdropMeta>,
mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
) {
meta.vertices.clear();
meta.atlas_bind_groups.clear();
uniforms_meta.uniforms.clear();
uniforms_meta.bind_group = None;
let Some(pipeline) = pipeline else {
return;
};
if extracted.layers.is_empty() {
return;
}
let mut ranges: Vec<Option<Range<u32>>> = vec![None; extracted.layers.len()];
let mut gated: Vec<usize> = Vec::new();
for (idx, layer) in extracted.layers.iter().enumerate() {
let Some(slot) = store.slots.get_mut(&layer.main_entity) else {
continue;
};
let bind_group = if layer.chain.is_some() {
let Some(filter) = slot.filter.as_mut() else {
gated.push(idx);
continue;
};
if !filter.output_valid {
filter.gated_frames = filter.gated_frames.saturating_add(1);
if !filter.gate_warned {
let compile_error = filter_meta
.runs
.get(idx)
.and_then(|run| run.as_ref())
.and_then(|run| {
run.passes.iter().find_map(|pass| {
match pipeline_cache.get_render_pipeline_state(pass.pipeline) {
CachedPipelineState::Err(
e @ (ShaderCacheError::ProcessShaderError(_)
| ShaderCacheError::CreateShaderModule(_)),
) => Some(e.to_string()),
_ => None,
}
})
});
if let Some(err) = compile_error {
warn!(
"UI layer {:?}: a filter pass shader failed to compile — the \
layer's subtree is invisible (the composite gate never falls \
back to unfiltered content) and its filter run restages every \
frame. Error: {err}",
layer.main_entity,
);
filter.gate_warned = true;
} else if filter.gated_frames == STUCK_GATE_HANG_FRAMES {
warn!(
"UI layer {:?}: composite quad withheld for {} consecutive \
frames and its filter pipeline is still not ready (no compile \
error reported — a hung/queued compile?). Until it resolves, \
the layer's subtree is invisible and its filter run restages \
every frame.",
layer.main_entity, STUCK_GATE_HANG_FRAMES,
);
filter.gate_warned = true;
}
}
gated.push(idx);
continue;
}
let output = filter.output_index;
if layer.transform3d.is_some() && filter.mips_valid {
let Some(chain) = &filter.mips[output] else {
unreachable!("mips_valid implies a staged chain");
};
if !matches!(&filter.composite_bind_group_mips, Some((built, _)) if *built == output)
{
filter.composite_bind_group_mips = Some((
output,
render_device.create_bind_group(
"ui_layer_composite_filtered_mips",
&pipeline_cache.get_bind_group_layout(&pipeline.atlas_layout),
&BindGroupEntries::sequential((
&chain.full_view,
&pipeline.sampler_mips,
)),
),
));
}
let (_, bind_group) = filter.composite_bind_group_mips.as_ref().expect("just set");
bind_group.clone()
} else {
if !matches!(&filter.composite_bind_group, Some((built, _)) if *built == output) {
filter.composite_bind_group = Some((
output,
render_device.create_bind_group(
"ui_layer_composite_filtered",
&pipeline_cache.get_bind_group_layout(&pipeline.atlas_layout),
&BindGroupEntries::sequential((
&filter.textures[output].default_view,
&pipeline.sampler,
)),
),
));
}
let (_, bind_group) = filter.composite_bind_group.as_ref().expect("just set");
bind_group.clone()
}
} else if layer.transform3d.is_some()
&& slot.mips_valid
&& let Some(chain) = &slot.mips
{
if slot.bind_group_mips.is_none() {
slot.bind_group_mips = Some(render_device.create_bind_group(
"ui_layer_composite_atlas_mips",
&pipeline_cache.get_bind_group_layout(&pipeline.atlas_layout),
&BindGroupEntries::sequential((&chain.full_view, &pipeline.sampler_mips)),
));
}
slot.bind_group_mips.clone().expect("just set")
} else {
if slot.bind_group.is_none() {
slot.bind_group = Some(render_device.create_bind_group(
"ui_layer_composite_atlas",
&pipeline_cache.get_bind_group_layout(&pipeline.atlas_layout),
&BindGroupEntries::sequential((&slot.texture.default_view, &pipeline.sampler)),
));
}
slot.bind_group.clone().expect("just set")
};
let start = meta.vertices.len() as u32;
let (q, model, clip_rect, feather) = match layer.transform3d {
Some(model) => (
inflated_transform_quad(layer.min, layer.size, EDGE_AA_INFLATE_PX),
model,
layer.quad_clip,
EDGE_AA_INFLATE_PX,
),
None => {
let Some(q) = clip::clip_quad(layer.min, layer.size, layer.quad_clip) else {
continue;
};
(q, Mat4::IDENTITY, None, 0.0)
}
};
let (min, max) = (q.pos_min, q.pos_max);
let (uv_min, uv_max) = (q.uv_min, q.uv_max);
let corners = [
([min.x, min.y, 0.0], [uv_min.x, uv_min.y]),
([max.x, min.y, 0.0], [uv_max.x, uv_min.y]),
([max.x, max.y, 0.0], [uv_max.x, uv_max.y]),
([min.x, min.y, 0.0], [uv_min.x, uv_min.y]),
([max.x, max.y, 0.0], [uv_max.x, uv_max.y]),
([min.x, max.y, 0.0], [uv_min.x, uv_max.y]),
];
for (position, uv) in corners {
meta.vertices.push(LayerCompositeVertex {
position,
uv,
alpha: layer.alpha,
});
}
ranges[idx] = Some(start..start + 6);
let atlas_index = meta.atlas_bind_groups.len();
meta.atlas_bind_groups.push(bind_group);
let (open_min, open_max) = transform3d::open_clip();
let uniform_offset = uniforms_meta
.uniforms
.push(&transform3d::CompositeUniforms {
model,
clip_min: clip_rect.map_or(open_min, |r| r.min),
clip_max: clip_rect.map_or(open_max, |r| r.max),
edge_feather: feather,
pad_a: 0.0,
pad_b: Vec2::ZERO,
radius: Vec4::ZERO,
box_center: Vec2::ZERO,
box_size: Vec2::ZERO,
});
commands
.entity(layer.quad_entity)
.insert(LayerCompositeBatch {
range: ranges[idx].clone().unwrap(),
atlas: atlas_index,
uniform_offset,
});
}
let mut backdrop_ranges: Vec<Option<Range<u32>>> = vec![None; extracted.layers.len()];
for (idx, layer) in extracted.layers.iter().enumerate() {
let Some(backdrop_quad_entity) = layer.backdrop_quad_entity else {
continue;
};
let Some(slot) = store.slots.get_mut(&layer.main_entity) else {
continue;
};
let Some(backdrop_slot) = slot.backdrop.as_mut() else {
continue;
};
let Some(bind_group) = backdrop::backdrop_gate(
idx,
layer.main_entity,
backdrop_slot,
&backdrop_meta,
&pipeline_cache,
&render_device,
&pipeline.atlas_layout,
&pipeline.sampler,
) else {
continue;
};
let Some(q) = backdrop::backdrop_quad(layer.min, layer.size, layer.outset, layer.quad_clip)
else {
continue;
};
let start = meta.vertices.len() as u32;
let (min, max) = (q.pos_min, q.pos_max);
let (uv_min, uv_max) = (q.uv_min, q.uv_max);
let corners = [
([min.x, min.y, 0.0], [uv_min.x, uv_min.y]),
([max.x, min.y, 0.0], [uv_max.x, uv_min.y]),
([max.x, max.y, 0.0], [uv_max.x, uv_max.y]),
([min.x, min.y, 0.0], [uv_min.x, uv_min.y]),
([max.x, max.y, 0.0], [uv_max.x, uv_max.y]),
([min.x, max.y, 0.0], [uv_min.x, uv_max.y]),
];
for (position, uv) in corners {
meta.vertices.push(LayerCompositeVertex {
position,
uv,
alpha: layer.alpha,
});
}
backdrop_ranges[idx] = Some(start..start + 6);
let atlas_index = meta.atlas_bind_groups.len();
meta.atlas_bind_groups.push(bind_group);
let box_min = layer.min + Vec2::splat(layer.outset as f32);
let box_max = layer.min + layer.size.as_vec2() - Vec2::splat(layer.outset as f32);
let (open_min, open_max) = transform3d::open_clip();
let uniform_offset = uniforms_meta
.uniforms
.push(&transform3d::CompositeUniforms {
model: Mat4::IDENTITY,
clip_min: open_min,
clip_max: open_max,
edge_feather: 0.0,
pad_a: 0.0,
pad_b: Vec2::ZERO,
radius: Vec4::from(layer.corner_radius),
box_center: (box_min + box_max) * 0.5,
box_size: box_max - box_min,
});
commands
.entity(backdrop_quad_entity)
.insert(LayerCompositeBatch {
range: backdrop_ranges[idx].clone().unwrap(),
atlas: atlas_index,
uniform_offset,
});
}
for idx in gated {
walk_enclosing(idx, &extracted.enclosing, |outer| {
if let Some(slot) = store.slots.get_mut(&extracted.layers[outer].main_entity) {
slot.content_valid = false;
}
true
});
}
meta.vertices.write_buffer(&render_device, &render_queue);
uniforms_meta
.uniforms
.write_buffer(&render_device, &render_queue);
uniforms_meta.bind_group = uniforms_meta.uniforms.binding().map(|binding| {
render_device.create_bind_group(
"ui_layer_composite_uniforms",
&pipeline_cache.get_bind_group_layout(&pipeline.uniform_layout),
&BindGroupEntries::single(binding),
)
});
for phase in phases.values_mut() {
for item in phase.items.values_mut() {
let drawable = extracted
.layers
.iter()
.position(|l| l.quad_entity == item.entity())
.is_some_and(|idx| ranges[idx].is_some())
|| extracted
.layers
.iter()
.position(|l| l.backdrop_quad_entity == Some(item.entity()))
.is_some_and(|idx| backdrop_ranges[idx].is_some());
if drawable {
item.batch_range = 0..1;
}
}
}
}
#[derive(Resource)]
pub struct LayerCompositePipeline {
pub view_layout: BindGroupLayoutDescriptor,
pub atlas_layout: BindGroupLayoutDescriptor,
pub uniform_layout: BindGroupLayoutDescriptor,
pub sampler: Sampler,
pub sampler_mips: Sampler,
pub shader: Handle<Shader>,
}
pub fn init_layer_composite_pipeline(
mut commands: Commands,
render_device: Res<RenderDevice>,
asset_server: Res<AssetServer>,
) {
let view_layout = BindGroupLayoutDescriptor::new(
"ui_layer_composite_view_layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX_FRAGMENT,
uniform_buffer::<ViewUniform>(true),
),
);
let atlas_layout = BindGroupLayoutDescriptor::new(
"ui_layer_composite_atlas_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
),
),
);
let uniform_layout = BindGroupLayoutDescriptor::new(
"ui_layer_composite_uniform_layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX_FRAGMENT,
uniform_buffer::<transform3d::CompositeUniforms>(true),
),
);
commands.insert_resource(LayerCompositePipeline {
view_layout,
atlas_layout,
uniform_layout,
sampler: render_device.create_sampler(&SamplerDescriptor {
label: Some("ui_layer_composite_sampler"),
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
..Default::default()
}),
sampler_mips: render_device.create_sampler(&SamplerDescriptor {
label: Some("ui_layer_composite_sampler_mips"),
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
mipmap_filter: bevy::render::render_resource::MipmapFilterMode::Linear,
anisotropy_clamp: 8,
..Default::default()
}),
shader: bevy::asset::load_embedded_asset!(asset_server.as_ref(), "composite.wgsl"),
});
}
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub struct LayerCompositePipelineKey {
pub target_format: TextureFormat,
}
impl SpecializedRenderPipeline for LayerCompositePipeline {
type Key = LayerCompositePipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let vertex_layout = VertexBufferLayout::from_vertex_formats(
VertexStepMode::Vertex,
vec![
VertexFormat::Float32x3,
VertexFormat::Float32x2,
VertexFormat::Float32,
],
);
RenderPipelineDescriptor {
vertex: VertexState {
shader: self.shader.clone(),
buffers: vec![vertex_layout],
..Default::default()
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
targets: vec![Some(ColorTargetState {
format: key.target_format,
blend: Some(BlendState {
color: BlendComponent {
src_factor: BlendFactor::One,
dst_factor: BlendFactor::OneMinusSrcAlpha,
operation: BlendOperation::Add,
},
alpha: BlendComponent {
src_factor: BlendFactor::One,
dst_factor: BlendFactor::OneMinusSrcAlpha,
operation: BlendOperation::Add,
},
}),
write_mask: ColorWrites::ALL,
})],
..Default::default()
}),
layout: vec![
self.view_layout.clone(),
self.atlas_layout.clone(),
self.uniform_layout.clone(),
],
label: Some("ui_layer_composite_pipeline".into()),
..Default::default()
}
}
}
pub struct SetLayerAtlasBindGroup<const I: usize>;
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetLayerAtlasBindGroup<I> {
type Param = SRes<LayerCompositeMeta>;
type ViewQuery = ();
type ItemQuery = bevy::ecs::system::lifetimeless::Read<LayerCompositeBatch>;
#[inline]
fn render<'w>(
_item: &P,
_view: (),
batch: Option<&'w LayerCompositeBatch>,
meta: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(batch) = batch else {
return RenderCommandResult::Skip;
};
let Some(bind_group) = meta.into_inner().atlas_bind_groups.get(batch.atlas) else {
return RenderCommandResult::Failure("layer atlas bind group missing");
};
pass.set_bind_group(I, bind_group, &[]);
RenderCommandResult::Success
}
}
pub struct DrawLayerQuad;
impl<P: PhaseItem> RenderCommand<P> for DrawLayerQuad {
type Param = SRes<LayerCompositeMeta>;
type ViewQuery = ();
type ItemQuery = bevy::ecs::system::lifetimeless::Read<LayerCompositeBatch>;
#[inline]
fn render<'w>(
_item: &P,
_view: (),
batch: Option<&'w LayerCompositeBatch>,
meta: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(batch) = batch else {
return RenderCommandResult::Skip;
};
let Some(vertices) = meta.into_inner().vertices.buffer() else {
return RenderCommandResult::Failure("layer composite vertices missing");
};
pass.set_vertex_buffer(0, vertices.slice(..));
pass.draw(batch.range.clone(), 0..1);
RenderCommandResult::Success
}
}
pub type DrawLayerComposite = (
SetItemPipeline,
SetUiViewBindGroup<0>,
SetLayerAtlasBindGroup<1>,
transform3d::SetCompositeUniforms<2>,
DrawLayerQuad,
);
#[derive(Clone, Copy, ShaderType)]
pub struct FilterUniforms {
pub time: f32,
pub pad_a: f32,
pub resolution: Vec2,
pub texel_size: Vec2,
pub pad_b: Vec2,
pub params: [Vec4; MAX_FILTER_PARAM_VECS],
}
#[derive(Resource)]
pub struct LayerFilterPipeline {
pub layout: BindGroupLayoutDescriptor,
pub sampler: Sampler,
pub prelude: Handle<Shader>,
}
pub fn init_layer_filter_pipeline(
mut commands: Commands,
render_device: Res<RenderDevice>,
asset_server: Res<AssetServer>,
) {
let layout = BindGroupLayoutDescriptor::new(
"ui_layer_filter_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
uniform_buffer::<FilterUniforms>(true),
texture_2d(TextureSampleType::Float { filterable: true }),
),
),
);
commands.insert_resource(LayerFilterPipeline {
layout,
sampler: render_device.create_sampler(&SamplerDescriptor {
label: Some("ui_layer_filter_sampler"),
address_mode_u: AddressMode::ClampToEdge,
address_mode_v: AddressMode::ClampToEdge,
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
..Default::default()
}),
prelude: bevy::asset::load_embedded_asset!(asset_server.as_ref(), "filter_prelude.wgsl"),
});
}
#[derive(Clone, Hash, PartialEq, Eq)]
pub struct LayerFilterPipelineKey {
pub shader: Handle<Shader>,
pub target_format: TextureFormat,
}
impl SpecializedRenderPipeline for LayerFilterPipeline {
type Key = LayerFilterPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
vertex: VertexState {
shader: self.prelude.clone(),
entry_point: Some("vertex".into()),
..Default::default()
},
fragment: Some(FragmentState {
shader: key.shader,
entry_point: Some("fragment".into()),
targets: vec![Some(ColorTargetState {
format: key.target_format,
blend: None,
write_mask: ColorWrites::ALL,
})],
..Default::default()
}),
layout: vec![self.layout.clone()],
label: Some("ui_layer_filter_pipeline".into()),
..Default::default()
}
}
}
pub const fn needs_filter_run(
needs_capture: bool,
chain_version: u32,
stored_version: u32,
always_dirty: bool,
output_valid: bool,
) -> bool {
needs_capture || chain_version != stored_version || always_dirty || !output_valid
}
fn walk_enclosing(start: usize, enclosing: &[Option<usize>], mut visit: impl FnMut(usize) -> bool) {
let mut cur = start;
for _ in 0..MAX_LAYER_DEPTH {
let Some(outer) = enclosing[cur] else {
break;
};
if !visit(outer) {
break;
}
cur = outer;
}
}
pub const fn filter_source_index(pass: usize) -> Option<usize> {
if pass == 0 {
None
} else {
Some((pass - 1) % 2)
}
}
pub const fn filter_target_index(pass: usize) -> usize {
pass % 2
}
pub const fn filter_output_index(len: usize) -> usize {
(len.saturating_sub(1)) % 2
}
pub struct LayerFilterPass {
pub pipeline: CachedRenderPipelineId,
pub bind_group: BindGroup,
pub uniform_offset: u32,
pub target: TextureView,
}
pub struct LayerFilterRun {
pub passes: Vec<LayerFilterPass>,
}
#[derive(Resource)]
pub struct LayerFilterMeta {
pub uniforms: DynamicUniformBuffer<FilterUniforms>,
pub runs: Vec<Option<LayerFilterRun>>,
}
impl Default for LayerFilterMeta {
fn default() -> Self {
let mut uniforms = DynamicUniformBuffer::default();
uniforms.set_label(Some("ui_layer_filter_uniforms"));
Self {
uniforms,
runs: Vec::new(),
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn prepare_layer_filters(
extracted: Res<ExtractedUiLayers>,
mut store: ResMut<LayerTextureStore>,
pipeline: Option<Res<LayerFilterPipeline>>,
mut specialized: ResMut<SpecializedRenderPipelines<LayerFilterPipeline>>,
pipeline_cache: Res<PipelineCache>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
time: Res<Time>,
mut meta: ResMut<LayerFilterMeta>,
) {
let LayerFilterMeta { uniforms, runs } = &mut *meta;
uniforms.clear();
runs.clear();
runs.resize_with(extracted.layers.len(), || None);
let Some(pipeline) = pipeline else {
return;
};
struct StagedPass {
pipeline: CachedRenderPipelineId,
uniform_offset: u32,
}
let mut staged: Vec<(usize, Vec<StagedPass>)> = Vec::new();
for (idx, layer) in extracted.layers.iter().enumerate() {
let Some(chain) = &layer.chain else {
continue;
};
let Some(slot) = store.slots.get_mut(&layer.main_entity) else {
continue;
};
let size = slot.size;
let Some(filter) = slot.filter.as_mut() else {
continue;
};
if !needs_filter_run(
layer.needs_capture,
chain.version,
filter.params_version,
chain.always_dirty,
filter.output_valid,
) {
continue;
}
if filter.params_version != chain.version {
filter.gated_frames = 0;
filter.gate_warned = false;
}
filter.params_version = chain.version;
filter.output_valid = false;
filter.mips_valid = false;
filter.output_index = filter_output_index(chain.passes.len());
let resolution = size.as_vec2();
let texel_size = Vec2::ONE / resolution;
let mut passes = Vec::with_capacity(chain.passes.len());
for pass in &chain.passes {
let id = specialized.specialize(
&pipeline_cache,
&pipeline,
LayerFilterPipelineKey {
shader: pass.shader.clone(),
target_format: layer.target_format,
},
);
let uniform_offset = uniforms.push(&FilterUniforms {
time: time.elapsed_secs(),
pad_a: 0.0,
resolution,
texel_size,
pad_b: Vec2::ZERO,
params: pass.params,
});
passes.push(StagedPass {
pipeline: id,
uniform_offset,
});
}
staged.push((idx, passes));
}
if staged.is_empty() {
return;
}
uniforms.write_buffer(&render_device, &render_queue);
let Some(uniform_binding) = uniforms.binding() else {
return;
};
let layout = pipeline_cache.get_bind_group_layout(&pipeline.layout);
for (idx, staged_passes) in staged {
let layer = &extracted.layers[idx];
let Some(slot) = store.slots.get(&layer.main_entity) else {
continue;
};
let Some(filter) = slot.filter.as_ref() else {
continue;
};
let passes = staged_passes
.into_iter()
.enumerate()
.map(|(i, pass)| {
let source = match filter_source_index(i) {
None => &slot.texture.default_view,
Some(ping) => &filter.textures[ping].default_view,
};
let bind_group = render_device.create_bind_group(
"ui_layer_filter",
&layout,
&BindGroupEntries::sequential((
source,
&pipeline.sampler,
uniform_binding.clone(),
&slot.texture.default_view,
)),
);
LayerFilterPass {
pipeline: pass.pipeline,
bind_group,
uniform_offset: pass.uniform_offset,
target: filter.textures[filter_target_index(i)].default_view.clone(),
}
})
.collect();
runs[idx] = Some(LayerFilterRun { passes });
}
for (idx, run) in runs.iter().enumerate() {
let Some(run) = run else {
continue;
};
let Some(slot) = store.slots.get_mut(&extracted.layers[idx].main_entity) else {
continue;
};
let ready = run
.passes
.iter()
.all(|pass| pipeline_cache.get_render_pipeline(pass.pipeline).is_some());
if ready
&& slot.content_valid
&& let Some(filter) = slot.filter.as_mut()
{
filter.output_valid = true;
filter.gated_frames = 0;
filter.gate_warned = false;
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn ui_layer_capture_pass(
world: &World,
view: ViewQuery<Entity>,
extracted: Res<ExtractedUiLayers>,
atlases: Res<LayerAtlases>,
phases: Res<ViewSortedRenderPhases<TransparentUi>>,
filter_meta: Res<LayerFilterMeta>,
mip_meta: Res<mips::LayerMipMeta>,
backdrop_meta: Res<backdrop::BackdropMeta>,
blit_pipeline: Option<Res<backdrop::BackdropBlitPipeline>>,
pipeline_cache: Res<PipelineCache>,
mut ctx: RenderContext,
) {
if extracted.camera_render_entity != Some(view.into_inner()) {
return;
}
let main_texture = backdrop::camera_main_texture(world, extracted.camera_render_entity);
for &idx in &extracted.capture_order {
let layer = &extracted.layers[idx];
if let Some(main_texture) = &main_texture {
backdrop::run_backdrop_passes(
idx,
&backdrop_meta,
blit_pipeline.as_deref(),
main_texture,
&pipeline_cache,
&mut ctx,
);
}
if layer.needs_capture
&& let Some(texture) = atlases.textures.get(idx)
&& let Some(phase) = phases.get(&layer.retained)
&& !phase.items.is_empty()
{
let mut pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("ui_layer_capture"),
color_attachments: &[Some(RenderPassColorAttachment {
view: &texture.default_view,
depth_slice: None,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(LinearRgba::NONE.into()),
store: StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
if let Err(err) = phase.render(&mut pass, world, layer.view_entity) {
bevy::log::error!("layer capture pass failed: {err:?}");
}
}
if let Some(run) = filter_meta.runs.get(idx).and_then(Option::as_ref) {
let pipelines: Option<Vec<_>> = run
.passes
.iter()
.map(|pass| pipeline_cache.get_render_pipeline(pass.pipeline))
.collect();
if let Some(pipelines) = pipelines {
for (pass_data, pipeline) in run.passes.iter().zip(pipelines) {
let mut pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("ui_layer_filter"),
color_attachments: &[Some(RenderPassColorAttachment {
view: &pass_data.target,
depth_slice: None,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(LinearRgba::NONE.into()),
store: StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_render_pipeline(pipeline);
pass.set_bind_group(0, &pass_data.bind_group, &[pass_data.uniform_offset]);
pass.draw(0..3, 0..1);
}
}
}
if let Some(run) = mip_meta.runs.get(idx).and_then(Option::as_ref)
&& let Some(pipeline) = pipeline_cache.get_render_pipeline(run.pipeline)
{
for level in &run.levels {
let mut pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("ui_layer_mip_blit"),
color_attachments: &[Some(RenderPassColorAttachment {
view: &level.target,
depth_slice: None,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(LinearRgba::NONE.into()),
store: StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_render_pipeline(pipeline);
pass.set_bind_group(0, &level.bind_group, &[]);
pass.draw(0..3, 0..1);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::render::render_resource::encase::UniformBuffer;
fn f32_at(bytes: &[u8], offset: usize) -> f32 {
f32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap())
}
#[test]
fn filter_uniforms_match_the_documented_wgsl_layout() {
assert_eq!(FilterUniforms::min_size().get(), 160);
let mut params = [Vec4::ZERO; MAX_FILTER_PARAM_VECS];
params[0] = Vec4::new(1.0, 2.0, 3.0, 4.0);
params[7] = Vec4::new(5.0, 6.0, 7.0, 8.0);
let value = FilterUniforms {
time: 1.5,
pad_a: 0.0,
resolution: Vec2::new(320.0, 240.0),
texel_size: Vec2::new(0.5, 0.25),
pad_b: Vec2::ZERO,
params,
};
let mut buffer = UniformBuffer::new(Vec::<u8>::new());
buffer.write(&value).expect("uniform write");
let bytes = buffer.into_inner();
assert_eq!(bytes.len(), 160);
assert_eq!(f32_at(&bytes, 0), 1.5); assert_eq!(f32_at(&bytes, 8), 320.0); assert_eq!(f32_at(&bytes, 12), 240.0); assert_eq!(f32_at(&bytes, 16), 0.5); assert_eq!(f32_at(&bytes, 20), 0.25); assert_eq!(f32_at(&bytes, 32), 1.0); assert_eq!(f32_at(&bytes, 44), 4.0); assert_eq!(f32_at(&bytes, 32 + 7 * 16), 5.0); assert_eq!(f32_at(&bytes, 32 + 7 * 16 + 12), 8.0); }
#[test]
fn needs_filter_run_decision_table() {
let cases = [
(false, 3, 3, false, true, false),
(true, 3, 3, false, true, true),
(false, 4, 3, false, true, true),
(false, 1, 3, false, true, true),
(false, 3, 3, true, true, true),
(false, 3, 3, false, false, true),
(false, 1, 0, false, false, true),
];
for (capture, chain_v, stored_v, dirty, valid, expected) in cases {
assert_eq!(
needs_filter_run(capture, chain_v, stored_v, dirty, valid),
expected,
"needs_capture={capture} chain={chain_v} stored={stored_v} \
always_dirty={dirty} output_valid={valid}"
);
}
}
#[test]
fn filter_ping_pong_indices() {
assert_eq!(filter_source_index(0), None);
assert_eq!(filter_target_index(0), 0);
assert_eq!(filter_source_index(1), Some(0));
assert_eq!(filter_target_index(1), 1);
assert_eq!(filter_source_index(2), Some(1));
assert_eq!(filter_target_index(2), 0);
assert_eq!(filter_source_index(3), Some(0));
assert_eq!(filter_target_index(3), 1);
for pass in 1..8 {
assert_eq!(
filter_source_index(pass),
Some(filter_target_index(pass - 1)),
"pass {pass} must read pass {}'s target",
pass - 1
);
assert_ne!(filter_source_index(pass), Some(filter_target_index(pass)));
}
for len in 1..8 {
assert_eq!(filter_output_index(len), filter_target_index(len - 1));
}
assert_eq!(filter_output_index(1), 0);
assert_eq!(filter_output_index(2), 1);
assert_eq!(filter_output_index(3), 0);
}
#[test]
fn walk_enclosing_table() {
let visited = |start: usize, enclosing: &[Option<usize>]| {
let mut seen = Vec::new();
walk_enclosing(start, enclosing, |outer| {
seen.push(outer);
true
});
seen
};
let chain = [None, Some(0), Some(1)];
assert_eq!(visited(2, &chain), vec![1, 0]);
assert_eq!(visited(1, &chain), vec![0]);
assert_eq!(visited(0, &chain), Vec::<usize>::new());
let long: Vec<Option<usize>> = (0..MAX_LAYER_DEPTH + 10)
.map(|i| i.checked_sub(1))
.collect();
let seen = visited(long.len() - 1, &long);
assert_eq!(seen.len(), MAX_LAYER_DEPTH);
assert_eq!(seen[0], long.len() - 2);
assert_eq!(seen[MAX_LAYER_DEPTH - 1], long.len() - 1 - MAX_LAYER_DEPTH);
let cycle = [Some(0)];
assert_eq!(visited(0, &cycle), vec![0; MAX_LAYER_DEPTH]);
let cycle2 = [Some(1), Some(0)];
assert_eq!(visited(0, &cycle2).len(), MAX_LAYER_DEPTH);
let mut seen = Vec::new();
walk_enclosing(2, &chain, |outer| {
seen.push(outer);
false
});
assert_eq!(seen, vec![1]);
}
#[test]
fn inflated_transform_quad_extends_uvs_proportionally() {
let q = inflated_transform_quad(Vec2::new(100.0, 50.0), UVec2::new(200, 100), 1.0);
assert_eq!(q.pos_min, Vec2::new(99.0, 49.0));
assert_eq!(q.pos_max, Vec2::new(301.0, 151.0));
assert_eq!(q.uv_min, Vec2::new(-1.0 / 200.0, -1.0 / 100.0));
assert_eq!(q.uv_max, Vec2::new(1.0 + 1.0 / 200.0, 1.0 + 1.0 / 100.0));
let span = q.pos_max - q.pos_min;
let uv_span = q.uv_max - q.uv_min;
let at_uv_zero = q.pos_min + span * (Vec2::ZERO - q.uv_min) / uv_span;
assert!(at_uv_zero.abs_diff_eq(Vec2::new(100.0, 50.0), 1e-4));
let degenerate = inflated_transform_quad(Vec2::ZERO, UVec2::ZERO, 1.0);
assert!(degenerate.uv_min.is_finite());
}
}