use bevy::core_pipeline::FullscreenShader;
use bevy::math::{UVec2, Vec2};
use bevy::prelude::*;
use bevy::render::camera::ExtractedCamera;
use bevy::render::render_resource::binding_types::{sampler, texture_2d, uniform_buffer};
use bevy::render::render_resource::*;
use bevy::render::renderer::{RenderContext, RenderDevice, RenderQueue};
use bevy::render::texture::CachedTexture;
use bevy::render::view::ViewTarget;
use bevy::shader::Shader;
use super::store::{PassBindGroups, PassBindKey, alloc_capture_texture};
use super::{
ExtractedUiLayers, FilterUniforms, LayerFilterPass, LayerFilterPipeline,
LayerFilterPipelineKey, LayerFilterRun, LayerTextureStore, filter_output_index,
filter_source_index, filter_target_index,
};
pub struct BackdropSlot {
pub snapshot: CachedTexture,
pub textures: [CachedTexture; 2],
pub gate: super::GateState,
pub output_index: usize,
pub composite_bind_group: Option<(usize, BindGroup)>,
pub pass_bind_groups: PassBindGroups,
}
pub fn alloc_backdrop_slot(
render_device: &RenderDevice,
alloc: UVec2,
format: TextureFormat,
) -> BackdropSlot {
let alloc =
|label: &'static str| alloc_capture_texture(render_device, label, alloc, format, false).0;
BackdropSlot {
snapshot: alloc("ui_layer_backdrop_snapshot"),
textures: [
alloc("ui_layer_backdrop_ping"),
alloc("ui_layer_backdrop_pong"),
],
gate: super::GateState::default(),
output_index: 0,
composite_bind_group: None,
pass_bind_groups: PassBindGroups::default(),
}
}
#[derive(Clone, Copy, ShaderType)]
pub struct BackdropBlitUniforms {
pub src_uv_min: Vec2,
pub src_uv_scale: Vec2,
}
#[derive(Resource)]
pub struct BackdropBlitPipeline {
pub layout: BindGroupLayoutDescriptor,
pub sampler: Sampler,
pub shader: Handle<Shader>,
pub fullscreen: FullscreenShader,
}
pub fn init_backdrop_blit_pipeline(
mut commands: Commands,
render_device: Res<RenderDevice>,
asset_server: Res<AssetServer>,
fullscreen: Res<FullscreenShader>,
) {
let layout = BindGroupLayoutDescriptor::new(
"ui_layer_backdrop_blit_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
uniform_buffer::<BackdropBlitUniforms>(true),
),
),
);
commands.insert_resource(BackdropBlitPipeline {
layout,
sampler: render_device.create_sampler(&SamplerDescriptor {
label: Some("ui_layer_backdrop_blit_sampler"),
address_mode_u: AddressMode::ClampToEdge,
address_mode_v: AddressMode::ClampToEdge,
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
..Default::default()
}),
shader: bevy::asset::load_embedded_asset!(asset_server.as_ref(), "backdrop_blit.wgsl"),
fullscreen: fullscreen.clone(),
});
}
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub struct BackdropBlitPipelineKey {
pub target_format: TextureFormat,
}
impl SpecializedRenderPipeline for BackdropBlitPipeline {
type Key = BackdropBlitPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
vertex: self.fullscreen.to_vertex_state(),
fragment: Some(FragmentState {
shader: self.shader.clone(),
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_backdrop_blit_pipeline".into()),
..Default::default()
}
}
}
pub struct BackdropBlit {
pub pipeline: CachedRenderPipelineId,
pub uniform_offset: u32,
pub target: TextureView,
pub viewport: Option<UVec2>,
}
#[derive(Resource)]
pub struct BackdropMeta {
pub blit_uniforms: DynamicUniformBuffer<BackdropBlitUniforms>,
pub filter_uniforms: DynamicUniformBuffer<FilterUniforms>,
pub blits: Vec<Option<BackdropBlit>>,
pub runs: Vec<Option<LayerFilterRun>>,
}
impl Default for BackdropMeta {
fn default() -> Self {
let mut blit_uniforms = DynamicUniformBuffer::default();
blit_uniforms.set_label(Some("ui_layer_backdrop_blit_uniforms"));
let mut filter_uniforms = DynamicUniformBuffer::default();
filter_uniforms.set_label(Some("ui_layer_backdrop_filter_uniforms"));
Self {
blit_uniforms,
filter_uniforms,
blits: Vec::new(),
runs: Vec::new(),
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn prepare_layer_backdrops(
extracted: Res<ExtractedUiLayers>,
mut store: ResMut<LayerTextureStore>,
filter_pipeline: Option<Res<LayerFilterPipeline>>,
blit_pipeline: Option<Res<BackdropBlitPipeline>>,
mut specialized_filters: ResMut<SpecializedRenderPipelines<LayerFilterPipeline>>,
mut specialized_blits: ResMut<SpecializedRenderPipelines<BackdropBlitPipeline>>,
pipeline_cache: Res<PipelineCache>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
cameras: Query<&ExtractedCamera>,
time: Res<Time>,
mut meta: ResMut<BackdropMeta>,
) {
let BackdropMeta {
blit_uniforms,
filter_uniforms,
blits,
runs,
} = &mut *meta;
blit_uniforms.clear();
filter_uniforms.clear();
blits.clear();
runs.clear();
blits.resize_with(extracted.layers.len(), || None);
runs.resize_with(extracted.layers.len(), || None);
let (Some(filter_pipeline), Some(blit_pipeline)) = (filter_pipeline, blit_pipeline) else {
return;
};
let Some(camera) = extracted
.camera_render_entity
.and_then(|e| cameras.get(e).ok())
else {
return;
};
let Some(target_size) = camera.physical_target_size else {
return;
};
let viewport_offset = camera
.viewport
.as_ref()
.map_or(Vec2::ZERO, |v| v.physical_position.as_vec2());
let target_size = target_size.as_vec2().max(Vec2::ONE);
struct StagedBackdrop {
blit_pipeline: CachedRenderPipelineId,
blit_offset: u32,
passes: Vec<(CachedRenderPipelineId, u32)>,
}
let mut staged: Vec<(usize, StagedBackdrop)> = Vec::new();
for (idx, layer) in extracted.layers.iter().enumerate() {
let Some(chain) = &layer.backdrop_chain else {
continue;
};
let Some(slot) = store.slots.get_mut(&layer.main_entity) else {
continue;
};
let size = slot.size;
let Some(backdrop) = slot.backdrop.as_mut() else {
continue;
};
backdrop.gate.restage(chain.version);
backdrop.output_index = filter_output_index(chain.passes.len());
let blit_id = specialized_blits.specialize(
&pipeline_cache,
&blit_pipeline,
BackdropBlitPipelineKey {
target_format: layer.target_format,
},
);
let blit_offset = blit_uniforms.push(&BackdropBlitUniforms {
src_uv_min: (viewport_offset + layer.min) / target_size,
src_uv_scale: size.as_vec2() / target_size,
});
let resolution = size.as_vec2();
let texel_size = Vec2::ONE / resolution;
let passes = chain
.passes
.iter()
.map(|pass| {
let id = specialized_filters.specialize(
&pipeline_cache,
&filter_pipeline,
LayerFilterPipelineKey {
shader: pass.shader.clone(),
target_format: layer.target_format,
},
);
let offset = filter_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,
});
(id, offset)
})
.collect();
staged.push((
idx,
StagedBackdrop {
blit_pipeline: blit_id,
blit_offset,
passes,
},
));
}
if staged.is_empty() {
return;
}
blit_uniforms.write_buffer(&render_device, &render_queue);
filter_uniforms.write_buffer(&render_device, &render_queue);
let (Some(uniform_binding), Some(uniform_buffer)) =
(filter_uniforms.binding(), filter_uniforms.buffer())
else {
return;
};
let layout = pipeline_cache.get_bind_group_layout(&filter_pipeline.layout);
for (idx, staged) in staged {
let layer = &extracted.layers[idx];
let Some(slot) = store.slots.get_mut(&layer.main_entity) else {
continue;
};
let viewport = slot.image_viewport();
let Some(backdrop) = slot.backdrop.as_mut() else {
continue;
};
backdrop.pass_bind_groups.truncate(staged.passes.len());
let passes = staged
.passes
.iter()
.enumerate()
.map(|(i, &(pipeline, uniform_offset))| {
let source = match filter_source_index(i) {
None => &backdrop.snapshot.default_view,
Some(ping) => &backdrop.textures[ping].default_view,
};
let capture = &backdrop.snapshot.default_view;
let key = PassBindKey {
source: source.id(),
capture: capture.id(),
uniforms: uniform_buffer.id(),
};
let bind_group = backdrop.pass_bind_groups.get_or_create(i, key, || {
render_device.create_bind_group(
"ui_layer_backdrop_filter",
&layout,
&BindGroupEntries::sequential((
source,
&filter_pipeline.sampler,
uniform_binding.clone(),
capture,
)),
)
});
LayerFilterPass {
pipeline,
bind_group,
uniform_offset,
target: backdrop.textures[filter_target_index(i)]
.default_view
.clone(),
}
})
.collect();
blits[idx] = Some(BackdropBlit {
pipeline: staged.blit_pipeline,
uniform_offset: staged.blit_offset,
target: backdrop.snapshot.default_view.clone(),
viewport,
});
runs[idx] = Some(LayerFilterRun { passes, viewport });
}
for idx in 0..extracted.layers.len() {
let (Some(blit), Some(run)) = (
blits.get(idx).and_then(Option::as_ref),
runs.get(idx).and_then(Option::as_ref),
) else {
continue;
};
let Some(slot) = store.slots.get_mut(&extracted.layers[idx].main_entity) else {
continue;
};
let ready = pipeline_cache.get_render_pipeline(blit.pipeline).is_some()
&& run
.passes
.iter()
.all(|pass| pipeline_cache.get_render_pipeline(pass.pipeline).is_some());
if ready && let Some(backdrop) = slot.backdrop.as_mut() {
backdrop.gate.ready();
}
}
}
pub fn run_backdrop_passes(
idx: usize,
meta: &BackdropMeta,
blit_pipeline: Option<&BackdropBlitPipeline>,
main_texture: &TextureView,
pipeline_cache: &PipelineCache,
ctx: &mut RenderContext,
) {
let (Some(blit), Some(run)) = (
meta.blits.get(idx).and_then(Option::as_ref),
meta.runs.get(idx).and_then(Option::as_ref),
) else {
return;
};
let Some(blit_pipeline_res) = blit_pipeline else {
return;
};
let Some(blit_compiled) = pipeline_cache.get_render_pipeline(blit.pipeline) else {
return;
};
let Some(chain_compiled) = super::run_pipelines(run, pipeline_cache) else {
return;
};
let Some(blit_binding) = meta.blit_uniforms.binding() else {
return;
};
let blit_bind_group = ctx.render_device().create_bind_group(
"ui_layer_backdrop_blit",
&pipeline_cache.get_bind_group_layout(&blit_pipeline_res.layout),
&BindGroupEntries::sequential((main_texture, &blit_pipeline_res.sampler, blit_binding)),
);
super::fullscreen_pass(
ctx,
"ui_layer_backdrop_blit",
&blit.target,
blit.viewport,
blit_compiled,
&blit_bind_group,
&[blit.uniform_offset],
);
super::replay_run(ctx, "ui_layer_backdrop_filter", run, chain_compiled);
}
pub fn camera_main_texture(world: &World, camera: Option<Entity>) -> Option<TextureView> {
let target = world.get::<ViewTarget>(camera?)?;
Some(target.main_texture_view().clone())
}
#[allow(clippy::too_many_arguments)]
pub fn backdrop_gate(
idx: usize,
main_entity: bevy::render::sync_world::MainEntity,
backdrop: &mut BackdropSlot,
meta: &BackdropMeta,
pipeline_cache: &PipelineCache,
render_device: &RenderDevice,
atlas_layout: &BindGroupLayoutDescriptor,
sampler: &Sampler,
) -> Option<BindGroup> {
if !backdrop.gate.output_valid {
backdrop.gate.on_gated(
meta.runs
.get(idx)
.and_then(Option::as_ref)
.into_iter()
.flat_map(|run| run.passes.iter().map(|p| p.pipeline))
.chain(
meta.blits
.get(idx)
.and_then(Option::as_ref)
.map(|b| b.pipeline),
),
pipeline_cache,
main_entity,
"a backdropFilter pass",
"the region shows the UNFILTERED frame until it resolves (the backdrop gate is \
graceful; the node's own content still draws)",
);
return None;
}
let output = backdrop.output_index;
if !matches!(&backdrop.composite_bind_group, Some((built, _)) if *built == output) {
backdrop.composite_bind_group = Some((
output,
render_device.create_bind_group(
"ui_layer_composite_backdrop",
&pipeline_cache.get_bind_group_layout(atlas_layout),
&BindGroupEntries::sequential((&backdrop.textures[output].default_view, sampler)),
),
));
}
backdrop
.composite_bind_group
.as_ref()
.map(|(_, bind_group)| bind_group.clone())
}
pub const BACKDROP_UNDERLAY_EPSILON: f32 = 0.005;
#[cfg(test)]
mod tests {
use super::super::clip::clip_quad;
use super::*;
#[test]
fn backdrop_quad_shrinks_to_border_box_with_inflated_uvs() {
let min = Vec2::new(100.0, 200.0);
let size = UVec2::new(132, 96); let q = clip_quad(min, size, 16.0, None).expect("quad");
assert_eq!(q.pos_min, Vec2::new(116.0, 216.0));
assert_eq!(q.pos_max, Vec2::new(216.0, 280.0));
assert_eq!(q.uv_min, Vec2::new(16.0 / 132.0, 16.0 / 96.0));
assert_eq!(q.uv_max, Vec2::new(116.0 / 132.0, 80.0 / 96.0));
let q = clip_quad(min, size, 0.0, None).expect("quad");
assert_eq!(q.pos_min, min);
assert_eq!(q.uv_min, Vec2::ZERO);
assert_eq!(q.uv_max, Vec2::ONE);
let clip = Rect::new(150.0, 216.0, 400.0, 400.0);
let q = clip_quad(min, size, 16.0, Some(clip)).expect("quad");
assert_eq!(q.pos_min, Vec2::new(150.0, 216.0));
assert_eq!(q.pos_max, Vec2::new(216.0, 280.0));
assert_eq!(q.uv_min, Vec2::new(50.0 / 132.0, 16.0 / 96.0));
let far = Rect::new(1000.0, 1000.0, 2000.0, 2000.0);
assert!(clip_quad(min, size, 16.0, Some(far)).is_none());
assert!(clip_quad(min, UVec2::new(20, 20), 16.0, None).is_none());
}
#[test]
fn underlay_epsilon_sorts_under_content_quad() {
for first_key in [0.0f32, -0.1, 0.08, 41.0, -3.05] {
let content = first_key; let backdrop = first_key - BACKDROP_UNDERLAY_EPSILON;
assert!(backdrop < content, "key {first_key}");
assert!(content - backdrop < 1.0, "must stay within the stack slot");
}
}
}