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 super::store::{PassBindGroups, PassBindKey, alloc_capture_texture};
use super::{
ExtractedFilterPass, ExtractedLayer, ExtractedUiLayers, FilterUniforms, LayerFilterPass,
LayerFilterPipeline, LayerFilterPipelineKey, LayerFilterRun, LayerSlot, LayerTextureStore,
};
pub struct ExtractedMorph {
pub freeze_seq: u64,
pub progress: f32,
pub version: u32,
pub pass: ExtractedFilterPass,
}
pub struct MorphSlot {
pub snapshot: Option<MorphSnapshot>,
pub blend: CachedTexture,
pub blend_alloc: UVec2,
pub blend_image: UVec2,
pub seen_seq: u64,
pub gate: super::GateState,
pub composite_bind_group: Option<BindGroup>,
pub pass_bind_group: PassBindGroups,
}
pub struct MorphSnapshot {
pub texture: CachedTexture,
pub image: UVec2,
}
pub fn freeze_morph_snapshot(
slot: &mut LayerSlot,
layer: &ExtractedLayer,
wanted: UVec2,
alloc: 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.gate.output_valid => Some(MorphSnapshot {
texture: prev.blend,
image: prev.blend_image,
}),
_ => {
if slot.content_valid {
let (fresh, fresh_mips) = alloc_capture_texture(
render_device,
"ui_layer_capture",
slot.alloc,
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(MorphSnapshot {
texture: stolen,
image: slot.size,
})
} else {
None
}
}
};
let (blend, _) = alloc_capture_texture(
render_device,
"ui_layer_morph_blend",
alloc,
layer.target_format,
false,
);
slot.morph = Some(MorphSlot {
snapshot,
blend,
blend_alloc: alloc,
blend_image: wanted,
seen_seq: morph.freeze_seq,
gate: super::GateState::default(),
composite_bind_group: None,
pass_bind_group: PassBindGroups::default(),
});
}
pub fn maintain_morph_blend(
slot: &mut LayerSlot,
layer: &ExtractedLayer,
render_device: &RenderDevice,
) {
if layer.morph.is_none() {
slot.morph = None;
return;
}
let Some(morph) = slot.morph.as_mut() else {
return;
};
if morph.blend_alloc != slot.alloc {
let (blend, _) = alloc_capture_texture(
render_device,
"ui_layer_morph_blend",
slot.alloc,
layer.target_format,
false,
);
morph.blend = blend;
morph.blend_alloc = slot.alloc;
morph.gate.output_valid = false;
morph.composite_bind_group = None;
}
morph.blend_image = slot.size;
}
#[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;
};
morph.gate.restage(extracted_morph.version);
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),
from_image_size: morph
.snapshot
.as_ref()
.map_or(resolution, |s| s.image.as_vec2()),
pad_b: Vec2::ZERO,
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), Some(uniform_buffer)) = (uniforms.binding(), uniforms.buffer())
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_mut(&layer.main_entity) else {
continue;
};
let live_view = &slot.texture.default_view;
let viewport = slot.image_viewport();
let Some(morph) = slot.morph.as_mut() else {
continue;
};
let from_view = morph
.snapshot
.as_ref()
.map_or(live_view, |s| &s.texture.default_view);
let key = PassBindKey {
source: live_view.id(),
capture: from_view.id(),
uniforms: uniform_buffer.id(),
};
let bind_group = morph.pass_bind_group.get_or_create(0, key, || {
render_device.create_bind_group(
"ui_layer_morph",
&layout,
&BindGroupEntries::sequential((
live_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(),
}],
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());
if ready
&& slot.content_valid
&& let Some(morph) = slot.morph.as_mut()
{
morph.gate.ready();
}
}
}
pub fn run_morph_passes(
idx: usize,
meta: &MorphMeta,
pipeline_cache: &PipelineCache,
ctx: &mut RenderContext,
) {
if let Some(run) = meta.runs.get(idx).and_then(Option::as_ref)
&& let Some(pipelines) = super::run_pipelines(run, pipeline_cache)
{
super::replay_run(ctx, "ui_layer_morph", run, pipelines);
}
}
#[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.gate.output_valid {
morph.gate.on_gated(
meta.runs
.get(idx)
.and_then(Option::as_ref)
.into_iter()
.flat_map(|run| run.passes.iter().map(|pass| pass.pipeline)),
pipeline_cache,
main_entity,
"the morphFilter pass",
"the layer's subtree is invisible while the morph is in flight (the composite \
gate never falls back to unblended content)",
);
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");
}
}