pub mod backdrop;
pub mod cached;
pub mod clip;
pub mod mips;
pub mod morph;
pub mod stock_camera;
pub mod store;
pub mod transform3d;
pub mod warm;
pub use store::*;
use std::ops::Range;
use bevy::asset::{AssetServer, Handle};
use bevy::camera::{Camera, Camera2d, Camera3d};
use bevy::core_pipeline::FullscreenShader;
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, HashSet};
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};
use bevy::render::view::{ExtractedView, RetainedViewEntity, ViewUniform};
use bevy::shader::Shader;
use bevy::shader::ShaderCacheError;
use bevy::ui::{ComputedNode, ComputedStackIndex, 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;
#[derive(Default, Debug)]
pub struct GateState {
pub params_version: u32,
pub output_valid: bool,
pub gated_frames: u32,
pub gate_warned: bool,
}
impl GateState {
pub fn restage(&mut self, version: u32) {
if self.params_version != version {
self.gated_frames = 0;
self.gate_warned = false;
}
self.params_version = version;
self.output_valid = false;
}
pub fn ready(&mut self) {
self.output_valid = true;
self.gated_frames = 0;
self.gate_warned = false;
}
pub fn on_gated(
&mut self,
pipelines: impl IntoIterator<Item = CachedRenderPipelineId>,
pipeline_cache: &PipelineCache,
layer: MainEntity,
what: &str,
effect: &str,
) {
self.gated_frames = self.gated_frames.saturating_add(1);
if self.gate_warned {
return;
}
let compile_error = pipelines.into_iter().find_map(|pipeline| {
match pipeline_cache.get_render_pipeline_state(pipeline) {
CachedPipelineState::Err(
e @ (ShaderCacheError::ProcessShaderError(_)
| ShaderCacheError::CreateShaderModule(_)),
) => Some(e.to_string()),
_ => None,
}
});
if let Some(err) = compile_error {
tracing::warn!(
"UI layer {layer:?}: {what} shader failed to compile — {effect}. Error: {err}"
);
self.gate_warned = true;
} else if self.gated_frames == STUCK_GATE_HANG_FRAMES {
tracing::warn!(
"UI layer {layer:?}: composite withheld for {STUCK_GATE_HANG_FRAMES} \
consecutive frames and {what} pipeline is still not ready (no compile \
error reported — a hung/queued compile?) — {effect}."
);
self.gate_warned = true;
}
}
}
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,
pub bucketable: 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,
bucketable: chain.bucketable,
})
}
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,
pub bucketable: bool,
pub morph: Option<morph::ExtractedMorph>,
pub morph_warm: Option<Handle<Shader>>,
pub root_sort_key: Option<FloatOrd>,
pub root_visible: bool,
}
impl ExtractedLayer {
pub fn fallback_sort_key(&self) -> Option<FloatOrd> {
if self.morph.is_some() || (!self.needs_capture && self.root_visible) {
self.root_sort_key
} else {
None
}
}
}
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct ExtractUiItems;
#[derive(Resource, Default)]
pub struct ExtractedUiLayers {
pub layers: Vec<ExtractedLayer>,
pub membership: HashMap<MainEntity, usize>,
pub enclosing: Vec<Option<usize>>,
pub membership_rebuilds: u64,
pub roots: Vec<Entity>,
pub stock_view: Option<RetainedViewEntity>,
pub camera_render_entity: Option<Entity>,
pub warned_cameras: HashSet<Entity>,
pub capture_order: Vec<usize>,
}
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub fn extract_ui_layers(
mut commands: Commands,
mut views: Query<&mut ExtractedView, With<LayerCaptureView>>,
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>,
Option<&ComputedStackIndex>,
Option<&crate::filters::MorphState>,
Option<&crate::filters::ResolvedMorphChain>,
Option<&InheritedVisibility>,
)>,
>,
membership: Extract<Res<LayerMembership>>,
repaints: Extract<Res<super::LayerRepaintState>>,
clips: Extract<Res<crate::layer::clip::LayerClips>>,
cameras: Extract<
Query<
(
RenderEntity,
&Camera,
Option<&bevy::camera::RenderTarget>,
Has<bevy::ui::IsDefaultUiCamera>,
),
Or<(With<Camera2d>, With<Camera3d>)>,
>,
>,
main_pass_formats: Res<CameraMainPassTextureFormats>,
mut store: ResMut<LayerTextureStore>,
) {
let store = &mut *store;
extracted.layers.clear();
extracted.capture_order.clear();
extracted.stock_view = None;
extracted.camera_render_entity = None;
if layers.is_empty() {
extracted.membership.clear();
extracted.enclosing.clear();
extracted.roots.clear();
for (_, entities) in store.entities.drain() {
despawn_layer_entities(&mut commands, &entities);
}
return;
}
let camera_of = |target_camera: &ComputedUiTargetCamera| {
let camera_main = target_camera.get()?;
let (camera_render, camera, target, is_default_ui) = cameras.get(camera_main).ok()?;
if !camera.is_active {
return None;
}
let target_format = main_pass_formats.get(&camera_render).copied()?;
let window_target = matches!(target, Some(bevy::camera::RenderTarget::Window(_)));
Some((
stock_camera::CameraCandidate {
entity: camera_main,
is_default_ui,
window_target,
},
camera_render,
target_format,
))
};
let stock_camera = stock_camera::pick_stock_camera(
layers
.iter()
.filter_map(|l| camera_of(l.3).map(|(candidate, _, _)| candidate)),
);
let mut layer_index: HashMap<Entity, usize> = HashMap::default();
for (
root,
rect,
alpha,
target_camera,
filter_chain,
backdrop,
transform3d,
promoted,
computed,
stack_index,
morph_state,
morph_chain,
root_visibility,
) in layers.iter()
{
let Some((candidate, camera_render, target_format)) = camera_of(target_camera) else {
continue;
};
let camera_main = candidate.entity;
if Some(camera_main) != stock_camera {
let stock = stock_camera.expect("a candidate exists, so a stock camera was picked");
if extracted.warned_cameras.insert(camera_main) {
stock_camera::warn_skipped(camera_main, stock);
}
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 retained =
RetainedViewEntity::new(MainEntity::from(root), None, UI_LAYER_CAPTURE_SUBVIEW);
let entities = store
.entities
.entry(MainEntity::from(root))
.or_insert_with(|| LayerEntities {
view: commands.spawn(LayerCaptureView).id(),
view_extracted: false,
quad: commands.spawn(LayerCompositeBatch::default()).id(),
backdrop_quad: None,
});
let view_entity = entities.view;
let quad_entity = entities.quad;
phases.prepare_for_new_frame(retained);
let wants_mips = promoted.reasons.0 & crate::layer::PromotionReasons::TRANSFORM3D != 0;
let bucketable = !wants_mips
&& filter_chain.is_none_or(|c| c.bucketable)
&& backdrop.is_none_or(|b| b.0.bucketable)
&& morph_chain.is_none_or(|m| m.0.bucketable);
let cached_ok = store
.slots
.get(&MainEntity::from(root))
.is_some_and(|slot| {
slot.content_valid
&& slot.size == size
&& !slot.needs_realloc(size, target_format, wants_mips, bucketable)
});
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 = match (backdrop_chain.is_some(), entities.backdrop_quad) {
(true, Some(quad)) => Some(quad),
(true, None) => {
let quad = commands.spawn(LayerCompositeBatch::default()).id();
entities.backdrop_quad = Some(quad);
Some(quad)
}
(false, Some(quad)) => {
commands.entity(quad).despawn();
entities.backdrop_quad = None;
None
}
(false, None) => None,
};
let morph = morph_state.and_then(|state| {
if !state.active {
return None;
}
let chain = &morph_chain?.0;
let pass = extract_chain(Some(chain))
.and_then(|mut c| (c.passes.len() == 1).then(|| c.passes.remove(0)))?;
Some(morph::ExtractedMorph {
freeze_seq: state.freeze_seq,
progress: state.progress,
version: chain.version,
pass,
})
});
let morph_warm = match &morph {
Some(_) => None,
None => morph_chain
.and_then(|c| (c.0.passes.len() == 1).then(|| c.0.passes[0].shader.clone())),
};
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,
bucketable,
root_sort_key: stack_index
.map(|s| FloatOrd(s.0 as f32 + stack_z_offsets::BACKGROUND_COLOR)),
root_visible: root_visibility.is_some_and(|v| v.get()),
morph,
morph_warm,
});
}
let live: HashSet<RetainedViewEntity> = extracted.layers.iter().map(|l| l.retained).collect();
phases.retain(|retained, _| {
retained.subview_index != UI_LAYER_CAPTURE_SUBVIEW || live.contains(retained)
});
store.entities.retain(|main, entities| {
let live = layer_index.contains_key(&main.id());
if !live {
despawn_layer_entities(&mut commands, entities);
}
live
});
let roots_changed = extracted.roots.len() != extracted.layers.len()
|| extracted
.roots
.iter()
.zip(&extracted.layers)
.any(|(root, layer)| *root != layer.main_entity.id());
if roots_changed || extracted.membership_rebuilds != membership.rebuilds {
let extracted = &mut *extracted;
extracted.membership_rebuilds = membership.rebuilds;
extracted.roots.clear();
extracted
.roots
.extend(extracted.layers.iter().map(|l| l.main_entity.id()));
extracted.membership.clear();
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;
for layer in &extracted.layers {
let Some(entities) = store.entities.get_mut(&layer.main_entity) else {
continue;
};
match (layer.needs_capture, entities.view_extracted) {
(true, true) => {
if let Ok(mut view) = views.get_mut(entities.view) {
update_capture_view(&mut view, layer);
}
}
(true, false) => {
commands.entity(entities.view).insert(capture_view(layer));
entities.view_extracted = true;
}
(false, true) => {
commands.entity(entities.view).remove::<ExtractedView>();
entities.view_extracted = false;
}
(false, false) => {}
}
}
}
fn capture_view(layer: &ExtractedLayer) -> ExtractedView {
let (min, size) = (layer.min, layer.size);
ExtractedView {
retained_view_entity: layer.retained,
clip_from_view: Mat4::orthographic_rh(
min.x,
min.x + size.x as f32,
min.y + size.y as f32,
min.y,
0.0,
UI_CAMERA_FAR,
),
world_from_view: GlobalTransform::from_xyz(
0.0,
0.0,
UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET,
),
clip_from_world: None,
target_format: layer.target_format,
viewport: UVec4::new(0, 0, size.x, size.y),
color_grading: Default::default(),
invert_culling: false,
}
}
fn update_capture_view(view: &mut Mut<ExtractedView>, layer: &ExtractedLayer) {
let fresh = capture_view(layer);
if view.clip_from_view != fresh.clip_from_view
|| view.viewport != fresh.viewport
|| view.target_format != fresh.target_format
|| view.retained_view_entity != fresh.retained_view_entity
{
**view = fresh;
}
}
fn despawn_layer_entities(commands: &mut Commands, entities: &LayerEntities) {
commands.entity(entities.view).despawn();
commands.entity(entities.quad).despawn();
if let Some(quad) = entities.backdrop_quad {
commands.entity(quad).despawn();
}
}
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));
}
}
fill_fallback_sort_keys(
&mut quad_sort_keys,
extracted
.layers
.iter()
.map(ExtractedLayer::fallback_sort_key),
);
propagate_quad_sort_keys(&mut quad_sort_keys, &extracted.enclosing);
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);
}
}
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; 2],
pub uv: [f32; 2],
pub alpha: f32,
}
#[derive(Resource)]
pub struct LayerCompositeMeta {
pub vertices: RawBufferVec<LayerCompositeVertex>,
pub uploaded: Vec<LayerCompositeVertex>,
pub atlas_bind_groups: Vec<BindGroup>,
}
impl Default for LayerCompositeMeta {
fn default() -> Self {
Self {
vertices: RawBufferVec::new(BufferUsages::VERTEX),
uploaded: Vec::new(),
atlas_bind_groups: Vec::new(),
}
}
}
#[derive(Component, Default, Clone)]
pub struct LayerCompositeBatch {
pub range: Range<u32>,
pub atlas: usize,
pub uniform_offset: u32,
}
const EDGE_AA_INFLATE_PX: f32 = 1.0;
struct StagedQuad {
entity: Entity,
range: Range<u32>,
atlas: usize,
uniform: usize,
}
fn push_quad_vertices(
vertices: &mut RawBufferVec<LayerCompositeVertex>,
min: Vec2,
max: Vec2,
uv_min: Vec2,
uv_max: Vec2,
alpha: f32,
) {
let corners = [
([min.x, min.y], [uv_min.x, uv_min.y]),
([max.x, min.y], [uv_max.x, uv_min.y]),
([max.x, max.y], [uv_max.x, uv_max.y]),
([min.x, min.y], [uv_min.x, uv_min.y]),
([max.x, max.y], [uv_max.x, uv_max.y]),
([min.x, max.y], [uv_min.x, uv_max.y]),
];
for (position, uv) in corners {
vertices.push(LayerCompositeVertex {
position,
uv,
alpha,
});
}
}
#[allow(clippy::too_many_arguments)]
pub fn prepare_layer_composites(
extracted: Res<ExtractedUiLayers>,
mut store: ResMut<LayerTextureStore>,
mut batches: Query<&mut LayerCompositeBatch>,
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>,
morph_meta: Res<morph::MorphMeta>,
mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
) {
meta.vertices.clear();
meta.atlas_bind_groups.clear();
uniforms_meta.staged.clear();
let Some(pipeline) = pipeline else {
return;
};
if extracted.layers.is_empty() {
return;
}
let mut drawable: HashSet<Entity> = HashSet::default();
let mut staged_quads: Vec<StagedQuad> = Vec::new();
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 image_size = slot.size.as_vec2();
let bind_group = if layer.chain.is_some() {
let Some(filter) = slot.filter.as_mut() else {
gated.push(idx);
continue;
};
if !filter.gate.output_valid {
filter.gate.on_gated(
filter_meta
.runs
.get(idx)
.and_then(Option::as_ref)
.into_iter()
.flat_map(|run| run.passes.iter().map(|pass| pass.pipeline)),
&pipeline_cache,
layer.main_entity,
"a filter pass",
"the layer's subtree is invisible (the composite gate never falls back \
to unfiltered content) and its filter run restages every frame",
);
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.morph.is_some() {
let Some(morph_slot) = slot.morph.as_mut() else {
gated.push(idx);
continue;
};
let Some(bind_group) = morph::morph_gate(
idx,
layer.main_entity,
morph_slot,
&morph_meta,
&pipeline_cache,
&render_device,
&pipeline.atlas_layout,
&pipeline.sampler,
) else {
gated.push(idx);
continue;
};
bind_group
} 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) => (
clip::clip_quad(layer.min, layer.size, -EDGE_AA_INFLATE_PX, None)
.expect("a grown quad is never empty"),
model,
layer.quad_clip,
EDGE_AA_INFLATE_PX,
),
None => {
let Some(q) = clip::clip_quad(layer.min, layer.size, 0.0, 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);
push_quad_vertices(&mut meta.vertices, min, max, uv_min, uv_max, layer.alpha);
drawable.insert(layer.quad_entity);
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 = uniforms_meta.staged.len();
uniforms_meta.staged.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,
image_size,
radius: Vec4::ZERO,
box_center: Vec2::ZERO,
box_size: Vec2::ZERO,
});
staged_quads.push(StagedQuad {
entity: layer.quad_entity,
range: start..start + 6,
atlas: atlas_index,
uniform,
});
}
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 image_size = slot.size.as_vec2();
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) = clip::clip_quad(layer.min, layer.size, layer.outset as f32, layer.quad_clip)
else {
continue;
};
let start = meta.vertices.len() as u32;
push_quad_vertices(
&mut meta.vertices,
q.pos_min,
q.pos_max,
q.uv_min,
q.uv_max,
layer.alpha,
);
drawable.insert(backdrop_quad_entity);
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 = uniforms_meta.staged.len();
uniforms_meta.staged.push(transform3d::CompositeUniforms {
model: Mat4::IDENTITY,
clip_min: open_min,
clip_max: open_max,
edge_feather: 0.0,
pad_a: 0.0,
image_size,
radius: Vec4::from(layer.corner_radius),
box_center: (box_min + box_max) * 0.5,
box_size: box_max - box_min,
});
staged_quads.push(StagedQuad {
entity: backdrop_quad_entity,
range: start..start + 6,
atlas: atlas_index,
uniform,
});
}
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
});
}
{
let LayerCompositeMeta {
vertices, uploaded, ..
} = &mut *meta;
let staged: &[u8] = bytemuck::cast_slice(vertices.values());
if vertices.buffer().is_none() || staged != bytemuck::cast_slice::<_, u8>(uploaded) {
vertices.write_buffer(&render_device, &render_queue);
uploaded.clear();
uploaded.extend_from_slice(vertices.values());
}
}
let uniforms_meta = &mut *uniforms_meta;
if uniforms_meta.uniforms.buffer().is_none() || uniforms_meta.staged != uniforms_meta.uploaded {
let transform3d::CompositeUniformsMeta {
uniforms,
staged,
uploaded,
offsets,
..
} = uniforms_meta;
uniforms.clear();
offsets.clear();
offsets.extend(staged.iter().map(|entry| uniforms.push(entry)));
uniforms.write_buffer(&render_device, &render_queue);
std::mem::swap(staged, uploaded);
}
if let Some(buffer) = uniforms_meta.uniforms.buffer()
&& !matches!(&uniforms_meta.bind_group, Some((id, _)) if *id == buffer.id())
{
let binding = uniforms_meta.uniforms.binding().expect("buffer exists");
uniforms_meta.bind_group = Some((
buffer.id(),
render_device.create_bind_group(
"ui_layer_composite_uniforms",
&pipeline_cache.get_bind_group_layout(&pipeline.uniform_layout),
&BindGroupEntries::single(binding),
),
));
}
for staged in staged_quads {
if let Ok(mut batch) = batches.get_mut(staged.entity) {
*batch = LayerCompositeBatch {
range: staged.range,
atlas: staged.atlas,
uniform_offset: uniforms_meta.offsets[staged.uniform],
};
}
}
for phase in phases.values_mut() {
for item in phase.items.values_mut() {
if drawable.contains(&item.entity()) {
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::Float32x2,
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::PREMULTIPLIED_ALPHA_BLENDING),
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 content_inset: Vec2,
pub from_image_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 fullscreen: FullscreenShader,
}
pub fn init_layer_filter_pipeline(
mut commands: Commands,
render_device: Res<RenderDevice>,
fullscreen: Res<FullscreenShader>,
) {
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()
}),
fullscreen: fullscreen.clone(),
});
}
#[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: self.fullscreen.to_vertex_state(),
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;
}
}
fn fill_fallback_sort_keys(
keys: &mut [Option<FloatOrd>],
fallbacks: impl IntoIterator<Item = Option<FloatOrd>>,
) {
for (key, fallback) in keys.iter_mut().zip(fallbacks) {
if key.is_none() {
*key = fallback;
}
}
}
fn propagate_quad_sort_keys(keys: &mut [Option<FloatOrd>], enclosing: &[Option<usize>]) {
for idx in 0..keys.len() {
let Some(key) = keys[idx] else {
continue;
};
walk_enclosing(idx, enclosing, |outer| match keys[outer] {
Some(existing) if existing <= key => false,
_ => {
keys[outer] = Some(key);
true
}
});
}
}
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>,
pub viewport: Option<UVec2>,
}
pub(super) fn set_image_viewport(pass: &mut TrackedRenderPass, image: UVec2) {
pass.set_viewport(0.0, 0.0, image.x as f32, image.y as f32, 0.0, 1.0);
pass.set_scissor_rect(0, 0, image.x, image.y);
}
pub(super) fn clear_pass<'a>(
ctx: &'a mut RenderContext,
label: &'static str,
target: &TextureView,
viewport: Option<UVec2>,
) -> TrackedRenderPass<'a> {
let mut pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
label: Some(label),
color_attachments: &[Some(RenderPassColorAttachment {
view: 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,
});
if let Some(image) = viewport {
set_image_viewport(&mut pass, image);
}
pass
}
pub(super) fn fullscreen_pass(
ctx: &mut RenderContext,
label: &'static str,
target: &TextureView,
viewport: Option<UVec2>,
pipeline: &RenderPipeline,
bind_group: &BindGroup,
offsets: &[u32],
) {
let mut pass = clear_pass(ctx, label, target, viewport);
pass.set_render_pipeline(pipeline);
pass.set_bind_group(0, bind_group, offsets);
pass.draw(0..3, 0..1);
}
pub(super) fn run_pipelines<'a>(
run: &LayerFilterRun,
pipeline_cache: &'a PipelineCache,
) -> Option<Vec<&'a RenderPipeline>> {
run.passes
.iter()
.map(|pass| pipeline_cache.get_render_pipeline(pass.pipeline))
.collect()
}
pub(super) fn replay_run(
ctx: &mut RenderContext,
label: &'static str,
run: &LayerFilterRun,
pipelines: Vec<&RenderPipeline>,
) {
for (pass, pipeline) in run.passes.iter().zip(pipelines) {
fullscreen_pass(
ctx,
label,
&pass.target,
run.viewport,
pipeline,
&pass.bind_group,
&[pass.uniform_offset],
);
}
}
#[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.gate.params_version,
chain.always_dirty,
filter.gate.output_valid,
) && layer.morph.is_none()
{
continue;
}
filter.gate.restage(chain.version);
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,
content_inset: Vec2::splat(layer.outset as f32),
from_image_size: resolution,
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), Some(uniform_buffer)) = (uniforms.binding(), uniforms.buffer())
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_mut(&layer.main_entity) else {
continue;
};
let effective_capture = slot
.morph
.as_ref()
.map_or(&slot.texture.default_view, |m| &m.blend.default_view);
let viewport = slot.image_viewport();
let Some(filter) = slot.filter.as_mut() else {
continue;
};
filter.pass_bind_groups.truncate(staged_passes.len());
let passes = staged_passes
.into_iter()
.enumerate()
.map(|(i, pass)| {
let source = match filter_source_index(i) {
None => effective_capture,
Some(ping) => &filter.textures[ping].default_view,
};
let key = PassBindKey {
source: source.id(),
capture: effective_capture.id(),
uniforms: uniform_buffer.id(),
};
let bind_group = filter.pass_bind_groups.get_or_create(i, key, || {
render_device.create_bind_group(
"ui_layer_filter",
&layout,
&BindGroupEntries::sequential((
source,
&pipeline.sampler,
uniform_binding.clone(),
effective_capture,
)),
)
});
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, viewport });
}
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());
let morph_ok = slot.morph.as_ref().is_none_or(|m| m.gate.output_valid);
if ready
&& slot.content_valid
&& morph_ok
&& let Some(filter) = slot.filter.as_mut()
{
filter.gate.ready();
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn ui_layer_capture_pass(
world: &World,
view: ViewQuery<Entity>,
extracted: Res<ExtractedUiLayers>,
store: Res<LayerTextureStore>,
phases: Res<ViewSortedRenderPhases<TransparentUi>>,
filter_meta: Res<LayerFilterMeta>,
mip_meta: Res<mips::LayerMipMeta>,
backdrop_meta: Res<backdrop::BackdropMeta>,
morph_meta: Res<morph::MorphMeta>,
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(slot) = store.slots.get(&layer.main_entity)
&& let Some(phase) = phases.get(&layer.retained)
{
let mut pass = clear_pass(
&mut ctx,
"ui_layer_capture",
&slot.texture.default_view,
slot.image_viewport(),
);
if let Err(err) = phase.render(&mut pass, world, layer.view_entity) {
tracing::error!("layer capture pass failed: {err:?}");
}
}
morph::run_morph_passes(idx, &morph_meta, &pipeline_cache, &mut ctx);
if let Some(run) = filter_meta.runs.get(idx).and_then(Option::as_ref)
&& let Some(pipelines) = run_pipelines(run, &pipeline_cache)
{
replay_run(&mut ctx, "ui_layer_filter", run, pipelines);
}
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 {
fullscreen_pass(
&mut ctx,
"ui_layer_mip_blit",
&level.target,
None,
pipeline,
&level.bind_group,
&[],
);
}
}
}
}
#[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(), 176);
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),
content_inset: Vec2::new(9.0, 9.5),
from_image_size: Vec2::new(300.0, 200.0),
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(), 176);
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, 24), 9.0); assert_eq!(f32_at(&bytes, 28), 9.5); assert_eq!(f32_at(&bytes, 32), 300.0); assert_eq!(f32_at(&bytes, 36), 200.0); assert_eq!(f32_at(&bytes, 48), 1.0); assert_eq!(f32_at(&bytes, 60), 4.0); assert_eq!(f32_at(&bytes, 48 + 7 * 16), 5.0); assert_eq!(f32_at(&bytes, 48 + 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 quad_sort_keys_propagate_to_bare_enclosing_layers() {
let key = |v: f32| Some(FloatOrd(v));
let enclosing = [None, Some(0), Some(1)];
let mut keys = [None, key(5.0), key(7.0)];
propagate_quad_sort_keys(&mut keys, &enclosing);
assert_eq!(keys, [key(5.0), key(5.0), key(7.0)]);
let enclosing = [None, Some(0), Some(0)];
let mut keys = [key(9.0), key(3.0), key(12.0)];
propagate_quad_sort_keys(&mut keys, &enclosing);
assert_eq!(keys, [key(3.0), key(3.0), key(12.0)]);
let enclosing = [None, Some(0)];
let mut keys: [Option<FloatOrd>; 2] = [None, None];
propagate_quad_sort_keys(&mut keys, &enclosing);
assert_eq!(keys, [None, None]);
}
#[test]
fn fallback_sort_keys_fill_morphing_empty_layers() {
let key = |v: f32| Some(FloatOrd(v));
let mut keys = [key(5.0), None, None];
fill_fallback_sort_keys(&mut keys, [key(1.0), key(4.0), None]);
assert_eq!(keys, [key(5.0), key(4.0), None]);
let enclosing = [None, Some(0)];
let mut keys = [None, None];
fill_fallback_sort_keys(&mut keys, [None, key(6.0)]);
propagate_quad_sort_keys(&mut keys, &enclosing);
assert_eq!(keys, [key(6.0), key(6.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 grown_transform_quad_extends_uvs_proportionally() {
let q = clip::clip_quad(Vec2::new(100.0, 50.0), UVec2::new(200, 100), -1.0, None).unwrap();
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));
let uv_max = Vec2::new(1.0 + 1.0 / 200.0, 1.0 + 1.0 / 100.0);
assert!(q.uv_max.abs_diff_eq(uv_max, 1e-6));
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 = clip::clip_quad(Vec2::ZERO, UVec2::ZERO, -1.0, None).unwrap();
assert!(degenerate.uv_min.is_finite());
}
}