use bevy::math::{UVec2, Vec2, Vec4};
use bevy::prelude::*;
use bevy::render::render_resource::*;
use bevy::render::renderer::{RenderContext, RenderDevice, RenderQueue};
use bevy::render::texture::CachedTexture;
use bevy::shader::ShaderCacheError;
use super::store::alloc_capture_texture;
use super::{
ExtractedFilterPass, ExtractedLayer, ExtractedUiLayers, FilterUniforms, LayerFilterPass,
LayerFilterPipeline, LayerFilterPipelineKey, LayerFilterRun, LayerSlot, LayerTextureStore,
STUCK_GATE_HANG_FRAMES,
};
pub struct ExtractedMorph {
pub freeze_seq: u64,
pub progress: f32,
pub version: u32,
pub pass: ExtractedFilterPass,
}
pub struct MorphSlot {
pub snapshot: Option<CachedTexture>,
pub blend: CachedTexture,
pub blend_size: UVec2,
pub seen_seq: u64,
pub params_version: u32,
pub output_valid: bool,
pub gated_frames: u32,
pub gate_warned: bool,
pub composite_bind_group: Option<BindGroup>,
}
pub fn freeze_morph_snapshot(
slot: &mut LayerSlot,
layer: &ExtractedLayer,
wanted: UVec2,
render_device: &RenderDevice,
) {
let Some(morph) = &layer.morph else {
return;
};
if slot
.morph
.as_ref()
.is_some_and(|m| m.seen_seq == morph.freeze_seq)
{
return;
}
let snapshot = match slot.morph.take() {
Some(prev) if prev.output_valid => Some(prev.blend),
_ => {
if slot.content_valid {
let (fresh, fresh_mips) = alloc_capture_texture(
render_device,
"ui_layer_capture",
slot.size,
slot.format,
slot.mips.is_some(),
);
let stolen = std::mem::replace(&mut slot.texture, fresh);
slot.mips = fresh_mips;
slot.mips_valid = false;
slot.bind_group = None;
slot.bind_group_mips = None;
slot.content_valid = false;
Some(stolen)
} else {
None
}
}
};
let (blend, _) = alloc_capture_texture(
render_device,
"ui_layer_morph_blend",
wanted,
layer.target_format,
false,
);
slot.morph = Some(MorphSlot {
snapshot,
blend,
blend_size: wanted,
seen_seq: morph.freeze_seq,
params_version: 0,
output_valid: false,
gated_frames: 0,
gate_warned: false,
composite_bind_group: None,
});
}
pub fn maintain_morph_blend(
slot: &mut LayerSlot,
layer: &ExtractedLayer,
wanted: UVec2,
render_device: &RenderDevice,
) {
if layer.morph.is_none() {
slot.morph = None;
return;
}
if let Some(morph) = slot.morph.as_mut()
&& morph.blend_size != wanted
{
let (blend, _) = alloc_capture_texture(
render_device,
"ui_layer_morph_blend",
wanted,
layer.target_format,
false,
);
morph.blend = blend;
morph.blend_size = wanted;
morph.output_valid = false;
morph.composite_bind_group = None;
}
}
#[derive(Resource)]
pub struct MorphMeta {
pub uniforms: DynamicUniformBuffer<FilterUniforms>,
pub runs: Vec<Option<LayerFilterRun>>,
}
impl Default for MorphMeta {
fn default() -> Self {
let mut uniforms = DynamicUniformBuffer::default();
uniforms.set_label(Some("ui_layer_morph_uniforms"));
Self {
uniforms,
runs: Vec::new(),
}
}
}
fn morph_engine_params(
user: &[Vec4; crate::filters::MAX_FILTER_PARAM_VECS],
progress: f32,
) -> [Vec4; crate::filters::MAX_FILTER_PARAM_VECS] {
let mut params = *user;
params[7].x = progress.clamp(0.0, 1.0);
params
}
#[allow(clippy::too_many_arguments)]
pub fn prepare_layer_morphs(
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<MorphMeta>,
) {
let MorphMeta { uniforms, runs } = &mut *meta;
uniforms.clear();
runs.clear();
runs.resize_with(extracted.layers.len(), || None);
let Some(pipeline) = pipeline else {
return;
};
let mut staged: Vec<(usize, CachedRenderPipelineId, u32)> = Vec::new();
for (idx, layer) in extracted.layers.iter().enumerate() {
if let Some(shader) = &layer.morph_warm {
specialized.specialize(
&pipeline_cache,
&pipeline,
LayerFilterPipelineKey {
shader: shader.clone(),
target_format: layer.target_format,
},
);
}
let Some(extracted_morph) = &layer.morph else {
continue;
};
let Some(slot) = store.slots.get_mut(&layer.main_entity) else {
continue;
};
let size = slot.size;
let Some(morph) = slot.morph.as_mut() else {
continue;
};
if morph.params_version != extracted_morph.version {
morph.gated_frames = 0;
morph.gate_warned = false;
}
morph.params_version = extracted_morph.version;
morph.output_valid = false;
let id = specialized.specialize(
&pipeline_cache,
&pipeline,
LayerFilterPipelineKey {
shader: extracted_morph.pass.shader.clone(),
target_format: layer.target_format,
},
);
let resolution = size.as_vec2();
let offset = uniforms.push(&FilterUniforms {
time: time.elapsed_secs(),
pad_a: 0.0,
resolution,
texel_size: Vec2::ONE / resolution,
content_inset: Vec2::splat(layer.outset as f32),
params: morph_engine_params(&extracted_morph.pass.params, extracted_morph.progress),
});
staged.push((idx, id, offset));
}
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, pipeline_id, uniform_offset) in staged {
let layer = &extracted.layers[idx];
let Some(slot) = store.slots.get(&layer.main_entity) else {
continue;
};
let Some(morph) = slot.morph.as_ref() else {
continue;
};
let from_view = morph
.snapshot
.as_ref()
.map_or(&slot.texture.default_view, |s| &s.default_view);
let bind_group = render_device.create_bind_group(
"ui_layer_morph",
&layout,
&BindGroupEntries::sequential((
&slot.texture.default_view,
&pipeline.sampler,
uniform_binding.clone(),
from_view,
)),
);
runs[idx] = Some(LayerFilterRun {
passes: vec![LayerFilterPass {
pipeline: pipeline_id,
bind_group,
uniform_offset,
target: morph.blend.default_view.clone(),
}],
});
}
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(morph) = slot.morph.as_mut()
{
morph.output_valid = true;
morph.gated_frames = 0;
morph.gate_warned = false;
}
}
}
pub fn run_morph_passes(
idx: usize,
meta: &MorphMeta,
pipeline_cache: &PipelineCache,
ctx: &mut RenderContext,
) {
let Some(run) = meta.runs.get(idx).and_then(Option::as_ref) else {
return;
};
for pass_data in &run.passes {
let Some(pipeline) = pipeline_cache.get_render_pipeline(pass_data.pipeline) else {
return;
};
let mut pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("ui_layer_morph"),
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);
}
}
#[allow(clippy::too_many_arguments)]
pub fn morph_gate(
idx: usize,
main_entity: bevy::render::sync_world::MainEntity,
morph: &mut MorphSlot,
meta: &MorphMeta,
pipeline_cache: &PipelineCache,
render_device: &RenderDevice,
atlas_layout: &BindGroupLayoutDescriptor,
sampler: &Sampler,
) -> Option<BindGroup> {
if !morph.output_valid {
morph.gated_frames = morph.gated_frames.saturating_add(1);
if !morph.gate_warned {
let compile_error = 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 {main_entity:?}: the morphFilter pass shader failed to \
compile — the layer's subtree is invisible while the morph is in \
flight (the composite gate never falls back to unblended content). \
Error: {err}",
);
morph.gate_warned = true;
} else if morph.gated_frames == STUCK_GATE_HANG_FRAMES {
warn!(
"UI layer {main_entity:?}: morph composite withheld for {} consecutive \
frames and its pipeline is still not ready (no compile error \
reported). The layer's subtree is invisible until it resolves.",
STUCK_GATE_HANG_FRAMES,
);
morph.gate_warned = true;
}
}
return None;
}
if morph.composite_bind_group.is_none() {
morph.composite_bind_group = Some(render_device.create_bind_group(
"ui_layer_composite_morph",
&pipeline_cache.get_bind_group_layout(atlas_layout),
&BindGroupEntries::sequential((&morph.blend.default_view, sampler)),
));
}
morph.composite_bind_group.clone()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filters::MAX_FILTER_PARAM_VECS;
#[test]
fn morph_engine_params_writes_clamped_progress() {
let mut user = [Vec4::ZERO; MAX_FILTER_PARAM_VECS];
user[0] = Vec4::new(1.0, 2.0, 3.0, 4.0);
let p = morph_engine_params(&user, 0.25);
assert_eq!(p[0], user[0], "user params untouched");
assert_eq!(p[6], Vec4::ZERO, "spare slot untouched");
assert_eq!(p[7].x, 0.25);
let p = morph_engine_params(&user, 2.0);
assert_eq!(p[7].x, 1.0, "progress clamps");
}
}