1use bevy::math::{Rect, UVec2, Vec2};
32use bevy::prelude::*;
33use bevy::render::camera::ExtractedCamera;
34use bevy::render::render_resource::binding_types::{sampler, texture_2d, uniform_buffer};
35use bevy::render::render_resource::*;
36use bevy::render::renderer::{RenderContext, RenderDevice, RenderQueue};
37use bevy::render::texture::CachedTexture;
38use bevy::render::view::ViewTarget;
39use bevy::shader::Shader;
40
41use super::clip::ClippedQuad;
42use super::store::alloc_capture_texture;
43use super::{
44 ExtractedUiLayers, FilterUniforms, LayerFilterPass, LayerFilterPipeline,
45 LayerFilterPipelineKey, LayerFilterRun, LayerTextureStore, STUCK_GATE_HANG_FRAMES,
46 filter_output_index, filter_source_index, filter_target_index,
47};
48
49pub struct BackdropSlot {
57 pub snapshot: CachedTexture,
61 pub textures: [CachedTexture; 2],
64 pub params_version: u32,
68 pub output_valid: bool,
73 pub gated_frames: u32,
75 pub gate_warned: bool,
77 pub output_index: usize,
79 pub composite_bind_group: Option<(usize, BindGroup)>,
82}
83
84pub fn alloc_backdrop_slot(
86 render_device: &RenderDevice,
87 size: UVec2,
88 format: TextureFormat,
89) -> BackdropSlot {
90 let alloc =
91 |label: &'static str| alloc_capture_texture(render_device, label, size, format, false).0;
92 BackdropSlot {
93 snapshot: alloc("ui_layer_backdrop_snapshot"),
94 textures: [
95 alloc("ui_layer_backdrop_ping"),
96 alloc("ui_layer_backdrop_pong"),
97 ],
98 params_version: 0,
99 output_valid: false,
100 gated_frames: 0,
101 gate_warned: false,
102 output_index: 0,
103 composite_bind_group: None,
104 }
105}
106
107#[derive(Clone, Copy, ShaderType)]
110pub struct BackdropBlitUniforms {
111 pub src_uv_min: Vec2,
112 pub src_uv_scale: Vec2,
113}
114
115#[derive(Resource)]
120pub struct BackdropBlitPipeline {
121 pub layout: BindGroupLayoutDescriptor,
122 pub sampler: Sampler,
123 pub shader: Handle<Shader>,
124}
125
126pub fn init_backdrop_blit_pipeline(
127 mut commands: Commands,
128 render_device: Res<RenderDevice>,
129 asset_server: Res<AssetServer>,
130) {
131 let layout = BindGroupLayoutDescriptor::new(
132 "ui_layer_backdrop_blit_layout",
133 &BindGroupLayoutEntries::sequential(
134 ShaderStages::FRAGMENT,
135 (
136 texture_2d(TextureSampleType::Float { filterable: true }),
137 sampler(SamplerBindingType::Filtering),
138 uniform_buffer::<BackdropBlitUniforms>(true),
139 ),
140 ),
141 );
142 commands.insert_resource(BackdropBlitPipeline {
143 layout,
144 sampler: render_device.create_sampler(&SamplerDescriptor {
145 label: Some("ui_layer_backdrop_blit_sampler"),
146 address_mode_u: AddressMode::ClampToEdge,
147 address_mode_v: AddressMode::ClampToEdge,
148 mag_filter: FilterMode::Linear,
149 min_filter: FilterMode::Linear,
150 ..Default::default()
151 }),
152 shader: bevy::asset::load_embedded_asset!(asset_server.as_ref(), "backdrop_blit.wgsl"),
153 });
154}
155
156#[derive(Clone, Copy, Hash, PartialEq, Eq)]
157pub struct BackdropBlitPipelineKey {
158 pub target_format: TextureFormat,
159}
160
161impl SpecializedRenderPipeline for BackdropBlitPipeline {
162 type Key = BackdropBlitPipelineKey;
163
164 fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
165 RenderPipelineDescriptor {
166 vertex: VertexState {
167 shader: self.shader.clone(),
168 entry_point: Some("vertex".into()),
169 ..Default::default()
170 },
171 fragment: Some(FragmentState {
172 shader: self.shader.clone(),
173 entry_point: Some("fragment".into()),
174 targets: vec![Some(ColorTargetState {
175 format: key.target_format,
176 blend: None,
178 write_mask: ColorWrites::ALL,
179 })],
180 ..Default::default()
181 }),
182 layout: vec![self.layout.clone()],
183 label: Some("ui_layer_backdrop_blit_pipeline".into()),
184 ..Default::default()
185 }
186 }
187}
188
189pub struct BackdropBlit {
193 pub pipeline: CachedRenderPipelineId,
194 pub uniform_offset: u32,
195 pub target: TextureView,
196}
197
198#[derive(Resource)]
204pub struct BackdropMeta {
205 pub blit_uniforms: DynamicUniformBuffer<BackdropBlitUniforms>,
206 pub filter_uniforms: DynamicUniformBuffer<FilterUniforms>,
207 pub blits: Vec<Option<BackdropBlit>>,
208 pub runs: Vec<Option<LayerFilterRun>>,
209}
210
211impl Default for BackdropMeta {
212 fn default() -> Self {
213 let mut blit_uniforms = DynamicUniformBuffer::default();
214 blit_uniforms.set_label(Some("ui_layer_backdrop_blit_uniforms"));
215 let mut filter_uniforms = DynamicUniformBuffer::default();
216 filter_uniforms.set_label(Some("ui_layer_backdrop_filter_uniforms"));
217 Self {
218 blit_uniforms,
219 filter_uniforms,
220 blits: Vec::new(),
221 runs: Vec::new(),
222 }
223 }
224}
225
226#[allow(clippy::too_many_arguments)]
233pub fn prepare_layer_backdrops(
234 extracted: Res<ExtractedUiLayers>,
235 mut store: ResMut<LayerTextureStore>,
236 filter_pipeline: Option<Res<LayerFilterPipeline>>,
237 blit_pipeline: Option<Res<BackdropBlitPipeline>>,
238 mut specialized_filters: ResMut<SpecializedRenderPipelines<LayerFilterPipeline>>,
239 mut specialized_blits: ResMut<SpecializedRenderPipelines<BackdropBlitPipeline>>,
240 pipeline_cache: Res<PipelineCache>,
241 render_device: Res<RenderDevice>,
242 render_queue: Res<RenderQueue>,
243 cameras: Query<&ExtractedCamera>,
244 time: Res<Time>,
245 mut meta: ResMut<BackdropMeta>,
246) {
247 let BackdropMeta {
248 blit_uniforms,
249 filter_uniforms,
250 blits,
251 runs,
252 } = &mut *meta;
253 blit_uniforms.clear();
254 filter_uniforms.clear();
255 blits.clear();
256 runs.clear();
257 blits.resize_with(extracted.layers.len(), || None);
258 runs.resize_with(extracted.layers.len(), || None);
259 let (Some(filter_pipeline), Some(blit_pipeline)) = (filter_pipeline, blit_pipeline) else {
260 return;
261 };
262 let Some(camera) = extracted
266 .camera_render_entity
267 .and_then(|e| cameras.get(e).ok())
268 else {
269 return;
270 };
271 let Some(target_size) = camera.physical_target_size else {
272 return;
273 };
274 let viewport_offset = camera
275 .viewport
276 .as_ref()
277 .map_or(Vec2::ZERO, |v| v.physical_position.as_vec2());
278 let target_size = target_size.as_vec2().max(Vec2::ONE);
279
280 struct StagedBackdrop {
282 blit_pipeline: CachedRenderPipelineId,
283 blit_offset: u32,
284 passes: Vec<(CachedRenderPipelineId, u32)>,
285 }
286 let mut staged: Vec<(usize, StagedBackdrop)> = Vec::new();
287 for (idx, layer) in extracted.layers.iter().enumerate() {
288 let Some(chain) = &layer.backdrop_chain else {
289 continue;
290 };
291 let Some(slot) = store.slots.get_mut(&layer.main_entity) else {
292 continue;
293 };
294 let size = slot.size;
295 let Some(backdrop) = slot.backdrop.as_mut() else {
296 continue;
297 };
298 if backdrop.params_version != chain.version {
301 backdrop.gated_frames = 0;
302 backdrop.gate_warned = false;
303 }
304 backdrop.params_version = chain.version;
305 backdrop.output_valid = false;
308 backdrop.output_index = filter_output_index(chain.passes.len());
309
310 let blit_id = specialized_blits.specialize(
311 &pipeline_cache,
312 &blit_pipeline,
313 BackdropBlitPipelineKey {
314 target_format: layer.target_format,
315 },
316 );
317 let blit_offset = blit_uniforms.push(&BackdropBlitUniforms {
318 src_uv_min: (viewport_offset + layer.min) / target_size,
319 src_uv_scale: size.as_vec2() / target_size,
320 });
321
322 let resolution = size.as_vec2();
323 let texel_size = Vec2::ONE / resolution;
324 let passes = chain
325 .passes
326 .iter()
327 .map(|pass| {
328 let id = specialized_filters.specialize(
329 &pipeline_cache,
330 &filter_pipeline,
331 LayerFilterPipelineKey {
332 shader: pass.shader.clone(),
333 target_format: layer.target_format,
334 },
335 );
336 let offset = filter_uniforms.push(&FilterUniforms {
337 time: time.elapsed_secs(),
338 pad_a: 0.0,
339 resolution,
340 texel_size,
341 pad_b: Vec2::ZERO,
342 params: pass.params,
343 });
344 (id, offset)
345 })
346 .collect();
347 staged.push((
348 idx,
349 StagedBackdrop {
350 blit_pipeline: blit_id,
351 blit_offset,
352 passes,
353 },
354 ));
355 }
356 if staged.is_empty() {
357 return;
358 }
359
360 blit_uniforms.write_buffer(&render_device, &render_queue);
364 filter_uniforms.write_buffer(&render_device, &render_queue);
365 let Some(uniform_binding) = filter_uniforms.binding() else {
366 return;
367 };
368 let layout = pipeline_cache.get_bind_group_layout(&filter_pipeline.layout);
369 for (idx, staged) in staged {
370 let layer = &extracted.layers[idx];
371 let Some(slot) = store.slots.get(&layer.main_entity) else {
372 continue;
373 };
374 let Some(backdrop) = slot.backdrop.as_ref() else {
375 continue;
376 };
377 let passes = staged
378 .passes
379 .iter()
380 .enumerate()
381 .map(|(i, &(pipeline, uniform_offset))| {
382 let source = match filter_source_index(i) {
383 None => &backdrop.snapshot.default_view,
385 Some(ping) => &backdrop.textures[ping].default_view,
386 };
387 let bind_group = render_device.create_bind_group(
388 "ui_layer_backdrop_filter",
389 &layout,
390 &BindGroupEntries::sequential((
391 source,
392 &filter_pipeline.sampler,
393 uniform_binding.clone(),
394 &backdrop.snapshot.default_view,
398 )),
399 );
400 LayerFilterPass {
401 pipeline,
402 bind_group,
403 uniform_offset,
404 target: backdrop.textures[filter_target_index(i)]
405 .default_view
406 .clone(),
407 }
408 })
409 .collect();
410 blits[idx] = Some(BackdropBlit {
411 pipeline: staged.blit_pipeline,
412 uniform_offset: staged.blit_offset,
413 target: backdrop.snapshot.default_view.clone(),
414 });
415 runs[idx] = Some(LayerFilterRun { passes });
416 }
417
418 for idx in 0..extracted.layers.len() {
423 let (Some(blit), Some(run)) = (
424 blits.get(idx).and_then(Option::as_ref),
425 runs.get(idx).and_then(Option::as_ref),
426 ) else {
427 continue;
428 };
429 let Some(slot) = store.slots.get_mut(&extracted.layers[idx].main_entity) else {
430 continue;
431 };
432 let ready = pipeline_cache.get_render_pipeline(blit.pipeline).is_some()
433 && run
434 .passes
435 .iter()
436 .all(|pass| pipeline_cache.get_render_pipeline(pass.pipeline).is_some());
437 if ready && let Some(backdrop) = slot.backdrop.as_mut() {
438 backdrop.output_valid = true;
439 backdrop.gated_frames = 0;
440 backdrop.gate_warned = false;
441 }
442 }
443}
444
445pub fn run_backdrop_passes(
450 idx: usize,
451 meta: &BackdropMeta,
452 blit_pipeline: Option<&BackdropBlitPipeline>,
453 main_texture: &TextureView,
454 pipeline_cache: &PipelineCache,
455 ctx: &mut RenderContext,
456) {
457 let (Some(blit), Some(run)) = (
458 meta.blits.get(idx).and_then(Option::as_ref),
459 meta.runs.get(idx).and_then(Option::as_ref),
460 ) else {
461 return;
462 };
463 let Some(blit_pipeline_res) = blit_pipeline else {
464 return;
465 };
466 let Some(blit_compiled) = pipeline_cache.get_render_pipeline(blit.pipeline) else {
470 return;
471 };
472 let chain_compiled: Option<Vec<_>> = run
473 .passes
474 .iter()
475 .map(|pass| pipeline_cache.get_render_pipeline(pass.pipeline))
476 .collect();
477 let Some(chain_compiled) = chain_compiled else {
478 return;
479 };
480 let Some(blit_binding) = meta.blit_uniforms.binding() else {
481 return;
482 };
483 let blit_bind_group = ctx.render_device().create_bind_group(
488 "ui_layer_backdrop_blit",
489 &pipeline_cache.get_bind_group_layout(&blit_pipeline_res.layout),
490 &BindGroupEntries::sequential((main_texture, &blit_pipeline_res.sampler, blit_binding)),
491 );
492 {
493 let mut pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
494 label: Some("ui_layer_backdrop_blit"),
495 color_attachments: &[Some(RenderPassColorAttachment {
496 view: &blit.target,
497 depth_slice: None,
498 resolve_target: None,
499 ops: Operations {
500 load: LoadOp::Clear(LinearRgba::NONE.into()),
503 store: StoreOp::Store,
504 },
505 })],
506 depth_stencil_attachment: None,
507 timestamp_writes: None,
508 occlusion_query_set: None,
509 multiview_mask: None,
510 });
511 pass.set_render_pipeline(blit_compiled);
512 pass.set_bind_group(0, &blit_bind_group, &[blit.uniform_offset]);
513 pass.draw(0..3, 0..1);
514 }
515 for (pass_data, pipeline) in run.passes.iter().zip(chain_compiled) {
516 let mut pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
517 label: Some("ui_layer_backdrop_filter"),
518 color_attachments: &[Some(RenderPassColorAttachment {
519 view: &pass_data.target,
520 depth_slice: None,
521 resolve_target: None,
522 ops: Operations {
523 load: LoadOp::Clear(LinearRgba::NONE.into()),
524 store: StoreOp::Store,
525 },
526 })],
527 depth_stencil_attachment: None,
528 timestamp_writes: None,
529 occlusion_query_set: None,
530 multiview_mask: None,
531 });
532 pass.set_render_pipeline(pipeline);
533 pass.set_bind_group(0, &pass_data.bind_group, &[pass_data.uniform_offset]);
534 pass.draw(0..3, 0..1);
535 }
536}
537
538pub fn camera_main_texture(world: &World, camera: Option<Entity>) -> Option<TextureView> {
541 let target = world.get::<ViewTarget>(camera?)?;
542 Some(target.main_texture_view().clone())
543}
544
545pub fn backdrop_quad(
551 min: Vec2,
552 size: UVec2,
553 outset: u32,
554 clip: Option<Rect>,
555) -> Option<ClippedQuad> {
556 let size = size.as_vec2();
557 let inset = Vec2::splat(outset as f32);
558 let box_min = min + inset;
559 let box_max = min + size - inset;
560 let (pos_min, pos_max) = match clip {
561 None => (box_min, box_max),
562 Some(c) => (box_min.max(c.min), box_max.min(c.max)),
563 };
564 if pos_min.x >= pos_max.x || pos_min.y >= pos_max.y {
565 return None;
566 }
567 Some(ClippedQuad {
568 pos_min,
569 pos_max,
570 uv_min: (pos_min - min) / size,
571 uv_max: (pos_max - min) / size,
572 })
573}
574
575#[allow(clippy::too_many_arguments)]
581pub fn backdrop_gate(
582 idx: usize,
583 main_entity: bevy::render::sync_world::MainEntity,
584 backdrop: &mut BackdropSlot,
585 meta: &BackdropMeta,
586 pipeline_cache: &PipelineCache,
587 render_device: &RenderDevice,
588 atlas_layout: &BindGroupLayoutDescriptor,
589 sampler: &Sampler,
590) -> Option<BindGroup> {
591 if !backdrop.output_valid {
592 backdrop.gated_frames = backdrop.gated_frames.saturating_add(1);
593 if !backdrop.gate_warned {
594 let compile_error = meta
595 .runs
596 .get(idx)
597 .and_then(|run| run.as_ref())
598 .into_iter()
599 .flat_map(|run| run.passes.iter().map(|p| p.pipeline))
600 .chain(
601 meta.blits
602 .get(idx)
603 .and_then(|b| b.as_ref())
604 .map(|b| b.pipeline),
605 )
606 .find_map(
607 |pipeline| match pipeline_cache.get_render_pipeline_state(pipeline) {
608 CachedPipelineState::Err(
609 e @ (bevy::shader::ShaderCacheError::ProcessShaderError(_)
610 | bevy::shader::ShaderCacheError::CreateShaderModule(_)),
611 ) => Some(e.to_string()),
612 _ => None,
613 },
614 );
615 if let Some(err) = compile_error {
616 warn!(
617 "UI layer {main_entity:?}: a backdropFilter pass shader failed to \
618 compile — the region shows the UNFILTERED frame until fixed (the \
619 backdrop gate is graceful; the node's own content still draws). \
620 Error: {err}",
621 );
622 backdrop.gate_warned = true;
623 } else if backdrop.gated_frames == STUCK_GATE_HANG_FRAMES {
624 warn!(
625 "UI layer {main_entity:?}: backdrop quad withheld for {} consecutive \
626 frames and its pipeline is still not ready (no compile error \
627 reported). The region shows the unfiltered frame until it resolves.",
628 STUCK_GATE_HANG_FRAMES,
629 );
630 backdrop.gate_warned = true;
631 }
632 }
633 return None;
634 }
635 let output = backdrop.output_index;
636 if !matches!(&backdrop.composite_bind_group, Some((built, _)) if *built == output) {
637 backdrop.composite_bind_group = Some((
638 output,
639 render_device.create_bind_group(
640 "ui_layer_composite_backdrop",
641 &pipeline_cache.get_bind_group_layout(atlas_layout),
642 &BindGroupEntries::sequential((&backdrop.textures[output].default_view, sampler)),
643 ),
644 ));
645 }
646 backdrop
647 .composite_bind_group
648 .as_ref()
649 .map(|(_, bind_group)| bind_group.clone())
650}
651
652pub const BACKDROP_UNDERLAY_EPSILON: f32 = 0.005;
659
660#[cfg(test)]
663mod tests {
664 use super::*;
665
666 #[test]
671 fn backdrop_quad_shrinks_to_border_box_with_inflated_uvs() {
672 let min = Vec2::new(100.0, 200.0);
673 let size = UVec2::new(132, 96); let q = backdrop_quad(min, size, 16, None).expect("quad");
675 assert_eq!(q.pos_min, Vec2::new(116.0, 216.0));
676 assert_eq!(q.pos_max, Vec2::new(216.0, 280.0));
677 assert_eq!(q.uv_min, Vec2::new(16.0 / 132.0, 16.0 / 96.0));
678 assert_eq!(q.uv_max, Vec2::new(116.0 / 132.0, 80.0 / 96.0));
679
680 let q = backdrop_quad(min, size, 0, None).expect("quad");
682 assert_eq!(q.pos_min, min);
683 assert_eq!(q.uv_min, Vec2::ZERO);
684 assert_eq!(q.uv_max, Vec2::ONE);
685
686 let clip = Rect::new(150.0, 216.0, 400.0, 400.0);
688 let q = backdrop_quad(min, size, 16, Some(clip)).expect("quad");
689 assert_eq!(q.pos_min, Vec2::new(150.0, 216.0));
690 assert_eq!(q.pos_max, Vec2::new(216.0, 280.0));
691 assert_eq!(q.uv_min, Vec2::new(50.0 / 132.0, 16.0 / 96.0));
692
693 let far = Rect::new(1000.0, 1000.0, 2000.0, 2000.0);
695 assert!(backdrop_quad(min, size, 16, Some(far)).is_none());
696
697 assert!(backdrop_quad(min, UVec2::new(20, 20), 16, None).is_none());
699 }
700
701 #[test]
705 fn underlay_epsilon_sorts_under_content_quad() {
706 for first_key in [0.0f32, -0.1, 0.08, 41.0, -3.05] {
707 let content = first_key; let backdrop = first_key - BACKDROP_UNDERLAY_EPSILON;
709 assert!(backdrop < content, "key {first_key}");
710 assert!(content - backdrop < 1.0, "must stay within the stack slot");
711 }
712 }
713}