use bevy::platform::collections::HashMap;
use bevy::prelude::*;
use bevy::render::render_phase::ViewSortedRenderPhases;
use bevy::render::render_resource::{
BindGroup, BufferId, Extent3d, PipelineCache, TextureDescriptor, TextureDimension,
TextureFormat, TextureUsages, TextureViewDescriptor, TextureViewId,
};
use bevy::render::renderer::RenderDevice;
use bevy::render::sync_world::MainEntity;
use bevy::render::texture::CachedTexture;
use bevy::ui_render::TransparentUi;
use super::{ExtractedUiLayers, mips};
pub struct LayerSlot {
pub texture: CachedTexture,
pub size: UVec2,
pub alloc: UVec2,
pub format: TextureFormat,
pub bind_group: Option<BindGroup>,
pub mips: Option<mips::MipChain>,
pub mips_valid: bool,
pub bind_group_mips: Option<BindGroup>,
pub content_valid: bool,
pub filter: Option<FilterSlot>,
pub backdrop: Option<super::backdrop::BackdropSlot>,
pub morph: Option<super::morph::MorphSlot>,
pub last_seen: u64,
}
impl LayerSlot {
pub fn needs_realloc(
&self,
wanted: UVec2,
format: TextureFormat,
mipped: bool,
bucketable: bool,
) -> bool {
self.format != format
|| self.mips.is_some() != mipped
|| !alloc_fits(self.alloc, wanted, bucketable)
}
pub fn image_viewport(&self) -> Option<UVec2> {
(self.alloc != self.size).then_some(self.size)
}
}
pub const BUCKET_PX: u32 = 32;
const SHRINK_SLACK_PX: u32 = 2 * BUCKET_PX;
pub fn bucket_size(wanted: UVec2) -> UVec2 {
let round_up = |v: u32| v.max(1).div_ceil(BUCKET_PX) * BUCKET_PX;
UVec2::new(round_up(wanted.x), round_up(wanted.y))
}
pub fn alloc_fits(alloc: UVec2, wanted: UVec2, bucketable: bool) -> bool {
if bucketable {
alloc.cmpge(wanted).all() && alloc.cmple(bucket_size(wanted) + SHRINK_SLACK_PX).all()
} else {
alloc == wanted
}
}
pub fn alloc_for(wanted: UVec2, bucketable: bool) -> UVec2 {
if bucketable {
bucket_size(wanted)
} else {
wanted
}
}
pub struct FilterSlot {
pub textures: [CachedTexture; 2],
pub gate: super::GateState,
pub output_index: usize,
pub composite_bind_group: Option<(usize, BindGroup)>,
pub mips: [Option<mips::MipChain>; 2],
pub mips_valid: bool,
pub composite_bind_group_mips: Option<(usize, BindGroup)>,
pub pass_bind_groups: PassBindGroups,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct PassBindKey {
pub source: TextureViewId,
pub capture: TextureViewId,
pub uniforms: BufferId,
}
#[derive(Default)]
pub struct PassBindGroups {
entries: Vec<(PassBindKey, BindGroup)>,
}
impl PassBindGroups {
pub fn truncate(&mut self, len: usize) {
self.entries.truncate(len);
}
pub fn get_or_create(
&mut self,
index: usize,
key: PassBindKey,
build: impl FnOnce() -> BindGroup,
) -> BindGroup {
match self.entries.get_mut(index) {
Some((cached_key, bind_group)) => {
if *cached_key != key {
*cached_key = key;
*bind_group = build();
}
bind_group.clone()
}
None => {
let bind_group = build();
if index == self.entries.len() {
self.entries.push((key, bind_group.clone()));
}
bind_group
}
}
}
}
#[derive(Component)]
pub struct LayerCaptureView;
pub struct LayerEntities {
pub view: Entity,
pub view_extracted: bool,
pub quad: Entity,
pub backdrop_quad: Option<Entity>,
}
#[derive(Resource, Default)]
pub struct LayerTextureStore {
pub slots: HashMap<MainEntity, LayerSlot>,
pub entities: HashMap<MainEntity, LayerEntities>,
pub frame: u64,
}
pub fn prepare_layer_textures(
extracted: Res<ExtractedUiLayers>,
render_device: Res<RenderDevice>,
pipeline_cache: Res<PipelineCache>,
phases: Res<ViewSortedRenderPhases<TransparentUi>>,
mut store: ResMut<LayerTextureStore>,
) {
let store = &mut *store;
store.frame += 1;
let frame = store.frame;
for layer in &extracted.layers {
let wanted = layer.size.max(UVec2::ONE);
let slot = store.slots.entry(layer.main_entity).or_insert_with(|| {
alloc_layer_slot(
&render_device,
wanted,
alloc_for(wanted, layer.bucketable),
layer.target_format,
layer.wants_mips,
)
});
let realloc = slot.needs_realloc(
wanted,
layer.target_format,
layer.wants_mips,
layer.bucketable,
);
let alloc = if realloc {
alloc_for(wanted, layer.bucketable)
} else {
slot.alloc
};
super::morph::freeze_morph_snapshot(slot, layer, wanted, alloc, &render_device);
if realloc {
let morph = slot.morph.take();
*slot = alloc_layer_slot(
&render_device,
wanted,
alloc,
layer.target_format,
layer.wants_mips,
);
slot.morph = morph;
}
slot.size = wanted;
super::morph::maintain_morph_blend(slot, layer, &render_device);
if layer.chain.is_some() {
if slot.filter.is_none() {
slot.filter = Some(alloc_filter_slot(
&render_device,
slot.alloc,
layer.target_format,
layer.wants_mips,
));
}
} else {
slot.filter = None;
}
if layer.backdrop_chain.is_some() {
if slot.backdrop.is_none() {
slot.backdrop = Some(super::backdrop::alloc_backdrop_slot(
&render_device,
slot.alloc,
layer.target_format,
));
}
} else {
slot.backdrop = None;
}
if layer.needs_capture {
slot.content_valid = phases.get(&layer.retained).is_some_and(|phase| {
phase
.items
.values()
.all(|i| pipeline_cache.get_render_pipeline(i.pipeline).is_some())
});
slot.mips_valid = false;
}
slot.last_seen = frame;
}
store.slots.retain(|_, slot| slot.last_seen + 3 >= frame);
}
pub(super) fn alloc_capture_texture(
render_device: &RenderDevice,
label: &'static str,
size: UVec2,
format: TextureFormat,
mipped: bool,
) -> (CachedTexture, Option<mips::MipChain>) {
let levels = if mipped {
mips::mip_level_count(size)
} else {
1
};
let texture = render_device.create_texture(&TextureDescriptor {
label: Some(label),
size: Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
mip_level_count: levels,
sample_count: 1,
dimension: TextureDimension::D2,
format,
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let default_view = texture.create_view(&TextureViewDescriptor {
mip_level_count: Some(1),
..Default::default()
});
let chain = mipped.then(|| mips::build_mip_chain(&texture, levels));
(
CachedTexture {
texture,
default_view,
},
chain,
)
}
fn alloc_layer_slot(
render_device: &RenderDevice,
size: UVec2,
alloc: UVec2,
format: TextureFormat,
mipped: bool,
) -> LayerSlot {
let (texture, mips) =
alloc_capture_texture(render_device, "ui_layer_capture", alloc, format, mipped);
LayerSlot {
texture,
size,
alloc,
format,
bind_group: None,
mips,
mips_valid: false,
bind_group_mips: None,
content_valid: false,
filter: None,
backdrop: None,
morph: None,
last_seen: 0,
}
}
fn alloc_filter_slot(
render_device: &RenderDevice,
alloc: UVec2,
format: TextureFormat,
mipped: bool,
) -> FilterSlot {
let alloc_one =
|label: &'static str| alloc_capture_texture(render_device, label, alloc, format, mipped);
let (ping, ping_mips) = alloc_one("ui_layer_filter_ping");
let (pong, pong_mips) = alloc_one("ui_layer_filter_pong");
FilterSlot {
textures: [ping, pong],
gate: super::GateState::default(),
output_index: 0,
composite_bind_group: None,
mips: [ping_mips, pong_mips],
mips_valid: false,
composite_bind_group_mips: None,
pass_bind_groups: PassBindGroups::default(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bucket_size_rounds_up_to_whole_buckets() {
assert_eq!(bucket_size(UVec2::new(1, 1)), UVec2::splat(BUCKET_PX));
assert_eq!(bucket_size(UVec2::ZERO), UVec2::splat(BUCKET_PX));
assert_eq!(bucket_size(UVec2::new(32, 33)), UVec2::new(32, 64));
assert_eq!(bucket_size(UVec2::new(213, 150)), UVec2::new(224, 160));
}
#[test]
fn alloc_fits_table() {
let exact = |a: (u32, u32), w: (u32, u32)| {
alloc_fits(UVec2::new(a.0, a.1), UVec2::new(w.0, w.1), false)
};
assert!(exact((200, 150), (200, 150)));
assert!(!exact((201, 150), (200, 150)), "larger is not exact");
assert!(!exact((199, 150), (200, 150)));
let bucketed = |a: (u32, u32), w: (u32, u32)| {
alloc_fits(UVec2::new(a.0, a.1), UVec2::new(w.0, w.1), true)
};
assert!(bucketed((224, 160), (213, 150)));
assert!(bucketed((224, 160), (224, 160)));
assert!(!bucketed((224, 160), (225, 160)), "one px over the extent");
assert!(bucketed((256, 160), (193, 150)));
assert!(bucketed((256, 160), (192, 150)));
assert!(!bucketed((256, 160), (160, 150)), "bucket 160 + 64 < 256");
for w in 180..=220 {
assert!(bucketed((224, 192), (w, 170)), "width {w}");
}
assert!(bucketed((32, 32), (1, 1)));
assert!(!bucketed((128, 32), (1, 1)));
}
#[test]
fn alloc_for_is_a_fit() {
for (w, h) in [(1, 1), (31, 33), (200, 150), (1280, 832)] {
let wanted = UVec2::new(w, h);
for bucketable in [false, true] {
let alloc = alloc_for(wanted, bucketable);
assert!(alloc.cmpge(wanted).all());
assert!(
alloc_fits(alloc, wanted, bucketable),
"{wanted} {bucketable}"
);
}
assert_eq!(alloc_for(wanted, false), wanted);
assert_eq!(alloc_for(wanted, true), bucket_size(wanted));
}
}
}