pub mod pyramid;
#[cfg(test)]
mod tests;
use std::collections::{HashMap, HashSet};
use bevy::asset::{AssetEvent, AssetId};
use bevy::ecs::system::ParamSet;
use bevy::image::{
ImageFilterMode, ImageSampler, ImageSamplerDescriptor, TRANSPARENT_IMAGE_HANDLE,
};
use bevy::prelude::*;
use bevy::render::render_resource::TextureUsages;
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future::block_on, poll_once};
use bevy::ui::widget::ImageNode;
use crate::bridge::ReactNode;
use crate::ext::LiveTexture;
use crate::layer::LayerContentDirt;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum ImageRendering {
#[default]
Auto,
Bilinear,
Trilinear,
Nearest,
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
pub struct ImageRenderingMode(pub ImageRendering);
pub const WARN_KIND: &str = "imageRendering";
type VariantKey = (AssetId<Image>, ImageRendering);
struct Variant {
handle: Handle<Image>,
source: Handle<Image>,
users: usize,
ready: bool,
pyramid: Option<Task<Image>>,
rebuild: bool,
}
struct Bound {
source: Handle<Image>,
mode: ImageRendering,
key: Option<VariantKey>,
}
#[derive(Resource)]
pub struct ImageVariants {
pub default_sampler: ImageSamplerDescriptor,
variants: HashMap<VariantKey, Variant>,
by_variant: HashMap<AssetId<Image>, VariantKey>,
bound: HashMap<Entity, Bound>,
pending: HashSet<Entity>,
}
impl Default for ImageVariants {
fn default() -> Self {
Self {
default_sampler: ImageSamplerDescriptor::linear(),
variants: HashMap::new(),
by_variant: HashMap::new(),
bound: HashMap::new(),
pending: HashSet::new(),
}
}
}
impl ImageVariants {
pub fn variant_count(&self) -> usize {
self.variants.len()
}
fn release(&mut self, entity: Entity) -> Option<(Bound, Option<AssetId<Image>>)> {
self.pending.remove(&entity);
let bound = self.bound.remove(&entity)?;
let mut variant_id = None;
if let Some(key) = bound.key
&& let Some(v) = self.variants.get_mut(&key)
{
variant_id = Some(v.handle.id());
v.users = v.users.saturating_sub(1);
if v.users == 0 {
let v = self.variants.remove(&key).expect("present");
self.by_variant.remove(&v.handle.id());
}
}
Some((bound, variant_id))
}
pub(crate) fn from_app(app: &App) -> Self {
let default_sampler = app
.get_added_plugins::<bevy::image::ImagePlugin>()
.first()
.map(|p| p.default_sampler.clone())
.unwrap_or_else(ImageSamplerDescriptor::linear);
Self {
default_sampler,
..Default::default()
}
}
}
#[cfg(test)]
pub(crate) fn register(app: &mut App) {
app.init_resource::<ImageVariants>();
app.init_resource::<LayerContentDirt>();
app.add_systems(Update, bind_image_rendering);
}
fn sampler_for(mode: ImageRendering) -> ImageSampler {
match mode {
ImageRendering::Auto => ImageSampler::Default,
ImageRendering::Bilinear => ImageSampler::Descriptor(ImageSamplerDescriptor {
lod_max_clamp: 0.0,
..ImageSamplerDescriptor::linear()
}),
ImageRendering::Trilinear => ImageSampler::Descriptor(ImageSamplerDescriptor::linear()),
ImageRendering::Nearest => ImageSampler::Descriptor(ImageSamplerDescriptor {
lod_max_clamp: 0.0,
..ImageSamplerDescriptor::nearest()
}),
}
}
fn satisfies(image: &Image, mode: ImageRendering, default: &ImageSamplerDescriptor) -> bool {
use ImageFilterMode::{Linear, Nearest};
let levels = image.texture_descriptor.mip_level_count;
let d = match &image.sampler {
ImageSampler::Default => default,
ImageSampler::Descriptor(d) => d,
};
let (mag, min, mip, lod_max) = (d.mag_filter, d.min_filter, d.mipmap_filter, d.lod_max_clamp);
match mode {
ImageRendering::Auto => true,
ImageRendering::Bilinear => {
mag == Linear && min == Linear && (levels == 1 || lod_max == 0.0)
}
ImageRendering::Trilinear => {
levels > 1
&& mag == Linear
&& min == Linear
&& mip == Linear
&& lod_max >= (levels - 1) as f32
}
ImageRendering::Nearest => {
mag == Nearest && min == Nearest && (levels == 1 || lod_max == 0.0)
}
}
}
fn mode_name(mode: ImageRendering) -> &'static str {
match mode {
ImageRendering::Auto => "auto",
ImageRendering::Bilinear => "bilinear",
ImageRendering::Trilinear => "trilinear",
ImageRendering::Nearest => "nearest",
}
}
fn warn(node: Option<&ReactNode>, mode: ImageRendering, message: &str) {
let _scope = node.map(|n| crate::diag::node_scope(n.0));
crate::diag::report(WARN_KIND, mode_name(mode), message);
}
fn spawn_pyramid(source: &Image) -> Task<Image> {
let mut work = source.clone();
work.sampler = sampler_for(ImageRendering::Trilinear);
AsyncComputeTaskPool::get().spawn(async move {
let _ = pyramid::build_pyramid(&mut work);
work
})
}
type LiveMarkers = Has<LiveTexture>;
fn plan(
source: &Image,
mode: ImageRendering,
live_element: bool,
default: &ImageSamplerDescriptor,
) -> Result<Option<ImageRendering>, String> {
let live = live_element
|| source
.texture_descriptor
.usage
.contains(TextureUsages::RENDER_ATTACHMENT);
if live {
return Err(format!(
"imageRendering \"{}\" is ignored on a live texture (render target, canvas, svg, \
`{{ texture }}`): it can't be copied and is sampled as-is",
mode_name(mode)
));
}
if satisfies(source, mode, default) {
return Ok(None);
}
if source.data.is_none() {
return Err(format!(
"imageRendering \"{}\": the image has no CPU-side pixel data to derive from \
(RENDER_WORLD-only asset usage); sampled as-is",
mode_name(mode)
));
}
if mode == ImageRendering::Trilinear
&& let Err(err) = pyramid::check(source)
{
return Err(format!(
"imageRendering \"trilinear\": can't build a mip pyramid — {err}; sampled as-is"
));
}
Ok(Some(mode))
}
type ChangedQuery<'w, 's> = Query<
'w,
's,
Entity,
(
With<ImageRenderingMode>,
Or<(Changed<ImageRenderingMode>, Changed<ImageNode>)>,
),
>;
type NodeQuery<'w, 's> = Query<
'w,
's,
(
Option<&'static ImageRenderingMode>,
&'static mut ImageNode,
Option<&'static ReactNode>,
LiveMarkers,
),
>;
pub fn bind_image_rendering(
mut variants: ResMut<ImageVariants>,
mut images: ResMut<Assets<Image>>,
mut dirt: ResMut<LayerContentDirt>,
mut events: MessageReader<AssetEvent<Image>>,
mut removed_modes: RemovedComponents<ImageRenderingMode>,
mut removed_images: RemovedComponents<ImageNode>,
mut queries: ParamSet<(ChangedQuery, NodeQuery)>,
) {
let mut reloaded: Vec<AssetId<Image>> = events
.read()
.filter_map(|e| match e {
AssetEvent::Modified { id } => Some(*id),
_ => None,
})
.filter(|id| variants.variants.values().any(|v| v.source.id() == *id))
.collect();
reloaded.sort_unstable();
reloaded.dedup();
let released: Vec<Entity> = removed_modes.read().chain(removed_images.read()).collect();
for entity in released {
let Some((bound, variant_id)) = variants.release(entity) else {
continue;
};
if let Some(variant_id) = variant_id
&& let Ok((None, mut node, ..)) = queries.p1().get_mut(entity)
&& node.image.id() == variant_id
{
node.image = bound.source.clone();
dirt.nodes.push(entity);
}
}
let mut work: Vec<Entity> = queries.p0().iter().collect();
work.extend(variants.pending.iter().copied());
work.sort_unstable();
work.dedup();
let mut nodes = queries.p1();
for entity in work {
let Ok((Some(mode), mut node, react, live)) = nodes.get_mut(entity) else {
variants.pending.remove(&entity);
continue;
};
let live_element = live;
bind(
entity,
mode.0,
&mut node,
react,
live_element,
&mut variants,
&mut images,
&mut dirt,
);
}
for id in reloaded {
let Some(source) = images.get(id).cloned() else {
continue;
};
let keys: Vec<VariantKey> = variants
.variants
.iter()
.filter(|(_, v)| v.source.id() == id)
.map(|(k, _)| *k)
.collect();
for key in keys {
let v = variants.variants.get_mut(&key).expect("listed");
if key.1 == ImageRendering::Trilinear {
if v.pyramid.is_some() {
v.rebuild = true;
} else {
v.pyramid = Some(spawn_pyramid(&source));
}
} else {
let mut image = source.clone();
image.sampler = sampler_for(key.1);
images
.insert(v.handle.id(), image)
.expect("the variant handle is held strong");
}
}
}
let mut landed: Vec<VariantKey> = Vec::new();
for (key, v) in variants.variants.iter_mut() {
let Some(task) = v.pyramid.as_mut() else {
continue;
};
let Some(image) = block_on(poll_once(task)) else {
continue;
};
images
.insert(v.handle.id(), image)
.expect("the variant handle is held strong");
v.pyramid = None;
if !v.ready {
v.ready = true;
landed.push(*key);
}
if std::mem::take(&mut v.rebuild)
&& let Some(source) = images.get(v.source.id())
{
v.pyramid = Some(spawn_pyramid(source));
}
}
for key in landed {
let handle = variants.variants[&key].handle.clone();
let users: Vec<Entity> = variants
.bound
.iter()
.filter(|(_, b)| b.key == Some(key))
.map(|(e, _)| *e)
.collect();
for entity in users {
if let Ok((_, mut node, ..)) = nodes.get_mut(entity)
&& node.image.id() != handle.id()
{
node.image = handle.clone();
dirt.nodes.push(entity);
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn bind(
entity: Entity,
mode: ImageRendering,
node: &mut Mut<ImageNode>,
react: Option<&ReactNode>,
live_element: bool,
variants: &mut ImageVariants,
images: &mut Assets<Image>,
dirt: &mut LayerContentDirt,
) {
let current = node.image.id();
if current == TRANSPARENT_IMAGE_HANDLE.id() {
variants.pending.remove(&entity);
return;
}
let source: Handle<Image> = match variants.by_variant.get(¤t) {
Some(key) => variants.variants[key].source.clone(),
None => node.image.clone(),
};
if let Some(b) = variants.bound.get(&entity)
&& b.source.id() == source.id()
&& b.mode == mode
{
let expected = match b.key.and_then(|k| variants.variants.get(&k)) {
Some(v) if v.ready => v.handle.clone(),
_ => source.clone(),
};
variants.pending.remove(&entity);
if node.image.id() != expected.id() {
node.image = expected;
dirt.nodes.push(entity);
}
return;
}
variants.release(entity);
let Some(src_img) = images.get(source.id()) else {
variants.pending.insert(entity);
return;
};
let key = match plan(src_img, mode, live_element, &variants.default_sampler) {
Ok(Some(mode)) => Some((source.id(), mode)),
Ok(None) => None,
Err(message) => {
warn(react, mode, &message);
None
}
};
let handle = match key {
None => source.clone(),
Some(key) => {
if !variants.variants.contains_key(&key) {
let (handle, ready, pyramid) = if mode == ImageRendering::Trilinear {
(images.reserve_handle(), false, Some(spawn_pyramid(src_img)))
} else {
let mut image = src_img.clone();
image.sampler = sampler_for(mode);
(images.add(image), true, None)
};
variants.by_variant.insert(handle.id(), key);
variants.variants.insert(
key,
Variant {
handle,
source: source.clone(),
users: 0,
ready,
pyramid,
rebuild: false,
},
);
}
let v = variants.variants.get_mut(&key).expect("just ensured");
v.users += 1;
if v.ready {
v.handle.clone()
} else {
source.clone()
}
}
};
variants.bound.insert(entity, Bound { source, mode, key });
if node.image.id() != handle.id() {
node.image = handle;
dirt.nodes.push(entity);
}
}